Centralite Pearl Thermostat

This is a collection of several thermostat drivers from ST, I wanted to be able to support the Hold button on the thermostat. The Generic ZigBee Thermostat driver in Hubitat may eventually support everything that this driver does. Fixes/Improvements welcome!

/*
 *  Centralite Pearl Thermostat
 */
metadata {
    definition (name: "CentraLite Pearl Thermostat", namespace: "hubitat", author: "dagrider") {
        capability "Actuator"
        capability "Temperature Measurement"
        capability "Thermostat"
        capability "Configuration"
        capability "Refresh"
        capability "Sensor"
        capability "Polling"
        capability "Battery"
                                
        // Custom commands 
        command "raiseHeatLevel"
        command "lowerHeatLevel"
        command "raiseCoolLevel"
        command "lowerCoolLevel"
        //command "setTemperature"
        command "setThermostatHoldMode"
        //command "getPowerSource"
 
        // thermostat capability commands
        // setHeatingSetpoint(number)
        // setCoolingSetpoint(number)
        // off()
        // heat()
        // emergencyHeat()
        // cool()
        // setThermostatMode(enum)
        // fanOn()
        // fanAuto()
        // fanCirculate()
        // setThermostatFanMode(enum)
        // auto()
 
        // thermostat capability attributes
        // temperature
        // heatingSetpoint
        // coolingSetpoint
        // thermostatSetpoint
        // thermostatMode (auto, emergency heat, heat, off, cool)
        // thermostatFanMode (auto, on, circulate)
        // thermostatOperatingState (heating, idle, pending cool, vent economizer, cooling, pending heat, fan only)
 
        attribute "thermostatHoldMode", "string"
        attribute "powerSource", "string"
                                
        fingerprint profileId: "0104", inClusters: "0000,0001,0003,0020,0201,0202,0204,0B05", outClusters: "000A, 0019"
    }
}
 
def installed() {
    log.debug "installed"
    configure()
}
 
def updated() {
    log.debug "updated"
    configure()
}
 
def configure() {
    log.debug "configure"
    def cmds = 
        [
            "zdo bind 0x${device.deviceNetworkId} 1 1 0x0001 {${device.zigbeeId}} {}", "delay 500",
            "zcl global send-me-a-report 0x0001 0x20 0x20 3600 86400 {}", "delay 500", //battery report request
            "send 0x${device.deviceNetworkId} 1 1",
            "zdo bind 0x${device.deviceNetworkId} 1 1 0x0201 {${device.zigbeeId}} {}", "delay 500",
            "zcl global send-me-a-report 0x0201 0x0000 0x29 5 300 {3200}", "delay 500", // report temperature changes over 0.5°C (0x3200 in little endian)
            "send 0x${device.deviceNetworkId} 1 1",
            "zcl global send-me-a-report 0x0201 0x0011 0x29 5 300 {3200}", "delay 500", // report cooling setpoint delta: 0.5°C
            "send 0x${device.deviceNetworkId} 1 1",
            "zcl global send-me-a-report 0x0201 0x0012 0x29 5 300 {3200}", "delay 500", // report heating setpoint delta: 0.5°C
            "send 0x${device.deviceNetworkId} 1 1",
            "zcl global send-me-a-report 0x0201 0x001C 0x30 5 300 {}", "delay 500",     // report system mode
            "send 0x${device.deviceNetworkId} 1 1",
            "zcl global send-me-a-report 0x0201 0x0029 0x19 5 300 {}", "delay 500",    // report running state
            "send 0x${device.deviceNetworkId} 1 1",
           "zcl global send-me-a-report 0x0201 0x0023 0x30 5 300 {}", "delay 500",     // report hold mode
            "send 0x${device.deviceNetworkId} 1 1", 
            "zdo bind 0x${device.deviceNetworkId} 1 1 0x0202 {${device.zigbeeId}} {}", "delay 500",
            "zcl global send-me-a-report 0x0202 0 0x30 5 300 {}","delay 500",          // report fan mode
            "send 0x${device.deviceNetworkId} 1 1", 
        ]
}
 
def refresh() {
    log.debug "refresh called"
    // 0000 07 power source
    // 0201 00 temperature
    // 0201 11 cooling setpoint
    // 0201 12 heating setpoint
    // 0201 1C thermostat mode 
    // 0201 1E run mode
    // 0201 23 hold mode
    // 0001 20 battery
    // 0202 00 fan mode
 
    def cmds = zigbee.readAttribute(0x0000, 0x0007) +
               zigbee.readAttribute(0x0201, 0x0000) +
               zigbee.readAttribute(0x0201, 0x0011) +
               zigbee.readAttribute(0x0201, 0x0012) +
               zigbee.readAttribute(0x0201, 0x001C) +
               zigbee.readAttribute(0x0201, 0x001E) +
               zigbee.readAttribute(0x0201, 0x0023) +
               zigbee.readAttribute(0x0201, 0x0029) +
               zigbee.readAttribute(0x0001, 0x0020) +
               zigbee.readAttribute(0x0202, 0x0000)
    
    return cmds
}
 
def raiseHeatLevel(){
    if (isHoldOn()) return
    
    def currentLevel = device.currentValue("heatingSetpoint")
    int nextLevel = currentLevel.toInteger() + 1
    log.debug "raiseHeatLevel: calling setHeatingSetpoint with ${nextLevel}"
    setHeatingSetpoint(nextLevel)
}
 
def lowerHeatLevel(){
    if (isHoldOn()) return
    
    def currentLevel = device.currentValue("heatingSetpoint")
    int nextLevel = currentLevel.toInteger() - 1
    log.debug "lowerHeatLevel: calling setHeatingSetpoint with ${nextLevel}"
    setHeatingSetpoint(nextLevel)
}
 
def raiseCoolLevel(){
    if (isHoldOn()) return
    
    def currentLevel = device.currentValue("coolingSetpoint")
    int nextLevel = currentLevel.toInteger() + 1
    log.debug "raiseCoolLevel: calling setCoolingSetpoint with ${nextLevel}"
    setCoolingSetpoint(nextLevel)
}
 
def lowerCoolLevel(){
    if (isHoldOn()) return
    
    def currentLevel = device.currentValue("coolingSetpoint")
    int nextLevel = currentLevel.toInteger() - 1
    log.debug "lowerCoolLevel: calling setCoolingSetpoint with ${nextLevel}"
    setCoolingSetpoint(nextLevel)
}
 
def parse(String description) {
    log.debug "Parse description $description"
    def map = [:]
 
    if (description?.startsWith("read attr -")) {
        def descMap = zigbee.parseDescriptionAsMap(description)
        
        if (descMap.cluster == "0201" && descMap.attrId == "0000") {
            log.debug "TEMPERATURE"
            map.name = "temperature"
            map.value = getTemperature(descMap.value)
        } else if (descMap.cluster == "0201" && descMap.attrId == "0011") {
            log.debug "COOLING SETPOINT"
            map.name = "coolingSetpoint"
            map.value = getTemperature(descMap.value)
        } else if (descMap.cluster == "0201" && descMap.attrId == "0012") {
            log.debug "HEATING SETPOINT"
            map.name = "heatingSetpoint"
            map.value = getTemperature(descMap.value)
        } else if (descMap.cluster == "0201" && descMap.attrId == "001C") {
            log.debug "MODE"
            map.name = "thermostatMode"
            map.value = getModeMap()[descMap.value]
        } else if (descMap.cluster == "0202" && descMap.attrId == "0000") {
            log.debug "FAN MODE"
            map.name = "thermostatFanMode"
            map.value = getFanModeMap()[descMap.value]
        } else if (descMap.cluster == "0001" && descMap.attrId == "0020") {
            log.debug "BATTERY"
            map.name = "battery"
            map.value = getBatteryLevel(descMap.value)
        } else if (descMap.cluster == "0201" && descMap.attrId == "001E") {
            log.debug "RUN MODE"
            map.name = "thermostatRunMode"
            map.value = getModeMap()[descMap.value]
        } else if (descMap.cluster == "0201" && descMap.attrId == "0023") {
            log.debug "HOLD MODE"
            map.name = "thermostatHoldMode"
            map.value = getHoldModeMap()[descMap.value]  
        } else if (descMap.cluster == "0201" && descMap.attrId == "0029") {
            log.debug "OPERATING MODE"
            map.name = "thermostatOperatingState"
            map.value = getThermostatOperatingStateMap()[descMap.value]  
        } else if (descMap.cluster == "0000" && descMap.attrId == "0007") {
            log.debug "POWER SOURCE"
            map.name = "powerSource"
            map.value = getPowerSource()[descMap.value]                                  
        }
    }
 
    def result = null
 
    if (map) {
        result = createEvent(map)
    }
 
    log.debug "Parse returned $map"
    return result
}
 
def getModeMap() { 
    [
        "00":"off",
        "01":"auto",
        "03":"cool",
        "04":"heat",
        "05":"emergency heat",
        "06":"precooling",
        "07":"fan only",
        "08":"dry",
        "09":"sleep"
    ]
}
 
def modes() {
    ["off", "cool", "heat", "emergencyHeat"]
}
 
def getHoldModeMap() { 
    [
        "00":"holdOff",
        "01":"holdOn",
    ]
}
 
def getPowerSource() { 
    [
        "01":"24VAC",
        "03":"Battery",
        "81":"24VAC"
    ]
}
 
def getFanModeMap() { 
    [
        "04":"fanOn",
        "05":"fanAuto"
    ]
}

def getThermostatOperatingStateMap() {
    /**  Bit Number
    //  0 Heat State
    //  1 Cool State
    //  2 Fan State
    //  3 Heat 2nd Stage State
    //  4 Cool 2nd Stage State
    //  5 Fan 2nd Stage State
    //  6 Fan 3rd Stage Stage
    **/
    [
        "0000":"idle",
        "0001":"heating",
        "0002":"cooling",
        "0004":"fan only",
        "0005":"heating",
        "0006":"cooling",
        "0008":"heating",
        "0009":"heating",
        "000A":"heating",
        "000D":"heating",
        "0010":"cooling",
        "0012":"cooling",
        "0014":"cooling",
        "0015":"cooling"
    ]
}
 
def getTemperature(value) {
    if (value != null) {
        def celsius = Integer.parseInt(value, 16) / 100

        if (getTemperatureScale() == "C") {
            return celsius
        } else {
            def fahrenheit = Math.round(celsiusToFahrenheit(celsius)) 
            return fahrenheit
        }
    }
}
 
def setThermostatHoldMode() {
    log.debug "setThermostatHoldMode"
    def currentHoldMode = device.currentState("thermostatHoldMode")?.value
    def returnCommand
 
    if (!currentHoldMode) { 
        log.debug "Unable to determine thermostat hold mode, setting to hold off"
        returnCommand = holdOff() 
    } else {
        log.debug "Switching thermostat from current mode: $currentHoldMode"
 
        switch (currentHoldMode) {
            case "holdOff":
                returnCommand = holdOn()
                break
            case "holdOn":
                returnCommand = holdOff()
                break
        }
    }
 
    returnCommand
}
 
def setThermostatMode(String value) {
    log.debug "setThermostatMode to {$value}"
    "$value"()
}
 
def setThermostatFanMode(String value) {
    log.debug "setThermostatFanMode({$value})"
    "$value"()
}
 
def setThermostatHoldMode(String value) {
    log.debug "setThermostatHoldMode({$value})"
    "$value"()
}
 
def off() {
    log.debug "off"
    sendEvent("name":"thermostatMode", "value":"off")
    zigbee.writeAttribute(0x0201, 0x1C, 0x30, 0)
}
 
def cool() {
    log.debug "cool"
    sendEvent("name":"thermostatMode", "value":"cool")
    zigbee.writeAttribute(0x0201, 0x1C, 0x30, 3)
}
 
def heat() {
    log.debug "heat"
    sendEvent("name":"thermostatMode", "value":"heat")
    zigbee.writeAttribute(0x0201, 0x1C, 0x30, 4)
}
 
def emergencyHeat() {
    log.debug "emergencyHeat"
    sendEvent("name":"thermostatMode", "value":"emergencyHeat")
    zigbee.writeAttribute(0x0201, 0x1C, 0x30, 5)
}
 
def on() {
    log.debug "on"
    fanOn()
}
 
def fanOn() {
    log.debug "fanOn"
    sendEvent("name":"thermostatFanMode", "value":"fanOn")
    zigbee.writeAttribute(0x0202, 0x00, 0x30, 4)
}
 
def auto() {
    log.debug "auto"
    // thermostat doesn't support auto
}
 
def fanAuto() {
    log.debug "fanAuto"
    sendEvent("name":"thermostatFanMode", "value":"fanAuto")
    zigbee.writeAttribute(0x0202, 0x00, 0x30, 5)
}
 
def holdOn() {
    log.debug "holdOn"
    sendEvent("name":"thermostatHoldMode", "value":"holdOn")
    zigbee.writeAttribute(0x0201, 0x23, 0x30, 1)
}
 
def holdOff() {
    log.debug "Set Hold Off for thermostat"
    sendEvent("name":"thermostatHoldMode", "value":"holdOff")
    zigbee.writeAttribute(0x0201, 0x23, 0x30, 0)
}
 
// Commment out below if no C-wire since it will kill the batteries.
def poll() {
//            log.debug "Executing 'poll'"
//            refresh()
}
 
private getBatteryLevel(rawValue) {
    def intValue = Integer.parseInt(rawValue,16)
    def min = 2.1
    def max = 3.0
    def vBatt = intValue / 10
    return ((vBatt - min) / (max - min) * 100) as int
}
 
private cvtTemp(value) {
    new BigInteger(Math.round(value))
}

private isHoldOn() {
    if (device.currentState("thermostatHoldMode")?.value == "holdOn") return true
    return false
}
 
def setHeatingSetpoint(degrees) {
    log.debug "setHeatingSetpoint to $degrees"
    
    if (isHoldOn()) return
 
    if (degrees != null) {
        def temperatureScale = getTemperatureScale()
        def degreesInteger = Math.round(degrees)
        def maxTemp
        def minTemp
 
        if (temperatureScale == "C") {
            maxTemp = 44 
            minTemp = 7 
            log.debug "Location is in Celsius, maxTemp: $maxTemp, minTemp: $minTemp"
        } else {
            maxTemp = 86 
            minTemp = 30 
            log.debug "Location is in Farenheit, maxTemp: $maxTemp, minTemp: $minTemp"
                }
 
        if (degreesInteger > maxTemp) degreesInteger = maxTemp
        if (degreesInteger < minTemp) degreesInteger = minTemp
 
        log.debug "setHeatingSetpoint degrees $degreesInteger $temperatureScale"
        def celsius = (temperatureScale == "C") ? degreesInteger : (fahrenheitToCelsius(degreesInteger) as Double).round(2)
        zigbee.writeAttribute(0x0201, 0x12, 0x29, cvtTemp(celsius * 100))
    }
}
 
def setCoolingSetpoint(degrees) {
    log.debug "setCoolingSetpoint to $degrees"
    
    if (isHoldOn()) return
 
    if (degrees != null) {
        def temperatureScale = getTemperatureScale()
        def degreesInteger = Math.round(degrees)
        def maxTemp
        def minTemp
 
        if (temperatureScale == "C") {
            maxTemp = 44 
            minTemp = 7 
            log.debug "Location is in Celsius, maxTemp: $maxTemp, minTemp: $minTemp"
        } else {
            maxTemp = 86 
            minTemp = 30 
            log.debug "Location is in Farenheit, maxTemp: $maxTemp, minTemp: $minTemp"
        }
 
        if (degreesInteger > maxTemp) degreesInteger = maxTemp
        if (degreesInteger < minTemp) degreesInteger = minTemp
 
        log.debug "setCoolingSetpoint degrees $degreesInteger $temperatureScale"
        def celsius = (temperatureScale == "C") ? degreesInteger : (fahrenheitToCelsius(degreesInteger) as Double).round(2)
        zigbee.writeAttribute(0x0201, 0x11, 0x29, cvtTemp(celsius * 100))
    }
}
3 Likes

Select the driver code and use the preformated text icon </> to put into a field. :slight_smile:

Hey Mate, is there any chance you can redo the code so it shows in one screen, tried copy and pasting, but keeps having errors.

You might try the built-in Generic ZigBee Thermostat driver...I think it now does everything that this one does. I've just not taken the time to try it out yet.

/*
 *  Centralite Pearl Thermostat
 */
metadata {
    definition (name: "CentraLite Pearl Thermostat", namespace: "hubitat", author: "dagrider") {
        capability "Actuator"
        capability "Temperature Measurement"
        capability "Thermostat"
        capability "Configuration"
        capability "Refresh"
        capability "Sensor"
        capability "Polling"
        capability "Battery"
                                
        // Custom commands 
        command "raiseHeatLevel"
        command "lowerHeatLevel"
        command "raiseCoolLevel"
        command "lowerCoolLevel"
        //command "setTemperature"
        command "setThermostatHoldMode"
        //command "getPowerSource"
 
        // thermostat capability commands
        // setHeatingSetpoint(number)
        // setCoolingSetpoint(number)
        // off()
        // heat()
        // emergencyHeat()
        // cool()
        // setThermostatMode(enum)
        // fanOn()
        // fanAuto()
        // fanCirculate()
        // setThermostatFanMode(enum)
        // auto()
 
        // thermostat capability attributes
        // temperature
        // heatingSetpoint
        // coolingSetpoint
        // thermostatSetpoint
        // thermostatMode (auto, emergency heat, heat, off, cool)
        // thermostatFanMode (auto, on, circulate)
        // thermostatOperatingState (heating, idle, pending cool, vent economizer, cooling, pending heat, fan only)
 
        attribute "thermostatHoldMode", "string"
        attribute "powerSource", "string"
                                
        fingerprint profileId: "0104", inClusters: "0000,0001,0003,0020,0201,0202,0204,0B05", outClusters: "000A, 0019"
    }
}
 
def installed() {
    log.debug "installed"
    configure()
}
 
def updated() {
    log.debug "updated"
    configure()
}
 
def configure() {
    log.debug "configure"
    def cmds = 
        [
            //"zdo bind 0x${device.deviceNetworkId} 1 1 0x0001 {${device.zigbeeId}} {}", "delay 500",
            //"zcl global send-me-a-report 0x0001 0x20 0x20 3600 86400 {01}", "delay 500", //battery report request
            //"send 0x${device.deviceNetworkId} 1 1",
            "zdo bind 0x${device.deviceNetworkId} 1 1 0x0001 {${device.zigbeeId}} {}", "delay 500",
            "zcl global send-me-a-report 0x0001 0x20 0x20 3600 86400 {}", "delay 500", //battery report request
            "send 0x${device.deviceNetworkId} 1 1",
            "zdo bind 0x${device.deviceNetworkId} 1 1 0x0201 {${device.zigbeeId}} {}", "delay 500",
            "zcl global send-me-a-report 0x0201 0x0000 0x29 5 300 {3200}", "delay 500", // report temperature changes over 0.5°C (0x3200 in little endian)
            "send 0x${device.deviceNetworkId} 1 1",
            "zcl global send-me-a-report 0x0201 0x0011 0x29 5 300 {3200}", "delay 500", // report cooling setpoint delta: 0.5°C
            "send 0x${device.deviceNetworkId} 1 1",
            "zcl global send-me-a-report 0x0201 0x0012 0x29 5 300 {3200}", "delay 500", // report heating setpoint delta: 0.5°C
            "send 0x${device.deviceNetworkId} 1 1",
            "zcl global send-me-a-report 0x0201 0x001C 0x30 5 300 {}", "delay 500",     // report system mode
            "send 0x${device.deviceNetworkId} 1 1",
            "zcl global send-me-a-report 0x0201 0x0029 0x19 5 300 {}", "delay 500",    // report running state
            "send 0x${device.deviceNetworkId} 1 1",
           "zcl global send-me-a-report 0x0201 0x0023 0x30 5 300 {}", "delay 500",     // report hold mode
            "send 0x${device.deviceNetworkId} 1 1", 
            "zdo bind 0x${device.deviceNetworkId} 1 1 0x0202 {${device.zigbeeId}} {}", "delay 500",
            "zcl global send-me-a-report 0x0202 0 0x30 5 300 {}","delay 500",          // report fan mode
            "send 0x${device.deviceNetworkId} 1 1", 
        ]
    
    //cmds += zigbee.batteryConfig()
    
}
 
def refresh() {
    log.debug "refresh called"
    // 0000 07 power source
    // 0201 00 temperature
    // 0201 11 cooling setpoint
    // 0201 12 heating setpoint
    // 0201 1C thermostat mode 
    // 0201 1E run mode
    // 0201 23 hold mode
    // 0001 20 battery
    // 0202 00 fan mode
 
    def cmds = zigbee.readAttribute(0x0000, 0x0007) +
               zigbee.readAttribute(0x0201, 0x0000) +
               zigbee.readAttribute(0x0201, 0x0011) +
               zigbee.readAttribute(0x0201, 0x0012) +
               zigbee.readAttribute(0x0201, 0x001C) +
               zigbee.readAttribute(0x0201, 0x001E) +
               zigbee.readAttribute(0x0201, 0x0023) +
               zigbee.readAttribute(0x0201, 0x0029) +
               zigbee.readAttribute(0x0001, 0x0020) +
               zigbee.readAttribute(0x0202, 0x0000)
    
    return cmds
}
 
def raiseHeatLevel(){
    if (isHoldOn()) return
    
    def currentLevel = device.currentValue("heatingSetpoint")
    int nextLevel = currentLevel.toInteger() + 1
    log.debug "raiseHeatLevel: calling setHeatingSetpoint with ${nextLevel}"
    setHeatingSetpoint(nextLevel)
}
 
def lowerHeatLevel(){
    if (isHoldOn()) return
    
    def currentLevel = device.currentValue("heatingSetpoint")
    int nextLevel = currentLevel.toInteger() - 1
    log.debug "lowerHeatLevel: calling setHeatingSetpoint with ${nextLevel}"
    setHeatingSetpoint(nextLevel)
}
 
def raiseCoolLevel(){
    if (isHoldOn()) return
    
    def currentLevel = device.currentValue("coolingSetpoint")
    int nextLevel = currentLevel.toInteger() + 1
    log.debug "raiseCoolLevel: calling setCoolingSetpoint with ${nextLevel}"
    setCoolingSetpoint(nextLevel)
}
 
def lowerCoolLevel(){
    if (isHoldOn()) return
    
    def currentLevel = device.currentValue("coolingSetpoint")
    int nextLevel = currentLevel.toInteger() - 1
    log.debug "lowerCoolLevel: calling setCoolingSetpoint with ${nextLevel}"
    setCoolingSetpoint(nextLevel)
}
 
def parse(String description) {
    log.debug "Parse description $description"
    def map = [:]
 
    if (description?.startsWith("read attr -")) {
        def descMap = zigbee.parseDescriptionAsMap(description)
        
        if (descMap.cluster == "0201" && descMap.attrId == "0000") {
            log.debug "TEMPERATURE"
            map.name = "temperature"
            map.unit = getTemperatureScale()
            map.value = getTemperature(descMap.value)
        } else if (descMap.cluster == "0201" && descMap.attrId == "0011") {
            log.debug "COOLING SETPOINT"
            map.name = "coolingSetpoint"
            map.unit = getTemperatureScale()
            map.value = getTemperature(descMap.value)
        } else if (descMap.cluster == "0201" && descMap.attrId == "0012") {
            log.debug "HEATING SETPOINT"
            map.name = "heatingSetpoint"
            map.unit = getTemperatureScale()
            map.value = getTemperature(descMap.value)
        } else if (descMap.cluster == "0201" && descMap.attrId == "001C") {
            log.debug "MODE"
            map.name = "thermostatMode"
            map.value = getModeMap()[descMap.value]
        } else if (descMap.cluster == "0202" && descMap.attrId == "0000") {
            log.debug "FAN MODE"
            map.name = "thermostatFanMode"
            map.value = getFanModeMap()[descMap.value]
        } else if (descMap.cluster == "0001" && descMap.attrId == "0020") {
            log.debug "BATTERY"
            map.name = "battery"
            map.value = getBatteryLevel(descMap.value)
        } else if (descMap.cluster == "0201" && descMap.attrId == "001E") {
            log.debug "RUN MODE"
            map.name = "thermostatRunMode"
            map.value = getModeMap()[descMap.value]
        } else if (descMap.cluster == "0201" && descMap.attrId == "0023") {
            log.debug "HOLD MODE"
            map.name = "thermostatHoldMode"
            map.value = getHoldModeMap()[descMap.value]  
        } else if (descMap.cluster == "0201" && descMap.attrId == "0029") {
            log.debug "OPERATING MODE"
            map.name = "thermostatOperatingState"
            map.value = getThermostatOperatingStateMap()[descMap.value]  
        } else if (descMap.cluster == "0000" && descMap.attrId == "0007") {
            log.debug "POWER SOURCE"
            map.name = "powerSource"
            map.value = getPowerSource()[descMap.value]                                  
        }
    }
 
    def result = null
 
    if (map) {
        result = createEvent(map)
    }
 
    log.debug "Parse returned $map"
    return result
}
 
def getModeMap() { 
    [
        "00":"off",
        "01":"auto",
        "03":"cool",
        "04":"heat",
        "05":"emergency heat",
        "06":"precooling",
        "07":"fan only",
        "08":"dry",
        "09":"sleep"
    ]
}
 
def modes() {
    ["off", "cool", "heat", "emergencyHeat"]
}
 
def getHoldModeMap() { 
    [
        "00":"holdOff",
        "01":"holdOn",
    ]
}
 
def getPowerSource() { 
    [
        "01":"24VAC",
        "03":"Battery",
        "81":"24VAC"
    ]
}
 
def getFanModeMap() { 
    [
        "04":"fanOn",
        "05":"fanAuto"
    ]
}

def getThermostatOperatingStateMap() {
    /**  Bit Number
    //  0 Heat State
    //  1 Cool State
    //  2 Fan State
    //  3 Heat 2nd Stage State
    //  4 Cool 2nd Stage State
    //  5 Fan 2nd Stage State
    //  6 Fan 3rd Stage Stage
    **/
    [
        "0000":"idle",
        "0001":"heating",
        "0002":"cooling",
        "0004":"fan only",
        "0005":"heating",
        "0006":"cooling",
        "0008":"heating",
        "0009":"heating",
        "000A":"heating",
        "000D":"heating",
        "0010":"cooling",
        "0012":"cooling",
        "0014":"cooling",
        "0015":"cooling"
    ]
}
 
def getTemperature(value) {
    if (value != null) {
        def temp = new BigInteger(value, 16) & 0xFFFF
        def celsius = temp / 100

        if (getTemperatureScale() == "C") {
            return celsius
        } else {
            def fahrenheit = Math.round(celsiusToFahrenheit(celsius)) 
            return fahrenheit
        }
    }
}
 
def setThermostatHoldMode() {
    log.debug "setThermostatHoldMode"
    def currentHoldMode = device.currentState("thermostatHoldMode")?.value
    def returnCommand
 
    if (!currentHoldMode) { 
        log.debug "Unable to determine thermostat hold mode, setting to hold off"
        returnCommand = holdOff() 
    } else {
        log.debug "Switching thermostat from current mode: $currentHoldMode"
 
        switch (currentHoldMode) {
            case "holdOff":
                returnCommand = holdOn()
                break
            case "holdOn":
                returnCommand = holdOff()
                break
        }
    }
 
    returnCommand
}
 
def setThermostatMode(String value) {
    log.debug "setThermostatMode to {$value}"
    "$value"()
}
 
def setThermostatFanMode(String value) {
    log.debug "setThermostatFanMode({$value})"
    "$value"()
}
 
def setThermostatHoldMode(String value) {
    log.debug "setThermostatHoldMode({$value})"
    "$value"()
}
 
def off() {
    log.debug "off"
    sendEvent("name":"thermostatMode", "value":"off")
    zigbee.writeAttribute(0x0201, 0x1C, 0x30, 0)
}
 
def cool() {
    log.debug "cool"
    sendEvent("name":"thermostatMode", "value":"cool")
    zigbee.writeAttribute(0x0201, 0x1C, 0x30, 3)
}
 
def heat() {
    log.debug "heat"
    sendEvent("name":"thermostatMode", "value":"heat")
    zigbee.writeAttribute(0x0201, 0x1C, 0x30, 4)
}
 
def emergencyHeat() {
    log.debug "emergencyHeat"
    sendEvent("name":"thermostatMode", "value":"emergencyHeat")
    zigbee.writeAttribute(0x0201, 0x1C, 0x30, 5)
}
 
def on() {
    log.debug "on"
    fanOn()
}
 
def fanOn() {
    log.debug "fanOn"
    sendEvent("name":"thermostatFanMode", "value":"fanOn")
    zigbee.writeAttribute(0x0202, 0x00, 0x30, 4)
}
 
def auto() {
    log.debug "auto"
    // thermostat doesn't support auto
}
 
def fanAuto() {
    log.debug "fanAuto"
    sendEvent("name":"thermostatFanMode", "value":"fanAuto")
    zigbee.writeAttribute(0x0202, 0x00, 0x30, 5)
}
 
def holdOn() {
    log.debug "holdOn"
    sendEvent("name":"thermostatHoldMode", "value":"holdOn")
    zigbee.writeAttribute(0x0201, 0x23, 0x30, 1)
}
 
def holdOff() {
    log.debug "Set Hold Off for thermostat"
    sendEvent("name":"thermostatHoldMode", "value":"holdOff")
    zigbee.writeAttribute(0x0201, 0x23, 0x30, 0)
}
 
// Commment out below if no C-wire since it will kill the batteries.
def poll() {
//            log.debug "Executing 'poll'"
//            refresh()
}
 
private getBatteryLevel(rawValue) {
    def intValue = Integer.parseInt(rawValue,16)
    def min = 2.1
    def max = 3.0
    def vBatt = intValue / 10
    return ((vBatt - min) / (max - min) * 100) as int
}
 
private cvtTemp(value) {
    new BigInteger(Math.round(value))
}

private isHoldOn() {
    if (device.currentState("thermostatHoldMode")?.value == "holdOn") return true
    return false
}
 
def setHeatingSetpoint(degrees) {
    log.debug "setHeatingSetpoint to $degrees"
    
    if (isHoldOn()) return
 
    if (degrees != null) {
        def temperatureScale = getTemperatureScale()
        def degreesInteger = Math.round(degrees)
        def maxTemp
        def minTemp
 
        if (temperatureScale == "C") {
            maxTemp = 44 
            minTemp = 7 
            log.debug "Location is in Celsius, maxTemp: $maxTemp, minTemp: $minTemp"
        } else {
            maxTemp = 86 
            minTemp = 30 
            log.debug "Location is in Farenheit, maxTemp: $maxTemp, minTemp: $minTemp"
                }
 
        if (degreesInteger > maxTemp) degreesInteger = maxTemp
        if (degreesInteger < minTemp) degreesInteger = minTemp
 
        log.debug "setHeatingSetpoint degrees $degreesInteger $temperatureScale"
        def celsius = (temperatureScale == "C") ? degreesInteger : (fahrenheitToCelsius(degreesInteger) as Double).round(2)
        zigbee.writeAttribute(0x0201, 0x12, 0x29, cvtTemp(celsius * 100))
    }
}
 
def setCoolingSetpoint(degrees) {
    log.debug "setCoolingSetpoint to $degrees"
    
    if (isHoldOn()) return
 
    if (degrees != null) {
        def temperatureScale = getTemperatureScale()
        def degreesInteger = Math.round(degrees)
        def maxTemp
        def minTemp
 
        if (temperatureScale == "C") {
            maxTemp = 44 
            minTemp = 7 
            log.debug "Location is in Celsius, maxTemp: $maxTemp, minTemp: $minTemp"
        } else {
            maxTemp = 86 
            minTemp = 30 
            log.debug "Location is in Farenheit, maxTemp: $maxTemp, minTemp: $minTemp"
        }
 
        if (degreesInteger > maxTemp) degreesInteger = maxTemp
        if (degreesInteger < minTemp) degreesInteger = minTemp
 
        log.debug "setCoolingSetpoint degrees $degreesInteger $temperatureScale"
        def celsius = (temperatureScale == "C") ? degreesInteger : (fahrenheitToCelsius(degreesInteger) as Double).round(2)
        zigbee.writeAttribute(0x0201, 0x11, 0x29, cvtTemp(celsius * 100))
    }
}
2 Likes

Thank you Denise. It doesnt have the Hold and can't place the thermostat in Auto with the driver. Going to try yours, thank you!.

Maybe you know how to set Auto, or it that what Hold will do? Just hate to keep programming Heating or Cooling.

Lyle

The Centralite Pearl doesn't do automatic switching (auto), it only supports heat and cool being set manually. You could write an app to set the thermostat to heat or cool based on temperature.

The way I implemented Hold in the driver is if you set Hold (at the thermostat or via the device driver) then any temperature changes that you might schedule (via an app on the hub) will be ignored. That means you can set a temperature and set Hold, then that temperature won't change until you remove the Hold.

I actually didn't know this...,

in any event I've finally gotten around to ordering the bits I need to build an hvac test rig to hook stats up to for testing and driver development.

1 Like

Got ya. It was actually quite easy writing a app to switch it from Cool to Heat depending on Temp. Thanks again Denise.

1 Like

@denise.grider
Great work on the driver. Would you consider adding thermostatSetpoint to the device driver as well?

I'm the developer of SharpTools.io (web dashboard and Rule Engine) and a user reported that the thermostat tiles weren't working as expected. As we dug into it, we found that the driver isn't reporting the thermostatSetpoint attribute as expected by the Thermostat capability.

You could probably do something like the following near line 213:

    if (map) {
        result = createEvent(map)
        if(map.name == "coolingSetpoint" || map.name == "heatingSetpoint"){
            result = [result, createEvent(name: "thermostatSetpoint", value: map.value, unit: map.unit)]
        }
    }

Basically just report the thermostatSetpoint any time either the heatingSetpoint or coolingSetpoint change.

1 Like

I will take a look at this...it doesn't look difficult.

But if the user doesn't need the Hold functionality, then I think that the built-in Zigbee Thermostat driver should work. My husband REALLY wanted the Hold functionality so that's why I cobbled together that driver.

@mike.maxwell, is it feasible to add the Hold functionality to the built-in Zigbee Thermostat driver? That would get rid of my funky driver altogether...

3 Likes

Is this the issue where the smaller tile just shows 0.0? I've had to keep my tiles on the double size to show the actual temp properly.

I didn't see a screenshot, but they mentioned that it couldn't control the temperature and didn't display the current temperature.

The SharpTools single-height thermostat tile uses the thermostatSetpoint attribute for the main display. Behind the scenes, when you change the temperature it sets the heating or cooling setpoint based on the mode.

As you noted, one workaround is to use the double height thermostat as it directly uses the coolingSetpoint and heatingSetpoint attributes.

If the driver implements thermostatSetpoint, sometimes it's nice to have the slimmer single height thermostat. :slight_smile:

1 Like

@denise.grider Thank you for sharing your version of this driver. I recently had to replace a Zen, that had taken on a mind of it's own, and chose the Pearl. The hold and level up/down commands are especially helpful for integrating with Alexa. Thanks again!

2 Likes

Hi, I finally got my hands on a Pearl thermostat, I just have the batteries in so that I can add it to Hubitat and play around with it before actually hooking it up to the house.

Using either of the drivers posted above, changing the Hold status on the physical thermostat does not update the value for thermostatHoldMode in the Hubitat device. I have to manually tell it to Refresh ... this isn't intended is it? Is anyone else experiencing this?

(Also, I'm curious as to the difference between the two drivers posted and which might be preferred?)

The built in driver does not have hold implemented.

yup, am aware (nice work there btw, detected no prob when adding).

Was referring to @denise.grider custom drivers in the posts above, numbers 1 and 4 in this thread.

Does the Centralite support "auto?" I've seen postings saying both yes and no. And, does it need a refresh Rule to keep the driver updated? I have a CT100 2gig and I need a one minute refresh rule to keep track of the "operatingState" as the driver doesn't always pick up with the device switches on/off the heating or AC.

The first post I messed up, but looks like somebody fixed it. The second post was properly formatted. Both pieces of code should be exactly the same.

I tested and when I set hold at the thermostat, it immediately gets updated on the device status page. I have my thermostat wired so I wonder if having yours connected via battery could be part of the problem. The thermostat display will go black after a certain amount of time to conserve battery when on battery power only, so perhaps reports are delayed for that reason too.

1 Like

No, you have to set Heat or Cool manually. But you could create a rule to set Heat or Cool based on your criteria.

And, does it need a refresh Rule to keep the driver updated?

I have no refresh rules and the driver works like a charm. But I do have it wired in, not running only off battery.

Appreciate the response - not having it wired was something i was concerned about, is why I mentioned it, I guess I'll pick the warmer day and do the swap this weekend :slight_smile: Hopefully will start seeing the same behavior as you.

Regarding the two drivers, they definitely have differences, the 2nd one has 10 more lines in it, and I threw them both into www.diffchecker.com and it highlights 6 places where there are code differences. You may want to take a peek.