Skip to content
Merged
Changes from all commits
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
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) {
Copy link
Contributor

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 switchMode and setThermostatSetpointUp and setThermostatSetpointDown. Check out:

Copy link
Contributor Author

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).

Copy link
Contributor

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?

Copy link
Contributor

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 supportedThermostatModes is encoded as JSON.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yep, that've worked.

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) {
Copy link
Contributor

Choose a reason for hiding this comment

The 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)

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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

Copy link
Contributor

Choose a reason for hiding this comment

The 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 isEverspringRadiatorThermostat to help set it, but not assume the scale to be "C". If someone dumps a new fingerprint in this DTH and isn't diligent they will hit a bug here.

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) {
Copy link
Contributor

Choose a reason for hiding this comment

The 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:

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) {
Copy link
Contributor

Choose a reason for hiding this comment

The 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])),
Copy link
Contributor

Choose a reason for hiding this comment

The 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...)

Copy link
Contributor

Choose a reason for hiding this comment

The 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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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"
}