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.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;
45 import com.google.gson.JsonParseException;
48 * The {@link RdsHandler} is the OpenHab Handler for Siemens RDS smart
51 * @author Andrew Fiddian-Green - Initial contribution
55 public class RdsHandler extends BaseThingHandler {
57 protected final Logger logger = LoggerFactory.getLogger(RdsHandler.class);
59 private @Nullable ScheduledFuture<?> lazyPollingScheduler = null;
60 private @Nullable ScheduledFuture<?> fastPollingScheduler = null;
62 private final AtomicInteger fastPollingCallsToGo = new AtomicInteger();
64 private RdsDebouncer debouncer = new RdsDebouncer();
66 private @Nullable RdsConfiguration config = null;
68 private @Nullable RdsDataPoints points = null;
70 public RdsHandler(Thing thing) {
75 public void handleCommand(ChannelUID channelUID, Command command) {
76 if (command != RefreshType.REFRESH) {
77 doHandleCommand(channelUID.getId(), command);
79 startFastPollingBurst();
83 public void initialize() {
84 updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.CONFIGURATION_PENDING);
86 RdsConfiguration config = this.config = getConfigAs(RdsConfiguration.class);
88 if (config.plantId.isEmpty()) {
89 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "missing Plant Id");
93 updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.CONFIGURATION_PENDING);
96 RdsCloudHandler cloud = getCloudHandler();
98 if (cloud.getThing().getStatus() != ThingStatus.ONLINE) {
99 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE, "cloud server offline");
104 } catch (RdsCloudException e) {
105 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "missing cloud server handler");
110 public void initializePolling() {
112 int pollInterval = getCloudHandler().getPollInterval();
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);
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);
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());
137 public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
138 if (bridgeStatusInfo.getStatus() == ThingStatus.ONLINE) {
139 if (fastPollingScheduler == null) {
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;
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;
163 * private method: initiate a burst of fast polling requests
165 public void startFastPollingBurst() {
166 fastPollingCallsToGo.set(FAST_POLL_CYCLES);
170 * private method: this is the callback used by the lazy polling scheduler..
171 * polls for the info for all points
173 private synchronized void lazyPollingSchedulerExecute() {
175 if (fastPollingCallsToGo.get() > 0) {
176 fastPollingCallsToGo.decrementAndGet();
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
185 private void fastPollingSchedulerExecute() {
186 if (fastPollingCallsToGo.get() > 0) {
187 lazyPollingSchedulerExecute();
192 * private method: send request to the cloud server for a new list of data point
195 private void doPollNow() {
197 RdsCloudHandler cloud = getCloudHandler();
199 if (cloud.getThing().getStatus() != ThingStatus.ONLINE) {
200 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE, "cloud server offline");
204 RdsDataPoints points = this.points;
205 if ((points == null || (!points.refresh(cloud.getApiKey(), cloud.getToken())))) {
206 points = fetchPoints();
209 if (points == null) {
210 if (getThing().getStatus() == ThingStatus.ONLINE) {
211 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "missing data points");
213 throw new RdsCloudException("missing data points");
216 if (!points.isOnline()) {
217 if (getThing().getStatus() == ThingStatus.ONLINE) {
218 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
219 "cloud server reports device offline");
224 if (getThing().getStatus() != ThingStatus.ONLINE) {
225 updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE, "server response ok");
228 for (ChannelMap channel : CHAN_MAP) {
229 if (!debouncer.timeExpired(channel.id)) {
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);
244 switch (channel.id) {
246 case CHA_ROOM_HUMIDITY:
247 case CHA_OUTSIDE_TEMP:
248 case CHA_TARGET_TEMP: {
249 state = point.getState();
252 case CHA_ROOM_AIR_QUALITY:
253 case CHA_ENERGY_SAVINGS_LEVEL: {
254 state = point.getEnum();
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);
265 case CHA_STAT_AUTO_MODE: {
266 state = OnOffType.from(point.getPresentPriority() > 13
267 || points.getPointByClass(HIE_STAT_OCC_MODE_PRESENT).asInt() == 2);
270 case CHA_STAT_OCC_MODE_PRESENT: {
271 state = OnOffType.from(point.asInt() == 3);
274 case CHA_DHW_AUTO_MODE: {
275 state = OnOffType.from(point.getPresentPriority() > 13);
278 case CHA_DHW_OUTPUT_STATE: {
279 state = OnOffType.from(point.asInt() == 2);
285 updateState(channel.id, state);
288 } catch (RdsCloudException e) {
289 logger.warn(LOG_SYSTEM_EXCEPTION, "doPollNow()", e.getClass().getName(), e.getMessage());
294 * private method: sends a new channel value to the cloud server
296 private synchronized void doHandleCommand(String channelId, Command command) {
297 RdsDataPoints points = this.points;
299 RdsCloudHandler cloud = getCloudHandler();
301 String apiKey = cloud.getApiKey();
302 String token = cloud.getToken();
304 if ((points == null || (!points.refresh(apiKey, token)))) {
305 points = fetchPoints();
308 if (points == null) {
309 throw new RdsCloudException("missing data points");
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);
324 points.setValue(apiKey, token, channel.clazz, doCommand.format("%s"));
325 debouncer.initialize(channelId);
328 case CHA_STAT_AUTO_MODE: {
330 * this command is particularly funky.. use Green Leaf = 5 to set to Auto, and
331 * use Comfort Button = 1 to set to Manual
333 if (command == OnOffType.ON) {
334 points.setValue(apiKey, token, HIE_ENERGY_SAVINGS_LEVEL, "5");
336 points.setValue(apiKey, token, HIE_STAT_CMF_BTN, "1");
338 debouncer.initialize(channelId);
341 case CHA_STAT_OCC_MODE_PRESENT: {
342 points.setValue(apiKey, token, channel.clazz, command == OnOffType.OFF ? "2" : "3");
343 debouncer.initialize(channelId);
346 case CHA_DHW_AUTO_MODE: {
347 if (command == OnOffType.ON) {
348 points.setValue(apiKey, token, channel.clazz, "0");
350 points.setValue(apiKey, token, channel.clazz,
351 Integer.toString(points.getPointByClass(channel.clazz).asInt()));
353 debouncer.initialize(channelId);
356 case CHA_DHW_OUTPUT_STATE: {
357 points.setValue(apiKey, token, channel.clazz, command == OnOffType.OFF ? "1" : "2");
358 debouncer.initialize(channelId);
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);
373 } catch (RdsCloudException e) {
374 logger.warn(LOG_SYSTEM_EXCEPTION, "doHandleCommand()", e.getClass().getName(), e.getMessage());
379 * private method: returns the cloud server handler
381 private RdsCloudHandler getCloudHandler() throws RdsCloudException {
387 if ((b = getBridge()) != null && (h = b.getHandler()) != null && h instanceof RdsCloudHandler) {
388 return (RdsCloudHandler) h;
390 throw new RdsCloudException("no cloud handler found");
393 public @Nullable RdsDataPoints fetchPoints() {
394 RdsConfiguration config = this.config;
396 if (config == null) {
397 throw new RdsCloudException("missing configuration");
400 String url = String.format(URL_POINTS, config.plantId);
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)));
410 RdsCloudHandler cloud = getCloudHandler();
411 String apiKey = cloud.getApiKey();
412 String token = cloud.getToken();
414 String json = RdsDataPoints.httpGenericGetJson(apiKey, token, url);
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)));
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());
430 return this.points = null;