Skip to content

Instantly share code, notes, and snippets.

@joshualyon
Last active November 29, 2019 16:41
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save joshualyon/eaf45129a0e68c46e859278ca3faac8f to your computer and use it in GitHub Desktop.
Save joshualyon/eaf45129a0e68c46e859278ca3faac8f to your computer and use it in GitHub Desktop.
/**
* Copyright 2017 SmartThings
*
* 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.
*
*/
import groovy.transform.Field
// enummaps
@Field final Map MODE = [
AUTO: "auto",
HEAT: "heat"
]
@Field final Map OP_STATE = [
HEATING: "heating",
FAN: "fan only",
PEND_HEAT: "pending heat",
VENT_ECO: "vent economizer",
IDLE: "idle"
]
@Field final Map SETPOINT_TYPE = [
HEATING: "heating"
]
@Field final List HEAT_ONLY_MODES = [MODE.HEAT, MODE.EHEAT]
@Field final List DUAL_SETPOINT_MODES = [MODE.AUTO]
@Field final List RUNNING_OP_STATES = [OP_STATE.HEATING]
// config - TODO: move these to a pref page
@Field List SUPPORTED_MODES = [MODE.AUTO, MODE.HEAT] // [MODE.OFF, MODE.HEAT, MODE.AUTO, MODE.EHEAT]
@Field final Float THRESHOLD_DEGREES = 0.5
@Field final Integer MIN_SETPOINT = 0
@Field final Integer MAX_SETPOINT = 200
@Field final Integer AUTO_MODE_SETPOINT_SPREAD = 4 // In auto mode, heat & cool setpoints must be this far apart
// end config
// derivatives
@Field final IntRange FULL_SETPOINT_RANGE = (MIN_SETPOINT..MAX_SETPOINT)
@Field final IntRange HEATING_SETPOINT_RANGE = (MIN_SETPOINT..(MAX_SETPOINT - AUTO_MODE_SETPOINT_SPREAD))
// defaults
@Field final String DEFAULT_MODE = MODE.HEAT
@Field final String DEFAULT_PREVIOUS_STATE = OP_STATE.HEATING
@Field final String DEFAULT_SETPOINT_TYPE = SETPOINT_TYPE.HEATING
@Field final Integer DEFAULT_TEMPERATURE = 20
@Field final Integer DEFAULT_HEATING_SETPOINT = 20
@Field final Integer DEFAULT_THERMOSTAT_SETPOINT = DEFAULT_HEATING_SETPOINT
metadata {
// Automatically generated. Make future change here.
definition (name: "Simulated Heating Thermostat", namespace: "smartthings/testing", author: "SmartThings") {
capability "Sensor"
capability "Actuator"
capability "Health Check"
capability "Temperature Measurement"
capability "Thermostat Heating Setpoint"
capability "Thermostat Mode"
//capability "Relative Humidity Measurement"
capability "Configuration"
capability "Refresh"
command "tempUp"
command "tempDown"
command "heatUp"
command "heatDown"
//command "setpointUp"
//command "setpointDown"
command "setTemperature", ["number"]
}
tiles(scale: 2) {
multiAttributeTile(name:"thermostatMulti", type:"thermostat", width:6, height:4) {
tileAttribute("device.temperature", key: "PRIMARY_CONTROL") {
attributeState("temp", label:'${currentValue}°', unit:"°C", defaultState: true)
}
tileAttribute("device.temperature", key: "VALUE_CONTROL") {
attributeState("VALUE_UP", action: "setpointUp")
attributeState("VALUE_DOWN", action: "setpointDown")
}
tileAttribute("device.thermostatOperatingState", key: "OPERATING_STATE") {
attributeState("idle", backgroundColor: "#FFFFFF")
attributeState("heating", backgroundColor: "#E86D13")
}
tileAttribute("device.thermostatMode", key: "THERMOSTAT_MODE") {
attributeState("off", label: '${name}')
attributeState("heat", label: '${name}')
attributeState("auto", label: '${name}')
attributeState("emergency heat", label: 'e-heat')
}
tileAttribute("device.heatingSetpoint", key: "HEATING_SETPOINT") {
attributeState("default", label: '${currentValue}', unit: "°C", defaultState: true)
}
}
standardTile("mode", "device.thermostatMode", width: 2, height: 2, decoration: "flat") {
state "off", action: "cycleMode", nextState: "updating", icon: "st.thermostat.heating-cooling-off", backgroundColor: "#CCCCCC", defaultState: true
state "heat", action: "cycleMode", nextState: "updating", icon: "st.thermostat.heat"
state "auto", action: "cycleMode", nextState: "updating", icon: "st.thermostat.auto"
state "emergency heat", action: "cycleMode", nextState: "updating", icon: "st.thermostat.emergency-heat"
state "updating", label: "Working"
}
valueTile("heatingSetpoint", "device.heatingSetpoint", width: 2, height: 2, decoration: "flat") {
state "heat", label:'Heat\n${currentValue}°', unit: "°C", backgroundColor:"#E86D13"
}
standardTile("heatDown", "device.temperature", width: 1, height: 1, decoration: "flat") {
state "default", label: "heat", action: "heatDown", icon: "st.thermostat.thermostat-down"
}
standardTile("heatUp", "device.temperature", width: 1, height: 1, decoration: "flat") {
state "default", label: "heat", action: "heatUp", icon: "st.thermostat.thermostat-up"
}
valueTile("roomTemp", "device.temperature", width: 2, height: 1, decoration: "flat") {
state "default", label:'${currentValue}°', unit: "°C", backgroundColors: [
// Celsius Color Range
[value: 0, color: "#153591"],
[value: 7, color: "#1E9CBB"],
[value: 15, color: "#90D2A7"],
[value: 23, color: "#44B621"],
[value: 29, color: "#F1D801"],
[value: 33, color: "#D04E00"],
[value: 36, color: "#BC2323"],
// Fahrenheit Color Range
[value: 40, color: "#153591"],
[value: 44, color: "#1E9CBB"],
[value: 59, color: "#90D2A7"],
[value: 74, color: "#44B621"],
[value: 84, color: "#F1D801"],
[value: 92, color: "#D04E00"],
[value: 96, color: "#BC2323"]
]
}
standardTile("tempDown", "device.temperature", width: 1, height: 1, decoration: "flat") {
state "default", label: "temp", action: "tempDown", icon: "st.thermostat.thermostat-down"
}
standardTile("tempUp", "device.temperature", width: 1, height: 1, decoration: "flat") {
state "default", label: "temp", action: "tempUp", icon: "st.thermostat.thermostat-up"
}
valueTile("blank1x1", "device.switch", width: 1, height: 1, decoration: "flat") {
state "default", label: ""
}
valueTile("blank2x1", "device.switch", width: 2, height: 1, decoration: "flat") {
state "default", label: ""
}
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
state "default", label: "", action: "refresh", icon: "st.secondary.refresh"
}
valueTile("reset", "device.switch", width: 2, height: 2, decoration: "flat") {
state "default", label: "Reset to Defaults", action: "configure"
}
main("roomTemp")
details(["thermostatMulti",
"heatDown", "heatUp",
"mode",
"heatingSetpoint",
"refresh", "reset",
"tempDown", "tempUp",
"roomTemp"
])
}
}
def installed() {
log.trace "Executing 'installed'"
configure()
done()
}
def updated() {
log.trace "Executing 'updated'"
initialize()
done()
}
def configure() {
log.trace "Executing 'configure'"
// this would be for a physical device when it gets a handler assigned to it
initialize()
done()
}
private initialize() {
log.trace "Executing 'initialize'"
sendEvent(name: "temperature", value: DEFAULT_TEMPERATURE, unit: "°C")
//sendEvent(name: "humidity", value: DEFAULT_HUMIDITY, unit: "%")
sendEvent(name: "heatingSetpoint", value: DEFAULT_HEATING_SETPOINT, unit: "°C")
sendEvent(name: "heatingSetpointMin", value: HEATING_SETPOINT_RANGE.getFrom(), unit: "°C")
sendEvent(name: "heatingSetpointMax", value: HEATING_SETPOINT_RANGE.getTo(), unit: "°C")
sendEvent(name: "thermostatSetpoint", value: DEFAULT_THERMOSTAT_SETPOINT, unit: "°C")
sendEvent(name: "thermostatMode", value: DEFAULT_MODE)
sendEvent(name: "thermostatOperatingState", value: DEFAULT_OP_STATE)
state.isHvacRunning = false
state.lastOperatingState = DEFAULT_OP_STATE
state.lastUserSetpointMode = DEFAULT_PREVIOUS_STATE
unschedule()
}
// parse events into attributes
def parse(String description) {
log.trace "Executing parse $description"
def parsedEvents
def pair = description?.split(":")
if (!pair || pair.length < 2) {
log.warn "parse() could not extract an event name and value from '$description'"
} else {
String name = pair[0]?.trim()
if (name) {
name = name.replaceAll(~/\W/, "_").replaceAll(~/_{2,}?/, "_")
}
parsedEvents = createEvent(name: name, value: pair[1]?.trim())
}
done()
return parsedEvents
}
def refresh() {
log.trace "Executing refresh"
sendEvent(name: "thermostatMode", value: getThermostatMode())
//sendEvent(name: "thermostatFanMode", value: getFanMode())
sendEvent(name: "thermostatOperatingState", value: getOperatingState())
sendEvent(name: "thermostatSetpoint", value: getThermostatSetpoint(), unit: "°C")
sendEvent(name: "heatingSetpoint", value: getHeatingSetpoint(), unit: "°C")
sendEvent(name: "temperature", value: getTemperature(), unit: "°C")
//sendEvent(name: "humidity", value: getHumidityPercent(), unit: "%")
done()
}
// Thermostat mode
private String getThermostatMode() {
return device.currentValue("thermostatMode") ?: DEFAULT_MODE
}
def setThermostatMode(String value) {
log.trace "Executing 'setThermostatMode' $value"
if (value in SUPPORTED_MODES) {
proposeSetpoints(getHeatingSetpoint(), state.lastUserSetpointMode)
sendEvent(name: "thermostatMode", value: value)
evaluateOperatingState()
} else {
log.warn "'$value' is not a supported mode. Please set one of ${SUPPORTED_MODES.join(', ')}"
}
done()
}
private String cycleMode() {
log.trace "Executing 'cycleMode'"
String nextMode = nextListElement(SUPPORTED_MODES, getThermostatMode())
setThermostatMode(nextMode)
done()
return nextMode
}
private Boolean isThermostatOff() {
return getThermostatMode() == MODE.OFF
}
private String nextListElement(List uniqueList, currentElt) {
if (uniqueList != uniqueList.unique().asList()) {
throw InvalidPararmeterException("Each element of the List argument must be unique.")
} else if (!(currentElt in uniqueList)) {
throw InvalidParameterException("currentElt '$currentElt' must be a member element in List uniqueList, but was not found.")
}
Integer listIdxMax = uniqueList.size() -1
Integer currentEltIdx = uniqueList.indexOf(currentElt)
Integer nextEltIdx = currentEltIdx < listIdxMax ? ++currentEltIdx : 0
String nextElt = uniqueList[nextEltIdx] as String
return nextElt
}
// setpoint
private Integer getThermostatSetpoint() {
def ts = device.currentState("thermostatSetpoint")
return ts ? ts.getIntegerValue() : DEFAULT_THERMOSTAT_SETPOINT
}
private Integer getHeatingSetpoint() {
def hs = device.currentState("heatingSetpoint")
return hs ? hs.getIntegerValue() : DEFAULT_HEATING_SETPOINT
}
def setHeatingSetpoint(Double degreesF) {
log.trace "Executing 'setHeatingSetpoint' $degreesF"
state.lastUserSetpointMode = SETPOINT_TYPE.HEATING
setHeatingSetpointInternal(degreesF)
done()
}
private setHeatingSetpointInternal(Double degreesF) {
log.debug "setHeatingSetpointInternal($degreesF)"
//proposeHeatSetpoint(degreesF as Integer)
//evaluateOperatingState(heatingSetpoint: degreesF)
sendEvent(name:"heatingSetpoint", value: degreesF)
}
private heatUp() {
log.trace "Executing 'heatUp'"
def newHsp = getHeatingSetpoint() + 1
if (getThermostatMode() in HEAT_ONLY_MODES + DUAL_SETPOINT_MODES) {
setHeatingSetpoint(newHsp)
}
done()
}
private heatDown() {
log.trace "Executing 'heatDown'"
def newHsp = getHeatingSetpoint() - 1
if (getThermostatMode() in HEAT_ONLY_MODES + DUAL_SETPOINT_MODES) {
setHeatingSetpoint(newHsp)
}
done()
}
// simulated temperature
private Integer getTemperature() {
def ts = device.currentState("temperature")
Integer currentTemp = DEFAULT_TEMPERATURE
try {
currentTemp = ts.numericValue
} catch (all) {
log.warn "Encountered an error getting Integer value of temperature state. Value is '$ts.stringValue'. Reverting to default of $DEFAULT_TEMPERATURE"
setTemperature(DEFAULT_TEMPERATURE)
}
return currentTemp
}
// changes the "room" temperature for the simulation
private setTemperature(newTemp) {
sendEvent(name:"temperature", value: newTemp)
evaluateOperatingState(temperature: newTemp)
}
private tempUp() {
def newTemp = getTemperature() ? getTemperature() + 1 : DEFAULT_TEMPERATURE
setTemperature(newTemp)
}
private tempDown() {
def newTemp = getTemperature() ? getTemperature() - 1 : DEFAULT_TEMPERATURE
setTemperature(newTemp)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment