/** Copyright Sinopé Technologies 1.0.0 SVN-547 * 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. **/ metadata { definition(name: "Sinope Thermostat", namespace: "Sinope Technologies", author: "Sinope Technologies") { capability "thermostatHeatingSetpoint" capability "thermostatOperatingState" capability "Temperature Measurement" capability "Refresh" attribute "outdoorTemp", "string" command "heatLevelUp" command "heatLevelDown" fingerprint profileId: "0104", deviceId: "119C", manufacturer: "Sinope Technologies", model: "TH1123ZB", deviceJoinName: "TH1123ZB" fingerprint profileId: "0104", deviceId: "119C", manufacturer: "Sinope Technologies", model: "TH1124ZB", deviceJoinName: "TH1124ZB" fingerprint profileId: "0104", deviceId: "119C", manufacturer: "Sinope Technologies", model: "TH1300ZB", deviceJoinName: "TH1300ZB" fingerprint profileId: "0104", deviceId: "119C", manufacturer: "Sinope Technologies", model: "TH1400ZB", deviceJoinName: "TH1400ZB" } //-------------------------------------------------------------------------------------------------------- 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.heatingSetpoint", key: "HEATING_SETPOINT") { attributeState("default", label: '${currentValue}', unit: "dF") } } standardTile("refresh", "device.temperature", inactiveLabel: false, width: 2, height: 2, decoration: "flat") { state "default", action: "refresh.refresh", icon: "st.secondary.refresh" } //-- Main & Details ---------------------------------------------------------------------------------------- main("thermostatMulti") details(["thermostatMulti","refresh"]) } } //-- Installation ---------------------------------------------------------------------------------------- def installed() { initialize() } def updated() { if (!state.updatedLastRanAt || now() >= state.updatedLastRanAt + 5000) { state.updatedLastRanAt = now() try { unschedule() } catch (e) { } runIn(1,refresh_misc) runEvery15Minutes(refresh_misc) } } void initialize() { state?.scale = getTemperatureScale() runIn(2,refresh) def supportedThermostatModes = ['off', 'heat'] state?.supportedThermostatModes = supportedThermostatModes sendEvent(name: "supportedThermostatModes", value: supportedThermostatModes, displayed: (settings.trace ?: false)) setTileHeatingSetpointRange() } def ping() { refresh() } def uninstalled() { unschedule() } //-- Parsing --------------------------------------------------------------------------------------------- // parse events into attributes def parse(String description) { def result = [] def scale = getTemperatureScale() state?.scale = scale def cluster = zigbee.parse(description) if (description?.startsWith("read attr -")) { def descMap = zigbee.parseDescriptionAsMap(description) result += createCustomMap(descMap) if(descMap.additionalAttrs){ def mapAdditionnalAttrs = descMap.additionalAttrs mapAdditionnalAttrs.each{add -> add.cluster = descMap.cluster result += createCustomMap(add) } } } 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) } else if (descMap.cluster == "0201" && descMap.attrId == "0008") { map.name = "thermostatOperatingState" map.value = getHeatingDemand(descMap.value) map.value = (map.value.toInteger() < 10) ? "idle" : "heating" } else if (descMap.cluster == "0201" && descMap.attrId == "0012") { map.name = "heatingSetpoint" map.value = getTemperatureValue(descMap.value, true) log.info "heatingSetpoint: ${map.value}" } else if (descMap.cluster == "0201" && descMap.attrId == "0015") { map.name = "heatingSetpointRangeLow" map.value = getTemperatureValue(descMap.value, true) } else if (descMap.cluster == "0201" && descMap.attrId == "0016") { map.name = "heatingSetpointRangeHigh" map.value = getTemperatureValue(descMap.value, true) } 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) } return result } //-- Temperature ----------------------------------------------------------------------------------------- def getTemperatureValue(value, doRounding = false) { def scale = state?.scale if (value != null) { //do hexadecimal to decimal conversion 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()) } else if (tempValueString.matches(".*([.,][789])")) { celsius = celsius.intValue() + 1 tempValueString = String.format('%2d.0', celsius.intValue()) } else { tempValueString = String.format('%2d.0', celsius.intValue()) } return tempValueString.toDouble().round(1) } else { return celsius.round(1) } } else { return Math.round(celsiusToFahrenheit(celsius)) } } } 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) def celsius = (scale == "C") ? degreesDouble : (fahrenheitToCelsius(degreesDouble) as Double).round(1) def cmds = [] cmds += zigbee.writeAttribute(0x201, 0x12, 0x29, hex(celsius * 100)) sendZigbeeCommands(cmds) } //-- Heating Setpoint ------------------------------------------------------------------------------------ def heatLevelUp() { def scale = state?.scale log.error "scale: ${scale}" def heatingSetpointRangeHigh double nextLevel heatingSetpointRangeHigh = state.heatingSetpointLimitHigh heatingSetpointRangeHigh = (heatingSetpointRangeHigh) ?: (scale == 'C') ? 10.0 : 50 log.debug "heatingSetpointRangeHigh: ${heatingSetpointRangeHigh}" 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") log.error "nextLevel: ${nextLevel}" nextLevel = (nextLevel + 1) if (nextLevel > heatingSetpointRangeHigh.toDouble()) { nextLevel = heatingSetpointRangeHigh.toDouble() } setHeatingSetpoint(nextLevel.intValue()) } } def heatLevelDown() { def scale = state?.scale def heatingSetpointRangeLow double nextLevel heatingSetpointRangeLow = state.heatingSetpointLimitLow 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()) } } //-- Heating Demand -------------------------------------------------------------------------------------- def getHeatingDemand(value) { if (value != null) { def demand = Integer.parseInt(value, 16) return demand.toString() } } //-- Temperature Display Mode ---------------------------------------------------------------------------- def refresh() { if (!state.updatedLastRanAt || now() >= state.updatedLastRanAt + 20000) { state.updatedLastRanAt = now() state?.scale = getTemperatureScale() 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.configureReporting(0x0201, 0x0000, 0x29, 19, 301, 50) //local temperature cmds += zigbee.configureReporting(0x0201, 0x0012, 0x0029, 15, 302, 40) //occupied heating setpoint cmds += zigbee.configureReporting(0x0201, 0x0008, 0x0020, 4, 300, 10) //PI heating demand setTileHeatingSetpointRange() sendZigbeeCommands(cmds) refresh_misc() } else { } } private void setTileHeatingSetpointRange() { def scale = state?.scale if((device.getDataValue("model") == "TH1123ZB") || (device.getDataValue("model") == "TH1124ZB")) { state.heatingSetpointLimitLow = (device.currentValue("heatingSetpointRangeLow")) ?: (scale=='C')?5:41 state.heatingSetpointLimitHigh = (device.currentValue("heatingSetpointRangeHigh")) ?: (scale=='C')?30:86 } else if(device.getDataValue("model") == "TH1300ZB" || device.getDataValue("model") == "TH1400ZB") { state.heatingSetpointLimitLow = (device.currentValue("heatingSetpointRangeLow")) ?: (scale=='C')?5:41 state.heatingSetpointLimitHigh = (device.currentValue("heatingSetpointRangeHigh")) ?: (scale=='C')?36:97 } } void refresh_misc() { def weather = get_weather() def cmds=[] if (weather) { double tempValue int outdoorTemp = weather.toInteger() if(state?.scale == 'F') {//the value sent to the thermostat must be in C //the thermostat make the conversion to F outdoorTemp = fahrenheitToCelsius(outdoorTemp).toDouble().round() } String outdoorTempString def isChange = isStateChange(device, name, outdoorTempString) def isDisplayed = isChange int outdoorTempValue int outdoorTempToSend cmds += zigbee.writeAttribute(0xFF01, 0x0011, 0x21, 10800)//set the outdoor temperature timeout to 3 hours if (outdoorTemp < 0) { outdoorTempValue = -outdoorTemp*100 - 65536 outdoorTempValue = -outdoorTempValue outdoorTempToSend = zigbee.convertHexToInt(swapEndianHex(hex(outdoorTempValue))) 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 cmds += zigbee.writeAttribute(0xFF01, 0x0010, 0x29, outdoorTempToSend, [mfgCode: 0x119C]) } //the newer thermostats uses the time cluster, but the older ones need to receive the hour 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]) sendZigbeeCommands(cmds) } } //-- 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() { def mymap = getTwcConditions() def weather = mymap.temperature return weather } private hex(value) { String hex=new BigInteger(Math.round(value).toString()).toString(16) return hex } 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 }