]> git.basschouten.com Git - openhab-addons.git/blob
96ee36cb8cc5e4b0b9c66e3a53612709d0a80fd4
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2024 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.nikohomecontrol.internal.handler;
14
15 import static org.openhab.binding.nikohomecontrol.internal.NikoHomeControlBindingConstants.*;
16 import static org.openhab.binding.nikohomecontrol.internal.protocol.NikoHomeControlConstants.*;
17 import static org.openhab.core.library.unit.SIUnits.CELSIUS;
18 import static org.openhab.core.types.RefreshType.REFRESH;
19
20 import java.math.BigDecimal;
21 import java.util.HashMap;
22 import java.util.Map;
23 import java.util.concurrent.ScheduledFuture;
24 import java.util.concurrent.TimeUnit;
25
26 import org.eclipse.jdt.annotation.NonNullByDefault;
27 import org.eclipse.jdt.annotation.Nullable;
28 import org.openhab.binding.nikohomecontrol.internal.protocol.NhcThermostat;
29 import org.openhab.binding.nikohomecontrol.internal.protocol.NhcThermostatEvent;
30 import org.openhab.binding.nikohomecontrol.internal.protocol.NikoHomeControlCommunication;
31 import org.openhab.binding.nikohomecontrol.internal.protocol.nhc2.NhcThermostat2;
32 import org.openhab.core.library.types.DecimalType;
33 import org.openhab.core.library.types.QuantityType;
34 import org.openhab.core.library.types.StringType;
35 import org.openhab.core.thing.Bridge;
36 import org.openhab.core.thing.ChannelUID;
37 import org.openhab.core.thing.Thing;
38 import org.openhab.core.thing.ThingStatus;
39 import org.openhab.core.thing.ThingStatusDetail;
40 import org.openhab.core.thing.ThingStatusInfo;
41 import org.openhab.core.thing.binding.BaseThingHandler;
42 import org.openhab.core.types.Command;
43 import org.slf4j.Logger;
44 import org.slf4j.LoggerFactory;
45
46 /**
47  * The {@link NikoHomeControlThermostatHandler} is responsible for handling commands, which are
48  * sent to one of the channels.
49  *
50  * @author Mark Herwege - Initial Contribution
51  */
52 @NonNullByDefault
53 public class NikoHomeControlThermostatHandler extends BaseThingHandler implements NhcThermostatEvent {
54
55     private final Logger logger = LoggerFactory.getLogger(NikoHomeControlThermostatHandler.class);
56
57     private volatile @Nullable NhcThermostat nhcThermostat;
58
59     private volatile boolean initialized = false;
60
61     private String thermostatId = "";
62     private int overruleTime;
63
64     private volatile @Nullable ScheduledFuture<?> refreshTimer; // used to refresh the remaining overrule time every
65                                                                 // minute
66
67     public NikoHomeControlThermostatHandler(Thing thing) {
68         super(thing);
69     }
70
71     @Override
72     public void handleCommand(ChannelUID channelUID, Command command) {
73         NikoHomeControlCommunication nhcComm = getCommunication(getBridgeHandler());
74         if (nhcComm == null) {
75             logger.debug("communication not up yet, cannot handle command {} for {}", command, channelUID);
76             return;
77         }
78
79         // This can be expensive, therefore do it in a job.
80         scheduler.submit(() -> {
81             if (!nhcComm.communicationActive()) {
82                 restartCommunication(nhcComm);
83             }
84
85             if (nhcComm.communicationActive()) {
86                 handleCommandSelection(channelUID, command);
87             }
88         });
89     }
90
91     private void handleCommandSelection(ChannelUID channelUID, Command command) {
92         NhcThermostat nhcThermostat = this.nhcThermostat;
93         if (nhcThermostat == null) {
94             logger.debug("thermostat with ID {} not initialized", thermostatId);
95             return;
96         }
97
98         logger.debug("handle command {} for {}", command, channelUID);
99
100         if (REFRESH.equals(command)) {
101             thermostatEvent(nhcThermostat.getMeasured(), nhcThermostat.getSetpoint(), nhcThermostat.getMode(),
102                     nhcThermostat.getOverrule(), nhcThermostat.getDemand());
103             return;
104         }
105
106         switch (channelUID.getId()) {
107             case CHANNEL_MEASURED:
108             case CHANNEL_DEMAND:
109             case CHANNEL_HEATING_DEMAND:
110                 updateStatus(ThingStatus.ONLINE);
111                 break;
112             case CHANNEL_MODE:
113                 if (command instanceof DecimalType decimalCommand) {
114                     nhcThermostat.executeMode(decimalCommand.intValue());
115                 }
116                 updateStatus(ThingStatus.ONLINE);
117                 break;
118             case CHANNEL_HEATING_MODE:
119                 if (command instanceof StringType) {
120                     nhcThermostat.executeMode(command.toString());
121                 }
122                 updateStatus(ThingStatus.ONLINE);
123                 break;
124             case CHANNEL_SETPOINT:
125                 // Always set the new setpoint temperature as an overrule
126                 // If no overrule time is given yet, set the overrule time to the configuration parameter
127                 int time = nhcThermostat.getOverruletime();
128                 if (time <= 0) {
129                     time = overruleTime;
130                 }
131                 if (command instanceof QuantityType<?> quantityCommand) {
132                     QuantityType<?> setpoint = quantityCommand.toUnit(CELSIUS);
133                     if (setpoint != null) {
134                         nhcThermostat.executeOverrule(Math.round(setpoint.floatValue() * 10), time);
135                     }
136                 } else if (command instanceof DecimalType decimalCommand) {
137                     BigDecimal setpoint = decimalCommand.toBigDecimal();
138                     nhcThermostat.executeOverrule(Math.round(setpoint.floatValue() * 10), time);
139                 }
140                 updateStatus(ThingStatus.ONLINE);
141                 break;
142             case CHANNEL_OVERRULETIME:
143                 if (command instanceof DecimalType decimalCommand) {
144                     int overruletime = decimalCommand.intValue();
145                     int overrule = nhcThermostat.getOverrule();
146                     if (overruletime <= 0) {
147                         overruletime = 0;
148                         overrule = 0;
149                     }
150                     nhcThermostat.executeOverrule(overrule, overruletime);
151                 }
152                 updateStatus(ThingStatus.ONLINE);
153                 break;
154             default:
155                 logger.debug("unexpected command for channel {}", channelUID.getId());
156         }
157     }
158
159     @Override
160     public void initialize() {
161         initialized = false;
162
163         NikoHomeControlThermostatConfig config = getConfig().as(NikoHomeControlThermostatConfig.class);
164
165         thermostatId = config.thermostatId;
166         overruleTime = config.overruleTime;
167
168         NikoHomeControlBridgeHandler bridgeHandler = getBridgeHandler();
169         if (bridgeHandler == null) {
170             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
171                     "@text/offline.configuration-error.invalid-bridge-handler");
172             return;
173         }
174
175         updateStatus(ThingStatus.UNKNOWN);
176
177         Bridge bridge = getBridge();
178         if ((bridge != null) && ThingStatus.ONLINE.equals(bridge.getStatus())) {
179             // We need to do this in a separate thread because we may have to wait for the
180             // communication to become active
181             scheduler.submit(this::startCommunication);
182         }
183     }
184
185     private synchronized void startCommunication() {
186         NikoHomeControlCommunication nhcComm = getCommunication(getBridgeHandler());
187
188         if (nhcComm == null) {
189             return;
190         }
191
192         if (!nhcComm.communicationActive()) {
193             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
194                     "@text/offline.communication-error");
195             return;
196         }
197
198         NhcThermostat nhcThermostat = nhcComm.getThermostats().get(thermostatId);
199         if (nhcThermostat == null) {
200             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
201                     "@text/offline.configuration-error.thermostatId");
202             return;
203         }
204
205         nhcThermostat.setEventHandler(this);
206
207         updateProperties(nhcThermostat);
208
209         String thermostatLocation = nhcThermostat.getLocation();
210         if (thing.getLocation() == null) {
211             thing.setLocation(thermostatLocation);
212         }
213
214         this.nhcThermostat = nhcThermostat;
215
216         logger.debug("thermostat intialized {}", thermostatId);
217
218         Bridge bridge = getBridge();
219         if ((bridge != null) && (bridge.getStatus() == ThingStatus.ONLINE)) {
220             updateStatus(ThingStatus.ONLINE);
221         } else {
222             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
223         }
224
225         thermostatEvent(nhcThermostat.getMeasured(), nhcThermostat.getSetpoint(), nhcThermostat.getMode(),
226                 nhcThermostat.getOverrule(), nhcThermostat.getDemand());
227
228         initialized = true;
229     }
230
231     @Override
232     public void dispose() {
233         NikoHomeControlCommunication nhcComm = getCommunication(getBridgeHandler());
234         if (nhcComm != null) {
235             NhcThermostat thermostat = nhcComm.getThermostats().get(thermostatId);
236             if (thermostat != null) {
237                 thermostat.unsetEventHandler();
238             }
239         }
240         nhcThermostat = null;
241         super.dispose();
242     }
243
244     private void updateProperties(NhcThermostat nhcThermostat) {
245         Map<String, String> properties = new HashMap<>();
246
247         if (nhcThermostat instanceof NhcThermostat2 thermostat) {
248             properties.put(PROPERTY_DEVICE_TYPE, thermostat.getDeviceType());
249             properties.put(PROPERTY_DEVICE_TECHNOLOGY, thermostat.getDeviceTechnology());
250             properties.put(PROPERTY_DEVICE_MODEL, thermostat.getDeviceModel());
251         }
252
253         thing.setProperties(properties);
254     }
255
256     @Override
257     public void thermostatEvent(int measured, int setpoint, int mode, int overrule, int demand) {
258         NhcThermostat nhcThermostat = this.nhcThermostat;
259         if (nhcThermostat == null) {
260             logger.debug("thermostat with ID {} not initialized", thermostatId);
261             return;
262         }
263
264         updateState(CHANNEL_MEASURED, new QuantityType<>(measured / 10.0, CELSIUS));
265
266         int overruletime = nhcThermostat.getRemainingOverruletime();
267         updateState(CHANNEL_OVERRULETIME, new DecimalType(overruletime));
268         // refresh the remaining time every minute
269         scheduleRefreshOverruletime(nhcThermostat);
270
271         // If there is an overrule temperature set, use this in the setpoint channel, otherwise use the original
272         // setpoint temperature
273         if (overruletime == 0) {
274             updateState(CHANNEL_SETPOINT, new QuantityType<>(setpoint / 10.0, CELSIUS));
275         } else {
276             updateState(CHANNEL_SETPOINT, new QuantityType<>(overrule / 10.0, CELSIUS));
277         }
278
279         updateState(CHANNEL_MODE, new DecimalType(mode));
280         updateState(CHANNEL_HEATING_MODE, new StringType(THERMOSTATMODES[mode]));
281
282         updateState(CHANNEL_DEMAND, new DecimalType(demand));
283         updateState(CHANNEL_HEATING_DEMAND, new StringType(THERMOSTATDEMAND[Math.abs(demand) <= 1 ? (demand + 1) : 0]));
284
285         updateStatus(ThingStatus.ONLINE);
286     }
287
288     /**
289      * Method to update state of overruletime channel every minute with remaining time.
290      *
291      * @param NhcThermostat object
292      *
293      */
294     private void scheduleRefreshOverruletime(NhcThermostat nhcThermostat) {
295         cancelRefreshTimer();
296
297         if (nhcThermostat.getRemainingOverruletime() == 0) {
298             return;
299         }
300
301         refreshTimer = scheduler.scheduleWithFixedDelay(() -> {
302             int remainingTime = nhcThermostat.getRemainingOverruletime();
303             updateState(CHANNEL_OVERRULETIME, new DecimalType(remainingTime));
304             if (remainingTime == 0) {
305                 cancelRefreshTimer();
306             }
307         }, 1, 1, TimeUnit.MINUTES);
308     }
309
310     private void cancelRefreshTimer() {
311         ScheduledFuture<?> timer = refreshTimer;
312         if (timer != null) {
313             timer.cancel(true);
314         }
315         refreshTimer = null;
316     }
317
318     @Override
319     public void thermostatInitialized() {
320         Bridge bridge = getBridge();
321         if ((bridge != null) && (bridge.getStatus() == ThingStatus.ONLINE)) {
322             updateStatus(ThingStatus.ONLINE);
323         }
324     }
325
326     @Override
327     public void thermostatRemoved() {
328         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
329                 "@text/offline.configuration-error.thermostatRemoved");
330     }
331
332     private void restartCommunication(NikoHomeControlCommunication nhcComm) {
333         // We lost connection but the connection object is there, so was correctly started.
334         // Try to restart communication.
335         nhcComm.scheduleRestartCommunication();
336         // If still not active, take thing offline and return.
337         if (!nhcComm.communicationActive()) {
338             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
339                     "@text/offline.communication-error");
340             return;
341         }
342         // Also put the bridge back online
343         NikoHomeControlBridgeHandler nhcBridgeHandler = getBridgeHandler();
344         if (nhcBridgeHandler != null) {
345             nhcBridgeHandler.bridgeOnline();
346         } else {
347             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
348                     "@text/offline.configuration-error.invalid-bridge-handler");
349         }
350     }
351
352     private @Nullable NikoHomeControlCommunication getCommunication(
353             @Nullable NikoHomeControlBridgeHandler nhcBridgeHandler) {
354         return nhcBridgeHandler != null ? nhcBridgeHandler.getCommunication() : null;
355     }
356
357     private @Nullable NikoHomeControlBridgeHandler getBridgeHandler() {
358         Bridge nhcBridge = getBridge();
359         return nhcBridge != null ? (NikoHomeControlBridgeHandler) nhcBridge.getHandler() : null;
360     }
361
362     @Override
363     public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
364         ThingStatus bridgeStatus = bridgeStatusInfo.getStatus();
365         if (ThingStatus.ONLINE.equals(bridgeStatus)) {
366             if (!initialized) {
367                 scheduler.submit(this::startCommunication);
368             } else {
369                 updateStatus(ThingStatus.ONLINE);
370             }
371         } else {
372             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
373         }
374     }
375 }