I thought my Stelpro STZB402+ line voltage thermostat was working under the generic ZigBee thermostat device, but I was wrong. It will report the temperature, but any changes to the heat setpoint do not make it to the thermostat. That is, with the thermostat in heating mode (of course), any value for Set Heating Setpoint won't change the state thermostat setpoint even after a dozen refreshes etc. However if I manually change the setpoint on the thermostat, it is properly reflected in the device. I excluded and included the device, and even used a DH from ST but it suffers from some porting issues related to writeattribute() that hopefully soon I can correct.
Use the ST driver, not sure if this is the same you used
/**
* Copyright 2017 Stelpro
*
* 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.
*
* Stelpro Ki ZigBee Thermostat
*
* Author: Stelpro
*
* Date: 2018-04-04
*/
preferences {
input("lock", "enum", title: "Do you want to lock your thermostat's physical keypad?", options: ["No", "Yes"], defaultValue: "No", required: false, displayDuringSetup: false)
input("heatdetails", "enum", title: "Do you want a detailed operating state notification?", options: ["No", "Yes"], defaultValue: "No", required: false, displayDuringSetup: true)
input("zipcode", "text", title: "ZipCode (Outdoor Temperature)", description: "[Do not use space](Blank = No Forecast)")
}
metadata {
definition (name: "Stelpro Ki ZigBee Thermostat", namespace: "Stelpro", author: "Stelpro") {
capability "Actuator"
capability "Temperature Measurement"
capability "Thermostat"
capability "Configuration"
capability "Polling"
capability "Sensor"
capability "Refresh"
attribute "outsideTemp", "number"
command "switchMode"
command "quickSetHeat"
command "quickSetOutTemp"
command "increaseHeatSetpoint"
command "decreaseHeatSetpoint"
command "parameterSetting"
command "setCustomThermostatMode"
command "eco"
command "updateWeather"
fingerprint profileId: "0104", endpointId: "19", inClusters: " 0000,0003,0201,0204", outClusters: "0402"
}
// simulator metadata
simulator { }
tiles(scale : 2) {
multiAttributeTile(name:"thermostatMulti", type:"thermostat", width:6, height:4, canChangeIcon: true) {
tileAttribute("device.temperature", key: "PRIMARY_CONTROL") {
attributeState("temp", label:'${currentValue}')
attributeState("high", label:'HIGH')
attributeState("low", label:'LOW')
attributeState("--", label:'--')
}
tileAttribute("device.heatingSetpoint", key: "VALUE_CONTROL") {
attributeState("VALUE_UP", action: "increaseHeatSetpoint")
attributeState("VALUE_DOWN", action: "decreaseHeatSetpoint")
}
tileAttribute("device.thermostatOperatingState", key: "OPERATING_STATE") {
attributeState("idle", backgroundColor:"#44b621")
attributeState("heating", backgroundColor:"#ffa81e")
}
tileAttribute("device.thermostatMode", key: "THERMOSTAT_MODE") {
attributeState("off", label:'${name}')
attributeState("heat", label:'${name}')
attributeState("eco", label:'${name}')
}
tileAttribute("device.heatingSetpoint", key: "HEATING_SETPOINT") {
attributeState("heatingSetpoint", label:'${currentValue}')
}
}
standardTile("mode", "device.thermostatMode", width: 2, height: 2) {
state "off", label:'${name}', action:"switchMode", nextState:"heat"
state "heat", label:'${name}', action:"switchMode", nextState:"eco", icon:"http://cdn.device-icons.smartthings.com/Home/home29-icn@2x.png"
state "eco", label:'${name}', action:"switchMode", nextState:"off", icon:"http://cdn.device-icons.smartthings.com/Outdoor/outdoor3-icn@2x.png"
}
valueTile("heatingSetpoint", "device.heatingSetpoint", width: 2, height: 2) {
state "temperature", label:'Setpoint ${currentValue}', backgroundColors:[
[value: 31, color: "#153591"],
[value: 44, color: "#1e9cbb"],
[value: 59, color: "#90d2a7"],
[value: 74, color: "#44b621"],
[value: 84, color: "#f1d801"],
[value: 95, color: "#d04e00"],
[value: 96, color: "#bc2323"]
]
state "--", label:'--', backgroundColor:"#bdbdbd"
}
standardTile("refresh", "device.refresh", decoration: "flat", width: 2, height: 2) {
state "default", action:"refresh.refresh", icon:"st.secondary.refresh"
}
standardTile("configure", "device.configure", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
state "configure", label:'', action:"configuration.configure", icon:"st.secondary.configure"
}
main ("thermostatMulti")
details(["thermostatMulti", "mode", "heatingSetpoint", "refresh", "configure"])
}
}
def parse(String description) {
return parseCalled(description)
}
def parseCalled(String description) {
log.debug "Parse description $description"
def map = [:]
if (description?.startsWith("read attr -")) {
def descMap = parseDescriptionAsMap(description)
log.debug "Desc Map: $descMap"
if (descMap.cluster == "0201" && descMap.attrId == "0000")
{
map.name = "temperature"
map.value = getTemperature(descMap.value)
if (descMap.value == "7ffd") { //0x7FFD
map.value = "low"
}
else if (descMap.value == "7fff") { //0x7FFF
map.value = "high"
}
else if (descMap.value == "8000") { //0x8000
map.value = "--"
}
else if (descMap.value > "8000") {
map.value = -(Math.round(2*(655.36 - map.value))/2)
}
sendEvent(name:"temperature", value:map.value)
}
else if (descMap.cluster == "0201" && descMap.attrId == "0012") {
log.debug "HEATING SETPOINT"
map.name = "heatingSetpoint"
map.value = getTemperature(descMap.value)
if (descMap.value == "8000") { //0x8000
map.value = "--"
}
sendEvent(name:"heatingSetpoint", value:map.value)
}
else if (descMap.cluster == "0201" && descMap.attrId == "001c") {
if (descMap.value.size() == 8) {
log.debug "MODE"
map.name = "thermostatMode"
map.value = getModeMap()[descMap.value]
sendEvent(name:"thermostatMode", value:map.value)
}
else if (descMap.value.size() == 10) {
log.debug "MODE & SETPOINT MODE"
def twoModesAttributes = descMap.value[0..-9]
map.name = "thermostatMode"
map.value = getModeMap()[twoModesAttributes]
sendEvent(name:"thermostatMode", value:map.value)
}
}
else if (descMap.cluster == "0201" && descMap.attrId == "401c") {
log.debug "SETPOINT MODE"
log.debug "descMap.value $descMap.value"
map.name = "thermostatMode"
map.value = getModeMap()[descMap.value]
sendEvent(name:"thermostatMode", value:map.value)
}
else if (descMap.cluster == "0201" && descMap.attrId == "0008") {
log.debug "HEAT DEMAND"
map.name = "thermostatOperatingState"
map.value = getModeMap()[descMap.value]
if (descMap.value < "10") {
map.value = "idle"
}
else {
map.value = "heating"
}
sendEvent(name:"thermostatOperatingState", value:map.value)
if (settings.heatdetails == "No") {
map.displayed = false
}
}
}
def result = null
if (map) {
result = createEvent(map)
}
log.debug "Parse returned $map"
return result
}
def parseDescriptionAsMap(description) {
(description - "read attr - ").split(",").inject([:]) { map, param ->
def nameAndValue = param.split(":")
map += [(nameAndValue[0].trim()):nameAndValue[1].trim()]
}
}
def updateWeather() {
log.info "updating weather"
def weather
// If there is a zipcode defined, weather forecast will be sent. Otherwise, no weather forecast.
if (settings.zipcode) {
log.debug "ZipCode: ${settings.zipcode}"
weather = getWeatherFeature( "conditions", settings.zipcode )
// Check if the variable is populated, otherwise return.
if (!weather) {
log.debug( "Something went wrong, no data found." )
return false
}
def tempToSend
if(getTemperatureScale() == "C" ) {
tempToSend = weather.current_observation.temp_c
log.debug( "Outdoor Temperature: ${tempToSend} C" )
}
else {
tempToSend = weather.current_observation.temp_f
log.debug( "Outdoor Temperature: ${tempToSend} F" )
}
sendEvent( name: 'outsideTemp', value: tempToSend )
quickSetOutTemp(tempToSend)
}
}
def getModeMap() { [
"00":"off",
"04":"heat",
"05":"eco"
]}
def getFanModeMap() { [
"04":"fanOn",
"05":"fanAuto"
]}
def poll() {
return pollCalled()
}
def pollCalled() {
delayBetween([
updateWeather(),
zigbee.readAttribute(0x201, 0x0000), //Read Local Temperature
zigbee.readAttribute(0x201, 0x0008), //Read PI Heating State
zigbee.readAttribute(0x201, 0x0012), //Read Heat Setpoint
zigbee.readAttribute(0x201, 0x001C), //Read System Mode
my_readAttribute(0x201, 0x401C, ["mfgCode": "0x1185"]), //Read Manufacturer Specific Setpoint Mode
zigbee.readAttribute(0x204, 0x0000), //Read Temperature Display Mode
zigbee.readAttribute(0x204, 0x0001), //Read Keypad Lockout
sendEvent( name: 'change', value: 0 )
], 200)
}
def getTemperature(value) {
if (value != null) {
log.debug("value $value")
def celsius = Integer.parseInt(value, 16) / 100
if (getTemperatureScale() == "C") {
return celsius
}
else {
return Math.round(celsiusToFahrenheit(celsius))
}
}
}
def refresh() {
poll()
}
def quickSetHeat(degrees) {
sendEvent( name: 'change', value: 1 )
setHeatingSetpoint(degrees, 0)
}
def setHeatingSetpoint(preciseDegrees, delay = 0) {
if (preciseDegrees != null) {
def temperatureScale = getTemperatureScale()
def degrees = new BigDecimal(preciseDegrees).setScale(1, BigDecimal.ROUND_HALF_UP)
log.debug "setHeatingSetpoint({$degrees} ${temperatureScale})"
sendEvent(name: "heatingSetpoint", value: degrees, unit: temperatureScale)
sendEvent(name: "thermostatSetpoint", value: degrees, unit: temperatureScale)
def celsius = (getTemperatureScale() == "C") ? degrees : (fahrenheitToCelsius(degrees) as Float).round(2)
delayBetween([
zigbee.writeAttribute(0x201, 0x12, 0x29, hex(celsius * 100)),
zigbee.readAttribute(0x201, 0x12), //Read Heat Setpoint
zigbee.readAttribute(0x201, 0x08), //Read PI Heat demand
], 100)
}
}
def setCoolingSetpoint(degrees) {
if (degrees != null) {
def degreesInteger = Math.round(degrees)
log.debug "setCoolingSetpoint({$degreesInteger} ${temperatureScale})"
sendEvent("name": "coolingSetpoint", "value": degreesInteger)
def celsius = (getTemperatureScale() == "C") ? degreesInteger : (fahrenheitToCelsius(degreesInteger) as Double).round(2)
"st wattr 0x${device.deviceNetworkId} 0x19 0x201 0x11 0x29 {" + hex(celsius * 100) + "}"
}
}
def quickSetOutTemp(degrees) {
setOutdoorTemperature(degrees, 0)
}
def setOutdoorTemperature(degrees, delay = 0) {
setOutdoorTemperature(degrees.toDouble(), delay)
}
def setOutdoorTemperature(Double degrees, Integer delay = 0) {
def p = (state.precision == null) ? 1 : state.precision
Integer tempToSend
def tempToSendInString
def celsius = (getTemperatureScale() == "C") ? degrees : (fahrenheitToCelsius(degrees) as Float).round(2)
if (celsius < 0) {
tempToSend = (celsius*100) + 65536
}
else {
tempToSend = (celsius*100)
}
tempToSendInString = zigbee.convertToHexString(tempToSend, 4)
my_writeAttribute(0x201, 0x4001, 0x29, tempToSendInString, ["mfgCode": "0x1185"])
}
def increaseHeatSetpoint() {
def currentMode = device.currentState("thermostatMode")?.value
if (currentMode != "off") {
float currentSetpoint = device.currentValue("heatingSetpoint")
def locationScale = getTemperatureScale()
float maxSetpoint
float step
if (locationScale == "C") {
maxSetpoint = 30;
step = 0.5
}
else {
maxSetpoint = 86
step = 1
}
if (currentSetpoint < maxSetpoint) {
currentSetpoint = currentSetpoint + step
quickSetHeat(currentSetpoint)
}
}
}
def decreaseHeatSetpoint() {
def currentMode = device.currentState("thermostatMode")?.value
if (currentMode != "off") {
float currentSetpoint = device.currentValue("heatingSetpoint")
def locationScale = getTemperatureScale()
float minSetpoint
float step
if (locationScale == "C") {
minSetpoint = 5;
step = 0.5
}
else {
minSetpoint = 41
step = 1
}
if (currentSetpoint > minSetpoint) {
currentSetpoint = currentSetpoint - step
quickSetHeat(currentSetpoint)
}
}
}
def modes() {
["heat", "eco", "off"]
}
def switchMode() {
def currentMode = device.currentState("thermostatMode")?.value
def lastTriedMode = state.lastTriedMode ?: currentMode ?: "heat"
def modeOrder = modes()
def next = { modeOrder[modeOrder.indexOf(it) + 1] ?: modeOrder[0] }
def nextMode = next(currentMode)
def modeNumber;
Integer setpointModeNumber;
def modeToSendInString;
if (nextMode == "heat") {
modeNumber = 04
setpointModeNumber = 04
}
else if (nextMode == "eco") {
modeNumber = 04
setpointModeNumber = 05
}
else {
modeNumber = 00
setpointModeNumber = 00
}
if (supportedModes?.contains(currentMode)) {
while (!supportedModes.contains(nextMode) && nextMode != "heat") {
nextMode = next(nextMode)
}
}
state.lastTriedMode = nextMode
modeToSendInString = zigbee.convertToHexString(setpointModeNumber, 2)
delayBetween([
"st wattr 0x${device.deviceNetworkId} 0x19 0x201 0x001C 0x30 {$modeNumber}",
my_writeAttribute(0x201, 0x401C, 0x30, modeToSendInString, ["mfgCode": "0x1185"]),
poll()
], 1000)
}
def setThermostatMode() {
log.debug "switching thermostatMode"
def currentMode = device.currentState("thermostatMode")?.value
def modeOrder = modes()
def index = modeOrder.indexOf(currentMode)
def next = index >= 0 && index < modeOrder.size() - 1 ? modeOrder[index + 1] : modeOrder[0]
log.debug "switching mode from $currentMode to $next"
"$next"()
}
def setThermostatFanMode() {
log.debug "Switching fan mode"
def currentFanMode = device.currentState("thermostatFanMode")?.value
log.debug "switching fan from current mode: $currentFanMode"
def returnCommand
switch (currentFanMode) {
case "fanAuto":
returnCommand = fanOn()
break
case "fanOn":
returnCommand = fanAuto()
break
}
if(!currentFanMode) { returnCommand = fanAuto() }
returnCommand
}
def setThermostatMode(String value) {
log.debug "setThermostatMode({$value})"
def currentMode = device.currentState("thermostatMode")?.value
def lastTriedMode = state.lastTriedMode ?: currentMode ?: "heat"
def modeNumber;
Integer setpointModeNumber;
def modeToSendInString;
if (value == "heat") {
modeNumber = 04
setpointModeNumber = 04
}
else if (value == "eco") {
modeNumber = 04
setpointModeNumber = 05
}
else {
modeNumber = 00
setpointModeNumber = 00
}
if (supportedModes?.contains(currentMode)) {
while (!supportedModes.contains(value) && value != "heat") {
value = next(value)
}
}
state.lastTriedMode = value
modeToSendInString = zigbee.convertToHexString(setpointModeNumber, 2)
delayBetween([
"st wattr 0x${device.deviceNetworkId} 0x19 0x201 0x001C 0x30 {$modeNumber}",
my_writeAttribute(0x201, 0x401C, 0x30, modeToSendInString, ["mfgCode": "0x1185"]),
poll()
], 1000)
}
def setThermostatFanMode(String value) {
log.debug "setThermostatFanMode({$value})"
"$value"()
}
def off() {
log.debug "off"
sendEvent("name":"thermostatMode", "value":"off")
"st wattr 0x${device.deviceNetworkId} 0x19 0x201 0x1C 0x30 {00}"
}
def cool() {
log.debug "cool"
sendEvent("name":"thermostatMode", "value":"cool")
"st wattr 0x${device.deviceNetworkId} 0x19 0x201 0x1C 0x30 {03}"
}
def heat() {
log.debug "heat"
sendEvent("name":"thermostatMode", "value":"heat")
"st wattr 0x${device.deviceNetworkId} 0x19 0x201 0x1C 0x30 {04}"
}
def eco() {
Integer setpointModeNumber;
def modeToSendInString;
log.debug "eco"
setpointModeNumber = 05
modeToSendInString = zigbee.convertToHexString(setpointModeNumber, 2)
sendEvent("name":"thermostatMode", "value":"eco")
"st wattr 0x${device.deviceNetworkId} 0x19 0x201 0x1C 0x30 {04}"
my_writeAttribute(0x201, 0x401C, 0x30, modeToSendInString, ["mfgCode": "0x1185"])
}
def emergencyHeat() {
log.debug "emergencyHeat"
sendEvent("name":"thermostatMode", "value":"emergency heat")
"st wattr 0x${device.deviceNetworkId} 0x19 0x201 0x1C 0x30 {05}"
}
def setCustomThermostatMode(mode) {
setThermostatMode(mode)
}
def on() {
fanOn()
}
def fanOn() {
log.debug "fanOn"
sendEvent("name":"thermostatFanMode", "value":"fanOn")
"st wattr 0x${device.deviceNetworkId} 0x19 0x202 0 0x30 {04}"
}
def auto() {
fanAuto()
}
def fanAuto() {
log.debug "fanAuto"
sendEvent("name":"thermostatFanMode", "value":"fanAuto")
"st wattr 0x${device.deviceNetworkId} 1 0x202 0 0x30 {05}"
}
def configure() {
log.debug "binding to Thermostat cluster"
delayBetween([
"zdo bind 0x${device.deviceNetworkId} 1 0x19 0x201 {${device.zigbeeId}} {}",
//Cluster ID (0x0201 = Thermostat Cluster), Attribute ID, Data Type, Payload (Min report, Max report, On change trigger)
zigbee.configureReporting(0x0201, 0x0000, 0x29, 10, 60, 50), //Attribute ID 0x0000 = local temperature, Data Type: S16BIT
zigbee.configureReporting(0x0201, 0x0012, 0x29, 1, 0, 50), //Attribute ID 0x0012 = occupied heat setpoint, Data Type: S16BIT
zigbee.configureReporting(0x0201, 0x001C, 0x30, 1, 0, 1), //Attribute ID 0x001C = system mode, Data Type: 8 bits enum
zigbee.configureReporting(0x0201, 0x401C, 0x30, 1, 0, 1), //Attribute ID 0x401C = manufacturer specific setpoint mode, Data Type: 8 bits enum
zigbee.configureReporting(0x0201, 0x0008, 0x20, 300, 900, 5), //Attribute ID 0x0008 = pi heating demand, Data Type: U8BIT
//Cluster ID (0x0204 = Thermostat Ui Conf Cluster), Attribute ID, Data Type, Payload (Min report, Max report, On change trigger)
zigbee.configureReporting(0x0204, 0x0000, 0x30, 1, 0, 1), //Attribute ID 0x0000 = temperature display mode, Data Type: 8 bits enum
zigbee.configureReporting(0x0204, 0x0001, 0x30, 1, 0, 1), //Attribute ID 0x0001 = keypad lockout, Data Type: 8 bits enum
//Read the configured variables
zigbee.readAttribute(0x201, 0x0000), //Read Local Temperature
zigbee.readAttribute(0x201, 0x0012), //Read Heat Setpoint
zigbee.readAttribute(0x201, 0x001C), //Read System Mode
my_readAttribute(0x201, 0x401C, ["mfgCode": "0x1185"]), //Read Manufacturer Specific Setpoint Mode
zigbee.readAttribute(0x201, 0x0008), //Read PI Heating State
zigbee.readAttribute(0x204, 0x0000), //Read Temperature Display Mode
zigbee.readAttribute(0x204, 0x0001), //Read Keypad Lockout
], 200)
}
def updated() {
response(parameterSetting())
}
def parameterSetting() {
def lockmode = null
def valid_lock = 0
log.info "lock : $settings.lock"
if (settings.lock == "Yes") {
lockmode = 0x01
valid_lock = 1
}
else if (settings.lock == "No") {
lockmode = 0x00
valid_lock = 1
}
if (valid_lock == 1)
{
log.info "lock valid"
delayBetween([
zigbee.writeAttribute(0x204, 0x01, 0x30, lockmode), //Write Lock Mode
poll(),
], 200)
}
else {
log.info "nothing valid"
}
}
private hex(value) {
new BigInteger(Math.round(value).toString()).toString(16)
}
private getEndpointId() {
new BigInteger(device.endpointId, 16).toString()
}
private String swapEndianHex(String hex) {
reverseArray(hex.decodeHex()).encodeHex()
}
private byte[] reverseArray(byte[] array) {
int i = 0;
int j = array.length - 1;
byte tmp;
while (j > i) {
tmp = array[j];
array[j] = array[i];
array[i] = tmp;
j--;
i++;
}
return array
}
def my_readAttribute(cluster, attributeId, Map additional=null)
{
if (additional?.get("mfgCode")) {
delayBetween([
"zcl mfg-code ${additional['mfgCode']}",
"zcl global read ${cluster} ${attributeId}",
"send 0x${device.deviceNetworkId} 1 ${endpointId}"
], 200)
log.info "send 0x${device.deviceNetworkId} 1 ${endpointId}"
}
else {
zigbee.readAttribute(cluster, attributeId)
}
}
def my_writeAttribute(cluster, attributeId, dataType, value, Map additional=null)
{
value = swapEndianHex(value)
if (additional?.get("mfgCode")) {
delayBetween([
"zcl mfg-code ${additional['mfgCode']}",
"zcl global write ${cluster} ${attributeId} ${dataType} {${value}}",
"send 0x${device.deviceNetworkId} 1 ${endpointId}"
], 200)
}
else {
zigbee.writeAttribute(cluster, attributeId, dataType, value)
}
}
That code works for you? It does nothing on my end. It even has all sorts of errors that fill up the log like:
2019-02-02 12:29:00.045 pm errorgroovy.lang.MissingMethodException: No signature of method: com.hubitat.zigbee.Zigbee.writeAttribute() is applicable for argument types: (java.lang.Integer, java.lang.Integer, java.lang.Integer, java.lang.String) values: [513, 18, 41, 834] Possible solutions: writeAttribute(java.lang.Integer, java.lang.Integer, java.lang.Integer, java.lang.Integer), writeAttribute(java.lang.Integer, java.lang.Integer, java.lang.Integer, java.lang.Integer, java.util.Map), writeAttribute(java.lang.Integer, java.lang.Integer, java.lang.Integer, java.lang.Integer, java.util.Map, int) on line 295 (setHeatingSetpoint)
Even if I change the offending line (having Integer and not BigInteger in the writeattribute) it still doesn't work at all. I'll try removing the device and including again who knows....
A lot of the offending lines deal with F to C conversion. What temperature scale are you using??
Sorry, I don't have the thermostat, just trying to help.
Thanks. I used that code on ST with no problems but there appear to be some changes in writeattribute that render it problematic. Unfortunately the generic driver does everything but set the temperature.