/** Copyright Sinopé Technologies 1.0.1 SVN-435 * 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. **/ preferences { input("zipcode", "text", title: "ZipCode", description: "Enter your ZipCode for setting outdoor Temp") input("BacklightAutoDimParam", "enum", title:"Backlight setting (default: blank)", description: "On Demand or Sensing", options: ["On Demand", "Sensing"], multiple: false, required: false) input("trace", "bool", title: "Trace", description:"Set it to true to enable tracing") input("logFilter", "number", title: "(1=ERROR only,2=<1+WARNING>,3=<2+INFO>,4=<3+DEBUG>,5=<4+TRACE>)", range: "1..5", description: "optional") } metadata { definition(name: "TH1123ZB-TH1124ZB Sinope Thermostat", namespace: "Sinope Technologies", author: "Sinope Technologies") { capability "thermostatHeatingSetpoint" capability "thermostatMode" capability "thermostatOperatingState" capability "thermostatSetpoint" capability "Actuator" capability "Temperature Measurement" capability "Thermostat" capability "Configuration" capability "Refresh" capability "Sensor" attribute "temperatureDisplayMode", "enum", ["Deg_C", "Deg_F"] attribute "occupancyStatus", "enum", ["unoccupy", "occupy"] attribute "outdoorTemp", "string" attribute "heatingSetpointRangeHigh", "string" attribute "heatingSetpointRangeLow", "string" attribute "heatingSetpointRange", "VECTOR3" attribute "verboseTrace", "string" attribute "presence", "enum", ["present", "non present"] attribute "keypadLockStatus", "enum", ["unlock", "lock1"] // attribute "keypadLockStatus" ,"enum", ["unlock", "lock1","lock2","lock3","lock4","lock5"] command "heatLevelUp" command "heatLevelDown" command "setThermostatMode" command "switchMode" command "setTemperatureDisplayMode" command "setOccupancyStatus" command "setThermostatSetpoint", ["number"] command "setHeatingSetpointRangeHigh" command "setHeatingSetpointRangeLow" command "setHeatingSetpointRange" command "unoccupy" command "occupy" command "present" command "away" command "unlock" command "lock1" command "setLockStatus" fingerprint endpoint: "1", profileId: "0104", inClusters: "0000,0003,0004,0005,0201,0204,0402,0B04,0B05", outClusters: "0019" fingerprint endpoint: "1", profileId: "0104", inClusters: "0000,0003,0004,0005,0201,0204,0402,0B04,0B05", outClusters: "0019" // fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0201,0204,0402,0B04,0B05", manufacturer: "Sinope Technologies", model: "TH1123ZB" // fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0201,0204,0402,0B04,0B05", manufacturer: "Sinope Technologies", model: "TH1124ZB" } //-------------------------------------------------------------------------------------------------------- simulator {} //-------------------------------------------------------------------------------------------------------- tiles(scale: 2) { multiAttributeTile(name: "thermostatMulti", type: "thermostat", width: 6, height: 4, canChangeIcon: true) { tileAttribute("device.temperature", key: "PRIMARY_CONTROL") { attributeState("default", label: '${currentValue}', unit: "dF", backgroundColor: "#269bd2") } tileAttribute("device.heatingSetpoint", key: "VALUE_CONTROL") { attributeState("VALUE_UP", action: "heatLevelUp") attributeState("VALUE_DOWN", action: "heatLevelDown") } tileAttribute("device.heatingDemand", key: "SECONDARY_CONTROL") { attributeState("default", label: '${currentValue}%', unit: "%") } tileAttribute("device.thermostatOperatingState", key: "OPERATING_STATE") { attributeState("idle", backgroundColor: "#44b621") attributeState("heating", backgroundColor: "#ffa81e") attributeState("cooling", backgroundColor: "#269bd2") } tileAttribute("device.thermostatMode", key: "THERMOSTAT_MODE") { attributeState("off", label: '${name}') attributeState("heat", label: '${name}') } tileAttribute("device.heatingSetpoint", key: "HEATING_SETPOINT") { attributeState("default", label: '${currentValue}', unit: "dF") } } //-- Value Tiles ------------------------------------------------------------------------------------------- valueTile("temperature", "device.temperature", width: 4, height: 2) { state("temperature", label: '${currentValue}°', backgroundColors: getBackgroundColors() ) } valueTile("heatingDemand", "device.heatingDemand", inactiveLabel: false, width: 2, height: 2, decoration: "flat") { state "heatingDemand", label: '${currentValue}%', unit: "%", backgroundColor: "#ffffff" } //-- Standard Tiles ---------------------------------------------------------------------------------------- valueTile("heatingSetpoint", "device.heatingSetpoint", inactiveLabel: false, width: 2, height: 2, decoration: "flat") { state "heat", label: '${currentValue}°', backgroundColor: "#ffffff" } standardTile("heatLevelUp", "device.heatingSetpoint", inactiveLabel: false, width: 2, height: 2, decoration: "flat") { state "default", label: '', action: "heatLevelUp", icon: "st.thermostat.thermostat-up" } standardTile("heatLevelDown", "device.heatingSetpoint", inactiveLabel: false, width: 2, height: 2, decoration: "flat") { state "default", label: '', action: "heatLevelDown", icon: "st.thermostat.thermostat-down" } standardTile("operatingState", "device.thermostatOperatingState", width: 2, height: 2) { state "idle", label: '${name}', backgroundColor: "#ffffff" state "heating", label: '${name}', backgroundColor: "#e86d13" state "cooling", label: '${name}', backgroundColor: "#00A0DC" } standardTile("thermostatMode", "device.thermostatMode", inactiveLabel: false, height: 2, width: 2, decoration: "flat") { state "off", label: '', action: "switchMode", icon: "st.thermostat.heating-cooling-off" state "heat", label: '', action: "switchMode", icon: "st.thermostat.heat", defaultState: true } standardTile("temperatureDisplayMode", "device.temperatureDisplayMode", inactiveLabel: false, height: 2, width: 2, decoration: "flat") { state "Deg_C", label: '${name}', action: "setTemperatureDisplayMode", icon: "st.alarm.temperature.normal", defaultState: true state "Deg_F", label: '${name}', action: "setTemperatureDisplayMode", icon: "st.alarm.temperature.normal" } standardTile("occupancyStatus", "device.occupancyStatus", inactiveLabel: false, height: 2, width: 2, decoration: "flat") { state "occupy", label: '${name}', action: "setOccupancyStatus", icon: "st.Home.home4", defaultState: true state "unoccupy", label: '${name}', action: "setOccupancyStatus", icon: "st.presence.car.car" } standardTile("lockStatus", "device.keypadLockStatus", inactiveLabel: false, height: 2, width: 2, decoration: "flat") { state "unlock", label: '${name}', action: "setLockStatus", icon: "st.presence.house.unlocked", defaultState: true state "lock1", label: '${name}', action: "setLockStatus", icon: "st.presence.house.secured" } standardTile("refresh", "device.temperature", inactiveLabel: false, width: 2, height: 2, decoration: "flat") { state "default", action: "refresh.refresh", icon: "st.secondary.refresh" } standardTile("weatherTemperature", "device.outdoorTemp", inactiveLabel: false, width: 2, height: 2, decoration: "flat", canChangeIcon: false) { state "default", label: 'OutdoorTemp ${currentValue}°', unit: "dF", icon: "st.Weather.weather2", backgroundColor: "#ffffff" } standardTile("configure", "device.configure", inactiveLabel: false, width: 2, height: 2, decoration: "flat") { state "configure", label: '', action: "configuration.configure", icon: "st.secondary.configure" } //-- Main & Details ---------------------------------------------------------------------------------------- main("thermostatMulti") details(["thermostatMulti", // "heatLevelUp","heatingSetpoint","heatLevelDown", "thermostatMode", // "occupancyStatus", // "lockStatus", // "temperatureDisplayMode", // "configure", "refresh" ]) } } def getBackgroundColors() { def results if (state?.scale == 'C') { // Celsius Color Range results = [ [value: 0, color: "#153591"], [value: 7, color: "#1e9cbb"], [value: 15, color: "#90d2a7"], [value: 23, color: "#44b621"], [value: 29, color: "#f1d801"], [value: 35, color: "#d04e00"], [value: 37, color: "#bc2323"] ] } else { results = // Fahrenheit Color Range [ [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"] ] } return results } //-- Installation ---------------------------------------------------------------------------------------- def installed() { traceEvent(settings.logFilter, "installed>Device is now Installed", settings.trace) initialize() //configure('installed') } def updated() { if (!state.updatedLastRanAt || now() >= state.updatedLastRanAt + 5000) { state.updatedLastRanAt = now() traceEvent(settings.logFilter, "updated>Device is now updated", settings.trace) try { unschedule() } catch (e) { traceEvent(settings.logFilter, "updated>exception $e, continue processing", settings.trace, get_LOG_ERROR()) } //initialize() runIn(1,refresh_misc) runEvery15Minutes(refresh_misc) configure('updated') } } void initialize() { state?.scale = getTemperatureScale() runIn(2,refresh) def supportedThermostatModes = ['off', 'heat'] state?.supportedThermostatModes = supportedThermostatModes sendEvent(name: "supportedThermostatModes", value: supportedThermostatModes, displayed: (settings.trace ?: false)) def supportedThermostatFanModes = [] sendEvent(name: "supportedThermostatFanModes", value: supportedThermostatFanModes, displayed: (settings.trace ?: false)) } def ping() { refresh() } def uninstalled() { unschedule() } //-- Parsing --------------------------------------------------------------------------------------------- // parse events into attributes def parse(String description) { def result = [] def scale = getTemperatureScale() state?.scale = scale traceEvent(settings.logFilter, "parse>Description :( $description )", settings.trace) def cluster = zigbee.parse(description) traceEvent(settings.logFilter, "parse>Cluster : $cluster", settings.trace) if (description?.startsWith("read attr -")) { def descMap = zigbee.parseDescriptionAsMap(description) result += createCustomMap(descMap) if(descMap.additionalAttrs){ def mapAdditionnalAttrs = descMap.additionalAttrs mapAdditionnalAttrs.each{add -> traceEvent(settings.logFilter,"parse> mapAdditionnalAttributes : ( ${add} )",settings.trace) add.cluster = descMap.cluster result += createCustomMap(add) } } } traceEvent(settings.logFilter, "Parse returned $result", settings.trace) return result } //-------------------------------------------------------------------------------------------------------- def createCustomMap(descMap){ def result = null def map = [: ] if (descMap.cluster == "0201" && descMap.attrId == "0000") { map.name = "temperature" map.value = getTemperatureValue(descMap.value) traceEvent(settings.logFilter, "parse>ACTUAL TEMP: ${map.value}", settings.trace) } else if (descMap.cluster == "0201" && descMap.attrId == "0008") { map.name = "heatingDemand" map.value = getHeatingDemand(descMap.value) def operatingState = (map.value.toInteger() < 10) ? "idle" : "heating" def isChange = isStateChange(device, "thermostatOperatingState", operatingState) def isDisplayed = isChange sendEvent(name: "thermostatOperatingState", value: operatingState, displayed: isDisplayed) traceEvent(settings.logFilter, "parse>HEATING DEMAND: ${map.value}, operatingState=$operatingState", settings.trace) } else if (descMap.cluster == "0201" && descMap.attrId == "0012") { def currentStatus = device.currentState("occupancyStatus")?.value if (currentStatus == "occupy") { map.name = "heatingSetpoint" map.value = getTemperatureValue(descMap.value, true) def isChange = isStateChange(device, "thermostatSetpoint", map.value.toString()) def isDisplayed = isChange sendEvent(name: "thermostatSetpoint", value: map.value, unit: scale, displayed: isDisplayed) traceEvent(settings.logFilter, "parse>OCCUPY HEATING SETPOINT:${map.value} ", settings.trace) } } else if (descMap.cluster == "0201" && descMap.attrId == "0014") { def currentStatus = device.currentState("occupancyStatus")?.value if (currentStatus == "unoccupy") { map.name = "heatingSetpoint" map.value = getTemperatureValue(descMap.value, true) def isChange = isStateChange(device, "thermostatSetpoint", map.value.toString()) def isDisplayed = isChange sendEvent(name: "thermostatSetpoint", value: map.value, unit: scale, displayed: isDisplayed) traceEvent(settings.logFilter, "parse>UNOCCUPY HEATING SETPOINT: ${map.value}", settings.trace) } } else if (descMap.cluster == "0201" && descMap.attrId == "0015") { map.name = "heatingSetpointRangeLow" map.value = getTemperatureValue(descMap.value, true) traceEvent(settings.logFilter, "parse>LOW HeatingSetpoint: ${map.value}", settings.trace) } else if (descMap.cluster == "0201" && descMap.attrId == "0016") { map.name = "heatingSetpointRangeHigh" map.value = getTemperatureValue(descMap.value, true) traceEvent(settings.logFilter, "parse>HIGH HeatingSetpoint: ${map.value}", settings.trace) } else if (descMap.cluster == "0201" && descMap.attrId == "001c") { map.name = "thermostatMode" map.value = getModeMap()[descMap.value] traceEvent(settings.logFilter, "MODE: ${map.value}", settings.trace) } else if (descMap.cluster == "0201" && descMap.attrId == "0002") { map.name = "occupancyStatus" map.value = getOccupancyMap()[descMap.value] traceEvent(settings.logFilter, "parse>OCCUPANCY: ${map.value}", settings.trace) if (map.value == "occupy") { def isChange = isStateChange(device, "presence", "present") def isDisplayed = isChange sendEvent(name: "presence", value: "present", displayed: isDisplayed) } else { def isChange = isStateChange(device, "presence", "non present") def isDisplayed = isChange sendEvent(name: "presence", value: "non present", displayed: isDisplayed) } } else if (descMap.cluster == "0201" && descMap.attrId == "0400") { map.name = "occupancyStatus" map.value = getOccupancyMap()[descMap.value] traceEvent(settings.logFilter, "parse>MF-OCCUPANCY", settings.trace) if (map.value == "occupy") { def isChange = isStateChange(device, "presence", "present") def isDisplayed = isChange sendEvent(name: "presence", value: "present", displayed: isDisplayed) } else { def isChange = isStateChange(device, "presence", "non present") def isDisplayed = isChange sendEvent(name: "presence", value: "non present", displayed: isDisplayed) } } else if (descMap.cluster == "0204" && descMap.attrId == "0000") { map.name = "temperatureDisplayMode" map.value = getTemperatureDisplayModeMap()[descMap.value] traceEvent(settings.logFilter, "parse>DISPLAY MODE: ${map.value}", settings.trace) } else if (descMap.cluster == "0204" && descMap.attrId == "0001") { map.name = "keypadLockStatus" map.value = getLockMap()[descMap.value] traceEvent(settings.logFilter, "parse>KEYPAD LOCK STATUS: ${map.value}", settings.trace) } if (map) { def isChange = isStateChange(device, map.name, map.value.toString()) map.displayed = isChange if ((map.name.toLowerCase().contains("temp")) || (map.name.toLowerCase().contains("setpoint"))) { map.scale = scale } result = createEvent(map) // sendEvent(map) } return result } //-- Temperature ----------------------------------------------------------------------------------------- def getTemperatureValue(value, doRounding = false) { def scale = state?.scale if (value != null) { double celsius = (Integer.parseInt(value, 16) / 100).toDouble() if (scale == "C") { if (doRounding) { def tempValueString = String.format('%2.1f', celsius) if (tempValueString.matches(".*([.,][456])")) { tempValueString = String.format('%2d.5', celsius.intValue()) traceEvent(settings.logFilter, "getTemperatureValue>value of $tempValueString which ends with 456=> rounded to .5", settings.trace) } else if (tempValueString.matches(".*([.,][789])")) { traceEvent(settings.logFilter, "getTemperatureValue>value of$tempValueString which ends with 789=> rounded to next .0", settings.trace) celsius = celsius.intValue() + 1 tempValueString = String.format('%2d.0', celsius.intValue()) } else { traceEvent(settings.logFilter, "getTemperatureValue>value of $tempValueString which ends with 0123=> rounded to previous .0", settings.trace) tempValueString = String.format('%2d.0', celsius.intValue()) } return tempValueString.toDouble().round(1) } else { return celsius.round(1) } } else { return Math.round(celsiusToFahrenheit(celsius)) } } } //-- Heating Demand -------------------------------------------------------------------------------------- def getHeatingDemand(value) { if (value != null) { def demand = Integer.parseInt(value, 16) return demand.toString() } } //-- Heating Setpoint ------------------------------------------------------------------------------------ def heatLevelUp() { def scale = state?.scale def heatingSetpointRangeHigh double nextLevel try { heatingSetpointRangeHigh = device.latestValue("heatingSetpointRangeHigh") } catch (any) { traceEvent(settings.logFilter, "heatLevelUp>not able to get heatingSetpointRangeLow ($heatingSetpointRangeLow),using default value", settings.trace, get_LOG_WARN()) } heatingSetpointRangeHigh = (heatingSetpointRangeHigh) ?: (scale == 'C') ? 10.0 : 50 if (scale == 'C') { nextLevel = device.currentValue("heatingSetpoint").toDouble() nextLevel = (nextLevel + 0.5).round(1) if (nextLevel > heatingSetpointRangeHigh.toDouble().round(1)) { nextLevel = heatingSetpointRangeHigh.toDouble().round(1) } setHeatingSetpoint(nextLevel) } else { nextLevel = device.currentValue("heatingSetpoint") nextLevel = (nextLevel + 1) if (nextLevel > heatingSetpointRangeHigh.toDouble()) { nextLevel = heatingSetpointRangeHigh.toDouble() } setHeatingSetpoint(nextLevel.intValue()) } } def heatLevelDown() { def scale = state?.scale def heatingSetpointRangeLow double nextLevel try { heatingSetpointRangeLow = device.latestValue("heatingSetpointRangeLow") } catch (any) { traceEvent(settings.logFilter, "heatLevelDown>not able to get heatingSetpointRangeLow ($heatingSetpointRangeLow),using default value", settings.trace, get_LOG_WARN()) } heatingSetpointRangeLow = (heatingSetpointRangeLow) ?: (scale == 'C') ? 10.0 : 50 if (scale == 'C') { nextLevel = device.currentValue("heatingSetpoint").toDouble() nextLevel = (nextLevel - 0.5).round(1) if (nextLevel < heatingSetpointRangeLow.toDouble().round(1)) { nextLevel = heatingSetpointRangeLow.toDouble().round(1) } setHeatingSetpoint(nextLevel) } else { nextLevel = device.currentValue("heatingSetpoint") nextLevel = (nextLevel - 1) if (nextLevel < heatingSetpointRangeLow.toDouble()) { nextLevel = heatingSetpointRangeLow.toDouble() } setHeatingSetpoint(nextLevel.intValue()) } } void setThermostatSetpoint(temp) { setHeatingSetpoint(temp) } def setHeatingSetpoint(degrees) { def scale = state?.scale def degreesDouble = degrees as Double String tempValueString if (scale == "C") { tempValueString = String.format('%2.1f', degreesDouble) } else { tempValueString = String.format('%2d', degreesDouble.intValue()) } sendEvent("name": "heatingSetpoint", "value": tempValueString, displayed: true) sendEvent("name": "thermostatSetpoint", "value": tempValueString, displayed: true) traceEvent(settings.logFilter, "setHeatingSetpoint> new setPoint: $tempValueString", settings.trace) def celsius = (scale == "C") ? degreesDouble : (fahrenheitToCelsius(degreesDouble) as Double).round(1) def cmds = [] cmds += zigbee.writeAttribute(0x201, 0x12, 0x29, hex(celsius * 100)) sendZigbeeCommands(cmds) } def setCoolingSetpoint(temp) { traceEvent(settings.logFilter, "coolingSetpoint is not supported by this thermostat", settings.trace, get_LOG_WARN()) } void setHeatingSetpointRange(rangeArray = []) { if ((!rangeArray) || (rangeArray.size() != 2)) { traceEvent(settings.logFilter, "setHeatingSetpointRange>cannot change the thermostat Range, value ($rangeArray) is null or invalid", settings.trace, get_LOG_WARN(), true) return } def temp_min = rangeArray[0] def temp_max = rangeArray[1] setHeatingSetpointRangeLow(temp_min) setHeatingSetpointRangeHigh(temp_max) } def setHeatingSetpointRangeLow(degrees) { def scale = state?.scale def degreesDouble = degrees as Double traceEvent(settings.logFilter, "setHeatingSetpointRangeLow> new Low: $degreesDouble", settings.trace) def celsius = (scale == "C") ? degreesDouble : (fahrenheitToCelsius(degreesDouble) as Double).round(1) def cmds = [] cmds += zigbee.writeAttribute(0x201, 0x15, 0x29, hex(celsius * 100)) cmds += zigbee.readAttribute(0x0201, 0x0012) cmds += zigbee.readAttribute(0x0201, 0x0015) sendZigbeeCommands(cmds) float temp = degrees.toFloat().round(1) sendEvent(name: 'heatingSetpointRangeLow', value: temp, isStateChange: true, unit: scale) def setpointRangeHigh = device.currentValue('heatingSetpointRangeHigh') def heatingSetpointRange = [temp, setpointRangeHigh] sendEvent(name: "heatingSetpointRange", value: heatingSetpointRange, isStateChange: true, displayed: (settings.trace ?: false)) } def setHeatingSetpointRangeHigh(degrees) { def scale = state?.scale def degreesDouble = degrees as Double traceEvent(settings.logFilter, "setHeatingSetpointRangeHigh> new High: $degreesDouble", settings.trace) def celsius = (scale == "C") ? degreesDouble : (fahrenheitToCelsius(degreesDouble) as Double).round(1) def cmds = [] cmds += zigbee.writeAttribute(0x201, 0x16, 0x29, hex(celsius * 100)) cmds += zigbee.readAttribute(0x0201, 0x0012) cmds += zigbee.readAttribute(0x0201, 0x0016) sendZigbeeCommands(cmds) float temp = degrees.toFloat().round(1) sendEvent(name: 'heatingSetpointRangeHigh', value: temp, isStateChange: true, unit: scale) def setpointRangeLow = device.currentValue('heatingSetpointRangeLow') def heatingSetpointRange = [setpointRangeLow, temp] sendEvent(name: "heatingSetpointRange", value: heatingSetpointRange, isStateChange: true, displayed: (settings.trace ?: false)) } //-- Thermostat and Fan Modes ------------------------------------------------------------------------------------- void off() { setThermostatMode('off') } void auto() { setThermostatMode('auto') } void heat() { setThermostatMode('heat') } void emergencyHeat() { setThermostatMode('heat') } void cool() { setThermostatMode('cool') } void fanOn() { setThermostatFanMode('on') } void fanAuto() { setThermostatFanMode('auto') } void fanOff() { setThermostatFanMode('off') } def fanCirculate() { setThermostatFanMode('circulate') } def modes() { ["mode_off", "mode_heat"] } def getModeMap() { [ "00": "off", "04": "heat" ] } def getSupportedThermostatModes() { if (!state?.supportedThermostatModes) { state?.supportedThermostatModes = (device.currentValue("supportedThermostatModes")) ? device.currentValue("supportedThermostatModes").toString().minus('[').minus(']').tokenize(',') : ['off', 'heat'] } return state?.supportedThermostatModes } def setThermostatFanMode(mode) { traceEvent(settings.logFilter, "setThermostatFanMode to $mode is not supported by this thermostat", settings.trace, get_LOG_WARN()) } def switchMode() { traceEvent(settings.logFilter, "swithMode>begin", settings.trace) def currentMode = "mode_" + 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] traceEvent(settings.logFilter, "swithMode>switching thermostatMode to $next", settings.trace) "$next" () } def setThermostatMode(mode) { traceEvent(settings.logFilter, "setThermostatMode>switching thermostatMode", settings.trace) mode = mode?.toLowerCase() def supportedThermostatModes = getSupportedThermostatModes() if (mode in supportedThermostatModes.toString()) { "mode_$mode" () } else { traceEvent(settings.logFilter, "setThermostatMode to $mode is not supported by this thermostat", settings.trace, get_LOG_WARN()) } } def mode_off() { traceEvent(settings.logFilter, "off>begin", settings.trace) sendEvent("name": "thermostatMode", "value": "off") def cmds = [] cmds += zigbee.writeAttribute(0x0201, 0x001C, 0x30, 0) cmds += zigbee.readAttribute(0x0201, 0x0008) sendZigbeeCommands(cmds) traceEvent(settings.logFilter, "off>end", settings.trace) } def mode_heat() { traceEvent(settings.logFilter, "heat>begin", settings.trace) sendEvent("name": "thermostatMode", "value": "heat") def cmds = [] cmds += zigbee.writeAttribute(0x0201, 0x001C, 0x30, 4) cmds += zigbee.readAttribute(0x0201, 0x0008) sendZigbeeCommands(cmds) traceEvent(settings.logFilter, "heat>end", settings.trace) } //-- Temperature Display Mode ---------------------------------------------------------------------------- def temperatureDisplayModes() { ["Deg_C", "Deg_F"] } def getTemperatureDisplayModeMap() { [ "00": "Deg_C", "01": "Deg_F" ] } def setTemperatureDisplayMode() { traceEvent(settings.logFilter, "setTemperatureDisplayMode>begin", settings.trace) def currentMode = device.currentState("temperatureDisplayMode")?.value def modeOrder = temperatureDisplayModes() def index = modeOrder.indexOf(currentMode) def next = index == 0 ? modeOrder[1] : modeOrder[0] traceEvent(settings.logFilter, "setTemperatureDisplayMode>switching to $next", settings.trace) "$next" () } def Deg_C() { traceEvent(settings.logFilter, "Deg_C>begin", settings.trace) def currentStatus = device.currentState("occupancyStatus")?.value sendEvent("name": "temperatureDisplayMode", "value": "Deg_C") def cmds = [] if (currentStatus == "occupy") { cmds += zigbee.writeAttribute(0x0204, 0x0000, 0x30, 0) cmds += zigbee.readAttribute(0x0201, 0x0000) cmds += zigbee.readAttribute(0x0201, 0x0012) } else { cmds += zigbee.writeAttribute(0x0204, 0x0000, 0x30, 0) cmds += zigbee.readAttribute(0x0201, 0x0000) cmds += zigbee.readAttribute(0x0201, 0x0014) } sendZigbeeCommands(cmds) traceEvent(settings.logFilter, "Deg_C>end", settings.trace) } def Deg_F() { traceEvent(settings.logFilter, "Deg_F>begin", settings.trace) def currentStatus = device.currentState("occupancyStatus")?.value sendEvent("name": "temperatureDisplayMode", "value": "Deg_F") def cmds = [] if (currentStatus == "occupy") { cmds += zigbee.writeAttribute(0x0204, 0x0000, 0x30, 1) cmds += zigbee.readAttribute(0x0201, 0x0000) cmds += zigbee.readAttribute(0x0201, 0x0012) } else { cmds += zigbee.writeAttribute(0x0204, 0x0000, 0x30, 1) cmds += zigbee.readAttribute(0x0201, 0x0000) cmds += zigbee.readAttribute(0x0201, 0x0014) } sendZigbeeCommands(cmds) traceEvent(settings.logFilter, "Deg_F>end", settings.trace) } //-- Occupancy ------------------------------------------------------------------------------------------- def occupancySts() { ["unoccupy", "occupy"] } def getOccupancyMap() { [ "00": "unoccupy", "01": "occupy" ] } def setOccupancyStatus() { traceEvent(settings.logFilter, "setOccupancyStatus>switching occupancy status", settings.trace) def currentStatus = device.currentState("occupancyStatus")?.value traceEvent(settings.logFilter, "setOccupancyStatus>Occupancy :$currentStatus", settings.trace) def statusOrder = occupancySts() def index = statusOrder.indexOf(currentStatus) traceEvent(settings.logFilter, "setOccupancyStatus>Index = $index", settings.trace) def next = (index >= 0 && index < (statusOrder.size() - 1)) ? statusOrder[index + 1] : statusOrder[0] traceEvent(settings.logFilter, "setOccupancyStatus>switching occupancy from $currentStatus to $next", settings.trace) "$next" () } void away() { unoccupy() } void unoccupy() { traceEvent(settings.logFilter, "unoccupy>Set unoccupy", settings.trace) sendEvent(name: "occupancyStatus", value: "unoccupy", displayed: true) sendEvent(name: "presence", value: "non present", displayed: true) state?.previousOccupyTemp = device.currentValue("heatingSetpoint") def cmds = [] cmds += zigbee.writeAttribute(0x0201, 0x0400, 0x30, 0x00, [mfgCode: 0x119C]) cmds += zigbee.readAttribute(0x0201, 0x0002) cmds += zigbee.readAttribute(0x0201, 0x0014) sendZigbeeCommands(cmds) def scale = state?.scale def heatingSetpointRangeLow try { heatingSetpointRangeLow = device.latestValue("heatingSetpointRangeLow") } catch (any) { traceEvent(settings.logFilter, "unoccupy>not able to get heatingSetpointRangeLow ($heatingSetpointRangeLow),using default value", settings.trace, get_LOG_WARN()) } heatingSetpointRangeLow = (heatingSetpointRangeLow) ?: (scale == 'C') ? 10.0 : 50 sendEvent(name: "thermostatSetpoint", value: heatingSetpointRangeLow, unit: scale, displayed: true) sendEvent(name: "heatingSetpoint", value: heatingSetpointRangeLow, unit: scale, displayed: true) } void present() { occupy() } void occupy() { def scale = state?.scale traceEvent(settings.logFilter, "occupy>Set occupy", settings.trace) sendEvent(name: "occupancyStatus", value: "occupy", displayed: true) sendEvent(name: "presence", value: "present", displayed: true) def cmds = [] cmds += zigbee.writeAttribute(0x0201, 0x0400, 0x30, 0x01, [mfgCode: 0x119C]) cmds += zigbee.readAttribute(0x0201, 0x0002) cmds += zigbee.readAttribute(0x0201, 0x0012) sendZigbeeCommands(cmds) def temp = (state?.previousOccupyTemp) ? state ?.previousOccupyTemp : (scale == 'C') ? 20.0 : 70 sendEvent(name: "thermostatSetpoint", value: temp, unit: scale, displayed: true) sendEvent(name: "heatingSetpoint", value: temp, unit: scale, displayed: true) } //-- Keypad Lock ----------------------------------------------------------------------------------------- def keypadLockLevel() { ["unlock", "lock1"] //only those level are used for the moment //["Unlock","Lock1","Lock2","Lock3","Lock4","Lock5"] } def getLockMap() { [ "00": "unlock", "01": "lock1", "02": "lock2", "03": "lock3", "04": "lock4", "05": "lock5" ] } def setLockStatus() { traceEvent(settings.logFilter, "setLockStatus> begin", settings.trace) def currentLockLevel = device.currentValue("keypadLockStatus") traceEvent(settings.logFilter, "setLockStatus>KeypadLock : $currentLockLevel", settings.trace) def lockOrder = keypadLockLevel() def index = lockOrder.indexOf(currentLockLevel) traceEvent(settings.logFilter, "index = $index", settings.trace) def next = (index >= 0 && index < (lockOrder.size() - 1)) ? lockOrder[index + 1] : lockOrder[0] traceEvent(settings.logFilter, "change keypad lock level from $currentLockLevel to $next", settings.trace) "$next" () traceEvent(settings.logFilter, "setLockStatus> end", settings.trace) } def unlock() { traceEvent(settings.logFilter, "unlock>begin", settings.trace) sendEvent("name": "keypadLockStatus", "value": "Unlock", displayed: true) def cmds = [] cmds += zigbee.writeAttribute(0x0204, 0x0001, 0x30, 0x00) sendZigbeeCommands(cmds) traceEvent(settings.logFilter, "unlock>end", settings.trace) } def lock1() { traceEvent(settings.logFilter, "lock1>begin", settings.trace) sendEvent("name": "keypadLockStatus", "value": "lock1", displayed: true) def cmds = [] cmds += zigbee.writeAttribute(0x0204, 0x0001, 0x30, 0x01) sendZigbeeCommands(cmds) traceEvent(settings.logFilter, "lock1>end", settings.trace) } def configure(event = 'appTouch') { /*state?.scale = getTemperatureScale() traceEvent(settings.logFilter, "binding to Thermostat cluster", settings.trace)[ "zdo bind 0x${device.deviceNetworkId} 1 1 0x201 {${device.zigbeeId}} {}", "delay 200", //thermostat cluster //"zdo bind 0x${device.deviceNetworkId} 1 1 0x204 {${device.zigbeeId}} {}" //thermostat user interface cluster //"zdo bind 0x${device.deviceNetworkId} 1 1 0x202 {${device.zigbeeId}} {}" ]*/ //Report for Sinope Locate //zigbee.configureReporting(0x0003, 0x0400, 0x10, 1, 65000, 1) } def refresh() { if (!state.updatedLastRanAt || now() >= state.updatedLastRanAt + 20000) { state.updatedLastRanAt = now() state?.scale = getTemperatureScale() traceEvent(settings.logFilter, "refresh>scale=${state.scale}", settings.trace) def cmds = [] cmds += zigbee.readAttribute(0x0204, 0x0000) // Rd thermostat display mode if (state?.scale == 'C') { cmds += zigbee.writeAttribute(0x0204, 0x0000, 0x30, 0) // Wr °C on thermostat display } else { cmds += zigbee.writeAttribute(0x0204, 0x0000, 0x30, 1) // Wr °F on thermostat display } cmds += zigbee.readAttribute(0x0201, 0x0000) // Rd thermostat Local temperature cmds += zigbee.readAttribute(0x0201, 0x0012) // Rd thermostat Occupied heating setpoint cmds += zigbee.readAttribute(0x0201, 0x0008) // Rd thermostat PI heating demand cmds += zigbee.readAttribute(0x0201, 0x001C) // Rd thermostat System Mode cmds += zigbee.readAttribute(0x0201, 0x0002) // Rd thermostat Occupency cmds += zigbee.readAttribute(0x0204, 0x0001) // Rd thermostat Keypad lockout cmds += zigbee.readAttribute(0x0201, 0x0014) // Rd thermostat Unoccupied heating setpoint cmds += zigbee.readAttribute(0x0201, 0x0015) // Rd thermostat Minimum heating setpoint cmds += zigbee.readAttribute(0x0201, 0x0016) // Rd thermostat Maximum heating setpoint cmds += zigbee.configureReporting(0x0201, 0x0000, 0x29, 19, 301, 50) //local temperature cmds += zigbee.configureReporting(0x0201, 0x0008, 0x0020, 4, 300, 10) //heating demand cmds += zigbee.configureReporting(0x0201, 0x0012, 0x0029, 15, 302, 40) //occupied heating setpoint sendZigbeeCommands(cmds) refresh_misc() } else { traceEvent(settings.logFilter, "updated(): Ran within last 20 seconds so aborting", settings.trace, get_LOG_TRACE()) } } void refresh_misc() { def scale=(state?.scale) ?: getTemperatureScale() def weather = get_weather(settings.zipcode) traceEvent(settings.logFilter,"refresh_misc>begin, scale=$scale, settings.zipcode=${settings.zipcode}, weather=$weather", settings.trace) def cmds=[] if (weather) { double tempValue int outdoorTemp = weather.current_observation.temp_c.toInteger() traceEvent(settings.logFilter,"refresh>outdoorTemp: ${weather.current_observation.temp_c}C",settings.trace) String outdoorTempString if (scale == "C") { tempValue = outdoorTemp.toDouble().round(1) outdoorTempString = String.format('%2.1f', tempValue) } else { tempValue = celsiusToFahrenheit(outdoorTemp).toDouble().round() outdoorTempString = String.format('%2d', tempValue.intValue()) } def isChange = isStateChange(device, name, outdoorTempString) def isDisplayed = isChange sendEvent( name: "outdoorTemp", value: outdoorTempString, unit: scale, displayed: isDisplayed) int outdoorTempValue int outdoorTempToSend if (outdoorTemp < 0) { outdoorTempValue = -outdoorTemp*100 - 65536 outdoorTempValue = -outdoorTempValue outdoorTempToSend = zigbee.convertHexToInt(swapEndianHex(hex(outdoorTempValue))) if (settings.zipcode) { cmds += zigbee.writeAttribute(0xFF01, 0x0010, 0x29, outdoorTempToSend, [mfgCode: 0x119C]) } } else { outdoorTempValue = outdoorTemp*100 int tempa = outdoorTempValue.intdiv(256) int tempb = (outdoorTempValue % 256) * 256 outdoorTempToSend = tempa + tempb if (settings.zipcode) { cmds += zigbee.writeAttribute(0xFF01, 0x0010, 0x29, outdoorTempToSend, [mfgCode: 0x119C]) } } def mytimezone = location.getTimeZone() long dstSavings = 0 if(mytimezone.useDaylightTime() && mytimezone.inDaylightTime(new Date())) { dstSavings = mytimezone.getDSTSavings() } //To refresh the time long secFrom2000 = (((now().toBigInteger() + mytimezone.rawOffset + dstSavings ) / 1000) - (10957 * 24 * 3600)).toLong() //number of second from 2000-01-01 00:00:00h long secIndian = zigbee.convertHexToInt(swapEndianHex(hex(secFrom2000).toString())) //switch endianess cmds += zigbee.writeAttribute(0xFF01, 0x0020, 0x23, secIndian, [mfgCode: 0x119C]) if(BacklightAutoDimParam == "On Demand"){ //Backlight when needed traceEvent(settings.logFilter,"Backlight on press",settings.trace) cmds += zigbee.writeAttribute(0x0201, 0x0402, 0x30, 0x0000) } else{ //Backlight sensing traceEvent(settings.logFilter,"Backlight Sensing",settings.trace) cmds += zigbee.writeAttribute(0x0201, 0x0402, 0x30, 0x0001) } sendZigbeeCommands(cmds) } //refreshTime() traceEvent(settings.logFilter,"refresh_misc> about to refresh other misc variables, scale=${state.scale}", settings.trace) def heatingSetpointRangeHigh= (device.currentValue("heatingSetpointRangeHigh")) ?: (scale=='C')?30:99 def heatingSetpointRangeLow= (device.currentValue("heatingSetpointRangeLow")) ?: (scale=='C')?10:50 def low = heatingSetpointRangeLow.toFloat().round(1) def high = heatingSetpointRangeHigh.toFloat().round(1) def heatingSetpointRange= [low,high] def isChanged= isStateChange(device, "heatingSetpointRange", heatingSetpointRange?.toString()) sendEvent(name: "heatingSetpointRange", value: heatingSetpointRange, isStateChange: isChanged, displayed: (settings.trace?:false)) traceEvent(settings.logFilter,"refresh_misc>end", settings.trace) } //-- Private functions ----------------------------------------------------------------------------------- void sendZigbeeCommands(cmds, delay = 1000) { cmds.removeAll { it.startsWith("delay") } // convert each command into a HubAction cmds = cmds.collect { new physicalgraph.device.HubAction(it) } sendHubCommand(cmds, delay) } private def get_weather(zipcode) { def weather if (zipcode) { traceEvent(settings.logFilter,"refresh>ZipCode: ${zipcode}",settings.trace) weather = getWeatherFeature( "conditions", zipcode.trim() ) } else { traceEvent(settings.logFilter,"refresh>ZipCode: current location",settings.trace) weather = getWeatherFeature( "conditions" ) } return weather } private hex(value) { String hex=new BigInteger(Math.round(value).toString()).toString(16) traceEvent(settings.logFilter,"hex>value=$value, hex=$hex",settings.trace) return hex } def refreshTime() { //IS NOT USED def mytimezone = location.getTimeZone() long secFrom2000 = (((now().toBigInteger() + mytimezone.rawOffset + mytimezone.dstSavings ) / 1000) - (10957 * 24 * 3600)).toLong() //number of second from 2000-01-01 00:00:00h long secIndian = zigbee.convertHexToInt(swapEndianHex(hex(secFrom2000).toString())) //switch endianess traceEvent(settings.logFilter, "refreshTime>myTime = ${secFrom2000} reversed = ${secIndian}", settings.trace) def cmds = [] cmds += zigbee.writeAttribute(0xFF01, 0x0020, 0x23, secIndian, [mfgCode: 0x119C]) sendZigbeeCommands(cmds) //IS NOT USED } 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 } private int get_LOG_ERROR() { return 1 } private int get_LOG_WARN() { return 2 } private int get_LOG_INFO() { return 3 } private int get_LOG_DEBUG() { return 4 } private int get_LOG_TRACE() { return 5 } def traceEvent(logFilter, message, displayEvent = false, traceLevel = 4, sendMessage = true) { int LOG_ERROR = get_LOG_ERROR() int LOG_WARN = get_LOG_WARN() int LOG_INFO = get_LOG_INFO() int LOG_DEBUG = get_LOG_DEBUG() int LOG_TRACE = get_LOG_TRACE() int filterLevel = (logFilter) ? logFilter.toInteger() : get_LOG_WARN() if ((displayEvent) || (sendMessage)) { def results = [ name: "verboseTrace", value: message, displayed: ((displayEvent) ?: false) ] if ((displayEvent) && (filterLevel >= traceLevel)) { switch (traceLevel) { case LOG_ERROR: log.error "${message}" break case LOG_WARN: log.warn "${message}" break case LOG_INFO: log.info "${message}" break case LOG_TRACE: log.trace "${message}" break case LOG_DEBUG: default: log.debug "${message}" break } /* end switch*/ if (sendMessage) sendEvent(results) } /* end if displayEvent*/ } }