]> git.basschouten.com Git - openhab-addons.git/blob
987db143a84942ab1e999377a660a54f7926330f
[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.siemensrds.internal;
14
15 import static org.openhab.binding.siemensrds.internal.RdsBindingConstants.*;
16
17 import java.io.IOException;
18 import java.util.concurrent.ScheduledFuture;
19 import java.util.concurrent.TimeUnit;
20 import java.util.concurrent.atomic.AtomicInteger;
21
22 import javax.measure.Unit;
23
24 import org.eclipse.jdt.annotation.NonNullByDefault;
25 import org.eclipse.jdt.annotation.Nullable;
26 import org.openhab.binding.siemensrds.points.BasePoint;
27 import org.openhab.core.library.types.OnOffType;
28 import org.openhab.core.library.types.QuantityType;
29 import org.openhab.core.library.types.StringType;
30 import org.openhab.core.thing.Bridge;
31 import org.openhab.core.thing.ChannelUID;
32 import org.openhab.core.thing.Thing;
33 import org.openhab.core.thing.ThingStatus;
34 import org.openhab.core.thing.ThingStatusDetail;
35 import org.openhab.core.thing.ThingStatusInfo;
36 import org.openhab.core.thing.binding.BaseThingHandler;
37 import org.openhab.core.thing.binding.BridgeHandler;
38 import org.openhab.core.types.Command;
39 import org.openhab.core.types.RefreshType;
40 import org.openhab.core.types.State;
41 import org.openhab.core.types.UnDefType;
42 import org.slf4j.Logger;
43 import org.slf4j.LoggerFactory;
44
45 import com.google.gson.JsonParseException;
46
47 /**
48  * The {@link RdsHandler} is the OpenHab Handler for Siemens RDS smart
49  * thermostats
50  *
51  * @author Andrew Fiddian-Green - Initial contribution
52  *
53  */
54 @NonNullByDefault
55 public class RdsHandler extends BaseThingHandler {
56
57     protected final Logger logger = LoggerFactory.getLogger(RdsHandler.class);
58
59     private @Nullable ScheduledFuture<?> lazyPollingScheduler = null;
60     private @Nullable ScheduledFuture<?> fastPollingScheduler = null;
61
62     private final AtomicInteger fastPollingCallsToGo = new AtomicInteger();
63
64     private RdsDebouncer debouncer = new RdsDebouncer();
65
66     private @Nullable RdsConfiguration config = null;
67
68     private @Nullable RdsDataPoints points = null;
69
70     public RdsHandler(Thing thing) {
71         super(thing);
72     }
73
74     @Override
75     public void handleCommand(ChannelUID channelUID, Command command) {
76         if (command != RefreshType.REFRESH) {
77             doHandleCommand(channelUID.getId(), command);
78         }
79         startFastPollingBurst();
80     }
81
82     @Override
83     public void initialize() {
84         updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.CONFIGURATION_PENDING);
85
86         RdsConfiguration config = this.config = getConfigAs(RdsConfiguration.class);
87
88         if (config.plantId.isEmpty()) {
89             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "missing Plant Id");
90             return;
91         }
92
93         updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.CONFIGURATION_PENDING);
94
95         try {
96             RdsCloudHandler cloud = getCloudHandler();
97
98             if (cloud.getThing().getStatus() != ThingStatus.ONLINE) {
99                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE, "cloud server offline");
100                 return;
101             }
102
103             initializePolling();
104         } catch (RdsCloudException e) {
105             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "missing cloud server handler");
106             return;
107         }
108     }
109
110     public void initializePolling() {
111         try {
112             int pollInterval = getCloudHandler().getPollInterval();
113
114             // create a "lazy" polling scheduler
115             ScheduledFuture<?> lazyPollingScheduler = this.lazyPollingScheduler;
116             if (lazyPollingScheduler == null || lazyPollingScheduler.isCancelled()) {
117                 this.lazyPollingScheduler = scheduler.scheduleWithFixedDelay(this::lazyPollingSchedulerExecute,
118                         pollInterval, pollInterval, TimeUnit.SECONDS);
119             }
120
121             // create a "fast" polling scheduler
122             fastPollingCallsToGo.set(FAST_POLL_CYCLES);
123             ScheduledFuture<?> fastPollingScheduler = this.fastPollingScheduler;
124             if (fastPollingScheduler == null || fastPollingScheduler.isCancelled()) {
125                 this.fastPollingScheduler = scheduler.scheduleWithFixedDelay(this::fastPollingSchedulerExecute,
126                         FAST_POLL_INTERVAL, FAST_POLL_INTERVAL, TimeUnit.SECONDS);
127             }
128
129             startFastPollingBurst();
130         } catch (RdsCloudException e) {
131             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
132             logger.warn(LOG_SYSTEM_EXCEPTION, "initializePolling()", e.getClass().getName(), e.getMessage());
133         }
134     }
135
136     @Override
137     public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
138         if (bridgeStatusInfo.getStatus() == ThingStatus.ONLINE) {
139             if (fastPollingScheduler == null) {
140                 initializePolling();
141             }
142         }
143     }
144
145     @Override
146     public void dispose() {
147         // clean up the lazy polling scheduler
148         ScheduledFuture<?> lazyPollingScheduler = this.lazyPollingScheduler;
149         if (lazyPollingScheduler != null && !lazyPollingScheduler.isCancelled()) {
150             lazyPollingScheduler.cancel(true);
151             this.lazyPollingScheduler = null;
152         }
153
154         // clean up the fast polling scheduler
155         ScheduledFuture<?> fastPollingScheduler = this.fastPollingScheduler;
156         if (fastPollingScheduler != null && !fastPollingScheduler.isCancelled()) {
157             fastPollingScheduler.cancel(true);
158             this.fastPollingScheduler = null;
159         }
160     }
161
162     /*
163      * private method: initiate a burst of fast polling requests
164      */
165     public void startFastPollingBurst() {
166         fastPollingCallsToGo.set(FAST_POLL_CYCLES);
167     }
168
169     /*
170      * private method: this is the callback used by the lazy polling scheduler..
171      * polls for the info for all points
172      */
173     private synchronized void lazyPollingSchedulerExecute() {
174         doPollNow();
175         if (fastPollingCallsToGo.get() > 0) {
176             fastPollingCallsToGo.decrementAndGet();
177         }
178     }
179
180     /*
181      * private method: this is the callback used by the fast polling scheduler..
182      * checks if a fast polling burst is scheduled, and if so calls
183      * lazyPollingSchedulerExecute
184      */
185     private void fastPollingSchedulerExecute() {
186         if (fastPollingCallsToGo.get() > 0) {
187             lazyPollingSchedulerExecute();
188         }
189     }
190
191     /*
192      * private method: send request to the cloud server for a new list of data point
193      * states
194      */
195     private void doPollNow() {
196         try {
197             RdsCloudHandler cloud = getCloudHandler();
198
199             if (cloud.getThing().getStatus() != ThingStatus.ONLINE) {
200                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE, "cloud server offline");
201                 return;
202             }
203
204             RdsDataPoints points = this.points;
205             if ((points == null || (!points.refresh(cloud.getApiKey(), cloud.getToken())))) {
206                 points = fetchPoints();
207             }
208
209             if (points == null) {
210                 if (getThing().getStatus() == ThingStatus.ONLINE) {
211                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "missing data points");
212                 }
213                 throw new RdsCloudException("missing data points");
214             }
215
216             if (!points.isOnline()) {
217                 if (getThing().getStatus() == ThingStatus.ONLINE) {
218                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
219                             "cloud server reports device offline");
220                 }
221                 return;
222             }
223
224             if (getThing().getStatus() != ThingStatus.ONLINE) {
225                 updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE, "server response ok");
226             }
227
228             for (ChannelMap channel : CHAN_MAP) {
229                 if (!debouncer.timeExpired(channel.id)) {
230                     continue;
231                 }
232
233                 BasePoint point;
234                 try {
235                     point = points.getPointByClass(channel.clazz);
236                 } catch (RdsCloudException e) {
237                     logger.debug("{} \"{}\" not implemented; set state to UNDEF", channel.id, channel.clazz);
238                     updateState(channel.id, UnDefType.UNDEF);
239                     continue;
240                 }
241
242                 State state = null;
243
244                 switch (channel.id) {
245                     case CHA_ROOM_TEMP:
246                     case CHA_ROOM_HUMIDITY:
247                     case CHA_OUTSIDE_TEMP:
248                     case CHA_TARGET_TEMP: {
249                         state = point.getState();
250                         break;
251                     }
252                     case CHA_ROOM_AIR_QUALITY:
253                     case CHA_ENERGY_SAVINGS_LEVEL: {
254                         state = point.getEnum();
255                         break;
256                     }
257                     case CHA_OUTPUT_STATE: {
258                         state = point.getEnum();
259                         // convert the state text "Neither" to the easier to understand word "Off"
260                         if (STATE_NEITHER.equals(state.toString())) {
261                             state = new StringType(STATE_OFF);
262                         }
263                         break;
264                     }
265                     case CHA_STAT_AUTO_MODE: {
266                         state = OnOffType.from(point.getPresentPriority() > 13
267                                 || points.getPointByClass(HIE_STAT_OCC_MODE_PRESENT).asInt() == 2);
268                         break;
269                     }
270                     case CHA_STAT_OCC_MODE_PRESENT: {
271                         state = OnOffType.from(point.asInt() == 3);
272                         break;
273                     }
274                     case CHA_DHW_AUTO_MODE: {
275                         state = OnOffType.from(point.getPresentPriority() > 13);
276                         break;
277                     }
278                     case CHA_DHW_OUTPUT_STATE: {
279                         state = OnOffType.from(point.asInt() == 2);
280                         break;
281                     }
282                 }
283
284                 if (state != null) {
285                     updateState(channel.id, state);
286                 }
287             }
288         } catch (RdsCloudException e) {
289             logger.warn(LOG_SYSTEM_EXCEPTION, "doPollNow()", e.getClass().getName(), e.getMessage());
290         }
291     }
292
293     /*
294      * private method: sends a new channel value to the cloud server
295      */
296     private synchronized void doHandleCommand(String channelId, Command command) {
297         RdsDataPoints points = this.points;
298         try {
299             RdsCloudHandler cloud = getCloudHandler();
300
301             String apiKey = cloud.getApiKey();
302             String token = cloud.getToken();
303
304             if ((points == null || (!points.refresh(apiKey, token)))) {
305                 points = fetchPoints();
306             }
307
308             if (points == null) {
309                 throw new RdsCloudException("missing data points");
310             }
311
312             for (ChannelMap channel : CHAN_MAP) {
313                 if (channelId.equals(channel.id)) {
314                     switch (channel.id) {
315                         case CHA_TARGET_TEMP: {
316                             Command doCommand = command;
317                             if (command instanceof QuantityType<?>) {
318                                 Unit<?> unit = points.getPointByClass(channel.clazz).getUnit();
319                                 QuantityType<?> temp = ((QuantityType<?>) command).toUnit(unit);
320                                 if (temp != null) {
321                                     doCommand = temp;
322                                 }
323                             }
324                             points.setValue(apiKey, token, channel.clazz, doCommand.format("%s"));
325                             debouncer.initialize(channelId);
326                             break;
327                         }
328                         case CHA_STAT_AUTO_MODE: {
329                             /*
330                              * this command is particularly funky.. use Green Leaf = 5 to set to Auto, and
331                              * use Comfort Button = 1 to set to Manual
332                              */
333                             if (command == OnOffType.ON) {
334                                 points.setValue(apiKey, token, HIE_ENERGY_SAVINGS_LEVEL, "5");
335                             } else {
336                                 points.setValue(apiKey, token, HIE_STAT_CMF_BTN, "1");
337                             }
338                             debouncer.initialize(channelId);
339                             break;
340                         }
341                         case CHA_STAT_OCC_MODE_PRESENT: {
342                             points.setValue(apiKey, token, channel.clazz, command == OnOffType.OFF ? "2" : "3");
343                             debouncer.initialize(channelId);
344                             break;
345                         }
346                         case CHA_DHW_AUTO_MODE: {
347                             if (command == OnOffType.ON) {
348                                 points.setValue(apiKey, token, channel.clazz, "0");
349                             } else {
350                                 points.setValue(apiKey, token, channel.clazz,
351                                         Integer.toString(points.getPointByClass(channel.clazz).asInt()));
352                             }
353                             debouncer.initialize(channelId);
354                             break;
355                         }
356                         case CHA_DHW_OUTPUT_STATE: {
357                             points.setValue(apiKey, token, channel.clazz, command == OnOffType.OFF ? "1" : "2");
358                             debouncer.initialize(channelId);
359                             break;
360                         }
361                         case CHA_ROOM_TEMP:
362                         case CHA_ROOM_HUMIDITY:
363                         case CHA_OUTSIDE_TEMP:
364                         case CHA_ROOM_AIR_QUALITY:
365                         case CHA_OUTPUT_STATE: {
366                             logger.debug("error: unexpected command to channel {}", channel.id);
367                             break;
368                         }
369                     }
370                     break;
371                 }
372             }
373         } catch (RdsCloudException e) {
374             logger.warn(LOG_SYSTEM_EXCEPTION, "doHandleCommand()", e.getClass().getName(), e.getMessage());
375         }
376     }
377
378     /*
379      * private method: returns the cloud server handler
380      */
381     private RdsCloudHandler getCloudHandler() throws RdsCloudException {
382         @Nullable
383         Bridge b;
384         @Nullable
385         BridgeHandler h;
386
387         if ((b = getBridge()) != null && (h = b.getHandler()) != null && h instanceof RdsCloudHandler) {
388             return (RdsCloudHandler) h;
389         }
390         throw new RdsCloudException("no cloud handler found");
391     }
392
393     public @Nullable RdsDataPoints fetchPoints() {
394         RdsConfiguration config = this.config;
395         try {
396             if (config == null) {
397                 throw new RdsCloudException("missing configuration");
398             }
399
400             String url = String.format(URL_POINTS, config.plantId);
401
402             if (logger.isTraceEnabled()) {
403                 logger.trace(LOG_HTTP_COMMAND, HTTP_GET, url.length());
404                 logger.trace(LOG_PAYLOAD_FMT, LOG_SENDING_MARK, url);
405             } else if (logger.isDebugEnabled()) {
406                 logger.debug(LOG_HTTP_COMMAND_ABR, HTTP_GET, url.length());
407                 logger.debug(LOG_PAYLOAD_FMT_ABR, LOG_SENDING_MARK, url.substring(0, Math.min(url.length(), 30)));
408             }
409
410             RdsCloudHandler cloud = getCloudHandler();
411             String apiKey = cloud.getApiKey();
412             String token = cloud.getToken();
413
414             String json = RdsDataPoints.httpGenericGetJson(apiKey, token, url);
415
416             if (logger.isTraceEnabled()) {
417                 logger.trace(LOG_CONTENT_LENGTH, LOG_RECEIVED_MSG, json.length());
418                 logger.trace(LOG_PAYLOAD_FMT, LOG_RECEIVED_MARK, json);
419             } else if (logger.isDebugEnabled()) {
420                 logger.debug(LOG_CONTENT_LENGTH_ABR, LOG_RECEIVED_MSG, json.length());
421                 logger.debug(LOG_PAYLOAD_FMT_ABR, LOG_RECEIVED_MARK, json.substring(0, Math.min(json.length(), 30)));
422             }
423
424             return this.points = RdsDataPoints.createFromJson(json);
425         } catch (RdsCloudException e) {
426             logger.warn(LOG_SYSTEM_EXCEPTION, "fetchPoints()", e.getClass().getName(), e.getMessage());
427         } catch (JsonParseException | IOException e) {
428             logger.warn(LOG_RUNTIME_EXCEPTION, "fetchPoints()", e.getClass().getName(), e.getMessage());
429         }
430         return this.points = null;
431     }
432 }