]> git.basschouten.com Git - openhab-addons.git/blob
1e2b2d17f0895026e09e2b607ae99b7f9f39c566
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2020 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
7  * This program and the accompanying materials are made available under the
8  * terms of the Eclipse Public License 2.0 which is available at
9  * http://www.eclipse.org/legal/epl-2.0
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.ecobee.internal.handler;
14
15 import static org.openhab.binding.ecobee.internal.EcobeeBindingConstants.*;
16
17 import java.lang.reflect.Field;
18 import java.util.Collection;
19 import java.util.Collections;
20 import java.util.HashMap;
21 import java.util.List;
22 import java.util.Map;
23 import java.util.concurrent.ConcurrentHashMap;
24 import java.util.concurrent.CopyOnWriteArrayList;
25 import java.util.concurrent.Future;
26 import java.util.concurrent.TimeUnit;
27 import java.util.stream.Collectors;
28 import java.util.stream.Stream;
29
30 import javax.measure.Unit;
31
32 import org.apache.commons.lang.WordUtils;
33 import org.eclipse.jdt.annotation.NonNullByDefault;
34 import org.eclipse.jdt.annotation.Nullable;
35 import org.openhab.binding.ecobee.action.EcobeeActions;
36 import org.openhab.binding.ecobee.internal.api.EcobeeApi;
37 import org.openhab.binding.ecobee.internal.config.EcobeeThermostatConfiguration;
38 import org.openhab.binding.ecobee.internal.discovery.SensorDiscoveryService;
39 import org.openhab.binding.ecobee.internal.dto.SelectionDTO;
40 import org.openhab.binding.ecobee.internal.dto.thermostat.AlertDTO;
41 import org.openhab.binding.ecobee.internal.dto.thermostat.ClimateDTO;
42 import org.openhab.binding.ecobee.internal.dto.thermostat.EventDTO;
43 import org.openhab.binding.ecobee.internal.dto.thermostat.HouseDetailsDTO;
44 import org.openhab.binding.ecobee.internal.dto.thermostat.LocationDTO;
45 import org.openhab.binding.ecobee.internal.dto.thermostat.ManagementDTO;
46 import org.openhab.binding.ecobee.internal.dto.thermostat.ProgramDTO;
47 import org.openhab.binding.ecobee.internal.dto.thermostat.RemoteSensorDTO;
48 import org.openhab.binding.ecobee.internal.dto.thermostat.RuntimeDTO;
49 import org.openhab.binding.ecobee.internal.dto.thermostat.SettingsDTO;
50 import org.openhab.binding.ecobee.internal.dto.thermostat.TechnicianDTO;
51 import org.openhab.binding.ecobee.internal.dto.thermostat.ThermostatDTO;
52 import org.openhab.binding.ecobee.internal.dto.thermostat.ThermostatUpdateRequestDTO;
53 import org.openhab.binding.ecobee.internal.dto.thermostat.VersionDTO;
54 import org.openhab.binding.ecobee.internal.dto.thermostat.WeatherDTO;
55 import org.openhab.binding.ecobee.internal.dto.thermostat.WeatherForecastDTO;
56 import org.openhab.binding.ecobee.internal.function.AbstractFunction;
57 import org.openhab.binding.ecobee.internal.function.FunctionRequest;
58 import org.openhab.core.i18n.TimeZoneProvider;
59 import org.openhab.core.library.types.DecimalType;
60 import org.openhab.core.library.types.OnOffType;
61 import org.openhab.core.library.types.QuantityType;
62 import org.openhab.core.library.types.StringType;
63 import org.openhab.core.library.unit.ImperialUnits;
64 import org.openhab.core.library.unit.SIUnits;
65 import org.openhab.core.library.unit.SmartHomeUnits;
66 import org.openhab.core.thing.Bridge;
67 import org.openhab.core.thing.Channel;
68 import org.openhab.core.thing.ChannelUID;
69 import org.openhab.core.thing.Thing;
70 import org.openhab.core.thing.ThingStatus;
71 import org.openhab.core.thing.ThingStatusDetail;
72 import org.openhab.core.thing.ThingStatusInfo;
73 import org.openhab.core.thing.binding.BaseBridgeHandler;
74 import org.openhab.core.thing.binding.ThingHandler;
75 import org.openhab.core.thing.binding.ThingHandlerService;
76 import org.openhab.core.thing.type.ChannelType;
77 import org.openhab.core.thing.type.ChannelTypeRegistry;
78 import org.openhab.core.thing.type.ChannelTypeUID;
79 import org.openhab.core.types.Command;
80 import org.openhab.core.types.RefreshType;
81 import org.openhab.core.types.State;
82 import org.slf4j.Logger;
83 import org.slf4j.LoggerFactory;
84
85 /**
86  * The {@link EcobeeThermostatBridgeHandler} is the handler for an Ecobee thermostat.
87  *
88  * @author Mark Hilbush - Initial contribution
89  */
90 @NonNullByDefault
91 public class EcobeeThermostatBridgeHandler extends BaseBridgeHandler {
92
93     private static final int SENSOR_DISCOVERY_STARTUP_DELAY_SECONDS = 30;
94     private static final int SENSOR_DISCOVERY_INTERVAL_SECONDS = 300;
95
96     private final Logger logger = LoggerFactory.getLogger(EcobeeThermostatBridgeHandler.class);
97
98     private TimeZoneProvider timeZoneProvider;
99     private ChannelTypeRegistry channelTypeRegistry;
100
101     private @NonNullByDefault({}) String thermostatId;
102
103     private final Map<String, EcobeeSensorThingHandler> sensorHandlers = new ConcurrentHashMap<>();
104
105     private @Nullable Future<?> discoverSensorsJob;
106     private @Nullable SensorDiscoveryService discoveryService;
107
108     private @Nullable ThermostatDTO savedThermostat;
109     private @Nullable List<RemoteSensorDTO> savedSensors;
110     private List<String> validClimateRefs = new CopyOnWriteArrayList<>();
111     private Map<String, State> stateCache = new ConcurrentHashMap<>();
112     private Map<ChannelUID, Boolean> channelReadOnlyMap = new HashMap<>();
113     private Map<Integer, String> symbolMap = new HashMap<>();
114     private Map<Integer, String> skyMap = new HashMap<>();
115
116     public EcobeeThermostatBridgeHandler(Bridge bridge, TimeZoneProvider timeZoneProvider,
117             ChannelTypeRegistry channelTypeRegistry) {
118         super(bridge);
119         this.timeZoneProvider = timeZoneProvider;
120         this.channelTypeRegistry = channelTypeRegistry;
121     }
122
123     @Override
124     public void initialize() {
125         thermostatId = getConfigAs(EcobeeThermostatConfiguration.class).thermostatId;
126         logger.debug("ThermostatBridge: Initializing thermostat '{}'", thermostatId);
127         initializeWeatherMaps();
128         initializeReadOnlyChannels();
129         clearSavedState();
130         scheduleDiscoveryJob();
131         updateStatus(EcobeeUtils.isBridgeOnline(getBridge()) ? ThingStatus.ONLINE : ThingStatus.OFFLINE);
132     }
133
134     @Override
135     public void dispose() {
136         cancelDiscoveryJob();
137         logger.debug("ThermostatBridge: Disposing thermostat '{}'", thermostatId);
138     }
139
140     @Override
141     public void childHandlerInitialized(ThingHandler sensorHandler, Thing sensorThing) {
142         String sensorId = (String) sensorThing.getConfiguration().get(CONFIG_SENSOR_ID);
143         sensorHandlers.put(sensorId, (EcobeeSensorThingHandler) sensorHandler);
144         logger.debug("ThermostatBridge: Saving sensor handler for {} with id {}", sensorThing.getUID(), sensorId);
145     }
146
147     @Override
148     public void childHandlerDisposed(ThingHandler sensorHandler, Thing sensorThing) {
149         String sensorId = (String) sensorThing.getConfiguration().get(CONFIG_SENSOR_ID);
150         sensorHandlers.remove(sensorId);
151         logger.debug("ThermostatBridge: Removing sensor handler for {} with id {}", sensorThing.getUID(), sensorId);
152     }
153
154     @Override
155     public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
156         if (bridgeStatusInfo.getStatus() == ThingStatus.ONLINE) {
157             updateStatus(ThingStatus.ONLINE);
158         } else {
159             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
160         }
161     }
162
163     @SuppressWarnings("null")
164     @Override
165     public void handleCommand(ChannelUID channelUID, Command command) {
166         if (command instanceof RefreshType) {
167             State state = stateCache.get(channelUID.getId());
168             if (state != null) {
169                 updateState(channelUID.getId(), state);
170             }
171             return;
172         }
173         if (isChannelReadOnly(channelUID)) {
174             logger.debug("Can't apply command '{}' to '{}' because channel is readonly", command, channelUID.getId());
175             return;
176         }
177         scheduler.execute(() -> {
178             handleThermostatCommand(channelUID, command);
179         });
180     }
181
182     public void setDiscoveryService(SensorDiscoveryService discoveryService) {
183         this.discoveryService = discoveryService;
184     }
185
186     /**
187      * Called by the AccountBridgeHandler to create a Selection that
188      * includes only the Ecobee objects for which there's at least one
189      * item linked to one of that object's channels.
190      *
191      * @return Selection
192      */
193     public SelectionDTO getSelection() {
194         final SelectionDTO selection = new SelectionDTO();
195         for (String group : CHANNEL_GROUPS) {
196             for (Channel channel : thing.getChannelsOfGroup(group)) {
197                 if (isLinked(channel.getUID())) {
198                     try {
199                         Field field = selection.getClass().getField("include" + WordUtils.capitalize(group));
200                         logger.trace("ThermostatBridge: Thermostat thing '{}' including object '{}' in selection",
201                                 thing.getUID(), field.getName());
202                         field.set(selection, Boolean.TRUE);
203                         break;
204                     } catch (IllegalArgumentException | IllegalAccessException | NoSuchFieldException
205                             | SecurityException e) {
206                         logger.debug("ThermostatBridge: Exception setting selection for group '{}'", group, e);
207                     }
208                 }
209             }
210         }
211         return selection;
212     }
213
214     public List<RemoteSensorDTO> getSensors() {
215         List<RemoteSensorDTO> localSavedSensors = savedSensors;
216         return localSavedSensors == null ? EMPTY_SENSORS : localSavedSensors;
217     }
218
219     public @Nullable String getAlerts() {
220         ThermostatDTO thermostat = savedThermostat;
221         if (thermostat != null && thermostat.alerts != null) {
222             return EcobeeApi.getGson().toJson(thermostat.alerts);
223         }
224         return null;
225     }
226
227     public @Nullable String getEvents() {
228         ThermostatDTO thermostat = savedThermostat;
229         if (thermostat != null && thermostat.events != null) {
230             return EcobeeApi.getGson().toJson(thermostat.events);
231         }
232         return null;
233     }
234
235     public @Nullable String getClimates() {
236         ThermostatDTO thermostat = savedThermostat;
237         if (thermostat != null && thermostat.program != null && thermostat.program.climates != null) {
238             return EcobeeApi.getGson().toJson(thermostat.program.climates);
239         }
240         return null;
241     }
242
243     public boolean isValidClimateRef(String climateRef) {
244         return validClimateRefs.contains(climateRef);
245     }
246
247     public String getThermostatId() {
248         return thermostatId;
249     }
250
251     /*
252      * Called by EcobeeActions to perform a thermostat function
253      */
254     public boolean actionPerformFunction(AbstractFunction function) {
255         logger.debug("ThermostatBridge: Perform function '{}' on thermostat {}", function.type, thermostatId);
256         SelectionDTO selection = new SelectionDTO();
257         selection.setThermostats(Collections.singleton(thermostatId));
258         FunctionRequest request = new FunctionRequest(selection);
259         request.functions = Collections.singletonList(function);
260         EcobeeAccountBridgeHandler handler = getBridgeHandler();
261         if (handler != null) {
262             return handler.performThermostatFunction(request);
263         }
264         return false;
265     }
266
267     @Override
268     public Collection<Class<? extends ThingHandlerService>> getServices() {
269         return Collections.unmodifiableList(
270                 Stream.of(EcobeeActions.class, SensorDiscoveryService.class).collect(Collectors.toList()));
271     }
272
273     public void updateChannels(ThermostatDTO thermostat) {
274         logger.debug("ThermostatBridge: Updating channels for thermostat id {}", thermostat.identifier);
275         savedThermostat = thermostat;
276         updateAlert(thermostat.alerts);
277         updateHouseDetails(thermostat.houseDetails);
278         updateInfo(thermostat);
279         updateEquipmentStatus(thermostat);
280         updateLocation(thermostat.location);
281         updateManagement(thermostat.management);
282         updateProgram(thermostat.program);
283         updateEvent(thermostat.events);
284         updateRemoteSensors(thermostat.remoteSensors);
285         updateRuntime(thermostat.runtime);
286         updateSettings(thermostat.settings);
287         updateTechnician(thermostat.technician);
288         updateVersion(thermostat.version);
289         updateWeather(thermostat.weather);
290         savedSensors = thermostat.remoteSensors;
291     }
292
293     private void handleThermostatCommand(ChannelUID channelUID, Command command) {
294         logger.debug("Got command '{}' for channel '{}' of thing '{}'", command, channelUID, getThing().getUID());
295         String channelId = channelUID.getIdWithoutGroup();
296         String groupId = channelUID.getGroupId();
297         if (groupId == null) {
298             logger.info("Can't handle command because channel's groupId is null");
299             return;
300         }
301         ThermostatDTO thermostat = new ThermostatDTO();
302         Field field;
303         try {
304             switch (groupId) {
305                 case CHGRP_INFO:
306                     field = thermostat.getClass().getField(channelId);
307                     setField(field, thermostat, command);
308                     break;
309                 case CHGRP_SETTINGS:
310                     SettingsDTO settings = new SettingsDTO();
311                     field = settings.getClass().getField(channelId);
312                     setField(field, settings, command);
313                     thermostat.settings = settings;
314                     break;
315                 case CHGRP_LOCATION:
316                     LocationDTO location = new LocationDTO();
317                     field = location.getClass().getField(channelId);
318                     setField(field, location, command);
319                     thermostat.location = location;
320                     break;
321                 case CHGRP_HOUSE_DETAILS:
322                     HouseDetailsDTO houseDetails = new HouseDetailsDTO();
323                     field = houseDetails.getClass().getField(channelId);
324                     setField(field, houseDetails, command);
325                     thermostat.houseDetails = houseDetails;
326                     break;
327                 default:
328                     // All other groups contain only read-only fields
329                     return;
330             }
331             performThermostatUpdate(thermostat);
332         } catch (NoSuchFieldException | SecurityException e) {
333             logger.info("Unable to get field for '{}.{}'", groupId, channelId);
334         }
335     }
336
337     private void setField(Field field, Object object, Command command) {
338         logger.info("Setting field '{}.{}' to value '{}'", object.getClass().getSimpleName().toLowerCase(),
339                 field.getName(), command);
340         Class<?> fieldClass = field.getType();
341         try {
342             boolean success = false;
343             if (String.class.isAssignableFrom(fieldClass)) {
344                 if (command instanceof StringType) {
345                     logger.debug("Set field of type String to value of StringType");
346                     field.set(object, command.toString());
347                     success = true;
348                 }
349             } else if (Integer.class.isAssignableFrom(fieldClass)) {
350                 if (command instanceof DecimalType) {
351                     logger.debug("Set field of type Integer to value of DecimalType");
352                     field.set(object, Integer.valueOf(((DecimalType) command).intValue()));
353                     success = true;
354                 } else if (command instanceof QuantityType) {
355                     Unit<?> unit = ((QuantityType<?>) command).getUnit();
356                     logger.debug("Set field of type Integer to value of QuantityType with unit {}", unit);
357                     if (unit.equals(ImperialUnits.FAHRENHEIT) || unit.equals(SIUnits.CELSIUS)) {
358                         QuantityType<?> quantity = ((QuantityType<?>) command).toUnit(ImperialUnits.FAHRENHEIT);
359                         if (quantity != null) {
360                             field.set(object, quantity.intValue() * 10);
361                             success = true;
362                         }
363                     }
364                 }
365             } else if (Boolean.class.isAssignableFrom(fieldClass)) {
366                 if (command instanceof OnOffType) {
367                     logger.debug("Set field of type Boolean to value of OnOffType");
368                     field.set(object, command == OnOffType.ON);
369                     success = true;
370                 }
371             }
372             if (!success) {
373                 logger.info("Don't know how to convert command of type '{}' to {}.{}",
374                         command.getClass().getSimpleName(), object.getClass().getSimpleName(), field.getName());
375             }
376         } catch (IllegalArgumentException | IllegalAccessException e) {
377             logger.info("Unable to set field '{}.{}' to value '{}'", object.getClass().getSimpleName(), field.getName(),
378                     command, e);
379         }
380     }
381
382     private void updateInfo(ThermostatDTO thermostat) {
383         final String grp = CHGRP_INFO + "#";
384         updateChannel(grp + CH_IDENTIFIER, EcobeeUtils.undefOrString(thermostat.identifier));
385         updateChannel(grp + CH_NAME, EcobeeUtils.undefOrString(thermostat.name));
386         updateChannel(grp + CH_THERMOSTAT_REV, EcobeeUtils.undefOrString(thermostat.thermostatRev));
387         updateChannel(grp + CH_IS_REGISTERED, EcobeeUtils.undefOrOnOff(thermostat.isRegistered));
388         updateChannel(grp + CH_MODEL_NUMBER, EcobeeUtils.undefOrString(thermostat.modelNumber));
389         updateChannel(grp + CH_BRAND, EcobeeUtils.undefOrString(thermostat.brand));
390         updateChannel(grp + CH_FEATURES, EcobeeUtils.undefOrString(thermostat.features));
391         updateChannel(grp + CH_LAST_MODIFIED, EcobeeUtils.undefOrDate(thermostat.lastModified, timeZoneProvider));
392         updateChannel(grp + CH_THERMOSTAT_TIME, EcobeeUtils.undefOrDate(thermostat.thermostatTime, timeZoneProvider));
393     }
394
395     private void updateEquipmentStatus(ThermostatDTO thermostat) {
396         final String grp = CHGRP_EQUIPMENT_STATUS + "#";
397         updateChannel(grp + CH_EQUIPMENT_STATUS, EcobeeUtils.undefOrString(thermostat.equipmentStatus));
398     }
399
400     private void updateRuntime(@Nullable RuntimeDTO runtime) {
401         if (runtime == null) {
402             return;
403         }
404         final String grp = CHGRP_RUNTIME + "#";
405         updateChannel(grp + CH_RUNTIME_REV, EcobeeUtils.undefOrString(runtime.runtimeRev));
406         updateChannel(grp + CH_CONNECTED, EcobeeUtils.undefOrOnOff(runtime.connected));
407         updateChannel(grp + CH_FIRST_CONNECTED, EcobeeUtils.undefOrDate(runtime.firstConnected, timeZoneProvider));
408         updateChannel(grp + CH_CONNECT_DATE_TIME, EcobeeUtils.undefOrDate(runtime.connectDateTime, timeZoneProvider));
409         updateChannel(grp + CH_DISCONNECT_DATE_TIME,
410                 EcobeeUtils.undefOrDate(runtime.disconnectDateTime, timeZoneProvider));
411         updateChannel(grp + CH_RT_LAST_MODIFIED, EcobeeUtils.undefOrDate(runtime.lastModified, timeZoneProvider));
412         updateChannel(grp + CH_RT_LAST_STATUS_MODIFIED,
413                 EcobeeUtils.undefOrDate(runtime.lastStatusModified, timeZoneProvider));
414         updateChannel(grp + CH_RUNTIME_DATE, EcobeeUtils.undefOrString(runtime.runtimeDate));
415         updateChannel(grp + CH_RUNTIME_INTERVAL, EcobeeUtils.undefOrDecimal(runtime.runtimeInterval));
416         updateChannel(grp + CH_ACTUAL_TEMPERATURE, EcobeeUtils.undefOrTemperature(runtime.actualTemperature));
417         updateChannel(grp + CH_ACTUAL_HUMIDITY,
418                 EcobeeUtils.undefOrQuantity(runtime.actualHumidity, SmartHomeUnits.PERCENT));
419         updateChannel(grp + CH_RAW_TEMPERATURE, EcobeeUtils.undefOrTemperature(runtime.rawTemperature));
420         updateChannel(grp + CH_SHOW_ICON_MODE, EcobeeUtils.undefOrDecimal(runtime.showIconMode));
421         updateChannel(grp + CH_DESIRED_HEAT, EcobeeUtils.undefOrTemperature(runtime.desiredHeat));
422         updateChannel(grp + CH_DESIRED_COOL, EcobeeUtils.undefOrTemperature(runtime.desiredCool));
423         updateChannel(grp + CH_DESIRED_HUMIDITY,
424                 EcobeeUtils.undefOrQuantity(runtime.desiredHumidity, SmartHomeUnits.PERCENT));
425         updateChannel(grp + CH_DESIRED_DEHUMIDITY,
426                 EcobeeUtils.undefOrQuantity(runtime.desiredDehumidity, SmartHomeUnits.PERCENT));
427         updateChannel(grp + CH_DESIRED_FAN_MODE, EcobeeUtils.undefOrString(runtime.desiredFanMode));
428         if (runtime.desiredHeatRange != null && runtime.desiredHeatRange.size() == 2) {
429             updateChannel(grp + CH_DESIRED_HEAT_RANGE_LOW,
430                     EcobeeUtils.undefOrTemperature(runtime.desiredHeatRange.get(0)));
431             updateChannel(grp + CH_DESIRED_HEAT_RANGE_HIGH,
432                     EcobeeUtils.undefOrTemperature(runtime.desiredHeatRange.get(1)));
433         }
434         if (runtime.desiredCoolRange != null && runtime.desiredCoolRange.size() == 2) {
435             updateChannel(grp + CH_DESIRED_COOL_RANGE_LOW,
436                     EcobeeUtils.undefOrTemperature(runtime.desiredCoolRange.get(0)));
437             updateChannel(grp + CH_DESIRED_COOL_RANGE_HIGH,
438                     EcobeeUtils.undefOrTemperature(runtime.desiredCoolRange.get(1)));
439         }
440     }
441
442     private void updateSettings(@Nullable SettingsDTO settings) {
443         if (settings == null) {
444             return;
445         }
446         final String grp = CHGRP_SETTINGS + "#";
447         updateChannel(grp + CH_HVAC_MODE, EcobeeUtils.undefOrString(settings.hvacMode));
448         updateChannel(grp + CH_LAST_SERVICE_DATE, EcobeeUtils.undefOrString(settings.lastServiceDate));
449         updateChannel(grp + CH_SERVICE_REMIND_ME, EcobeeUtils.undefOrOnOff(settings.serviceRemindMe));
450         updateChannel(grp + CH_MONTHS_BETWEEN_SERVICE, EcobeeUtils.undefOrDecimal(settings.monthsBetweenService));
451         updateChannel(grp + CH_REMIND_ME_DATE, EcobeeUtils.undefOrString(settings.remindMeDate));
452         updateChannel(grp + CH_VENT, EcobeeUtils.undefOrString(settings.vent));
453         updateChannel(grp + CH_VENTILATOR_MIN_ON_TIME, EcobeeUtils.undefOrDecimal(settings.ventilatorMinOnTime));
454         updateChannel(grp + CH_SERVICE_REMIND_TECHNICIAN, EcobeeUtils.undefOrOnOff(settings.serviceRemindTechnician));
455         updateChannel(grp + CH_EI_LOCATION, EcobeeUtils.undefOrString(settings.eiLocation));
456         updateChannel(grp + CH_COLD_TEMP_ALERT, EcobeeUtils.undefOrTemperature(settings.coldTempAlert));
457         updateChannel(grp + CH_COLD_TEMP_ALERT_ENABLED, EcobeeUtils.undefOrOnOff(settings.coldTempAlertEnabled));
458         updateChannel(grp + CH_HOT_TEMP_ALERT, EcobeeUtils.undefOrTemperature(settings.hotTempAlert));
459         updateChannel(grp + CH_HOT_TEMP_ALERT_ENABLED, EcobeeUtils.undefOrOnOff(settings.hotTempAlertEnabled));
460         updateChannel(grp + CH_COOL_STAGES, EcobeeUtils.undefOrDecimal(settings.coolStages));
461         updateChannel(grp + CH_HEAT_STAGES, EcobeeUtils.undefOrDecimal(settings.heatStages));
462         updateChannel(grp + CH_MAX_SET_BACK, EcobeeUtils.undefOrDecimal(settings.maxSetBack));
463         updateChannel(grp + CH_MAX_SET_FORWARD, EcobeeUtils.undefOrDecimal(settings.maxSetForward));
464         updateChannel(grp + CH_QUICK_SAVE_SET_BACK, EcobeeUtils.undefOrDecimal(settings.quickSaveSetBack));
465         updateChannel(grp + CH_QUICK_SAVE_SET_FORWARD, EcobeeUtils.undefOrDecimal(settings.quickSaveSetForward));
466         updateChannel(grp + CH_HAS_HEAT_PUMP, EcobeeUtils.undefOrOnOff(settings.hasHeatPump));
467         updateChannel(grp + CH_HAS_FORCED_AIR, EcobeeUtils.undefOrOnOff(settings.hasForcedAir));
468         updateChannel(grp + CH_HAS_BOILER, EcobeeUtils.undefOrOnOff(settings.hasBoiler));
469         updateChannel(grp + CH_HAS_HUMIDIFIER, EcobeeUtils.undefOrOnOff(settings.hasHumidifier));
470         updateChannel(grp + CH_HAS_ERV, EcobeeUtils.undefOrOnOff(settings.hasErv));
471         updateChannel(grp + CH_HAS_HRV, EcobeeUtils.undefOrOnOff(settings.hasHrv));
472         updateChannel(grp + CH_CONDENSATION_AVOID, EcobeeUtils.undefOrOnOff(settings.condensationAvoid));
473         updateChannel(grp + CH_USE_CELSIUS, EcobeeUtils.undefOrOnOff(settings.useCelsius));
474         updateChannel(grp + CH_USE_TIME_FORMAT_12, EcobeeUtils.undefOrOnOff(settings.useTimeFormat12));
475         updateChannel(grp + CH_LOCALE, EcobeeUtils.undefOrString(settings.locale));
476         updateChannel(grp + CH_HUMIDITY, EcobeeUtils.undefOrString(settings.humidity));
477         updateChannel(grp + CH_HUMIDIFIER_MODE, EcobeeUtils.undefOrString(settings.humidifierMode));
478         updateChannel(grp + CH_BACKLIGHT_ON_INTENSITY, EcobeeUtils.undefOrDecimal(settings.backlightOnIntensity));
479         updateChannel(grp + CH_BACKLIGHT_SLEEP_INTENSITY, EcobeeUtils.undefOrDecimal(settings.backlightSleepIntensity));
480         updateChannel(grp + CH_BACKLIGHT_OFF_TIME, EcobeeUtils.undefOrDecimal(settings.backlightOffTime));
481         updateChannel(grp + CH_SOUND_TICK_VOLUME, EcobeeUtils.undefOrDecimal(settings.soundTickVolume));
482         updateChannel(grp + CH_SOUND_ALERT_VOLUME, EcobeeUtils.undefOrDecimal(settings.soundAlertVolume));
483         updateChannel(grp + CH_COMPRESSOR_PROTECTION_MIN_TIME,
484                 EcobeeUtils.undefOrDecimal(settings.compressorProtectionMinTime));
485         updateChannel(grp + CH_COMPRESSOR_PROTECTION_MIN_TEMP,
486                 EcobeeUtils.undefOrTemperature(settings.compressorProtectionMinTemp));
487         updateChannel(grp + CH_STAGE1_HEATING_DIFFERENTIAL_TEMP,
488                 EcobeeUtils.undefOrDecimal(settings.stage1HeatingDifferentialTemp));
489         updateChannel(grp + CH_STAGE1_COOLING_DIFFERENTIAL_TEMP,
490                 EcobeeUtils.undefOrDecimal(settings.stage1CoolingDifferentialTemp));
491         updateChannel(grp + CH_STAGE1_HEATING_DISSIPATION_TIME,
492                 EcobeeUtils.undefOrDecimal(settings.stage1HeatingDissipationTime));
493         updateChannel(grp + CH_STAGE1_COOLING_DISSIPATION_TIME,
494                 EcobeeUtils.undefOrDecimal(settings.stage1CoolingDissipationTime));
495         updateChannel(grp + CH_HEAT_PUMP_REVERSAL_ON_COOL, EcobeeUtils.undefOrOnOff(settings.heatPumpReversalOnCool));
496         updateChannel(grp + CH_FAN_CONTROLLER_REQUIRED, EcobeeUtils.undefOrOnOff(settings.fanControlRequired));
497         updateChannel(grp + CH_FAN_MIN_ON_TIME, EcobeeUtils.undefOrDecimal(settings.fanMinOnTime));
498         updateChannel(grp + CH_HEAT_COOL_MIN_DELTA, EcobeeUtils.undefOrDecimal(settings.heatCoolMinDelta));
499         updateChannel(grp + CH_TEMP_CORRECTION, EcobeeUtils.undefOrDecimal(settings.tempCorrection));
500         updateChannel(grp + CH_HOLD_ACTION, EcobeeUtils.undefOrString(settings.holdAction));
501         updateChannel(grp + CH_HEAT_PUMP_GROUND_WATER, EcobeeUtils.undefOrOnOff(settings.heatPumpGroundWater));
502         updateChannel(grp + CH_HAS_ELECTRIC, EcobeeUtils.undefOrOnOff(settings.hasElectric));
503         updateChannel(grp + CH_HAS_DEHUMIDIFIER, EcobeeUtils.undefOrOnOff(settings.hasDehumidifier));
504         updateChannel(grp + CH_DEHUMIDIFIER_MODE, EcobeeUtils.undefOrString(settings.dehumidifierMode));
505         updateChannel(grp + CH_DEHUMIDIFIER_LEVEL, EcobeeUtils.undefOrDecimal(settings.dehumidifierLevel));
506         updateChannel(grp + CH_DEHUMIDIFY_WITH_AC, EcobeeUtils.undefOrOnOff(settings.dehumidifyWithAC));
507         updateChannel(grp + CH_DEHUMIDIFY_OVERCOOL_OFFSET,
508                 EcobeeUtils.undefOrDecimal(settings.dehumidifyOvercoolOffset));
509         updateChannel(grp + CH_AUTO_HEAT_COOL_FEATURE_ENABLED,
510                 EcobeeUtils.undefOrOnOff(settings.autoHeatCoolFeatureEnabled));
511         updateChannel(grp + CH_WIFI_OFFLINE_ALERT, EcobeeUtils.undefOrOnOff(settings.wifiOfflineAlert));
512         updateChannel(grp + CH_HEAT_MIN_TEMP, EcobeeUtils.undefOrTemperature(settings.heatMinTemp));
513         updateChannel(grp + CH_HEAT_MAX_TEMP, EcobeeUtils.undefOrTemperature(settings.heatMaxTemp));
514         updateChannel(grp + CH_COOL_MIN_TEMP, EcobeeUtils.undefOrTemperature(settings.coolMinTemp));
515         updateChannel(grp + CH_COOL_MAX_TEMP, EcobeeUtils.undefOrTemperature(settings.coolMaxTemp));
516         updateChannel(grp + CH_HEAT_RANGE_HIGH, EcobeeUtils.undefOrTemperature(settings.heatRangeHigh));
517         updateChannel(grp + CH_HEAT_RANGE_LOW, EcobeeUtils.undefOrTemperature(settings.heatRangeLow));
518         updateChannel(grp + CH_COOL_RANGE_HIGH, EcobeeUtils.undefOrTemperature(settings.coolRangeHigh));
519         updateChannel(grp + CH_COOL_RANGE_LOW, EcobeeUtils.undefOrTemperature(settings.coolRangeLow));
520         updateChannel(grp + CH_USER_ACCESS_CODE, EcobeeUtils.undefOrString(settings.userAccessCode));
521         updateChannel(grp + CH_USER_ACCESS_SETTING, EcobeeUtils.undefOrDecimal(settings.userAccessSetting));
522         updateChannel(grp + CH_AUX_RUNTIME_ALERT, EcobeeUtils.undefOrDecimal(settings.auxRuntimeAlert));
523         updateChannel(grp + CH_AUX_OUTDOOR_TEMP_ALERT, EcobeeUtils.undefOrTemperature(settings.auxOutdoorTempAlert));
524         updateChannel(grp + CH_AUX_MAX_OUTDOOR_TEMP, EcobeeUtils.undefOrTemperature(settings.auxMaxOutdoorTemp));
525         updateChannel(grp + CH_AUX_RUNTIME_ALERT_NOTIFY, EcobeeUtils.undefOrOnOff(settings.auxRuntimeAlertNotify));
526         updateChannel(grp + CH_AUX_OUTDOOR_TEMP_ALERT_NOTIFY,
527                 EcobeeUtils.undefOrOnOff(settings.auxOutdoorTempAlertNotify));
528         updateChannel(grp + CH_AUX_RUNTIME_ALERT_NOTIFY_TECHNICIAN,
529                 EcobeeUtils.undefOrOnOff(settings.auxRuntimeAlertNotifyTechnician));
530         updateChannel(grp + CH_AUX_OUTDOOR_TEMP_ALERT_NOTIFY_TECHNICIAN,
531                 EcobeeUtils.undefOrOnOff(settings.auxOutdoorTempAlertNotifyTechnician));
532         updateChannel(grp + CH_DISABLE_PREHEATING, EcobeeUtils.undefOrOnOff(settings.disablePreHeating));
533         updateChannel(grp + CH_DISABLE_PRECOOLING, EcobeeUtils.undefOrOnOff(settings.disablePreCooling));
534         updateChannel(grp + CH_INSTALLER_CODE_REQUIRED, EcobeeUtils.undefOrOnOff(settings.installerCodeRequired));
535         updateChannel(grp + CH_DR_ACCEPT, EcobeeUtils.undefOrString(settings.drAccept));
536         updateChannel(grp + CH_IS_RENTAL_PROPERTY, EcobeeUtils.undefOrOnOff(settings.isRentalProperty));
537         updateChannel(grp + CH_USE_ZONE_CONTROLLER, EcobeeUtils.undefOrOnOff(settings.useZoneController));
538         updateChannel(grp + CH_RANDOM_START_DELAY_COOL, EcobeeUtils.undefOrDecimal(settings.randomStartDelayCool));
539         updateChannel(grp + CH_RANDOM_START_DELAY_HEAT, EcobeeUtils.undefOrDecimal(settings.randomStartDelayHeat));
540         updateChannel(grp + CH_HUMIDITY_HIGH_ALERT,
541                 EcobeeUtils.undefOrQuantity(settings.humidityHighAlert, SmartHomeUnits.PERCENT));
542         updateChannel(grp + CH_HUMIDITY_LOW_ALERT,
543                 EcobeeUtils.undefOrQuantity(settings.humidityLowAlert, SmartHomeUnits.PERCENT));
544         updateChannel(grp + CH_DISABLE_HEAT_PUMP_ALERTS, EcobeeUtils.undefOrOnOff(settings.disableHeatPumpAlerts));
545         updateChannel(grp + CH_DISABLE_ALERTS_ON_IDT, EcobeeUtils.undefOrOnOff(settings.disableAlertsOnIdt));
546         updateChannel(grp + CH_HUMIDITY_ALERT_NOTIFY, EcobeeUtils.undefOrOnOff(settings.humidityAlertNotify));
547         updateChannel(grp + CH_HUMIDITY_ALERT_NOTIFY_TECHNICIAN,
548                 EcobeeUtils.undefOrOnOff(settings.humidityAlertNotifyTechnician));
549         updateChannel(grp + CH_TEMP_ALERT_NOTIFY, EcobeeUtils.undefOrOnOff(settings.tempAlertNotify));
550         updateChannel(grp + CH_TEMP_ALERT_NOTIFY_TECHNICIAN,
551                 EcobeeUtils.undefOrOnOff(settings.tempAlertNotifyTechnician));
552         updateChannel(grp + CH_MONTHLY_ELECTRICITY_BILL_LIMIT,
553                 EcobeeUtils.undefOrDecimal(settings.monthlyElectricityBillLimit));
554         updateChannel(grp + CH_ENABLE_ELECTRICITY_BILL_ALERT,
555                 EcobeeUtils.undefOrOnOff(settings.enableElectricityBillAlert));
556         updateChannel(grp + CH_ENABLE_PROJECTED_ELECTRICITY_BILL_ALERT,
557                 EcobeeUtils.undefOrOnOff(settings.enableProjectedElectricityBillAlert));
558         updateChannel(grp + CH_ELECTRICITY_BILLING_DAY_OF_MONTH,
559                 EcobeeUtils.undefOrDecimal(settings.electricityBillingDayOfMonth));
560         updateChannel(grp + CH_ELECTRICITY_BILL_CYCLE_MONTHS,
561                 EcobeeUtils.undefOrDecimal(settings.electricityBillCycleMonths));
562         updateChannel(grp + CH_ELECTRICITY_BILL_START_MONTH,
563                 EcobeeUtils.undefOrDecimal(settings.electricityBillStartMonth));
564         updateChannel(grp + CH_VENTILATOR_MIN_ON_TIME_HOME,
565                 EcobeeUtils.undefOrDecimal(settings.ventilatorMinOnTimeHome));
566         updateChannel(grp + CH_VENTILATOR_MIN_ON_TIME_AWAY,
567                 EcobeeUtils.undefOrDecimal(settings.ventilatorMinOnTimeAway));
568         updateChannel(grp + CH_BACKLIGHT_OFF_DURING_SLEEP, EcobeeUtils.undefOrOnOff(settings.backlightOffDuringSleep));
569         updateChannel(grp + CH_AUTO_AWAY, EcobeeUtils.undefOrOnOff(settings.autoAway));
570         updateChannel(grp + CH_SMART_CIRCULATION, EcobeeUtils.undefOrOnOff(settings.smartCirculation));
571         updateChannel(grp + CH_FOLLOW_ME_COMFORT, EcobeeUtils.undefOrOnOff(settings.followMeComfort));
572         updateChannel(grp + CH_VENTILATOR_TYPE, EcobeeUtils.undefOrString(settings.ventilatorType));
573         updateChannel(grp + CH_IS_VENTILATOR_TIMER_ON, EcobeeUtils.undefOrOnOff(settings.isVentilatorTimerOn));
574         updateChannel(grp + CH_VENTILATOR_OFF_DATE_TIME, EcobeeUtils.undefOrString(settings.ventilatorOffDateTime));
575         updateChannel(grp + CH_HAS_UV_FILTER, EcobeeUtils.undefOrOnOff(settings.hasUVFilter));
576         updateChannel(grp + CH_COOLING_LOCKOUT, EcobeeUtils.undefOrOnOff(settings.coolingLockout));
577         updateChannel(grp + CH_VENTILATOR_FREE_COOLING, EcobeeUtils.undefOrOnOff(settings.ventilatorFreeCooling));
578         updateChannel(grp + CH_DEHUMIDIFY_WHEN_HEATING, EcobeeUtils.undefOrOnOff(settings.dehumidifyWhenHeating));
579         updateChannel(grp + CH_VENTILATOR_DEHUMIDIFY, EcobeeUtils.undefOrOnOff(settings.ventilatorDehumidify));
580         updateChannel(grp + CH_GROUP_REF, EcobeeUtils.undefOrString(settings.groupRef));
581         updateChannel(grp + CH_GROUP_NAME, EcobeeUtils.undefOrString(settings.groupName));
582         updateChannel(grp + CH_GROUP_SETTING, EcobeeUtils.undefOrDecimal(settings.groupSetting));
583     }
584
585     private void updateProgram(@Nullable ProgramDTO program) {
586         if (program == null) {
587             return;
588         }
589         final String grp = CHGRP_PROGRAM + "#";
590         updateChannel(grp + CH_PROGRAM_CURRENT_CLIMATE_REF, EcobeeUtils.undefOrString(program.currentClimateRef));
591         if (program.climates != null) {
592             saveValidClimateRefs(program.climates);
593         }
594     }
595
596     private void saveValidClimateRefs(List<ClimateDTO> climates) {
597         validClimateRefs.clear();
598         for (ClimateDTO climate : climates) {
599             validClimateRefs.add(climate.climateRef);
600         }
601     }
602
603     private void updateAlert(@Nullable List<AlertDTO> alerts) {
604         AlertDTO firstAlert;
605         if (alerts == null || alerts.isEmpty()) {
606             firstAlert = EMPTY_ALERT;
607         } else {
608             firstAlert = alerts.get(0);
609         }
610         final String grp = CHGRP_ALERT + "#";
611         updateChannel(grp + CH_ALERT_ACKNOWLEDGE_REF, EcobeeUtils.undefOrString(firstAlert.acknowledgeRef));
612         updateChannel(grp + CH_ALERT_DATE, EcobeeUtils.undefOrString(firstAlert.date));
613         updateChannel(grp + CH_ALERT_TIME, EcobeeUtils.undefOrString(firstAlert.time));
614         updateChannel(grp + CH_ALERT_SEVERITY, EcobeeUtils.undefOrString(firstAlert.severity));
615         updateChannel(grp + CH_ALERT_TEXT, EcobeeUtils.undefOrString(firstAlert.text));
616         updateChannel(grp + CH_ALERT_ALERT_NUMBER, EcobeeUtils.undefOrDecimal(firstAlert.alertNumber));
617         updateChannel(grp + CH_ALERT_ALERT_TYPE, EcobeeUtils.undefOrString(firstAlert.alertType));
618         updateChannel(grp + CH_ALERT_IS_OPERATOR_ALERT, EcobeeUtils.undefOrOnOff(firstAlert.isOperatorAlert));
619         updateChannel(grp + CH_ALERT_REMINDER, EcobeeUtils.undefOrString(firstAlert.reminder));
620         updateChannel(grp + CH_ALERT_SHOW_IDT, EcobeeUtils.undefOrOnOff(firstAlert.showIdt));
621         updateChannel(grp + CH_ALERT_SHOW_WEB, EcobeeUtils.undefOrOnOff(firstAlert.showWeb));
622         updateChannel(grp + CH_ALERT_SEND_EMAIL, EcobeeUtils.undefOrOnOff(firstAlert.sendEmail));
623         updateChannel(grp + CH_ALERT_ACKNOWLEDGEMENT, EcobeeUtils.undefOrString(firstAlert.acknowledgement));
624         updateChannel(grp + CH_ALERT_REMIND_ME_LATER, EcobeeUtils.undefOrOnOff(firstAlert.remindMeLater));
625         updateChannel(grp + CH_ALERT_THERMOSTAT_IDENTIFIER, EcobeeUtils.undefOrString(firstAlert.thermostatIdentifier));
626         updateChannel(grp + CH_ALERT_NOTIFICATION_TYPE, EcobeeUtils.undefOrString(firstAlert.notificationType));
627     }
628
629     private void updateEvent(@Nullable List<EventDTO> events) {
630         EventDTO runningEvent = EMPTY_EVENT;
631         if (events != null && !events.isEmpty()) {
632             for (EventDTO event : events) {
633                 if (event.running) {
634                     runningEvent = event;
635                     break;
636                 }
637             }
638         }
639         final String grp = CHGRP_EVENT + "#";
640         updateChannel(grp + CH_EVENT_NAME, EcobeeUtils.undefOrString(runningEvent.name));
641         updateChannel(grp + CH_EVENT_TYPE, EcobeeUtils.undefOrString(runningEvent.type));
642         updateChannel(grp + CH_EVENT_RUNNING, EcobeeUtils.undefOrOnOff(runningEvent.running));
643         updateChannel(grp + CH_EVENT_START_DATE, EcobeeUtils.undefOrString(runningEvent.startDate));
644         updateChannel(grp + CH_EVENT_START_TIME, EcobeeUtils.undefOrString(runningEvent.startTime));
645         updateChannel(grp + CH_EVENT_END_DATE, EcobeeUtils.undefOrString(runningEvent.endDate));
646         updateChannel(grp + CH_EVENT_END_TIME, EcobeeUtils.undefOrString(runningEvent.endTime));
647         updateChannel(grp + CH_EVENT_IS_OCCUPIED, EcobeeUtils.undefOrOnOff(runningEvent.isOccupied));
648         updateChannel(grp + CH_EVENT_IS_COOL_OFF, EcobeeUtils.undefOrOnOff(runningEvent.isCoolOff));
649         updateChannel(grp + CH_EVENT_IS_HEAT_OFF, EcobeeUtils.undefOrOnOff(runningEvent.isHeatOff));
650         updateChannel(grp + CH_EVENT_COOL_HOLD_TEMP, EcobeeUtils.undefOrTemperature(runningEvent.coolHoldTemp));
651         updateChannel(grp + CH_EVENT_HEAT_HOLD_TEMP, EcobeeUtils.undefOrTemperature(runningEvent.heatHoldTemp));
652         updateChannel(grp + CH_EVENT_FAN, EcobeeUtils.undefOrString(runningEvent.fan));
653         updateChannel(grp + CH_EVENT_VENT, EcobeeUtils.undefOrString(runningEvent.vent));
654         updateChannel(grp + CH_EVENT_VENTILATOR_MIN_ON_TIME,
655                 EcobeeUtils.undefOrDecimal(runningEvent.ventilatorMinOnTime));
656         updateChannel(grp + CH_EVENT_IS_OPTIONAL, EcobeeUtils.undefOrOnOff(runningEvent.isOptional));
657         updateChannel(grp + CH_EVENT_IS_TEMPERATURE_RELATIVE,
658                 EcobeeUtils.undefOrOnOff(runningEvent.isTemperatureRelative));
659         updateChannel(grp + CH_EVENT_COOL_RELATIVE_TEMP, EcobeeUtils.undefOrDecimal(runningEvent.coolRelativeTemp));
660         updateChannel(grp + CH_EVENT_HEAT_RELATIVE_TEMP, EcobeeUtils.undefOrDecimal(runningEvent.heatRelativeTemp));
661         updateChannel(grp + CH_EVENT_IS_TEMPERATURE_ABSOLUTE,
662                 EcobeeUtils.undefOrOnOff(runningEvent.isTemperatureAbsolute));
663         updateChannel(grp + CH_EVENT_DUTY_CYCLE_PERCENTAGE,
664                 EcobeeUtils.undefOrDecimal(runningEvent.dutyCyclePercentage));
665         updateChannel(grp + CH_EVENT_FAN_MIN_ON_TIME, EcobeeUtils.undefOrDecimal(runningEvent.fanMinOnTime));
666         updateChannel(grp + CH_EVENT_OCCUPIED_SENSOR_ACTIVE,
667                 EcobeeUtils.undefOrOnOff(runningEvent.occupiedSensorActive));
668         updateChannel(grp + CH_EVENT_UNOCCUPIED_SENSOR_ACTIVE,
669                 EcobeeUtils.undefOrOnOff(runningEvent.unoccupiedSensorActive));
670         updateChannel(grp + CH_EVENT_DR_RAMP_UP_TEMP, EcobeeUtils.undefOrDecimal(runningEvent.drRampUpTemp));
671         updateChannel(grp + CH_EVENT_DR_RAMP_UP_TIME, EcobeeUtils.undefOrDecimal(runningEvent.drRampUpTime));
672         updateChannel(grp + CH_EVENT_LINK_REF, EcobeeUtils.undefOrString(runningEvent.linkRef));
673         updateChannel(grp + CH_EVENT_HOLD_CLIMATE_REF, EcobeeUtils.undefOrString(runningEvent.holdClimateRef));
674     }
675
676     private void updateWeather(@Nullable WeatherDTO weather) {
677         if (weather == null || weather.forecasts == null) {
678             return;
679         }
680         final String weatherGrp = CHGRP_WEATHER + "#";
681         updateChannel(weatherGrp + CH_WEATHER_TIMESTAMP, EcobeeUtils.undefOrDate(weather.timestamp, timeZoneProvider));
682         updateChannel(weatherGrp + CH_WEATHER_WEATHER_STATION, EcobeeUtils.undefOrString(weather.weatherStation));
683
684         for (int index = 0; index < weather.forecasts.size(); index++) {
685             final String grp = CHGRP_FORECAST + String.format("%d", index) + "#";
686             WeatherForecastDTO forecast = weather.forecasts.get(index);
687             if (forecast != null) {
688                 updateChannel(grp + CH_FORECAST_WEATHER_SYMBOL, EcobeeUtils.undefOrDecimal(forecast.weatherSymbol));
689                 updateChannel(grp + CH_FORECAST_WEATHER_SYMBOL_TEXT,
690                         EcobeeUtils.undefOrString(symbolMap.get(forecast.weatherSymbol)));
691                 updateChannel(grp + CH_FORECAST_DATE_TIME,
692                         EcobeeUtils.undefOrDate(forecast.dateTime, timeZoneProvider));
693                 updateChannel(grp + CH_FORECAST_CONDITION, EcobeeUtils.undefOrString(forecast.condition));
694                 updateChannel(grp + CH_FORECAST_TEMPERATURE, EcobeeUtils.undefOrTemperature(forecast.temperature));
695                 updateChannel(grp + CH_FORECAST_PRESSURE,
696                         EcobeeUtils.undefOrQuantity(forecast.pressure, ImperialUnits.INCH_OF_MERCURY));
697                 updateChannel(grp + CH_FORECAST_RELATIVE_HUMIDITY,
698                         EcobeeUtils.undefOrQuantity(forecast.relativeHumidity, SmartHomeUnits.PERCENT));
699                 updateChannel(grp + CH_FORECAST_DEWPOINT, EcobeeUtils.undefOrTemperature(forecast.dewpoint));
700                 updateChannel(grp + CH_FORECAST_VISIBILITY,
701                         EcobeeUtils.undefOrQuantity(forecast.visibility, SIUnits.METRE));
702                 updateChannel(grp + CH_FORECAST_WIND_SPEED,
703                         EcobeeUtils.undefOrQuantity(forecast.windSpeed, ImperialUnits.MILES_PER_HOUR));
704                 updateChannel(grp + CH_FORECAST_WIND_GUST,
705                         EcobeeUtils.undefOrQuantity(forecast.windGust, ImperialUnits.MILES_PER_HOUR));
706                 updateChannel(grp + CH_FORECAST_WIND_DIRECTION, EcobeeUtils.undefOrString(forecast.windDirection));
707                 updateChannel(grp + CH_FORECAST_WIND_BEARING,
708                         EcobeeUtils.undefOrQuantity(forecast.windBearing, SmartHomeUnits.DEGREE_ANGLE));
709                 updateChannel(grp + CH_FORECAST_POP, EcobeeUtils.undefOrQuantity(forecast.pop, SmartHomeUnits.PERCENT));
710                 updateChannel(grp + CH_FORECAST_TEMP_HIGH, EcobeeUtils.undefOrTemperature(forecast.tempHigh));
711                 updateChannel(grp + CH_FORECAST_TEMP_LOW, EcobeeUtils.undefOrTemperature(forecast.tempLow));
712                 updateChannel(grp + CH_FORECAST_SKY, EcobeeUtils.undefOrDecimal(forecast.sky));
713                 updateChannel(grp + CH_FORECAST_SKY_TEXT, EcobeeUtils.undefOrString(skyMap.get(forecast.sky)));
714             }
715         }
716     }
717
718     private void updateVersion(@Nullable VersionDTO version) {
719         if (version == null) {
720             return;
721         }
722         final String grp = CHGRP_VERSION + "#";
723         updateChannel(grp + CH_THERMOSTAT_FIRMWARE_VERSION,
724                 EcobeeUtils.undefOrString(version.thermostatFirmwareVersion));
725     }
726
727     private void updateLocation(@Nullable LocationDTO loc) {
728         LocationDTO location = EMPTY_LOCATION;
729         if (loc != null) {
730             location = loc;
731         }
732         final String grp = CHGRP_LOCATION + "#";
733         updateChannel(grp + CH_TIME_ZONE_OFFSET_MINUTES, EcobeeUtils.undefOrDecimal(location.timeZoneOffsetMinutes));
734         updateChannel(grp + CH_TIME_ZONE, EcobeeUtils.undefOrString(location.timeZone));
735         updateChannel(grp + CH_IS_DAYLIGHT_SAVING, EcobeeUtils.undefOrOnOff(location.isDaylightSaving));
736         updateChannel(grp + CH_STREET_ADDRESS, EcobeeUtils.undefOrString(location.streetAddress));
737         updateChannel(grp + CH_CITY, EcobeeUtils.undefOrString(location.city));
738         updateChannel(grp + CH_PROVINCE_STATE, EcobeeUtils.undefOrString(location.provinceState));
739         updateChannel(grp + CH_COUNTRY, EcobeeUtils.undefOrString(location.country));
740         updateChannel(grp + CH_POSTAL_CODE, EcobeeUtils.undefOrString(location.postalCode));
741         updateChannel(grp + CH_PHONE_NUMBER, EcobeeUtils.undefOrString(location.phoneNumber));
742         updateChannel(grp + CH_MAP_COORDINATES, EcobeeUtils.undefOrPoint(location.mapCoordinates));
743     }
744
745     private void updateHouseDetails(@Nullable HouseDetailsDTO hd) {
746         HouseDetailsDTO houseDetails = EMPTY_HOUSEDETAILS;
747         if (hd != null) {
748             houseDetails = hd;
749         }
750         final String grp = CHGRP_HOUSE_DETAILS + "#";
751         updateChannel(grp + CH_HOUSEDETAILS_STYLE, EcobeeUtils.undefOrString(houseDetails.style));
752         updateChannel(grp + CH_HOUSEDETAILS_SIZE, EcobeeUtils.undefOrDecimal(houseDetails.size));
753         updateChannel(grp + CH_HOUSEDETAILS_NUMBER_OF_FLOORS, EcobeeUtils.undefOrDecimal(houseDetails.numberOfFloors));
754         updateChannel(grp + CH_HOUSEDETAILS_NUMBER_OF_ROOMS, EcobeeUtils.undefOrDecimal(houseDetails.numberOfRooms));
755         updateChannel(grp + CH_HOUSEDETAILS_NUMBER_OF_OCCUPANTS,
756                 EcobeeUtils.undefOrDecimal(houseDetails.numberOfOccupants));
757         updateChannel(grp + CH_HOUSEDETAILS_AGE, EcobeeUtils.undefOrDecimal(houseDetails.age));
758         updateChannel(grp + CH_HOUSEDETAILS_WINDOW_EFFICIENCY,
759                 EcobeeUtils.undefOrDecimal(houseDetails.windowEfficiency));
760     }
761
762     private void updateManagement(@Nullable ManagementDTO mgmt) {
763         ManagementDTO management = EMPTY_MANAGEMENT;
764         if (mgmt != null) {
765             management = mgmt;
766         }
767         final String grp = CHGRP_MANAGEMENT + "#";
768         updateChannel(grp + CH_MANAGEMENT_ADMIN_CONTACT, EcobeeUtils.undefOrString(management.administrativeContact));
769         updateChannel(grp + CH_MANAGEMENT_BILLING_CONTACT, EcobeeUtils.undefOrString(management.billingContact));
770         updateChannel(grp + CH_MANAGEMENT_NAME, EcobeeUtils.undefOrString(management.name));
771         updateChannel(grp + CH_MANAGEMENT_PHONE, EcobeeUtils.undefOrString(management.phone));
772         updateChannel(grp + CH_MANAGEMENT_EMAIL, EcobeeUtils.undefOrString(management.email));
773         updateChannel(grp + CH_MANAGEMENT_WEB, EcobeeUtils.undefOrString(management.web));
774         updateChannel(grp + CH_MANAGEMENT_SHOW_ALERT_IDT, EcobeeUtils.undefOrOnOff(management.showAlertIdt));
775         updateChannel(grp + CH_MANAGEMENT_SHOW_ALERT_WEB, EcobeeUtils.undefOrOnOff(management.showAlertWeb));
776     }
777
778     private void updateTechnician(@Nullable TechnicianDTO tech) {
779         TechnicianDTO technician = EMPTY_TECHNICIAN;
780         if (tech != null) {
781             technician = tech;
782         }
783         final String grp = CHGRP_TECHNICIAN + "#";
784         updateChannel(grp + CH_TECHNICIAN_CONTRACTOR_REF, EcobeeUtils.undefOrString(technician.contractorRef));
785         updateChannel(grp + CH_TECHNICIAN_NAME, EcobeeUtils.undefOrString(technician.name));
786         updateChannel(grp + CH_TECHNICIAN_PHONE, EcobeeUtils.undefOrString(technician.phone));
787         updateChannel(grp + CH_TECHNICIAN_STREET_ADDRESS, EcobeeUtils.undefOrString(technician.streetAddress));
788         updateChannel(grp + CH_TECHNICIAN_CITY, EcobeeUtils.undefOrString(technician.city));
789         updateChannel(grp + CH_TECHNICIAN_PROVINCE_STATE, EcobeeUtils.undefOrString(technician.provinceState));
790         updateChannel(grp + CH_TECHNICIAN_COUNTRY, EcobeeUtils.undefOrString(technician.country));
791         updateChannel(grp + CH_TECHNICIAN_POSTAL_CODE, EcobeeUtils.undefOrString(technician.postalCode));
792         updateChannel(grp + CH_TECHNICIAN_EMAIL, EcobeeUtils.undefOrString(technician.email));
793         updateChannel(grp + CH_TECHNICIAN_WEB, EcobeeUtils.undefOrString(technician.web));
794     }
795
796     private void updateChannel(String channelId, State state) {
797         updateState(channelId, state);
798         stateCache.put(channelId, state);
799     }
800
801     @SuppressWarnings("null")
802     private void updateRemoteSensors(@Nullable List<RemoteSensorDTO> remoteSensors) {
803         if (remoteSensors == null) {
804             return;
805         }
806         logger.debug("ThermostatBridge: Thermostat '{}' has {} remote sensors", thermostatId, remoteSensors.size());
807         for (RemoteSensorDTO sensor : remoteSensors) {
808             EcobeeSensorThingHandler handler = sensorHandlers.get(sensor.id);
809             if (handler != null) {
810                 logger.debug("ThermostatBridge: Sending data to sensor handler '{}({})' of type '{}'", sensor.id,
811                         sensor.name, sensor.type);
812                 handler.updateChannels(sensor);
813             }
814         }
815     }
816
817     private void performThermostatUpdate(ThermostatDTO thermostat) {
818         SelectionDTO selection = new SelectionDTO();
819         selection.setThermostats(Collections.singleton(thermostatId));
820         ThermostatUpdateRequestDTO request = new ThermostatUpdateRequestDTO(selection);
821         request.thermostat = thermostat;
822         EcobeeAccountBridgeHandler handler = getBridgeHandler();
823         if (handler != null) {
824             handler.performThermostatUpdate(request);
825         }
826     }
827
828     private void scheduleDiscoveryJob() {
829         logger.debug("ThermostatBridge: Scheduling sensor discovery job");
830         cancelDiscoveryJob();
831         discoverSensorsJob = scheduler.scheduleWithFixedDelay(this::discoverSensors,
832                 SENSOR_DISCOVERY_STARTUP_DELAY_SECONDS, SENSOR_DISCOVERY_INTERVAL_SECONDS, TimeUnit.SECONDS);
833     }
834
835     private void cancelDiscoveryJob() {
836         Future<?> localDiscoverSensorsJob = discoverSensorsJob;
837         if (localDiscoverSensorsJob != null) {
838             localDiscoverSensorsJob.cancel(true);
839             logger.debug("ThermostatBridge: Canceling sensor discovery job");
840         }
841     }
842
843     private void discoverSensors() {
844         EcobeeAccountBridgeHandler handler = getBridgeHandler();
845         if (handler != null && handler.isDiscoveryEnabled()) {
846             SensorDiscoveryService localDiscoveryService = discoveryService;
847             if (localDiscoveryService != null) {
848                 logger.debug("ThermostatBridge: Running sensor discovery");
849                 localDiscoveryService.startBackgroundDiscovery();
850             }
851         }
852     }
853
854     private @Nullable EcobeeAccountBridgeHandler getBridgeHandler() {
855         EcobeeAccountBridgeHandler handler = null;
856         Bridge bridge = getBridge();
857         if (bridge != null) {
858             handler = (EcobeeAccountBridgeHandler) bridge.getHandler();
859         }
860         return handler;
861     }
862
863     @SuppressWarnings("null")
864     private boolean isChannelReadOnly(ChannelUID channelUID) {
865         Boolean isReadOnly = channelReadOnlyMap.get(channelUID);
866         return isReadOnly != null ? isReadOnly : true;
867     }
868
869     private void clearSavedState() {
870         savedThermostat = null;
871         savedSensors = null;
872         stateCache.clear();
873     }
874
875     private void initializeReadOnlyChannels() {
876         channelReadOnlyMap.clear();
877         for (Channel channel : thing.getChannels()) {
878             ChannelTypeUID channelTypeUID = channel.getChannelTypeUID();
879             if (channelTypeUID != null) {
880                 ChannelType channelType = channelTypeRegistry.getChannelType(channelTypeUID, null);
881                 if (channelType != null) {
882                     channelReadOnlyMap.putIfAbsent(channel.getUID(), channelType.getState().isReadOnly());
883                 }
884             }
885         }
886     }
887
888     private void initializeWeatherMaps() {
889         initializeSymbolMap();
890         initializeSkyMap();
891     }
892
893     private void initializeSymbolMap() {
894         symbolMap.clear();
895         symbolMap.put(-2, "NO SYMBOL");
896         symbolMap.put(0, "SUNNY");
897         symbolMap.put(1, "FEW CLOUDS");
898         symbolMap.put(2, "PARTLY CLOUDY");
899         symbolMap.put(3, "MOSTLY CLOUDY");
900         symbolMap.put(4, "OVERCAST");
901         symbolMap.put(5, "DRIZZLE");
902         symbolMap.put(6, "RAIN");
903         symbolMap.put(7, "FREEZING RAIN");
904         symbolMap.put(8, "SHOWERS");
905         symbolMap.put(9, "HAIL");
906         symbolMap.put(10, "SNOW");
907         symbolMap.put(11, "FLURRIES");
908         symbolMap.put(12, "FREEZING SNOW");
909         symbolMap.put(13, "BLIZZARD");
910         symbolMap.put(14, "PELLETS");
911         symbolMap.put(15, "THUNDERSTORM");
912         symbolMap.put(16, "WINDY");
913         symbolMap.put(17, "TORNADO");
914         symbolMap.put(18, "FOG");
915         symbolMap.put(19, "HAZE");
916         symbolMap.put(20, "SMOKE");
917         symbolMap.put(21, "DUST");
918     }
919
920     private void initializeSkyMap() {
921         skyMap.clear();
922         skyMap.put(1, "SUNNY");
923         skyMap.put(2, "CLEAR");
924         skyMap.put(3, "MOSTLY SUNNY");
925         skyMap.put(4, "MOSTLY CLEAR");
926         skyMap.put(5, "HAZY SUNSHINE");
927         skyMap.put(6, "HAZE");
928         skyMap.put(7, "PASSING CLOUDS");
929         skyMap.put(8, "MORE SUN THAN CLOUDS");
930         skyMap.put(9, "SCATTERED CLOUDS");
931         skyMap.put(10, "PARTLY CLOUDY");
932         skyMap.put(11, "A MIXTURE OF SUN AND CLOUDS");
933         skyMap.put(12, "HIGH LEVEL CLOUDS");
934         skyMap.put(13, "MORE CLOUDS THAN SUN");
935         skyMap.put(14, "PARTLY SUNNY");
936         skyMap.put(15, "BROKEN CLOUDS");
937         skyMap.put(16, "MOSTLY CLOUDY");
938         skyMap.put(17, "CLOUDY");
939         skyMap.put(18, "OVERCAST");
940         skyMap.put(19, "LOW CLOUDS");
941         skyMap.put(20, "LIGHT FOG");
942         skyMap.put(21, "FOG");
943         skyMap.put(22, "DENSE FOG");
944         skyMap.put(23, "ICE FOG");
945         skyMap.put(24, "SANDSTORM");
946         skyMap.put(25, "DUSTSTORM");
947         skyMap.put(26, "INCREASING CLOUDINESS");
948         skyMap.put(27, "DECREASING CLOUDINESS");
949         skyMap.put(28, "CLEARING SKIES");
950         skyMap.put(29, "BREAKS OF SUN LATE");
951         skyMap.put(30, "EARLY FOG FOLLOWED BY SUNNY SKIES");
952         skyMap.put(31, "AFTERNOON CLOUDS");
953         skyMap.put(32, "MORNING CLOUDS");
954         skyMap.put(33, "SMOKE");
955         skyMap.put(34, "LOW LEVEL HAZE");
956     }
957 }