Created
January 31, 2017 20:50
-
-
Save bjensen/c93af58739b083a6ad295c7ef00aab4f to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* | |
* | |
* 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: "Danfoss Living Connect LC-13 Thermostat", namespace: "BechmannCom", author: "René Bechmann") | |
{ | |
// This handler has commands | |
capability "Actuator" | |
// This handler has attributes | |
capability "Sensor" | |
// Thermostat capability | |
capability "Thermostat" | |
command "setHeatingSetpoint" | |
command "setCoolingSetpoint" | |
command "off" | |
command "heat" | |
command "emergencyHeat" | |
command "cool" | |
command "setThermostatMode" | |
command "fanOn" | |
command "fanAuto" | |
command "fanCirculate" | |
command "setThermostatFanMode" | |
command "auto" | |
attribute "temperature", "string" | |
attribute "heatingSetpoint", "string" | |
attribute "coolingSetpoint", "string" | |
attribute "thermostatSetpoint", "string" | |
attribute "thermostatMode", "string" | |
attribute "thermostatFanMode", "string" | |
attribute "thermostatOperatingState", "string" | |
// Battery capability | |
capability "Battery" | |
attribute "battery", "string" | |
// Custom attribute | |
attribute "protection", "string" | |
// Danfoss thermostat fingerprint "NQ-500-EU" | |
// https://store.northq.com/products/danfoss-living-connect-nq-500-eu | |
// https://cdn.shopify.com/s/files/1/1238/1276/files/Danfoss_Living_Connect_Specifications.pdf?583524900071384087 | |
// Raw device data: zw:S type:0804 mfr:0002 prod:0005 model:0003 ver:2.51 zwv:2.67 lib:06 cc:80,46,81,72,8F,75,43,86,84 ccOut:46,81,8F | |
// http://products.z-wavealliance.org/products/932 | |
// Supported classes: http://products.z-wavealliance.org/products/932/classes | |
// Product web site: Product Website: http://heating.consumers.danfoss.com/xxTypex/585379.html | |
fingerprint type: "0804", mfr: "0002", prod: "0005", model: "0003", cc: "80,46,81,72,8F,75,43,86,84", ccOut:"46,81,8F" | |
//fingerprint deviceId: "0x0804" | |
//fingerprint inClusters: "0x80, 0x46, 0x81, 0x72, 0x8F, 0x75, 0x43, 0x86, 0x84" | |
} | |
simulator {} | |
tiles (scale: 2) | |
{ | |
multiAttributeTile(name:"thermostatFull", type:"thermostat", width:6, height:4, canChangeIcon: true) | |
{ | |
tileAttribute("device.heatingSetpoint", key: "PRIMARY_CONTROL") | |
{ | |
attributeState("default", label:'${currentValue}°', unit:"", | |
backgroundColors:[ | |
[value: 0, color: "#ededed"], | |
[value: 4, color: "#153591"], | |
[value: 16, color: "#178998"], | |
[value: 18, color: "#199f5c"], | |
[value: 20, color: "#2da71c"], | |
[value: 21, color: "#5baa1d"], | |
[value: 22, color: "#8aae1e"], | |
[value: 23, color: "#b1a81f"], | |
[value: 24, color: "#b57d20"], | |
[value: 26, color: "#b85122"], | |
[value: 28, color: "#bc2323"]]) | |
} | |
tileAttribute("device.nextHeatingSetpoint", key: "SECONDARY_CONTROL") | |
{ | |
attributeState("default", label:'${currentValue}° next', unit:"") | |
} | |
} | |
controlTile("nextHeatingSetpointSlider", "device.nextHeatingSetpoint", "slider", height: 1, width: 6, inactiveLabel: false, range:"(4..28)") | |
{ | |
state "default", action: "setHeatingSetpoint", backgroundColor:"#d04e00" | |
} | |
valueTile("batteryTile", "device.battery", inactiveLabel: true, decoration: "flat", width: 1, height: 1) | |
{ | |
tileAttribute ("device.battery", key: "PRIMARY_CONTROL") | |
{ | |
state "default", label:'${currentValue}% battery', unit:"%" | |
} | |
} | |
valueTile("protectionTile", "device.protection", inactiveLabel: true, decoration: "flat", width: 1, height: 1) | |
{ | |
tileAttribute ("device.protection", key: "PRIMARY_CONTROL") | |
{ | |
state "", label:'${currentValue}', defaultState: true | |
} | |
} | |
main "thermostatFull" | |
details(["thermostatFull", "nextHeatingSetpointSlider", "batteryTile", "protectionTile"]) | |
} | |
preferences | |
{ | |
input "wakeUpInterval", "number", title: "Wake up interval", description: "Seconds until next wake up notification", range: "60..3600", displayDuringSetup: true | |
input "isProtected", "bool", title: "Child protection", description: "Disable the buttons on the thermostat", displayDuringSetup: true | |
} | |
} | |
def parse(String description) | |
{ | |
def results = null | |
try | |
{ | |
//log.debug "RAW: $description" | |
def cmd = zwave.parse(description) //(description,[0x80: 1, 0x46: 1, 0x81: 1, 0x72: 2, 0x8F: 1, 0x75: 2, 0x43: 2, 0x86: 1, 0x84: 2]) | |
if (cmd != null) | |
{ | |
//log.debug "CMD: $cmd" | |
results = zwaveEvent(cmd) | |
} | |
if ((state.thermostatModeSet ?: "false") == "false") | |
{ | |
heat() | |
} | |
} | |
catch (Exception ex) | |
{ | |
log.error "$ex" | |
} | |
return results | |
} | |
def zwaveEvent(physicalgraph.zwave.commands.protectionv2.ProtectionReport cmd) | |
{ | |
def eventList = [] | |
try | |
{ | |
def pretectionState = (state.localProtectionState ?: "") | |
if (cmd.localProtectionState != pretectionState) | |
{ | |
if (cmd.localProtectionState == physicalgraph.zwave.commands.protectionv2.ProtectionReport.PROTECTION_STATE_UNPROTECTED) | |
{ | |
eventList << createEvent(descriptionText: "Device reports thermostat unlocked", isStateChange: true) | |
log.debug("Device reports unlocked thermostat (new)") | |
eventList << createEvent(name:"protection", value: "disabled", displayed: false) | |
isProtected = false; | |
} | |
else if (cmd.localProtectionState == physicalgraph.zwave.commands.protectionv2.ProtectionReport.PROTECTION_STATE_NO_OPERATION_POSSIBLE) | |
{ | |
eventList << createEvent(descriptionText: "Device reports thermostat locked", isStateChange: true) | |
log.debug("Device reports locked thermostat (new)") | |
eventList << createEvent(name:"protection", value: "enabled", displayed: false) | |
isProtected = true; | |
} | |
else | |
{ | |
eventList << createEvent(name:"protection", value: "", displayed: false) | |
} | |
state.battery = cmd.localProtectionState | |
} | |
else | |
{ | |
if (cmd.localProtectionState == physicalgraph.zwave.commands.protectionv2.ProtectionReport.PROTECTION_STATE_UNPROTECTED) | |
{ | |
log.debug("Device reports unlocked thermostat (unchanged)") | |
} | |
else if (cmd.localProtectionState == physicalgraph.zwave.commands.protectionv2.ProtectionReport.PROTECTION_STATE_NO_OPERATION_POSSIBLE) | |
{ | |
log.debug("Device reports locked thermostat (unchanged)") | |
} | |
} | |
state.lastbatt = new Date().time | |
} | |
catch (Exception ex) | |
{ | |
log.error "$ex" | |
} | |
eventList | |
} | |
def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) | |
{ | |
def eventList = [] | |
try | |
{ | |
def battery = (state.battery ?: -1) | |
if (cmd.batteryLevel != battery) | |
{ | |
if (cmd.batteryLevel == 0xFF) | |
{ | |
eventList << createEvent(descriptionText: "Device reports low battery", isStateChange: true) | |
log.debug("Device reports low battery (new)") | |
eventList << createEvent(name:"battery", value: 1, unit: "%", displayed: false) | |
} | |
else | |
{ | |
eventList << createEvent(descriptionText: "Device reports battery at ${cmd.batteryLevel}%", isStateChange: true) | |
log.debug("Device reports battery at ${cmd.batteryLevel}% (new)") | |
eventList << createEvent(name:"battery", value: cmd.batteryLevel, unit: "%", displayed: false) | |
} | |
state.battery = cmd.batteryLevel | |
} | |
else | |
{ | |
if (cmd.batteryLevel == 0xFF) | |
{ | |
log.debug("Device reports low battery (unchanged)") | |
} | |
else | |
{ | |
log.debug("Device reports battery at ${cmd.batteryLevel}% (unchanged)") | |
} | |
} | |
state.lastbatt = new Date().time | |
} | |
catch (Exception ex) | |
{ | |
log.error "$ex" | |
} | |
eventList | |
} | |
def zwaveEvent(physicalgraph.zwave.commands.thermostatsetpointv2.ThermostatSetpointReport cmd) | |
{ | |
def eventList = [] | |
try | |
{ | |
def heatingSetpoint = (state.heatingSetpoint ?: "") | |
def value = convertTemperatureIfNeeded(cmd.scaledValue, (cmd.scale == 1 ? "F" : "C"), cmd.precision) | |
value = Double.parseDouble(value).toString() - ".0" | |
if (heatingSetpoint != value) | |
{ | |
eventList << createEvent(name:"heatingSetpoint", value: value, unit: getTemperatureScale(), isStateChange: true, descriptionText: "Device reports thermostat at ${value}°") | |
log.debug("Device reports thermostat at ${value}° (new)") | |
state.heatingSetpoint = value; | |
state.size = cmd.size | |
state.scale = cmd.scale | |
state.precision = cmd.precision | |
} | |
else | |
{ | |
log.debug("Device reports thermostat at ${value}° (unchanged)") | |
} | |
} | |
catch (Exception ex) | |
{ | |
log.error "$ex" | |
} | |
eventList | |
} | |
def zwaveEvent(physicalgraph.zwave.commands.wakeupv2.WakeUpNotification cmd) | |
{ | |
def eventList = [] | |
try | |
{ | |
def delay = 400 | |
def interval = (wakeUpInterval ?: 600) | |
// This try catch block ensures that we allways send the wakeUpNoMoreInformation command | |
try | |
{ | |
log.debug("WakeUpNotification received. Send delay: $delay, Wake Up Interval: $interval") | |
def heatingSetpoint = (state.heatingSetpoint ?: "") | |
def nextHeatingSetpoint = (state.nextHeatingSetpoint ?: "") | |
if (nextHeatingSetpoint != "") | |
{ | |
if (heatingSetpoint != nextHeatingSetpoint) | |
{ | |
log.debug("Device is set to ${nextHeatingSetpoint}°") | |
eventList << response(zwave.thermostatSetpointV2.thermostatSetpointSet(setpointType: 1, scale: 0, precision: 1, scaledValue: new BigDecimal(nextHeatingSetpoint)).format()) | |
eventList << response("delay $delay") | |
eventList << response(zwave.thermostatSetpointV2.thermostatSetpointGet(setpointType: 1).format()) | |
eventList << response("delay $delay") | |
} | |
state.nextHeatingSetpoint = "" | |
} | |
else if (heatingSetpoint == "") | |
{ | |
log.debug("Requesting thermostat set point") | |
eventList << response(zwave.thermostatSetpointV2.thermostatSetpointGet(setpointType: 1).format()) | |
eventList << response("delay $delay") | |
} | |
if ((state.wakeUpIntervalConfigured ?: "false") == "false" || | |
(state.currentWakeUpInterval ?: 0) != interval) | |
{ | |
log.debug("Wake up Interval set to ${interval} seconds") | |
eventList << response(zwave.wakeUpV1.wakeUpIntervalSet(seconds:interval, nodeid:zwaveHubNodeId).format()) | |
eventList << response("delay $delay") | |
state.currentWakeUpInterval = interval | |
state.wakeUpIntervalConfigured = "true" | |
} | |
def battery = (state.battery ?: "") | |
if (battery == "") | |
{ | |
log.debug("Requesting batery level") | |
eventList << response(zwave.batteryV1.batteryGet()) | |
eventList << response("delay $delay") | |
} | |
// Set clock once a day | |
def nowTime = new Date().time | |
def ageInMinutes = state.lastClockSet ? (int)(nowTime - state.lastClockSet)/60000 : 1440 | |
if (ageInMinutes >= 1440) | |
{ | |
def nowCal = Calendar.getInstance(location.timeZone) // get current location timezone | |
def weekday = nowCal.get(Calendar.DAY_OF_WEEK) | |
def hour = nowCal.get(Calendar.HOUR_OF_DAY) | |
def minute = nowCal.get(Calendar.MINUTE) | |
log.debug "Setting clock to weekday:$weekday hour:$hour minute:$minute" | |
eventList << response(zwave.clockV1.clockSet(hour: hour, minute: minute, weekday: weekday).format()) | |
eventList << response("delay $delay") | |
state.lastClockSet = nowTime | |
state.clock = "" | |
} | |
ageInMinutes = 0 // Clock poll every 5 minutes disabled because we are not using it currently | |
//ageInMinutes = state.lastClockGet ? (int)(nowTime - state.lastClockGet)/60000 : 5 | |
def clock = state.clock ?: "" | |
if (clock == "" || | |
ageInMinutes >= 5) | |
{ | |
eventList << response(zwave.clockV1.clockGet().format()) | |
eventList << response("delay $delay") | |
state.lastClockGet = nowTime | |
} | |
def protection = (isProtected ?: false) | |
if ((state.protectionConfigured ?: "false") == "false") | |
{ | |
log.debug("Requesting protection state") | |
eventList << response(protectionGet()) | |
eventList << response("delay $delay") | |
state.protectionConfigured = "true" | |
} | |
if ((state.isProtected ?: false) != protection) | |
{ | |
log.debug("Protection set to ${protection}") | |
eventList << response(protectionSet(protection)) | |
eventList << response("delay $delay") | |
eventList << response(protectionGet()) | |
eventList << response("delay $delay") | |
state.isProtected = protection | |
} | |
// eventList << response(protectionEcGet()) | |
// eventList << response(protectionTimeoutGet()) | |
// for (def i = 1; i <= 1; i++) | |
// { | |
// eventList << response(scheduleGet(i)) | |
// eventList << response("delay $delay") | |
// } | |
// eventList << response(scheduleChangedGet()) | |
} | |
catch (Exception ex) | |
{ | |
log.error "Exception in WakeUpNotification: $ex" | |
} | |
eventList << response(zwave.wakeUpV1.wakeUpNoMoreInformation().format()) | |
} | |
catch (Exception ex) | |
{ | |
log.error "Exception in WakeUpNotification: $ex" | |
} | |
eventList | |
} | |
def scheduleGet(weekday) | |
{ | |
def rc = "" | |
if (weekday >= 1 && weekday <= 7) | |
{ | |
rc = "4602" + String.format("%02X", weekday) | |
} | |
return rc | |
} | |
def protectionGet() | |
{ | |
return "7502" | |
} | |
def scheduleChangedGet() | |
{ | |
return "4604" | |
} | |
def scheduleOverrideGet() | |
{ | |
return "4607" | |
} | |
def scheduleOverrideSet(overrideState, setback) | |
{ | |
def rc = "" | |
if (([0,1,2,121,122]).contains(overrideState)) | |
{ | |
rc = "4606" + String.format("%02X", overrideState) + String.format("%02X", setback) | |
} | |
return rc | |
} | |
def protectionSet(protect) | |
{ | |
def rc = "" | |
rc = "7501" + (protect == false? "00": "02") + "00" | |
return rc | |
} | |
def zwaveEvent(physicalgraph.zwave.commands.climatecontrolschedulev1.ScheduleOverrideReport cmd) | |
{ | |
try | |
{ | |
switch(cmd.overrideState) | |
{ | |
case 0: | |
log.debug "ScheduleOverrideReport: No override" | |
break | |
case 1: | |
log.debug "ScheduleOverrideReport: Temporary override" | |
break | |
case 2: | |
log.debug "ScheduleOverrideReport: Permanent override" | |
break | |
case 121: | |
log.debug "ScheduleOverrideReport: Frost protection" | |
break | |
case 122: | |
log.debug "ScheduleOverrideReport: Energy saving mode" | |
break | |
case 127: | |
log.debug "ScheduleOverrideReport: Unused" | |
break | |
default: | |
log.debug "ScheduleOverrideReport: Unknown" | |
break | |
} | |
} | |
catch (Exception ex) | |
{ | |
log.error "$ex" | |
} | |
} | |
def zwaveEvent(physicalgraph.zwave.commands.climatecontrolschedulev1.ScheduleChangedReport cmd) | |
{ | |
try | |
{ | |
log.debug "ScheduleChangedReport: $cmd" | |
} | |
catch (Exception ex) | |
{ | |
log.error "$ex" | |
} | |
} | |
def weekDay(dayOfWeek) | |
{ | |
def rc = "" | |
def weekDays = ["Monday","Tueday","Wednesday","Thursday","Friday","Saturday","Sunday"] | |
if (dayOfWeek >= 1 && dayOfWeek <=7) | |
{ | |
rc = weekDays[dayOfWeek-1] | |
} | |
return rc | |
} | |
byte[] toBytes(int i) | |
{ | |
byte[] result = new byte[4]; | |
result[0] = (byte) (i >> 24); | |
result[1] = (byte) (i >> 16); | |
result[2] = (byte) (i >> 8); | |
result[3] = (byte) (i /*>> 0*/); | |
return result; | |
} | |
def parseSwitchPoint(switchPoint) | |
{ | |
def byteArray = toBytes(switchPoint) | |
return [extra: byteArray[0], hour: byteArray[1], minute: byteArray[2], scheduleState: byteArray[3]] | |
} | |
double adjustSetPoint(double setPoint) | |
{ | |
double whole = (int)setPoint | |
double remainder = setPoint - whole | |
double adjust = 0.0 | |
if (remainder < 0.25) | |
{ | |
adjust = 0.0; | |
} | |
else if (remainder >= 0.25 && remainder < 0.75) | |
{ | |
adjust = 0.5; | |
} | |
else if (remainder >= 0.75) | |
{ | |
adjust = 1.0 | |
} | |
return whole + adjust | |
} | |
def zwaveEvent(physicalgraph.zwave.commands.climatecontrolschedulev1.ScheduleReport cmd) | |
{ | |
try | |
{ | |
def weekDay = weekDay(cmd.weekday) | |
def switchPoints = [] | |
switchPoints << parseSwitchPoint(cmd.switchpoint1) | |
switchPoints << parseSwitchPoint(cmd.switchpoint2) | |
switchPoints << parseSwitchPoint(cmd.switchpoint3) | |
switchPoints << parseSwitchPoint(cmd.switchpoint4) | |
switchPoints << parseSwitchPoint(cmd.switchpoint5) | |
switchPoints << parseSwitchPoint(cmd.switchpoint6) | |
switchPoints << parseSwitchPoint(cmd.switchpoint7) | |
switchPoints << parseSwitchPoint(cmd.switchpoint8) | |
switchPoints[0].hour = 18 | |
switchPoints[0].minute = 30 | |
switchPoints[0].scheduleState = 33 | |
def schedules = [] | |
for(def switchPoint in switchPoints) | |
{ | |
if (switchPoint.scheduleState != 127) | |
{ | |
def setBack = 0.0 | |
def frostProtection = false | |
def energySaving = false | |
if (switchPoint.scheduleState >= -128 && switchPoint.scheduleState <= 120) | |
{ | |
setBack = switchPoint.scheduleState / 10.0 | |
} | |
else if (switchPoint.scheduleState == 121) | |
{ | |
frostProtection = true | |
} | |
else if (switchPoint.scheduleState == 121) | |
{ | |
energySaving = true | |
} | |
schedules << [hour: switchPoint.hour, minute: switchPoint.minute, setBack: adjustSetPoint(setBack), frostProtection: frostProtection, energySaving: energySaving] | |
} | |
else | |
{ | |
break | |
} | |
} | |
log.debug "Device schedule received: schedules=${schedules}, weekday=${weekDay}" | |
} | |
catch (Exception ex) | |
{ | |
log.error "$ex" | |
} | |
} | |
def zwaveEvent(physicalgraph.zwave.commands.clockv1.ClockReport cmd) | |
{ | |
try | |
{ | |
log.debug "Device clock received: weekday:${cmd.weekday} hour:${cmd.hour} minute:${cmd.minute}" | |
state.clock = "${cmd.weekday},${cmd.hour}:${cmd.minute}" | |
} | |
catch (Exception ex) | |
{ | |
log.error "$ex" | |
} | |
} | |
def zwaveEvent(physicalgraph.zwave.Command cmd) | |
{ | |
try | |
{ | |
log.debug "Catch all: $cmd" | |
} | |
catch (Exception ex) | |
{ | |
log.error "$ex" | |
} | |
} | |
def setHeatingSetpoint(degrees) | |
{ | |
try | |
{ | |
setSetpoint(degrees) | |
} | |
catch (Exception ex) | |
{ | |
log.error "$ex" | |
} | |
} | |
def setSetpoint(degrees) | |
{ | |
try | |
{ | |
def deviceScale = (state.scale ?: 2) | |
def deviceScaleString = (deviceScale == 2 ? "C" : "F") | |
def locationScale = getTemperatureScale() | |
def p = (state.precision ?: 1) | |
Double convertedDegrees = degrees | |
if (locationScale == "C" && | |
deviceScaleString == "F") | |
{ | |
convertedDegrees = celsiusToFahrenheit(degrees) | |
} | |
else if (locationScale == "F" && | |
deviceScaleString == "C") | |
{ | |
convertedDegrees = fahrenheitToCelsius(degrees) | |
} | |
convertedDegrees = Math.round(convertedDegrees * 10) / 10 | |
if (convertedDegrees >= 4.0 && convertedDegrees <= 28.0) | |
{ | |
def value = convertedDegrees.toString() - ".0" | |
sendEvent(name:"nextHeatingSetpoint", value: value, displayed: false , isStateChange: true) | |
log.debug ("Setting device to ${value}° on next wake up") | |
state.nextHeatingSetpoint = value | |
} | |
} | |
catch (Exception ex) | |
{ | |
log.error "$ex" | |
} | |
} | |
def setCoolingSetpoint(degrees) | |
{ | |
try | |
{ | |
setSetpoint(degrees) | |
} | |
catch (Exception ex) | |
{ | |
log.error "$ex" | |
} | |
} | |
def off() | |
{ | |
try | |
{ | |
setSetpoint(4.0) | |
} | |
catch (Exception ex) | |
{ | |
log.error "$ex" | |
} | |
} | |
def heat() | |
{ | |
try | |
{ | |
setThermostatMode("heat") | |
} | |
catch (Exception ex) | |
{ | |
log.error "$ex" | |
} | |
} | |
def emergencyHeat() | |
{ | |
try | |
{ | |
setSetpoint(28.0) | |
} | |
catch (Exception ex) | |
{ | |
log.error "$ex" | |
} | |
} | |
def cool() | |
{ | |
try | |
{ | |
setThermostatMode("heat") | |
} | |
catch (Exception ex) | |
{ | |
log.error "$ex" | |
} | |
} | |
def setThermostatMode(mode) | |
{ | |
try | |
{ | |
sendEvent(name:"thermostatMode", value: mode, displayed: false) | |
state.thermostatModeSet = "true" | |
log.debug("Thermostat mode is set to '${mode}'") | |
} | |
catch (Exception ex) | |
{ | |
log.error "$ex" | |
} | |
} | |
def fanOn() | |
{ | |
// Not implemented | |
} | |
def fanAuto() | |
{ | |
// Not implemented | |
} | |
def fanCirculate() | |
{ | |
// Not implemented | |
} | |
def setThermostatFanMode(string) | |
{ | |
// Not implemented | |
} | |
def auto() | |
{ | |
// Not implemented | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment