Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WWST-1611] New fingerprint for Zigbee Thermostat - KONOz Lux Thermostat. #3584

Merged
merged 3 commits into from
Oct 22, 2018
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
337 changes: 337 additions & 0 deletions devicetypes/smartthings/zigbee-thermostat.src/zigbee-thermostat.groovy
Original file line number Diff line number Diff line change
@@ -0,0 +1,337 @@
/**
* Copyright 2018 SmartThings
*
* 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.
*
* CentraLite Thermostat
*
* Author: SRPOL
* Date: 2018-10-15
*/

import physicalgraph.zigbee.zcl.DataType

metadata {
definition (name: "Zigbee Thermostat", namespace: "smartthings", author: "SmartThings", mnmn: "SmartThings", vid: "SmartThings-smartthings-Z-Wave_Thermostat") {
capability "Actuator"
capability "Temperature Measurement"
Copy link
Contributor

@greens greens Oct 16, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
capability "Temperature Measurement"
capability "Temperature Measurement"
capability "Thermostat"

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In order to work with SmartApps, it still needs the old deprecated capability.

capability "Thermostat Mode"
capability "Thermostat Fan Mode"
capability "Thermostat Cooling Setpoint"
capability "Thermostat Heating Setpoint"
capability "Thermostat Operating State"
capability "Configuration"
capability "Health Check"
capability "Refresh"
capability "Sensor"

command "lowerHeatingSetpoint"
command "raiseHeatingSetpoint"
command "lowerCoolSetpoint"
command "raiseCoolSetpoint"

fingerprint profileId: "0104", inClusters: "0000,0001,0003,0004,0005,0020,0201,0202,0204,0B05", outClusters: "000A, 0019", manufacturer: "LUX", model: "KONOZ", deviceJoinName: "LUX KONOz Thermostat"
}

tiles {
multiAttributeTile(name:"temperature", type:"generic", width:3, height:2, canChangeIcon: true) {
tileAttribute("device.temperature", key: "PRIMARY_CONTROL") {
attributeState("temperature", label:'${currentValue}°', icon: "st.alarm.temperature.normal",
backgroundColors:[
// Celsius
[value: 0, color: "#153591"],
[value: 7, color: "#1e9cbb"],
[value: 15, color: "#90d2a7"],
[value: 23, color: "#44b621"],
[value: 28, color: "#f1d801"],
[value: 35, color: "#d04e00"],
[value: 37, color: "#bc2323"],
// Fahrenheit
[value: 40, 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"]
]
)
}
}
standardTile("thermostatMode", "device.thermostatMode", width:2, height:2, inactiveLabel: false, decoration: "flat") {
state "cool", action: "thermostatMode.off", icon: "st.thermostat.cool", nextState: "..."
state "off", action: "thermostatMode.heat", icon: "st.thermostat.heating-cooling-off", nextState: "..."
state "heat", action: "thermostatMode.cool", icon: "st.thermostat.heat", nextState: "..."
state "...", label: "Updating...", nextState:"..."
}
standardTile("thermostatFanMode", "device.thermostatFanMode", width:2, height:2, inactiveLabel: false, decoration: "flat") {
state "auto", action:"thermostatFanMode.fanOn", nextState:"...", icon: "st.thermostat.fan-auto"
state "on", action:"thermostatFanMode.fanAuto", nextState:"...", icon: "st.thermostat.fan-on"
state "...", label: "Updating...", nextState:"...", backgroundColor:"#ffffff"
}
standardTile("lowerHeatingSetpoint", "device.heatingSetpoint", width:2, height:1, inactiveLabel: false, decoration: "flat") {
state "heatingSetpoint", action:"lowerHeatingSetpoint", icon:"st.thermostat.thermostat-left"
}
valueTile("heatingSetpoint", "device.heatingSetpoint", width:2, height:1, inactiveLabel: false, decoration: "flat") {
state "heatingSetpoint", label:'${currentValue}° heat', backgroundColor:"#ffffff"
}
standardTile("raiseHeatingSetpoint", "device.heatingSetpoint", width:2, height:1, inactiveLabel: false, decoration: "flat") {
state "heatingSetpoint", action:"raiseHeatingSetpoint", icon:"st.thermostat.thermostat-right"
}
standardTile("lowerCoolSetpoint", "device.coolingSetpoint", width:2, height:1, inactiveLabel: false, decoration: "flat") {
state "coolingSetpoint", action:"lowerCoolSetpoint", icon:"st.thermostat.thermostat-left"
}
valueTile("coolingSetpoint", "device.coolingSetpoint", width:2, height:1, inactiveLabel: false, decoration: "flat") {
state "coolingSetpoint", label:'${currentValue}° cool', backgroundColor:"#ffffff"
}
standardTile("raiseCoolSetpoint", "device.heatingSetpoint", width:2, height:1, inactiveLabel: false, decoration: "flat") {
state "heatingSetpoint", action:"raiseCoolSetpoint", icon:"st.thermostat.thermostat-right"
}
standardTile("thermostatOperatingState", "device.thermostatOperatingState", width: 2, height:1, decoration: "flat") {
state "thermostatOperatingState", label:'${currentValue}', backgroundColor:"#ffffff"
}
standardTile("refresh", "device.thermostatMode", width:2, height:1, inactiveLabel: false, decoration: "flat") {
state "default", action:"refresh.refresh", icon:"st.secondary.refresh"
}
main "temperature"
details(["temperature", "lowerHeatingSetpoint", "heatingSetpoint", "raiseHeatingSetpoint", "lowerCoolSetpoint",
"coolingSetpoint", "raiseCoolSetpoint", "thermostatMode", "thermostatFanMode", "thermostatOperatingState", "refresh"])
}
}

def parse(String description) {
def map = zigbee.getEvent(description)
def result
if(!map) {
result = parseAttrMessage(description)
} else {
log.warn "Some event is not parsed: ${map}"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is odd, because you are writing printing this warning when the library successfully parses an event. I think a message like "Unexpected event: " would make more sense.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mostly it was for debugging purposes, I wanted to check if there's any command from the device which is returned by zigbee.getEvent() (plot twist, there isn't). But yeah, I can agree that this is odd.

}
log.debug "Description ${description} parsed to ${result}"
return result
}

private parseAttrMessage(description) {
def descMap = zigbee.parseDescriptionAsMap(description)
log.debug "Desc Map: $descMap"
def result = []
List attrData = [[cluster: descMap.clusterInt, attribute: descMap.attrInt, value: descMap.value]]
descMap.additionalAttrs.each {
attrData << [cluster: descMap.clusterInt, attribute: it.attrInt, value: it.value]
}
attrData.each {
def map = [:]
if (it.cluster == THERMOSTAT_CLUSTER && it.attribute == LOCAL_TEMPERATURE) {
log.debug "TEMP"
map.name = "temperature"
map.value = getTemperature(it.value)
map.unit = temperatureScale
} else if (it.cluster == THERMOSTAT_CLUSTER && it.attribute == COOLING_SETPOINT) {
log.debug "COOLING SETPOINT"
map.name = "coolingSetpoint"
map.value = getTemperature(it.value)
map.unit = temperatureScale
} else if (it.cluster == THERMOSTAT_CLUSTER && it.attribute == HEATING_SETPOINT) {
log.debug "HEATING SETPOINT"
map.name = "heatingSetpoint"
map.value = getTemperature(it.value)
map.unit = temperatureScale
} else if (it.cluster == THERMOSTAT_CLUSTER && (it.attribute == THERMOSTAT_MODE || it.attribute == THERMOSTAT_RUNNING_MODE)) {
log.debug "MODE"
map.name = "thermostatMode"
map.value = THERMOSTAT_MODE_MAP[it.value]
} else if (it.cluster == THERMOSTAT_CLUSTER && it.attribute == THERMOSTAT_RUNNING_STATE) {
log.debug "RUNNING STATE"
def binValue = extendString(bin(hexToInt(it.value)), 16, '0').reverse()
map.name = "thermostatOperatingState"
if(binValue[0] == "1") {
map.value = "heating"
} else if(binValue[1] == "1") {
map.value = "cooling"
} else {
map.value = binValue[2] == "1" ? "fan only" : "idle"
}
} else if (it.cluster == FAN_CONTROL_CLUSTER && it.attribute == FAN_MODE) {
log.debug "FAN MODE"
map.name = "thermostatFanMode"
map.value = FAN_MODE_MAP[it.value]
}
if(map) {
result << createEvent(map)
}
}
return result
}

def installed() {
refresh()
}

def refresh() {
return zigbee.readAttribute(THERMOSTAT_CLUSTER, LOCAL_TEMPERATURE) +
zigbee.readAttribute(THERMOSTAT_CLUSTER, COOLING_SETPOINT) +
zigbee.readAttribute(THERMOSTAT_CLUSTER, HEATING_SETPOINT) +
zigbee.readAttribute(THERMOSTAT_CLUSTER, THERMOSTAT_MODE) +
zigbee.readAttribute(THERMOSTAT_CLUSTER, THERMOSTAT_RUNNING_STATE) +
zigbee.readAttribute(FAN_CONTROL_CLUSTER, FAN_MODE)
}

def ping() {
refresh()
}

def configure() {
def binding = zigbee.addBinding(THERMOSTAT_CLUSTER) + zigbee.addBinding(FAN_CONTROL_CLUSTER)
def startValues = zigbee.writeAttribute(THERMOSTAT_CLUSTER, HEATING_SETPOINT, DataType.INT16, 0x0A28) +
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any reason these values were chosen? The heating set point seems a little high.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They're taken from ZCL reference but... It appears I've accidentally switched heating and cooling setpoints default values...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I see now, they are the default values for those attributes. If they are actually the defaults I wouldn't think we would have to set them on join, but I don't have any problem with it. Swap them so they are the correct defaults and this PR looks good to me.

zigbee.writeAttribute(THERMOSTAT_CLUSTER, COOLING_SETPOINT, DataType.INT16, 0x07D0)

return binding + startValues + refresh()
}


def getTemperature(value) {
if (value != null) {
def celsius = Integer.parseInt(value, 16) / 100
if (temperatureScale == "C") {
return celsius
} else {
return Math.round(celsiusToFahrenheit(celsius))
}
}
}

def setThermostatMode(value) {
switch(value) {
case "heat":
heat()
break
case "cool":
cool()
break
default:
off()
}
}

def off() {
return zigbee.writeAttribute(THERMOSTAT_CLUSTER, THERMOSTAT_MODE, DataType.ENUM8, THERMOSTAT_MODE_OFF) +
zigbee.readAttribute(THERMOSTAT_CLUSTER, THERMOSTAT_MODE)
}

def cool() {
return zigbee.writeAttribute(THERMOSTAT_CLUSTER, THERMOSTAT_MODE, DataType.ENUM8, THERMOSTAT_MODE_COOL) +
zigbee.readAttribute(THERMOSTAT_CLUSTER, THERMOSTAT_MODE)
}

def heat() {
return zigbee.writeAttribute(THERMOSTAT_CLUSTER, THERMOSTAT_MODE, DataType.ENUM8, THERMOSTAT_MODE_HEAT) +
zigbee.readAttribute(THERMOSTAT_CLUSTER, THERMOSTAT_MODE)
}

def fanAuto() {
return zigbee.writeAttribute(FAN_CONTROL_CLUSTER, FAN_MODE, DataType.ENUM8, FAN_MODE_AUTO) +
zigbee.readAttribute(FAN_CONTROL_CLUSTER, FAN_MODE)
}

def fanOn() {
return zigbee.writeAttribute(FAN_CONTROL_CLUSTER, FAN_MODE, DataType.ENUM8, FAN_MODE_ON) +
zigbee.readAttribute(FAN_CONTROL_CLUSTER, FAN_MODE)
}

def setCoolingSetpoint(degrees) {
if (degrees != null) {
def degreesInteger = Math.round(degrees)
def celsius = (temperatureScale == "C") ? degreesInteger : (fahrenheitToCelsius(degreesInteger) as Double).round(2)
return zigbee.writeAttribute(THERMOSTAT_CLUSTER, COOLING_SETPOINT, DataType.INT16, hex(celsius * 100)) +
zigbee.readAttribute(THERMOSTAT_CLUSTER, COOLING_SETPOINT)
}
}

def setHeatingSetpoint(degrees) {
if (degrees != null) {
def degreesInteger = Math.round(degrees)

def celsius = (temperatureScale == "C") ? degreesInteger : (fahrenheitToCelsius(degreesInteger) as Double).round(2)
return zigbee.writeAttribute(THERMOSTAT_CLUSTER, HEATING_SETPOINT, DataType.INT16, hex(celsius * 100)) +
zigbee.readAttribute(THERMOSTAT_CLUSTER, HEATING_SETPOINT)
}
}

def raiseHeatingSetpoint() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some of our other thermostat DTHs have a delay here before sending the actual command in order to collect multiple arrow presses into just one set instead of several "up by 1"s.

I know OneApp doesn't work like this, but I think it's worth doing for the classic DTH for a better user experience.

If it's too complicated and error-prone that you don't think it's worth it, I'd understand that too.

alterSetpoint(true, "heatingSetpoint")
}

def lowerHeatingSetpoint() {
alterSetpoint(false, "heatingSetpoint")
}

def raiseCoolSetpoint() {
alterSetpoint(true, "coolingSetpoint")
}

def lowerCoolSetpoint() {
alterSetpoint(false, "coolingSetpoint")
}

/*
* This method uses Setpoint Raise/Lower Command
* MSB is responsible for choosing heating/cooling setpoint,
* 0x00 for heat, 0x01 for cool.
* LSB is signed 8-bit integer, which specifies with how many steps setpoint will be changed.
* One step: 0.1 C
*/
def alterSetpoint(raise, setpoint) {
def MSB = (setpoint == "heatingSetpoint") ? "00" : "01"
def LSB = raise ? "05" : "FB" // +0.5 C : -0.5 C
def payload = MSB + LSB
zigbee.command(THERMOSTAT_CLUSTER, SETPOINT_RAISE_LOWER_CMD, payload)
}

private hex(value) {
return new BigInteger(Math.round(value).toString()).toString(16)
}

private bin(value) {
return new BigInteger(Math.round(value).toString()).toString(2)
}

private hexToInt(value) {
new BigInteger(value, 16)
}

private extendString(str, size, character) {
return character * (size - str.length()) + str
}

private getTHERMOSTAT_CLUSTER() { 0x0201 }
private getLOCAL_TEMPERATURE() { 0x0000 }
private getCOOLING_SETPOINT() { 0x0011 }
private getHEATING_SETPOINT() { 0x0012 }
private getTHERMOSTAT_RUNNING_MODE() { 0x001E }
private getTHERMOSTAT_MODE() { 0x001C }
private getTHERMOSTAT_MODE_OFF() { 0x00 }
private getTHERMOSTAT_MODE_COOL() { 0x03 }
private getTHERMOSTAT_MODE_HEAT() { 0x04 }
private getTHERMOSTAT_MODE_MAP() { [
"00":"off",
"03":"cool",
"04":"heat",
]}
private getTHERMOSTAT_RUNNING_STATE() { 0x0029 }
private getSETPOINT_RAISE_LOWER_CMD() { 0x00 }

private getFAN_CONTROL_CLUSTER() { 0x0202 }
private getFAN_MODE() { 0x0000 }
private getFAN_MODE_ON() { 0x04 }
private getFAN_MODE_AUTO() { 0x05 }
private getFAN_MODE_MAP() { [
"04":"on",
"05":"auto"
]}