]> git.basschouten.com Git - openhab-addons.git/blob
51756a38e736e506d0840bf7b1f4d5c8fd3d68aa
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2022 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.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 = points.getPointByClass(channel.clazz);
233                 State state = null;
234
235                 switch (channel.id) {
236                     case CHA_ROOM_TEMP:
237                     case CHA_ROOM_HUMIDITY:
238                     case CHA_OUTSIDE_TEMP:
239                     case CHA_TARGET_TEMP: {
240                         state = point.getState();
241                         break;
242                     }
243                     case CHA_ROOM_AIR_QUALITY:
244                     case CHA_ENERGY_SAVINGS_LEVEL: {
245                         state = point.getEnum();
246                         break;
247                     }
248                     case CHA_OUTPUT_STATE: {
249                         state = point.getEnum();
250                         // convert the state text "Neither" to the easier to understand word "Off"
251                         if (STATE_NEITHER.equals(state.toString())) {
252                             state = new StringType(STATE_OFF);
253                         }
254                         break;
255                     }
256                     case CHA_STAT_AUTO_MODE: {
257                         state = OnOffType.from(point.getPresentPriority() > 13
258                                 || points.getPointByClass(HIE_STAT_OCC_MODE_PRESENT).asInt() == 2);
259                         break;
260                     }
261                     case CHA_STAT_OCC_MODE_PRESENT: {
262                         state = OnOffType.from(point.asInt() == 3);
263                         break;
264                     }
265                     case CHA_DHW_AUTO_MODE: {
266                         state = OnOffType.from(point.getPresentPriority() > 13);
267                         break;
268                     }
269                     case CHA_DHW_OUTPUT_STATE: {
270                         state = OnOffType.from(point.asInt() == 2);
271                         break;
272                     }
273                 }
274
275                 if (state != null) {
276                     updateState(channel.id, state);
277                 }
278             }
279         } catch (RdsCloudException e) {
280             logger.warn(LOG_SYSTEM_EXCEPTION, "doPollNow()", e.getClass().getName(), e.getMessage());
281         }
282     }
283
284     /*
285      * private method: sends a new channel value to the cloud server
286      */
287     private synchronized void doHandleCommand(String channelId, Command command) {
288         RdsDataPoints points = this.points;
289         try {
290             RdsCloudHandler cloud = getCloudHandler();
291
292             String apiKey = cloud.getApiKey();
293             String token = cloud.getToken();
294
295             if ((points == null || (!points.refresh(apiKey, token)))) {
296                 points = fetchPoints();
297             }
298
299             if (points == null) {
300                 throw new RdsCloudException("missing data points");
301             }
302
303             for (ChannelMap channel : CHAN_MAP) {
304                 if (channelId.equals(channel.id)) {
305                     switch (channel.id) {
306                         case CHA_TARGET_TEMP: {
307                             Command doCommand = command;
308                             if (command instanceof QuantityType<?>) {
309                                 Unit<?> unit = points.getPointByClass(channel.clazz).getUnit();
310                                 QuantityType<?> temp = ((QuantityType<?>) command).toUnit(unit);
311                                 if (temp != null) {
312                                     doCommand = temp;
313                                 }
314                             }
315                             points.setValue(apiKey, token, channel.clazz, doCommand.format("%s"));
316                             debouncer.initialize(channelId);
317                             break;
318                         }
319                         case CHA_STAT_AUTO_MODE: {
320                             /*
321                              * this command is particularly funky.. use Green Leaf = 5 to set to Auto, and
322                              * use Comfort Button = 1 to set to Manual
323                              */
324                             if (command == OnOffType.ON) {
325                                 points.setValue(apiKey, token, HIE_ENERGY_SAVINGS_LEVEL, "5");
326                             } else {
327                                 points.setValue(apiKey, token, HIE_STAT_CMF_BTN, "1");
328                             }
329                             debouncer.initialize(channelId);
330                             break;
331                         }
332                         case CHA_STAT_OCC_MODE_PRESENT: {
333                             points.setValue(apiKey, token, channel.clazz, command == OnOffType.OFF ? "2" : "3");
334                             debouncer.initialize(channelId);
335                             break;
336                         }
337                         case CHA_DHW_AUTO_MODE: {
338                             if (command == OnOffType.ON) {
339                                 points.setValue(apiKey, token, channel.clazz, "0");
340                             } else {
341                                 points.setValue(apiKey, token, channel.clazz,
342                                         Integer.toString(points.getPointByClass(channel.clazz).asInt()));
343                             }
344                             debouncer.initialize(channelId);
345                             break;
346                         }
347                         case CHA_DHW_OUTPUT_STATE: {
348                             points.setValue(apiKey, token, channel.clazz, command == OnOffType.OFF ? "1" : "2");
349                             debouncer.initialize(channelId);
350                             break;
351                         }
352                         case CHA_ROOM_TEMP:
353                         case CHA_ROOM_HUMIDITY:
354                         case CHA_OUTSIDE_TEMP:
355                         case CHA_ROOM_AIR_QUALITY:
356                         case CHA_OUTPUT_STATE: {
357                             logger.debug("error: unexpected command to channel {}", channel.id);
358                             break;
359                         }
360                     }
361                     break;
362                 }
363             }
364         } catch (RdsCloudException e) {
365             logger.warn(LOG_SYSTEM_EXCEPTION, "doHandleCommand()", e.getClass().getName(), e.getMessage());
366         }
367     }
368
369     /*
370      * private method: returns the cloud server handler
371      */
372     private RdsCloudHandler getCloudHandler() throws RdsCloudException {
373         @Nullable
374         Bridge b;
375         @Nullable
376         BridgeHandler h;
377
378         if ((b = getBridge()) != null && (h = b.getHandler()) != null && h instanceof RdsCloudHandler) {
379             return (RdsCloudHandler) h;
380         }
381         throw new RdsCloudException("no cloud handler found");
382     }
383
384     public @Nullable RdsDataPoints fetchPoints() {
385         RdsConfiguration config = this.config;
386         try {
387             if (config == null) {
388                 throw new RdsCloudException("missing configuration");
389             }
390
391             String url = String.format(URL_POINTS, config.plantId);
392
393             if (logger.isTraceEnabled()) {
394                 logger.trace(LOG_HTTP_COMMAND, HTTP_GET, url.length());
395                 logger.trace(LOG_PAYLOAD_FMT, LOG_SENDING_MARK, url);
396             } else if (logger.isDebugEnabled()) {
397                 logger.debug(LOG_HTTP_COMMAND_ABR, HTTP_GET, url.length());
398                 logger.debug(LOG_PAYLOAD_FMT_ABR, LOG_SENDING_MARK, url.substring(0, Math.min(url.length(), 30)));
399             }
400
401             RdsCloudHandler cloud = getCloudHandler();
402             String apiKey = cloud.getApiKey();
403             String token = cloud.getToken();
404
405             String json = RdsDataPoints.httpGenericGetJson(apiKey, token, url);
406
407             if (logger.isTraceEnabled()) {
408                 logger.trace(LOG_CONTENT_LENGTH, LOG_RECEIVED_MSG, json.length());
409                 logger.trace(LOG_PAYLOAD_FMT, LOG_RECEIVED_MARK, json);
410             } else if (logger.isDebugEnabled()) {
411                 logger.debug(LOG_CONTENT_LENGTH_ABR, LOG_RECEIVED_MSG, json.length());
412                 logger.debug(LOG_PAYLOAD_FMT_ABR, LOG_RECEIVED_MARK, json.substring(0, Math.min(json.length(), 30)));
413             }
414
415             return this.points = RdsDataPoints.createFromJson(json);
416         } catch (RdsCloudException e) {
417             logger.warn(LOG_SYSTEM_EXCEPTION, "fetchPoints()", e.getClass().getName(), e.getMessage());
418         } catch (JsonParseException | IOException e) {
419             logger.warn(LOG_RUNTIME_EXCEPTION, "fetchPoints()", e.getClass().getName(), e.getMessage());
420         }
421         return this.points = null;
422     }
423 }