2 * Copyright (c) 2010-2024 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.siemenshvac.internal.handler;
15 import java.math.BigDecimal;
16 import java.util.Locale;
17 import java.util.concurrent.ScheduledFuture;
18 import java.util.concurrent.TimeUnit;
19 import java.util.concurrent.locks.Lock;
20 import java.util.concurrent.locks.ReentrantLock;
22 import org.eclipse.jdt.annotation.NonNullByDefault;
23 import org.eclipse.jdt.annotation.Nullable;
24 import org.openhab.binding.siemenshvac.internal.converter.ConverterException;
25 import org.openhab.binding.siemenshvac.internal.converter.ConverterFactory;
26 import org.openhab.binding.siemenshvac.internal.converter.ConverterTypeException;
27 import org.openhab.binding.siemenshvac.internal.converter.TypeConverter;
28 import org.openhab.binding.siemenshvac.internal.metadata.SiemensHvacMetadataDataPoint;
29 import org.openhab.binding.siemenshvac.internal.metadata.SiemensHvacMetadataRegistry;
30 import org.openhab.binding.siemenshvac.internal.network.SiemensHvacCallback;
31 import org.openhab.binding.siemenshvac.internal.network.SiemensHvacConnector;
32 import org.openhab.binding.siemenshvac.internal.network.SiemensHvacRequestListener.ErrorSource;
33 import org.openhab.core.library.types.DecimalType;
34 import org.openhab.core.thing.Bridge;
35 import org.openhab.core.thing.Channel;
36 import org.openhab.core.thing.ChannelUID;
37 import org.openhab.core.thing.Thing;
38 import org.openhab.core.thing.ThingStatus;
39 import org.openhab.core.thing.ThingStatusDetail;
40 import org.openhab.core.thing.ThingStatusInfo;
41 import org.openhab.core.thing.binding.BaseThingHandler;
42 import org.openhab.core.thing.binding.ThingHandlerCallback;
43 import org.openhab.core.thing.type.ChannelType;
44 import org.openhab.core.thing.type.ChannelTypeRegistry;
45 import org.openhab.core.types.Command;
46 import org.openhab.core.types.RefreshType;
47 import org.openhab.core.types.State;
48 import org.openhab.core.types.StateDescription;
49 import org.openhab.core.types.Type;
50 import org.slf4j.Logger;
51 import org.slf4j.LoggerFactory;
53 import com.google.gson.JsonObject;
56 * The {@link SiemensHvacHandler} is responsible for handling commands, which are
57 * sent to one of the channels.
59 * @author Laurent ARNAL - Initial contribution
62 public class SiemensHvacHandlerImpl extends BaseThingHandler {
64 private Lock lockObj = new ReentrantLock();
66 private final Logger logger = LoggerFactory.getLogger(SiemensHvacHandlerImpl.class);
68 private @Nullable ScheduledFuture<?> pollingJob = null;
70 private final @Nullable SiemensHvacConnector hvacConnector;
71 private final @Nullable SiemensHvacMetadataRegistry metaDataRegistry;
72 private final ChannelTypeRegistry channelTypeRegistry;
74 private long lastWrite = 0;
76 public SiemensHvacHandlerImpl(Thing thing, @Nullable SiemensHvacConnector hvacConnector,
77 @Nullable SiemensHvacMetadataRegistry metaDataRegistry, ChannelTypeRegistry channelTypeRegistry) {
80 this.hvacConnector = hvacConnector;
81 this.metaDataRegistry = metaDataRegistry;
82 this.channelTypeRegistry = channelTypeRegistry;
86 public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
90 public void initialize() {
91 updateStatus(ThingStatus.UNKNOWN);
92 pollingJob = scheduler.scheduleWithFixedDelay(this::pollingCode, 0, 5, TimeUnit.SECONDS);
96 public void dispose() {
97 ScheduledFuture<?> lcPollingJob = pollingJob;
98 if (lcPollingJob != null) {
99 lcPollingJob.cancel(true);
104 private void pollingCode() {
105 Bridge lcBridge = getBridge();
107 if (lcBridge == null) {
111 if (lcBridge.getStatus() == ThingStatus.OFFLINE) {
112 if (!ThingStatusDetail.COMMUNICATION_ERROR.equals(lcBridge.getStatusInfo().getStatusDetail())) {
113 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
118 if (lcBridge.getStatus() != ThingStatus.ONLINE) {
119 if (!ThingStatusDetail.COMMUNICATION_ERROR.equals(lcBridge.getStatusInfo().getStatusDetail())) {
120 logger.debug("Bridge is not ready, don't enter polling for now!");
125 long start = System.currentTimeMillis();
126 var chList = this.getThing().getChannels();
128 SiemensHvacConnector lcHvacConnector = hvacConnector;
129 if (lcHvacConnector != null) {
130 int previousRequestCount = lcHvacConnector.getRequestCount();
131 int previousErrorCount = lcHvacConnector.getErrorCount();
133 logger.debug("readChannels:");
134 for (Channel channel : chList) {
135 readChannel(channel);
138 logger.debug("WaitAllPendingRequest:Start waiting()");
139 lcHvacConnector.waitAllPendingRequest();
140 long end = System.currentTimeMillis();
141 logger.debug("WaitAllPendingRequest:All request done(): {}", (end - start) / 1000.00);
143 int newRequestCount = lcHvacConnector.getRequestCount();
144 int newErrorCount = lcHvacConnector.getErrorCount();
146 int requestCount = newRequestCount - previousRequestCount;
147 int errorCount = newErrorCount - previousErrorCount;
149 double errorRate = (double) errorCount / requestCount * 100.00;
151 if (errorRate > 50) {
152 SiemensHvacBridgeThingHandler bridgeHandler = (SiemensHvacBridgeThingHandler) lcBridge.getHandler();
154 if (lcHvacConnector.getErrorSource() == ErrorSource.ErrorBridge) {
155 if (bridgeHandler != null) {
156 bridgeHandler.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
157 String.format("Communication ErrorRate to gateway is too high: %f", errorRate));
159 } else if (lcHvacConnector.getErrorSource() == ErrorSource.ErrorThings) {
160 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
161 String.format("Communication ErrorRate to thing is too high: %f", errorRate));
164 updateStatus(ThingStatus.ONLINE);
166 SiemensHvacBridgeThingHandler bridgeHandler = (SiemensHvacBridgeThingHandler) lcBridge.getHandler();
168 // Automatically recover from communication errors if errorRate is ok.
169 if (bridgeHandler != null) {
170 if (ThingStatusDetail.COMMUNICATION_ERROR
171 .equals(bridgeHandler.getThing().getStatusInfo().getStatusDetail())) {
172 bridgeHandler.updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE, "");
177 lcHvacConnector.displayRequestStats();
181 private void readChannel(Channel channel) {
182 ThingHandlerCallback cb = this.getCallback();
183 boolean isLink = false;
186 isLink = cb.isChannelLinked(channel.getUID());
193 logger.debug("readChannel: {}", channel.getDescription());
195 ChannelType tp = channelTypeRegistry.getChannelType(channel.getChannelTypeUID());
201 String id = channel.getProperties().get("id");
202 String uid = channel.getUID().getId();
203 String type = tp.getItemType();
206 id = (String) channel.getConfiguration().getProperties().get("id");
210 logger.debug("pollingCode: Id is null {} ", channel);
214 logger.debug("pollingCode: type is null {} ", channel);
218 readDp(id, uid, tp, type, true);
221 public void decodeReadDp(@Nullable JsonObject response, @Nullable String uid, @Nullable String dp, ChannelType tp,
222 @Nullable String type) {
223 SiemensHvacMetadataRegistry lcMetaDataRegistry = metaDataRegistry;
224 if (lcMetaDataRegistry == null) {
228 if (response != null && response.has("Data")) {
229 JsonObject subResult = (JsonObject) response.get("Data");
231 String updateKey = "" + uid;
235 if (subResult.has("Type")) {
236 typer = subResult.get("Type").getAsString().trim();
241 TypeConverter converter = ConverterFactory.getConverter(typer);
243 Locale local = lcMetaDataRegistry.getUserLocale();
245 local = Locale.getDefault();
248 State state = converter.convertFromBinding(subResult, tp, local);
249 updateState(updateKey, state);
251 } catch (ConverterTypeException ex) {
252 logger.warn("{}, for uid : {}, please check the item type", ex.getMessage(), uid);
253 } catch (ConverterException ex) {
254 logger.warn("{}, for uid: {}, please check the item type", ex.getMessage(), uid);
260 private void readDp(String dp, String uid, ChannelType tp, String type, boolean async) {
261 SiemensHvacConnector lcHvacConnector = hvacConnector;
263 if ("-1".equals(dp)) {
270 logger.trace("Start read: {}", dp);
271 String request = "api/menutree/read_datapoint.json?Id=" + dp;
273 logger.debug("siemensHvac:ReadDp:DoRequest(): {}", request);
276 if (lcHvacConnector != null) {
277 lcHvacConnector.doRequest(request, new SiemensHvacCallback() {
280 public void execute(java.net.URI uri, int status, @Nullable Object response) {
281 // prevent async read if we just write so we have no overlaps
282 long now = System.currentTimeMillis();
283 if (now - lastWrite < 5000) {
287 logger.trace("End read: {}", dp);
289 if (response instanceof JsonObject jsonResponse) {
290 decodeReadDp(jsonResponse, uid, dp, tp, type);
296 if (lcHvacConnector != null) {
297 JsonObject js = lcHvacConnector.doRequest(request);
298 decodeReadDp(js, uid, dp, tp, type);
302 logger.trace("End read: {}", dp);
307 private void writeDp(String dp, Type dpVal, ChannelType tp, String type) {
308 SiemensHvacConnector lcHvacConnector = hvacConnector;
310 if (lcHvacConnector != null) {
311 lcHvacConnector.displayRequestStats();
314 if ("-1".equals(dp)) {
320 logger.trace("Start write: {}", dp);
321 lastWrite = System.currentTimeMillis();
323 Object valUpdate = "0";
326 TypeConverter converter = ConverterFactory.getConverter(type);
328 valUpdate = converter.convertToBinding(dpVal, tp);
329 if (valUpdate != null) {
330 String request = String.format("api/menutree/write_datapoint.json?Id=%s&Value=%s&Type=%s", dp,
333 if (lcHvacConnector != null) {
334 logger.trace("Write request for: {} ", valUpdate);
335 JsonObject response = lcHvacConnector.doRequest(request);
337 logger.trace("Write request response: {} ", response);
341 logger.debug("Failed to get converted state from datapoint '{}'", dp);
343 } catch (ConverterTypeException ex) {
344 logger.warn("{}, please check the item type and the commands in your scripts", ex.getMessage());
345 } catch (ConverterException ex) {
346 logger.warn("{}, please check the item type and the commands in your scripts", ex.getMessage());
349 logger.debug("End write: {}", dp);
354 private Command applyState(ChannelType tp, Command command) {
355 StateDescription sd = tp.getState();
356 Command result = command;
359 BigDecimal maxb = sd.getMaximum();
360 BigDecimal minb = sd.getMinimum();
361 BigDecimal step = sd.getStep();
362 boolean doMods = false;
364 if (command instanceof DecimalType decimalCommand) {
365 double v1 = decimalCommand.doubleValue();
371 if (step.doubleValue() == 0.5) {
373 } else if (step.doubleValue() == 0.1) {
375 } else if (step.doubleValue() == 0.02) {
377 } else if (step.doubleValue() == 0.01) {
385 if (minb != null && v1 < minb.floatValue()) {
387 v1 = minb.floatValue();
389 if (maxb != null && v1 > maxb.floatValue()) {
391 v1 = maxb.floatValue();
395 result = new DecimalType(v1);
403 public void handleCommand(ChannelUID channelUID, Command command) {
404 SiemensHvacMetadataRegistry lcMetaDataRegistry = metaDataRegistry;
405 logger.debug("handleCommand");
406 if (command instanceof RefreshType) {
407 var channel = this.getThing().getChannel(channelUID);
408 if (channel != null) {
409 readChannel(channel);
412 Channel channel = getThing().getChannel(channelUID);
413 if (channel == null) {
417 Command commandVar = command;
418 ChannelType tp = channelTypeRegistry.getChannelType(channel.getChannelTypeUID());
424 String type = tp.getItemType();
426 String id = channel.getProperties().get("id");
427 SiemensHvacMetadataDataPoint md = null;
430 id = (String) channel.getConfiguration().getProperties().get("id");
433 if (lcMetaDataRegistry != null) {
434 md = (SiemensHvacMetadataDataPoint) lcMetaDataRegistry.getDptMap(id);
436 id = "" + md.getId();
437 dptType = md.getDptType();
441 if (command instanceof State commandState) {
442 commandVar = applyState(tp, commandVar);
443 this.updateState(channelUID, commandState);
446 if (id != null && type != null) {
447 writeDp(id, commandVar, tp, dptType);