Compare commits

...

1 Commits

Author SHA1 Message Date
Gupta Fam
db4c096d14 MSA-2490: * This is a variation of the default CT100 thermostat. I found the cycling through modes and increasing
*  setpoints one degree at a time very cumbersome. Following is a summary of changes from the default
 *
 *	1. Displaying all possible information within the Thermostat multiTile
 *		a. Removed icon so temperature is visible in large font
 *		b. Since I have it wired to C-wire, replaced battery info with humidity
 *		c. Add a dash of color (I am not a graphic designer so...)
 *
 *	2. Instead of cycling through all the modes creating dedicated buttons for each mode
 *		b. Added some colors - slight text overlay issue (can be fixed by replacin with custom icons)
 *
 *	3. Besides the one degree up and down controls added a slider control so heating
 *		and cooling setpoints can be changed in larger increments
2017-12-26 08:53:00 -08:00

View File

@@ -0,0 +1,937 @@
/**
* Copyright Sandeep Gupta
*
* This software comes with absolutely no WARRANTIES OR CONDITIONS OF ANY KIND under any scenarioor condition
*
* This is a variation of the default CT100 thermostat. I found the cycling through modes and increasing
* setpoints one degree at a time very cumbersome. Following is a summary of changes from the default
*
* 1. Displaying all possible information within the Thermostat multiTile
* a. Removed icon so temperature is visible in large font
* b. Since I have it wired to C-wire, replaced battery info with humidity
* c. Add a dash of color (I am not a graphic designer so...)
*
* 2. Instead of cycling through all the modes creating dedicated buttons for each mode
* b. Added some colors - slight text overlay issue (can be fixed by replacin with custom icons)
*
* 3. Besides the one degree up and down controls added a slider control so heating
* and cooling setpoints can be changed in larger increments
*
*/
metadata {
// Automatically generated. Make future change here.
definition (name: "Gupta CT100 Thermostat", namespace: "gupta", author: "Sandeep Gupta") {
capability "Actuator"
capability "Temperature Measurement"
capability "Relative Humidity Measurement"
capability "Thermostat"
capability "Battery"
capability "Refresh"
capability "Sensor"
capability "Health Check"
attribute "thermostatFanState", "string"
command "switchMode"
command "cycleModeHeat"
command "cycleModeCool"
command "cycleModeAuto"
command "cycleModeOff"
command "switchFanMode"
command "lowerHeatingSetpoint"
command "raiseHeatingSetpoint"
command "lowerCoolSetpoint"
command "raiseCoolSetpoint"
command "poll"
fingerprint deviceId: "0x08", inClusters: "0x43,0x40,0x44,0x31,0x80,0x85,0x60"
fingerprint mfr:"0098", prod:"6401", model:"0107", deviceJoinName: "2Gig CT100 Programmable Thermostat"
}
tiles (scale: 2){
multiAttributeTile(name:"temperature", type:"thermostat", width:6, height:4, canChangeIcon: true) {
tileAttribute("device.temperature", key: "PRIMARY_CONTROL") {
attributeState("temperature", label:'${currentValue}°F',
backgroundColors:[
// Celsius
[value: 0, color: "#153591"],
[value: 7, color: "#1e9cbb"],
[value: 15, color: "#90d2a7"],
[value: 23, color: "#44b621"],
[value: 28, color: "#f1d801"],
[value: 35, color: "#d04e00"],
[value: 37, color: "#bc2323"],
// Fahrenheit
[value: 40, color: "#153591"],
[value: 44, color: "#1e9cbb"],
[value: 59, color: "#90d2a7"],
[value: 74, color: "#44b621"],
[value: 84, color: "#f1d801"],
[value: 95, color: "#d04e00"],
[value: 96, color: "#bc2323"]
]
)
}
tileAttribute("device.humidity", key: "SECONDARY_CONTROL") {
attributeState("humidity", label: '${currentValue}%', unit: "%",defaultState: true)
}
tileAttribute("device.thermostatOperatingState", key: "OPERATING_STATE") {
attributeState("idle",backgroundColor: "#649A7B")
attributeState("heating", backgroundColor: "#E86D13")
attributeState("cooling", backgroundColor: "#00A0DC")
}
tileAttribute("device.thermostatMode", key: "THERMOSTAT_MODE") {
attributeState("off", label: '${name}')
attributeState("heat", label: '${name}')
attributeState("cool", label: '${name}')
attributeState("auto", label: '${name}')
attributeState("emergency heat", label: 'e-heat')
}
tileAttribute("device.heatingSetpoint", key: "HEATING_SETPOINT") {
attributeState("default", label: '${currentValue}', unit: "°F")
}
tileAttribute("device.coolingSetpoint", key: "COOLING_SETPOINT") {
attributeState("default", label: '${currentValue}', unit: "°F")
}
}
standardTile("heat", "device.thermostatMode", width: 2, height: 2, inactiveLabel: false, decoration: "flat") {
state "heat", label:'heat', action: "cycleModeHeat", nextState: "updating", icon: "st.thermostat.heat", backgroundColor:"#C35403", defaultState: true
state "off", action: "cycleModeHeat", nextState: "updating", icon: "st.thermostat.heat", backgroundColor: "#CCCCCC"
state "cool", action: "cycleModeHeat", nextState: "updating", icon: "st.thermostat.heat", backgroundColor: "#CCCCCC"
state "auto", action: "cycleModeHeat", nextState: "updating", icon: "st.thermostat.heat", backgroundColor: "#CCCCCC"
state "updating", label: "Working"
}
standardTile("cool", "device.thermostatMode", width: 2, height: 2, inactiveLabel: false, decoration: "flat") {
state "cool", label:'cool', action: "cycleModeCool", nextState: "updating", icon: "st.thermostat.cool", backgroundColor:"#008CC1", defaultState: true
state "off", action: "cycleModeCool", nextState: "updating", icon: "st.thermostat.cool", backgroundColor: "#CCCCCC"
state "heat", action: "cycleModeCool", nextState: "updating", icon: "st.thermostat.cool", backgroundColor: "#CCCCCC"
state "auto", action: "cycleModeCool", nextState: "updating", icon: "st.thermostat.cool", backgroundColor: "#CCCCCC"
state "updating", label: "Working"
}
standardTile("auto", "device.thermostatMode", width: 2, height: 2, inactiveLabel: false, decoration: "flat") {
state "auto", label:'auto', action: "cycleModeAuto", nextState: "updating", icon: "st.thermostat.auto", backgroundColor:"#407031", defaultState: true
state "off", action: "cycleModeAuto", nextState: "updating", icon: "st.thermostat.auto", backgroundColor: "#CCCCCC"
state "heat", action: "cycleModeAuto", nextState: "updating", icon: "st.thermostat.auto", backgroundColor: "#CCCCCC"
state "cool", action: "cycleModeAuto", nextState: "updating", icon: "st.thermostat.auto", backgroundColor: "#CCCCCC"
state "updating", label: "Working"
}
standardTile("off", "device.thermostatMode", width: 2, height: 2, inactiveLabel: false, decoration: "flat") {
state "off", label:'off', action: "cycleModeOff", nextState: "updating", icon: "st.thermostat.heating-cooling-off", backgroundColor:"#CA3D3D", defaultState: true
state "auto", action: "cycleModeOff", nextState: "updating", icon: "st.thermostat.heating-cooling-off", backgroundColor: "#CCCCCC"
state "heat", action: "cycleModeOff", nextState: "updating", icon: "st.thermostat.heating-cooling-off", backgroundColor: "#CCCCCC"
state "cool", action: "cycleModeOff", nextState: "updating", icon: "st.thermostat.heating-cooling-off", backgroundColor: "#CCCCCC"
state "updating", label: "Working"
}
standardTile("fanMode", "device.thermostatFanMode", width:2, height:2, inactiveLabel: false, decoration: "flat") {
state "auto", action:"switchFanMode", nextState:"...", icon: "st.thermostat.fan-auto"
state "on", action:"switchFanMode", nextState:"...", icon: "st.thermostat.fan-on"
state "circulate", action:"switchFanMode", nextState:"...", icon: "st.thermostat.fan-circulate"
state "...", label: "Updating...", nextState:"...", backgroundColor:"#ffffff"
}
standardTile("lowerHeatingSetpoint", "device.heatingSetpoint", width:1, height:1, inactiveLabel: false, decoration: "flat") {
state "heatingSetpoint", label: "heat", action:"lowerHeatingSetpoint", icon:"st.thermostat.thermostat-down", backgroundColor: "#F84646"
}
controlTile("heatSlider", "device.heatingSetpoint", "slider", width: 1, height: 2, range: (35..95)) {
state "heatingSetpoint", label: "heat", action: "setHeatingSetpoint", backgroundColor:"#E86D13"
}
standardTile("raiseHeatingSetpoint", "device.heatingSetpoint", width:1, height:1, inactiveLabel: false, decoration: "flat") {
state "heatingSetpoint", label: "heat", action:"raiseHeatingSetpoint", icon:"st.thermostat.thermostat-up", backgroundColor: "#649A7B"
}
standardTile("lowerCoolSetpoint", "device.coolingSetpoint", width:1, height:1, inactiveLabel: false, decoration: "flat") {
state "coolingSetpoint", label: "cool", action:"lowerCoolSetpoint", icon:"st.thermostat.thermostat-down", backgroundColor: "#F84646"
}
controlTile("coolSlider", "device.coolingSetpoint", "slider", width: 1, height: 2, range: (35..95)) {
state "coolingSetpoint", label: "cool", action: "setCoolingSetpoint", backgroundColor: "#00A0DC"
}
standardTile("raiseCoolSetpoint", "device.heatingSetpoint", width:1, height:1, inactiveLabel: false, decoration: "flat") {
state "heatingSetpoint", label: "cool", action:"raiseCoolSetpoint", icon:"st.thermostat.thermostat-up", backgroundColor: "#649A7B"
}
standardTile("thermostatOperatingState", "device.thermostatOperatingState", width: 2, height:2, decoration: "flat") {
state "idle", label:'${currentValue}', backgroundColor:"#649A7B"
state "heating", label:'${currentValue}', backgroundColor:"#E86D13"
state "cooling", label:'${currentValue}', backgroundColor:"#00A0DC"
}
standardTile("refresh", "device.thermostatMode", width:2, height:2, inactiveLabel: false, decoration: "flat") {
state "default", action:"refresh.refresh", icon:"st.secondary.refresh"
}
main "temperature"
details(["temperature",
"raiseHeatingSetpoint", "heat","cool","raiseCoolSetpoint",
"heatSlider","coolSlider",
"auto","off",
"lowerHeatingSetpoint","lowerCoolSetpoint",
"thermostatOperatingState","fanMode","refresh"
])
}
}
def cycleModeHeat(){
def currentMode = device.currentValue("thermostatMode")
if (currentMode == "heat"){
off()
}else {
heat()
}
}
def cycleModeCool(){
def currentMode = device.currentValue("thermostatMode")
if (currentMode == "cool"){
off()
}else {
cool()
}
}
def cycleModeAuto(){
def currentMode = device.currentValue("thermostatMode")
if (currentMode == "auto"){
off()
}else {
auto()
}
}
def cycleModeOff () {
def currentMode = device.currentValue("thermostatMode")
if (currentMode == "off"){
on()
}else {
off()
}
}
def on() {
def currentMode = device.currentValue("thermostatMode")
def currentTemp = device.currentValue("temperature")
def currentHSP = device.currentValue("heatingSetpoint")
def currentCSP = device.currentValue("coolingSetpoint")
if (mode == "off"){
if (currentHSP > currentTemp){
heat()
} else if (currentCSP < currentTemp){
cool()
} else {
auto()
}
}
}
def installed() {
// Configure device
def cmds = [new physicalgraph.device.HubAction(zwave.associationV1.associationSet(groupingIdentifier:1, nodeId:[zwaveHubNodeId]).format()),
new physicalgraph.device.HubAction(zwave.manufacturerSpecificV2.manufacturerSpecificGet().format())]
sendHubCommand(cmds)
runIn(3, "initialize", [overwrite: true]) // Allow configure command to be sent and acknowledged before proceeding
}
def updated() {
// If not set update ManufacturerSpecific data
if (!getDataValue("manufacturer")) {
sendHubCommand(new physicalgraph.device.HubAction(zwave.manufacturerSpecificV2.manufacturerSpecificGet().format()))
runIn(2, "initialize", [overwrite: true]) // Allow configure command to be sent and acknowledged before proceeding
} else {
initialize()
}
}
def initialize() {
unschedule()
// Device-Watch simply pings if no device events received for 32min(checkInterval)
sendEvent(name: "checkInterval", value: 2 * 15 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID])
// Poll device for additional data that will be updated by refresh tile
pollDevice()
}
def parse(String description)
{
def result = null
if (description == "updated") {
} else {
def zwcmd = zwave.parse(description, [0x42:2, 0x43:2, 0x31: 2, 0x60: 3])
if (zwcmd) {
result = zwaveEvent(zwcmd)
// Check battery level at least once every 2 days
if (!state.lastbatt || now() - state.lastbatt > 48*60*60*1000) {
sendHubCommand(new physicalgraph.device.HubAction(zwave.batteryV1.batteryGet().format()))
}
} else {
log.debug "$device.displayName couldn't parse $description"
}
}
if (!result) {
return []
}
return [result]
}
def zwaveEvent(physicalgraph.zwave.commands.multichannelv3.MultiInstanceCmdEncap cmd) {
def encapsulatedCommand = cmd.encapsulatedCommand([0x31: 3])
if (encapsulatedCommand) {
zwaveEvent(encapsulatedCommand)
}
}
// Event Generation
def zwaveEvent(physicalgraph.zwave.commands.thermostatsetpointv2.ThermostatSetpointReport cmd) {
def sendCmd = []
def unit = getTemperatureScale()
def cmdScale = cmd.scale == 1 ? "F" : "C"
def setpoint = getTempInLocalScale(cmd.scaledValue, cmdScale)
def heatingSetpoint = (setpoint == "heatingSetpoint") ? value : getTempInLocalScale("heatingSetpoint")
def coolingSetpoint = (setpoint == "coolingSetpoint") ? value : getTempInLocalScale("coolingSetpoint")
def mode = device.currentValue("thermostatMode")
// Save device scale, precision, scale as they are used when enforcing setpoint limits
if (cmd.setpointType == 1 || cmd.setpointType == 2) {
state.size = cmd.size
state.scale = cmd.scale
state.precision = cmd.precision
}
switch (cmd.setpointType) {
case 1: // "heatingSetpoint"
state.deviceHeatingSetpoint = cmd.scaledValue
if (state.targetHeatingSetpoint) {
state.targetHeatingSetpoint = null
sendEvent(name: "heatingSetpoint", value: setpoint, unit: getTemperatureScale())
} else if (mode != "cool") {
// if mode is cool heatingSetpoint can't be changed on device, disregard update
// else update heatingSetpoint and enforce limits on coolingSetpoint
updateEnforceSetpointLimits("heatingSetpoint", setpoint)
}
break;
case 2: // "coolingSetpoint"
state.deviceCoolingSetpoint = cmd.scaledValue
if (state.targetCoolingSetpoint) {
state.targetCoolingSetpoint = null
sendEvent(name: "coolingSetpoint", value: setpoint, unit: getTemperatureScale())
} else if (mode != "heat" || mode != "emergency heat") {
// if mode is heat or emergency heat coolingSetpoint can't be changed on device, disregard update
// else update coolingSetpoint and enforce limits on heatingSetpoint
updateEnforceSetpointLimits("coolingSetpoint", setpoint)
}
break;
default:
log.debug "unknown setpointType $cmd.setpointType"
return
}
}
def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv2.SensorMultilevelReport cmd) {
def map = [:]
if (cmd.sensorType == 1) {
map.name = "temperature"
map.unit = getTemperatureScale()
map.value = getTempInLocalScale(cmd.scaledSensorValue, (cmd.scale == 1 ? "F" : "C"))
updateThermostatSetpoint(null, null)
} else if (cmd.sensorType == 5) {
map.name = "humidity"
map.unit = "%"
map.value = cmd.scaledSensorValue
}
sendEvent(map)
}
def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv3.SensorMultilevelReport cmd) {
def map = [:]
if (cmd.sensorType == 1) {
map.name = "temperature"
map.unit = getTemperatureScale()
map.value = getTempInLocalScale(cmd.scaledSensorValue, (cmd.scale == 1 ? "F" : "C"))
updateThermostatSetpoint(null, null)
} else if (cmd.sensorType == 5) {
map.value = cmd.scaledSensorValue
map.unit = "%"
map.name = "humidity"
}
sendEvent(map)
}
def zwaveEvent(physicalgraph.zwave.commands.thermostatoperatingstatev2.ThermostatOperatingStateReport cmd) {
def map = [name: "thermostatOperatingState"]
switch (cmd.operatingState) {
case physicalgraph.zwave.commands.thermostatoperatingstatev2.ThermostatOperatingStateReport.OPERATING_STATE_IDLE:
map.value = "idle"
break
case physicalgraph.zwave.commands.thermostatoperatingstatev2.ThermostatOperatingStateReport.OPERATING_STATE_HEATING:
map.value = "heating"
break
case physicalgraph.zwave.commands.thermostatoperatingstatev2.ThermostatOperatingStateReport.OPERATING_STATE_COOLING:
map.value = "cooling"
break
case physicalgraph.zwave.commands.thermostatoperatingstatev2.ThermostatOperatingStateReport.OPERATING_STATE_FAN_ONLY:
map.value = "fan only"
break
case physicalgraph.zwave.commands.thermostatoperatingstatev2.ThermostatOperatingStateReport.OPERATING_STATE_PENDING_HEAT:
map.value = "pending heat"
break
case physicalgraph.zwave.commands.thermostatoperatingstatev2.ThermostatOperatingStateReport.OPERATING_STATE_PENDING_COOL:
map.value = "pending cool"
break
case physicalgraph.zwave.commands.thermostatoperatingstatev2.ThermostatOperatingStateReport.OPERATING_STATE_VENT_ECONOMIZER:
map.value = "vent economizer"
break
}
sendEvent(map)
}
def zwaveEvent(physicalgraph.zwave.commands.thermostatfanstatev1.ThermostatFanStateReport cmd) {
def map = [name: "thermostatFanState", unit: ""]
switch (cmd.fanOperatingState) {
case 0:
map.value = "idle"
break
case 1:
map.value = "running"
break
case 2:
map.value = "running high"
break
}
sendEvent(map)
}
def zwaveEvent(physicalgraph.zwave.commands.thermostatmodev2.ThermostatModeReport cmd) {
def map = [name: "thermostatMode", data:[supportedThermostatModes: state.supportedModes]]
switch (cmd.mode) {
case physicalgraph.zwave.commands.thermostatmodev2.ThermostatModeReport.MODE_OFF:
map.value = "off"
break
case physicalgraph.zwave.commands.thermostatmodev2.ThermostatModeReport.MODE_HEAT:
map.value = "heat"
break
case physicalgraph.zwave.commands.thermostatmodev2.ThermostatModeReport.MODE_AUXILIARY_HEAT:
map.value = "emergency heat"
break
case physicalgraph.zwave.commands.thermostatmodev2.ThermostatModeReport.MODE_COOL:
map.value = "cool"
break
case physicalgraph.zwave.commands.thermostatmodev2.ThermostatModeReport.MODE_AUTO:
map.value = "auto"
break
}
sendEvent(map)
// Now that mode and temperature is known we can request setpoints in correct order
// Also makes sure operating state is in sync as it isn't being reported when changed
def cmds = []
def heatingSetpoint = getTempInLocalScale("heatingSetpoint")
def coolingSetpoint = getTempInLocalScale("coolingSetpoint")
def currentTemperature = getTempInLocalScale("temperature")
cmds << new physicalgraph.device.HubAction(zwave.thermostatOperatingStateV1.thermostatOperatingStateGet().format())
if (map.value == "cool" || ((map.value == "auto" || map.value == "off") && (currentTemperature > (heatingSetpoint + coolingSetpoint)/2))) {
// request cooling setpoint first
cmds << new physicalgraph.device.HubAction(zwave.thermostatSetpointV1.thermostatSetpointGet(setpointType: 2).format()) // CoolingSetpoint
cmds << new physicalgraph.device.HubAction(zwave.thermostatSetpointV1.thermostatSetpointGet(setpointType: 1).format()) // HeatingSetpoint
} else {
// request heating setpoint first
cmds << new physicalgraph.device.HubAction(zwave.thermostatSetpointV1.thermostatSetpointGet(setpointType: 1).format()) // HeatingSetpoint
cmds << new physicalgraph.device.HubAction(zwave.thermostatSetpointV1.thermostatSetpointGet(setpointType: 2).format()) // CoolingSetpoint
}
sendHubCommand(cmds)
}
def zwaveEvent(physicalgraph.zwave.commands.thermostatfanmodev3.ThermostatFanModeReport cmd) {
def map = [name: "thermostatFanMode", data:[supportedThermostatFanModes: state.supportedFanModes]]
switch (cmd.fanMode) {
case physicalgraph.zwave.commands.thermostatfanmodev3.ThermostatFanModeReport.FAN_MODE_AUTO_LOW:
map.value = "auto"
break
case physicalgraph.zwave.commands.thermostatfanmodev3.ThermostatFanModeReport.FAN_MODE_LOW:
map.value = "on"
break
case physicalgraph.zwave.commands.thermostatfanmodev3.ThermostatFanModeReport.FAN_MODE_CIRCULATION:
map.value = "circulate"
break
}
sendEvent(map)
}
def zwaveEvent(physicalgraph.zwave.commands.thermostatmodev2.ThermostatModeSupportedReport cmd) {
def supportedModes = []
if(cmd.heat) { supportedModes << "heat" }
if(cmd.cool) { supportedModes << "cool" }
// Make sure off is before auto, this ensures the right setpoint is used based on current temperature when auto is set
if(cmd.off) { supportedModes << "off" }
if(cmd.auto) { supportedModes << "auto" }
if(cmd.auxiliaryemergencyHeat) { supportedModes << "emergency heat" }
state.supportedModes = supportedModes
sendEvent(name: "supportedThermostatModes", value: supportedModes, displayed: false)
}
def zwaveEvent(physicalgraph.zwave.commands.thermostatfanmodev3.ThermostatFanModeSupportedReport cmd) {
def supportedFanModes = []
if(cmd.auto) { supportedFanModes << "auto" }
if(cmd.low) { supportedFanModes << "on" }
if(cmd.circulation) { supportedFanModes << "circulate" }
state.supportedFanModes = supportedFanModes
sendEvent(name: "supportedThermostatFanModes", value: supportedFanModes, displayed: false)
}
def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) {
log.debug "Zwave BasicReport: $cmd"
}
def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) {
def batteryState = cmd.batteryLevel
def map = [name: "battery", unit: "%", value: cmd.batteryLevel]
if ((cmd.batteryLevel == 0xFF) || (cmd.batteryLevel == 0x00)) { // Special value for low battery alert
map.value = 1
map.descriptionText = "${device.displayName} battery is low"
map.isStateChange = true
batteryState = "low_battery"
}
state.lastbatt = now()
sendEvent(name: "batteryIcon", value: batteryState, displayed: false)
sendEvent(map)
}
def zwaveEvent(physicalgraph.zwave.Command cmd) {
log.warn "Unexpected zwave command $cmd"
}
def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) {
if (cmd.manufacturerName) {
updateDataValue("manufacturer", cmd.manufacturerName)
}
if (cmd.productTypeId) {
updateDataValue("productTypeId", cmd.productTypeId.toString())
}
if (cmd.productId) {
updateDataValue("productId", cmd.productId.toString())
}
}
def poll() {
// Call refresh which will cap the polling to once every 2 minutes
refresh()
}
def refresh() {
// Only allow refresh every 2 minutes to prevent flooding the Zwave network
def timeNow = now()
if (!state.refreshTriggeredAt || (2 * 60 * 1000 < (timeNow - state.refreshTriggeredAt))) {
state.refreshTriggeredAt = timeNow
// refresh will request battery, prevent multiple request by setting lastbatt now
state.lastbatt = timeNow
// use runIn with overwrite to prevent multiple DTH instances run before state.refreshTriggeredAt has been saved
runIn(2, "pollDevice", [overwrite: true])
}
}
def pollDevice() {
def cmds = []
cmds << new physicalgraph.device.HubAction(zwave.thermostatModeV2.thermostatModeSupportedGet().format())
cmds << new physicalgraph.device.HubAction(zwave.thermostatFanModeV3.thermostatFanModeSupportedGet().format())
cmds << new physicalgraph.device.HubAction(zwave.thermostatFanModeV3.thermostatFanModeGet().format())
cmds << new physicalgraph.device.HubAction(zwave.batteryV1.batteryGet().format())
cmds << new physicalgraph.device.HubAction(zwave.multiChannelV3.multiInstanceCmdEncap(instance: 2).encapsulate(zwave.sensorMultilevelV3.sensorMultilevelGet()).format()) // humidity
cmds << new physicalgraph.device.HubAction(zwave.multiChannelV3.multiInstanceCmdEncap(instance: 1).encapsulate(zwave.sensorMultilevelV3.sensorMultilevelGet()).format()) // temperature
cmds << new physicalgraph.device.HubAction(zwave.thermostatOperatingStateV1.thermostatOperatingStateGet().format())
def time = getTimeAndDay()
if (time) {
cmds << new physicalgraph.device.HubAction(zwave.clockV1.clockSet(time).format())
}
// ThermostatModeReport will spawn request for operating state and setpoints so request this last
// this as temperature and mode is needed to determine which setpoints should be requested first
cmds << new physicalgraph.device.HubAction(zwave.thermostatModeV2.thermostatModeGet().format())
// Add 2 seconds delay between each command to avoid flooding the Z-Wave network choking the hub
sendHubCommand(cmds, 2000)
}
def raiseHeatingSetpoint() {
alterSetpoint(true, "heatingSetpoint")
}
def lowerHeatingSetpoint() {
alterSetpoint(false, "heatingSetpoint")
}
def raiseCoolSetpoint() {
alterSetpoint(true, "coolingSetpoint")
}
def lowerCoolSetpoint() {
alterSetpoint(false, "coolingSetpoint")
}
// Adjusts nextHeatingSetpoint either .5° C/1° F) if raise true/false
def alterSetpoint(raise, setpoint) {
def locationScale = getTemperatureScale()
def deviceScale = (state.scale == 1) ? "F" : "C"
def heatingSetpoint = getTempInLocalScale("heatingSetpoint")
def coolingSetpoint = getTempInLocalScale("coolingSetpoint")
def targetValue = (setpoint == "heatingSetpoint") ? heatingSetpoint : coolingSetpoint
def delta = (locationScale == "F") ? 1 : 0.5
targetValue += raise ? delta : - delta
def data = enforceSetpointLimits(setpoint, [targetValue: targetValue, heatingSetpoint: heatingSetpoint, coolingSetpoint: coolingSetpoint], raise)
// update UI without waiting for the device to respond, this to give user a smoother UI experience
// also, as runIn's have to overwrite and user can change heating/cooling setpoint separately separate runIn's have to be used
if (data.targetHeatingSetpoint) {
sendEvent("name": "heatingSetpoint", "value": getTempInLocalScale(data.targetHeatingSetpoint, deviceScale),
unit: getTemperatureScale(), eventType: "ENTITY_UPDATE", displayed: false)
}
if (data.targetCoolingSetpoint) {
sendEvent("name": "coolingSetpoint", "value": getTempInLocalScale(data.targetCoolingSetpoint, deviceScale),
unit: getTemperatureScale(), eventType: "ENTITY_UPDATE", displayed: false)
}
if (data.targetHeatingSetpoint && data.targetCoolingSetpoint) {
runIn(5, "updateHeatingSetpoint", [data: data, overwrite: true])
} else if (setpoint == "heatingSetpoint" && data.targetHeatingSetpoint) {
runIn(5, "updateHeatingSetpoint", [data: data, overwrite: true])
} else if (setpoint == "coolingSetpoint" && data.targetCoolingSetpoint) {
runIn(5, "updateCoolingSetpoint", [data: data, overwrite: true])
}
}
def updateHeatingSetpoint(data) {
updateSetpoints(data)
}
def updateCoolingSetpoint(data) {
updateSetpoints(data)
}
def updateEnforceSetpointLimits(setpoint, setpointValue) {
def heatingSetpoint = (setpoint == "heatingSetpoint") ? setpointValue : getTempInLocalScale("heatingSetpoint")
def coolingSetpoint = (setpoint == "coolingSetpoint") ? setpointValue : getTempInLocalScale("coolingSetpoint")
sendEvent(name: setpoint, value: setpointValue, unit: getTemperatureScale(), displayed: false)
updateThermostatSetpoint(setpoint, setpointValue)
// Enforce coolingSetpoint limits, as device doesn't
def data = enforceSetpointLimits(setpoint, [targetValue: setpointValue,
heatingSetpoint: heatingSetpoint, coolingSetpoint: coolingSetpoint])
if (setpoint == "heatingSetpoint" && data.targetHeatingSetpoint) {
data.targetHeatingSetpoint = null
} else if (setpoint == "coolingSetpoint" && data.targetCoolingSetpoint) {
data.targetCoolingSetpoint = null
}
if (data.targetHeatingSetpoint != null || data.targetCoolingSetpoint != null) {
updateSetpoints(data)
}
}
def enforceSetpointLimits(setpoint, data, raise = null) {
def locationScale = getTemperatureScale()
def deviceScale = (state.scale == 1) ? "F" : "C"
// min/max with 3°F/2°C deadband consideration
def minSetpoint = (setpoint == "heatingSetpoint") ? getTempInDeviceScale(35, "F") : getTempInDeviceScale(38, "F")
def maxSetpoint = (setpoint == "heatingSetpoint") ? getTempInDeviceScale(92, "F") : getTempInDeviceScale(95, "F")
def deadband = (deviceScale == "F") ? 3 : 2
def delta = (locationScale == "F") ? 1 : 0.5
def targetValue = getTempInDeviceScale(data.targetValue, locationScale)
def heatingSetpoint = null
def coolingSetpoint = null
// Enforce min/mix for setpoints
if (targetValue > maxSetpoint) {
heatingSetpoint = (setpoint == "heatingSetpoint") ? maxSetpoint : getTempInDeviceScale(data.heatingSetpoint, locationScale)
coolingSetpoint = (setpoint == "heatingSetpoint") ? maxSetpoint + deadband : maxSetpoint
} else if (targetValue < minSetpoint) {
heatingSetpoint = (setpoint == "coolingSetpoint") ? minSetpoint - deadband : minSetpoint
coolingSetpoint = (setpoint == "coolingSetpoint") ? minSetpoint : getTempInDeviceScale(data.coolingSetpoint, locationScale)
}
// Enforce deadband between setpoints
if (setpoint == "heatingSetpoint" && !coolingSetpoint) {
// Note if new value is same as old value we need to move it in the direction the user wants to change it, 1°F or 0.5°C,
heatingSetpoint = (targetValue != getTempInDeviceScale(data.heatingSetpoint, locationScale) || !raise) ?
targetValue : (raise ? targetValue + delta : targetValue - delta)
coolingSetpoint = (heatingSetpoint + deadband > getTempInDeviceScale(data.coolingSetpoint, locationScale)) ? heatingSetpoint + deadband : null
}
if (setpoint == "coolingSetpoint" && !heatingSetpoint) {
coolingSetpoint = (targetValue != getTempInDeviceScale(data.coolingSetpoint, locationScale) || !raise) ?
targetValue : (raise ? targetValue + delta : targetValue - delta)
heatingSetpoint = (coolingSetpoint - deadband < getTempInDeviceScale(data.heatingSetpoint, locationScale)) ? coolingSetpoint - deadband : null
}
return [targetHeatingSetpoint: heatingSetpoint, targetCoolingSetpoint: coolingSetpoint]
}
def setHeatingSetpoint(degrees) {
if (degrees) {
state.heatingSetpoint = degrees.toDouble()
runIn(2, "updateSetpoints", [overwrite: true])
}
}
def setCoolingSetpoint(degrees) {
if (degrees) {
state.coolingSetpoint = degrees.toDouble()
runIn(2, "updateSetpoints", [overwrite: true])
}
}
def updateSetpoints() {
def deviceScale = (state.scale == 1) ? "F" : "C"
def data = [targetHeatingSetpoint: null, targetCoolingSetpoint: null]
def heatingSetpoint = getTempInLocalScale("heatingSetpoint")
def coolingSetpoint = getTempInLocalScale("coolingSetpoint")
if (state.heatingSetpoint) {
data = enforceSetpointLimits("heatingSetpoint", [targetValue: state.heatingSetpoint,
heatingSetpoint: heatingSetpoint, coolingSetpoint: coolingSetpoint])
}
if (state.coolingSetpoint) {
heatingSetpoint = data.targetHeatingSetpoint ? getTempInLocalScale(data.targetHeatingSetpoint, deviceScale) : heatingSetpoint
coolingSetpoint = data.targetCoolingSetpoint ? getTempInLocalScale(data.targetCoolingSetpoint, deviceScale) : coolingSetpoint
data = enforceSetpointLimits("coolingSetpoint", [targetValue: state.coolingSetpoint,
heatingSetpoint: heatingSetpoint, coolingSetpoint: coolingSetpoint])
data.targetHeatingSetpoint = data.targetHeatingSetpoint ?: heatingSetpoint
}
state.heatingSetpoint = null
state.coolingSetpoint = null
updateSetpoints(data)
}
def updateSetpoints(data) {
unschedule("updateSetpoints")
def cmds = []
if (data.targetHeatingSetpoint) {
state.targetHeatingSetpoint = data.targetHeatingSetpoint
cmds << new physicalgraph.device.HubAction(zwave.thermostatSetpointV1.thermostatSetpointSet(
setpointType: 1, scale: state.scale, precision: state.precision, scaledValue: data.targetHeatingSetpoint).format())
}
if (data.targetCoolingSetpoint) {
state.targetCoolingSetpoint = data.targetCoolingSetpoint
cmds << new physicalgraph.device.HubAction(zwave.thermostatSetpointV1.thermostatSetpointSet(
setpointType: 2, scale: state.scale, precision: state.precision, scaledValue: data.targetCoolingSetpoint).format())
}
// Also make sure temperature and operating state is in sync
cmds << new physicalgraph.device.HubAction(zwave.multiChannelV3.multiInstanceCmdEncap(instance: 1).encapsulate(zwave.sensorMultilevelV3.sensorMultilevelGet()).format()) // temperature
cmds << new physicalgraph.device.HubAction(zwave.thermostatOperatingStateV1.thermostatOperatingStateGet().format())
if (data.targetHeatingSetpoint) {
cmds << new physicalgraph.device.HubAction(zwave.thermostatSetpointV1.thermostatSetpointGet(setpointType: 1).format())
}
if (data.targetCoolingSetpoint) {
cmds << new physicalgraph.device.HubAction(zwave.thermostatSetpointV1.thermostatSetpointGet(setpointType: 2).format())
}
sendHubCommand(cmds)
}
// thermostatSetpoint is not displayed by any tile as it can't be predictable calculated due to
// the device's quirkiness but it is defined by the capability so it must be set, set it to the most likely value
def updateThermostatSetpoint(setpoint, value) {
def scale = getTemperatureScale()
def heatingSetpoint = (setpoint == "heatingSetpoint") ? value : getTempInLocalScale("heatingSetpoint")
def coolingSetpoint = (setpoint == "coolingSetpoint") ? value : getTempInLocalScale("coolingSetpoint")
def mode = device.currentValue("thermostatMode")
def thermostatSetpoint = heatingSetpoint // corresponds to (mode == "heat" || mode == "emergency heat")
if (mode == "cool") {
thermostatSetpoint = coolingSetpoint
} else if (mode == "auto" || mode == "off") {
// Set thermostatSetpoint to the setpoint closest to the current temperature
def currentTemperature = getTempInLocalScale("temperature")
if (currentTemperature > (heatingSetpoint + coolingSetpoint)/2) {
thermostatSetpoint = coolingSetpoint
}
}
sendEvent(name: "thermostatSetpoint", value: thermostatSetpoint, unit: getTemperatureScale())
}
/**
* PING is used by Device-Watch in attempt to reach the Device
* */
def ping() {
log.debug "ping() called"
// Just get Operating State as it is not reported when it changes and there's no need to flood more commands
sendHubCommand(new physicalgraph.device.HubAction(zwave.thermostatOperatingStateV1.thermostatOperatingStateGet().format()))
}
def switchMode() {
def currentMode = device.currentValue("thermostatMode")
def supportedModes = state.supportedModes
// Old version of supportedModes was as string, make sure it gets updated
if (supportedModes && supportedModes.size() && supportedModes[0].size() > 1) {
def next = { supportedModes[supportedModes.indexOf(it) + 1] ?: supportedModes[0] }
def nextMode = next(currentMode)
runIn(2, "setGetThermostatMode", [data: [nextMode: nextMode], overwrite: true])
} else {
log.warn "supportedModes not defined"
getSupportedModes()
}
}
def switchToMode(nextMode) {
def supportedModes = state.supportedModes
// Old version of supportedModes was as string, make sure it gets updated
if (supportedModes && supportedModes.size() && supportedModes[0].size() > 1) {
if (supportedModes.contains(nextMode)) {
runIn(2, "setGetThermostatMode", [data: [nextMode: nextMode], overwrite: true])
} else {
log.debug("ThermostatMode $nextMode is not supported by ${device.displayName}")
}
} else {
log.warn "supportedModes not defined"
getSupportedModes()
}
}
def getSupportedModes() {
def cmds = []
cmds << new physicalgraph.device.HubAction(zwave.thermostatModeV2.thermostatModeSupportedGet().format())
sendHubCommand(cmds)
}
def switchFanMode() {
def currentMode = device.currentValue("thermostatFanMode")
def supportedFanModes = state.supportedFanModes
// Old version of supportedFanModes was as string, make sure it gets updated
if (supportedFanModes && supportedFanModes.size() && supportedFanModes[0].size() > 1) {
def next = { supportedFanModes[supportedFanModes.indexOf(it) + 1] ?: supportedFanModes[0] }
def nextMode = next(currentMode)
runIn(2, "setGetThermostatFanMode", [data: [nextMode: nextMode], overwrite: true])
} else {
log.warn "supportedFanModes not defined"
getSupportedFanModes()
}
}
def switchToFanMode(nextMode) {
def supportedFanModes = state.supportedFanModes
// Old version of supportedFanModes was as string, make sure it gets updated
if (supportedFanModes && supportedFanModes.size() && supportedFanModes[0].size() > 1) {
if (supportedFanModes.contains(nextMode)) {
runIn(2, "setGetThermostatFanMode", [data: [nextMode: nextMode], overwrite: true])
} else {
log.debug("FanMode $nextMode is not supported by ${device.displayName}")
}
} else {
log.warn "supportedFanModes not defined"
getSupportedFanModes()
}
}
def getSupportedFanModes() {
def cmds = [new physicalgraph.device.HubAction(zwave.thermostatFanModeV3.thermostatFanModeSupportedGet().format())]
sendHubCommand(cmds)
}
def getModeMap() { [
"off": 0,
"heat": 1,
"cool": 2,
"auto": 3,
"emergency heat": 4
]}
def setThermostatMode(String value) {
switchToMode(value)
}
def setGetThermostatMode(data) {
def cmds = [new physicalgraph.device.HubAction(zwave.thermostatModeV2.thermostatModeSet(mode: modeMap[data.nextMode]).format()),
new physicalgraph.device.HubAction(zwave.thermostatModeV2.thermostatModeGet().format())]
sendHubCommand(cmds)
}
def getFanModeMap() { [
"auto": 0,
"on": 1,
"circulate": 6
]}
def setThermostatFanMode(String value) {
switchToFanMode(value)
}
def setGetThermostatFanMode(data) {
def cmds = [new physicalgraph.device.HubAction(zwave.thermostatFanModeV3.thermostatFanModeSet(fanMode: fanModeMap[data.nextMode]).format()),
new physicalgraph.device.HubAction(zwave.thermostatFanModeV3.thermostatFanModeGet().format())]
sendHubCommand(cmds)
}
def off() {
switchToMode("off")
}
def heat() {
switchToMode("heat")
}
def emergencyHeat() {
switchToMode("emergency heat")
}
def cool() {
switchToMode("cool")
}
def auto() {
switchToMode("auto")
}
def fanOn() {
switchToFanMode("on")
}
def fanAuto() {
switchToFanMode("auto")
}
def fanCirculate() {
switchToFanMode("circulate")
}
private getTimeAndDay() {
def timeNow = now()
// Need to check that location have timeZone as SC may have created the location without setting it
// Don't update clock more than once a day
if (location.timeZone && (!state.timeClockSet || (24 * 60 * 60 * 1000 < (timeNow - state.timeClockSet)))) {
def currentDate = Calendar.getInstance(location.timeZone)
state.timeClockSet = timeNow
return [hour: currentDate.get(Calendar.HOUR_OF_DAY), minute: currentDate.get(Calendar.MINUTE), weekday: currentDate.get(Calendar.DAY_OF_WEEK)]
}
}
// Get stored temperature from currentState in current local scale
def getTempInLocalScale(state) {
def temp = device.currentState(state)
if (temp && temp.value && temp.unit) {
return getTempInLocalScale(temp.value.toBigDecimal(), temp.unit)
}
return 0
}
// get/convert temperature to current local scale
def getTempInLocalScale(temp, scale) {
if (temp && scale) {
def scaledTemp = convertTemperatureIfNeeded(temp.toBigDecimal(), scale).toDouble()
return (getTemperatureScale() == "F" ? scaledTemp.round(0).toInteger() : roundC(scaledTemp))
}
return 0
}
def getTempInDeviceScale(state) {
def temp = device.currentState(state)
if (temp && temp.value && temp.unit) {
return getTempInDeviceScale(temp.value.toBigDecimal(), temp.unit)
}
return 0
}
def getTempInDeviceScale(temp, scale) {
if (temp && scale) {
def deviceScale = (state.scale == 1) ? "F" : "C"
return (deviceScale == scale) ? temp :
(deviceScale == "F" ? celsiusToFahrenheit(temp).toDouble().round(0).toInteger() : roundC(fahrenheitToCelsius(temp)))
}
return 0
}
def roundC (tempC) {
return (Math.round(tempC.toDouble() * 2))/2
}