Zen thermostat code

I was trying to get my Zen Zigbee thermostat to work with both Alexa and Hubitat.
I found some code online to do this but it didn't really work for me. Code seemed over complicated for my single stage system so I trien to strip it down to something I could figure out and thought I'd share the result. As I am not a programmer by any means and don't understand the rules about posting here, please be kind. Any way this seems to work for me but probably has bugs.

/*
*

  • Simple ZEN Single Stage Thermostat Manager
  • Author: Ron Doan - based on original code by ELFEGE
  • Version: 1.0.0
  • Date: 04/21/2020
  • 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.
  • Notes:
  • Version 1.0.0
  • Simple ZEN single stage thermostat manager app using virtual dimmer (might work on other devices).
  • Thanks to ELFEGE for original source - this code is a stripped down verson of his code (with a few tweaks).
  • Supports single stage heating and cooling only - Thats all I need where I live and cannot test anything else.
  • Receives temperature input data from virtual dimmer switch which Alexa can also control.
  • Switches from heat to cold and from cold to heat based on inside temperature reading from thermostat when user changes temperature.
  • Should not interfere with manual programming of thermostat - but time will tell.
  • Does not use auto heating/cooling mode.
  • Does not support heat pump.
  • BUG - cannot switch fan mode - suggest manually setting fan mode to auto when using this app.
  • I am not a professional programmer and only spent a few evenings on this but so far so good.
    */

import java.text.SimpleDateFormat
import groovy.transform.Field
import groovy.json.JsonOutput

/* Globals */
@Field static int LastDimmerSetting = 0
@Field static boolean setpointSentByApp = false

definition(
name: "Simple ZEN Single Stage Thermostat Manager",
namespace: "Doan",
author: "DOAN based on original code by ELFEGE",
description: "Manage your thermostats with dimmer",
category: "Green Living",
iconUrl: " ",
iconX2Url: " ",
iconX3Url: " ",
image: " "

)

preferences
{
page name: "settings"
}

def settings() // 1
{
if(state.paused)
{
log.debug "new app label: ${app.label} 1"

while(app.label.contains(" (Paused) "))
{
  app.updateLabel(app.label.minus("(Paused)" ))
}
app.updateLabel(app.label + ("<font color = 'red'> (Paused) </font>" ))

}
else if(app.label.contains("(Paused)"))
{
app.updateLabel(app.label.minus(" (Paused) " ))

 while(app.label.contains(" (Paused) ")){app.updateLabel(app.label.minus("(Paused)" ))}
 
 log.debug "new app label: ${app.label}     1"

}

if(state.paused == true)
{
state.button_name = "Resume"
log.debug "button name is: $state.button_name 1"
}
else
{
state.button_name = "Pause"
log.debug "button name is: $state.button_name 1"
}

def pageProperties = [
name: "settings",
title: "Thermostats and other devices",
nextPage: null,
install: true,
uninstall: true
]

dynamicPage(pageProperties)
{
section()
{
input "pause", "button", title: "$state.button_name"
}

 section() 
 {
    label title: "Assign a name", required: false
    input "restricted", "mode", title: "Do not run this app while in these modes", multiple: true
 }

 section("Select the thermostat you want to control") 
 { 
    input "thermostat", "capability.thermostat", title: "Select a thermostat", required: true, multiple: false, description: null, submitOnChange:true
    input "dimmer", "capability.switchLevel", title: "Use this dimmer as set point input source", required: true, submitOnChange:true
 }

 section()
 {
    input "run", "button", title: "RUN"
    input "update", "button", title: "UPDATE"
    input "poll", "button", title: "REFRESH"
    input "polldevices", "bool", title: "Poll devices"
    input "enabledebug", "bool", title: "Debug", submitOnChange:true
    input "description", "bool", title: "Description Text", submitOnChange:true
 }

}
}

def installed() // 2
{
logging("Installed with settings: ${settings} 2")
initialize()
}

def updated() // 3
{
logging("updated with settings: ${settings} 3")
unsubscribe()
unschedule()
initialize()
}

def initialize() // 4
{
if(enabledebug)
{
log.warn "debug enabled 4"
state.EnableDebugTime = now()
runIn(1800,disablelogging)
descriptiontext "debug will be disabled in 30 minutes"
}
else
{
log.warn "debug disabled 4"
}

state.paused = false
state.restricted = false

logging("subscribing to events... 4")
subscribe(location, "mode", ChangedModeHandler)
subscribe(thermostat, "temperature", temperatureHandler)
subscribe(weatherSensor, "temperature", temperatureHandler)
subscribe(dimmer, "level", dimmerHandler)
descriptiontext "subscribed $dimmer to dimmerHandler"
subscribe(thermostat, "heatingSetpoint", setPointHandler)
descriptiontext "subscribed ${thermostat}'s heatingSetpoint to setPointHandler"
subscribe(thermostat, "coolingSetpoint", setPointHandler)
descriptiontext "subscribed ${thermostat}'s coolingSetpoint to setPointHandler"

if(polldevices)
{
schedule("0 0/5 * * * ?", Poll)
}

schedule("0 0/1 * * * ?", mainloop)
descriptiontext "INITIALIZATION DONE"
}

/EVT HANDLERS*********/
def appButtonHandler(btn) // 5
{
switch(btn)
{
case "pause":state.paused = !state.paused
logging("state.paused = ${state.paused} 5")

    if(state.paused)
    {
       log.debug "unsuscribing from events...     5"
       unsubscribe()  
       log.debug "unschedule()...     5"
       unschedule()
       break
    }
    else
    {
      updated()            
      break
    }
  
  case "update":
    state.paused = false
    updated()
    break
 
  case "run":
    if(!state.paused) mainloop()
    break
  
  case "poll":
    Poll()
    break

}
}

def ChangedModeHandler(evt) // 6
{
logging("mode is ${evt.value} 6")

if(evt.value in restricted)
{
state.paused = true
state.restricted = true
}
else if(state.paused == true && state.restricted == true)
{
updated()
}
}

def temperatureHandler(evt) //7
{
logging("$evt.device returns ${evt.value}F 7")
mainloop()
}

def setPointHandler(evt) // 8
{
descriptiontext "new $evt.name is $evt.value---------------------------------------"
log.debug "entered setpoint handler. 8"
def currDim = dimmer.currentValue("level") // get value from dimmer
logging"""
Current dimmer value is $currDim
evt.value = $evt.value """
mainloop()
}

def dimmerHandler(evt) // 9
{
descriptiontext "New dimmer level is $evt.value"
log.debug "entered dimmer handler. 9"
setpointSentByApp = true
mainloop()
}

/MAIN loop*********/

def mainloop() // 10
{
int InsideTemperature
int DimmerSetpoint
boolean changed

if(!state.paused)

{

  InsideTemperature = GetInsideTemp()
  DimmerSetpoint = GetDimmerSetpoint()               // Get dimmer's current value  
  
  
  changed = DidSetpointChange(DimmerSetpoint) // Check to see if dimmer requested setpoint change.
  logging("changed = ${changed}       10")
 
  if(changed)  
  {  
     
     if(DimmerSetpoint > InsideTemperature)
     {
        thermostat.setCoolingSetpoint(DimmerSetpoint -2) 
        thermostat.setHeatingSetpoint(DimmerSetpoint)
        thermostat.setThermostatMode("heat")
        log.debug "Thermostat's heating set point set to ${DimmerSetpoint}       10"
     }    
     else if(DimmerSetpoint < InsideTemperature)
     {
        thermostat.setHeatingSetpoint(DimmerSetpoint + 2)
        thermostat.setCoolingSetpoint(DimmerSetpoint) 
        thermostat.setThermostatMode("cool")
        log.debug "Thermostat's cooling set point set to ${DimmerSetpoint}       10"
     }

     logging("waiting 2 seconds      10")
    // pauseExecution(2000) // give plenty of time for respective commands to be received and processed by the device driver
    // thermostat.refresh()  
  
  }     

}
}

/INPUTS*********/

def GetDimmerSetpoint() // 11
{
int val = 75

val = dimmer.currentValue("level") 
log.debug "Dimmer's temperature setting is ${val}F     11"
return val

}

def GetInsideTemp() // 12
{
float val

val = thermostat.currentValue("temperature") // get inside temperature
log.debug "Thermostat's temperature reading is ${val}F     12"
return val

}

def Poll() // 13
{

if(polldevices)
{
boolean thermPoll = thermostat.hasCommand("poll")
boolean thermRefresh = thermostat.hasCommand("refresh")

 if(thermRefresh)
 {
   thermostat.refresh()
   logging("refreshing $thermostat      13")
 }
 else if(thermPoll)
 {
   thermostat.poll()

logging("polling ${thermostat} 13")
}

 boolean heaterPoll = heater?.hasCommand("poll")
 boolean heaterRefresh = heater?.hasCommand("refresh") 
 
 if(heaterRefresh)
 {
    heater.refresh()
 logging("refreshing ${heater}      13")
 }
 else if(heaterPoll)
 {
    heater.poll()
    logging("polling ${heater}      13")
 }

}
}

/BOOLEANS*********/

boolean DidSetpointChange(CurrentDimmerSetting) // 14
{
boolean changed = false

logging("LastDimmerSetting = ${LastDimmerSetting} 14")
logging("CurrentDimmerSetting = ${CurrentDimmerSetting} 14")

if(setpointSentByApp == true) // setpoint issued by dommer
{
logging("Setpoint was sent by app 14")
if(LastDimmerSetting != CurrentDimmerSetting)
{
changed = true
LastDimmerSetting = CurrentDimmerSetting
}
}
else // setpoint issued by thermostat panel or Hubitat ZEN Thermostat application UIR
{
def thermMode = thermostat.currentValue("thermostatMode")
logging("Thermostat Mode = ${thermMode} 14") // get thermostat mode this might have changed too

   if(thermMode == "heat")
   {
      def manualSetpoint = thermostat.currentValue("heatingSetpoint").toInteger()  // get setpoint from thermostat
       logging("Manual heating setpoint = ${manualSetpoint}     14")
      dimmer.setLevel(manualSetpoint) // update dimmer to match new setpoint issued by non dimmer command
      LastDimmerSetting = manualSetpoint   
   }
   else if(thermMode == "cool")
   {
      def manualSetpoint = thermostat.currentValue("coolingSetpoint").toInteger() // get setpoint from thermostat
      logging("Manual cooling setpoint = ${manualSetpoint}      14")
      dimmer.setLevel(manualSetpoint) // update dimmer to match new setpoint issued by non dimmer command
      LastDimmerSetting = manualSetpoint   
   }

}

logging("Setpoint changed = ${changed} 14")
setpointSentByApp = false
return changed
}

/OTHER*********/

def logging(message)
{
if(enabledebug)
{
log.debug message
}
}

def descriptiontext(message)
{
if(description)
{
log.info message
}
}

def disablelogging() // 15
{
log.warn "debug logging disabled... 15"
app.updateSetting("enabledebug",[value:"false",type:"bool"])
}

The first thing you should do is edit your post.
Highlight all you code then hit this button..
image

This will format the code correctly so that people can easily read it :slight_smile:

it changes this...

log.info message
}
}

def disablelogging() // 15
{
log.warn "debug logging disabled... 15"
app.updateSetting("enabledebug",[value:"false",type:"bool"])
}

to this..

log.info message
}
}

def disablelogging() // 15
{
log.warn "debug logging disabled... 15"
app.updateSetting("enabledebug",[value:"false",type:"bool"])
}

Andy

1 Like
/*
 *
 *  Simple ZEN Single Stage Thermostat Manager
 *
 *  Author:  Ron Doan - based on original code by ELFEGE
 *  Version: 1.0.0 
 *  Date: 04/21/2020
 *
 *  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.
 *
 * Notes:
 * Version 1.0.0 
 *
 * Simple ZEN single stage thermostat manager app using virtual dimmer (might work on other devices).
 * Thanks to ELFEGE for original source - this code is a stripped down verson of his code (with a few tweaks). 
 * Supports single stage heating and cooling only - Thats all I need where I live and cannot test anything else.
 * Receives temperature input data from virtual dimmer switch which Alexa can also control.
 * Switches from heat to cold and from cold to heat based on inside temperature reading from thermostat when user changes temperature.
 * Should not interfere with manual programming of thermostat - but time will tell.
 * Does not use auto heating/cooling mode. 
 * Does not support heat pump.
 * BUG - cannot switch fan mode - suggest manually setting fan mode to auto when using this app.
 *
 * I am not a professional programmer and only spent a few evenings on this but so far so good.
 */

import java.text.SimpleDateFormat
import groovy.transform.Field
import groovy.json.JsonOutput

/* Globals */
@Field static int LastDimmerSetting = 0
@Field static boolean setpointSentByApp = false

definition(
  name: "Simple ZEN Single Stage Thermostat Manager",
  namespace: "Doan",
  author: "DOAN based on original code by ELFEGE",
  description: "Manage your thermostats with dimmer",
  category: "Green Living",
  iconUrl: " ",
  iconX2Url: " ",
  iconX3Url: " ", 
  image: " "
  
)

preferences
{
   page name: "settings"
}

def settings() //     1
{
  if(state.paused)
  {
    log.debug "new app label: ${app.label}     1"
    
    while(app.label.contains(" (Paused) "))
    {
      app.updateLabel(app.label.minus("(Paused)" ))
    }
    app.updateLabel(app.label + ("<font color = 'red'> (Paused) </font>" ))
  }
  else if(app.label.contains("(Paused)"))
  {
     app.updateLabel(app.label.minus("<font color = 'red'> (Paused) </font>" ))
     
     while(app.label.contains(" (Paused) ")){app.updateLabel(app.label.minus("(Paused)" ))}
     
     log.debug "new app label: ${app.label}     1"
  }

  if(state.paused == true)
  {
     state.button_name = "Resume"
     log.debug "button name is: $state.button_name     1"
  }
  else 
  {
     state.button_name = "Pause"
     log.debug "button name is: $state.button_name     1"
  }

  def pageProperties = [
      name: "settings",
      title: "Thermostats and other devices",
      nextPage: null,
      install: true,
      uninstall: true
  ]

  dynamicPage(pageProperties) 
  {
     section()
     {
        input "pause", "button", title: "$state.button_name"
     }

     section() 
     {
        label title: "Assign a name", required: false
        input "restricted", "mode", title: "Do not run this app while in these modes", multiple: true
     }

     section("Select the thermostat you want to control") 
     { 
        input "thermostat", "capability.thermostat", title: "Select a thermostat", required: true, multiple: false, description: null, submitOnChange:true
        input "dimmer", "capability.switchLevel", title: "Use this dimmer as set point input source", required: true, submitOnChange:true
     }

     section()
     {
        input "run", "button", title: "RUN"
        input "update", "button", title: "UPDATE"
        input "poll", "button", title: "REFRESH"
        input "polldevices", "bool", title: "Poll devices"
        input "enabledebug", "bool", title: "Debug", submitOnChange:true
        input "description", "bool", title: "Description Text", submitOnChange:true
     }
  }
}

def installed() // 2
{
   logging("Installed with settings: ${settings}     2")
   initialize()
}

def updated() // 3
{
   logging("updated with settings: ${settings}     3")
   unsubscribe()
   unschedule()
   initialize()
}

def initialize() // 4
{
   if(enabledebug)
   {
      log.warn "debug enabled     4"      
      state.EnableDebugTime = now()
      runIn(1800,disablelogging)
      descriptiontext "debug will be disabled in 30 minutes"
   }
   else 
   {
     log.warn "debug disabled     4"
   }
    
   state.paused = false
   state.restricted = false
   
   logging("subscribing to events...     4")
   subscribe(location, "mode", ChangedModeHandler) 
   subscribe(thermostat, "temperature", temperatureHandler)
   subscribe(weatherSensor, "temperature", temperatureHandler) 
   subscribe(dimmer, "level", dimmerHandler)
   descriptiontext "subscribed $dimmer to dimmerHandler"
   subscribe(thermostat, "heatingSetpoint", setPointHandler)
   descriptiontext "subscribed ${thermostat}'s heatingSetpoint to setPointHandler"
   subscribe(thermostat, "coolingSetpoint", setPointHandler)
   descriptiontext "subscribed ${thermostat}'s coolingSetpoint to setPointHandler"

   if(polldevices)
   {
      schedule("0 0/5 * * * ?", Poll)
   }

   schedule("0 0/1 * * * ?", mainloop)
   descriptiontext "INITIALIZATION DONE"
}


/************************************************EVT HANDLERS*********************************************************/
def appButtonHandler(btn) // 5
{
   switch(btn) 
   {
      case "pause":state.paused = !state.paused
       logging("state.paused = ${state.paused}     5")
        
        if(state.paused)
        {
           log.debug "unsuscribing from events...     5"
           unsubscribe()  
           log.debug "unschedule()...     5"
           unschedule()
           break
        }
        else
        {
          updated()            
          break
        }
      
      case "update":
        state.paused = false
        updated()
        break
     
      case "run":
        if(!state.paused) mainloop()
        break
      
      case "poll":
        Poll()
        break
   }
}

def ChangedModeHandler(evt) // 6
{
    logging("mode is ${evt.value}     6")
   
   if(evt.value in restricted)
   {
     state.paused = true   
     state.restricted = true
   }
   else if(state.paused == true && state.restricted == true)
   {
     updated()
   }
}

def temperatureHandler(evt) //7
{
   logging("$evt.device returns ${evt.value}F      7")
   mainloop()
}

def setPointHandler(evt) // 8
{
   descriptiontext "new $evt.name is $evt.value---------------------------------------"
   log.debug "entered setpoint handler.       8"
   def currDim = dimmer.currentValue("level") // get value from dimmer
   logging"""
   Current dimmer value is $currDim
   evt.value = $evt.value """  
   mainloop()
}

def dimmerHandler(evt) // 9
{
   descriptiontext "New dimmer level is $evt.value" 
   log.debug "entered dimmer handler.       9"
   setpointSentByApp = true 
   mainloop()
}


/************************************************MAIN loop*********************************************************/

def mainloop() // 10
{
   int InsideTemperature
   int DimmerSetpoint
   boolean changed
   
    if(!state.paused)
   {
             
      InsideTemperature = GetInsideTemp()
      DimmerSetpoint = GetDimmerSetpoint()               // Get dimmer's current value  
      
      
      changed = DidSetpointChange(DimmerSetpoint) // Check to see if dimmer requested setpoint change.
      logging("changed = ${changed}       10")
     
      if(changed)  
      {  
         
         if((DimmerSetpoint == InsideTemperature) || (DimmerSetpoint > InsideTemperature))
         {
            thermostat.setCoolingSetpoint(DimmerSetpoint -2) 
            thermostat.setHeatingSetpoint(DimmerSetpoint)
            thermostat.setThermostatMode("heat")
            log.debug "Thermostat's heating set point set to ${DimmerSetpoint}       10"
         }    
         else
         {
            thermostat.setHeatingSetpoint(DimmerSetpoint + 2)
            thermostat.setCoolingSetpoint(DimmerSetpoint) 
            thermostat.setThermostatMode("cool")
            log.debug "Thermostat's cooling set point set to ${DimmerSetpoint}       10"
         }
        
 
         logging("waiting 2 seconds      10")
         pauseExecution(2000) // give plenty of time for respective commands to be received and processed by the device driver
        // thermostat.refresh()  
      
      }     
   }
}      
        

/************************************************INPUTS*********************************************************/

def GetDimmerSetpoint() // 11
{
    int val = 75
    
    val = dimmer.currentValue("level") 
    log.debug "Dimmer's temperature setting is ${val}F     11"
    return val
}    

def GetInsideTemp() // 12
{
    float val
    
    val = thermostat.currentValue("temperature") // get inside temperature
    log.debug "Thermostat's temperature reading is ${val}F     12"
    return val
}

def Poll() // 13
{
   
   if(polldevices)
   {
     boolean thermPoll = thermostat.hasCommand("poll")
     boolean thermRefresh = thermostat.hasCommand("refresh") 
     
     if(thermRefresh)
     {
       thermostat.refresh()
       logging("refreshing $thermostat      13")
     }
     else if(thermPoll)
     {
       thermostat.poll()
logging("polling ${thermostat}      13")
     }
     
     boolean heaterPoll = heater?.hasCommand("poll")
     boolean heaterRefresh = heater?.hasCommand("refresh") 
     
     if(heaterRefresh)
     {
        heater.refresh()
     logging("refreshing ${heater}      13")
     }
     else if(heaterPoll)
     {
        heater.poll()
        logging("polling ${heater}      13")
     }
   }
}


/************************************************BOOLEANS*********************************************************/

boolean DidSetpointChange(CurrentDimmerSetting) // 14
{
   boolean changed = false
    
    
   logging("LastDimmerSetting = ${LastDimmerSetting}      14")
   logging("CurrentDimmerSetting = ${CurrentDimmerSetting}      14")
  
   if(setpointSentByApp == true) // setpoint issued by dommer
   {   
       logging("Setpoint was sent by app      14")
       if(LastDimmerSetting != CurrentDimmerSetting)
       {
          changed = true
          LastDimmerSetting = CurrentDimmerSetting
       }
   }   
   else // setpoint issued by thermostat panel or Hubitat ZEN Thermostat application UIR
   {
       def thermMode = thermostat.currentValue("thermostatMode")
       logging("Thermostat Mode = ${thermMode}      14") // get thermostat mode  this might have changed too
      
       if(thermMode == "heat")
       {
          def manualSetpoint = thermostat.currentValue("heatingSetpoint").toInteger()  // get setpoint from thermostat
           logging("Manual heating setpoint = ${manualSetpoint}     14")
          dimmer.setLevel(manualSetpoint) // update dimmer to match new setpoint issued by non dimmer command
          LastDimmerSetting = manualSetpoint   
       }
       else if(thermMode == "cool")
       {
          def manualSetpoint = thermostat.currentValue("coolingSetpoint").toInteger() // get setpoint from thermostat
          logging("Manual cooling setpoint = ${manualSetpoint}      14")
          dimmer.setLevel(manualSetpoint) // update dimmer to match new setpoint issued by non dimmer command
          LastDimmerSetting = manualSetpoint   
       }
       
   }    
   
   logging("Setpoint changed = ${changed}      14") 
   setpointSentByApp = false
   return changed
}



/************************************************OTHER*********************************************************/

def logging(message)
{
   if(enabledebug)
   {
     log.debug message
   }
}

def descriptiontext(message)
{
   if(description)
   {
     log.info message
   }
}

def disablelogging() // 15
{
   log.warn "debug logging disabled...      15"
   app.updateSetting("enabledebug",[value:"false",type:"bool"])
}

This topic was automatically closed 365 days after the last reply. New replies are no longer allowed.