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