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