2 * Copyright (c) 2010-2020 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.openhab.binding.tado.internal.TadoBindingConstants;
24 import org.openhab.binding.tado.internal.TadoBindingConstants.OperationMode;
25 import org.openhab.binding.tado.internal.TadoBindingConstants.TemperatureUnit;
26 import org.openhab.binding.tado.internal.TadoBindingConstants.ZoneType;
27 import org.openhab.binding.tado.internal.TadoHvacChange;
28 import org.openhab.binding.tado.internal.adapter.TadoZoneStateAdapter;
29 import org.openhab.binding.tado.internal.api.ApiException;
30 import org.openhab.binding.tado.internal.api.client.HomeApi;
31 import org.openhab.binding.tado.internal.api.model.GenericZoneCapabilities;
32 import org.openhab.binding.tado.internal.api.model.Overlay;
33 import org.openhab.binding.tado.internal.api.model.OverlayTemplate;
34 import org.openhab.binding.tado.internal.api.model.OverlayTerminationCondition;
35 import org.openhab.binding.tado.internal.api.model.Zone;
36 import org.openhab.binding.tado.internal.api.model.ZoneState;
37 import org.openhab.binding.tado.internal.config.TadoZoneConfig;
38 import org.openhab.core.library.types.DecimalType;
39 import org.openhab.core.library.types.OnOffType;
40 import org.openhab.core.library.types.QuantityType;
41 import org.openhab.core.library.types.StringType;
42 import org.openhab.core.library.unit.ImperialUnits;
43 import org.openhab.core.library.unit.SIUnits;
44 import org.openhab.core.thing.Bridge;
45 import org.openhab.core.thing.ChannelUID;
46 import org.openhab.core.thing.Thing;
47 import org.openhab.core.thing.ThingStatus;
48 import org.openhab.core.thing.ThingStatusDetail;
49 import org.openhab.core.thing.ThingStatusInfo;
50 import org.openhab.core.types.Command;
51 import org.openhab.core.types.RefreshType;
52 import org.openhab.core.types.State;
53 import org.slf4j.Logger;
54 import org.slf4j.LoggerFactory;
57 * The {@link TadoZoneHandler} is responsible for handling commands of zones and update their state.
59 * @author Dennis Frommknecht - Initial contribution
60 * @author Andrew Fiddian-Green - Added Low Battery Alarm, A/C Power and Open Window channels
63 public class TadoZoneHandler extends BaseHomeThingHandler {
64 private Logger logger = LoggerFactory.getLogger(TadoZoneHandler.class);
66 private TadoZoneConfig configuration;
67 private ScheduledFuture<?> refreshTimer;
68 private ScheduledFuture<?> scheduledHvacChange;
69 private GenericZoneCapabilities capabilities;
70 TadoHvacChange pendingHvacChange;
72 public TadoZoneHandler(Thing thing) {
76 public long getZoneId() {
77 return this.configuration.id;
80 public int getFallbackTimerDuration() {
81 return this.configuration.fallbackTimerDuration;
84 public ZoneType getZoneType() {
85 String zoneTypeStr = this.thing.getProperties().get(TadoBindingConstants.PROPERTY_ZONE_TYPE);
86 return ZoneType.valueOf(zoneTypeStr);
89 public OverlayTerminationCondition getDefaultTerminationCondition() throws IOException, ApiException {
90 OverlayTemplate overlayTemplate = getApi().showZoneDefaultOverlay(getHomeId(), getZoneId());
91 return terminationConditionTemplateToTerminationCondition(overlayTemplate.getTerminationCondition());
94 public ZoneState getZoneState() throws IOException, ApiException {
95 HomeApi api = getApi();
96 return api != null ? api.showZoneState(getHomeId(), getZoneId()) : null;
99 public GenericZoneCapabilities getZoneCapabilities() {
100 return this.capabilities;
103 public TemperatureUnit getTemperatureUnit() {
104 return getHomeHandler().getTemperatureUnit();
107 public Overlay setOverlay(Overlay overlay) throws IOException, ApiException {
108 logger.debug("Setting overlay of home {} and zone {}", getHomeId(), getZoneId());
109 return getApi().updateZoneOverlay(getHomeId(), getZoneId(), overlay);
112 public void removeOverlay() throws IOException, ApiException {
113 logger.debug("Removing overlay of home {} and zone {}", getHomeId(), getZoneId());
114 getApi().deleteZoneOverlay(getHomeId(), getZoneId());
118 public void handleCommand(ChannelUID channelUID, Command command) {
119 String id = channelUID.getId();
121 if (command == RefreshType.REFRESH) {
122 updateZoneState(false);
127 case TadoBindingConstants.CHANNEL_ZONE_HVAC_MODE:
128 pendingHvacChange.withHvacMode(((StringType) command).toFullString());
129 scheduleHvacChange();
131 case TadoBindingConstants.CHANNEL_ZONE_TARGET_TEMPERATURE:
132 QuantityType<Temperature> state = (QuantityType<Temperature>) command;
133 QuantityType<Temperature> stateInTargetUnit = getTemperatureUnit() == TemperatureUnit.FAHRENHEIT
134 ? state.toUnit(ImperialUnits.FAHRENHEIT)
135 : state.toUnit(SIUnits.CELSIUS);
137 if (stateInTargetUnit != null) {
138 pendingHvacChange.withTemperature(stateInTargetUnit.floatValue());
139 scheduleHvacChange();
143 case TadoBindingConstants.CHANNEL_ZONE_SWING:
144 pendingHvacChange.withSwing(((OnOffType) command) == OnOffType.ON);
145 scheduleHvacChange();
147 case TadoBindingConstants.CHANNEL_ZONE_FAN_SPEED:
148 pendingHvacChange.withFanSpeed(((StringType) command).toFullString());
149 scheduleHvacChange();
151 case TadoBindingConstants.CHANNEL_ZONE_OPERATION_MODE:
152 String operationMode = ((StringType) command).toFullString();
153 pendingHvacChange.withOperationMode(OperationMode.valueOf(operationMode));
154 scheduleHvacChange();
156 case TadoBindingConstants.CHANNEL_ZONE_TIMER_DURATION:
157 pendingHvacChange.activeFor(((DecimalType) command).intValue());
158 scheduleHvacChange();
164 public void initialize() {
165 configuration = getConfigAs(TadoZoneConfig.class);
167 if (configuration.refreshInterval <= 0) {
168 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Refresh interval of zone "
169 + getZoneId() + " of home " + getHomeId() + " must be greater than zero");
171 } else if (configuration.fallbackTimerDuration <= 0) {
172 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Fallback timer duration of zone "
173 + getZoneId() + " of home " + getHomeId() + " must be greater than zero");
175 } else if (configuration.hvacChangeDebounce <= 0) {
176 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "HVAC change debounce of zone "
177 + getZoneId() + " of home " + getHomeId() + " must be greater than zero");
181 Bridge bridge = getBridge();
182 if (bridge != null) {
183 bridgeStatusChanged(bridge.getStatusInfo());
188 public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
189 if (bridgeStatusInfo.getStatus() == ThingStatus.ONLINE) {
191 Zone zoneDetails = getApi().showZoneDetails(getHomeId(), getZoneId());
192 GenericZoneCapabilities capabilities = getApi().showZoneCapabilities(getHomeId(), getZoneId());
194 if (zoneDetails == null || capabilities == null) {
195 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
196 "Can not access zone " + getZoneId() + " of home " + getHomeId());
200 updateProperty(TadoBindingConstants.PROPERTY_ZONE_NAME, zoneDetails.getName());
201 updateProperty(TadoBindingConstants.PROPERTY_ZONE_TYPE, zoneDetails.getType().name());
202 this.capabilities = capabilities;
203 } catch (IOException | ApiException e) {
204 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
205 "Could not connect to server due to " + e.getMessage());
206 cancelScheduledZoneStateUpdate();
210 scheduleZoneStateUpdate();
211 pendingHvacChange = new TadoHvacChange(getThing());
213 updateStatus(ThingStatus.ONLINE);
215 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
216 cancelScheduledZoneStateUpdate();
220 private void updateZoneState(boolean forceUpdate) {
221 TadoHomeHandler home = getHomeHandler();
223 home.updateHomeState();
226 // No update during HVAC change debounce
227 if (!forceUpdate && scheduledHvacChange != null && !scheduledHvacChange.isDone()) {
232 ZoneState zoneState = getZoneState();
234 if (zoneState == null) {
238 logger.debug("Updating state of home {} and zone {}", getHomeId(), getZoneId());
240 TadoZoneStateAdapter state = new TadoZoneStateAdapter(zoneState, getTemperatureUnit());
241 updateStateIfNotNull(TadoBindingConstants.CHANNEL_ZONE_CURRENT_TEMPERATURE, state.getInsideTemperature());
242 updateStateIfNotNull(TadoBindingConstants.CHANNEL_ZONE_HUMIDITY, state.getHumidity());
244 updateStateIfNotNull(TadoBindingConstants.CHANNEL_ZONE_HEATING_POWER, state.getHeatingPower());
245 updateStateIfNotNull(TadoBindingConstants.CHANNEL_ZONE_AC_POWER, state.getAcPower());
247 updateState(TadoBindingConstants.CHANNEL_ZONE_OPERATION_MODE, state.getOperationMode());
249 updateState(TadoBindingConstants.CHANNEL_ZONE_HVAC_MODE, state.getMode());
250 updateState(TadoBindingConstants.CHANNEL_ZONE_TARGET_TEMPERATURE, state.getTargetTemperature());
251 updateState(TadoBindingConstants.CHANNEL_ZONE_FAN_SPEED, state.getFanSpeed());
252 updateState(TadoBindingConstants.CHANNEL_ZONE_SWING, state.getSwing());
254 updateState(TadoBindingConstants.CHANNEL_ZONE_TIMER_DURATION, state.getRemainingTimerDuration());
256 updateState(TadoBindingConstants.CHANNEL_ZONE_OVERLAY_EXPIRY, state.getOverlayExpiration());
258 updateState(TadoBindingConstants.CHANNEL_ZONE_OPEN_WINDOW_DETECTED, state.getOpenWindowDetected());
260 onSuccessfulOperation();
261 } catch (IOException | ApiException e) {
262 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
263 "Could not connect to server due to " + e.getMessage());
267 updateState(TadoBindingConstants.CHANNEL_ZONE_BATTERY_LOW_ALARM, home.getBatteryLowAlarm(getZoneId()));
271 private void scheduleZoneStateUpdate() {
272 if (refreshTimer == null || refreshTimer.isCancelled()) {
273 refreshTimer = scheduler.scheduleWithFixedDelay(new Runnable() {
276 updateZoneState(false);
278 }, 5, configuration.refreshInterval, TimeUnit.SECONDS);
282 private void cancelScheduledZoneStateUpdate() {
283 if (refreshTimer != null) {
284 refreshTimer.cancel(false);
288 private void scheduleHvacChange() {
289 if (scheduledHvacChange != null) {
290 scheduledHvacChange.cancel(false);
293 scheduledHvacChange = scheduler.schedule(() -> {
295 TadoHvacChange change = this.pendingHvacChange;
296 this.pendingHvacChange = new TadoHvacChange(getThing());
298 } catch (IOException e) {
299 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
300 } catch (ApiException e) {
301 logger.warn("Could not apply HVAC change on home {} and zone {}: {}", getHomeId(), getZoneId(),
304 updateZoneState(true);
306 }, configuration.hvacChangeDebounce, TimeUnit.SECONDS);
309 private void updateStateIfNotNull(String channelID, State state) {
311 updateState(channelID, state);