Develop a driver - (Venstar Thermostat Driver Development)

New version. More bug fixes.

.
UPDATE: removed old version.

I've seen that you added the line
attribute "thermostatFanOperatingState","enum",["off","on"]

and I need to see other lines too.

Maybe it's not important, but on that line there is no auto and the words are in tiny characters, so in the rule triggers. But the device shows On or Auto (capitals).
I'm more confused now :frowning:

I changed this on lines 228+

def getFanModeMap() {[
0:"auto",
1:"on"
]}

seems working with rules.

This attribute is a custom attribute.... As the default fan capabilities does not have an attribute for fan operating STATE. As far as there not being an "auto"... That is because a fan can physically be "on" or "off"... "auto" is a fan operating MODE. The "on" and "off" are in lowercase because that is the convention used in all other capabilities that use "on" and "off".

Wasn't that already changed?... In the latest version...

nope. as you can see in v0.12 they are Auto and On.

I'll test more tomorrow but it seems working with my rule

Right... Missed the uppercase... fixed... Also fixed the modes used by the dashboards (should not affect anything except removing referenced to fanOn and fanAuto).

You rules are a little confusing... what are "Therm.fan.on" and "Therm.fan.auto"??... I assume they are virtual devices (holdovers from before the driver worked properly??)... but I'm not sure...

New version

/**
 *  Venstar Hubitat Driver
 *
 *  Copyright 2014 Scottin Pollock
 *  Modifications copyright (C) 2019 David Tarin
 *  Fixes, refactoring & functionality copyright (C) 2019 CybrMage
 *
 *  Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
 *  in compliance with the License. You may obtain a copy of the License at:
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
 *  on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
 *  for the specific language governing permissions and limitations under the License.
 *
 */

import groovy.json.JsonSlurper

metadata {
	definition (name: "Venstar Thermostat", namespace: "Thermostat", author: "CybrMage, Scottin Pollock, David Tarin") {
		capability "Actuator"
		capability "Initialize"
		capability "Polling"
		capability "Refresh"
		capability "Sensor"
		capability "Thermostat"
		capability "RelativeHumidityMeasurement"
		capability "PresenceSensor"
		
		attribute "thermostatFanOperatingState","enum",["off","on"]
	}

	preferences {
		section("Driver config") {
			input "thermostatIP", "text", title: "Thermostat IP", required: false
			def pollRate = ["1" : "Poll every minute", "5" : "Poll every 5 minutes", "10" : "Poll every 10 minutes", "15" : "Poll every 15 minutes", "30" : "Poll every 30 minutes", "60" : "Poll every hour", "180" : "Poll every 3 hours"]
			input ("Poll_Rate", "enum", title: "Device Poll Rate", options: pollRate, defaultValue: "10")
			input name: "sensorEnable", type: "bool", title: "Enable sensor child devices", defaultValue: false
			input name: "logEnable", type: "bool", title: "Enable debug logging", defaultValue: true
			input name: "testMode", type: "bool", title: "Enable offline test mode", defaultValue: false
		}
	}
}

private getVERSION() { "v0.13" }

// parse events into attributes
def parseJsonData(result) {
	if (logEnable) log.debug("parseJsonData: data is: $result")
	if (result.success != null){
		//Do nothing as nothing can be done. (runIn doesn't appear to work here and apparently you can't make outbound calls here)
		if (logEnable) log.debug "parseJsonData: getapi()/postapi() data indicates success"
		return
	}
	if (result.error != null){
		if (logEnable) log.debug "parseJsonData: getapi()/postapi() data indicates error: ${result.reason}"
		return
	}
	if (result.mode != null){
		if (logEnable) log.debug "parseJsonData: parsing result.mode"
		def mode = getModeMap()[result.mode]
		state.thermostatMode = result.mode
		if(device.currentState("thermostatMode")?.value != mode){
			sendEvent(name: "thermostatMode", value: mode, descriptionText: "thermostatMode set to ${mode}", isStateChange: true)
		}
	}
	if (result.state != null){
		if (logEnable) log.debug "parseJsonData: parsing result.state"
		def mode = getOperatingModeMap()[result.state]
		state.thermostatOperatingState = result.state
		if(device.currentState("thermostatOperatingState")?.value != mode){
			sendEvent(name: "thermostatOperatingState", value: mode, descriptionText: "thermostatOperatingState set to ${mode}", isStateChange: true)
		}
	}
	if (result.fan != null){
		if (logEnable) log.debug "parseJsonData: parsing result.fan"
		def fan = getFanModeMap()[result.fan]
		state.thermostatFanMode = result.fan
		if (device.currentState("thermostatFanMode")?.value != fan){
			sendEvent(name: "thermostatFanMode", value: fan, descriptionText: "thermostatFanMode set to ${fan}", isStateChange: true)
		}
	}
	if (result.fanstate != null){
		if (logEnable) log.debug "parseJsonData: parsing result.fanstate"
		def mode = (result.fanstate == 0)?"off":"on"
		state.thermostatFanOperatingState = result.fanstate
		if(device.currentState("thermostatFanOperatingState")?.value != mode){
			sendEvent(name: "thermostatFanOperatingState", value: mode, descriptionText: "thermostatFanOperatingState set to ${mode}", isStateChange: true)
		}
	}
	if (result.tempunits != null){
		if (logEnable) log.debug "parseJsonData: parsing result.tempunits"
		def temperatureScale = ((result.tempunits as Integer) == 0) ? "F" : "C"
		state.temperatureScale = temperatureScale
		if (device.currentState("temperatureScale")?.value != temperatureScale.toString()){
			sendEvent(name: "temperatureScale", value: temperatureScale, descriptionText: "temperatureScale set to ${temperatureScale}", isStateChange: true)
		}
	} else {
		if (result.spacetemp != null){
			def temp = result.spacetemp
			if (temp < 32) {
				state.temperatureScale = "C"
			} else {
				state.temperatureScale = "F"
			}
		} else {
			state.temperatureScale = "F"
		}
		sendEvent(name: "temperatureScale", value: temperatureScale, descriptionText: "temperatureScale set to ${temperatureScale}", isStateChange: true)
	}
	if (result.cooltempmin != null){
		if (logEnable) log.debug "parseJsonData: parsing result.cooltempmin"
		state.cooltempmin = result.cooltempmin
	}
	if (result.cooltempmax != null){
		if (logEnable) log.debug "parseJsonData: parsing result.cooltempmax"
		state.cooltempmax = result.cooltempmax
	}
	if (result.heattempmin != null){
		if (logEnable) log.debug "parseJsonData: parsing result.heattempmin"
		state.heattempmin = result.heattempmin
	}
	if (result.heattempmax != null){
		if (logEnable) log.debug "parseJsonData: parsing result.heattempmax"
		state.heattempmax = result.heattempmax
	}
	if (result.cooltemp != null){
		if (logEnable) log.debug "parseJsonData: parsing result.cooltemp"
		def cooltemp = (getTemperatureHE(result.cooltemp) as Float).round(1)
		state.cooltemp = result.cooltemp
		if (device.currentState("coolingSetpoint")?.value != cooltemp.toString()){
			sendEvent(name: "coolingSetpoint", value: cooltemp, descriptionText: "coolingSetpoint set to ${cooltemp}", isStateChange: true)
		}
	}
	if (result.heattemp != null){
		if (logEnable) log.debug "parseJsonData: parsing result.heattemp"
		def heattemp = (getTemperatureHE(result.heattemp) as Float).round(1)
		state.heattemp = result.heattemp
		if (device.currentState("heatingSetpoint")?.value != heattemp.toString()){
			sendEvent(name: "heatingSetpoint", value: heattemp, , descriptionText: "Heating setpoint set to ${heattemp}", isStateChange: true)
		}
	}
	if (result.spacetemp != null){
		if (logEnable) log.debug "parseJsonData: parsing result.spacetemp"
		def temp = (getTemperatureHE(result.spacetemp) as Float).round(1)
		state.temperature = result.spacetemp
		if (device.currentState("temperature")?.value != spacetemp.toString()){
			sendEvent(name: "temperature", value: temp, descriptionText: "temperature set to ${temp}", isStateChange: true)
		}
	}
	if (result.state != null){
		if ( state.thermostatOperatingState == 2) {
			state.thermostatSetpoint = state.cooltemp
		} else {
			state.thermostatSetpoint = state.heattemp
		}
	    sendEvent(name: "thermostatSetpoint", value: (getTemperatureHE(state.thermostatSetpoint) as Float).round(1), descriptionText: "thermostatSetpoint set to ${(getTemperatureHE(state.thermostatSetpoint) as Float).round(1)}", isStateChange: true)
	}
	if (result.away != null){
		if (logEnable) log.debug "parseJsonData: parsing result.away"
		def mode = getAwayModes()[result.away]
		state.presence = result.away
		if(device.currentState("presence")?.value != mode){
			sendEvent(name: "presence", value: mode, descriptionText: "presence set to ${mode}", isStateChange: true)
		}
	}
	if (result.hum != null){
		if (logEnable) log.debug "parseJsonData: parsing result.hum"
		state.humidity = result.hum
		if(device.currentState("humidity")?.value != mode){
			sendEvent(name: "humidity", value: state.humidity, descriptionText: "humidity set to ${mode}", isStateChange: true)
		}
	}
	if (result.sensors != null) {
		if (logEnable) log.debug "parseJsonData: parsing SENSOR data"
		result.sensors.each { sensor ->
			def sensorDNI = device.deviceNetworkId + "-ep" + sensor.name.replaceAll("\\s","").replaceAll("\\W","")
			def targetChild = getChildDevice(sensorDNI)
			if (targetChild != null) {
				if (logEnable) log.debug "parseJsonData: updating SENSOR data for "+sensorDNI
				if (sensor.temp != null) targetChild.setTemperature(sensor.temp)
				if (sensor.hum != null) targetChild.setHumidity(sensor.hum)
			} else {
				if (logEnable) log.warn "parseJsonData: child SENSOR does not exist for "+sensorDNI
			}
		}
	}
}

def poll() {
  if (logEnable) log.debug("poll: Executing 'poll'")
  sendEvent(descriptionText: "poll keep alive", isStateChange: false)  // workaround to keep polling from being shut off
  refresh()
}

def modes() {
  ["off", "heat", "cool", "auto"]
}

def parseDescriptionAsMap(description) {
  description.split(",").inject([:]) { map, param ->
    def nameAndValue = param.split(":")
    map += [(nameAndValue[0].trim()):nameAndValue[1].trim()]
  }
}

def getAwayModes() { [
  0:"present",
  1:"not present"
]}

def getModeMap() { [
  0:"off",
  2:"cool",
  1:"heat",
  3:"auto"
]}

def getOperatingModeMap() { [
	0: "idle",
	1: "heating",
	2: "cooling",
	3: "lockout",
	4: "error"
]}

def getFanModeMap() {[
	0:"auto",
	1:"on"
]}

def clampTemp(temp) {
	def T1 = Math.floor(temp)
	def T2 = temp - T1
	def T3 = 0
	if ((T2 >= 0.25) && (T2 <= 0.75)) {
		T3 = 0.5
	} else if (T2 > 0.75) {
		T3 = 1.0
	}
	def T4 = T1 + T3
	if (logEnable) log.debug("  clampTemp: triming value ${temp} to ${T4}")
	return T4
}

def getTemperatureHE(value) {
	if (logEnable) log.debug("  getTemperatureHE: testing for temperature scale conversion for temperature value ${value}")
	def scaleHE = getTemperatureScale()
	def scaleVS = (device.currentState("temperatureScale")?.value)?(device.currentState("temperatureScale")?.value):state.temperatureScale
	if (logEnable) log.debug("  getTemperatureHE: temperatureScale - HE = ${scaleHE}  VS = ${scaleVS}")
	if ( !scaleHE || !scaleVS || (scaleHE == scaleVS)) {
		value = clampTemp(value)
		if (logEnable) log.debug("  getTemperatureHE: no temperature scale conversion required for temperature value ${value}")
		return value
	}
	if ((scaleVS == "F") && (scaleHE == "C")) {
		// F to C
		def tempC = (((value-32)*5.0)/9.0)
		tempC = clampTemp(tempC)
		if (logEnable) log.warn("  getTemperatureHE: temperature scale conversion required. temperature value ${value}F = ${tempC}C")
		return tempC
	} else {
		// C to F
		def tempF = (((value * 9.0)/5.0)+32)
		tempF = clampTemp(tempF)
		if (logEnable) log.warn("  getTemperatureHE: temperature scale conversion required. temperature value ${value}C = ${tempF}F")
		return tempF
	}
}

def getTemperatureVS(value) {
	if (logEnable) log.debug("  getTemperatureVS: testing for temperature scale conversion for temperature value ${value}")
	def scaleHE = getTemperatureScale()
	def scaleVS = (device.currentState("temperatureScale")?.value)?(device.currentState("temperatureScale")?.value):state.temperatureScale
	if (logEnable) log.debug("  getTemperatureVS: temperatureScale - VS = ${scaleVS}  HE = ${scaleHE}")
	if ( !scaleHE || !scaleVS || (scaleHE == scaleVS)) {
		value = clampTemp(value)
		if (logEnable) log.debug("  getTemperatureVS: no temperature scale conversion required for temperature value ${value}")
		return value
	}
	if ((scaleHE == "F") && (scaleVS == "C")) {
		// F to C
		def tempC = (((value-32)*5.0)/9.0)
		tempC = clampTemp(tempC)
		if (logEnable) log.warn("  getTemperatureVS: temperature scale conversion required. temperature value ${value}F = ${tempC}C")
		return tempC
	} else {
		// C to F
		def tempF = (((value * 9.0)/5.0)+32)
		tempF = clampTemp(tempF)
		if (logEnable) log.warn("  getTemperatureVS: temperature scale conversion required. temperature value ${value}C = ${tempF}F")
		return tempF
	}
}

// handle commands
def getValidHeatSetpoint(newTemp) {
	if (logEnable) log.debug("  getValidHeatSetpoint: testing heat setpoint for temperature value ${newTemp}")
	if (!state.heattempmin || !state.heattempmax) { 
		if (logEnable) log.debug("  getValidHeatSetpoint: no restriction for temperature value ${newTemp}")
		return newTemp 
	}
	def minTemp = state.heattempmin as Integer
	def maxTemp = state.heattempmax as Integer
	if (newTemp < minTemp) {
		if (logEnable) log.warn("  getValidHeatSetpoint: heat setpoint restricted to minimum temperature value ${minTemp}")
		return minTemp
	} else if (newTemp > maxTemp) {
		if (logEnable) log.warn("  getValidHeatSetpoint: heat setpoint restricted to maximum temperature value ${maxTemp}")
		return maxTemp
	} else {
		if (logEnable) log.debug("  getValidHeatSetpoint: no restriction for temperature value ${newTemp}")
		return newTemp
	}
}

def getValidCoolSetpoint(newTemp) {
	if (logEnable) log.debug("  getValidCoolSetpoint: testing cool setpoint for temperature value ${newTemp}")
	if (!state.cooltempmin || !state.cooltempmax) { 
		if (logEnable) log.debug("getValidCoolSetpoint: no restriction for temperature value ${newTemp}")
		return newTemp 
	}
	def minTemp = state.cooltempmin as Integer
	def maxTemp = state.cooltempmax as Integer
	if (newTemp < minTemp) {
		if (logEnable) log.warn("  getValidCoolSetpoint: cool setpoint restricted to minimum temperature value ${minTemp}")
		return minTemp
	} else if (newTemp > maxTemp) {
		if (logEnable) log.warn("  getValidCoolSetpoint: cool setpoint restricted to maximum temperature value ${maxTemp}")
		return maxTemp
	} else {
		if (logEnable) log.debug("  getValidCoolSetpoint: no restriction for temperature value ${newTemp}")
		return newTemp
	}
}

def setCommonSetpoint() {
	if ( state.thermostatOperatingState == 2) {
		state.thermostatSetpoint = state.cooltemp
	} else {
		state.thermostatSetpoint = state.heattemp
	}
	def newTemp = clampTemp(getTemperatureHE(state.thermostatSetpoint))
	if (logEnable) log.debug "  setCommonSetpoint: Executing 'setCommonSetpoint' with ${newTemp} (originally - ${state.thermostatSetpoint})"
    sendEvent(name: "thermostatSetpoint", value: newTemp, descriptionText: "thermostatSetpoint set to ${newTemp}", isStateChange: true)
}

def setHeatingSetpoint(degrees) {
	if (logEnable) log.debug "Executing 'setHeatingSetpoint' with ${degrees}"
	def degreesHE = clampTemp(degrees)
	def degreesVS = getTemperatureVS(degreesHE)
	state.heattemp = getValidHeatSetpoint(degreesVS)
	if (state.heattemp != degreesVS) {
		// temperature was restricted - convert restricted temperature back to HE format
		degreesHE = getTemperatureHE(degreesVS)
	}
	if (logEnable) log.debug "processing 'setHeatingSetpoint' with ${degreesHE} (originally ${degrees})"
	setCommonSetpoint()
	sendEvent(name: "heatingSetpoint", value: degreesHE, descriptionText: "Heating setpoint set to ${degreesHE}", isStateChange: true)
	postapi()
}

def setCoolingSetpoint(degrees) {
	if (logEnable) log.debug "Executing 'setCoolingSetpoint' with ${degrees}"
	def degreesHE = clampTemp(degrees)
	def degreesVS = getTemperatureVS(degreesHE)
	state.cooltemp = getValidCoolSetpoint(degreesVS)
	if (state.cooltemp != degreesVS) {
		// temperature was restricted - convert restricted temperature back to HE format
		degreesHE = getTemperatureHE(degreesVS)
	}
	if (logEnable) log.debug "processing 'setCoolingSetpoint' with ${degreesHE} (originally ${degrees})"
	sendEvent(name: "coolingSetpoint", value: degreesHE, descriptionText: "Cooling setpoint set to ${degreesHE}", isStateChange: true)
	setCommonSetpoint()
	postapi()
}

def off() {
	if (logEnable) log.debug "Executing 'off'"
	state.thermostatMode = 0
	sendEvent(name: "thermostatMode", value: getModeMap()[0], descriptionText: "Thermostat Mode set to ${getModeMap()[0]}", isStateChange: true)
	postapi()
}

def heat() {
	if (logEnable) log.debug "Executing 'heat'"
	state.thermostatMode = 1
	sendEvent(name: "thermostatMode", value: getModeMap()[1], descriptionText: "Thermostat Mode set to ${getModeMap()[1]}", isStateChange: true)
	postapi()
}

def cool() {
	if (logEnable) log.debug "Executing 'cool'"
	state.thermostatMode = 2
	sendEvent(name: "thermostatMode", value: getModeMap()[2], descriptionText: "Thermostat Mode set to ${getModeMap()[2]}", isStateChange: true)
	postapi()
}

def auto() {
	if (logEnable) log.debug "Executing 'auto'"
	state.thermostatMode = 3
	sendEvent(name: "thermostatMode", value: getModeMap()[3], descriptionText: "Thermostat Mode set to ${getModeMap()[3]}", isStateChange: true)
	postapi()
}

def emergencyHeat() {
	if (logEnable) log.error "'emergencyHeat' is not supported by this device"
}

def setSchedule(String json) {
	if (logEnable) log.error "'setSchedule' is not supported by this device driver"
}

def setThermostatMode(String newMode) {
	if (logEnable) log.debug("setThermostatMode(${newMode})")
	switch(newMode) {
		case "off":
			off()
			break
		case "heat":
			heat()
			break
		case "cool":
			cool()
			break
		case "auto":
			auto()
			break
		default:
			if (logEnable) log.error("setThermostatMode: mode '${newMode}' is not supported by this device")
			break
	}
}

def fanOn() {
	if (logEnable) log.debug "Executing 'fanOn'"
	state.thermostatFanMode = 1
	sendEvent(name: "thermostatFanMode", value: getFanModeMap()[1], descriptionText: "Thermostat Fan Mode set to ${getFanModeMap()[1]}", isStateChange: true)
	postapi()
}

def fanAuto() {
	if (logEnable) log.debug "Executing 'fanAuto'"
	state.thermostatFanMode = 0
	sendEvent(name: "thermostatFanMode", value: getFanModeMap()[0], descriptionText: "Thermostat Fan Mode set to ${getFanModeMap()[0]}", isStateChange: true)
	postapi()
}

def fanCirculate() {
	if (logEnable) log.error("'fanCirculate' is not supported by this device")
}

def setThermostatFanMode(String newMode) {
	if (logEnable) log.debug("setThermostatFanMode(${newMode.trim()})")
	switch(newMode.trim()) {
		case "on":
		case "fanOn":
			fanOn()
			break
		case "auto":
		case "fanAuto":
			fanAuto()
			break
		default:
			if (logEnable) log.error("setThermostatFanMode: mode '${newMode}' is not supported by this device")
			break
	}
}

def refresh() {
	if (logEnable) log.debug "Executing 'refresh'"
	getapi()
	if (sensorEnable) {
		if (logEnable) log.warn "Executing 'SENSOR refresh'"
		getSensors()
	}
}

private getapi() {
	def uri = "http://"+getHostAddress()+"/query/info"
	if (logEnable) log.debug("getapi: Executing get api to " + uri)
	if (testMode) uri = "http://127.0.0.1/api/json/utc/now"

	try {
		def requestParams =
		[
			uri:  uri,
			requestContentType: "application/x-www-form-urlencoded",
			contentType: "application/json"
		]
		httpGet(requestParams) { resp ->
			if (resp.status == 200) {
				if (logEnable) log.debug "getapi: httpGet returned: \n${resp.data}"
				parseJsonData(resp.data)
			} else {
				log.error("getapi: httpPost returned http failure code ${resp.status}")
			}
			if (logEnable) log.debug "getapi: getapi() completed"
		}
	} catch (Exception e) {
		if (testMode && (e.message == "Connect to 127.0.0.1:80 [/127.0.0.1] failed: Connection refused (Connection refused)")) {
			return parseJsonData(testDATA())
		}
		log.error "getapi: Call to getapi() failed: ${e.message}"
	}
}

private createModeChangeCommand(){
	def command = "mode=" + state.thermostatMode +
		"&fan=" + state.thermostatFanMode +
		"&heattemp=" + state.heattemp + 
		"&cooltemp=" + state.cooltemp
	return command
}

private postapi() {
	if (logEnable) log.info("postapi: Executing API Control update")
	def uri = "http://"+getHostAddress()+"/control"
	def command = createModeChangeCommand()

	if (logEnable) {
		if (testMode) {
			log.info("postapi: Test Mode: POST request to [${uri}] with command [${command}]")
		} else {
			log.info("postapi: Sending on POST request to [${uri}] with command [${command}]")
		}
	}
	if (testMode) {
		return getapi()
	}

	def postParams = [
		uri: uri,
		contentType: "application/json",
		requestContentType: "application/x-www-form-urlencoded",
		body : command
	]

	try {
		httpPost(postParams) { resp ->
			if (resp.status == 200) {
				if (logEnable) log.debug "postapi: httpPost returned: \n${resp.data}"
				if (resp.data.success == true) {
					if(logEnable) log.debug("postapi: httpGet returned success")
					// update the thermostat state
					getapi()
				} else{
					log.error("postapi: httpGet returned failed")
				}
			} else {
				log.error("postapi: httpPost returned http failure code ${resp.status}")
			}
		}
	} catch (Exception e) {
		log.error "postapi: Call to postapi(${command}) failed: ${e.message}"
	}
}

//helper methods
private getHostAddress() {
  return settings.thermostatIP
}

// driver default methods
def updated() {
  log.debug("updated: Venstar Driver ${VERSION} - Updating parameters")
  initialize()
}

def installed() {
  log.debug("installed: Venstar Driver ${VERSION} - Installed")
}

def initialize() {
	log.debug("initialize: Venstar Driver ${VERSION} - Initializing device ${device.getDeviceNetworkId()}")
	unschedule()
	sendEvent(name: "temperatureDisplayScale", value: getTemperatureScale(), descriptionText: "temperatureDisplayScale set to ${getTemperatureScale()}", isStateChange: true)
	sendEvent(name: "supportedThermostatFanModes", value: ["auto","on"], isStateChange: true)
	sendEvent(name: "supportedThermostatModes", value: ["off","heat","cool","auto"], isStateChange: true)
  
	if (getHostAddress()) {
		// get the initial thermostat state
		if (sensorEnable) {
			def sensorData = getSensors(true)
			createSensors(sensorData)
		} else {
			// remove child sensors if needed
			getChildDevices().each { sensor ->
				if (logEnable) log.debug ("removing disabled sensor ${sensor.deviceNetworkId}")
				deleteChildDevice(sensor.deviceNetworkId)
			}
		}
		getapi()
	} else {
		log.debug("initialize: Venstar IP Address not set")
		return
	}
	def poll = Poll_Rate ? Poll_Rate : "10"
	switch(poll) {
		case "1" :
			runEvery1Minute(refresh)
			log.debug("initialize: Poll Rate set to every 1 minute")
			break
		case "5" :
			runEvery5Minutes(refresh)
			log.debug("initialize: Poll Rate set to every 5 minutes")
			break
		case "10" :
			runEvery10Minutes(refresh)
			log.debug("initialize: Poll Rate set to every 10 minutes")
			break
		case "15" :
			runEvery15Minutes(refresh)
			log.debug("initialize: Poll Rate set to every 15 minutes")
			break
		case "30" :
			runEvery30Minutes(refresh)
			log.debug("initialize: Poll Rate set to every 30 minutes")
			break
		case "60" :
			runEvery1hour(refresh)
			log.debug("initialize: Poll Rate set to every 1 hour")
			break
		case "30" :
			runEvery3hours(refresh)
			log.debug("initialize: Poll Rate set to every 3 hours")
			break
	}
}

private getSensors(dataOnly = false) {
	def uri = "http://"+getHostAddress()+"/query/sensors"
	log.debug("getapi: Executing get api to " + uri)
	if (testMode) uri = "http://127.0.0.1/query/sensors"

	try {
		def requestParams =
		[
			uri:  uri,
			requestContentType: "application/x-www-form-urlencoded",
			contentType: "application/json"
		]
		httpGet(requestParams) { resp ->
			if (resp.status == 200) {
				if (logEnable) log.debug "getSensors: httpGet returned: \n${resp.data}"
				parseJsonData(resp.data)
			} else {
				log.error("getSensors: httpGet returned http failure code ${resp.status}")
			}
			if (logEnable) log.debug "getSensors: getSensors() completed"
		}
	} catch (Exception e) {
		if (testMode && (e.message == "Connect to 127.0.0.1:80 [/127.0.0.1] failed: Connection refused (Connection refused)")) {
			if (dataOnly) return testSensorDATA()
			return parseJsonData(testSensorDATA())
		}
		log.error "getSensors: Call to getSensors() failed: ${e.message}"
	}
}

def findChild(childDNI) {
	getChildDevices()?.find { it.deviceNetworkId == childDNI}
}

def createSensors(sensorData) {
	if (logEnable) log.debug("createSensors: Parsing sensor data: " + sensorData)
	def parentDNI = device.getDeviceNetworkId()
	if (sensorData.sensors) {
		sensorData.sensors.each { sensor ->
			def sensorName = sensor.name.replaceAll("\\s","").replaceAll("\\W","")
			def sensorDNI = "${device.deviceNetworkId}-ep${sensorName}"
			if (sensor.temp != null) {
				if (logEnable) log.debug("createSensors: Found temperature sensor device - " + sensor.name + "  DNI: "+sensorDNI)
				def childDevice = findChild(sensorDNI)
				if (childDevice == null) {
					if (logEnable) log.debug("createSensors: creating temperature Child "+sensorDNI)
					childDevice = addChildDevice("hubitat", "Virtual Temperature Sensor", sensorDNI, [label: "${device.displayName} (temp sensor - ${sensor.name})", isComponent: false])
				} else {
					if (logEnable) log.debug("createSensors: temperatureChild ${sensorDNI} already exists")
				}
				childDevice.setTemperature(sensor.temp)
			} else if (sensor.hum != null) {
				if (logEnable) log.debug("createSensors: Found humidity sensor device - " + sensor.name + "  DNI: "+sensorDNI)
				def childDevice = findChild(sensorDNI)
				if (childDevice == null) {
					if (logEnable) log.debug("createSensors: creating humidity Child "+sensorDNI)
					childDevice = addChildDevice("hubitat", "Virtual Humidity Sensor", "${device.deviceNetworkId}-ep${sensorName}", [label: "${device.displayName} (temp sensor - ${sensor.name})", isComponent: false])
				} else {
					if (logEnable) log.debug("createSensors: humidity Child ${sensorDNI} already exists")
				}
				childDevice.setHumidity(sensor.hum)
			} else {
				if (logEnable) log.debug("createSensors: Found unknown sensor device - " + sensor.name)
			}
		}
	}
}

def testDATA() { 
	def data = '{"name":"Test Data","mode":1,"state":0,"fan":0,"fanstate":1,"tempunits":1,"schedule":0,"schedulepart":255,"away":0,"spacetemp":21.0,"heattemp":21.0,"cooltemp":8.0,"cooltempmin":1.5,"cooltempmax":37.0,"heattempmin":1.5,"heattempmax":37.0,"setpointdelta":2.0,"availablemodes":1}'
	def slurper = new JsonSlurper()
	def result = slurper.parseText(data)
	result.mode = state.thermostatMode ?: 0
	result.state = state.thermostatMode ?: 0
	result.fan = state.thermostatFanMode ?: 0
	result.fanstate = (state.thermostatFanMode && (state.thermostatMode > 0)) ?: 0
	if (state.heattemp != null) result.heattemp = state.heattemp
	if (state.cooltemp!= null) result.cooltemp = state.cooltemp
	return result
}

def testAlertDATA() { 
	def data = '{"alerts": [{"name": "Air Filter","active": false},{"name": "UV Lamp","active": false},{"name": "Service","active": false}]}'
	def slurper = new JsonSlurper()
	def result = slurper.parseText(data)
	return result
}

def testSensorDATA() { 
	def data = '{"sensors": [{"name": "Thermostat","temp": 77},{"name": "Outdoor","temp": 0}]}'
	def slurper = new JsonSlurper()
	def result = slurper.parseText(data)
	return result
}
1 Like

About my rule:

the therm.fan.on /therm fan auto are some of the virtual switches I use in a dashboard to select the modes (heat,cool, stop) and the fan modes (on, auto, timer) instead of the built-in thermostat tile (pic displayed above).
And I can't just change the fan mode in the device itself because the new choice is not reflected in the dashboard (thermostat is ON but the tile still displays Auto).

the thermostat.Fan.Mode is a local variable to store the state of the fan before going on

the Shower.humidity.80 is a virtual switch to pause this rule for 30 minutes (after a 5 min run) because the humidity variation of my sensor is very slow.

I'll take my shower now to test the rule :slight_smile:
Thanks again for this update.

*Edit: it works !

I have a Water Furnace geothermal heat pump with four zones currently using 3 Water Furnace thermostats and one Honeywell that also controls an inline steam humidifier. The heat pump is two stage w/ electric backup so dual stage cooling and heating and electric emergency heat. The water furnace stats use 7 wires including one diagnostic wire for Water Furnace faults. I am unclear if that diagnostic wire is proprietary or could be used by the Venstar thermostat. I am guessing not.

I am thinking about getting Venstar thermostats since they have a wide range of model including some with Humidity control.

Seems dtdarin wrote an original driver then cybrmage updated it with testing by PPz. Is that correct?

I have some questions -

  1. Where is the latest driver located as cybrmage did not update dtdarin’s in GitHub and I cannot find his anywhere.
  2. What is the latest driver status?
  3. Is the driver fully functional or missing any feature implementation?
  4. Do you like the thermostats and have they been reliable?
  5. Can the Venstar stats still be used manually and with their mobile app alongside of Hubitat?
  6. Does Venstar offer good customer support and warranty?
  7. Is there any “installer” configuration at the thermostat itself that needs to be performed?

I am willing to help test and enhance the driver after I get more familiar with the thermostat and driver development, if any help is wanted.

Thx, Dave (still learning Hubitat and like it)

Hi Dave !
Answers:

  1. just look above here (#88). The latest version is 0.13

  2. Status ? Can you clarify ?

  3. for me it works as expected. I have a geothermal unit as well. Hooked with 5 wires (Power,Common, Fan, Compressor and Change-Over). My Geothermal unit has an error wiring but the Venstar doesn't take care of it. Look below for my very own setting. I struggled a little bit at the beginning (see point 6) to allow my thermostat to run cool and heat mode as expected but the Venstar has also dipswitches...
    I have an external temperature sensor hooked because the thermostat itself is placed inside a heated room (because of all my home automation stuff running there) and the built-in temperature sensor was displaying a constant false temperature. A regular external thermistor did the trick perfectly.

  4. yes at 95% and no at 5%. Sometimes my Venstar loses its connection to my wireless router. It still works in autonomous regular mode (because physically hooked to my geothermal unit) but I was sometimes fooled by a false state on my Hubitat Tablet. I solved this with 2 solutions:

  • I installed a pinger that displays the status of my thermostat (present or not): Web Pinger Plus from @bptworld
    Hubitat/Apps/Web Pinger at master · bptworld/Hubitat · GitHub

  • I added a NC push button (power) to reset my thermostat to allow it to reconnect to my wifi (because anything else has no positive results when the thermostat loses its wifi connection). It happens once every 3-4 months.

*Edit: I could update the NC button with a hubitat compatible dry relay to link the ping result and the reset of the thermostat (future update)

  1. Absolutely. Manual mode: no differences. And when you change a setting on it, soon after Hubitat shows the changes. App: absolutely. I have still my Android app installed and it works (but I don't use it). As you know, with a geothermal unit, you change the settings twice a year: heat mode in october, cool mode in may :smiley:

  2. Venstar ? Is it a company ? Hmmm, just kidding. Useless from the beginning (when I asked some help with my dipswitches, their answer was: "ask your installer") and useless for the app (they even didn't reply this time).

  3. You need to read the doc to be sure and avoid things that can harm your geothermal unit. Like bypass the pause between cool and heat. There is a built-in protection on my unit (and I suppose yours has it too), but reading both docs (geo unit and thermostat) will be always a nice idea.

  4. I have my T5800 since 3-4 years and had no problems with it (but I keep an old white-rodgers thermostat in case of any failure: Quebec here and having a backup thermostat on hand when it's -35C outside is always a good idea - WAF !)

Regards
Mike

1 Like

Hi Mike,

  1. Great. I was looking for a link to github. I copied and saved that driver and created a device so understand how to support it if and when I get a Venstar. I'll learn groovy and look at the code to understand it. Good the developer genealogy is in the comments.
  2. Was just wondering if the driver was considered finished or if any more changes were planned.
  3. Good to hear.
  4. I've got 3 wireless access points in the house so hopefully no issues.
  5. Glad manual coordinates with Hubitat!
  6. Do they at least give a manual for dip switch settings? Too bad they do not support home owners. Nowadays I don't even want any HVAC person in my house if I can at all help it.
  7. I'll have to review in detail. I already found a few setup issues in my Water Furnace that the original installer screwed up.
  8. I'll hang on to my old water furnace stats to be sure even though we don't get quite that cold here in Boone, NC! Hope you have some sort of gas aux heat.

Thanks so much for all the useful information! I'm hoping to hear back from Venstar soon on a proposition I made them for four of their stats. Also investigating Honeywell and Emerson stats but their WIFI versions have some API issues apparently. I'm waiting to hear back from Emerson as well but am not holding my breath...hopefully my HVAC guys will do the install and give them their blessing.

Dave

Hi Dave,

  1. considered finished because working as expected :+1:
  2. my thermostat is about 2m from my router, so not a certainty. Time to build a relay/pinger/rule to solve this the lazy way.
  3. yes, all their user/installer manuals are online. The pic I uploaded yesterday (dipswitches) is coming from my T5800 installer manual (in case of)
  4. Same here. That's the reason I'm a full DIYer since years. I replaced the mobo (flood in my basement), the fan motor (dead after its long life) and updated my unit with a lot of sensors to bypass the "alarm" (a lonely led blinking inside the machine) and to know more about geothermy (coil temperature, coax temp, loop dT, etc). It works now since 15 years like a charm !
  5. In fact no. the geo unit is ok for most of the time, including winter. In case of very cold temperature, I have an external electrical heater to give the extra joules needed.

That said, Zigbee/Zwave thermostats could be nice too. Because they don't rely to your wifi but to your zigbee/Zwave network. I choose to use my Venstar because I had it on hand, and that one nice guy here rebuilt the app for Hubitat. I also tested modbus thermostats (I'm into industrial plc's) but if I have to choose a brand new thermostat hubitat compatible, I'll search for Zigbee/Zwave units instead.

Mike

1 Like

Mike, thanks again for the information. I hope the later models have solid WIFI. Sounds like the one you got has some intermittent issues. I could not find a decent zigbee/z-wave thermostat that got good reviews on Amazon though I know some folks here have and like certain brands. Do you have any z-wave or zigbee units you would recommend? The Venstar appealed since it had local API control where the other WIFI stats did not. I need to choose carefully since I will need 4 stats total. Dave

asking the cavalry about good zigbee thermostat: @bertabcd1234, @SmartHomePrimer, @Cobra, @Ryan780, @zarthan, many others I hope they'll read this post, and obviously @mike.maxwell

Thank you for your advice to @dave7 (looks a little bit like THX1138, but who cares ?)
And please, don't reply him:

sorry dave

1 Like

:blush:

Why not buy from Canadians in your own province? :wink:

I have not personally used the thermostats, but @mike.maxwell had very good words about all their devices, they are inexpensive, and from what I personally experienced with their dimmer I tested, they are very high quality.

On the compatible devices list and have dedicated drivers.

Although I do have hydronic heating, their stats would not control my geothermal heat pump. I'm in the states, though my wife is Canadian...Dave

1 Like

I see. Sorry, didn't go back through the thread, or even look at who the OP was (...or the topic) :rofl:

Just saw the question from @PPz and thought he forgot about the people in his neighborhood. :stuck_out_tongue_winking_eye:

Seems every time someone asks about a local thermostat, I say "How about Sinope?" and they say "I have geothermal" :joy:

1 Like

Perhaps @aaiyar could comment on the Honeywell T6 Zwave Thermostat.

I've been using this and it's great, so thanks for publishing, but I think I found a couple of small bugs in the response handling:

  1. In the handling of result.tempunits, in the else branch the variable tempunits is not in scope for the call to sendEvent(), and I think the value should be state.tempunits for that call.

  2. In the handling of result.spacetemp, I believe the line that compares device.currentState().value to spacetemp.toString() should compare to temp.toString() instead -- there is no variable called spacetemp in that scope, there is result.spacetemp and the local temp.

  3. In the handling of result.hum, the comparison of device.currentState().value to mode should be a comparison to state.humidity.toString() or result.hum.toString(), as mode is not defined in this scope.

  4. The temperatureScale attribute wasn't being published by MakerAPI, so I added to the metadata.definition:

     attribute "temperatureScale","enum",["F","C"]

FWIW, @cybrmage is no longer active in this community and withdrew most of his integrations.