2 * Copyright (c) 2010-2023 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.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;
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)) {
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);
243 switch (channel.id) {
245 case CHA_ROOM_HUMIDITY:
246 case CHA_OUTSIDE_TEMP:
247 case CHA_TARGET_TEMP: {
248 state = point.getState();
251 case CHA_ROOM_AIR_QUALITY:
252 case CHA_ENERGY_SAVINGS_LEVEL: {
253 state = point.getEnum();
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);
264 case CHA_STAT_AUTO_MODE: {
265 state = OnOffType.from(point.getPresentPriority() > 13
266 || points.getPointByClass(HIE_STAT_OCC_MODE_PRESENT).asInt() == 2);
269 case CHA_STAT_OCC_MODE_PRESENT: {
270 state = OnOffType.from(point.asInt() == 3);
273 case CHA_DHW_AUTO_MODE: {
274 state = OnOffType.from(point.getPresentPriority() > 13);
277 case CHA_DHW_OUTPUT_STATE: {
278 state = OnOffType.from(point.asInt() == 2);
284 updateState(channel.id, state);
287 } catch (RdsCloudException e) {
288 logger.warn(LOG_SYSTEM_EXCEPTION, "doPollNow()", e.getClass().getName(), e.getMessage());
293 * private method: sends a new channel value to the cloud server
295 private synchronized void doHandleCommand(String channelId, Command command) {
296 RdsDataPoints points = this.points;
298 RdsCloudHandler cloud = getCloudHandler();
300 String apiKey = cloud.getApiKey();
301 String token = cloud.getToken();
303 if ((points == null || (!points.refresh(apiKey, token)))) {
304 points = fetchPoints();
307 if (points == null) {
308 throw new RdsCloudException("missing data points");
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);
323 points.setValue(apiKey, token, channel.clazz, doCommand.format("%s"));
324 debouncer.initialize(channelId);
327 case CHA_STAT_AUTO_MODE: {
329 * this command is particularly funky.. use Green Leaf = 5 to set to Auto, and
330 * use Comfort Button = 1 to set to Manual
332 if (command == OnOffType.ON) {
333 points.setValue(apiKey, token, HIE_ENERGY_SAVINGS_LEVEL, "5");
335 points.setValue(apiKey, token, HIE_STAT_CMF_BTN, "1");
337 debouncer.initialize(channelId);
340 case CHA_STAT_OCC_MODE_PRESENT: {
341 points.setValue(apiKey, token, channel.clazz, command == OnOffType.OFF ? "2" : "3");
342 debouncer.initialize(channelId);
345 case CHA_DHW_AUTO_MODE: {
346 if (command == OnOffType.ON) {
347 points.setValue(apiKey, token, channel.clazz, "0");
349 points.setValue(apiKey, token, channel.clazz,
350 Integer.toString(points.getPointByClass(channel.clazz).asInt()));
352 debouncer.initialize(channelId);
355 case CHA_DHW_OUTPUT_STATE: {
356 points.setValue(apiKey, token, channel.clazz, command == OnOffType.OFF ? "1" : "2");
357 debouncer.initialize(channelId);
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);
372 } catch (RdsCloudException e) {
373 logger.warn(LOG_SYSTEM_EXCEPTION, "doHandleCommand()", e.getClass().getName(), e.getMessage());
378 * private method: returns the cloud server handler
380 private RdsCloudHandler getCloudHandler() throws RdsCloudException {
384 if ((b = getBridge()) != null && (b.getHandler() instanceof RdsCloudHandler cloudHandler)) {
387 throw new RdsCloudException("no cloud handler found");
390 public @Nullable RdsDataPoints fetchPoints() {
391 RdsConfiguration config = this.config;
393 if (config == null) {
394 throw new RdsCloudException("missing configuration");
397 String url = String.format(URL_POINTS, config.plantId);
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)));
407 RdsCloudHandler cloud = getCloudHandler();
408 String apiKey = cloud.getApiKey();
409 String token = cloud.getToken();
411 String json = RdsDataPoints.httpGenericGetJson(apiKey, token, url);
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)));
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());
427 return this.points = null;