Converting code from ST

Firstly, I have no real clue what I'm doing here but I'm trying to convert the app I used to use for my EvoHome central heating system on smartthings to work on hubitat. I've been stumbling from error to error and have managed to solve a few and I think I'm close, but its throwing this error for each zone. I think its when it tries to create the child devices for each heating zone.

[app:1252](http://192.168.2.xxx/logs#app1252)2020-09-17 10:33:46.394 [error](http://192.168.2.xxx/installedapp/configure/1252)Evohome (Connect): updateChildDeviceConfig(): Error creating device: Name: Bed 4 Heating Zone (Evohome), DNI: Evohome.XXXXXXX.XXXXXXX.XXXXXXX.XXXXXXX, Error: com.hubitat.app.exception.UnknownDeviceTypeException: Device type 'Evohome Heating Zone' in namespace 'null' not found

[app:1252](http://192.168.2.xxx/logs#app1252)2020-09-17 10:33:46.388 [info](http://192.168.2.xxx/installedapp/configure/1252)Evohome (Connect): updateChildDeviceConfig(): Creating device: Name: Bed 4 Heating Zone (Evohome), DNI: Evohome.XXXXXXX.XXXXXXX.XXXXXXX.XXXXXXX

this is the code from the app

> /**
>  *  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}"
> 
> }
> 
> 
> /**
>  *  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.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(app.namespace, "Evohome Heating Zone", 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)
> 					}
> 				}
> 			}
> 		}
> 	}
> 	
> 	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
> }

seems to me like its looking for the device driver but can't find it. I have installed the code and the device driver is there when I create a virtual device.

Grateful for any help on this one. it was one of the last things I had on ST which broke when they "upgraded" me to the new app.
cheers
Edd

you need to update the addChildDevice() function. get rid of the null parameter, I use the following as a base in some custom device drivers and shows what HE is looking for:

	def options = [
		completedSetup: true, 
		label: "${device.label} - Child Device",
		isComponent: false
	]

	addChildDevice("namespace", "Child Device Driver", ChildDeviceNetworkId, options)

if you havnt found this thread yet, it is super useful for moving drivers over to HE:

Jackpot! Thanks for that I've managed to sort it out. one more string to ST cut!

so now i get the below error every 5 minutes

> [app:1252](http://192.168.2.xxx/logs#app1252)2020-09-17 13:40:52.340 [error](http://192.168.2.xxx/installedapp/configure/1252)groovy.lang.MissingMethodException: No signature of method: java.lang.Long.isNumber() is applicable for argument types: () values: [] on line 248 (manageAuth)
> 
> [app:1252](http://192.168.2.xxx/logs#app1252)2020-09-17 13:39:48.302 [info](http://192.168.2.xxx/installedapp/configure/1252)Evohome (Connect): getEvohomeStatus(): Getting status for all zones.

not a massive issue but annoying none the less

It's complaining because isNumber() only exists for String types.

I think you could fix this by making this change:

else if (!atomicState.evohomeAuth.expiresAt.isNumber() || now() >= atomicState.evohomeAuth.expiresAt)

to

else if (!atomicState.evohomeAuth.expiresAt.toString().isNumber() || now() >= atomicState.evohomeAuth.expiresAt)

great, thanks for this its stopped the error

1 Like

Hi @ApriliaEdd Can you share your converted code? I'm new here and I can't do it myself
I would be grateful

Hi @krzjasin welcome, I assume you're after the evohome stuff, its in this thread

code is here

before you start make a backup of your config just in case :slight_smile:

add the driver code then add the app code go to apps add user app and select evohome connect.

once installed open the app and enter your username and password for the total connect comfort website. click done and it should add the rooms to your devices the lads in the thread are pretty helpful if you get stuck. i'm not at home right now i'm afraid

goodluck

Thanks for the response.
I've read the threads you submitted and tried this many times

but in my device list is no thermostats
This is in my log

app:1422021-03-25 09:13:25.217 errorjava.lang.NullPointerException: Cannot get property 'userId' on null object on line 684 (updated)

app:1422021-03-25 09:13:25.197 infoEvohome (Connect): getEvohomeConfig(): Getting configuration for all locations.

app:1422021-03-25 09:13:25.170 debugEvohome (Connect): manageSchedules(): Re-scheduling poll()

app:1422021-03-25 09:13:25.114 debugEvohome (Connect): manageSchedules(): Re-scheduling manageAuth()

app:1422021-03-25 09:13:25.111 debugEvohome (Connect): manageSchedules()

app:1422021-03-25 09:13:25.089 debugEvohome (Connect): manageSubscriptions()

app:1422021-03-25 09:13:25.077 errorEvohome (Connect): getEvohomeUserAccount(): Error: e.statusCode 400

app:1422021-03-25 09:13:24.259 infoEvohome (Connect): getEvohomeUserAccount(): Getting user account information

Am I doing something wrong?

You need to enable oauth in the app code page, I think.... I always forget about that step

I have oauth enabled
I refreshed token one more time and the same :frowning:

Password and username are correct?

You can log in here right?
https://international.mytotalconnectcomfort.com/Account/Login?ReturnUrl=%2FLocations

Yes. Password and username are correct
I have checked it many times
I can log into the website and everything works fine there.
I have no idea what's going on.

I'm afraid I've expended my fault finding abilities on this one. All I can suggest is maybe remove the app code and driver code and try again.
Clicking "done" in the evohome connect app page where you enter your username and password should trigger the app to create the individual devices for each zone in your evohome system. After any changes click this done button to refresh the settings.

Like I said I'm away from home and have no access to my hubs for the next 4 weeks maybe try posting in the main thread and maybe someone there can guide you through with screenshots etc.

Thanks @ApriliaEdd for your help and for taking your time. As you said, I will try posting in the main thread

1 Like