Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
evohomeIntegration
/**
* Copyright 2016 David Lomas (codersaur)
*
* Name: Evohome Heating Zone
*
* Author: David Lomas (codersaur)
*
* Date: 2016-04-08
*
* Version: 0.09
*
* Description:
* - This device handler is a child device for the Evohome (Connect) SmartApp.
* - For latest documentation see: https://github.com/codersaur/SmartThings
*
* Version History:
*
* 2016-04-08: v0.09
* - calculateOptimisations(): Fixed comparison of temperature values.
*
* 2016-04-05: v0.08
* - New 'Update Refresh Time' setting from parent to control polling after making an update.
* - setThermostatMode(): Forces poll for all zones to ensure new thermostatMode is updated.
*
* 2016-04-04: v0.07
* - generateEvent(): hides events if name or value are null.
* - generateEvent(): log.info message for new values.
*
* 2016-04-03: v0.06
* - Initial Beta Release
*
* To Do:
* - Clean up device settings (preferences). Hide/Show prefSetpointDuration input dynamically depending on prefSetpointMode. - If supported for devices???
* - When thermostat mode is away or off, heatingSetpoint overrides should not allowed (although setting while away actually works). Should warn at least.
*
* License:
* 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.
* https://github.com/codersaur/SmartThings/blob/master/devices/evohome/evohome-heating-zone.groovy >>>>>>>>>>>> Original
Se conecta con la smartapp por el nombre del DTH?
*/
metadata {
definition (name: "EvohomecodesaurWchanges", namespace: "codersaur", author: "David Lomas", deviceTypeId:"Thermostat",ocfDeviceType:"oic.d.thermostat",vid:"6a738c4d-9308-378f-afbc-aa06591b6e50", mnmn:"SmartThingsCommunity") {
capability "Actuator"
capability "Sensor"
capability "Refresh"
capability "Temperature Measurement"
// capability "Thermostat"
capability "thermostatHeatingSetpoint"
capability "thermostatSetpoint"
capability "thermostatMode"
//command "poll" // Polling
command "refresh" // Refresh
command "setHeatingSetpoint" // Thermostat
command "raiseSetpoint" // Custom
command "lowerSetpoint" // Custom
command "setThermostatMode" // Thermostat
command "cycleThermostatMode" // Custom
command "off" // Thermostat
command "heat" // Thermostat
command "auto" // Custom
command "away" // Custom
command "economy" // Custom
command "dayOff" // Custom
command "custom" // Custom
command "resume" // Custom
command "boost" // Custom
command "suppress" // Custom
command "generateEvent" // Custom
command "test" // Custom
attribute "temperature","number" // Temperature Measurement
attribute "heatingSetpoint","number" // Thermostat
attribute "thermostatSetpoint","number" // Thermostat
attribute "thermostatSetpointMode", "string" // Custom
attribute "thermostatSetpointUntil", "string" // Custom
attribute "thermostatSetpointStatus", "string" // Custom
attribute "thermostatMode", "string" // Thermostat
attribute "thermostatOperatingState", "string" // Thermostat
attribute "thermostatStatus", "string" // Custom
attribute "scheduledSetpoint", "number" // Custom
attribute "nextScheduledSetpoint", "number" // Custom
attribute "nextScheduledTime", "string" // Custom
attribute "optimisation", "string" // Custom
attribute "windowFunction", "string" // Custom
}
tiles(scale: 2) {
// Main multi
multiAttributeTile(name:"multi", type:"thermostat", width:6, height:4) {
tileAttribute("device.temperature", key: "PRIMARY_CONTROL") {
attributeState("default", label:'${currentValue}°', unit:"C")
}
// Up and Down buttons:
//tileAttribute("device.temperature", key: "VALUE_CONTROL") {
// attributeState("VALUE_UP", action: "raiseSetpoint")
// attributeState("VALUE_DOWN", action: "lowerSetpoint")
//}
// Operating State - used to get background colour when type is 'thermostat'.
tileAttribute("device.thermostatStatus", key: "OPERATING_STATE") {
attributeState("Heating", backgroundColor:"#ffa81e", defaultState: true)
attributeState("Idle (Auto)", backgroundColor:"#44b621")
attributeState("Idle (Custom)", backgroundColor:"#44b621")
attributeState("Idle (Day Off)", backgroundColor:"#44b621")
attributeState("Idle (Economy)", backgroundColor:"#44b621")
attributeState("Idle (Away)", backgroundColor:"#44b621")
attributeState("Off", backgroundColor:"#269bd2")
}
//tileAttribute("device.thermostatMode", key: "THERMOSTAT_MODE") {
// attributeState("off", label:'${name}')
// attributeState("away", label:'${name}')
// attributeState("auto", label:'${name}')
// attributeState("economy", label:'${name}')
// attributeState("dayOff", label:'${name}')
// attributeState("custom", label:'${name}')
//}
//tileAttribute("device.heatingSetpoint", key: "HEATING_SETPOINT") {
// attributeState("default", label:'${currentValue}', unit:"C")
//}
//tileAttribute("device.coolingSetpoint", key: "COOLING_SETPOINT") {
// attributeState("default", label:'${currentValue}', unit:"C")
//}
}
// temperature tile:
valueTile("temperature", "device.temperature", width: 2, height: 2, canChangeIcon: true) {
state("temperature", label:'${currentValue}°', unit:"C", icon:"st.Weather.weather2",
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"]
]
)
}
// thermostatSetpoint tiles:
valueTile("thermostatSetpoint", "device.thermostatSetpoint", width: 3, height: 1) {
state "thermostatSetpoint", label:'Setpoint: ${currentValue}°', unit:"C"
}
valueTile("thermostatSetpointStatus", "device.thermostatSetpointStatus", width: 3, height: 1, decoration: "flat") {
state "thermostatSetpointStatus", label:'${currentValue}', backgroundColor:"#ffffff"
}
standardTile("raiseSetpoint", "device.thermostatSetpoint", width: 1, height: 1, decoration: "flat") {
state "setpoint", action:"raiseSetpoint", icon:"st.thermostat.thermostat-up"
}
standardTile("lowerSetpoint", "device.thermostatSetpoint", width: 1, height: 1, decoration: "flat") {
state "setpoint", action:"lowerSetpoint", icon:"st.thermostat.thermostat-down"
}
standardTile("resume", "device.resume", width: 1, height: 1, decoration: "flat") {
state "default", action:"resume", label:'Resume', icon:"st.samsung.da.oven_ic_send"
}
standardTile("boost", "device.boost", inactiveLabel: false, decoration: "flat", width: 1, height: 1) {
state "default", action:"boost", label:'Boost' // icon TBC
}
standardTile("suppress", "device.suppress", inactiveLabel: false, decoration: "flat", width: 1, height: 1) {
state "default", action:"suppress", label:'Suppress' // icon TBC
}
// thermostatMode/Status Tiles:
// thermostatStatus (also incorporated into the multi tile).
valueTile("thermostatStatus", "device.thermostatStatus", height: 1, width: 6, decoration: "flat") {
state "thermostatStatus", label:'${currentValue}', backgroundColor:"#ffffff"
}
// Single thermostatMode tile that cycles between all modes (too slow).
// To Do: Update with Evohome-specific modes:
standardTile("thermostatMode", "device.thermostatMode", inactiveLabel: false, decoration: "flat") {
state "off", action:"cycleMode", nextState: "updating", icon: "st.thermostat.heating-cooling-off"
state "heat", action:"cycleMode", nextState: "updating", icon: "st.thermostat.heat"
state "cool", action:"cycleMode", nextState: "updating", icon: "st.thermostat.cool"
state "auto", action:"cycleMode", nextState: "updating", icon: "st.thermostat.auto"
state "auxHeatOnly", action:"cycleMode", icon: "st.thermostat.emergency-heat"
state "updating", label:"Working", icon: "st.secondary.secondary"
}
// Individual Mode tiles:
standardTile("auto", "device.auto", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
state "default", action:"auto", icon: "st.thermostat.auto"
}
standardTile("away", "device.away", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
state "default", action:"away", label:'Away' // icon TBC
}
standardTile("custom", "device.custom", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
state "default", action:"custom", label:'Custom' // icon TBC
}
standardTile("dayOff", "device.dayOff", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
state "default", action:"dayOff", label:'Day Off' // icon TBC
}
standardTile("economy", "device.economy", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
state "default", action:"economy", label:'Economy' // icon TBC
}
standardTile("off", "device.off", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
state "default", action:"off", icon:"st.thermostat.heating-cooling-off"
}
// Other tiles:
standardTile("refresh", "device.thermostatMode", inactiveLabel: false, decoration: "flat") {
state "default", action:"refresh.refresh", icon:"st.secondary.refresh"
}
standardTile("test", "device.test", width: 1, height: 1, decoration: "flat") {
state "default", label:'Test', action:"test"
}
main "temperature"
details(
[
"multi",
"thermostatSetpoint","raiseSetpoint","boost","resume",
"thermostatSetpointStatus","lowerSetpoint","suppress","refresh",
"auto","away","custom","dayOff","economy","off"
]
)
}
preferences {
section { // Setpoint Adjustments:
input title: "Setpoint Duration", description: "Configure how long setpoint adjustments are applied for.", displayDuringSetup: true, type: "paragraph", element: "paragraph"
input 'prefSetpointMode', 'enum', title: 'Until', description: '', options: ["Next Switchpoint", "Midday", "Midnight", "Duration", "Permanent"], defaultValue: "Next Switchpoint", required: true, displayDuringSetup: true
input 'prefSetpointDuration', 'number', title: 'Duration (minutes)', description: 'Apply setpoint for this many minutes', range: "1..1440", defaultValue: 60, required: true, displayDuringSetup: true
//input 'prefSetpointTime', 'time', title: 'Time', description: 'Apply setpoint until this time', required: true, displayDuringSetup: true
input title: "Setpoint Temperatures", description: "Configure preset temperatures for the 'Boost' and 'Suppress' buttons.", displayDuringSetup: true, type: "paragraph", element: "paragraph"
input "prefBoostTemperature", "string", title: "'Boost' Temperature", defaultValue: "21.5", required: true, displayDuringSetup: true // use of 'decimal' input type in devices is currently broken.
input "prefSuppressTemperature", "string", title: "'Suppress' Temperature", defaultValue: "15.0", required: true, displayDuringSetup: true // use of 'decimal' input type in devices is currently broken.
}
}
}
/**********************************************************************
* Test Commands:
**********************************************************************/
/**
* test()
*
* Test method, called from test tile.
**/
def test() {
//log.debug "$device.displayName: test(): Properties: ${properties}"
//log.debug "$device.displayName: test(): Settings: ${settings}"
//log.debug "$device.displayName: test(): State: ${state}"
}
/**********************************************************************
* Setup and Configuration Commands:
**********************************************************************/
/**
* installed()
*
* Runs when the app is first installed.
*
* When a device is created by a SmartApp, settings are not populated
* with the defaultValues configured for each input. Therefore, we
* populate the corresponding state.* variables with the input defaultValues.
*
**/
def installed() {
log.debug "${app.label}: Installed with settings: ${settings}"
initialize()
state.installedAt = now()
// These default values will be overwritten by the Evohome SmartApp almost immediately:
state.debug = false
state.updateRefreshTime = 5 // Wait this many seconds after an update before polling.
state.zoneType = 'RadiatorZone'
state.minHeatingSetpoint = formatTemperature(5.0)
state.maxHeatingSetpoint = formatTemperature(35.0)
state.temperatureResolution = formatTemperature(0.5)
state.windowFunctionTemperature = formatTemperature(5.0)
state.targetSetpoint = state.minHeatingSetpoint
// Populate state.* with default values for each preference/input:
state.setpointMode = getInputDefaultValue('prefSetpointMode')
state.setpointDuration = getInputDefaultValue('prefSetpointDuration')
state.boostTemperature = getInputDefaultValue('prefBoostTemperature')
state.suppressTemperature = getInputDefaultValue('prefSuppressTemperature')
}
/**
* updated()
*
* Runs when device settings are changed.
**/
def updated() {
if (state.debug) log.debug "${device.label}: Updating with settings: ${settings}"
// Copy input values to state:
state.setpointMode = settings.prefSetpointMode
state.setpointDuration = settings.prefSetpointDuration
state.boostTemperature = formatTemperature(settings.prefBoostTemperature)
state.suppressTemperature = formatTemperature(settings.prefSuppressTemperature)
}
def initialize() {
sendEvent(name:"temperature", value:"72", unit:"F")
sendEvent(name:"heatingSetpoint", value:"7", unit:"F")
sendEvent(name:"thermostatSetpoint", value:"7", unit:"F")
sendEvent(name:"thermostatMode", value:"cool")
sendEvent(name:"supportedThermostatModes", value:"['auto','cool','eco','rush hour','emergency heat','heat','off']")
}
/**********************************************************************
* SmartApp-Child Interface Commands:
**********************************************************************/
/**
* generateEvent(values)
*
* Called by parent to update the state of this child device.
*
**/
void generateEvent(values) {
log.info "${device.label}: generateEvent(): New values: ${values}"
if(values) {
values.each { name, value ->
if ( name == 'minHeatingSetpoint'
|| name == 'maxHeatingSetpoint'
|| name == 'temperatureResolution'
|| name == 'windowFunctionTemperature'
|| name == 'zoneType'
|| name == 'locationId'
|| name == 'gatewayId'
|| name == 'systemId'
|| name == 'zoneId'
|| name == 'schedule'
|| name == 'debug'
|| name == 'updateRefreshTime'
) {
// Internal state only.
state."${name}" = value
}
else { // Attribute value, so generate an event:
if (name != null && value != null) {
if(name=='temperature'){
//add unit for Temperature because it is needed in Dashboard View
sendEvent(name: name, value: value, unit:"C", displayed: true)
}else{
sendEvent(name: name, value: value, displayed: true)
}
}
else { // If name or value is null, set displayed to false,
// otherwise the 'Recently' view on smartphone app clogs
// up with empty events.
sendEvent(name: name, value: value, displayed: false)
}
// Reset targetSetpoint (used by raiseSetpoint/lowerSetpoint) if heatingSetpoint has changed:
if (name == 'heatingSetpoint') {
state.targetSetpoint = value
}
}
}
}
// Calculate derived attributes (order is important here):
calculateThermostatOperatingState()
calculateOptimisations()
calculateThermostatStatus()
calculateThermostatSetpointStatus()
}
/**********************************************************************
* Capability-related Commands:
**********************************************************************/
/**
* poll()
*
* Polls the device. Required for the "Polling" capability
**/
void poll() {
if (state.debug) log.debug "${device.label}: poll()"
parent.poll(state.zoneId)
}
/**
* refresh()
*
* Refreshes values from the device. Required for the "Refresh" capability.
**/
void refresh() {
if (state.debug) log.debug "${device.label}: refresh()"
sendEvent(name: 'thermostatSetpointStatus', value: 'Updating', displayed: false)
parent.poll(state.zoneId)
}
/**
* setThermostatMode(mode, until=-1)
*
* Set thermostat mode until specified time.
*
* mode: Possible values: 'auto','off','away','dayOff','custom', or 'economy'.
*
* until: (Optional) Time to apply mode until, can be either:
* - Date: Date object representing when override should end.
* - ISO-8601 date string, in format "yyyy-MM-dd'T'HH:mm:ssXX", e.g.: "2016-04-01T00:00:00Z".
* - String: 'permanent'.
* - Number: Duration in hours if mode is 'economy', or days if mode is 'away'/'dayOff'/'custom'.
* Duration will be rounded down to align with Midnight i nthe local timezone
* (e.g. a duration of 1 day will end at midnight tonight). If 0, mode is permanent.
* If duration is not specified, a default value is used from the Evohome SmartApp settings.
*
* Notes: 'Auto' and 'Off' modes are always permanent.
* Thermostat mode is a property of the temperatureControlSystem (i.e. Evohome controller).
* Therefore changing the thermostatMode will affect all zones associated with the same controller.
*
* Example usage:
* setThermostatMode('off', 0) // Set off mode permanently.
* setThermostatMode('away', 1) // Set away mode for one day (i.e. until midnight tonight).
* setThermostatMode('dayOff', 2) // Set dayOff mode for two days (ends tomorrow night).
* setThermostatMode('economy', 2) // Set economy mode for two hours.
*
**/
def setThermostatMode(String mode, until=-1) {
log.info "${device.label}: setThermostatMode(Mode: ${mode}, Until: ${until})"
// Send update via parent:
if (!parent.setThermostatMode(state.systemId, mode, until)) {
sendEvent(name: 'thermostatSetpointStatus', value: 'Updating', displayed: false)
// Wait a few seconds as it takes a while for Evohome to update setpoints in response to a mode change.
pseudoSleep(state.updateRefreshTime * 1000)
parent.poll(0) // Force poll for all zones as thermostatMode is a property of the temperatureControlSystem.
return null
}
else {
log.error "${device.label}: setThermostatMode(): Error: Unable to set thermostat mode."
return 'error'
}
}
/**
* setHeatingSetpoint(setpoint, until=-1)
*
* Set heatingSetpoint until specified time.
*
* setpoint: Setpoint temperature, e.g.: "21.5". Can be a number or string.
* If setpoint is outside allowed range (i.e. minHeatingSetpoint to
* maxHeatingSetpoint) it will be re-written to the appropriate limit.
*
* until: (Optional) Time to apply setpoint until, can be either:
* - Date: date object representing when override should end.
* - ISO-8601 date string, in format "yyyy-MM-dd'T'HH:mm:ssXX", e.g.: "2016-04-01T00:00:00Z".
* - String: 'nextSwitchpoint', 'midnight', 'midday', or 'permanent'.
* - Number: duration in minutes (from now). 0 = permanent.
* If not specified, setpoint duration will default to the
* behaviour defined in the device settings.
*
* Example usage:
* setHeatingSetpoint(21.0) // Set until <device default>.
* setHeatingSetpoint(21.0, 'nextSwitchpoint') // Set until next scheduled switchpoint.
* setHeatingSetpoint(21.0, 'midnight') // Set until midnight.
* setHeatingSetpoint(21.0, 'permanent') // Set permanently.
* setHeatingSetpoint(21.0, 0) // Set permanently.
* setHeatingSetpoint(21.0, 6) // Set for 6 hours.
* setHeatingSetpoint(21.0, '2016-04-01T00:00:00Z') // Set until specific time.
*
**/
def setHeatingSetpoint(setpoint, until=-1) {
if (state.debug) log.debug "${device.label}: setHeatingSetpoint(Setpoint: ${setpoint}, Until: ${until})"
// Clean setpoint:
setpoint = formatTemperature(setpoint)
if (Float.parseFloat(setpoint) < Float.parseFloat(state.minHeatingSetpoint)) {
log.warn "${device.label}: setHeatingSetpoint(): Specified setpoint (${setpoint}) is less than zone's minimum setpoint (${state.minHeatingSetpoint})."
setpoint = state.minHeatingSetpoint
}
else if (Float.parseFloat(setpoint) > Float.parseFloat(state.maxHeatingSetpoint)) {
log.warn "${device.label}: setHeatingSetpoint(): Specified setpoint (${setpoint}) is greater than zone's maximum setpoint (${state.maxHeatingSetpoint})."
setpoint = state.maxHeatingSetpoint
}
// Clean and parse until value:
def untilRes
Calendar c = new GregorianCalendar()
def tzOffset = location.timeZone.getOffset(new Date().getTime()) // Timezone offset to UTC in milliseconds.
// If until has not been specified, determine behaviour from device state.setpointMode:
if (-1 == until) {
switch (state.setpointMode) {
case 'Next Switchpoint':
until = 'nextSwitchpoint'
break
case 'Midday':
until = 'midday'
break
case 'Midnight':
until = 'midnight'
break
case 'Duration':
until = state.setpointDuration ?: 0
break
case 'Time':
// TO DO : construct time, like we do for midnight.
// settings.prefSetpointTime appears to return an ISO dateformat string.
// However using an input of type "time" causes HTTP 500 errors in the IDE, so disabled for now.
// If time has passed, then need to make it the next day.
if (state.debug) log.debug "${device.label}: setHeatingSetpoint(): Time: ${state.SetpointTime}"
until = 'nextSwitchpoint'
break
case 'Permanent':
until = 'permanent'
break
default:
until = 'nextSwitchpoint'
break
}
}
if ('permanent' == until || 0 == until) {
untilRes = 0
}
else if (until instanceof Date) {
untilRes = until
}
else if ('nextSwitchpoint' == until) {
untilRes = new Date().parse("yyyy-MM-dd'T'HH:mm:ssXX", device.currentValue('nextScheduledTime'))
}
else if ('midday' == until) {
untilRes = new Date().parse("yyyy-MM-dd'T'HH:mm:ssXX", new Date().format("yyyy-MM-dd'T'12:00:00XX", location.timeZone))
}
else if ('midnight' == until) {
c.add(Calendar.DATE, 1 ) // Add one day to calendar and use to get midnight in local time:
untilRes = new Date().parse("yyyy-MM-dd'T'HH:mm:ssXX", c.getTime().format("yyyy-MM-dd'T'00:00:00XX", location.timeZone))
}
else if (until ==~ /\d+.*T.*/) { // until is a ISO-8601 date string, so parse:
untilRes = new Date().parse("yyyy-MM-dd'T'HH:mm:ssXX", until)
}
else if (until.isNumber()) { // until is a duration in minutes, so construct date from now():
// Evohome supposedly only accepts setpoints for up to 24 hours, so we should limit minutes to 1440.
// For now, just pass any duration and see if Evohome accepts it...
untilRes = new Date( now() + (Math.round(until) * 60000) )
}
else {
log.warn "${device.label}: setHeatingSetpoint(): until value could not be parsed. Setpoint will be applied permanently."
untilRes = 0
}
log.info "${device.label}: setHeatingSetpoint(): Setting setpoint to: ${setpoint} until: ${untilRes}"
// Send update via parent:
if (!parent.setHeatingSetpoint(state.zoneId, setpoint, untilRes)) {
// Command was successful, but it takes a few seconds for the Evohome cloud service to update with new values.
// Meanwhile, we know the new setpoint and thermostatSetpointMode anyway:
sendEvent(name: 'heatingSetpoint', value: setpoint)
sendEvent(name: 'thermostatSetpoint', value: setpoint)
sendEvent(name: 'thermostatSetpointMode', value: (0 == untilRes) ? 'permanentOverride' : 'temporaryOverride' )
sendEvent(name: 'thermostatSetpointUntil', value: (0 == untilRes) ? null : untilRes.format("yyyy-MM-dd'T'HH:mm:00XX", TimeZone.getTimeZone('UTC')))
calculateThermostatOperatingState()
calculateOptimisations()
calculateThermostatStatus()
sendEvent(name: 'thermostatSetpointStatus', value: 'Updating', displayed: false)
pseudoSleep(state.updateRefreshTime * 1000)
parent.poll(state.zoneId)
return null
}
else {
log.error "${device.label}: setHeatingSetpoint(): Error: Unable to set heating setpoint."
return 'error'
}
}
/**
* clearHeatingSetpoint()
*
* Clear the heatingSetpoint. Will return heatingSetpoint to scheduled value.
* thermostatSetpointMode should return to "followSchedule".
*
**/
def clearHeatingSetpoint() {
log.info "${device.label}: clearHeatingSetpoint()"
// Send update via parent:
if (!parent.clearHeatingSetpoint(state.zoneId)) {
// Command was successful, but it takes a few seconds for the Evohome cloud service
// to update the zone status with the new heatingSetpoint.
// Meanwhile, we know the new thermostatSetpointMode is "followSchedule".
sendEvent(name: 'thermostatSetpointMode', value: 'followSchedule')
sendEvent(name: 'thermostatSetpointStatus', value: 'Updating', displayed: false)
// sleep command is not allowed in SmartThings, so we use psuedoSleep().
pseudoSleep(state.updateRefreshTime * 1000)
parent.poll(state.zoneId)
return null
}
else {
log.error "${device.label}: clearHeatingSetpoint(): Error: Unable to clear heating setpoint."
return 'error'
}
}
/**
* raiseSetpoint()
*
* Raise heatingSetpoint and thermostatSetpoint.
* Increments by state.temperatureResolution (usually 0.5).
*
* Called by raiseSetpoint tile.
*
**/
void raiseSetpoint() {
if (state.debug) log.debug "${device.label}: raiseSetpoint()"
def mode = device.currentValue("thermostatMode")
def targetSp = new BigDecimal(state.targetSetpoint)
def tempRes = new BigDecimal(state.temperatureResolution) // (normally 0.5)
def maxSp = new BigDecimal(state.maxHeatingSetpoint)
if ('off' == mode || 'away' == mode) {
log.warn "${device.label}: raiseSetpoint(): thermostat mode (${mode}) does not allow altering the temperature setpoint."
}
else {
targetSp += tempRes
if (targetSp > maxSp) {
targetSp = maxSp
}
state.targetSetpoint = targetSp
log.info "${device.label}: raiseSetpoint(): Target setpoint raised to: ${targetSp}"
sendEvent(name: 'thermostatSetpointStatus', value: 'Updating', displayed: false)
runIn(3, "alterSetpoint", [overwrite: true]) // Wait three seconds in case targetSetpoint is changed again.
}
}
/**
* lowerSetpoint()
*
* Lower heatingSetpoint and thermostatSetpoint.
* Increments by state.temperatureResolution (usually 0.5).
*
* Called by lowerSetpoint tile.
*
**/
void lowerSetpoint() {
if (state.debug) log.debug "${device.label}: lowerSetpoint()"
def mode = device.currentValue("thermostatMode")
def targetSp = new BigDecimal(state.targetSetpoint)
def tempRes = new BigDecimal(state.temperatureResolution) // (normally 0.5)
def minSp = new BigDecimal(state.minHeatingSetpoint)
if ('off' == mode || 'away' == mode) {
log.warn "${device.label}: lowerSetpoint(): thermostat mode (${mode}) does not allow altering the temperature setpoint."
}
else {
targetSp -= tempRes
if (targetSp < minSp) {
targetSp = minSp
}
state.targetSetpoint = targetSp
log.info "${device.label}: lowerSetpoint(): Target setpoint lowered to: ${targetSp}"
sendEvent(name: 'thermostatSetpointStatus', value: 'Updating', displayed: false)
runIn(3, "alterSetpoint", [overwrite: true]) // Wait three seconds in case targetSetpoint is changed again.
}
}
/**
* alterSetpoint()
*
* Proxy command called by raiseSetpoint and lowerSetpoint, as runIn
* cannot pass targetSetpoint diretly to setHeatingSetpoint.
*
**/
private alterSetpoint() {
if (state.debug) log.debug "${device.label}: alterSetpoint()"
setHeatingSetpoint(state.targetSetpoint)
}
/**********************************************************************
* Convenience Commands:
* These commands alias other commands with preset parameters.
**********************************************************************/
void resume() {
if (state.debug) log.debug "${device.label}: resume()"
clearHeatingSetpoint()
}
void auto() {
if (state.debug) log.debug "${device.label}: auto()"
setThermostatMode('auto')
}
void heat() {
if (state.debug) log.debug "${device.label}: heat()"
setThermostatMode('auto')
}
void off() {
if (state.debug) log.debug "${device.label}: off()"
setThermostatMode('off')
}
void away(until=-1) {
if (state.debug) log.debug "${device.label}: away()"
setThermostatMode('away', until)
}
void custom(until=-1) {
if (state.debug) log.debug "${device.label}: custom()"
setThermostatMode('custom', until)
}
void dayOff(until=-1) {
if (state.debug) log.debug "${device.label}: dayOff()"
setThermostatMode('dayOff', until)
}
void economy(until=-1) {
if (state.debug) log.debug "${device.label}: economy()"
setThermostatMode('economy', until)
}
void boost() {
if (state.debug) log.debug "${device.label}: boost()"
setHeatingSetpoint(state.boostTemperature)
}
void suppress() {
if (state.debug) log.debug "${device.label}: suppress()"
setHeatingSetpoint(state.suppressTemperature)
}
/**********************************************************************
* Helper Commands:
**********************************************************************/
/**
* pseudoSleep(ms)
*
* Substitute for sleep() command.
*
**/
private pseudoSleep(ms) {
def start = now()
while (now() < start + ms) {
// Do nothing, just wait.
}
}
/**
* getInputDefaultValue(inputName)
*
* Get the default value for the specified input.
*
**/
private getInputDefaultValue(inputName) {
if (state.debug) log.debug "${device.label}: getInputDefaultValue()"
def returnValue
properties.preferences?.sections.each { section ->
section.input.each { input ->
if (input.name == inputName) {
returnValue = input.defaultValue
}
}
}
return returnValue
}
/**
* formatTemperature(t)
*
* Format temperature value to one decimal place.
* t: can be string, float, bigdecimal...
* Returns as string.
**/
private formatTemperature(t) {
//return Float.parseFloat("${t}").round(1)
//return String.format("%.1f", Float.parseFloat("${t}").round(1))
return Float.parseFloat("${t}").round(1).toString()
}
/**
* formatThermostatModeForDisp(mode)
*
* Translate SmartThings values to display values.
*
**/
private formatThermostatModeForDisp(mode) {
if (state.debug) log.debug "${device.label}: formatThermostatModeForDisp()"
switch (mode) {
case 'auto':
mode = 'Auto'
break
case 'economy':
mode = 'Economy'
break
case 'away':
mode = 'Away'
break
case 'custom':
mode = 'Custom'
break
case 'dayOff':
mode = 'Day Off'
break
case 'off':
mode = 'Off'
break
default:
mode = 'Unknown'
break
}
return mode
}
/**
* calculateThermostatOperatingState()
*
* Calculates thermostatOperatingState and generates event accordingly.
*
**/
private calculateThermostatOperatingState() {
if (state.debug) log.debug "${device.label}: calculateThermostatOperatingState()"
def tOS
if ('off' == device.currentValue('thermostatMode')) {
tOS = 'off'
}
else if (device.currentValue("temperature") < device.currentValue("thermostatSetpoint")) {
tOS = 'heating'
}
else {
tOS = 'idle'
}
sendEvent(name: 'thermostatOperatingState', value: tOS)
}
/**
* calculateOptimisations()
*
* Calculates if optimisation and windowFunction are active
* and generates events accordingly.
*
* This isn't going to be 100% perfect, but is reasonably accurate.
*
**/
private calculateOptimisations() {
if (state.debug) log.debug "${device.label}: calculateOptimisations()"
def newOptValue = 'inactive'
def newWdfValue = 'inactive'
// Convert temp values to BigDecimals for comparison:
def heatingSp = new BigDecimal(device.currentValue('heatingSetpoint'))
def scheduledSp = new BigDecimal(device.currentValue('scheduledSetpoint'))
def nextScheduledSp = new BigDecimal(device.currentValue('nextScheduledSetpoint'))
def windowTemp = new BigDecimal(state.windowFunctionTemperature)
if ('auto' != device.currentValue('thermostatMode')) {
// Optimisations cannot be active if thermostatMode is not 'auto'.
}
else if ('followSchedule' != device.currentValue('thermostatSetpointMode')) {
// Optimisations cannot be active if thermostatSetpointMode is not 'followSchedule'.
// There must be a manual override.
}
else if (heatingSp == scheduledSp) {
// heatingSetpoint is what it should be, so no reason to suspect that optimisations are active.
}
else if (heatingSp == nextScheduledSp) {
// heatingSetpoint is the nextScheduledSetpoint, so optimisation is likely active:
newOptValue = 'active'
}
else if (heatingSp == windowTemp) {
// heatingSetpoint is the windowFunctionTemp, so windowFunction is likely active:
newWdfValue = 'active'
}
sendEvent(name: 'optimisation', value: newOptValue)
sendEvent(name: 'windowFunction', value: newWdfValue)
}
/**
* calculateThermostatStatus()
*
* Calculates thermostatStatus and generates event accordingly.
*
* thermostatStatus is a text summary of thermostatMode and thermostatOperatingState.
*
**/
private calculateThermostatStatus() {
if (state.debug) log.debug "${device.label}: calculateThermostatStatus()"
def newThermostatStatus = ''
def thermostatModeDisp = formatThermostatModeForDisp(device.currentValue('thermostatMode'))
def setpoint = device.currentValue('thermostatSetpoint')
if ('Off' == thermostatModeDisp) {
newThermostatStatus = 'Off'
}
else if('heating' == device.currentValue('thermostatOperatingState')) {
newThermostatStatus = "Heating to ${setpoint}° (${thermostatModeDisp})"
}
else {
newThermostatStatus = "Idle (${thermostatModeDisp})"
}
sendEvent(name: 'thermostatStatus', value: newThermostatStatus)
}
/**
* calculateThermostatSetpointStatus()
*
* Calculates thermostatSetpointStatus and generates event accordingly.
*
* thermostatSetpointStatus is a text summary of thermostatSetpointMode and thermostatSetpointUntil.
* It also indicates if 'optimisation' or 'windowFunction' is active.
*
**/
private calculateThermostatSetpointStatus() {
if (state.debug) log.debug "${device.label}: calculateThermostatSetpointStatus()"
def newThermostatSetpointStatus = ''
def setpointMode = device.currentValue('thermostatSetpointMode')
if ('off' == device.currentValue('thermostatMode')) {
newThermostatSetpointStatus = 'Off'
}
else if ('away' == device.currentValue('thermostatMode')) {
newThermostatSetpointStatus = 'Away'
}
else if ('active' == device.currentValue('optimisation')) {
newThermostatSetpointStatus = 'Optimisation Active'
}
else if ('active' == device.currentValue('windowFunction')) {
newThermostatSetpointStatus = 'Window Function Active'
}
else if ('followSchedule' == setpointMode) {
newThermostatSetpointStatus = 'Following Schedule'
}
else if ('permanentOverride' == setpointMode) {
newThermostatSetpointStatus = 'Permanent'
}
else {
def untilStr = device.currentValue('thermostatSetpointUntil')
if (untilStr) {
//def nowDate = new Date()
// thermostatSetpointUntil is an ISO-8601 date format in UTC, and parse() seems to assume date is in UTC.
def untilDate = new Date().parse("yyyy-MM-dd'T'HH:mm:ssXX", untilStr)
def untilDisp = ''
if (untilDate.format("u") == new Date().format("u")) { // Compare day of week to current day of week (today).
untilDisp = untilDate.format("HH:mm", location.timeZone) // Same day, so just show time.
}
else {
untilDisp = untilDate.format("HH:mm 'on' EEEE", location.timeZone) // Different day, so include name of day.
}
newThermostatSetpointStatus = "Temporary Until ${untilDisp}"
}
else {
newThermostatSetpointStatus = "Temporary"
}
}
sendEvent(name: 'thermostatSetpointStatus', value: newThermostatSetpointStatus)
}
/**
* Copyright 2016 David Lomas (codersaur)
*
* Name: Evohome (Connect)
*
* Author: David Lomas (codersaur)
*
* Date: 2016-04-05
*
* Version: 0.08
*
* Description:
* - Connect your Honeywell Evohome System to SmartThings.
* - Requires the Evohome Heating Zone device handler.
* - For latest documentation see: https://github.com/codersaur/SmartThings
*
* Version History:
*
* 2016-04-05: v0.08
* - New 'Update Refresh Time' setting to control polling after making an update.
* - poll() - If onlyZoneId is 0, this will force a status update for all zones.
*
* 2016-04-04: v0.07
* - Additional info log messages.
*
* 2016-04-03: v0.06
* - Initial Beta Release
*
* To Do:
* - Add support for hot water zones (new device handler).
* - Tidy up settings: See: http://docs.smartthings.com/en/latest/smartapp-developers-guide/preferences-and-settings.html
* - Allow Evohome zones to be (de)selected as part of the setup process.
* - Enable notifications if connection to Evohome cloud fails.
* - Expose whether thremoStatMode is permanent or temporary, and if temporary for how long. Get from 'tcs.systemModeStatus.*'. i.e thermostatModeUntil
* - Investigate if Evohome supports registing a callback so changes in Evohome are pushed to SmartThings instantly (instead of relying on polling).
*
* License:
* 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: "Evohome (Connect)",
namespace: "codersaur",
author: "David Lomas (codersaur)",
description: "Connect your Honeywell Evohome System to SmartThings.",
category: "My Apps",
iconUrl: "http://cdn.device-icons.smartthings.com/Home/home1-icn.png",
iconX2Url: "http://cdn.device-icons.smartthings.com/Home/home1-icn.png",
iconX3Url: "http://cdn.device-icons.smartthings.com/Home/home1-icn.png",
singleInstance: true
)
preferences {
section ("Evohome:") {
input "prefEvohomeUsername", "text", title: "Username", required: true, displayDuringSetup: true
input "prefEvohomePassword", "password", title: "Password", required: true, displayDuringSetup: true
input title: "Advanced Settings:", displayDuringSetup: true, type: "paragraph", element: "paragraph", description: "Change these only if needed"
input "prefEvohomeStatusPollInterval", "number", title: "Polling Interval (minutes)", range: "1..60", defaultValue: 5, required: true, displayDuringSetup: true, description: "Poll Evohome every n minutes"
input "prefEvohomeUpdateRefreshTime", "number", title: "Update Refresh Time (seconds)", range: "2..60", defaultValue: 3, required: true, displayDuringSetup: true, description: "Wait n seconds after an update before polling"
input "prefEvohomeWindowFuncTemp", "decimal", title: "Window Function Temperature", range: "0..100", defaultValue: 5.0, required: true, displayDuringSetup: true, description: "Must match Evohome controller setting"
input title: "Thermostat Modes", description: "Configure how long thermostat modes are applied for by default. Set to zero to apply modes permanently.", displayDuringSetup: true, type: "paragraph", element: "paragraph"
input 'prefThermostatModeDuration', 'number', title: 'Away/Custom/DayOff Mode (days):', range: "0..99", defaultValue: 0, required: true, displayDuringSetup: true, description: 'Apply thermostat modes for this many days'
input 'prefThermostatEconomyDuration', 'number', title: 'Economy Mode (hours):', range: "0..24", defaultValue: 0, required: true, displayDuringSetup: true, description: 'Apply economy mode for this many hours'
}
section("General:") {
input "prefDebugMode", "bool", title: "Enable debug logging?", defaultValue: true, displayDuringSetup: true
}
}
/**********************************************************************
* Setup and Configuration Commands:
**********************************************************************/
/**
* installed()
*
* Runs when the app is first installed.
*
**/
def installed() {
atomicState.installedAt = now()
log.debug "${app.label}: Installed with settings: ${settings}"
//Manual update
//updateChildDeviceConfig()
//updateChildDevice()
}
/**
* uninstalled()
*
* Runs when the app is uninstalled.
*
**/
def uninstalled() {
if(getChildDevices()) {
removeChildDevices(getChildDevices())
}
}
/**
* updated()
*
* Runs when app settings are changed.
*
**/
void updated() {
if (atomicState.debug) log.debug "${app.label}: Updating with settings: ${settings}"
// General:
/*atomicState.debug = settings.prefDebugMode
// Evohome:
atomicState.evohomeEndpoint = 'https://mytotalconnectcomfort.com'
atomicState.evohomeAuth = [tokenLifetimePercentThreshold : 50] // Auth Token will be refreshed when down to 50% of its lifetime.
atomicState.evohomeStatusPollInterval = settings.prefEvohomeStatusPollInterval // Poll interval for status updates (minutes).
atomicState.evohomeSchedulePollInterval = 60 // Hardcoded to 1hr (minutes).
atomicState.evohomeUpdateRefreshTime = settings.prefEvohomeUpdateRefreshTime // Wait this many seconds after an update before polling.
// Thermostat Mode Durations:
atomicState.thermostatModeDuration = settings.prefThermostatModeDuration
atomicState.thermostatEconomyDuration = settings.prefThermostatEconomyDuration
// Force Authentication:
authenticate()
// Refresh Subscriptions and Schedules:
manageSubscriptions()
manageSchedules()
// Refresh child device configuration:
getEvohomeConfig()*/
//Manual update
updateChildDeviceConfig()
updateChildDevice()
// Run a poll, but defer it so that updated() returns sooner:
//runIn(5, "poll")
}
/**********************************************************************
* Management Commands:
**********************************************************************/
/**
* manageSchedules()
*
* Check scheduled tasks have not stalled, and re-schedule if necessary.
* Generates a random offset (seconds) for each scheduled task.
*
* Schedules:
* - manageAuth() - every 5 mins.
* - poll() - every minute.
*
**/
void manageSchedules() {
if (atomicState.debug) log.debug "${app.label}: manageSchedules()"
// Generate a random offset (1-60):
Random rand = new Random(now())
def randomOffset = 0
// manageAuth (every 5 mins):
if (1==1) { // To Do: Test if schedule has actually stalled.
if (atomicState.debug) log.debug "${app.label}: manageSchedules(): Re-scheduling manageAuth()"
try {
unschedule(manageAuth)
}
catch(e) {
//if (atomicState.debug) log.debug "${app.label}: manageSchedules(): Unschedule failed"
}
randomOffset = rand.nextInt(60)
schedule("${randomOffset} 0/5 * * * ?", "manageAuth")
}
// poll():
if (1==1) { // To Do: Test if schedule has actually stalled.
if (atomicState.debug) log.debug "${app.label}: manageSchedules(): Re-scheduling poll()"
try {
unschedule(poll)
}
catch(e) {
//if (atomicState.debug) log.debug "${app.label}: manageSchedules(): Unschedule failed"
}
randomOffset = rand.nextInt(60)
schedule("${randomOffset} 0/1 * * * ?", "poll")
}
}
/**
* manageSubscriptions()
*
* Unsubscribe/Subscribe.
**/
void manageSubscriptions() {
if (atomicState.debug) log.debug "${app.label}: manageSubscriptions()"
// Unsubscribe:
unsubscribe()
// Subscribe to App Touch events:
subscribe(app,handleAppTouch)
}
/**
* manageAuth()
*
* Ensures authenication token is valid.
* Refreshes Auth Token if lifetime has exceeded evohomeAuthTokenLifetimePercentThreshold.
* Re-authenticates if Auth Token has expired completely.
* Otherwise, done nothing.
*
* Should be scheduled to run every 1-5 minutes.
**/
void manageAuth() {
if (atomicState.debug) log.debug "${app.label}: manageAuth()"
// Check if Auth Token is valid, if not authenticate:
if (!atomicState.evohomeAuth.authToken) {
log.info "${app.label}: manageAuth(): No Auth Token. Authenticating..."
authenticate()
}
else if (atomicState.evohomeAuthFailed) {
log.info "${app.label}: manageAuth(): Auth has failed. Authenticating..."
authenticate()
}
else if (!atomicState.evohomeAuth.expiresAt.isNumber() || now() >= atomicState.evohomeAuth.expiresAt) {
log.info "${app.label}: manageAuth(): Auth Token has expired. Authenticating..."
authenticate()
}
else {
// Check if Auth Token should be refreshed:
def refreshAt = atomicState.evohomeAuth.expiresAt - ( 1000 * (atomicState.evohomeAuth.tokenLifetime * atomicState.evohomeAuth.tokenLifetimePercentThreshold / 100))
if (now() >= refreshAt) {
log.info "${app.label}: manageAuth(): Auth Token needs to be refreshed before it expires."
refreshAuthToken()
}
else {
log.info "${app.label}: manageAuth(): Auth Token is okay."
}
}
}
/**
* poll(onlyZoneId=-1)
*
* This is the main command that co-ordinates retrieval of information from the Evohome API
* and its dissemination to child devices. It should be scheduled to run every minute.
*
* Different types of information are collected on different schedules:
* - Zone status information is polled according to ${evohomeStatusPollInterval}.
* - Zone schedules are polled according to ${evohomeSchedulePollInterval}.
*
* poll() can be called by a child device when an update has been made, in which case
* onlyZoneId will be specified, and only that zone will be updated.
*
* If onlyZoneId is 0, this will force a status update for all zones, igonoring the poll
* interval. This should only be used after setThremostatMode() call.
*
* If onlyZoneId is not specified all zones are updated, but only if the relevent poll
* interval has been exceeded.
*
**/
void poll(onlyZoneId=-1) {
if (atomicState.debug) log.debug "${app.label}: poll(${onlyZoneId})"
// Check if there's been an authentication failure:
if (atomicState.evohomeAuthFailed) {
manageAuth()
}
if (onlyZoneId == 0) { // Force a status update for all zones (used after a thermostatMode update):
getEvohomeStatus()
updateChildDevice()
}
else if (onlyZoneId != -1) { // A zoneId has been specified, so just get the status and update the relevent device:
getEvohomeStatus(onlyZoneId)
updateChildDevice(onlyZoneId)
}
else { // Get status and schedule for all zones, but only if the relevent poll interval has been exceeded:
// Adjust intervals to allow for poll() execution time:
def evohomeStatusPollThresh = (atomicState.evohomeStatusPollInterval * 60) - 30
def evohomeSchedulePollThresh = (atomicState.evohomeSchedulePollInterval * 60) - 30
// Get zone status:
if (!atomicState.evohomeStatusUpdatedAt || atomicState.evohomeStatusUpdatedAt + (1000 * evohomeStatusPollThresh) < now()) {
getEvohomeStatus()
}
// Get zone schedules:
if (!atomicState.evohomeSchedulesUpdatedAt || atomicState.evohomeSchedulesUpdatedAt + (1000 * evohomeSchedulePollThresh) < now()) {
getEvohomeSchedules()
}
// Update all child devices:
updateChildDevice()
}
}
/**********************************************************************
* Event Handlers:
**********************************************************************/
/**
* handleAppTouch(evt)
*
* App touch event handler.
* Used for testing and debugging.
*
**/
void handleAppTouch(evt) {
if (atomicState.debug) log.debug "${app.label}: handleAppTouch()"
//manageAuth()
//manageSchedules()
//getEvohomeConfig()
//updateChildDeviceConfig()
poll()
}
/**********************************************************************
* SmartApp-Child Interface Commands:
**********************************************************************/
/**
* updateChildDeviceConfig()
*
* Add/Remove/Update Child Devices based on atomicState.evohomeConfig
* and update their internal state.
*
**/
void updateChildDeviceConfig() {
if (atomicState.debug) log.debug "${app.label}: updateChildDeviceConfig()"
def dni = 'networkID.test.123'
//activeDnis << dni
def values = [
'debug': '',
'updateRefreshTime': '',
'minHeatingSetpoint': '',
'maxHeatingSetpoint': '',
'temperatureResolution': '',
'windowFunctionTemperature': '',
'zoneType': '',
'locationId': '',
'gatewayId': '',
'systemId': '',
'zoneId': ''
]
def d = getChildDevice(dni)
if(!d) {
try {
values.put('label', "test Heating Zone (Evohome)")
log.info "${app.label}: updateChildDeviceConfig(): Creating device: Name: ${values.label}, DNI: ${dni}"
d = addChildDevice(app.namespace, "EvohomecodesaurWchanges", dni, null, values)
} catch (e) {
log.error "${app.label}: updateChildDeviceConfig(): Error creating device: Name: ${values.label}, DNI: ${dni}, Error: ${e}"
}
}
if(d) {
d.generateEvent(values)
}
// Build list of active DNIs, any existing children with DNIs not in here will be deleted.
/*def activeDnis = []
// Iterate through evohomeConfig, adding new Evohome Heating Zone devices where necessary.
atomicState.evohomeConfig.each { loc ->
loc.gateways.each { gateway ->
gateway.temperatureControlSystems.each { tcs ->
tcs.zones.each { zone ->
def dni = generateDni(loc.locationInfo.locationId, gateway.gatewayInfo.gatewayId, tcs.systemId, zone.zoneId )
activeDnis << dni
def values = [
'debug': atomicState.debug,
'updateRefreshTime': atomicState.evohomeUpdateRefreshTime,
'minHeatingSetpoint': formatTemperature(zone?.heatSetpointCapabilities?.minHeatSetpoint),
'maxHeatingSetpoint': formatTemperature(zone?.heatSetpointCapabilities?.maxHeatSetpoint),
'temperatureResolution': zone?.heatSetpointCapabilities?.valueResolution,
'windowFunctionTemperature': formatTemperature(settings.prefEvohomeWindowFuncTemp),
'zoneType': zone?.zoneType,
'locationId': loc.locationInfo.locationId,
'gatewayId': gateway.gatewayInfo.gatewayId,
'systemId': tcs.systemId,
'zoneId': zone.zoneId
]
def d = getChildDevice(dni)
if(!d) {
try {
values.put('label', "${zone.name} Heating Zone (Evohome)")
log.info "${app.label}: updateChildDeviceConfig(): Creating device: Name: ${values.label}, DNI: ${dni}"
d = addChildDevice(app.namespace, "EvohomecodesaurWchanges", dni, null, values)/*****check******
} catch (e) {
log.error "${app.label}: updateChildDeviceConfig(): Error creating device: Name: ${values.label}, DNI: ${dni}, Error: ${e}"
}
}
if(d) {
d.generateEvent(values)/***check****que onda con este generateEvent
}
}
}
}
}*/
/*if (atomicState.debug) log.debug "${app.label}: updateChildDeviceConfig(): Active DNIs: ${activeDnis}"
// Delete Devices:
def delete = getChildDevices().findAll { !activeDnis.contains(it.deviceNetworkId) }
if (atomicState.debug) log.debug "${app.label}: updateChildDeviceConfig(): Found ${delete.size} devices to delete."
delete.each {
log.info "${app.label}: updateChildDeviceConfig(): Deleting device with DNI: ${it.deviceNetworkId}"
try {
deleteChildDevice(it.deviceNetworkId)
}
catch(e) {
log.error "${app.label}: updateChildDeviceConfig(): Error deleting device with DNI: ${it.deviceNetworkId}. Error: ${e}"
}
}*/
}
/**
* updateChildDevice(onlyZoneId=-1)
*
* Update the attributes of a child device from atomicState.evohomeStatus
* and atomicState.evohomeSchedules.
*
* If onlyZoneId is not specified, then all zones are updated.
*
* Recalculates scheduledSetpoint, nextScheduledSetpoint, and nextScheduledTime.
*
**/
void updateChildDevice(onlyZoneId=-1) {
if (atomicState.debug) log.debug "${app.label}: updateChildDevice(${onlyZoneId})"
def dni = 'networkID.test.123'
def d = getChildDevice(dni)
if(d) {
def values = [
'temperature': '35',
'heatingSetpoint': '27',
'thermostatSetpoint': '38',
'thermostatMode': formatThermostatMode('Eco')
]
if (atomicState.debug) log.debug "${app.label}: updateChildDevice(): Updating Device with DNI: ${dni} with data: ${values}"
d.generateEvent(values)
} else {
if (atomicState.debug) log.debug "${app.label}: updateChildDevice(): Device with DNI: ${dni} does not exist, so skipping status update."
}
/*atomicState.evohomeStatus.each { loc ->
loc.gateways.each { gateway ->
gateway.temperatureControlSystems.each { tcs ->
tcs.zones.each { zone ->
if (onlyZoneId == -1 || onlyZoneId == zone.zoneId) { // Filter on zoneId if one has been specified.
def dni = generateDni(loc.locationId, gateway.gatewayId, tcs.systemId, zone.zoneId)
def d = getChildDevice(dni)
if(d) {
def schedule = atomicState.evohomeSchedules.find { it.dni == dni}
def currSw = getCurrentSwitchpoint(schedule.schedule)
def nextSw = getNextSwitchpoint(schedule.schedule)
def values = [
'temperature': formatTemperature(zone?.temperatureStatus?.temperature),
//'isTemperatureAvailable': zone?.temperatureStatus?.isAvailable,
'heatingSetpoint': formatTemperature(zone?.heatSetpointStatus?.targetTemperature),
'thermostatSetpoint': formatTemperature(zone?.heatSetpointStatus?.targetTemperature),
'thermostatSetpointMode': formatSetpointMode(zone?.heatSetpointStatus?.setpointMode),
'thermostatSetpointUntil': zone?.heatSetpointStatus?.until,
'thermostatMode': formatThermostatMode(tcs?.systemModeStatus?.mode),
'scheduledSetpoint': formatTemperature(currSw.temperature),
'nextScheduledSetpoint': formatTemperature(nextSw.temperature),
'nextScheduledTime': nextSw.time
]
if (atomicState.debug) log.debug "${app.label}: updateChildDevice(): Updating Device with DNI: ${dni} with data: ${values}"
d.generateEvent(values)/****check -- this is where the capabilities are updated, how they identify the device?***
} else {
if (atomicState.debug) log.debug "${app.label}: updateChildDevice(): Device with DNI: ${dni} does not exist, so skipping status update."
}
}
}
}
}
}*/
}
/**********************************************************************
* Evohome API Commands:
**********************************************************************/
/**
* authenticate()
*
* Authenticate to Evohome.
*
**/
private authenticate() {
if (atomicState.debug) log.debug "${app.label}: authenticate()"
def requestParams = [
method: 'POST',
uri: 'https://mytotalconnectcomfort.com',
path: '/Auth/OAuth/Token',
headers: [
'Authorization': 'Basic YjAxM2FhMjYtOTcyNC00ZGJkLTg4OTctMDQ4YjlhYWRhMjQ5OnRlc3Q=',
'Accept': 'application/json, application/xml, text/json, text/x-json, text/javascript, text/xml',
'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8'
],
body: [
'grant_type': 'password',
'scope': 'EMEA-V1-Basic EMEA-V1-Anonymous EMEA-V1-Get-Current-User-Account EMEA-V1-Get-Location-Installation-Info-By-UserId',
'Username': settings.prefEvohomeUsername,
'Password': settings.prefEvohomePassword
]
]
try {
httpPost(requestParams) { resp ->
if(resp.status == 200 && resp.data) {
// Update evohomeAuth:
// We can't just '.put' or '<<' with atomicState, we have to make a temp copy, edit, and then re-assign.
def tmpAuth = atomicState.evohomeAuth ?: [:]
tmpAuth.put('lastUpdated' , now())
tmpAuth.put('authToken' , resp?.data?.access_token)
tmpAuth.put('tokenLifetime' , resp?.data?.expires_in.toInteger() ?: 0)
tmpAuth.put('expiresAt' , now() + (tmpAuth.tokenLifetime * 1000))
tmpAuth.put('refreshToken' , resp?.data?.refresh_token)
atomicState.evohomeAuth = tmpAuth
atomicState.evohomeAuthFailed = false
if (atomicState.debug) log.debug "${app.label}: authenticate(): New evohomeAuth: ${atomicState.evohomeAuth}"
def exp = new Date(tmpAuth.expiresAt)
log.info "${app.label}: authenticate(): New Auth Token Expires At: ${exp}"
// Update evohomeHeaders:
def tmpHeaders = atomicState.evohomeHeaders ?: [:]
tmpHeaders.put('Authorization',"bearer ${atomicState.evohomeAuth.authToken}")
tmpHeaders.put('applicationId', 'b013aa26-9724-4dbd-8897-048b9aada249')/******este ID que es?**********/
tmpHeaders.put('Accept', 'application/json, application/xml, text/json, text/x-json, text/javascript, text/xml')
atomicState.evohomeHeaders = tmpHeaders
if (atomicState.debug) log.debug "${app.label}: authenticate(): New evohomeHeaders: ${atomicState.evohomeHeaders}"
// Now get User Account info:
getEvohomeUserAccount()
}
else {
log.error "${app.label}: authenticate(): No Data. Response Status: ${resp.status}"
atomicState.evohomeAuthFailed = true
}
}
} catch (groovyx.net.http.HttpResponseException e) {
log.error "${app.label}: authenticate(): Error: e.statusCode ${e.statusCode}"
atomicState.evohomeAuthFailed = true
}
}
/**
* refreshAuthToken()
*
* Refresh Auth Token.
* If token refresh fails, then authenticate() is called.
* Request is simlar to authenticate, but with grant_type = 'refresh_token' and 'refresh_token'.
*
**/
private refreshAuthToken() {
if (atomicState.debug) log.debug "${app.label}: refreshAuthToken()"
def requestParams = [
method: 'POST',
uri: 'https://mytotalconnectcomfort.com',
path: '/Auth/OAuth/Token',
headers: [
'Authorization': 'Basic YjAxM2FhMjYtOTcyNC00ZGJkLTg4OTctMDQ4YjlhYWRhMjQ5OnRlc3Q=',
'Accept': 'application/json, application/xml, text/json, text/x-json, text/javascript, text/xml',
'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8'
],
body: [
'grant_type': 'refresh_token',
'scope': 'EMEA-V1-Basic EMEA-V1-Anonymous EMEA-V1-Get-Current-User-Account EMEA-V1-Get-Location-Installation-Info-By-UserId',
'refresh_token': atomicState.evohomeAuth.refreshToken
]
]
try {
httpPost(requestParams) { resp ->
if(resp.status == 200 && resp.data) {
// Update evohomeAuth:
// We can't just '.put' or '<<' with atomicState, we have to make a temp copy, edit, and then re-assign.
def tmpAuth = atomicState.evohomeAuth ?: [:]
tmpAuth.put('lastUpdated' , now())
tmpAuth.put('authToken' , resp?.data?.access_token)
tmpAuth.put('tokenLifetime' , resp?.data?.expires_in.toInteger() ?: 0)
tmpAuth.put('expiresAt' , now() + (tmpAuth.tokenLifetime * 1000))
tmpAuth.put('refreshToken' , resp?.data?.refresh_token)
atomicState.evohomeAuth = tmpAuth
atomicState.evohomeAuthFailed = false
if (atomicState.debug) log.debug "${app.label}: refreshAuthToken(): New evohomeAuth: ${atomicState.evohomeAuth}"
def exp = new Date(tmpAuth.expiresAt)
log.info "${app.label}: refreshAuthToken(): New Auth Token Expires At: ${exp}"
// Update evohomeHeaders:
def tmpHeaders = atomicState.evohomeHeaders ?: [:]
tmpHeaders.put('Authorization',"bearer ${atomicState.evohomeAuth.authToken}")
tmpHeaders.put('applicationId', 'b013aa26-9724-4dbd-8897-048b9aada249')
tmpHeaders.put('Accept', 'application/json, application/xml, text/json, text/x-json, text/javascript, text/xml')
atomicState.evohomeHeaders = tmpHeaders
if (atomicState.debug) log.debug "${app.label}: refreshAuthToken(): New evohomeHeaders: ${atomicState.evohomeHeaders}"
// Now get User Account info:
getEvohomeUserAccount()
}
else {
log.error "${app.label}: refreshAuthToken(): No Data. Response Status: ${resp.status}"
}
}
} catch (groovyx.net.http.HttpResponseException e) {
log.error "${app.label}: refreshAuthToken(): Error: e.statusCode ${e.statusCode}"
// If Unauthorized (401) then re-authenticate:
if (e.statusCode == 401) {
atomicState.evohomeAuthFailed = true
authenticate()
}
}
}
/**
* getEvohomeUserAccount()
*
* Gets user account info and stores in atomicState.evohomeUserAccount.
*
**/
private getEvohomeUserAccount() {
log.info "${app.label}: getEvohomeUserAccount(): Getting user account information."
def requestParams = [
method: 'GET',
uri: atomicState.evohomeEndpoint,
path: '/WebAPI/emea/api/v1/userAccount',
headers: atomicState.evohomeHeaders
]
try {
httpGet(requestParams) { resp ->
if (resp.status == 200 && resp.data) {
atomicState.evohomeUserAccount = resp.data
if (atomicState.debug) log.debug "${app.label}: getEvohomeUserAccount(): Data: ${atomicState.evohomeUserAccount}"
}
else {
log.error "${app.label}: getEvohomeUserAccount(): No Data. Response Status: ${resp.status}"
}
}
} catch (groovyx.net.http.HttpResponseException e) {
log.error "${app.label}: getEvohomeUserAccount(): Error: e.statusCode ${e.statusCode}"
if (e.statusCode == 401) {
atomicState.evohomeAuthFailed = true
}
}
}
/**
* getEvohomeConfig()
*
* Gets Evohome configuration for all locations and stores in atomicState.evohomeConfig.
*
**/
private getEvohomeConfig() {
log.info "${app.label}: getEvohomeConfig(): Getting configuration for all locations."
def requestParams = [
method: 'GET',
uri: atomicState.evohomeEndpoint,
path: '/WebAPI/emea/api/v1/location/installationInfo',
query: [
'userId': atomicState.evohomeUserAccount.userId,
'includeTemperatureControlSystems': 'True'
],
headers: atomicState.evohomeHeaders
]
try {
httpGet(requestParams) { resp ->
if (resp.status == 200 && resp.data) {
if (atomicState.debug) log.debug "${app.label}: getEvohomeConfig(): Data: ${resp.data}"
atomicState.evohomeConfig = resp.data
atomicState.evohomeConfigUpdatedAt = now()
return null
}
else {
log.error "${app.label}: getEvohomeConfig(): No Data. Response Status: ${resp.status}"
return 'error'
}
}
} catch (groovyx.net.http.HttpResponseException e) {
log.error "${app.label}: getEvohomeConfig(): Error: e.statusCode ${e.statusCode}"
if (e.statusCode == 401) {
atomicState.evohomeAuthFailed = true
}
return e
}
}
/**
* getEvohomeStatus(onlyZoneId=-1)
*
* Gets Evohome Status for specified zone and stores in atomicState.evohomeStatus.
* If onlyZoneId is not specified, all zones are updated.
*
**/
private getEvohomeStatus(onlyZoneId=-1) {
if (atomicState.debug) log.debug "${app.label}: getEvohomeStatus(${onlyZoneId})"
def newEvohomeStatus = []
if (onlyZoneId == -1) { // Update all zones (which can be obtained en-masse for each location):
log.info "${app.label}: getEvohomeStatus(): Getting status for all zones."
atomicState.evohomeConfig.each { loc ->
def locStatus = getEvohomeLocationStatus(loc.locationInfo.locationId)
if (locStatus) {
newEvohomeStatus << locStatus
}
}
if (newEvohomeStatus) {
// Write out newEvohomeStatus back to atomicState:
atomicState.evohomeStatus = newEvohomeStatus
atomicState.evohomeStatusUpdatedAt = now()
}
}
else { // Only update the specified zone:
log.info "${app.label}: getEvohomeStatus(): Getting status for zone ID: ${onlyZoneId}"
def newZoneStatus = getEvohomeZoneStatus(onlyZoneId)
if (newZoneStatus) {
// Get existing evohomeStatus and update only the specified zone, preserving data for other zones:
// Have to do this as atomicState.evohomeStatus can only be written in its entirety (as using atomicstate).
// If mutiple zones are requesting updates at the same time this could cause loss of new data, but
// the worse case is having out-of-date data for a few minutes...
newEvohomeStatus = atomicState.evohomeStatus
newEvohomeStatus.each { loc ->
loc.gateways.each { gateway ->
gateway.temperatureControlSystems.each { tcs ->
tcs.zones.each { zone ->
if (onlyZoneId == zone.zoneId) { // This is the zone that must be updated:
zone.activeFaults = newZoneStatus.activeFaults
zone.heatSetpointStatus = newZoneStatus.heatSetpointStatus
zone.temperatureStatus = newZoneStatus.temperatureStatus
}
}
}
}
}
// Write out newEvohomeStatus back to atomicState:
atomicState.evohomeStatus = newEvohomeStatus
// Note: atomicState.evohomeStatusUpdatedAt is NOT updated.
}
}
}
/**
* getEvohomeLocationStatus(locationId)
*
* Gets the status for a specific location and returns data as a map.
*
* Called by getEvohomeStatus().
**/
private getEvohomeLocationStatus(locationId) {
if (atomicState.debug) log.debug "${app.label}: getEvohomeLocationStatus: Location ID: ${locationId}"
def requestParams = [
'method': 'GET',
'uri': atomicState.evohomeEndpoint,
'path': "/WebAPI/emea/api/v1/location/${locationId}/status",
'query': [ 'includeTemperatureControlSystems': 'True'],
'headers': atomicState.evohomeHeaders
]
try {
httpGet(requestParams) { resp ->
if(resp.status == 200 && resp.data) {
if (atomicState.debug) log.debug "${app.label}: getEvohomeLocationStatus: Data: ${resp.data}"
return resp.data
}
else {
log.error "${app.label}: getEvohomeLocationStatus: No Data. Response Status: ${resp.status}"
return false
}
}
} catch (groovyx.net.http.HttpResponseException e) {
log.error "${app.label}: getEvohomeLocationStatus: Error: e.statusCode ${e.statusCode}"
if (e.statusCode == 401) {
atomicState.evohomeAuthFailed = true
}
return false
}
}
/**
* getEvohomeZoneStatus(zoneId)
*
* Gets the status for a specific zone and returns data as a map.
*
**/
private getEvohomeZoneStatus(zoneId) {
if (atomicState.debug) log.debug "${app.label}: getEvohomeZoneStatus(${zoneId})"
def requestParams = [
'method': 'GET',
'uri': atomicState.evohomeEndpoint,
'path': "/WebAPI/emea/api/v1/temperatureZone/${zoneId}/status",
'headers': atomicState.evohomeHeaders
]
try {
httpGet(requestParams) { resp ->
if(resp.status == 200 && resp.data) {
if (atomicState.debug) log.debug "${app.label}: getEvohomeZoneStatus: Data: ${resp.data}"
return resp.data
}
else {
log.error "${app.label}: getEvohomeZoneStatus: No Data. Response Status: ${resp.status}"
return false
}
}
} catch (groovyx.net.http.HttpResponseException e) {
log.error "${app.label}: getEvohomeZoneStatus: Error: e.statusCode ${e.statusCode}"
if (e.statusCode == 401) {
atomicState.evohomeAuthFailed = true
}
return false
}
}
/**
* getEvohomeSchedules()
*
* Gets Evohome Schedule for each zone and stores in atomicState.evohomeSchedules.
*
**/
private getEvohomeSchedules() {
log.info "${app.label}: getEvohomeSchedules(): Getting schedules for all zones."
def evohomeSchedules = []
atomicState.evohomeConfig.each { loc ->
loc.gateways.each { gateway ->
gateway.temperatureControlSystems.each { tcs ->
tcs.zones.each { zone ->
def dni = generateDni(loc.locationInfo.locationId, gateway.gatewayInfo.gatewayId, tcs.systemId, zone.zoneId )
def schedule = getEvohomeZoneSchedule(zone.zoneId)
if (schedule) {
evohomeSchedules << ['zoneId': zone.zoneId, 'dni': dni, 'schedule': schedule]
}
}
}
}
}
if (evohomeSchedules) {
// Write out complete schedules to state:
atomicState.evohomeSchedules = evohomeSchedules
atomicState.evohomeSchedulesUpdatedAt = now()
}
return evohomeSchedules
}
/**
* getEvohomeZoneSchedule(zoneId)
*
* Gets the schedule for a specific zone and returns data as a map.
*
**/
private getEvohomeZoneSchedule(zoneId) {
if (atomicState.debug) log.debug "${app.label}: getEvohomeZoneSchedule(${zoneId})"
def requestParams = [
'method': 'GET',
'uri': atomicState.evohomeEndpoint,
'path': "/WebAPI/emea/api/v1/temperatureZone/${zoneId}/schedule",
'headers': atomicState.evohomeHeaders
]
try {
httpGet(requestParams) { resp ->
if(resp.status == 200 && resp.data) {
if (atomicState.debug) log.debug "${app.label}: getEvohomeZoneSchedule: Data: ${resp.data}"
return resp.data
}
else {
log.error "${app.label}: getEvohomeZoneSchedule: No Data. Response Status: ${resp.status}"
return false
}
}
} catch (groovyx.net.http.HttpResponseException e) {
log.error "${app.label}: getEvohomeZoneSchedule: Error: e.statusCode ${e.statusCode}"
if (e.statusCode == 401) {
atomicState.evohomeAuthFailed = true
}
return false
}
}
/**
* setThermostatMode(systemId, mode, until)
*
* Set thermostat mode for specified controller, until specified time.
*
* systemId: SystemId of temperatureControlSystem. E.g.: 123456
*
* mode: String. Either: "auto", "off", "economy", "away", "dayOff", "custom".
*
* until: (Optional) Time to apply mode until, can be either:
* - Date: date object representing when override should end.
* - ISO-8601 date string, in format "yyyy-MM-dd'T'HH:mm:ssXX", e.g.: "2016-04-01T00:00:00Z".
* - String: 'permanent'.
* - Number: Duration in hours if mode is 'economy', or in days if mode is 'away'/'dayOff'/'custom'.
* Duration will be rounded down to align with Midnight in the local timezone
* (e.g. a duration of 1 day will end at midnight tonight). If 0, mode is permanent.
* If 'until' is not specified, a default value is used from the SmartApp settings.
*
* Notes: 'Auto' and 'Off' modes are always permanent.
* Thermostat mode is a property of the temperatureControlSystem (i.e. Evohome controller).
* Therefore changing the thermostatMode will affect all zones associated with the same controller.
*
*
* Example usage:
* setThermostatMode(123456, 'auto') // Set auto mode permanently, for controller 123456.
* setThermostatMode(123456, 'away','2016-04-01T00:00:00Z') // Set away mode until 1st April, for controller 123456.
* setThermostatMode(123456, 'dayOff','permanent') // Set dayOff mode permanently, for controller 123456.
* setThermostatMode(123456, 'dayOff', 2) // Set dayOff mode for 2 days (ends tomorrow night), for controller 123456.
* setThermostatMode(123456, 'economy', 2) // Set economy mode for 2 hours, for controller 123456.
*
**/
def setThermostatMode(systemId, mode, until=-1) {/******De donde se manda llamar********/
if (atomicState.debug) log.debug "${app.label}: setThermostatMode(): SystemID: ${systemId}, Mode: ${mode}, Until: ${until}"
// Clean mode (translate to index):
mode = mode.toLowerCase()
int modeIndex
switch (mode) {
case 'auto':
modeIndex = 0
break
case 'off':
modeIndex = 1
break
case 'economy':
modeIndex = 2
break
case 'away':
modeIndex = 3
break
case 'dayoff':
modeIndex = 4
break
case 'custom':
modeIndex = 6
break
default:
log.error "${app.label}: setThermostatMode(): Mode: ${mode} is not supported!"
modeIndex = 999
break
}
// Clean until:
def untilRes
// until has not been specified, so determine behaviour from settings:
if (-1 == until && 'economy' == mode) {
until = atomicState.thermostatEconomyDuration ?: 0 // Use Default duration for economy mode (hours):
}
else if (-1 == until && ( 'away' == mode ||'dayoff' == mode ||'custom' == mode )) {
until = atomicState.thermostatModeDuration ?: 0 // Use Default duration for other modes (days):
}
// Convert to date (or 0):
if ('permanent' == until || 0 == until || -1 == until) {
untilRes = 0
}
else if (until instanceof Date) {
untilRes = until.format("yyyy-MM-dd'T'HH:mm:00XX", TimeZone.getTimeZone('UTC')) // Round to nearest minute.
}
else if (until ==~ /\d+.*T.*/) { // until is a ISO-8601 date string already, but we'll re-format it anyway to ensure it's in UTC:
untilRes = new Date().parse("yyyy-MM-dd'T'HH:mm:ssXX", until).format("yyyy-MM-dd'T'HH:mm:00XX", TimeZone.getTimeZone('UTC')) // Round to nearest minute.
}
else if (until.isNumber() && 'economy' == mode) { // until is a duration in hours:
untilRes = new Date( now() + (Math.round(until) * 3600000) ).format("yyyy-MM-dd'T'HH:mm:00XX", TimeZone.getTimeZone('UTC')) // Round to nearest minute.
}
else if (until.isNumber() && ('away' == mode ||'dayoff' == mode ||'custom' == mode )) { // until is a duration in days:
untilRes = new Date( now() + (Math.round(until) * 86400000) ).format("yyyy-MM-dd'T'00:00:00XX", location.timeZone) // Round down to midnight in the LOCAL timezone.
}
else {
log.warn "${device.label}: setThermostatMode(): until value could not be parsed. Mode will be applied permanently."
untilRes = 0
}
// If mode is away/dayOff/custom the date needs to be rounded down to midnight in the local timezone, then converted back to string again:
if (0 != untilRes && ('away' == mode ||'dayoff' == mode ||'custom' == mode )) {
untilRes = new Date().parse("yyyy-MM-dd'T'HH:mm:ssXX", new Date().parse("yyyy-MM-dd'T'HH:mm:ssXX", untilRes).format("yyyy-MM-dd'T'00:00:00XX", location.timeZone) ).format("yyyy-MM-dd'T'HH:mm:00XX", TimeZone.getTimeZone('UTC'))
}
// Build request:
def body
if (0 == untilRes || 'off' == mode || 'auto' == mode) { // Mode is permanent:
body = ['SystemMode': modeIndex, 'TimeUntil': null, 'Permanent': 'True']
log.info "${app.label}: setThermostatMode(): System ID: ${systemId}, Mode: ${mode}, Permanent: True"
}
else { // Mode is temporary:
body = ['SystemMode': modeIndex, 'TimeUntil': untilRes, 'Permanent': 'False']
log.info "${app.label}: setThermostatMode(): System ID: ${systemId}, Mode: ${mode}, Permanent: False, Until: ${untilRes}"
}
def requestParams = [
'uri': atomicState.evohomeEndpoint,
'path': "/WebAPI/emea/api/v1/temperatureControlSystem/${systemId}/mode",
'body': body,
'headers': atomicState.evohomeHeaders
]
// Make request:
try {
httpPutJson(requestParams) { resp ->
if(resp.status == 201 && resp.data) {
if (atomicState.debug) log.debug "${app.label}: setThermostatMode(): Response: ${resp.data}"
return null
}
else {
log.error "${app.label}: setThermostatMode(): No Data. Response Status: ${resp.status}"
return 'error'
}
}
} catch (groovyx.net.http.HttpResponseException e) {
log.error "${app.label}: setThermostatMode(): Error: ${e}"
if (e.statusCode == 401) {
atomicState.evohomeAuthFailed = true
}
return e
}
}
/**
* setHeatingSetpoint(zoneId, setpoint, until=-1)
*
* Set heatingSetpoint for specified zoneId, until specified time.
*
* zoneId: Zone ID of zone, e.g.: "123456"
*
* setpoint: Setpoint temperature, e.g.: "21.5". Can be a number or string.
*
* until: (Optional) Time to apply setpoint until, can be either:
* - Date: date object representing when override should end.
* - ISO-8601 date string, in format "yyyy-MM-dd'T'HH:mm:ssXX", e.g.: "2016-04-01T00:00:00Z".
* - String: 'permanent'.
* If not specified, setpoint will be applied permanently.
*
* Example usage:
* setHeatingSetpoint(123456, 21.0) // Set temp of 21.0 permanently, for zone 123456.
* setHeatingSetpoint(123456, 21.0, 'permanent') // Set temp of 21.0 permanently, for zone 123456.
* setHeatingSetpoint(123456, 21.0, '2016-04-01T00:00:00Z') // Set until specific time, for zone 123456.
*
**/
def setHeatingSetpoint(zoneId, setpoint, until=-1) {
if (atomicState.debug) log.debug "${app.label}: setHeatingSetpoint(): Zone ID: ${zoneId}, Setpoint: ${setpoint}, Until: ${until}"
// Clean setpoint:
setpoint = formatTemperature(setpoint)
// Clean until:
def untilRes
if ('permanent' == until || 0 == until || -1 == until) {
untilRes = 0
}
else if (until instanceof Date) {
untilRes = until.format("yyyy-MM-dd'T'HH:mm:00XX", TimeZone.getTimeZone('UTC')) // Round to nearest minute.
}
else if (until ==~ /\d+.*T.*/) { // until is a ISO-8601 date string already, but we'll re-format it anyway to ensure it's in UTC:
untilRes = new Date().parse("yyyy-MM-dd'T'HH:mm:ssXX", until).format("yyyy-MM-dd'T'HH:mm:00XX", TimeZone.getTimeZone('UTC')) // Round to nearest minute.
}
else {
log.warn "${device.label}: setHeatingSetpoint(): until value could not be parsed. Setpoint will be applied permanently."
untilRes = 0
}
// Build request:
def body
if (0 == untilRes) { // Permanent:
body = ['HeatSetpointValue': setpoint, 'SetpointMode': 1, 'TimeUntil': null]
log.info "${app.label}: setHeatingSetpoint(): Zone ID: ${zoneId}, Setpoint: ${setpoint}, Until: Permanent"
}
else { // Temporary:
body = ['HeatSetpointValue': setpoint, 'SetpointMode': 2, 'TimeUntil': untilRes]
log.info "${app.label}: setHeatingSetpoint(): Zone ID: ${zoneId}, Setpoint: ${setpoint}, Until: ${untilRes}"
}
def requestParams = [
'uri': atomicState.evohomeEndpoint,
'path': "/WebAPI/emea/api/v1/temperatureZone/${zoneId}/heatSetpoint",
'body': body,
'headers': atomicState.evohomeHeaders
]
// Make request:
try {
httpPutJson(requestParams) { resp ->
if(resp.status == 201 && resp.data) {
if (atomicState.debug) log.debug "${app.label}: setHeatingSetpoint(): Response: ${resp.data}"
return null
}
else {
log.error "${app.label}: setHeatingSetpoint(): No Data. Response Status: ${resp.status}"
return 'error'
}
}
} catch (groovyx.net.http.HttpResponseException e) {
log.error "${app.label}: setHeatingSetpoint(): Error: ${e}"
if (e.statusCode == 401) {
atomicState.evohomeAuthFailed = true
}
return e
}
}
/**
* clearHeatingSetpoint(zoneId)
*
* Clear the heatingSetpoint for specified zoneId.
* zoneId: Zone ID of zone, e.g.: "123456"
**/
def clearHeatingSetpoint(zoneId) {
log.info "${app.label}: clearHeatingSetpoint(): Zone ID: ${zoneId}"
// Build request:
def requestParams = [
'uri': atomicState.evohomeEndpoint,
'path': "/WebAPI/emea/api/v1/temperatureZone/${zoneId}/heatSetpoint",
'body': ['HeatSetpointValue': 0.0, 'SetpointMode': 0, 'TimeUntil': null],
'headers': atomicState.evohomeHeaders
]
// Make request:
try {
httpPutJson(requestParams) { resp ->
if(resp.status == 201 && resp.data) {
if (atomicState.debug) log.debug "${app.label}: clearHeatingSetpoint(): Response: ${resp.data}"
return null
}
else {
log.error "${app.label}: clearHeatingSetpoint(): No Data. Response Status: ${resp.status}"
return 'error'
}
}
} catch (groovyx.net.http.HttpResponseException e) {
log.error "${app.label}: clearHeatingSetpoint(): Error: ${e}"
if (e.statusCode == 401) {
atomicState.evohomeAuthFailed = true
}
return e
}
}
/**********************************************************************
* Helper Commands:
**********************************************************************/
/**
* generateDni(locId,gatewayId,systemId,deviceId)
*
* Generate a device Network ID.
* Uses the same format as the official Evohome App, but with a prefix of "Evohome."
**/
private generateDni(locId,gatewayId,systemId,deviceId) {
log.debug 'generateDNI Evohome.' + [ locId, gatewayId, systemId, deviceId ].join('.')
return 'Evohome.' + [ locId, gatewayId, systemId, deviceId ].join('.')
}
/**
* formatTemperature(t)
*
* Format temperature value to one decimal place.
* t: can be string, float, bigdecimal...
* Returns as string.
**/
private formatTemperature(t) {
return Float.parseFloat("${t}").round(1).toString()
}
/**
* formatSetpointMode(mode)
*
* Format Evohome setpointMode values to SmartThings values:
*
**/
private formatSetpointMode(mode) {
switch (mode) {
case 'FollowSchedule':
mode = 'followSchedule'
break
case 'PermanentOverride':
mode = 'permanentOverride'
break
case 'TemporaryOverride':
mode = 'temporaryOverride'
break
default:
log.error "${app.label}: formatSetpointMode(): Mode: ${mode} unknown!"
mode = mode.toLowerCase()
break
}
return mode
}
/**
* formatThermostatMode(mode)
*
* Translate Evohome thermostatMode values to SmartThings values.
*
**/
private formatThermostatMode(mode) {
switch (mode) {
case 'Auto':
mode = 'auto'
break
case 'AutoWithEco':
mode = 'eco'
break
case 'Away':
mode = 'away'
break
case 'Custom':
mode = 'custom'
break
case 'DayOff':
mode = 'dayOff'
break
case 'HeatingOff':
mode = 'off'
break
default:
log.error "${app.label}: formatThermostatMode(): Mode: ${mode} unknown!"
mode = mode.toLowerCase()
break
}
return mode
}
/**
* getCurrentSwitchpoint(schedule)
*
* Returns the current active switchpoint in the given schedule.
* e.g. [timeOfDay:"23:00:00", temperature:"15.0000"]
*
**/
private getCurrentSwitchpoint(schedule) {
if (atomicState.debug) log.debug "${app.label}: getCurrentSwitchpoint()"
Calendar c = new GregorianCalendar()
def ScheduleToday = schedule.dailySchedules.find { it.dayOfWeek = c.getTime().format("EEEE", location.timeZone) }
// Sort and find next switchpoint:
ScheduleToday.switchpoints.sort {it.timeOfDay}
ScheduleToday.switchpoints.reverse(true)
def currentSwitchPoint = ScheduleToday.switchpoints.find {it.timeOfDay < c.getTime().format("HH:mm:ss", location.timeZone)}
if (!currentSwitchPoint) {
// There are no current switchpoints today, so we must look for the last Switchpoint yesterday.
if (atomicState.debug) log.debug "${app.label}: getCurrentSwitchpoint(): No current switchpoints today, so must look to yesterday's schedule."
c.add(Calendar.DATE, -1 ) // Subtract one DAY.
def ScheduleYesterday = schedule.dailySchedules.find { it.dayOfWeek = c.getTime().format("EEEE", location.timeZone) }
ScheduleYesterday.switchpoints.sort {it.timeOfDay}
ScheduleYesterday.switchpoints.reverse(true)
currentSwitchPoint = ScheduleYesterday.switchpoints[0] // There will always be one.
}
// Now construct the switchpoint time as a full ISO-8601 format date string in UTC:
def localDateStr = c.getTime().format("yyyy-MM-dd'T'", location.timeZone) + currentSwitchPoint.timeOfDay + c.getTime().format("XX", location.timeZone) // Switchpoint in local timezone.
def isoDateStr = new Date().parse("yyyy-MM-dd'T'HH:mm:ssXX", localDateStr).format("yyyy-MM-dd'T'HH:mm:ssXX", TimeZone.getTimeZone('UTC')) // Parse and re-format to UTC timezone.
currentSwitchPoint << [ 'time': isoDateStr ]
if (atomicState.debug) log.debug "${app.label}: getCurrentSwitchpoint(): Current Switchpoint: ${currentSwitchPoint}"
return currentSwitchPoint
}
/**
* getNextSwitchpoint(schedule)
*
* Returns the next switchpoint in the given schedule.
* e.g. [timeOfDay:"23:00:00", temperature:"15.0000"]
*
**/
private getNextSwitchpoint(schedule) {
if (atomicState.debug) log.debug "${app.label}: getNextSwitchpoint()"
Calendar c = new GregorianCalendar()
def ScheduleToday = schedule.dailySchedules.find { it.dayOfWeek = c.getTime().format("EEEE", location.timeZone) }
// Sort and find next switchpoint:
ScheduleToday.switchpoints.sort {it.timeOfDay}
def nextSwitchPoint = ScheduleToday.switchpoints.find {it.timeOfDay > c.getTime().format("HH:mm:ss", location.timeZone)}
if (!nextSwitchPoint) {
// There are no switchpoints left today, so we must look for the first Switchpoint tomorrow.
if (atomicState.debug) log.debug "${app.label}: getNextSwitchpoint(): No more switchpoints today, so must look to tomorrow's schedule."
c.add(Calendar.DATE, 1 ) // Add one DAY.
def ScheduleTmrw = schedule.dailySchedules.find { it.dayOfWeek = c.getTime().format("EEEE", location.timeZone) }
ScheduleTmrw.switchpoints.sort {it.timeOfDay}
nextSwitchPoint = ScheduleTmrw.switchpoints[0] // There will always be one.
}
// Now construct the switchpoint time as a full ISO-8601 format date string in UTC:
def localDateStr = c.getTime().format("yyyy-MM-dd'T'", location.timeZone) + nextSwitchPoint.timeOfDay + c.getTime().format("XX", location.timeZone) // Switchpoint in local timezone.
def isoDateStr = new Date().parse("yyyy-MM-dd'T'HH:mm:ssXX", localDateStr).format("yyyy-MM-dd'T'HH:mm:ssXX", TimeZone.getTimeZone('UTC')) // Parse and re-format to UTC timezone.
nextSwitchPoint << [ 'time': isoDateStr ]
if (atomicState.debug) log.debug "${app.label}: getNextSwitchpoint(): Next Switchpoint: ${nextSwitchPoint}"
return nextSwitchPoint
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment