2 * Copyright (c) 2010-2022 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.tado.internal.handler;
15 import static org.openhab.binding.tado.internal.api.TadoApiTypeUtils.terminationConditionTemplateToTerminationCondition;
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;
23 import javax.measure.quantity.Temperature;
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;
74 * The {@link TadoZoneHandler} is responsible for handling commands of zones and update their state.
76 * @author Dennis Frommknecht - Initial contribution
77 * @author Andrew Fiddian-Green - Added Low Battery Alarm, A/C Power and Open Window channels
80 public class TadoZoneHandler extends BaseHomeThingHandler {
81 private Logger logger = LoggerFactory.getLogger(TadoZoneHandler.class);
83 private final TadoStateDescriptionProvider stateDescriptionProvider;
85 private TadoZoneConfig configuration;
86 private ScheduledFuture<?> refreshTimer;
87 private ScheduledFuture<?> scheduledHvacChange;
88 private GenericZoneCapabilities capabilities;
89 TadoHvacChange pendingHvacChange;
91 public TadoZoneHandler(Thing thing, TadoStateDescriptionProvider stateDescriptionProvider) {
93 this.stateDescriptionProvider = stateDescriptionProvider;
96 public long getZoneId() {
97 return this.configuration.id;
100 public int getFallbackTimerDuration() {
101 return this.configuration.fallbackTimerDuration;
104 public @Nullable ZoneType getZoneType() {
105 String zoneTypeStr = this.thing.getProperties().get(TadoBindingConstants.PROPERTY_ZONE_TYPE);
106 return zoneTypeStr != null ? ZoneType.valueOf(zoneTypeStr) : null;
109 public OverlayTerminationCondition getDefaultTerminationCondition() throws IOException, ApiException {
110 OverlayTemplate overlayTemplate = getApi().showZoneDefaultOverlay(getHomeId(), getZoneId());
111 return terminationConditionTemplateToTerminationCondition(overlayTemplate.getTerminationCondition());
114 public ZoneState getZoneState() throws IOException, ApiException {
115 HomeApi api = getApi();
116 return api != null ? api.showZoneState(getHomeId(), getZoneId()) : null;
119 public GenericZoneCapabilities getZoneCapabilities() {
120 return this.capabilities;
123 public TemperatureUnit getTemperatureUnit() {
124 return getHomeHandler().getTemperatureUnit();
127 public Overlay setOverlay(Overlay overlay) throws IOException, ApiException {
128 logger.debug("Setting overlay of home {} and zone {} with overlay: {}", getHomeId(), getZoneId(),
130 return getApi().updateZoneOverlay(getHomeId(), getZoneId(), overlay);
133 public void removeOverlay() throws IOException, ApiException {
134 logger.debug("Removing overlay of home {} and zone {}", getHomeId(), getZoneId());
135 getApi().deleteZoneOverlay(getHomeId(), getZoneId());
139 public void handleCommand(ChannelUID channelUID, Command command) {
140 String id = channelUID.getId();
142 if (command == RefreshType.REFRESH) {
143 updateZoneState(false);
148 case TadoBindingConstants.CHANNEL_ZONE_HVAC_MODE:
149 pendingHvacChange.withHvacMode(((StringType) command).toFullString());
150 scheduleHvacChange();
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);
158 if (stateInTargetUnit != null) {
159 pendingHvacChange.withTemperature(stateInTargetUnit.floatValue());
160 scheduleHvacChange();
164 case TadoBindingConstants.CHANNEL_ZONE_SWING:
165 pendingHvacChange.withSwing(((OnOffType) command) == OnOffType.ON);
166 scheduleHvacChange();
168 case TadoBindingConstants.CHANNEL_ZONE_LIGHT:
169 pendingHvacChange.withLight(((OnOffType) command) == OnOffType.ON);
170 scheduleHvacChange();
172 case TadoBindingConstants.CHANNEL_ZONE_FAN_SPEED:
173 pendingHvacChange.withFanSpeed(((StringType) command).toFullString());
174 scheduleHvacChange();
176 case TadoBindingConstants.CHANNEL_ZONE_FAN_LEVEL:
177 String fanLevelString = ((StringType) command).toFullString();
178 pendingHvacChange.withFanLevel(FanLevel.valueOf(fanLevelString.toUpperCase()));
180 case TadoBindingConstants.CHANNEL_ZONE_HORIZONTAL_SWING:
181 String horizontalSwingString = ((StringType) command).toFullString();
182 pendingHvacChange.withHorizontalSwing(HorizontalSwing.valueOf(horizontalSwingString.toUpperCase()));
183 scheduleHvacChange();
185 case TadoBindingConstants.CHANNEL_ZONE_VERTICAL_SWING:
186 String verticalSwingString = ((StringType) command).toFullString();
187 pendingHvacChange.withVerticalSwing(VerticalSwing.valueOf(verticalSwingString.toUpperCase()));
188 scheduleHvacChange();
190 case TadoBindingConstants.CHANNEL_ZONE_OPERATION_MODE:
191 String operationMode = ((StringType) command).toFullString();
192 pendingHvacChange.withOperationMode(OperationMode.valueOf(operationMode));
193 scheduleHvacChange();
195 case TadoBindingConstants.CHANNEL_ZONE_TIMER_DURATION:
196 pendingHvacChange.activeFor(((DecimalType) command).intValue());
197 scheduleHvacChange();
203 public void initialize() {
204 configuration = getConfigAs(TadoZoneConfig.class);
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");
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");
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");
220 Bridge bridge = getBridge();
221 if (bridge != null) {
222 bridgeStatusChanged(bridge.getStatusInfo());
227 public void dispose() {
228 cancelScheduledZoneStateUpdate();
232 public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
233 if (bridgeStatusInfo.getStatus() == ThingStatus.ONLINE) {
235 Zone zoneDetails = getApi().showZoneDetails(getHomeId(), getZoneId());
236 GenericZoneCapabilities capabilities = getApi().showZoneCapabilities(getHomeId(), getZoneId());
238 if (zoneDetails == null || capabilities == null) {
239 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
240 "Can not access zone " + getZoneId() + " of home " + getHomeId());
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();
255 scheduleZoneStateUpdate();
256 pendingHvacChange = new TadoHvacChange(getThing());
258 updateStatus(ThingStatus.ONLINE);
260 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
261 cancelScheduledZoneStateUpdate();
265 private void updateZoneState(boolean forceUpdate) {
266 TadoHomeHandler home = getHomeHandler();
268 home.updateHomeState();
271 // No update during HVAC change debounce
272 if (!forceUpdate && scheduledHvacChange != null && !scheduledHvacChange.isDone()) {
277 ZoneState zoneState = getZoneState();
279 if (zoneState == null) {
283 logger.debug("Updating state of home {} and zone {}", getHomeId(), getZoneId());
285 TadoZoneStateAdapter state = new TadoZoneStateAdapter(zoneState, getTemperatureUnit());
286 updateStateIfNotNull(TadoBindingConstants.CHANNEL_ZONE_CURRENT_TEMPERATURE, state.getInsideTemperature());
287 updateStateIfNotNull(TadoBindingConstants.CHANNEL_ZONE_HUMIDITY, state.getHumidity());
289 updateStateIfNotNull(TadoBindingConstants.CHANNEL_ZONE_HEATING_POWER, state.getHeatingPower());
290 updateStateIfNotNull(TadoBindingConstants.CHANNEL_ZONE_AC_POWER, state.getAcPower());
292 updateState(TadoBindingConstants.CHANNEL_ZONE_OPERATION_MODE, state.getOperationMode());
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());
303 updateState(TadoBindingConstants.CHANNEL_ZONE_TIMER_DURATION, state.getRemainingTimerDuration());
305 updateState(TadoBindingConstants.CHANNEL_ZONE_OVERLAY_EXPIRY, state.getOverlayExpiration());
307 updateState(TadoBindingConstants.CHANNEL_ZONE_OPEN_WINDOW_DETECTED, state.getOpenWindowDetected());
309 updateDynamicStateDescriptions(zoneState);
311 onSuccessfulOperation();
312 } catch (IOException | ApiException e) {
313 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
314 "Could not connect to server due to " + e.getMessage());
318 updateState(TadoBindingConstants.CHANNEL_ZONE_BATTERY_LOW_ALARM, home.getBatteryLowAlarm(getZoneId()));
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.
326 * Note: currently this only applies to A/C devices that support fanLevel, horizontalSwing, or verticalSwing.
328 * @param zoneState the current zone Thing's state
330 private void updateDynamicStateDescriptions(ZoneState zoneState) {
331 GenericZoneSetting setting = zoneState.getSetting();
332 if (setting.getType() != TadoSystemType.AIR_CONDITIONING) {
336 AcModeCapabilities acCapabilities = TadoApiTypeUtils.getModeCapabilities(
337 (AirConditioningCapabilities) capabilities, ((CoolingZoneSetting) setting).getMode());
339 if (acCapabilities != null) {
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()));
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()));
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()));
368 private void scheduleZoneStateUpdate() {
369 if (refreshTimer == null || refreshTimer.isCancelled()) {
370 refreshTimer = scheduler.scheduleWithFixedDelay(new Runnable() {
373 updateZoneState(false);
375 }, 5, configuration.refreshInterval, TimeUnit.SECONDS);
379 private void cancelScheduledZoneStateUpdate() {
380 if (refreshTimer != null) {
381 refreshTimer.cancel(false);
385 private void scheduleHvacChange() {
386 if (scheduledHvacChange != null) {
387 scheduledHvacChange.cancel(false);
390 scheduledHvacChange = scheduler.schedule(() -> {
392 TadoHvacChange change = this.pendingHvacChange;
393 this.pendingHvacChange = new TadoHvacChange(getThing());
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(),
401 updateZoneState(true);
403 }, configuration.hvacChangeDebounce, TimeUnit.SECONDS);
406 private void updateStateIfNotNull(String channelID, State state) {
408 updateState(channelID, state);