Last active
November 29, 2019 16:41
-
-
Save joshualyon/eaf45129a0e68c46e859278ca3faac8f to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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