[RELEASE] Device driver for Stelpro Maestro Thermostat (ZigBee)

Alright, so I took a swing at translating Stelpro's SmartThings driver for their Maestro thermostat.. It's basically a simplified version of the Ki driver.

Here is a pre-release version of the Hubitat driver. I've haven't tested it, so let me know if it works, or if any errors pops up in the Hubitat's log.

Note: That thermostat has an humidity sensor, which I haven't coded yet. Let's start with that temperature thing first!

/**
 *  Copyright 2020 Philippe Charette
 *  Copyright 2018 Stelpro
 *
 *  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.
 *
 *  Stelpro Ki ZigBee Thermostat Hubitat Driver
 *
 *  Notice: This file is a modified version of the SmartThings Device Hander, found in this repository:
 *           https://github.com/stelpro/maestro-thermostat
 *
 *  Author: Philippe Charette
 *  Author: Stelpro
 *
 *  Date: 2020-01-22
 */

metadata {
	definition (name: "Stelpro Maestro ZigBee Thermostat", namespace: "PhilC", author: "PhilC") {
        capability "Configuration"
        capability "TemperatureMeasurement"
        capability "Thermostat"
        capability "ThermostatHeatingSetpoint"
        capability "ThermostatCoolingSetpoint"
        capability "ThermostatSetpoint"
        capability "Refresh"

		fingerprint profileId: "0104", endpointId: "19", inClusters: " 0000,0003,0201,0204,0405", outClusters: "0402"
    }
    
    preferences {
        input("lock", "enum", title: "Do you want to lock your thermostat's physical keypad?", options: ["No", "Yes"], defaultValue: "No", required: false, displayDuringSetup: false)
        input name: "logEnable", type: "bool", title: "Enable debug logging", defaultValue: true
    }
}
    

def parse(String description) {
	logDebug "Parse description $description"
    def descMap = zigbee.parseDescriptionAsMap(description)
    logDebug "Desc Map: $descMap"
	def map = [:]
	if (description?.startsWith("read attr -")) {
		if (descMap.cluster == "0201" && descMap.attrId == "0000")
        {
			map.name = "temperature"
			map.value = getTemperature(descMap.value)
            if (descMap.value == "7FFD") {		//0x7FFD
                map.value = "low"
            }
            else if (descMap.value == "7FFF") {	//0x7FFF
                map.value = "high"
            }
            else if (descMap.value == "8000") {	//0x8000
                map.value = "--"
            }
            
            else if (descMap.value > "8000") {
                map.value = -(Math.round(2*(655.36 - map.value))/2)
            }
                        
            sendEvent(name:"temperature", value:map.value)
		}
        else if (descMap.cluster == "0201" && descMap.attrId == "0012") {
			logDebug "HEATING SETPOINT"
			map.name = "heatingSetpoint"
			map.value = getTemperature(descMap.value)
            if (descMap.value == "8000") {		//0x8000
                map.value = getTemperature("01F4")  // 5 Celsius (minimum setpoint)
            }
            sendEvent(name:"heatingSetpoint", value:map.value)
            sendEvent(name:"thermostatSetpoint", value:map.value)
		}
        /* else if (descMap.cluster == "0201" && descMap.attrId == "001C") {
            logDebug "MODE"
            if (descMap.value != "04") {
                map.name = "thermostatMode"
                map.value = getModeMap()[descMap.value]
                sendEvent(name:"thermostatMode", value:map.value) 
            }
            else {
                logDebug "descMap.value == \"04\". Ignore and wait for SETPOINT MODE"
            }
		}
        else if (descMap.cluster == "0201" && descMap.attrId == "401C") {
            logDebug "SETPOINT MODE"
            logDebug "descMap.value $descMap.value"
            if (descMap.value != "00") {
                map.name = "thermostatMode"
                map.value = getModeMap()[descMap.value]
                sendEvent(name:"thermostatMode", value:map.value)
            }
            else {
                logDebug "descMap.value == \"00\". Ignore and wait for MODE"
            }
		}*/
        else if (descMap.cluster == "0201" && descMap.attrId == "0008") {
        	logDebug "HEAT DEMAND"
            map.name = "thermostatOperatingState"
            map.value = getModeMap()[descMap.value]
            if (descMap.value < "10") {
            	map.value = "idle"
            }
            else {
            	map.value = "heating"
            }
            sendEvent(name:"thermostatOperatingState", value:map.value)
        }
	}

	def result = null
	if (map) {
		result = createEvent(map)
	}
	logDebug "Parse returned $map"
	return result
}

def logsOff(){
    log.warn "debug logging disabled..."
    device.updateSetting("logEnable",[value:"false",type:"bool"])
}

def refresh() {
    logDebug "refresh"
    def cmds = []
    
    cmds += zigbee.readAttribute(0x201, 0x0000) //Read Local Temperature
    cmds += zigbee.readAttribute(0x201, 0x0008) //Read PI Heating State  
    cmds += zigbee.readAttribute(0x201, 0x0012) //Read Heat Setpoint
    //cmds += zigbee.readAttribute(0x201, 0x001C) //Read System Mode
    //cmds += zigbee.readAttribute(0x201, 0x401C, [mfgCode: "0x1185"]) //Read System Mode
    
    cmds += zigbee.readAttribute(0x204, 0x0000) //Read Temperature Display Mode
    cmds += zigbee.readAttribute(0x204, 0x0001) //Read Keypad Lockout
    
    return cmds
}    

def logDebug(value){
    if (logEnable) log.debug(value)
}

def configure(){    
    log.warn "configure..."
    runIn(1800,logsOff)    
	logDebug "binding to Thermostat cluster"
    
    // Set unused default values (for Google Home Integration)
    sendEvent(name: "coolingSetpoint", value:getTemperature("0BB8")) // 0x0BB8 =  30 Celsius
    sendEvent(name: "thermostatFanMode", value:"auto")
    updateDataValue("lastRunningMode", "heat") // heat is the only compatible mode for this device
    
    def cmds = [
    //bindings
        "zdo bind 0x${device.deviceNetworkId} 1 0x019 0x201 {${device.zigbeeId}} {}", "delay 200"
        ]
    
    //reporting
    cmds += zigbee.configureReporting(0x201, 0x0000, 0x29, 10, 60, 50)   //Attribute ID 0x0000 = local temperature, Data Type: S16BIT
    cmds += zigbee.configureReporting(0x201, 0x0008, 0x20, 10, 900, 5)   //Attribute ID 0x0008 = pi heating demand, Data Type: U8BIT
    cmds += zigbee.configureReporting(0x201, 0x0012, 0x29, 1, 0, 50)     //Attribute ID 0x0012 = occupied heat setpoint, Data Type: S16BIT
    //cmds += zigbee.configureReporting(0x201, 0x001C, 0x30, 1, 0, 1)      //Attribute ID 0x001C = system mode, Data Type: 8 bits enum
    //cmds += zigbee.configureReporting(0x201, 0x401C, 0x30, 1, 0, 1, [mfgCode: "0x1185"])   	   //Attribute ID 0x401C = manufacturer specific setpoint mode, Data Type: 8 bits enum
    
    cmds += zigbee.configureReporting(0x204, 0x0000, 0x30, 1, 0)   	  //Attribute ID 0x0000 = temperature display mode, Data Type: 8 bits enum
    cmds += zigbee.configureReporting(0x204, 0x0001, 0x30, 1, 0)   	  //Attribute ID 0x0001 = keypad lockout, Data Type: 8 bits enum
    
    return cmds + refresh()
}

def getModeMap() { [
	"00":"off",
    "04":"heat",
	"05":"eco"
]}

def getTemperature(value) {
	if (value != null) {
    	logDebug("getTemperature: value $value")
		def celsius = Integer.parseInt(value, 16) / 100
		if (getTemperatureScale() == "C") {
			return celsius
		}
        else {
			return Math.round(celsiusToFahrenheit(celsius))
		}
	}
}

def getTemperatureScale() {
    return "${location.temperatureScale}"
}

def off() {
	logDebug "off"
    zigbee.writeAttribute(0x201, 0x001C, 0x30, 0)
}

def heat() {
	logDebug "heat"
    
    def cmds = []
    cmds += zigbee.writeAttribute(0x201, 0x001C, 0x30, 04, [:], 1000) // MODE
    //cmds += zigbee.writeAttribute(0x201, 0x401C, 0x30, 04, [mfgCode: "0x1185"]) // SETPOINT MODE    
    return cmds
}

/*def eco() {
	logDebug "eco"
    
    def cmds = []
    cmds += zigbee.writeAttribute(0x201, 0x001C, 0x30, 04, [:], 1000) // MODE
    cmds += zigbee.writeAttribute(0x201, 0x401C, 0x30, 05, [mfgCode: "0x1185"]) // SETPOINT MODE    
    return cmds
}*/

def cool() {
    log.info "cool mode is not available for this device. => Defaulting to off instead."
    off()
}

def auto() {
    log.info "auto mode is not available for this device. => Defaulting to heat mode instead."
    heat()
}

def emergencyHeat() {
    log.info "emergencyHeat mode is not available for this device. => Defaulting to heat mode instead."
	heat()
}

def fanAuto() {
    log.info "fanAuto mode is not available for this device"
}

def fanCirculate(){
    log.info "fanCirculate mode is not available for this device"
}

def fanOn(){
    log.info "fanOn mode is not available for this device"
}

def setSchedule(JSON_OBJECT){
    log.info "setSchedule is not available for this device"
}

def setThermostatFanMode(fanmode){
    log.info "setThermostatFanMode is not available for this device"
}

def setHeatingSetpoint(preciseDegrees) {
	if (preciseDegrees != null) {
		def temperatureScale = getTemperatureScale()
		def degrees = new BigDecimal(preciseDegrees).setScale(1, BigDecimal.ROUND_HALF_UP)
        
		logDebug "setHeatingSetpoint(${degrees} ${temperatureScale})"
        
        def celsius = (temperatureScale == "C") ? degrees as Float : (fahrenheitToCelsius(degrees) as Float).round(2)
        int celsius100 = Math.round(celsius * 100)
        
        zigbee.writeAttribute(0x201, 0x0012, 0x29, celsius100) //Write Heat Setpoint      
	}
}

def setCoolingSetpoint(degrees) {
    log.info "setCoolingSetpoint is not available for this device"
}

def setThermostatMode(String value) {
	logDebug "setThermostatMode({$value})"
	def currentMode = device.currentState("thermostatMode")?.value
	def lastTriedMode = state.lastTriedMode ?: currentMode ?: "heat"
	def modeNumber;
	Integer setpointModeNumber;
	def modeToSendInString;
    switch (value) {
        case "heat":
        case "emergency heat":
        case "auto":
            return heat()
        
        case "cool":        
        default:
            return off()
    }
}

def updated() {
    parameterSetting()
}

def parameterSetting() {
    def lockmode = null
    def valid_lock = 0

    log.info "lock : $settings.lock"
    if (settings.lock == "Yes") {
        lockmode = 0x01
        valid_lock = 1
    }
    else if (settings.lock == "No") {
        lockmode = 0x00
        valid_lock = 1
    }
    
    if (valid_lock == 1)
    {
    	log.info "lock valid"
        def cmds = []
        
        cmds+= zigbee.writeAttribute(0x204, 0x01, 0x30, lockmode)	//Write Lock Mode
        cmds+= refresh()
        return cmds
    }
    else {
    	log.info "nothing valid"
    }
}
2 Likes

So I just tested this with one of my thermostats and it works. I am able to change set-points and also able to read current temperature.

Strangely, none of that appears in the logs at all, but it is working. No errors yet either, but I will monitor on this one thermostat for a couple of hours and then start adding the rest.

Thanks Philippe, this is great.

Great! I'm happy to hear it. If there were any errors it would show in the logs. In the meantime, if you want to see a bit more you must active the "Enable debug logging" option.

Next time I'll try and activate the humidity sensor. I'll keep you posted.

Ahh I see, it was shown to be enabled but I just disabled and re enabled it and now I am getting the logs in.

I'm going to start adding the rest of the thermostats in the next few hours. I'll keep you posted.

Cheers,

So far so good. 4/6 added. The other two are in rooms where people are still sleeping so I'll have to wait on those.

I did discover that in order to have the SPM pair correctly, that the Huibitat needed to be moved within close range, regardless of whatever mesh had been established.

Food for thought for the next person having some issues with the pairing process.

Added the rest. No issues.

Some points to consider.

  1. The Maestro doesn't really have heating modes. (I think Hubitat may need to see that info in a thermostat,) but at the end of the day, baseboard thermostats only have a few functions, heat up, heat down, and in this case, fan on or off. Looking at the smartthings driver as well as the hubitat driver that you wrote, there seems to be many other functions. I would even argue that Standby mode is really just bringing the setpoint all the way down to 5 degrees.

What do you think? Is most of the stuff int he driver just a carryover from other thermostats? Do you find that all these functions crowd your Ki driver as well?

Yeah, I had the same Issue with the Ki because I wanted compatibility with Google Home. And as far as I understand I need to implement the extra features (modes, cooling, etc...) for it to work, even if in reality it doesn't do anything.

Also, just to make sure, is the Maestro supposed to have seperate fan control? Because... I haven't coded that!

It does have a fan control, but I have it turned off because I only have baseboards. I think it is for those units you see in washrooms with fans attached. But like the Ki, the Maestro is a two wire thermostat. Very simple stuff. While the interface on the unit itself has a lot of features, (motion sensing night light, humidity sensor, keypad lockout), like all other line voltage thermostats, it just regulates voltage to the baseboard heaters.

EDIT. If you don't mind, I may play around a little with the driver you wrote. I am actually moving away from Google home, and I'd like to edit it to see how it looks if i take some of those features out.

Having the humidity sensors is a really nice touch on this thermostat. There are many use cases (especially in the washrooms and kitchen).

Cheers,

I have the Maestro as well and actually have been using it with HE and have actually been using it with @philippe.charette's Stelpro Ki driver (I can't remember if I had to make any edits or not). For basic usage it works fine - although I'll probably try this new Maestro driver later this week.

But, the main reason I chimed in here is regarding "Fan mode" on the thermostat. The thermostat in my living room controls a "baseboard" heater that looks like (and I use that term loosely) a fireplace and has a fan to distribute the heat. The fan itself has a physical switch which either disables it completely (but still allows the heater to be controlled by the thermostat) or enables it so that it can turn on whenever the baseboard is turned on by the thermostat.

What I found was that if I enable the fan in the "fireplace" but have the "fan mode" disabled on the thermostat then the fan makes a horrific noise. It sounds like if the fan turns on and off, about once per second (and to be honest it sounds not only like if it is being turned off, but physically stopped - there is a lot of banging).

Once you turn "fan mode" on though - smooth sailing.

Yes indeed. I think fans are quite present in kick heaters and also in some washroom versions. In my home, we only have baseboards, so I've never had a use for the fan mode. In the Maestro's I know you have to ENABLE fan mode either from the device itself or from the HUB.

Cheers,

Also, @philippe.charette's Maestro driver has been flawless for me.

Oh, you guys are talking about that fan option! The one directly on the thermostat, to indicate that it controls a fan-pushed heater. With the little fan icon in the display. Right?

That setting is only available on the device itself. My driver is not coded to modify that setting, and there are no traces of it in the official SmartThings driver either.

But I can see where the confusion stems from. The are "fan controls" on the device's page. But those functions don't do anything (other than write a little message in the logs). They are there only for the Google Home compatibility. And I agree with you @philippompili, I'd be happier without all that superfluous stuff!

What I have noticed, is that under the dashboard tiles, there are no "up/down" setpoint control for the Maestro.

Does the dashboard tiles come from the device driver? I thought they were generic and then just overlayes on the device.

Hmmm, I'm not sure how the dashboard tiles work. On my end I see the up/down arrows in my Ki thermostat's tile, but only if it is in "heat" mode. If it's "off" or "eco", I don't see the arrows.

So this is how it looks. Top row is my ecobee stuff and the rest is the stelpro stuff

Btw, this isn't a big deal for me as I rarely control stuff from the dashboard (I really do try tof focus on automation vs control)

That's not normal. I've noted the problem and I'll try and see how I can fix it... Eventually!

FWIW, I've switched one of my Maestro's to your driver and my dashboard Tile appears properly

Ugghh I wonder what it is then?

Philippe, one more thing I've noticed. The thermostats do not seem to report back to hubitat unless Refreshed.

I can change the temperature form the hubitat and that works just fine. When I change the temperature from the thermostat itself, the hubitat will continue to show the previous temperature.

I let this go for about 12hrs and still had the old temp, despite the temp on the thermostat being different. When I hit refresh on the device driver on Hubitat , it started reporting the correct and updated temperature.

Anyway to force it to refresh on every single change? (will that overwhelm the system)?


this is what my device looks like on Hubitat. You'll notice that there are no Up/Down buttons on there either.

Cheers,

Are all your thermostats configured? Make sure you hit the configure button, once for each thermostat. It's with that function that the hub subscribes with the device. That should take care of the report issue (hopefully).

Ahh, ok I had never hit that button. Coming from Vera, the Configure was something you never touched for risk of crashing your thermostats.

Ok. I will configure them all, give it some time, and get back.