Integration of z-Wave /danfoss thermostatw

Thanks for that :slight_smile:

DH updated from the code pasted above.

For the app i might be able to offer you somewhere to start? I’ve just added the capability “Thermostat” to your code so that I can use it with another app that I already have installed . The app in question comes courtesy of @meavydev and can be found here:

I’ve just applied a schedule to a Danfoss TRV (which is no selectable form the app following the addition of the thermostat capability).

I’ve seen it change the set point. I’m just waiting for a wakeup.

edit: all looks to be working well. FYI. I’m pretty sure that the minimum wakeup for the Danfoss TRV is 5 mins.

Top job. Thanks again.

Great thanks,

Ill have a look.

Its correct that 300 secs are recommended but the device accepts 60 as minimum value. The batteries seems to be unaffected so i went for the 60 secs to keep et snappy :slight_smile:

Only reason I mention is because I set to 60 but the TRV only woke up every 5 mins. That may relate to the FW version on the TRV though.

Check that it sends the configuration on the next wake up when the wake up interval is changed.

I was under the impression (im new to this smartthings stuff) that adding a capability like “Thermostat” requires you to implement all the methods and attributes that belongs to the capability. Since most were not needed i chose a more specific capability.

I have added the Thermostat capability with all its attributes and commands. All commands but one is just dummy commands that does nothing.

Edit: Added command definitions

/**
 *
 *
 *  Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
 *  in compliance with the License. You may obtain a copy of the License at:
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
 *  on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
 *  for the specific language governing permissions and limitations under the License.
 *
 * 
 */

metadata 
{
    definition (name: "Danfoss Living Connect", namespace: "NorthQ", author: "René Bechmann") 
    {
        capability "Thermostat"
        capability "Battery"

    command "setHeatingSetpoint"
    command "setCoolingSetpoint"
    command "off"
    command "heat"
    command "emergencyHeat"
    command "cool"
    command "setThermostatMode"
    command "fanOn"
    command "fanAuto"
    command "fanCirculate"
    command "setThermostatFanMode"
    command "auto"
        
		// Thermostat capability        
        attribute "temperature", "string"
        attribute "heatingSetpoint", "string"
        attribute "coolingSetpoint", "string"
        attribute "thermostatSetpoint", "string"
        attribute "thermostatMode", "string"
        attribute "thermostatFanMode", "string"
        attribute "thermostatOperatingState", "string"

		// Battery capability        
        attribute "battery", "string"

		// Custom
        attribute "nextHeatingSetpoint", "string"
               
        fingerprint deviceId: "0x0804"
        fingerprint inClusters: "0x80, 0x46, 0x81, 0x72, 0x8F, 0x75, 0x43, 0x86, 0x84"
    }

    simulator {}

    tiles (scale: 2)
    {
        multiAttributeTile(name:"thermostatFull", type:"thermostat", width:6, height:4) 
        {
            tileAttribute("device.heatingSetpoint", key: "PRIMARY_CONTROL") 
            {
                attributeState("default", label:'${currentValue}°', unit:"",
                      backgroundColors:[
                           [value: 0, color: "#ededed"],
                           [value: 4, color: "#153591"],
                           [value: 16, color: "#178998"],
                           [value: 18, color: "#199f5c"],
                           [value: 20, color: "#2da71c"],
                           [value: 21, color: "#5baa1d"],
                           [value: 22, color: "#8aae1e"],
                           [value: 23, color: "#b1a81f"],
                           [value: 24, color: "#b57d20"],
                           [value: 26, color: "#b85122"],
                           [value: 28, color: "#bc2323"]])
            }
            
            tileAttribute("device.nextHeatingSetpoint", key: "SECONDARY_CONTROL") 
            {
                attributeState("default", label:'${currentValue}° next', unit:"")
            }
        }
        
        controlTile("nextHeatingSetpointSlider", "device.nextHeatingSetpoint", "slider", height: 1, width: 6, inactiveLabel: false, range:"(4..28)" ) 
        {
            state "heatingSetpoint", action: "setHeatingSetpoint", backgroundColor:"#d04e00"
        }
        
        valueTile("batteryTile", "device.battery", inactiveLabel: true, decoration: "flat", width: 1, height: 1) 
        {
            tileAttribute ("device.battery", key: "PRIMARY_CONTROL")
            {
                state "default", label:'${currentValue}% battery', unit:"%"
            }
        }

        main "thermostatFull"
        details(["thermostatFull", "nextHeatingSetpointSlider", "batteryTile"])
    }
    
    preferences 
    {
        input "wakeUpInterval", "number", title: "Wake up interval", description: "Seconds until next wake up notification", range: "60..3600", displayDuringSetup: true
    }
}

def parse(String description) 
{
    def results = null
    
     def cmd = zwave.parse(description,[0x80: 1, 0x46: 1, 0x81: 1, 0x72: 2, 0x8F: 1, 0x75: 2, 0x43: 2, 0x86: 1, 0x84: 2])

    if (cmd != null &&
        (cmd instanceof physicalgraph.zwave.commands.batteryv1.BatteryReport ||
         cmd instanceof physicalgraph.zwave.commands.thermostatsetpointv2.ThermostatSetpointReport ||
         cmd instanceof physicalgraph.zwave.commands.wakeupv2.WakeUpNotification)) 
    {
        results = zwaveEvent(cmd)
    }
    
    return results
}

def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) 
{
        def eventList = []
        
        if (cmd.batteryLevel != (state.battery ?: -1))
        {  
            if (cmd.batteryLevel == 0xFF) 
            {  
                eventList << createEvent(descriptionText: "Device reports low battery", isStateChange: true)    
                eventList << createEvent(name:"battery", value: 1, unit: "%", displayed: false)
            } 
            else 
            {
                eventList << createEvent(descriptionText: "Device reports ${cmd.batteryLevel}% battery", isStateChange: true)    
                eventList << createEvent(name:"battery", value: cmd.batteryLevel, unit: "%", displayed: false)
                state.batery = cmd.batteryLevel
            }
           }

        state.lastbatt = new Date().time
        
         eventList
}

def zwaveEvent(physicalgraph.zwave.commands.thermostatsetpointv2.ThermostatSetpointReport cmd) 
{
    def eventList = []

    def value = convertTemperatureIfNeeded(cmd.scaledValue, (cmd.scale == 1 ? "F" : "C"), cmd.precision)

    value = Double.parseDouble(value).toString() - ".0"
    
    def descriptionText = "Device reports ${value}°"
    eventList << createEvent(name:"heatingSetpoint", value: value, unit: getTemperatureScale(), isStateChange: true, descriptionText: descriptionText)
    log.debug(descriptionText)
    
    state.heatingSetpoint = value;

    state.size = cmd.size
    state.scale = cmd.scale
    state.precision = cmd.precision
    
    eventList
}

def zwaveEvent(physicalgraph.zwave.commands.wakeupv2.WakeUpNotification cmd) 
{
    def eventList = []
  
     try 
    {
        def battery = (state.batery ?: "")
        
        if (battery == "")
        {
            log.debug("Requesting batery level")
            eventList << response(zwave.batteryV1.batteryGet())
            eventList << response("delay 1200")
        }

        def heatingSetpoint = (state.heatingSetpoint ?: "")
        def nextHeatingSetpoint = (state.nextHeatingSetpoint ?: "")

        if (nextHeatingSetpoint != "")
        {
            if (heatingSetpoint != nextHeatingSetpoint)
            {
                log.debug("Device is set to ${nextHeatingSetpoint}°")

                eventList << response(zwave.thermostatSetpointV2.thermostatSetpointSet(setpointType: 1, scale: 0, precision: 1, scaledValue: new BigDecimal(nextHeatingSetpoint)).format())
                eventList << response("delay 1200")
                eventList << response(zwave.thermostatSetpointV2.thermostatSetpointGet(setpointType: 1).format())
                eventList << response("delay 1200")
            }
            else
            {
                log.debug ("nextHeatingSetpoint is equal to heatingSetpoint. No action taken")
            }
            
            state.nextHeatingSetpoint = ""
        }
        else if (heatingSetpoint == "")
        {
            log.debug("Requesting thermostat set point")
            eventList << response(zwave.thermostatSetpointV2.thermostatSetpointGet(setpointType: 1).format())
            eventList << response("delay 1200")
        }

        def interval = (wakeUpInterval ?: 60)

        if ((state.configured ?: "false") == "false" ||
            (state.currentWakeUpInterval ?: 0) != interval)
        {
            log.debug("Configuration is sent to device. Wake up Interval: ${interval} seconds")
            
            eventList << response(zwave.configurationV1.configurationSet(parameterNumber:1, size:2, scaledConfigurationValue:100).format())
            eventList << response("delay 1200")
            eventList << response(zwave.associationV1.associationSet(groupingIdentifier:1, nodeId:[zwaveHubNodeId]).format())
            eventList << response("delay 1200")
            eventList << response(zwave.wakeUpV1.wakeUpIntervalSet(seconds:interval, nodeid:zwaveHubNodeId).format())
            eventList << response("delay 1200")
            
            state.currentWakeUpInterval = interval
            state.configured = "true"
        }
    }
    catch (all)
    {
    }
    
    eventList << response(zwave.wakeUpV1.wakeUpNoMoreInformation())
   
    eventList
}

def setHeatingSetpoint(number) 
{
    def deviceScale = state.scale ?: 2
    def deviceScaleString = deviceScale == 2 ? "C" : "F"
    def locationScale = getTemperatureScale()
    def p = (state.precision ?: 1)

    Double convertedDegrees = number
    
    if (locationScale == "C" && 
        deviceScaleString == "F") 
    {
        convertedDegrees = celsiusToFahrenheit(degrees)
    } 
    else if (locationScale == "F" && 
             deviceScaleString == "C") 
    {
        convertedDegrees = fahrenheitToCelsius(degrees)
    } 
    
    def value = convertedDegrees.toString() - ".0"

    sendEvent(name:"nextHeatingSetpoint", value: value, displayed: false , isStateChange: true)
    log.debug ("Setting device to ${value}° on next wake up")
    
    state.nextHeatingSetpoint = value
}

def setCoolingSetpoint(number)
{
	// Not implemented
}

def off()
{
	// Not implemented
}

def heat()
{
	// Not implemented
}

def emergencyHeat()
{
	// Not implemented
}

def cool()
{
	// Not implemented
}

def setThermostatMode(string)
{
	// Not implemented
}

def fanOn()
{
	// Not implemented
}

def fanAuto()
{
	// Not implemented
}

def fanCirculate()
{
	// Not implemented
}

def setThermostatFanMode(string)
{
	// Not implemented
}

def auto()
{
	// Not implemented
}

Thanks for the hard work everyone! i just got mine working :slight_smile:

Here is my first Smart app :slight_smile: and also my latest device handler.

The smart app receives the thermostat temperature from a single master and passes it along to a set of other thermostats. Remember not to select the master thermostat in the list of slave thermostats :wink:

/**
 *  Danfoss Thermostats
 *
 *  Copyright 2016 Rene Bechmann
 *
 *  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.
 *
 */
definition(
    name: "Danfoss Living Connect Thermostats - Follow my lead",
    namespace: "NorthQ",
    author: "Rene Bechmann",
    description: "Controlling Danfoss Living Connect thermostats. Syncronizing a set of thermostats with a single master thermostat",
    category: "Convenience",
    iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png",
    iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png",
    iconX3Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png")

preferences 
{
    section("Choose master thermostat... ") 
    {
        input "masterThermostat", "capability.thermostat"
    }
    section("Choose slave thermostats... ") 
    {
        input "slaveThermostats", "capability.thermostat", multiple: true
    }
}

def installed()
{
    subscribe(masterThermostat, "heatingSetpoint", heatingSetpointHandler)
}

def updated()
{
    unsubscribe()
    subscribe(masterThermostat, "heatingSetpoint", heatingSetpointHandler)
}

def heatingSetpointHandler(evt)
{
    log.debug "New master Heating Set Point received: ${evt.value}"
    for (def thermostat in slaveThermostats)
    {
	    log.debug "Invoking ${thermostat}.setHeatingSetpoint(BigDe${evt.value})"
    	thermostat.setHeatingSetpoint(Double.parseDouble(evt.value))
    }
}

// catchall
def event(evt)
{
    //log.debug "value: $evt.value, event: $evt, settings: $settings, handlerName: ${evt.handlerName}"
}

/**
     *
     *
     *  Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
     *  in compliance with the License. You may obtain a copy of the License at:
     *
     *      http://www.apache.org/licenses/LICENSE-2.0
     *
     *  Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
     *  on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
     *  for the specific language governing permissions and limitations under the License.
     *
     * 
     */

    metadata 
    {
        definition (name: "Danfoss Living Connect", namespace: "NorthQ", author: "René Bechmann") 
        {
            capability "Thermostat"
            capability "Battery"

            command "setHeatingSetpoint"
            command "setCoolingSetpoint"
            command "off"
            command "heat"
            command "emergencyHeat"
            command "cool"
            command "setThermostatMode"
            command "fanOn"
            command "fanAuto"
            command "fanCirculate"
            command "setThermostatFanMode"
            command "auto"
            
    		// Thermostat capability        
            attribute "temperature", "string"
            attribute "heatingSetpoint", "string"
            attribute "coolingSetpoint", "string"
            attribute "thermostatSetpoint", "string"
            attribute "thermostatMode", "string"
            attribute "thermostatFanMode", "string"
            attribute "thermostatOperatingState", "string"

    		// Battery capability        
            attribute "battery", "string"

            fingerprint deviceId: "0x0804"
            fingerprint inClusters: "0x80, 0x46, 0x81, 0x72, 0x8F, 0x75, 0x43, 0x86, 0x84"
        }

        simulator {}

        tiles (scale: 2)
        {
            multiAttributeTile(name:"thermostatFull", type:"thermostat", width:6, height:4) 
            {
                tileAttribute("device.heatingSetpoint", key: "PRIMARY_CONTROL") 
                {
                    attributeState("default", label:'${currentValue}°', unit:"",
                          backgroundColors:[
                               [value: 0, color: "#ededed"],
                               [value: 4, color: "#153591"],
                               [value: 16, color: "#178998"],
                               [value: 18, color: "#199f5c"],
                               [value: 20, color: "#2da71c"],
                               [value: 21, color: "#5baa1d"],
                               [value: 22, color: "#8aae1e"],
                               [value: 23, color: "#b1a81f"],
                               [value: 24, color: "#b57d20"],
                               [value: 26, color: "#b85122"],
                               [value: 28, color: "#bc2323"]])
                }
                
                tileAttribute("device.nextHeatingSetpoint", key: "SECONDARY_CONTROL") 
                {
                    attributeState("default", label:'${currentValue}° next', unit:"")
                }
            }
            
            controlTile("nextHeatingSetpointSlider", "device.nextHeatingSetpoint", "slider", height: 1, width: 6, inactiveLabel: false, range:"(4..28)" ) 
            {
                state "heatingSetpoint", action: "setHeatingSetpoint", backgroundColor:"#d04e00"
            }
            
            valueTile("batteryTile", "device.battery", inactiveLabel: true, decoration: "flat", width: 1, height: 1) 
            {
                tileAttribute ("device.battery", key: "PRIMARY_CONTROL")
                {
                    state "default", label:'${currentValue}% battery', unit:"%"
                }
            }

            main "thermostatFull"
            details(["thermostatFull", "nextHeatingSetpointSlider", "batteryTile"])
        }
        
        preferences 
        {
            input "wakeUpInterval", "number", title: "Wake up interval", description: "Seconds until next wake up notification", range: "60..3600", displayDuringSetup: true
        }
    }

    def parse(String description) 
    {
        def results = null
        
         def cmd = zwave.parse(description,[0x80: 1, 0x46: 1, 0x81: 1, 0x72: 2, 0x8F: 1, 0x75: 2, 0x43: 2, 0x86: 1, 0x84: 2])

        if (cmd != null &&
            (cmd instanceof physicalgraph.zwave.commands.batteryv1.BatteryReport ||
             cmd instanceof physicalgraph.zwave.commands.thermostatsetpointv2.ThermostatSetpointReport ||
             cmd instanceof physicalgraph.zwave.commands.wakeupv2.WakeUpNotification)) 
        {
            results = zwaveEvent(cmd)
        }
        
        return results
    }

    def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) 
    {
            def eventList = []
            
            if (cmd.batteryLevel != (state.battery ?: -1))
            {  
                if (cmd.batteryLevel == 0xFF) 
                {  
                	def descriptionText = "Device reports low battery"
                    eventList << createEvent(descriptionText: descriptionText, isStateChange: true)
                    log.debug(descriptionText)
                    eventList << createEvent(name:"battery", value: 1, unit: "%", displayed: false)
                } 
                else 
                {
                	def descriptionText = "Device reports battery at ${cmd.batteryLevel}%"
                    eventList << createEvent(descriptionText: descriptionText, isStateChange: true)    
                    log.debug(descriptionText)
                    eventList << createEvent(name:"battery", value: cmd.batteryLevel, unit: "%", displayed: false)
                    state.batery = cmd.batteryLevel
                }
               }

            state.lastbatt = new Date().time
            
             eventList
    }

    def zwaveEvent(physicalgraph.zwave.commands.thermostatsetpointv2.ThermostatSetpointReport cmd) 
    {
        def eventList = []

        def value = convertTemperatureIfNeeded(cmd.scaledValue, (cmd.scale == 1 ? "F" : "C"), cmd.precision)

        value = Double.parseDouble(value).toString() - ".0"
        
        def descriptionText = "Device reports thermostat at ${value}°"
        eventList << createEvent(name:"heatingSetpoint", value: value, unit: getTemperatureScale(), isStateChange: true, descriptionText: descriptionText)
        log.debug(descriptionText)
        
        state.heatingSetpoint = value;

        state.size = cmd.size
        state.scale = cmd.scale
        state.precision = cmd.precision
        
        eventList
    }

    def zwaveEvent(physicalgraph.zwave.commands.wakeupv2.WakeUpNotification cmd) 
    {
        def eventList = []
      
        try 
        {
            log.debug("WakeUpNotification received")

    		def battery = (state.batery ?: "")
            
            if (battery == "")
            {
                log.debug("Requesting batery level")
                eventList << response(zwave.batteryV1.batteryGet())
                eventList << response("delay 1200")
            }

            def heatingSetpoint = (state.heatingSetpoint ?: "")
            def nextHeatingSetpoint = (state.nextHeatingSetpoint ?: "")

            if (nextHeatingSetpoint != "")
            {
                if (heatingSetpoint != nextHeatingSetpoint)
                {
                    log.debug("Device is set to ${nextHeatingSetpoint}°")

                    eventList << response(zwave.thermostatSetpointV2.thermostatSetpointSet(setpointType: 1, scale: 0, precision: 1, scaledValue: new BigDecimal(nextHeatingSetpoint)).format())
                    eventList << response("delay 1200")
                    eventList << response(zwave.thermostatSetpointV2.thermostatSetpointGet(setpointType: 1).format())
                    eventList << response("delay 1200")
                }
                else
                {
                    log.debug ("nextHeatingSetpoint is equal to heatingSetpoint. No action taken")
                }
                
                state.nextHeatingSetpoint = ""
            }
            else if (heatingSetpoint == "")
            {
                log.debug("Requesting thermostat set point")
                eventList << response(zwave.thermostatSetpointV2.thermostatSetpointGet(setpointType: 1).format())
                eventList << response("delay 1200")
            }

            def interval = (wakeUpInterval ?: 60)

            if ((state.configured ?: "false") == "false" ||
                (state.currentWakeUpInterval ?: 0) != interval)
            {
                log.debug("Configuration is sent to device. Wake up Interval: ${interval} seconds")
                
                eventList << response(zwave.configurationV1.configurationSet(parameterNumber:1, size:2, scaledConfigurationValue:100).format())
                eventList << response("delay 1200")
                eventList << response(zwave.associationV1.associationSet(groupingIdentifier:1, nodeId:[zwaveHubNodeId]).format())
                eventList << response("delay 1200")
                eventList << response(zwave.wakeUpV1.wakeUpIntervalSet(seconds:interval, nodeid:zwaveHubNodeId).format())
                eventList << response("delay 1200")
                
                state.currentWakeUpInterval = interval
                state.configured = "true"
            }
        }
        catch (all)
        {
        }
        
        eventList << response(zwave.wakeUpV1.wakeUpNoMoreInformation())
       
        eventList
    }

    def setHeatingSetpoint(number) 
    {
        def deviceScale = state.scale ?: 2
        def deviceScaleString = deviceScale == 2 ? "C" : "F"
        def locationScale = getTemperatureScale()
        def p = (state.precision ?: 1)

        Double convertedDegrees = number
        
        if (locationScale == "C" && 
            deviceScaleString == "F") 
        {
            convertedDegrees = celsiusToFahrenheit(degrees)
        } 
        else if (locationScale == "F" && 
                 deviceScaleString == "C") 
        {
            convertedDegrees = fahrenheitToCelsius(degrees)
        } 
        
        def value = convertedDegrees.toString() - ".0"

        sendEvent(name:"nextHeatingSetpoint", value: value, displayed: false , isStateChange: true)
        log.debug ("Setting device to ${value}° on next wake up")
        
        state.nextHeatingSetpoint = value
    }

    def setCoolingSetpoint(number)
    {
    	// Not implemented
    }

    def off()
    {
    	// Not implemented
    }

    def heat()
    {
    	// Not implemented
    }

    def emergencyHeat()
    {
    	// Not implemented
    }

    def cool()
    {
    	// Not implemented
    }

    def setThermostatMode(string)
    {
    	// Not implemented
    }

    def fanOn()
    {
    	// Not implemented
    }

    def fanAuto()
    {
    	// Not implemented
    }

    def fanCirculate()
    {
    	// Not implemented
    }

    def setThermostatFanMode(string)
    {
    	// Not implemented
    }

    def auto()
    {
    	// Not implemented
    }
2 Likes

Anybody have problems setting the temperature from the App on the phone? After i tried this new device handler, ALL of my thermostats has stopped responding to “next setpoint” :frowning: It will however get the temp from the thermostat if i set it manually there…

For me it works fine using the mobile app. One time only i noticed that one thermostat kept sending wake up every few seconds. No wake up interval could change that until i reset the device.

I’m still trying to figure out how to set the temperature using Amazon Echo Dot. I think an extra capability is needed. Alexa says the device doesn’t accept input.

Try removing the device from the mobile app and add it again.

Edit: My thermotats are called “NQ-500-EU”

Edit 2: New and more specific fingerprint statement in Device Handler Definition:

        // Danfoss thermostat fingerprint
        // Raw device data: zw:S type:0804 mfr:0002 prod:0005 model:0003 ver:2.51 zwv:2.67 lib:06 cc:80,46,81,72,8F,75,43,86,84 ccOut:46,81,8F
        // http://products.z-wavealliance.org/products/967
        // Supported classes: http://products.z-wavealliance.org/products/967/classes
        
        fingerprint type: "0804", mfr: "0002", prod: "0005", model: "0003", cc: "80,46,81,72,8F,75,43,86,84", ccOut:"46,81,8F"

A ScheduleOverrideReport is recieved from the Thermostat. Does anybody know what it means and what it can be used for?

Schedule Override Report received: ScheduleOverrideReport(overrideState: 127, overrideType: 0, reserved01: 0)

Kind regards

René

I have a new version that works with Amazon Alexa voice commands!

It will also set the clock in the thermostat once a day.

Experimenting with schedules…

/**
 *
 *
 *  Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
 *  in compliance with the License. You may obtain a copy of the License at:
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
 *  on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
 *  for the specific language governing permissions and limitations under the License.
 *
 * 
 */

metadata 
{
    definition (name: "Danfoss Living Connect", namespace: "NorthQ", author: "René Bechmann") 
    {
    	// This handler has commands
        capability "Actuator"
        
        // This handler has attributes
        capability "Sensor"
        
		// Thermostat capability        
        capability "Thermostat"

		command "setHeatingSetpoint"
        command "setCoolingSetpoint"
        command "off"
        command "heat"
        command "emergencyHeat"
        command "cool"
        command "setThermostatMode"
        command "fanOn"
        command "fanAuto"
        command "fanCirculate"
        command "setThermostatFanMode"
        command "auto"
        
        attribute "temperature", "string"
        attribute "heatingSetpoint", "string"
        attribute "coolingSetpoint", "string"
        attribute "thermostatSetpoint", "string"
        attribute "thermostatMode", "string"
        attribute "thermostatFanMode", "string"
        attribute "thermostatOperatingState", "string"

		// Battery capability        
        capability "Battery"

		attribute "battery", "string"

		// Danfoss thermostat fingerprint "NQ-500-EU"
		// https://store.northq.com/products/danfoss-living-connect-nq-500-eu
		// https://cdn.shopify.com/s/files/1/1238/1276/files/Danfoss_Living_Connect_Specifications.pdf?583524900071384087
		// Raw device data: zw:S type:0804 mfr:0002 prod:0005 model:0003 ver:2.51 zwv:2.67 lib:06 cc:80,46,81,72,8F,75,43,86,84 ccOut:46,81,8F
		// http://products.z-wavealliance.org/products/932
		// Supported classes: http://products.z-wavealliance.org/products/932/classes
		// Product web site:  Product Website: http://heating.consumers.danfoss.com/xxTypex/585379.html

		fingerprint type: "0804", mfr: "0002", prod: "0005", model: "0003", cc: "80,46,81,72,8F,75,43,86,84", ccOut:"46,81,8F"

		//fingerprint deviceId: "0x0804"
        //fingerprint inClusters: "0x80, 0x46, 0x81, 0x72, 0x8F, 0x75, 0x43, 0x86, 0x84"
        
    }

    simulator {}

    tiles (scale: 2)
    {
        multiAttributeTile(name:"thermostatFull", type:"thermostat", width:6, height:4) 
        {
            tileAttribute("device.heatingSetpoint", key: "PRIMARY_CONTROL") 
            {
                attributeState("default", label:'${currentValue}°', unit:"",
                      backgroundColors:[
                           [value: 0, color: "#ededed"],
                           [value: 4, color: "#153591"],
                           [value: 16, color: "#178998"],
                           [value: 18, color: "#199f5c"],
                           [value: 20, color: "#2da71c"],
                           [value: 21, color: "#5baa1d"],
                           [value: 22, color: "#8aae1e"],
                           [value: 23, color: "#b1a81f"],
                           [value: 24, color: "#b57d20"],
                           [value: 26, color: "#b85122"],
                           [value: 28, color: "#bc2323"]])
            }
            
            tileAttribute("device.nextHeatingSetpoint", key: "SECONDARY_CONTROL") 
            {
                attributeState("default", label:'${currentValue}° next', unit:"")
            }
        }
        
        controlTile("nextHeatingSetpointSlider", "device.nextHeatingSetpoint", "slider", height: 1, width: 6, inactiveLabel: false, range:"(4..28)" ) 
        {
            state "heatingSetpoint", action: "setHeatingSetpoint", backgroundColor:"#d04e00"
        }
        
        valueTile("batteryTile", "device.battery", inactiveLabel: true, decoration: "flat", width: 1, height: 1) 
        {
            tileAttribute ("device.battery", key: "PRIMARY_CONTROL")
            {
                state "default", label:'${currentValue}% battery', unit:"%"
            }
        }

        main "thermostatFull"
        details(["thermostatFull", "nextHeatingSetpointSlider", "batteryTile"])
    }
    
    preferences 
    {
        input "wakeUpInterval", "number", title: "Wake up interval", description: "Seconds until next wake up notification", range: "60..3600", displayDuringSetup: true
    }
}

def installed()
{
    try
    {
        heat()
        sendEvent(name:"wakeUpInterval", value: "300", displayed: false)
	}
    catch (Exception ex)
    {
    	log.error "$ex"
    }
}

def parse(String description) 
{
    def results = null
    
    try
    {
		def cmd = zwave.parse(description) //(description,[0x80: 1, 0x46: 1, 0x81: 1, 0x72: 2, 0x8F: 1, 0x75: 2, 0x43: 2, 0x86: 1, 0x84: 2])

        if (cmd != null)
        {
            results = zwaveEvent(cmd)
        }
   	}
    catch (Exception ex)
    {
    	log.error "$ex"
    }

    return results
}

def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) 
{
	def eventList = []

	try
	{
		def battery = (state.battery ?: -1)

		if (cmd.batteryLevel != battery)
		{  
            if (cmd.batteryLevel == 0xFF) 
            {  
                eventList << createEvent(descriptionText: "Device reports low battery", isStateChange: true)
                log.debug("Device reports low battery (new)")
                eventList << createEvent(name:"battery", value: 1, unit: "%", displayed: false)
            } 
            else 
            {
                eventList << createEvent(descriptionText: "Device reports battery at ${cmd.batteryLevel}%", isStateChange: true)    
                log.debug("Device reports battery at ${cmd.batteryLevel}% (new)")

                eventList << createEvent(name:"battery", value: cmd.batteryLevel, unit: "%", displayed: false)
                state.battery = cmd.batteryLevel
            }
        }
        else
        {
            if (cmd.batteryLevel == 0xFF) 
            {  
                log.debug("Device reports low battery (unchanged)")
            } 
            else 
            {
                log.debug("Device reports battery at ${cmd.batteryLevel}% (unchanged)")
            }
        }

        state.lastbatt = new Date().time
	}
    catch (Exception ex)
    {
    	log.error "$ex"
    }
        
    eventList
}

def zwaveEvent(physicalgraph.zwave.commands.thermostatsetpointv2.ThermostatSetpointReport cmd) 
{
    def eventList = []

    try
    {
        def heatingSetpoint = (state.heatingSetpoint ?: "")

        def value = convertTemperatureIfNeeded(cmd.scaledValue, (cmd.scale == 1 ? "F" : "C"), cmd.precision)

        value = Double.parseDouble(value).toString() - ".0"

        if (heatingSetpoint != value)
        {
            eventList << createEvent(name:"heatingSetpoint", value: value, unit: getTemperatureScale(), isStateChange: true, descriptionText: "Device reports thermostat at ${value}°")
            log.debug("Device reports thermostat at ${value}° (new)")

            state.heatingSetpoint = value;

            state.size = cmd.size
            state.scale = cmd.scale
            state.precision = cmd.precision
        }
        else
        {
            log.debug("Device reports thermostat at ${value}° (unchanged)")
        }
  	}
    catch (Exception ex)
    {
    	log.error "$ex"
    }

    eventList
}

def zwaveEvent(physicalgraph.zwave.commands.wakeupv2.WakeUpNotification cmd) 
{
    def eventList = []
    
    try
    {
        def delay = 300

        // This try catch block ensures that we allways send the wakeUpNoMoreInformation command
        try 
        {
            log.debug("WakeUpNotification received. Send delay: $delay")

            def heatingSetpoint = (state.heatingSetpoint ?: "")
            def nextHeatingSetpoint = (state.nextHeatingSetpoint ?: "")

            if (nextHeatingSetpoint != "")
            {
                if (heatingSetpoint != nextHeatingSetpoint)
                {
                    log.debug("Device is set to ${nextHeatingSetpoint}°")

                    eventList << response(zwave.thermostatSetpointV2.thermostatSetpointSet(setpointType: 1, scale: 0, precision: 1, scaledValue: new BigDecimal(nextHeatingSetpoint)).format())
                    eventList << response("delay $delay")
                    eventList << response(zwave.thermostatSetpointV2.thermostatSetpointGet(setpointType: 1).format())
                    eventList << response("delay $delay")
                }

                state.nextHeatingSetpoint = ""
            }
            else if (heatingSetpoint == "")
            {
                log.debug("Requesting thermostat set point")
                eventList << response(zwave.thermostatSetpointV2.thermostatSetpointGet(setpointType: 1).format())
                eventList << response("delay $delay")
            }

            def interval = (wakeUpInterval ?: 300)

            if ((state.configured ?: "false") == "false" ||
                (state.currentWakeUpInterval ?: 0) != interval)
            {
                log.debug("Wake up Interval set to ${interval} seconds")

                eventList << response(zwave.wakeUpV1.wakeUpIntervalSet(seconds:interval, nodeid:zwaveHubNodeId).format())
                eventList << response("delay $delay")

                state.currentWakeUpInterval = interval
                state.configured = "true"
            }

            def battery = (state.battery ?: "")

            if (battery == "")
            {
                log.debug("Requesting batery level")
                eventList << response(zwave.batteryV1.batteryGet())
                eventList << response("delay $delay")
            }

            // Set clock once a day
            def nowTime = new Date().time
            def ageInMinutes = state.lastClockSet ? (int)(nowTime - state.lastClockSet)/60000 : 1440

            if (ageInMinutes >= 1440) 
            {
                def nowCal = Calendar.getInstance(location.timeZone) // get current location timezone

                def weekday = nowCal.get(Calendar.DAY_OF_WEEK)
                def hour = nowCal.get(Calendar.HOUR_OF_DAY)
                def minute = nowCal.get(Calendar.MINUTE)

                log.debug "Setting clock to weekday:$weekday hour:$hour minute:$minute"

                eventList << response(zwave.clockV1.clockSet(hour: hour, minute: minute, weekday: weekday).format())
                eventList << response("delay $delay")

                state.lastClockSet = nowTime
                state.clock = ""
            }

            ageInMinutes = 0 // Clock poll every 5 minutes disabled because we are not using it currently
            //ageInMinutes = state.lastClockGet ? (int)(nowTime - state.lastClockGet)/60000 : 5
            def clock = state.clock ?: ""

            if (clock == "" ||
                ageInMinutes >= 5)
            {
                eventList << response(zwave.clockV1.clockGet().format())
                eventList << response("delay $delay")

                state.lastClockGet = nowTime
            }

            // Experimental
    /*
            eventList << response(zwave.commands.climatecontrolschedulev1.ScheduleOverrideGet().format())
            eventList << response("delay $delay")

            eventList << response(zwave.commands.climatecontrolschedulev1.ScheduleChangedGet().format())
            eventList << response("delay $delay")

            eventList << response(zwave.commands.climatecontrolschedulev1.ScheduleGet(weekDay: 1).format())
            eventList << response("delay $delay")
    */        
        }
        catch (Exception ex)
        {
            log.error "$ex"
        }

        eventList << response(zwave.wakeUpV1.wakeUpNoMoreInformation())

        log.debug "Number of events to send ${eventList.size}"
  	}
    catch (Exception ex)
    {
    	log.error "$ex"
    }
    
    eventList
}

def zwaveEvent(physicalgraph.zwave.commands.climatecontrolschedulev1.ScheduleOverrideReport cmd)
{
    try
    {
        switch(cmd.overrideState)
        {
            case physicalgraph.zwave.commands.climatecontrolschedulev1.ScheduleOverrideReport.OVERRIDE_STATE_NO_OVERRIDE:
                log.debug "ScheduleOverrideReport: No override"
                break;
            case physicalgraph.zwave.commands.climatecontrolschedulev1.ScheduleOverrideReport.OVERRIDE_STATE_TEMPORARY_OVERRIDE:
                log.debug "ScheduleOverrideReport: Temporary override"
                break;
            case physicalgraph.zwave.commands.climatecontrolschedulev1.ScheduleOverrideReport.OVERRIDE_STATE_PERMANENT_OVERRIDE:
                log.debug "ScheduleOverrideReport: Permanent override"
                break;
            case physicalgraph.zwave.commands.climatecontrolschedulev1.ScheduleOverrideReport.OVERRIDE_STATE_RESERVED3:
                log.debug "ScheduleOverrideReport: Reserved"
                break;
        }
	}
    catch (Exception ex)
    {
    	log.error "$ex"
    }
}

def zwaveEvent(physicalgraph.zwave.commands.climatecontrolschedulev1.ScheduleChangedReport cmd)
{
    try
    {
        log.debug "ScheduleChangedReport: $cmd"
	}
    catch (Exception ex)
    {
    	log.error "$ex"
    }
}

def zwaveEvent(physicalgraph.zwave.commands.clockv1.ClockReport cmd)
{
    try
    {
        log.debug "Device clock received: weekday:${cmd.weekday} hour:${cmd.hour} minute:${cmd.minute}"
        state.clock = "${cmd.weekday},${cmd.hour}:${cmd.minute}"
	}
    catch (Exception ex)
    {
    	log.error "$ex"
    }
}

def zwaveEvent(physicalgraph.zwave.Command cmd)
{
    try
    {
		log.debug "Catch all: $cmd"
	}
    catch (Exception ex)
    {
    	log.error "$ex"
    }
}

def setHeatingSetpoint(number) 
{
    try
    {
        def deviceScale = (state.scale ?: 2)
        def deviceScaleString = (deviceScale == 2 ? "C" : "F")
        def locationScale = getTemperatureScale()
        def p = (state.precision ?: 1)

        Double convertedDegrees = number

        if (locationScale == "C" && 
            deviceScaleString == "F") 
        {
            convertedDegrees = celsiusToFahrenheit(degrees)
        } 
        else if (locationScale == "F" && 
                 deviceScaleString == "C") 
        {
            convertedDegrees = fahrenheitToCelsius(degrees)
        } 

        def value = convertedDegrees.toString() - ".0"

        sendEvent(name:"nextHeatingSetpoint", value: value, displayed: false , isStateChange: true)
        log.debug ("Setting device to ${value}° on next wake up")

        state.nextHeatingSetpoint = value

		heat()
	}
    catch (Exception ex)
    {
    	log.error "$ex"
    }
}

def setCoolingSetpoint(number)
{
	// Not implemented
}

def off()
{
	// Not implemented
}

def heat()
{
    try
    {
        sendEvent(name:"thermostatMode", value: "heat", displayed: false)
	}
    catch (Exception ex)
    {
    	log.error "$ex"
    }
}

def emergencyHeat()
{
	// Not implemented
}

def cool()
{
	// Not implemented
}

def setThermostatMode(string)
{
	// Not implemented
}

def fanOn()
{
	// Not implemented
}

def fanAuto()
{
	// Not implemented
}

def fanCirculate()
{
	// Not implemented
}

def setThermostatFanMode(string)
{
	// Not implemented
}

def auto()
{
	// Not implemented
}

Here’s my reboot of my handler. I think I’ve got it working when manually setting the temperature. There’s a niggly bug where if you set the temperature on the app and then also set it on the thermostat. The app sends the temperature it has. It usually wins but if you’re upping/downing the temperature at steady pace the manual setting wins.

I’ve tested it out a bit on Android. Be good to hear if it works ok on the iPhone too :slight_smile:

Noteworthy features:

  • Quick on & off button in the Thing list so you can quickly turn the heating on or off (temperatures configurable)
  • Thermostat capability for smart app scheduling etc
  • Switch capability for turning the heat on & off - smartapps or automations etc
  • Manual temperature setting.

Any feedback welcome :slight_smile:

3 Likes

Great job. Things are moving fast on this issue…

You can add these capabilities to get it working with Amazon Alexa voice commands :slight_smile:

// This handler has commands
capability "Actuator"
        
// This handler has attributes
capability "Sensor"
1 Like

TIL :slight_smile:

Added those 2 capabilities. Do you know why/how that makes it work with Alexa work?

Without them Alexa says that it cant issue commands to the device so i guess its checking the Actuator capability to see if its supports commands. Actuator is an “empty” capability as is Sensor that just means there is attributes that can be read.

I also tried with Google Home today and got surprised when i said “Set temperature to 20 degress”, the thermostat mode was set to “cool” and and the coolingSetPoint to 20 degrees. However if the command was refrased to “Set the heating to 20 degrees” mode was set to “heat” and the heatingSetPoint to 20.

Edit:

I have had some success getting schedules from the thermostat. Cant set them yet but its interesting :slight_smile:

def scheduleGet(weekday)
{
	def rc = ""
    
    if (weekday >= 1 && weekday <= 7)
    {
		rc = "4602" + String.format("%02d", weekday)
    }
    
    return rc
}

def scheduleChangedGet()
{
	return "4604"
}

def scheduleOverrideGet()
{
	return "4607"
}

def zwaveEvent(physicalgraph.zwave.commands.climatecontrolschedulev1.ScheduleOverrideReport cmd)
{
try
{
    switch(cmd.overrideState)
    {
        case 0:
            log.debug "ScheduleOverrideReport: No override"
            break
        case 1:
            log.debug "ScheduleOverrideReport: Temporary override"
            break
        case 2:
            log.debug "ScheduleOverrideReport: Permanent override"
            break
        case 121:
            log.debug "ScheduleOverrideReport: Frost protection"
            break
        case 122:
            log.debug "ScheduleOverrideReport: Energy saving mode"
            break
        case 127:
            log.debug "ScheduleOverrideReport: Unused"
            break
        default:
            log.debug "ScheduleOverrideReport: Unknown"
            break
    }
	}
catch (Exception ex)
{
	log.error "$ex"
}
}

def zwaveEvent(physicalgraph.zwave.commands.climatecontrolschedulev1.ScheduleChangedReport cmd)
{
try
{
    log.debug "ScheduleChangedReport: $cmd"
	}
catch (Exception ex)
{
	log.error "$ex"
}
}

def zwaveEvent(physicalgraph.zwave.commands.climatecontrolschedulev1.ScheduleReport cmd)
{
try
{
    log.debug "Device schedule received: 1=${cmd.switchpoint1}, 2=${cmd.switchpoint2}, 3=${cmd.switchpoint3}, 4=${cmd.switchpoint4}, 5=${cmd.switchpoint5}, 6=${cmd.switchpoint6}, 7=${cmd.switchpoint7}, 8=${cmd.switchpoint8}, weekday=${cmd.weekday}"
	}
catch (Exception ex)
{
	log.error "$ex"
}
}

You can issue the commands like this:

           cmd << scheduleGet(1)
1 Like

I forgot to mention, You can move the battery status up in the main display area by adding: (after line 90 in your code)

    tileAttribute("device.battery", key: "SECONDARY_CONTROL") 
    {
        attributeState("default", label:'${currentValue}% batt', unit:"")
    }

I tried early on with the schedules too but ditched it, in favour of using a smartapp to control it. Or as it has the switch capability using one of the light switch smartapps to turn it on/off. Might work with Google Home too, as in “Turn on/off radiator”?

I tried it with the SECONDARY_CONTROL but because it’s using the thermostat multi-tile, I got a raindrop icon :frowning: Humidity I think. The battery tile does look a little lost by itself down there.

Just tried enabling the Switch capability and have a go with Google home. It dident work. It seems to know its a thermostat and can set it to ThermostatMode “Off” but tells me that a thermostat cannot be turned on and suggest me to “Turn on the heat”

PS I dont get a raindrop icon on my iphone.

Hmm, looks like it’s only seeing the ‘off’ of the Thermostat. I don’t have Google Home or Alexa. Unsure which to go with just yet. :thinking:

Google is much more “understanding” you can issue geric commands like “turn on kitchen lights” or “set the thermostats to 21 degrees”. Alexa needs more exact commands.

Alexa does have more integration options like IFTTT and Logitech Harmony.

You can get both :slight_smile: i do

On a side note - Remember to get the integer value of the degrees (double value) after converting between C and F units. Otherwise you will get really odd numbers like 18.00032455322234.