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.concurrent.ScheduledFuture;
19 import java.util.concurrent.TimeUnit;
21 import javax.measure.quantity.Temperature;
23 import org.eclipse.jdt.annotation.Nullable;
24 import org.openhab.binding.tado.internal.TadoBindingConstants;
25 import org.openhab.binding.tado.internal.TadoBindingConstants.OperationMode;
26 import org.openhab.binding.tado.internal.TadoBindingConstants.TemperatureUnit;
27 import org.openhab.binding.tado.internal.TadoBindingConstants.ZoneType;
28 import org.openhab.binding.tado.internal.TadoHvacChange;
29 import org.openhab.binding.tado.internal.adapter.TadoZoneStateAdapter;
30 import org.openhab.binding.tado.internal.api.ApiException;
31 import org.openhab.binding.tado.internal.api.client.HomeApi;
32 import org.openhab.binding.tado.internal.api.model.GenericZoneCapabilities;
33 import org.openhab.binding.tado.internal.api.model.Overlay;
34 import org.openhab.binding.tado.internal.api.model.OverlayTemplate;
35 import org.openhab.binding.tado.internal.api.model.OverlayTerminationCondition;
36 import org.openhab.binding.tado.internal.api.model.Zone;
37 import org.openhab.binding.tado.internal.api.model.ZoneState;
38 import org.openhab.binding.tado.internal.config.TadoZoneConfig;
39 import org.openhab.core.library.types.DecimalType;
40 import org.openhab.core.library.types.OnOffType;
41 import org.openhab.core.library.types.QuantityType;
42 import org.openhab.core.library.types.StringType;
43 import org.openhab.core.library.unit.ImperialUnits;
44 import org.openhab.core.library.unit.SIUnits;
45 import org.openhab.core.thing.Bridge;
46 import org.openhab.core.thing.ChannelUID;
47 import org.openhab.core.thing.Thing;
48 import org.openhab.core.thing.ThingStatus;
49 import org.openhab.core.thing.ThingStatusDetail;
50 import org.openhab.core.thing.ThingStatusInfo;
51 import org.openhab.core.types.Command;
52 import org.openhab.core.types.RefreshType;
53 import org.openhab.core.types.State;
54 import org.slf4j.Logger;
55 import org.slf4j.LoggerFactory;
58 * The {@link TadoZoneHandler} is responsible for handling commands of zones and update their state.
60 * @author Dennis Frommknecht - Initial contribution
61 * @author Andrew Fiddian-Green - Added Low Battery Alarm, A/C Power and Open Window channels
64 public class TadoZoneHandler extends BaseHomeThingHandler {
65 private Logger logger = LoggerFactory.getLogger(TadoZoneHandler.class);
67 private TadoZoneConfig configuration;
68 private ScheduledFuture<?> refreshTimer;
69 private ScheduledFuture<?> scheduledHvacChange;
70 private GenericZoneCapabilities capabilities;
71 TadoHvacChange pendingHvacChange;
73 public TadoZoneHandler(Thing thing) {
77 public long getZoneId() {
78 return this.configuration.id;
81 public int getFallbackTimerDuration() {
82 return this.configuration.fallbackTimerDuration;
85 public @Nullable ZoneType getZoneType() {
86 String zoneTypeStr = this.thing.getProperties().get(TadoBindingConstants.PROPERTY_ZONE_TYPE);
87 return zoneTypeStr != null ? ZoneType.valueOf(zoneTypeStr) : null;
90 public OverlayTerminationCondition getDefaultTerminationCondition() throws IOException, ApiException {
91 OverlayTemplate overlayTemplate = getApi().showZoneDefaultOverlay(getHomeId(), getZoneId());
92 return terminationConditionTemplateToTerminationCondition(overlayTemplate.getTerminationCondition());
95 public ZoneState getZoneState() throws IOException, ApiException {
96 HomeApi api = getApi();
97 return api != null ? api.showZoneState(getHomeId(), getZoneId()) : null;
100 public GenericZoneCapabilities getZoneCapabilities() {
101 return this.capabilities;
104 public TemperatureUnit getTemperatureUnit() {
105 return getHomeHandler().getTemperatureUnit();
108 public Overlay setOverlay(Overlay overlay) throws IOException, ApiException {
109 logger.debug("Setting overlay of home {} and zone {}", getHomeId(), getZoneId());
110 return getApi().updateZoneOverlay(getHomeId(), getZoneId(), overlay);
113 public void removeOverlay() throws IOException, ApiException {
114 logger.debug("Removing overlay of home {} and zone {}", getHomeId(), getZoneId());
115 getApi().deleteZoneOverlay(getHomeId(), getZoneId());
119 public void handleCommand(ChannelUID channelUID, Command command) {
120 String id = channelUID.getId();
122 if (command == RefreshType.REFRESH) {
123 updateZoneState(false);
128 case TadoBindingConstants.CHANNEL_ZONE_HVAC_MODE:
129 pendingHvacChange.withHvacMode(((StringType) command).toFullString());
130 scheduleHvacChange();
132 case TadoBindingConstants.CHANNEL_ZONE_TARGET_TEMPERATURE:
133 QuantityType<Temperature> state = (QuantityType<Temperature>) command;
134 QuantityType<Temperature> stateInTargetUnit = getTemperatureUnit() == TemperatureUnit.FAHRENHEIT
135 ? state.toUnit(ImperialUnits.FAHRENHEIT)
136 : state.toUnit(SIUnits.CELSIUS);
138 if (stateInTargetUnit != null) {
139 pendingHvacChange.withTemperature(stateInTargetUnit.floatValue());
140 scheduleHvacChange();
144 case TadoBindingConstants.CHANNEL_ZONE_SWING:
145 pendingHvacChange.withSwing(((OnOffType) command) == OnOffType.ON);
146 scheduleHvacChange();
148 case TadoBindingConstants.CHANNEL_ZONE_FAN_SPEED:
149 pendingHvacChange.withFanSpeed(((StringType) command).toFullString());
150 scheduleHvacChange();
152 case TadoBindingConstants.CHANNEL_ZONE_OPERATION_MODE:
153 String operationMode = ((StringType) command).toFullString();
154 pendingHvacChange.withOperationMode(OperationMode.valueOf(operationMode));
155 scheduleHvacChange();
157 case TadoBindingConstants.CHANNEL_ZONE_TIMER_DURATION:
158 pendingHvacChange.activeFor(((DecimalType) command).intValue());
159 scheduleHvacChange();
165 public void initialize() {
166 configuration = getConfigAs(TadoZoneConfig.class);
168 if (configuration.refreshInterval <= 0) {
169 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Refresh interval of zone "
170 + getZoneId() + " of home " + getHomeId() + " must be greater than zero");
172 } else if (configuration.fallbackTimerDuration <= 0) {
173 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Fallback timer duration of zone "
174 + getZoneId() + " of home " + getHomeId() + " must be greater than zero");
176 } else if (configuration.hvacChangeDebounce <= 0) {
177 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "HVAC change debounce of zone "
178 + getZoneId() + " of home " + getHomeId() + " must be greater than zero");
182 Bridge bridge = getBridge();
183 if (bridge != null) {
184 bridgeStatusChanged(bridge.getStatusInfo());
189 public void dispose() {
190 cancelScheduledZoneStateUpdate();
194 public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
195 if (bridgeStatusInfo.getStatus() == ThingStatus.ONLINE) {
197 Zone zoneDetails = getApi().showZoneDetails(getHomeId(), getZoneId());
198 GenericZoneCapabilities capabilities = getApi().showZoneCapabilities(getHomeId(), getZoneId());
200 if (zoneDetails == null || capabilities == null) {
201 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
202 "Can not access zone " + getZoneId() + " of home " + getHomeId());
206 updateProperty(TadoBindingConstants.PROPERTY_ZONE_NAME, zoneDetails.getName());
207 updateProperty(TadoBindingConstants.PROPERTY_ZONE_TYPE, zoneDetails.getType().name());
208 this.capabilities = capabilities;
209 } catch (IOException | ApiException e) {
210 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
211 "Could not connect to server due to " + e.getMessage());
212 cancelScheduledZoneStateUpdate();
216 scheduleZoneStateUpdate();
217 pendingHvacChange = new TadoHvacChange(getThing());
219 updateStatus(ThingStatus.ONLINE);
221 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
222 cancelScheduledZoneStateUpdate();
226 private void updateZoneState(boolean forceUpdate) {
227 TadoHomeHandler home = getHomeHandler();
229 home.updateHomeState();
232 // No update during HVAC change debounce
233 if (!forceUpdate && scheduledHvacChange != null && !scheduledHvacChange.isDone()) {
238 ZoneState zoneState = getZoneState();
240 if (zoneState == null) {
244 logger.debug("Updating state of home {} and zone {}", getHomeId(), getZoneId());
246 TadoZoneStateAdapter state = new TadoZoneStateAdapter(zoneState, getTemperatureUnit());
247 updateStateIfNotNull(TadoBindingConstants.CHANNEL_ZONE_CURRENT_TEMPERATURE, state.getInsideTemperature());
248 updateStateIfNotNull(TadoBindingConstants.CHANNEL_ZONE_HUMIDITY, state.getHumidity());
250 updateStateIfNotNull(TadoBindingConstants.CHANNEL_ZONE_HEATING_POWER, state.getHeatingPower());
251 updateStateIfNotNull(TadoBindingConstants.CHANNEL_ZONE_AC_POWER, state.getAcPower());
253 updateState(TadoBindingConstants.CHANNEL_ZONE_OPERATION_MODE, state.getOperationMode());
255 updateState(TadoBindingConstants.CHANNEL_ZONE_HVAC_MODE, state.getMode());
256 updateState(TadoBindingConstants.CHANNEL_ZONE_TARGET_TEMPERATURE, state.getTargetTemperature());
257 updateState(TadoBindingConstants.CHANNEL_ZONE_FAN_SPEED, state.getFanSpeed());
258 updateState(TadoBindingConstants.CHANNEL_ZONE_SWING, state.getSwing());
260 updateState(TadoBindingConstants.CHANNEL_ZONE_TIMER_DURATION, state.getRemainingTimerDuration());
262 updateState(TadoBindingConstants.CHANNEL_ZONE_OVERLAY_EXPIRY, state.getOverlayExpiration());
264 updateState(TadoBindingConstants.CHANNEL_ZONE_OPEN_WINDOW_DETECTED, state.getOpenWindowDetected());
266 onSuccessfulOperation();
267 } catch (IOException | ApiException e) {
268 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
269 "Could not connect to server due to " + e.getMessage());
273 updateState(TadoBindingConstants.CHANNEL_ZONE_BATTERY_LOW_ALARM, home.getBatteryLowAlarm(getZoneId()));
277 private void scheduleZoneStateUpdate() {
278 if (refreshTimer == null || refreshTimer.isCancelled()) {
279 refreshTimer = scheduler.scheduleWithFixedDelay(new Runnable() {
282 updateZoneState(false);
284 }, 5, configuration.refreshInterval, TimeUnit.SECONDS);
288 private void cancelScheduledZoneStateUpdate() {
289 if (refreshTimer != null) {
290 refreshTimer.cancel(false);
294 private void scheduleHvacChange() {
295 if (scheduledHvacChange != null) {
296 scheduledHvacChange.cancel(false);
299 scheduledHvacChange = scheduler.schedule(() -> {
301 TadoHvacChange change = this.pendingHvacChange;
302 this.pendingHvacChange = new TadoHvacChange(getThing());
304 } catch (IOException e) {
305 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
306 } catch (ApiException e) {
307 logger.warn("Could not apply HVAC change on home {} and zone {}: {}", getHomeId(), getZoneId(),
310 updateZoneState(true);
312 }, configuration.hvacChangeDebounce, TimeUnit.SECONDS);
315 private void updateStateIfNotNull(String channelID, State state) {
317 updateState(channelID, state);