]> git.basschouten.com Git - openhab-addons.git/blob
ce30cb37d6e85eb8ab53ed512ba436800bf9ba2c
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2022 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.tado.internal.handler;
14
15 import static org.openhab.binding.tado.internal.api.TadoApiTypeUtils.terminationConditionTemplateToTerminationCondition;
16
17 import java.io.IOException;
18 import java.util.List;
19 import java.util.concurrent.ScheduledFuture;
20 import java.util.concurrent.TimeUnit;
21 import java.util.stream.Collectors;
22
23 import javax.measure.quantity.Temperature;
24
25 import org.eclipse.jdt.annotation.NonNullByDefault;
26 import org.eclipse.jdt.annotation.Nullable;
27 import org.openhab.binding.tado.internal.TadoBindingConstants;
28 import org.openhab.binding.tado.internal.TadoBindingConstants.FanLevel;
29 import org.openhab.binding.tado.internal.TadoBindingConstants.HorizontalSwing;
30 import org.openhab.binding.tado.internal.TadoBindingConstants.OperationMode;
31 import org.openhab.binding.tado.internal.TadoBindingConstants.TemperatureUnit;
32 import org.openhab.binding.tado.internal.TadoBindingConstants.VerticalSwing;
33 import org.openhab.binding.tado.internal.TadoBindingConstants.ZoneType;
34 import org.openhab.binding.tado.internal.TadoHvacChange;
35 import org.openhab.binding.tado.internal.adapter.TadoZoneStateAdapter;
36 import org.openhab.binding.tado.internal.api.ApiException;
37 import org.openhab.binding.tado.internal.api.GsonBuilderFactory;
38 import org.openhab.binding.tado.internal.api.TadoApiTypeUtils;
39 import org.openhab.binding.tado.internal.api.model.ACFanLevel;
40 import org.openhab.binding.tado.internal.api.model.ACHorizontalSwing;
41 import org.openhab.binding.tado.internal.api.model.ACVerticalSwing;
42 import org.openhab.binding.tado.internal.api.model.AcMode;
43 import org.openhab.binding.tado.internal.api.model.AcModeCapabilities;
44 import org.openhab.binding.tado.internal.api.model.CoolingZoneSetting;
45 import org.openhab.binding.tado.internal.api.model.GenericZoneCapabilities;
46 import org.openhab.binding.tado.internal.api.model.GenericZoneSetting;
47 import org.openhab.binding.tado.internal.api.model.Overlay;
48 import org.openhab.binding.tado.internal.api.model.OverlayTemplate;
49 import org.openhab.binding.tado.internal.api.model.OverlayTerminationCondition;
50 import org.openhab.binding.tado.internal.api.model.TadoSystemType;
51 import org.openhab.binding.tado.internal.api.model.Zone;
52 import org.openhab.binding.tado.internal.api.model.ZoneState;
53 import org.openhab.binding.tado.internal.config.TadoZoneConfig;
54 import org.openhab.core.library.types.DecimalType;
55 import org.openhab.core.library.types.OnOffType;
56 import org.openhab.core.library.types.QuantityType;
57 import org.openhab.core.library.types.StringType;
58 import org.openhab.core.library.unit.ImperialUnits;
59 import org.openhab.core.library.unit.SIUnits;
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.types.Command;
68 import org.openhab.core.types.RefreshType;
69 import org.openhab.core.types.StateOption;
70 import org.slf4j.Logger;
71 import org.slf4j.LoggerFactory;
72
73 import com.google.gson.Gson;
74
75 /**
76  * The {@link TadoZoneHandler} is responsible for handling commands of zones and update their state.
77  *
78  * @author Dennis Frommknecht - Initial contribution
79  * @author Andrew Fiddian-Green - Added Low Battery Alarm, A/C Power and Open Window channels
80  *
81  */
82 @NonNullByDefault
83 public class TadoZoneHandler extends BaseHomeThingHandler {
84     private Logger logger = LoggerFactory.getLogger(TadoZoneHandler.class);
85
86     private final TadoStateDescriptionProvider stateDescriptionProvider;
87     private TadoZoneConfig configuration;
88
89     private @Nullable ScheduledFuture<?> refreshTimer;
90     private @Nullable ScheduledFuture<?> scheduledHvacChange;
91     private @Nullable GenericZoneCapabilities capabilities;
92     private @Nullable TadoHvacChange pendingHvacChange;
93
94     private boolean disposing = false;
95     private @Nullable Gson gson;
96
97     public TadoZoneHandler(Thing thing, TadoStateDescriptionProvider stateDescriptionProvider) {
98         super(thing);
99         this.stateDescriptionProvider = stateDescriptionProvider;
100         configuration = getConfigAs(TadoZoneConfig.class);
101     }
102
103     public long getZoneId() {
104         return configuration.id;
105     }
106
107     public int getFallbackTimerDuration() {
108         return configuration.fallbackTimerDuration;
109     }
110
111     public ZoneType getZoneType() {
112         String zoneTypeStr = thing.getProperties().get(TadoBindingConstants.PROPERTY_ZONE_TYPE);
113         if (zoneTypeStr == null) {
114             throw new IllegalStateException("Zone type not initialized");
115         }
116         return ZoneType.valueOf(zoneTypeStr);
117     }
118
119     public OverlayTerminationCondition getDefaultTerminationCondition() throws IOException, ApiException {
120         OverlayTemplate overlayTemplate = getApi().showZoneDefaultOverlay(getHomeId(), getZoneId());
121         logApiTransaction(overlayTemplate, false);
122         return terminationConditionTemplateToTerminationCondition(overlayTemplate.getTerminationCondition());
123     }
124
125     public ZoneState getZoneState() throws IOException, ApiException {
126         ZoneState zoneState = getApi().showZoneState(getHomeId(), getZoneId());
127         logApiTransaction(zoneState, false);
128         return zoneState;
129     }
130
131     public GenericZoneCapabilities getZoneCapabilities() {
132         GenericZoneCapabilities capabilities = this.capabilities;
133         if (capabilities == null) {
134             throw new IllegalStateException("Zone capabilities not initialized");
135         }
136         return capabilities;
137     }
138
139     public TemperatureUnit getTemperatureUnit() {
140         return getHomeHandler().getTemperatureUnit();
141     }
142
143     public Overlay setOverlay(Overlay overlay) throws IOException, ApiException {
144         try {
145             logApiTransaction(overlay, true);
146             Overlay newOverlay = getApi().updateZoneOverlay(getHomeId(), getZoneId(), overlay);
147             logApiTransaction(newOverlay, false);
148             return newOverlay;
149         } catch (ApiException e) {
150             if (!logger.isTraceEnabled()) {
151                 logger.warn("ApiException sending JSON content:\n{}", convertToJsonString(overlay));
152             }
153             throw e;
154         }
155     }
156
157     public void removeOverlay() throws IOException, ApiException {
158         logger.debug("Removing overlay of home {} and zone {}", getHomeId(), getZoneId());
159         getApi().deleteZoneOverlay(getHomeId(), getZoneId());
160     }
161
162     @Override
163     public void handleCommand(ChannelUID channelUID, Command command) {
164         String id = channelUID.getId();
165
166         if (command == RefreshType.REFRESH) {
167             updateZoneState(false);
168             return;
169         }
170
171         synchronized (this) {
172             TadoHvacChange pendingHvacChange = this.pendingHvacChange;
173             if (pendingHvacChange == null) {
174                 throw new IllegalStateException("Zone pendingHvacChange not initialized");
175             }
176
177             switch (id) {
178                 case TadoBindingConstants.CHANNEL_ZONE_HVAC_MODE:
179                     pendingHvacChange.withHvacMode(((StringType) command).toFullString());
180                     scheduleHvacChange();
181                     break;
182                 case TadoBindingConstants.CHANNEL_ZONE_TARGET_TEMPERATURE:
183                     if (command instanceof QuantityType<?>) {
184                         @SuppressWarnings("unchecked")
185                         QuantityType<Temperature> state = (QuantityType<Temperature>) command;
186                         QuantityType<Temperature> stateInTargetUnit = getTemperatureUnit() == TemperatureUnit.FAHRENHEIT
187                                 ? state.toUnit(ImperialUnits.FAHRENHEIT)
188                                 : state.toUnit(SIUnits.CELSIUS);
189
190                         if (stateInTargetUnit != null) {
191                             pendingHvacChange.withTemperature(stateInTargetUnit.floatValue());
192                             scheduleHvacChange();
193                         }
194                     }
195                     break;
196                 case TadoBindingConstants.CHANNEL_ZONE_SWING:
197                     pendingHvacChange.withSwing(((OnOffType) command) == OnOffType.ON);
198                     scheduleHvacChange();
199                     break;
200                 case TadoBindingConstants.CHANNEL_ZONE_LIGHT:
201                     pendingHvacChange.withLight(((OnOffType) command) == OnOffType.ON);
202                     scheduleHvacChange();
203                     break;
204                 case TadoBindingConstants.CHANNEL_ZONE_FAN_SPEED:
205                     pendingHvacChange.withFanSpeed(((StringType) command).toFullString());
206                     scheduleHvacChange();
207                     break;
208                 case TadoBindingConstants.CHANNEL_ZONE_FAN_LEVEL:
209                     String fanLevelString = ((StringType) command).toFullString();
210                     pendingHvacChange.withFanLevel(FanLevel.valueOf(fanLevelString.toUpperCase()));
211                     scheduleHvacChange();
212                     break;
213                 case TadoBindingConstants.CHANNEL_ZONE_HORIZONTAL_SWING:
214                     String horizontalSwingString = ((StringType) command).toFullString();
215                     pendingHvacChange.withHorizontalSwing(HorizontalSwing.valueOf(horizontalSwingString.toUpperCase()));
216                     scheduleHvacChange();
217                     break;
218                 case TadoBindingConstants.CHANNEL_ZONE_VERTICAL_SWING:
219                     String verticalSwingString = ((StringType) command).toFullString();
220                     pendingHvacChange.withVerticalSwing(VerticalSwing.valueOf(verticalSwingString.toUpperCase()));
221                     scheduleHvacChange();
222                     break;
223                 case TadoBindingConstants.CHANNEL_ZONE_OPERATION_MODE:
224                     String operationMode = ((StringType) command).toFullString();
225                     pendingHvacChange.withOperationMode(OperationMode.valueOf(operationMode));
226                     scheduleHvacChange();
227                     break;
228                 case TadoBindingConstants.CHANNEL_ZONE_TIMER_DURATION:
229                     pendingHvacChange.activeForMinutes(((DecimalType) command).intValue());
230                     scheduleHvacChange();
231                     break;
232             }
233         }
234     }
235
236     @Override
237     public void initialize() {
238         disposing = false;
239         configuration = getConfigAs(TadoZoneConfig.class);
240         if (configuration.refreshInterval <= 0) {
241             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Refresh interval of zone "
242                     + getZoneId() + " of home " + getHomeId() + " must be greater than zero");
243             return;
244         } else if (configuration.fallbackTimerDuration <= 0) {
245             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Fallback timer duration of zone "
246                     + getZoneId() + " of home " + getHomeId() + " must be greater than zero");
247             return;
248         } else if (configuration.hvacChangeDebounce <= 0) {
249             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "HVAC change debounce of zone "
250                     + getZoneId() + " of home " + getHomeId() + " must be greater than zero");
251             return;
252         }
253
254         Bridge bridge = getBridge();
255         if (bridge != null) {
256             bridgeStatusChanged(bridge.getStatusInfo());
257         }
258     }
259
260     @Override
261     public void dispose() {
262         disposing = true;
263         cancelScheduledZoneStateUpdate();
264     }
265
266     @Override
267     public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
268         if (bridgeStatusInfo.getStatus() == ThingStatus.ONLINE) {
269             try {
270                 Zone zoneDetails = getApi().showZoneDetails(getHomeId(), getZoneId());
271                 logApiTransaction(zoneDetails, false);
272
273                 GenericZoneCapabilities capabilities = getApi().showZoneCapabilities(getHomeId(), getZoneId());
274                 logApiTransaction(capabilities, false);
275
276                 if (zoneDetails == null || capabilities == null) {
277                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
278                             "Can not access zone " + getZoneId() + " of home " + getHomeId());
279                     return;
280                 }
281
282                 updateProperty(TadoBindingConstants.PROPERTY_ZONE_NAME, zoneDetails.getName());
283                 updateProperty(TadoBindingConstants.PROPERTY_ZONE_TYPE, zoneDetails.getType().name());
284                 this.capabilities = capabilities;
285             } catch (IOException | ApiException e) {
286                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
287                         "Could not connect to server due to " + e.getMessage());
288                 cancelScheduledZoneStateUpdate();
289                 return;
290             }
291
292             scheduleZoneStateUpdate();
293             pendingHvacChange = new TadoHvacChange(getThing());
294
295             updateStatus(ThingStatus.ONLINE);
296         } else {
297             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
298             cancelScheduledZoneStateUpdate();
299         }
300     }
301
302     private void updateZoneState(boolean forceUpdate) {
303         if ((thing.getStatus() != ThingStatus.ONLINE) || disposing) {
304             return;
305         }
306
307         getHomeHandler().updateHomeState();
308
309         // No update during HVAC change debounce
310         ScheduledFuture<?> scheduledHvacChange = this.scheduledHvacChange;
311         if (!forceUpdate && scheduledHvacChange != null && !scheduledHvacChange.isDone()) {
312             return;
313         }
314
315         try {
316             ZoneState zoneState = getZoneState();
317
318             logger.debug("Updating state of home {} and zone {}", getHomeId(), getZoneId());
319
320             TadoZoneStateAdapter state = new TadoZoneStateAdapter(zoneState, getTemperatureUnit());
321             updateState(TadoBindingConstants.CHANNEL_ZONE_CURRENT_TEMPERATURE, state.getInsideTemperature());
322             updateState(TadoBindingConstants.CHANNEL_ZONE_HUMIDITY, state.getHumidity());
323
324             updateState(TadoBindingConstants.CHANNEL_ZONE_HEATING_POWER, state.getHeatingPower());
325             updateState(TadoBindingConstants.CHANNEL_ZONE_AC_POWER, state.getAcPower());
326
327             updateState(TadoBindingConstants.CHANNEL_ZONE_OPERATION_MODE, state.getOperationMode());
328
329             updateState(TadoBindingConstants.CHANNEL_ZONE_HVAC_MODE, state.getMode());
330             updateState(TadoBindingConstants.CHANNEL_ZONE_TARGET_TEMPERATURE, state.getTargetTemperature());
331             updateState(TadoBindingConstants.CHANNEL_ZONE_FAN_SPEED, state.getFanSpeed());
332             updateState(TadoBindingConstants.CHANNEL_ZONE_SWING, state.getSwing());
333             updateState(TadoBindingConstants.CHANNEL_ZONE_LIGHT, state.getLight());
334             updateState(TadoBindingConstants.CHANNEL_ZONE_FAN_LEVEL, state.getFanLevel());
335             updateState(TadoBindingConstants.CHANNEL_ZONE_HORIZONTAL_SWING, state.getHorizontalSwing());
336             updateState(TadoBindingConstants.CHANNEL_ZONE_VERTICAL_SWING, state.getVerticalSwing());
337
338             updateState(TadoBindingConstants.CHANNEL_ZONE_TIMER_DURATION, state.getRemainingTimerDuration());
339
340             updateState(TadoBindingConstants.CHANNEL_ZONE_OVERLAY_EXPIRY, state.getOverlayExpiration());
341
342             updateState(TadoBindingConstants.CHANNEL_ZONE_OPEN_WINDOW_DETECTED, state.getOpenWindowDetected());
343
344             updateDynamicStateDescriptions(zoneState);
345
346             onSuccessfulOperation();
347         } catch (IOException | ApiException e) {
348             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
349                     "Could not connect to server due to " + e.getMessage());
350         }
351
352         updateState(TadoBindingConstants.CHANNEL_ZONE_BATTERY_LOW_ALARM,
353                 getHomeHandler().getBatteryLowAlarm(getZoneId()));
354     }
355
356     /**
357      * Update the dynamic state descriptions for any channels which support an unknown sub- range of enumerator setting
358      * values, based on the list of capabilities reported by the respective zone.
359      *
360      * Note: currently this only applies to A/C devices that support fanLevel, horizontalSwing, or verticalSwing.
361      *
362      * @param zoneState the current zone Thing's state
363      */
364     private void updateDynamicStateDescriptions(ZoneState zoneState) {
365         GenericZoneSetting setting = zoneState.getSetting();
366         if (setting.getType() != TadoSystemType.AIR_CONDITIONING) {
367             return;
368         }
369
370         AcMode acMode = ((CoolingZoneSetting) setting).getMode();
371         AcModeCapabilities acModeCapabilities = acMode == null ? new AcModeCapabilities()
372                 : TadoApiTypeUtils.getModeCapabilities(acMode, capabilities);
373
374         // update the options list of supported fan levels
375         Channel channel = thing.getChannel(TadoBindingConstants.CHANNEL_ZONE_FAN_LEVEL);
376         if (channel != null) {
377             List<ACFanLevel> fanLevels = acModeCapabilities.getFanLevel();
378             if (fanLevels != null) {
379                 stateDescriptionProvider.setStateOptions(channel.getUID(),
380                         fanLevels.stream().map(u -> new StateOption(u.name(), u.name())).collect(Collectors.toList()));
381             }
382         }
383
384         // update the options list of supported horizontal swing settings
385         channel = thing.getChannel(TadoBindingConstants.CHANNEL_ZONE_HORIZONTAL_SWING);
386         if (channel != null) {
387             List<ACHorizontalSwing> horizontalSwings = acModeCapabilities.getHorizontalSwing();
388             if (horizontalSwings != null) {
389                 stateDescriptionProvider.setStateOptions(channel.getUID(), horizontalSwings.stream()
390                         .map(u -> new StateOption(u.name(), u.name())).collect(Collectors.toList()));
391             }
392         }
393
394         // update the options list of supported vertical swing settings
395         channel = thing.getChannel(TadoBindingConstants.CHANNEL_ZONE_VERTICAL_SWING);
396         if (channel != null) {
397             List<ACVerticalSwing> verticalSwings = acModeCapabilities.getVerticalSwing();
398             if (verticalSwings != null) {
399                 stateDescriptionProvider.setStateOptions(channel.getUID(), verticalSwings.stream()
400                         .map(u -> new StateOption(u.name(), u.name())).collect(Collectors.toList()));
401             }
402         }
403     }
404
405     private void scheduleZoneStateUpdate() {
406         ScheduledFuture<?> refreshTimer = this.refreshTimer;
407         if (refreshTimer == null || refreshTimer.isCancelled()) {
408             this.refreshTimer = scheduler.scheduleWithFixedDelay(new Runnable() {
409                 @Override
410                 public void run() {
411                     updateZoneState(false);
412                 }
413             }, 5, configuration.refreshInterval, TimeUnit.SECONDS);
414         }
415     }
416
417     private void cancelScheduledZoneStateUpdate() {
418         ScheduledFuture<?> refreshTimer = this.refreshTimer;
419         if (refreshTimer != null) {
420             refreshTimer.cancel(false);
421         }
422     }
423
424     private void scheduleHvacChange() {
425         ScheduledFuture<?> scheduledHvacChange = this.scheduledHvacChange;
426         if (scheduledHvacChange != null) {
427             scheduledHvacChange.cancel(false);
428         }
429         this.scheduledHvacChange = scheduler.schedule(() -> {
430             try {
431                 synchronized (this) {
432                     TadoHvacChange pendingHvacChange = this.pendingHvacChange;
433                     this.pendingHvacChange = new TadoHvacChange(getThing());
434                     if (pendingHvacChange != null) {
435                         pendingHvacChange.apply();
436                     }
437                 }
438             } catch (IOException e) {
439                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
440             } catch (ApiException e) {
441                 logger.warn("Could not apply HVAC change on home {} and zone {}: {}", getHomeId(), getZoneId(),
442                         e.getMessage(), e);
443             } finally {
444                 updateZoneState(true);
445             }
446         }, configuration.hvacChangeDebounce, TimeUnit.SECONDS);
447     }
448
449     /**
450      * Helper method to log an API transaction on the given object.
451      * If the logger level is 'debug', the transaction is simply logged.
452      * If the logger level is 'trace, the object's JSON serial contents are included.
453      *
454      * @param object the object to be logged.
455      * @param isCommand marks whether the transaction is a command to, or a response from, the server.
456      */
457     private void logApiTransaction(Object object, boolean isCommand) {
458         if (logger.isDebugEnabled() || logger.isTraceEnabled()) {
459             String logType = isCommand ? "command" : "response";
460             if (logger.isTraceEnabled()) {
461                 logger.trace("Api {}: homeId:{}, zoneId:{}, objectId:{}, content:\n{}", logType, getHomeId(),
462                         getZoneId(), object.getClass().getSimpleName(), convertToJsonString(object));
463             } else if (logger.isDebugEnabled()) {
464                 logger.debug("Api {}: homeId:{}, zoneId:{}, objectId:{}", logType, getHomeId(), getZoneId(),
465                         object.getClass().getSimpleName());
466             }
467         }
468     }
469
470     private synchronized String convertToJsonString(Object object) {
471         Gson gson = this.gson;
472         if (gson == null) {
473             gson = this.gson = GsonBuilderFactory.defaultGsonBuilder().setPrettyPrinting().create();
474         }
475         return gson.toJson(object);
476     }
477 }