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.km200.internal.handler;
15 import static org.openhab.binding.km200.internal.KM200BindingConstants.*;
17 import java.math.BigDecimal;
18 import java.net.InetAddress;
19 import java.net.UnknownHostException;
20 import java.security.NoSuchAlgorithmException;
21 import java.util.ArrayList;
22 import java.util.Collections;
23 import java.util.Iterator;
24 import java.util.LinkedHashMap;
25 import java.util.List;
27 import java.util.Map.Entry;
29 import java.util.concurrent.CopyOnWriteArrayList;
30 import java.util.concurrent.Executors;
31 import java.util.concurrent.ScheduledExecutorService;
32 import java.util.concurrent.TimeUnit;
34 import javax.crypto.Cipher;
36 import org.eclipse.jdt.annotation.NonNullByDefault;
37 import org.eclipse.jdt.annotation.Nullable;
38 import org.eclipse.jetty.client.HttpClient;
39 import org.openhab.binding.km200.internal.KM200Device;
40 import org.openhab.binding.km200.internal.KM200ServiceObject;
41 import org.openhab.binding.km200.internal.KM200ThingType;
42 import org.openhab.binding.km200.internal.KM200Utils;
43 import org.openhab.core.common.NamedThreadFactory;
44 import org.openhab.core.config.core.Configuration;
45 import org.openhab.core.library.types.DateTimeType;
46 import org.openhab.core.library.types.DecimalType;
47 import org.openhab.core.library.types.StringType;
48 import org.openhab.core.thing.Bridge;
49 import org.openhab.core.thing.Channel;
50 import org.openhab.core.thing.ChannelUID;
51 import org.openhab.core.thing.Thing;
52 import org.openhab.core.thing.ThingStatus;
53 import org.openhab.core.thing.ThingStatusDetail;
54 import org.openhab.core.thing.ThingStatusInfo;
55 import org.openhab.core.thing.ThingTypeUID;
56 import org.openhab.core.thing.binding.BaseBridgeHandler;
57 import org.openhab.core.types.Command;
58 import org.openhab.core.types.State;
59 import org.slf4j.Logger;
60 import org.slf4j.LoggerFactory;
62 import com.google.gson.JsonObject;
63 import com.google.gson.JsonParseException;
66 * The {@link KM200GatewayHandler} is responsible for handling commands, which are
67 * sent to one of the channels.
69 * @author Markus Eckhardt - Initial contribution
72 public class KM200GatewayHandler extends BaseBridgeHandler {
74 private final Logger logger = LoggerFactory.getLogger(KM200GatewayHandler.class);
76 public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_KMDEVICE);
78 private final Map<Channel, JsonObject> sendMap = Collections.synchronizedMap(new LinkedHashMap<>());
80 private List<KM200GatewayStatusListener> listeners = new CopyOnWriteArrayList<>();
83 * shared instance of HTTP client for (a)synchronous calls
85 private ScheduledExecutorService executor;
86 private final KM200Device remoteDevice;
87 private final KM200DataHandler dataHandler;
88 private int readDelay;
89 private int refreshInterval;
91 public KM200GatewayHandler(Bridge bridge, HttpClient httpClient) {
93 refreshInterval = 120;
95 remoteDevice = new KM200Device(httpClient);
96 dataHandler = new KM200DataHandler(remoteDevice);
97 executor = Executors.newScheduledThreadPool(2, new NamedThreadFactory("org.openhab.binding.km200", true));
101 public void handleCommand(ChannelUID channelUID, Command command) {
102 Channel channel = getThing().getChannel(channelUID.getId());
103 if (null != channel) {
104 if (command instanceof DateTimeType || command instanceof DecimalType || command instanceof StringType) {
105 prepareMessage(thing, channel, command);
107 logger.warn("Unsupported Command: {} Class: {}", command.toFullString(), command.getClass());
113 public void initialize() {
115 int maxKeyLen = Cipher.getMaxAllowedKeyLength("AES/ECB/NoPadding");
116 if (maxKeyLen <= 128) {
117 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
118 "Java Cryptography Extension (JCE) have to be installed");
121 } catch (NoSuchAlgorithmException e) {
122 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "AES encoding not supported");
125 if (!getDevice().getInited()) {
126 logger.info("Update KM50/100/200 gateway configuration, it takes a minute....");
128 if (getDevice().isConfigured()) {
129 if (!checkConfiguration()) {
132 /* configuration and communication seems to be ok */
134 updateStatus(ThingStatus.ONLINE);
136 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "No bridge configured");
137 logger.debug("The KM50/100/200 gateway configuration is not complete");
141 SendKM200Runnable sendRunnable = new SendKM200Runnable(sendMap, getDevice());
142 GetKM200Runnable receivingRunnable = new GetKM200Runnable(sendMap, this, getDevice());
143 if (!executor.isTerminated()) {
144 executor = Executors.newScheduledThreadPool(2,
145 new NamedThreadFactory("org.openhab.binding.km200", true));
146 executor.scheduleWithFixedDelay(receivingRunnable, 30, refreshInterval, TimeUnit.SECONDS);
147 executor.scheduleWithFixedDelay(sendRunnable, 60, refreshInterval * 2, TimeUnit.SECONDS);
153 public void dispose() {
156 if (!executor.awaitTermination(60000, TimeUnit.SECONDS)) {
157 logger.debug("Services didn't finish in 60000 seconds!");
159 } catch (InterruptedException e) {
160 executor.shutdownNow();
162 synchronized (getDevice()) {
163 getDevice().setInited(false);
164 getDevice().setIP4Address("");
165 getDevice().setCryptKeyPriv("");
166 getDevice().setMD5Salt("");
167 getDevice().setGatewayPassword("");
168 getDevice().setPrivatePassword("");
169 getDevice().serviceTreeMap.clear();
171 updateStatus(ThingStatus.OFFLINE);
175 public void handleRemoval() {
176 for (Thing actThing : getThing().getThings()) {
177 actThing.setStatusInfo(new ThingStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE, ""));
179 this.updateStatus(ThingStatus.REMOVED);
183 * Gets bridges configuration
185 private void getConfiguration() {
186 Configuration configuration = getConfig();
187 for (String key : configuration.keySet()) {
188 logger.trace("initialize Key: {} Value: {}", key, configuration.get(key));
191 String ip = (String) configuration.get("ip4Address");
192 if (ip != null && !ip.isBlank()) {
194 InetAddress.getByName(ip);
195 } catch (UnknownHostException e) {
196 logger.warn("IP4_address is not valid!: {}", ip);
198 getDevice().setIP4Address(ip);
200 logger.debug("No ip4_address configured!");
204 String privateKey = (String) configuration.get("privateKey");
205 if (privateKey != null && !privateKey.isBlank()) {
206 getDevice().setCryptKeyPriv(privateKey);
210 String md5Salt = (String) configuration.get("md5Salt");
211 if (md5Salt != null && !md5Salt.isBlank()) {
212 getDevice().setMD5Salt(md5Salt);
215 case "gatewayPassword":
216 String gatewayPassword = (String) configuration.get("gatewayPassword");
217 if (gatewayPassword != null && !gatewayPassword.isBlank()) {
218 getDevice().setGatewayPassword(gatewayPassword);
221 case "privatePassword":
222 String privatePassword = (String) configuration.get("privatePassword");
223 if (privatePassword != null && !privatePassword.isBlank()) {
224 getDevice().setPrivatePassword(privatePassword);
227 case "refreshInterval":
228 refreshInterval = ((BigDecimal) configuration.get("refreshInterval")).intValue();
229 logger.debug("Set refresh interval to: {} seconds.", refreshInterval);
232 readDelay = ((BigDecimal) configuration.get("readDelay")).intValue();
233 logger.debug("Set read delay to: {} seconds.", readDelay);
235 case "maxNbrRepeats":
236 Integer maxNbrRepeats = ((BigDecimal) configuration.get("maxNbrRepeats")).intValue();
237 logger.debug("Set max. number of repeats to: {} seconds.", maxNbrRepeats);
238 remoteDevice.setMaxNbrRepeats(maxNbrRepeats);
245 * Checks bridges configuration
247 private boolean checkConfiguration() {
248 /* Get HTTP Data from device */
249 JsonObject nodeRoot = remoteDevice.getServiceNode("/gateway/DateTime");
250 if (nodeRoot == null || nodeRoot.isJsonNull()) {
251 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
252 "No communication possible with gateway");
255 logger.debug("Test of the communication to the gateway was successful..");
257 /* Testing the received data, is decryption working? */
259 nodeRoot.get("type").getAsString();
260 nodeRoot.get("id").getAsString();
261 } catch (JsonParseException e) {
262 logger.debug("The data is not readable, check the key and password configuration! {}", e.getMessage());
263 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Wrong gateway configuration");
270 * Reads the devices capabilities and sets the data structures
272 private void readCapabilities() {
273 KM200VirtualServiceHandler virtualServiceHandler;
274 /* Checking of the device specific services and creating of a service list */
275 for (KM200ThingType thing : KM200ThingType.values()) {
276 String rootPath = thing.getRootPath();
277 if (!rootPath.isEmpty() && (rootPath.indexOf("/", 0) == rootPath.lastIndexOf("/", rootPath.length() - 1))) {
278 if (remoteDevice.getBlacklistMap().contains(thing.getRootPath())) {
279 logger.debug("Service on blacklist: {}", thing.getRootPath());
282 KM200ServiceHandler serviceHandler = new KM200ServiceHandler(thing.getRootPath(), null, remoteDevice);
283 serviceHandler.initObject();
286 /* Now init the virtual services */
287 virtualServiceHandler = new KM200VirtualServiceHandler(remoteDevice);
288 virtualServiceHandler.initVirtualObjects();
289 /* Output all available services in the log file */
290 getDevice().listAllServices();
291 updateBridgeProperties();
292 getDevice().setInited(true);
296 * Adds a GatewayConnectedListener
298 public void addGatewayStatusListener(KM200GatewayStatusListener listener) {
299 listeners.add(listener);
300 listener.gatewayStatusChanged(getThing().getStatus());
304 * Removes a GatewayConnectedListener
306 public void removeHubStatusListener(KM200GatewayStatusListener listener) {
307 listeners.remove(listener);
311 * Refreshes a channel
313 public void refreshChannel(Channel channel) {
314 GetSingleKM200Runnable runnable = new GetSingleKM200Runnable(sendMap, this, getDevice(), channel);
315 logger.debug("starting single runnable.");
316 scheduler.submit(runnable);
320 * Updates bridges properties
322 private void updateBridgeProperties() {
323 List<String> propertyServices = new ArrayList<>();
324 propertyServices.add(KM200ThingType.GATEWAY.getRootPath());
325 propertyServices.add(KM200ThingType.SYSTEM.getRootPath());
326 Map<String, String> bridgeProperties = editProperties();
328 for (KM200ThingType tType : KM200ThingType.values()) {
329 List<String> asProperties = tType.asBridgeProperties();
330 String rootPath = tType.getRootPath();
331 if (rootPath.isEmpty()) {
334 KM200ServiceObject serObj = getDevice().getServiceObject(rootPath);
335 if (null != serObj) {
336 for (String subKey : asProperties) {
337 if (serObj.serviceTreeMap.containsKey(subKey)) {
338 KM200ServiceObject subKeyObj = serObj.serviceTreeMap.get(subKey);
339 if (subKeyObj != null) {
340 String subKeyType = subKeyObj.getServiceType();
341 if (!DATA_TYPE_STRING_VALUE.equals(subKeyType)
342 && !DATA_TYPE_FLOAT_VALUE.equals(subKeyType)) {
345 if (bridgeProperties.containsKey(subKey)) {
346 bridgeProperties.remove(subKey);
348 Object value = subKeyObj.getValue();
349 logger.trace("Add Property: {} :{}", subKey, value);
351 bridgeProperties.put(subKey, value.toString());
358 updateProperties(bridgeProperties);
362 * Prepares a message for sending
364 public void prepareMessage(Thing thing, Channel channel, Command command) {
365 if (getDevice().getInited()) {
366 JsonObject newObject = null;
368 String service = KM200Utils.checkParameterReplacement(channel, getDevice());
369 String chTypes = channel.getAcceptedItemType();
370 if (null == chTypes) {
371 logger.warn("Channel {} has not accepted item types", channel.getLabel());
374 logger.trace("handleCommand channel: {} service: {}", channel.getLabel(), service);
375 newObject = dataHandler.sendProvidersState(service, command, chTypes,
376 KM200Utils.getChannelConfigurationStrings(channel));
377 synchronized (getDevice()) {
378 KM200ServiceObject serObjekt = getDevice().getServiceObject(service);
379 if (null != serObjekt) {
380 if (newObject != null) {
381 sendMap.put(channel, newObject);
382 } else if (getDevice().containsService(service) && serObjekt.getVirtual() == 1) {
383 String parent = serObjekt.getParent();
384 for (Thing actThing : getThing().getThings()) {
385 logger.trace("Checking: {}", actThing.getUID().getAsString());
386 for (Channel tmpChannel : actThing.getChannels()) {
387 String tmpChTypes = tmpChannel.getAcceptedItemType();
388 if (null == tmpChTypes) {
389 logger.warn("Channel {} has not accepted item types", tmpChannel.getLabel());
392 String actService = KM200Utils.checkParameterReplacement(tmpChannel, getDevice());
393 KM200ServiceObject actSerObjekt = getDevice().getServiceObject(actService);
394 if (null != actSerObjekt) {
395 String actParent = actSerObjekt.getParent();
396 if (actParent != null && actParent.equals(parent)) {
397 state = dataHandler.getProvidersState(actService, tmpChTypes,
398 KM200Utils.getChannelConfigurationStrings(tmpChannel));
401 updateState(tmpChannel.getUID(), state);
402 } catch (IllegalStateException e) {
403 logger.warn("Could not get updated item state", e);
411 logger.debug("Service is not availible: {}", service);
419 * Update the children
421 // Every thing has here a handler
422 private void updateChildren(Map<Channel, JsonObject> sendMap, KM200GatewayHandler gatewayHandler,
423 KM200Device remoteDevice, @Nullable String parent) {
425 synchronized (remoteDevice) {
426 if (parent != null) {
427 KM200ServiceObject serParObjekt = remoteDevice.getServiceObject(parent);
428 if (null != serParObjekt) {
429 serParObjekt.setUpdated(false);
432 for (Thing actThing : gatewayHandler.getThing().getThings()) {
433 for (Channel actChannel : actThing.getChannels()) {
434 String actChTypes = actChannel.getAcceptedItemType();
435 if (null == actChTypes) {
436 logger.warn("Channel {} has not accepted item types", actChannel.getLabel());
439 logger.trace("Checking: {} Root: {}", actChannel.getUID().getAsString(),
440 actChannel.getProperties().get("root"));
441 KM200ThingHandler actHandler = (KM200ThingHandler) actThing.getHandler();
442 if (actHandler != null) {
443 if (!actHandler.checkLinked(actChannel)) {
449 String tmpService = KM200Utils.checkParameterReplacement(actChannel, remoteDevice);
450 KM200ServiceObject tmpSerObjekt = remoteDevice.getServiceObject(tmpService);
451 if (null != tmpSerObjekt) {
452 if (parent == null || parent.equals(tmpSerObjekt.getParent())) {
453 synchronized (sendMap) {
454 JsonObject obj = sendMap.get(actChannel);
456 state = dataHandler.parseJSONData(obj, tmpSerObjekt.getServiceType(), tmpService,
457 actChTypes, KM200Utils.getChannelConfigurationStrings(actChannel));
459 state = dataHandler.getProvidersState(tmpService, actChTypes,
460 KM200Utils.getChannelConfigurationStrings(actChannel));
465 gatewayHandler.updateState(actChannel.getUID(), state);
466 } catch (IllegalStateException e) {
467 logger.warn("Could not get updated item state", e);
472 Thread.sleep(readDelay);
473 } catch (InterruptedException e) {
483 * Return the device instance.
485 public KM200Device getDevice() {
490 * The GetKM200Runnable class get the data from device to all items.
492 private class GetKM200Runnable implements Runnable {
494 private final KM200GatewayHandler gatewayHandler;
495 private final KM200Device remoteDevice;
496 private final Logger logger = LoggerFactory.getLogger(GetKM200Runnable.class);
497 private final Map<Channel, JsonObject> sendMap;
499 public GetKM200Runnable(Map<Channel, JsonObject> sendMap, KM200GatewayHandler gatewayHandler,
500 KM200Device remoteDevice) {
501 this.sendMap = sendMap;
502 this.gatewayHandler = gatewayHandler;
503 this.remoteDevice = remoteDevice;
508 logger.debug("GetKM200Runnable");
509 synchronized (remoteDevice) {
510 if (remoteDevice.getInited()) {
511 remoteDevice.resetAllUpdates(remoteDevice.serviceTreeMap);
512 updateChildren(sendMap, gatewayHandler, remoteDevice, null);
519 * The GetKM200Runnable class get the data from device for one channel.
521 private class GetSingleKM200Runnable implements Runnable {
523 private final Logger logger = LoggerFactory.getLogger(GetSingleKM200Runnable.class);
524 private final KM200GatewayHandler gatewayHandler;
525 private final KM200Device remoteDevice;
526 private final Channel channel;
527 private final Map<Channel, JsonObject> sendMap;
529 public GetSingleKM200Runnable(Map<Channel, JsonObject> sendMap, KM200GatewayHandler gatewayHandler,
530 KM200Device remoteDevice, Channel channel) {
531 this.gatewayHandler = gatewayHandler;
532 this.remoteDevice = remoteDevice;
533 this.channel = channel;
534 this.sendMap = sendMap;
539 logger.debug("GetKM200Runnable");
541 synchronized (remoteDevice) {
542 synchronized (sendMap) {
543 if (sendMap.containsKey(channel)) {
547 if (remoteDevice.getInited()) {
548 logger.trace("Checking: {} Root: {}", channel.getUID().getAsString(),
549 channel.getProperties().get("root"));
550 String chTypes = channel.getAcceptedItemType();
551 if (null == chTypes) {
552 logger.warn("Channel {} has not accepted item types", channel.getLabel());
555 String service = KM200Utils.checkParameterReplacement(channel, remoteDevice);
556 KM200ServiceObject object = remoteDevice.getServiceObject(service);
557 if (null != object) {
558 if (object.getVirtual() == 1) {
559 String parent = object.getParent();
560 updateChildren(sendMap, gatewayHandler, remoteDevice, parent);
562 object.setUpdated(false);
563 synchronized (sendMap) {
564 KM200ServiceObject serObjekt = remoteDevice.getServiceObject(service);
565 if (null != serObjekt) {
566 JsonObject obj = sendMap.get(channel);
568 state = dataHandler.parseJSONData(obj, serObjekt.getServiceType(), service,
569 chTypes, KM200Utils.getChannelConfigurationStrings(channel));
571 state = dataHandler.getProvidersState(service, chTypes,
572 KM200Utils.getChannelConfigurationStrings(channel));
577 gatewayHandler.updateState(channel.getUID(), state);
578 } catch (IllegalStateException e) {
579 logger.warn("Could not get updated item state", e);
591 * The sendKM200Thread class sends the data to the device.
593 private class SendKM200Runnable implements Runnable {
595 private final Logger logger = LoggerFactory.getLogger(SendKM200Runnable.class);
596 private final Map<Channel, JsonObject> newObject;
597 private final KM200Device remoteDevice;
599 public SendKM200Runnable(Map<Channel, JsonObject> newObject, KM200Device remoteDevice) {
600 this.newObject = newObject;
601 this.remoteDevice = remoteDevice;
606 logger.debug("Send-Executor started");
607 Map.Entry<Channel, JsonObject> nextEntry;
608 /* Check whether a new entry is availible, if yes then take and remove it */
611 synchronized (remoteDevice) {
612 synchronized (newObject) {
613 Iterator<Entry<Channel, JsonObject>> i = newObject.entrySet().iterator();
615 nextEntry = i.next();
619 if (nextEntry != null) {
620 /* Now send the data to the device */
621 Channel channel = nextEntry.getKey();
622 JsonObject newObject = nextEntry.getValue();
624 String service = KM200Utils.checkParameterReplacement(channel, remoteDevice);
625 KM200ServiceObject object = remoteDevice.getServiceObject(service);
626 if (null != object) {
627 if (object.getVirtual() == 0) {
628 remoteDevice.setServiceNode(service, newObject);
630 String parent = object.getParent();
631 if (null != parent) {
632 logger.trace("Sending: {} to : {}", newObject, service);
633 remoteDevice.setServiceNode(parent, newObject);
639 } while (nextEntry != null);