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