-
Notifications
You must be signed in to change notification settings - Fork 88.5k
[WWST-5165] Everspring Radiator Thermostat integration #11285
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||
|---|---|---|---|---|
| @@ -0,0 +1,259 @@ | ||||
| /** | ||||
| * Copyright 2019 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. | ||||
| * | ||||
| */ | ||||
| metadata { | ||||
| definition (name: "Z-Wave Radiator Thermostat", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "oic.d.thermostat") { | ||||
| capability "Thermostat Mode" | ||||
| capability "Refresh" | ||||
| capability "Battery" | ||||
| capability "Thermostat Heating Setpoint" | ||||
| capability "Health Check" | ||||
| capability "Thermostat" | ||||
| capability "Temperature Measurement" | ||||
|
|
||||
| fingerprint mfr: "0060", prod: "0015", model: "0001", deviceJoinName: "Everspring Thermostatic Radiator Valve", mnmn: "SmartThings", vid: "generic-radiator-thermostat" | ||||
| //this DTH is sending temperature setpoint commands using Celsius scale and assumes that they'll be handled correctly by device | ||||
| //if new device added to this DTH won't be able to do that, make sure to you'll handle conversion in a right way | ||||
| } | ||||
|
|
||||
| tiles(scale: 2) { | ||||
| multiAttributeTile(name:"thermostat", type:"general", width:6, height:4, canChangeIcon: false) { | ||||
| tileAttribute("device.thermostatMode", key: "PRIMARY_CONTROL") { | ||||
| attributeState("off", action:"switchMode", nextState:"...", icon: "st.thermostat.heating-cooling-off") | ||||
| attributeState("heat", action:"switchMode", nextState:"...", icon: "st.thermostat.heat") | ||||
| attributeState("emergency heat", action:"switchMode", nextState:"...", icon: "st.thermostat.emergency-heat") | ||||
| } | ||||
| tileAttribute("device.temperature", key: "SECONDARY_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"] | ||||
| ] | ||||
| ) | ||||
| } | ||||
| tileAttribute("device.heatingSetpoint", key: "HEATING_SETPOINT") { | ||||
| attributeState("default", label: '${currentValue}', unit: "°", defaultState: true) | ||||
| } | ||||
| } | ||||
| controlTile("thermostatMode", "device.thermostatMode", "enum", width: 2 , height: 2, supportedStates: "device.supportedThermostatModes") { | ||||
| state("off", action: "setThermostatMode", label: 'Off', icon: "st.thermostat.heating-cooling-off") | ||||
| state("heat", action: "setThermostatMode", label: 'Heat', icon: "st.thermostat.heat") | ||||
| state("emergency heat", action:"setThermostatMode", label: 'Emergency heat', icon: "st.thermostat.emergency-heat") | ||||
| } | ||||
| controlTile("heatingSetpoint", "device.heatingSetpoint", "slider", | ||||
| sliderType: "HEATING", | ||||
| debouncePeriod: 750, | ||||
| range: "device.heatingSetpointRange", | ||||
| width: 2, height: 2) { | ||||
| state "default", action:"setHeatingSetpoint", label:'${currentValue}', backgroundColor: "#E86D13" | ||||
| } | ||||
| valueTile("battery", "device.battery", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { | ||||
| state "battery", label: 'Battery:\n${currentValue}%', unit: "%" | ||||
| } | ||||
| standardTile("refresh", "command.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { | ||||
| state "refresh", label: 'refresh', action: "refresh.refresh", icon: "st.secondary.refresh-icon" | ||||
| } | ||||
| main "thermostat" | ||||
| details(["thermostat", "thermostatMode", "heatingSetpoint", "battery", "refresh"]) | ||||
| } | ||||
| } | ||||
|
|
||||
| def initialize() { | ||||
| sendEvent(name: "checkInterval", value: 4 * 60 * 60 + 24 * 60 , displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID, offlinePingable: "1"]) | ||||
| sendEvent(name: "supportedThermostatModes", value: thermostatSupportedModes.encodeAsJson(), displayed: false) | ||||
| sendEvent(name: "heatingSetpointRange", value: [minHeatingSetpointTemperature, maxHeatingSetpointTemperature], displayed: false) | ||||
| response(refresh()) | ||||
| } | ||||
|
|
||||
| def installed() { | ||||
| initialize() | ||||
| } | ||||
|
|
||||
| def updated() { | ||||
| initialize() | ||||
| } | ||||
|
|
||||
| def parse(String description) { | ||||
| def result = null | ||||
| def cmd = zwave.parse(description) | ||||
| if (cmd) { | ||||
| result = zwaveEvent(cmd) | ||||
| } else { | ||||
| log.warn "${device.displayName} - no-parsed event: ${description}" | ||||
| } | ||||
| log.debug "Parse returned: ${result}" | ||||
| return result | ||||
| } | ||||
|
|
||||
| def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { | ||||
| def encapsulatedCommand = cmd.encapsulatedCommand() | ||||
| if (encapsulatedCommand) { | ||||
| log.debug "SecurityMessageEncapsulation into: ${encapsulatedCommand}" | ||||
| zwaveEvent(encapsulatedCommand) | ||||
| } else { | ||||
| log.warn "unable to extract secure command from $cmd" | ||||
| createEvent(descriptionText: cmd.toString()) | ||||
| } | ||||
| } | ||||
|
|
||||
|
|
||||
|
|
||||
| def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) { | ||||
| def value = cmd.batteryLevel == 255 ? 1 : cmd.batteryLevel | ||||
| def map = [name: "battery", value: value, unit: "%"] | ||||
| createEvent(map) | ||||
| } | ||||
|
|
||||
| def zwaveEvent(physicalgraph.zwave.commands.thermostatmodev2.ThermostatModeReport cmd) { | ||||
| def map = [name: "thermostatMode", data:[supportedThermostatModes: thermostatSupportedModes.encodeAsJson()]] | ||||
| switch (cmd.mode) { | ||||
| case 1: | ||||
| map.value = "heat" | ||||
| break | ||||
| case 11: | ||||
| map.value = "emergency heat" | ||||
| break | ||||
| case 0: | ||||
| map.value = "off" | ||||
| break | ||||
| } | ||||
| createEvent(map) | ||||
| } | ||||
|
|
||||
| def zwaveEvent(physicalgraph.zwave.commands.thermostatsetpointv2.ThermostatSetpointReport cmd) { | ||||
| def deviceTemperatureScale = cmd.scale ? 'F' : 'C' | ||||
| createEvent(name: "heatingSetpoint", value: convertTemperatureIfNeeded(cmd.scaledValue, deviceTemperatureScale, cmd.precision), unit: temperatureScale) | ||||
| } | ||||
|
|
||||
| def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd) { | ||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. does the thermostat always report in Celsius, even if you change it to F mode (if possible)
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. right now we have only EU version of this device, afaik right now there's no US version available, so all instructions mention only Celsius and I don't see any option to change temperature unit as well
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since this DTH is designed as a "generic" DTH, I would base it off of the scale that the device sends us. If the device for some reason is not including that data properly in the packet, then I would use |
||||
| def deviceTemperatureScale = cmd.scale ? 'F' : 'C' | ||||
| createEvent(name: "temperature", value: convertTemperatureIfNeeded(cmd.scaledSensorValue, deviceTemperatureScale, cmd.precision), unit: temperatureScale) | ||||
| } | ||||
|
|
||||
| def zwaveEvent(physicalgraph.zwave.Command cmd) { | ||||
| log.warn "Unhandled command: ${cmd}" | ||||
| [:] | ||||
| } | ||||
|
|
||||
| def setThermostatMode(String mode) { | ||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since turning off the thermostat when an unsupported mode is sent in to this command would be bad behavior, you should enforce a check on mode. Like here: SmartThingsPublic/devicetypes/smartthings/zigbee-thermostat.src/zigbee-thermostat.groovy Line 366 in f6f0b3f
|
||||
| def modeValue = 0 | ||||
| if (thermostatSupportedModes.contains(mode)) { | ||||
| switch (mode) { | ||||
| case "heat": | ||||
| modeValue = 1 | ||||
| break | ||||
| case "emergency heat": | ||||
| modeValue = 11 | ||||
| break | ||||
| case "off": | ||||
| modeValue = 0 | ||||
| break | ||||
| } | ||||
| } else { | ||||
| log.debug "Unsupported mode ${mode}" | ||||
| } | ||||
|
|
||||
| [ | ||||
| secure(zwave.thermostatModeV2.thermostatModeSet(mode: modeValue)), | ||||
| "delay 2000", | ||||
| secure(zwave.thermostatModeV2.thermostatModeGet()) | ||||
| ] | ||||
| } | ||||
|
|
||||
| def heat() { | ||||
| setThermostatMode("heat") | ||||
| } | ||||
|
|
||||
| def emergencyHeat() { | ||||
| setThermostatMode("emergency heat") | ||||
| } | ||||
|
|
||||
| def off() { | ||||
| setThermostatMode("off") | ||||
| } | ||||
|
|
||||
| def setHeatingSetpoint(setpoint) { | ||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. just for safety's sake, you should enforce the setpoint limits here as well |
||||
| setpoint = temperatureScale == 'C' ? setpoint : fahrenheitToCelsius(setpoint) | ||||
| setpoint = Math.max(Math.min(setpoint, maxHeatingSetpointTemperature), minHeatingSetpointTemperature) | ||||
| [ | ||||
| secure(zwave.thermostatSetpointV2.thermostatSetpointSet([precision: 1, scale: 0, scaledValue: setpoint, setpointType: 1, size: 2])), | ||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @greens Given my comment above about the scale, does it make sense to convert to a scale that we know the device is using, or just force 'C'? If another device is added to this DTH and it is in "F" mode, if it receives a thermostat setpoint set with a value where the scale is "C" will it "do the right thing"? (I assume yes without pulling up the specs, but you know what assuming leads to...)
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd just leave a note near the fingerprints. Adding that conversion logic is a significant lift.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @greens I've added a note, but I'm not sure if it is what you've though about |
||||
| "delay 2000", | ||||
| secure(zwave.thermostatSetpointV2.thermostatSetpointGet(setpointType: 1)) | ||||
| ] | ||||
| } | ||||
|
|
||||
| def refresh() { | ||||
| def cmds = [ | ||||
| secure(zwave.batteryV1.batteryGet()), | ||||
| secure(zwave.thermostatSetpointV2.thermostatSetpointGet(setpointType: 1)), | ||||
| secure(zwave.sensorMultilevelV5.sensorMultilevelGet()), | ||||
| secure(zwave.thermostatModeV2.thermostatModeGet()) | ||||
| ] | ||||
|
|
||||
| delayBetween(cmds, 2500) | ||||
| } | ||||
|
|
||||
| def ping() { | ||||
| refresh() | ||||
| } | ||||
|
|
||||
| private secure(cmd) { | ||||
| if (zwaveInfo.zw.endsWith("s")) { | ||||
| zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() | ||||
| } else { | ||||
| cmd.format() | ||||
| } | ||||
| } | ||||
|
|
||||
| private getMaxHeatingSetpointTemperature() { | ||||
| if (isEverspringRadiatorThermostat()) { | ||||
| temperatureScale == 'C' ? 35 : 95 | ||||
| } else { | ||||
| temperatureScale == 'C' ? 30 : 86 | ||||
| } | ||||
| } | ||||
|
|
||||
| private getMinHeatingSetpointTemperature() { | ||||
| if (isEverspringRadiatorThermostat()) { | ||||
| temperatureScale == 'C' ? 15 : 59 | ||||
| } else { | ||||
| temperatureScale == 'C' ? 10 : 50 | ||||
| } | ||||
| } | ||||
|
|
||||
| private getThermostatSupportedModes() { | ||||
| if (isEverspringRadiatorThermostat()) { | ||||
| ["off", "heat", "emergency heat"] | ||||
| } else { | ||||
| ["off","heat"] | ||||
| } | ||||
| } | ||||
|
|
||||
| private isEverspringRadiatorThermostat() { | ||||
| zwaveInfo.mfr == "0060" && zwaveInfo.prod == "0015" | ||||
| } | ||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's actually a better way to do the tiles now so that you don't need to do
switchModeandsetThermostatSetpointUpandsetThermostatSetpointDown. Check out:SmartThingsPublic/devicetypes/smartthings/zigbee-thermostat.src/zigbee-thermostat.groovy
Line 42 in f6f0b3f
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It works well, but when I try to change thermostat mode, I see only one (heat) on the list, despite all of them being sent as an event (and all of them are listed in IDE in supportedThermostatModes list).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
any idea on this @dkirker ? does the metadata only specify one mode, maybe?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Make sure the payload for
supportedThermostatModesis encoded as JSON.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, that've worked.