]> git.basschouten.com Git - openhab-addons.git/blob
8493da617c63537046719b2186d1c239e369928a
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 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) {
114                     nhcThermostat.executeMode(((DecimalType) command).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<?>) {
132                     QuantityType<?> setpoint = ((QuantityType<?>) command).toUnit(CELSIUS);
133                     if (setpoint != null) {
134                         nhcThermostat.executeOverrule(Math.round(setpoint.floatValue() * 10), time);
135                     }
136                 } else if (command instanceof DecimalType) {
137                     BigDecimal setpoint = ((DecimalType) command).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) {
144                     int overruletime = ((DecimalType) command).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) {
248             NhcThermostat2 thermostat = (NhcThermostat2) nhcThermostat;
249             properties.put(PROPERTY_DEVICE_TYPE, thermostat.getDeviceType());
250             properties.put(PROPERTY_DEVICE_TECHNOLOGY, thermostat.getDeviceTechnology());
251             properties.put(PROPERTY_DEVICE_MODEL, thermostat.getDeviceModel());
252         }
253
254         thing.setProperties(properties);
255     }
256
257     @Override
258     public void thermostatEvent(int measured, int setpoint, int mode, int overrule, int demand) {
259         NhcThermostat nhcThermostat = this.nhcThermostat;
260         if (nhcThermostat == null) {
261             logger.debug("thermostat with ID {} not initialized", thermostatId);
262             return;
263         }
264
265         updateState(CHANNEL_MEASURED, new QuantityType<>(measured / 10.0, CELSIUS));
266
267         int overruletime = nhcThermostat.getRemainingOverruletime();
268         updateState(CHANNEL_OVERRULETIME, new DecimalType(overruletime));
269         // refresh the remaining time every minute
270         scheduleRefreshOverruletime(nhcThermostat);
271
272         // If there is an overrule temperature set, use this in the setpoint channel, otherwise use the original
273         // setpoint temperature
274         if (overruletime == 0) {
275             updateState(CHANNEL_SETPOINT, new QuantityType<>(setpoint / 10.0, CELSIUS));
276         } else {
277             updateState(CHANNEL_SETPOINT, new QuantityType<>(overrule / 10.0, CELSIUS));
278         }
279
280         updateState(CHANNEL_MODE, new DecimalType(mode));
281         updateState(CHANNEL_HEATING_MODE, new StringType(THERMOSTATMODES[mode]));
282
283         updateState(CHANNEL_DEMAND, new DecimalType(demand));
284         updateState(CHANNEL_HEATING_DEMAND, new StringType(THERMOSTATDEMAND[Math.abs(demand) <= 1 ? (demand + 1) : 0]));
285
286         updateStatus(ThingStatus.ONLINE);
287     }
288
289     /**
290      * Method to update state of overruletime channel every minute with remaining time.
291      *
292      * @param NhcThermostat object
293      *
294      */
295     private void scheduleRefreshOverruletime(NhcThermostat nhcThermostat) {
296         cancelRefreshTimer();
297
298         if (nhcThermostat.getRemainingOverruletime() == 0) {
299             return;
300         }
301
302         refreshTimer = scheduler.scheduleWithFixedDelay(() -> {
303             int remainingTime = nhcThermostat.getRemainingOverruletime();
304             updateState(CHANNEL_OVERRULETIME, new DecimalType(remainingTime));
305             if (remainingTime == 0) {
306                 cancelRefreshTimer();
307             }
308         }, 1, 1, TimeUnit.MINUTES);
309     }
310
311     private void cancelRefreshTimer() {
312         ScheduledFuture<?> timer = refreshTimer;
313         if (timer != null) {
314             timer.cancel(true);
315         }
316         refreshTimer = null;
317     }
318
319     @Override
320     public void thermostatInitialized() {
321         Bridge bridge = getBridge();
322         if ((bridge != null) && (bridge.getStatus() == ThingStatus.ONLINE)) {
323             updateStatus(ThingStatus.ONLINE);
324         }
325     }
326
327     @Override
328     public void thermostatRemoved() {
329         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
330                 "@text/offline.configuration-error.thermostatRemoved");
331     }
332
333     private void restartCommunication(NikoHomeControlCommunication nhcComm) {
334         // We lost connection but the connection object is there, so was correctly started.
335         // Try to restart communication.
336         nhcComm.scheduleRestartCommunication();
337         // If still not active, take thing offline and return.
338         if (!nhcComm.communicationActive()) {
339             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
340                     "@text/offline.communication-error");
341             return;
342         }
343         // Also put the bridge back online
344         NikoHomeControlBridgeHandler nhcBridgeHandler = getBridgeHandler();
345         if (nhcBridgeHandler != null) {
346             nhcBridgeHandler.bridgeOnline();
347         } else {
348             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
349                     "@text/offline.configuration-error.invalid-bridge-handler");
350         }
351     }
352
353     private @Nullable NikoHomeControlCommunication getCommunication(
354             @Nullable NikoHomeControlBridgeHandler nhcBridgeHandler) {
355         return nhcBridgeHandler != null ? nhcBridgeHandler.getCommunication() : null;
356     }
357
358     private @Nullable NikoHomeControlBridgeHandler getBridgeHandler() {
359         Bridge nhcBridge = getBridge();
360         return nhcBridge != null ? (NikoHomeControlBridgeHandler) nhcBridge.getHandler() : null;
361     }
362
363     @Override
364     public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
365         ThingStatus bridgeStatus = bridgeStatusInfo.getStatus();
366         if (ThingStatus.ONLINE.equals(bridgeStatus)) {
367             if (!initialized) {
368                 scheduler.submit(this::startCommunication);
369             } else {
370                 updateStatus(ThingStatus.ONLINE);
371             }
372         } else {
373             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
374         }
375     }
376 }