2 * Copyright (c) 2010-2021 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.siemensrds.internal;
15 import static org.openhab.binding.siemensrds.internal.RdsBindingConstants.*;
17 import java.io.IOException;
18 import java.util.concurrent.ScheduledFuture;
19 import java.util.concurrent.TimeUnit;
20 import java.util.concurrent.atomic.AtomicInteger;
22 import javax.measure.Unit;
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;
44 import com.google.gson.JsonParseException;
47 * The {@link RdsHandler} is the OpenHab Handler for Siemens RDS smart
50 * @author Andrew Fiddian-Green - Initial contribution
54 public class RdsHandler extends BaseThingHandler {
56 protected final Logger logger = LoggerFactory.getLogger(RdsHandler.class);
58 private @Nullable ScheduledFuture<?> lazyPollingScheduler = null;
59 private @Nullable ScheduledFuture<?> fastPollingScheduler = null;
61 private final AtomicInteger fastPollingCallsToGo = new AtomicInteger();
63 private RdsDebouncer debouncer = new RdsDebouncer();
65 private @Nullable RdsConfiguration config = null;
67 private @Nullable RdsDataPoints points = null;
69 public RdsHandler(Thing thing) {
74 public void handleCommand(ChannelUID channelUID, Command command) {
75 if (command != RefreshType.REFRESH) {
76 doHandleCommand(channelUID.getId(), command);
78 startFastPollingBurst();
82 public void initialize() {
83 updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.CONFIGURATION_PENDING);
85 RdsConfiguration config = this.config = getConfigAs(RdsConfiguration.class);
87 if (config.plantId.isEmpty()) {
88 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "missing Plant Id");
92 updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.CONFIGURATION_PENDING);
95 RdsCloudHandler cloud = getCloudHandler();
97 if (cloud.getThing().getStatus() != ThingStatus.ONLINE) {
98 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE, "cloud server offline");
103 } catch (RdsCloudException e) {
104 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "missing cloud server handler");
109 public void initializePolling() {
111 int pollInterval = getCloudHandler().getPollInterval();
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);
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);
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());
136 public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
137 if (bridgeStatusInfo.getStatus() == ThingStatus.ONLINE) {
138 if (fastPollingScheduler == null) {
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;
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;
162 * private method: initiate a burst of fast polling requests
164 public void startFastPollingBurst() {
165 fastPollingCallsToGo.set(FAST_POLL_CYCLES);
169 * private method: this is the callback used by the lazy polling scheduler..
170 * polls for the info for all points
172 private synchronized void lazyPollingSchedulerExecute() {
174 if (fastPollingCallsToGo.get() > 0) {
175 fastPollingCallsToGo.decrementAndGet();
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
184 private void fastPollingSchedulerExecute() {
185 if (fastPollingCallsToGo.get() > 0) {
186 lazyPollingSchedulerExecute();
191 * private method: send request to the cloud server for a new list of data point
194 private void doPollNow() {
196 RdsCloudHandler cloud = getCloudHandler();
198 if (cloud.getThing().getStatus() != ThingStatus.ONLINE) {
199 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE, "cloud server offline");
203 RdsDataPoints points = this.points;
204 if ((points == null || (!points.refresh(cloud.getApiKey(), cloud.getToken())))) {
205 points = fetchPoints();
208 if (points == null) {
209 if (getThing().getStatus() == ThingStatus.ONLINE) {
210 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "missing data points");
212 throw new RdsCloudException("missing data points");
215 if (!points.isOnline()) {
216 if (getThing().getStatus() == ThingStatus.ONLINE) {
217 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
218 "cloud server reports device offline");
223 if (getThing().getStatus() != ThingStatus.ONLINE) {
224 updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE, "server response ok");
227 for (ChannelMap channel : CHAN_MAP) {
228 if (!debouncer.timeExpired(channel.id)) {
232 BasePoint point = points.getPointByClass(channel.clazz);
235 switch (channel.id) {
237 case CHA_ROOM_HUMIDITY:
238 case CHA_OUTSIDE_TEMP:
239 case CHA_TARGET_TEMP: {
240 state = point.getState();
243 case CHA_ROOM_AIR_QUALITY:
244 case CHA_ENERGY_SAVINGS_LEVEL: {
245 state = point.getEnum();
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);
256 case CHA_STAT_AUTO_MODE: {
257 state = OnOffType.from(point.getPresentPriority() > 13
258 || points.getPointByClass(HIE_STAT_OCC_MODE_PRESENT).asInt() == 2);
261 case CHA_STAT_OCC_MODE_PRESENT: {
262 state = OnOffType.from(point.asInt() == 3);
265 case CHA_DHW_AUTO_MODE: {
266 state = OnOffType.from(point.getPresentPriority() > 13);
269 case CHA_DHW_OUTPUT_STATE: {
270 state = OnOffType.from(point.asInt() == 2);
276 updateState(channel.id, state);
279 } catch (RdsCloudException e) {
280 logger.warn(LOG_SYSTEM_EXCEPTION, "doPollNow()", e.getClass().getName(), e.getMessage());
285 * private method: sends a new channel value to the cloud server
287 private synchronized void doHandleCommand(String channelId, Command command) {
288 RdsDataPoints points = this.points;
290 RdsCloudHandler cloud = getCloudHandler();
292 String apiKey = cloud.getApiKey();
293 String token = cloud.getToken();
295 if ((points == null || (!points.refresh(apiKey, token)))) {
296 points = fetchPoints();
299 if (points == null) {
300 throw new RdsCloudException("missing data points");
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);
315 points.setValue(apiKey, token, channel.clazz, doCommand.format("%s"));
316 debouncer.initialize(channelId);
319 case CHA_STAT_AUTO_MODE: {
321 * this command is particularly funky.. use Green Leaf = 5 to set to Auto, and
322 * use Comfort Button = 1 to set to Manual
324 if (command == OnOffType.ON) {
325 points.setValue(apiKey, token, HIE_ENERGY_SAVINGS_LEVEL, "5");
327 points.setValue(apiKey, token, HIE_STAT_CMF_BTN, "1");
329 debouncer.initialize(channelId);
332 case CHA_STAT_OCC_MODE_PRESENT: {
333 points.setValue(apiKey, token, channel.clazz, command == OnOffType.OFF ? "2" : "3");
334 debouncer.initialize(channelId);
337 case CHA_DHW_AUTO_MODE: {
338 if (command == OnOffType.ON) {
339 points.setValue(apiKey, token, channel.clazz, "0");
341 points.setValue(apiKey, token, channel.clazz,
342 Integer.toString(points.getPointByClass(channel.clazz).asInt()));
344 debouncer.initialize(channelId);
347 case CHA_DHW_OUTPUT_STATE: {
348 points.setValue(apiKey, token, channel.clazz, command == OnOffType.OFF ? "1" : "2");
349 debouncer.initialize(channelId);
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);
364 } catch (RdsCloudException e) {
365 logger.warn(LOG_SYSTEM_EXCEPTION, "doHandleCommand()", e.getClass().getName(), e.getMessage());
370 * private method: returns the cloud server handler
372 private RdsCloudHandler getCloudHandler() throws RdsCloudException {
378 if ((b = getBridge()) != null && (h = b.getHandler()) != null && h instanceof RdsCloudHandler) {
379 return (RdsCloudHandler) h;
381 throw new RdsCloudException("no cloud handler found");
384 public @Nullable RdsDataPoints fetchPoints() {
385 RdsConfiguration config = this.config;
387 if (config == null) {
388 throw new RdsCloudException("missing configuration");
391 String url = String.format(URL_POINTS, config.plantId);
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)));
401 RdsCloudHandler cloud = getCloudHandler();
402 String apiKey = cloud.getApiKey();
403 String token = cloud.getToken();
405 String json = RdsDataPoints.httpGenericGetJson(apiKey, token, url);
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)));
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());
421 return this.points = null;