Suppress logging in unmaintained EvoHome community app & driver

I have a community app and driver for my Honeywell EvoHome heating system that is not maintained. This is the EU/UK version of Total Connect Comfort. Someone ported them over from SmartThings and it works great/has never thrown an error. My issue is the logging for both the app and the driver. There is no way in the preferences to disable it and so I get two lines in the logs every minute for the app and two lines every minute for the driver (per heating zone). I have 8 zones so my logs are spammed with 18 lines per minute. Everything runs well but it means when I have an issue with another app I don't get much history as this app and driver are filling it.

I'm wondering if whether anyone can advise by perusing the output I get and the code, if there's something I can leave there but comment out in some way to stop the logs (if I had an issue I could just restore the code to diagnose)

This is the output I get from the driver once a minute for every zone that uses it:

and this is the driver code:

Summary
/**
 *  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.
 *
 */ 
metadata {
	definition (name: "Evohome Heating Zone", namespace: "codersaur", author: "David Lomas") {
		capability "Actuator"
		capability "Sensor"
		capability "Refresh"
		capability "Temperature Measurement"
		// capability "Thermostat"
		capability "Thermostat Heating Setpoint"
		capability "Thermostat Setpoint"
		capability "Thermostat Mode"
		capability "Thermostat Operating State"
		
		//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}"

	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)

}


/**********************************************************************
 *  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) {
					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)
	def windowTemp = new BigDecimal(state.windowFunctionTemperature ?: formatTemperature(5.0))
    
	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)
}

Around line 353 I tried changing 'Displayed: true' to 'Displayed: false' but it made no difference. I'm not sure whether that should have worked. Maybe I'd need to delete the heating zones and re run from scratch to get the change to occur. Any ideas appreciated - I'm clueless with code.

Looks like you can comment out line 331.

	//log.info "${device.label}: generateEvent(): New values: ${values}"
2 Likes

Add a "//" to the beginning of log lines you don't want to execute. This is the event that was causing you grief.

// log.info "${device.label}: generateEvent(): New values: ${values}"

1 Like

@FriedCheese2006 @coreystup thanks for your input. I'll give that a try and report back.

1 Like

# is not a valid line comment character in Groovy. You'll need //. Or surround in /* */

1 Like

Yep. Brain fart.

1 Like

It worked, thanks. I'm going to have a quick look at the corresponding parent app, as I might need to do the same there in several places. That one is generating about 30 log entries per hour.

The "correct" thing would be to add parameters to the driver that allows for logging to be enabled/disabled, similar to how most Hubitat drivers work. But for your one off needs, just commenting them out is fine.

I was just thinking the same. As I've mentioned I'm clueless but I might have a look at another installed community app that has selectable logging and see if I can understand how it's done. Looking at the 'EvoHome (Connect)' app now I can see that all events that will be displayed are referenced 'log.info', 'log.debug', 'log.error' etc according to their level. I assume code would be added for a preference box that references those log levels.

I do wish I'd learned to code when I was younger as I find it mind-blowing what's possible

log.*** will automatically log the string after. What most do is create a function for the logging that's based on a preference. So, you could do give a toggle for debug logging in the preferences:

input "debugOutput", "bool", title: "Enable debug logging?", defaultValue: true, displayDuringSetup: false, required: false

Then you would have a function like this:

def logDebug(msg) {
    if (settings?.debugOutput) {
		log.debug msg
	}
}

Then, in the rest of the code, instead of saying

log.debug "Logging stuff"

you would put

logDebug "Logging stuff"

So, what happens is if debug is enabled in the preferences, then when "logDebug" is called, the string will get logged. If it is turned off then "if (settings?.debugOutput)" evaluates as false and nothing is logged.

I used debugging as an example, but the approach is applicable to any level of logging. Interestingly, this approach is semi-applied in the device code you posted above, but the preference is just hard coded for debugging at 283.

If you decide to make the change...ctrl+h is your friend (find and replace).

EDIT:
For bonus points, you can add another function to turn off debugging (or whatever preference you want for that matter):

def logsOff(){
    log.warn "debug logging disabled..."
    app.updateSetting("debugOutput",[value:"false",type:"bool"])
}

And add this to the installed() function. This will turn off the logs after 1800 seconds (30 minutes) if they are turned on.

runIn(1800,logsOff)
2 Likes

Thanks I'll try to get my head around that as even if I don't apply it now, I may come back to it.

In the parent app (which is posted below) there is already a preference for debug logging. It looks as though every reference to logging a debug message starts with:

if (atomicState.debug) log.debug

So for ease could I just replace:

log.info

with that 'if (atomicState.debug) log.debug' ? I think that would effectively just merge debug logging with info logging so both are on or off which is fine by me.

Here is the app code:

Summary

/**

  • 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.
  • 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).
    • 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}"

}

/**

  • 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/WebApi'
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()
updateChildDeviceConfig()

// 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.toString().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()"

// 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("codersaur", "Evohome Heating Zone", dni, values)
					} catch (e) {
						log.error "${app.label}: updateChildDeviceConfig(): Error creating device: Name: ${values.label}, DNI: ${dni}, Error: ${e}"
					}
				} 
				
				if(d) {
					d.generateEvent(values)
				}
			}
		}
	}
}

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})"

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)
					} 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/WebApi',
	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')
				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/WebApi',
	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) {

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) {
    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 = 'economy'
		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

}

Just a quick skim through and yeah, looks like that would work.

1 Like

Great thanks. I'll paste the original somewhere and set about making the changes. That'll do for the app. I might have a play with adding something to the driver afterwards but to be honest for the sake of commenting/uncommenting one line in the code it's maybe not worth it (though I might start to learn a bit)

1 Like

Download the Hubitat app