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 = Collections.singleton(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 updateStatus(ThingStatus.UNINITIALIZED, ThingStatusDetail.CONFIGURATION_PENDING);
96 remoteDevice = new KM200Device(httpClient);
97 dataHandler = new KM200DataHandler(remoteDevice);
98 executor = Executors.newScheduledThreadPool(2, new NamedThreadFactory("org.openhab.binding.km200", true));
102 public void handleCommand(ChannelUID channelUID, Command command) {
103 Channel channel = getThing().getChannel(channelUID.getId());
104 if (null != channel) {
105 if (command instanceof DateTimeType || command instanceof DecimalType || command instanceof StringType) {
106 prepareMessage(thing, channel, command);
108 logger.warn("Unsupported Command: {} Class: {}", command.toFullString(), command.getClass());
114 public void initialize() {
116 int maxKeyLen = Cipher.getMaxAllowedKeyLength("AES/ECB/NoPadding");
117 if (maxKeyLen <= 128) {
118 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
119 "Java Cryptography Extension (JCE) have to be installed");
122 } catch (NoSuchAlgorithmException e) {
123 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "AES encoding not supported");
126 if (!getDevice().getInited()) {
127 logger.info("Update KM50/100/200 gateway configuration, it takes a minute....");
129 if (getDevice().isConfigured()) {
130 if (!checkConfiguration()) {
133 /* configuration and communication seems to be ok */
135 updateStatus(ThingStatus.ONLINE);
137 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "No bridge configured");
138 logger.debug("The KM50/100/200 gateway configuration is not complete");
142 SendKM200Runnable sendRunnable = new SendKM200Runnable(sendMap, getDevice());
143 GetKM200Runnable receivingRunnable = new GetKM200Runnable(sendMap, this, getDevice());
144 if (!executor.isTerminated()) {
145 executor = Executors.newScheduledThreadPool(2,
146 new NamedThreadFactory("org.openhab.binding.km200", true));
147 executor.scheduleWithFixedDelay(receivingRunnable, 30, refreshInterval, TimeUnit.SECONDS);
148 executor.scheduleWithFixedDelay(sendRunnable, 60, refreshInterval * 2, TimeUnit.SECONDS);
154 public void dispose() {
157 if (!executor.awaitTermination(60000, TimeUnit.SECONDS)) {
158 logger.debug("Services didn't finish in 60000 seconds!");
160 } catch (InterruptedException e) {
161 executor.shutdownNow();
163 synchronized (getDevice()) {
164 getDevice().setInited(false);
165 getDevice().setIP4Address("");
166 getDevice().setCryptKeyPriv("");
167 getDevice().setMD5Salt("");
168 getDevice().setGatewayPassword("");
169 getDevice().setPrivatePassword("");
170 getDevice().serviceTreeMap.clear();
172 updateStatus(ThingStatus.OFFLINE);
176 public void handleRemoval() {
177 for (Thing actThing : getThing().getThings()) {
178 actThing.setStatusInfo(new ThingStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE, ""));
180 this.updateStatus(ThingStatus.REMOVED);
184 * Gets bridges configuration
186 private void getConfiguration() {
187 Configuration configuration = getConfig();
188 for (String key : configuration.keySet()) {
189 logger.trace("initialize Key: {} Value: {}", key, configuration.get(key));
192 String ip = (String) configuration.get("ip4Address");
193 if (ip != null && !ip.isBlank()) {
195 InetAddress.getByName(ip);
196 } catch (UnknownHostException e) {
197 logger.warn("IP4_address is not valid!: {}", ip);
199 getDevice().setIP4Address(ip);
201 logger.debug("No ip4_address configured!");
205 String privateKey = (String) configuration.get("privateKey");
206 if (privateKey != null && !privateKey.isBlank()) {
207 getDevice().setCryptKeyPriv(privateKey);
211 String md5Salt = (String) configuration.get("md5Salt");
212 if (md5Salt != null && !md5Salt.isBlank()) {
213 getDevice().setMD5Salt(md5Salt);
216 case "gatewayPassword":
217 String gatewayPassword = (String) configuration.get("gatewayPassword");
218 if (gatewayPassword != null && !gatewayPassword.isBlank()) {
219 getDevice().setGatewayPassword(gatewayPassword);
222 case "privatePassword":
223 String privatePassword = (String) configuration.get("privatePassword");
224 if (privatePassword != null && !privatePassword.isBlank()) {
225 getDevice().setPrivatePassword(privatePassword);
228 case "refreshInterval":
229 refreshInterval = ((BigDecimal) configuration.get("refreshInterval")).intValue();
230 logger.debug("Set refresh interval to: {} seconds.", refreshInterval);
233 readDelay = ((BigDecimal) configuration.get("readDelay")).intValue();
234 logger.debug("Set read delay to: {} seconds.", readDelay);
236 case "maxNbrRepeats":
237 Integer maxNbrRepeats = ((BigDecimal) configuration.get("maxNbrRepeats")).intValue();
238 logger.debug("Set max. number of repeats to: {} seconds.", maxNbrRepeats);
239 remoteDevice.setMaxNbrRepeats(maxNbrRepeats);
246 * Checks bridges configuration
248 private boolean checkConfiguration() {
249 /* Get HTTP Data from device */
250 JsonObject nodeRoot = remoteDevice.getServiceNode("/gateway/DateTime");
251 if (nodeRoot == null || nodeRoot.isJsonNull()) {
252 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
253 "No communication possible with gateway");
256 logger.debug("Test of the communication to the gateway was successful..");
258 /* Testing the received data, is decryption working? */
260 nodeRoot.get("type").getAsString();
261 nodeRoot.get("id").getAsString();
262 } catch (JsonParseException e) {
263 logger.debug("The data is not readable, check the key and password configuration! {}", e.getMessage());
264 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Wrong gateway configuration");
271 * Reads the devices capabilities and sets the data structures
273 private void readCapabilities() {
274 KM200VirtualServiceHandler virtualServiceHandler;
275 /* Checking of the device specific services and creating of a service list */
276 for (KM200ThingType thing : KM200ThingType.values()) {
277 String rootPath = thing.getRootPath();
278 if (!rootPath.isEmpty() && (rootPath.indexOf("/", 0) == rootPath.lastIndexOf("/", rootPath.length() - 1))) {
279 if (remoteDevice.getBlacklistMap().contains(thing.getRootPath())) {
280 logger.debug("Service on blacklist: {}", thing.getRootPath());
283 KM200ServiceHandler serviceHandler = new KM200ServiceHandler(thing.getRootPath(), null, remoteDevice);
284 serviceHandler.initObject();
287 /* Now init the virtual services */
288 virtualServiceHandler = new KM200VirtualServiceHandler(remoteDevice);
289 virtualServiceHandler.initVirtualObjects();
290 /* Output all available services in the log file */
291 getDevice().listAllServices();
292 updateBridgeProperties();
293 getDevice().setInited(true);
297 * Adds a GatewayConnectedListener
299 public void addGatewayStatusListener(KM200GatewayStatusListener listener) {
300 listeners.add(listener);
301 listener.gatewayStatusChanged(getThing().getStatus());
305 * Removes a GatewayConnectedListener
307 public void removeHubStatusListener(KM200GatewayStatusListener listener) {
308 listeners.remove(listener);
312 * Refreshes a channel
314 public void refreshChannel(Channel channel) {
315 GetSingleKM200Runnable runnable = new GetSingleKM200Runnable(sendMap, this, getDevice(), channel);
316 logger.debug("starting single runnable.");
317 scheduler.submit(runnable);
321 * Updates bridges properties
323 private void updateBridgeProperties() {
324 List<String> propertyServices = new ArrayList<>();
325 propertyServices.add(KM200ThingType.GATEWAY.getRootPath());
326 propertyServices.add(KM200ThingType.SYSTEM.getRootPath());
327 Map<String, String> bridgeProperties = editProperties();
329 for (KM200ThingType tType : KM200ThingType.values()) {
330 List<String> asProperties = tType.asBridgeProperties();
331 String rootPath = tType.getRootPath();
332 if (rootPath.isEmpty()) {
335 KM200ServiceObject serObj = getDevice().getServiceObject(rootPath);
336 if (null != serObj) {
337 for (String subKey : asProperties) {
338 if (serObj.serviceTreeMap.containsKey(subKey)) {
339 KM200ServiceObject subKeyObj = serObj.serviceTreeMap.get(subKey);
340 if (subKeyObj != null) {
341 String subKeyType = subKeyObj.getServiceType();
342 if (!DATA_TYPE_STRING_VALUE.equals(subKeyType)
343 && !DATA_TYPE_FLOAT_VALUE.equals(subKeyType)) {
346 if (bridgeProperties.containsKey(subKey)) {
347 bridgeProperties.remove(subKey);
349 Object value = subKeyObj.getValue();
350 logger.trace("Add Property: {} :{}", subKey, value);
352 bridgeProperties.put(subKey, value.toString());
359 updateProperties(bridgeProperties);
363 * Prepares a message for sending
365 public void prepareMessage(Thing thing, Channel channel, Command command) {
366 if (getDevice().getInited()) {
367 JsonObject newObject = null;
369 String service = KM200Utils.checkParameterReplacement(channel, getDevice());
370 String chTypes = channel.getAcceptedItemType();
371 if (null == chTypes) {
372 logger.warn("Channel {} has not accepted item types", channel.getLabel());
375 logger.trace("handleCommand channel: {} service: {}", channel.getLabel(), service);
376 newObject = dataHandler.sendProvidersState(service, command, chTypes,
377 KM200Utils.getChannelConfigurationStrings(channel));
378 synchronized (getDevice()) {
379 KM200ServiceObject serObjekt = getDevice().getServiceObject(service);
380 if (null != serObjekt) {
381 if (newObject != null) {
382 sendMap.put(channel, newObject);
383 } else if (getDevice().containsService(service) && serObjekt.getVirtual() == 1) {
384 String parent = serObjekt.getParent();
385 for (Thing actThing : getThing().getThings()) {
386 logger.trace("Checking: {}", actThing.getUID().getAsString());
387 for (Channel tmpChannel : actThing.getChannels()) {
388 String tmpChTypes = tmpChannel.getAcceptedItemType();
389 if (null == tmpChTypes) {
390 logger.warn("Channel {} has not accepted item types", tmpChannel.getLabel());
393 String actService = KM200Utils.checkParameterReplacement(tmpChannel, getDevice());
394 KM200ServiceObject actSerObjekt = getDevice().getServiceObject(actService);
395 if (null != actSerObjekt) {
396 String actParent = actSerObjekt.getParent();
397 if (actParent != null && actParent.equals(parent)) {
398 state = dataHandler.getProvidersState(actService, tmpChTypes,
399 KM200Utils.getChannelConfigurationStrings(tmpChannel));
402 updateState(tmpChannel.getUID(), state);
403 } catch (IllegalStateException e) {
404 logger.warn("Could not get updated item state", e);
412 logger.debug("Service is not availible: {}", service);
420 * Update the children
422 // Every thing has here a handler
423 private void updateChildren(Map<Channel, JsonObject> sendMap, KM200GatewayHandler gatewayHandler,
424 KM200Device remoteDevice, @Nullable String parent) {
426 synchronized (remoteDevice) {
427 if (parent != null) {
428 KM200ServiceObject serParObjekt = remoteDevice.getServiceObject(parent);
429 if (null != serParObjekt) {
430 serParObjekt.setUpdated(false);
433 for (Thing actThing : gatewayHandler.getThing().getThings()) {
434 for (Channel actChannel : actThing.getChannels()) {
435 String actChTypes = actChannel.getAcceptedItemType();
436 if (null == actChTypes) {
437 logger.warn("Channel {} has not accepted item types", actChannel.getLabel());
440 logger.trace("Checking: {} Root: {}", actChannel.getUID().getAsString(),
441 actChannel.getProperties().get("root"));
442 KM200ThingHandler actHandler = (KM200ThingHandler) actThing.getHandler();
443 if (actHandler != null) {
444 if (!actHandler.checkLinked(actChannel)) {
450 String tmpService = KM200Utils.checkParameterReplacement(actChannel, remoteDevice);
451 KM200ServiceObject tmpSerObjekt = remoteDevice.getServiceObject(tmpService);
452 if (null != tmpSerObjekt) {
453 if (parent == null || parent.equals(tmpSerObjekt.getParent())) {
454 synchronized (sendMap) {
455 JsonObject obj = sendMap.get(actChannel);
457 state = dataHandler.parseJSONData(obj, tmpSerObjekt.getServiceType(), tmpService,
458 actChTypes, KM200Utils.getChannelConfigurationStrings(actChannel));
460 state = dataHandler.getProvidersState(tmpService, actChTypes,
461 KM200Utils.getChannelConfigurationStrings(actChannel));
466 gatewayHandler.updateState(actChannel.getUID(), state);
467 } catch (IllegalStateException e) {
468 logger.warn("Could not get updated item state", e);
473 Thread.sleep(readDelay);
474 } catch (InterruptedException e) {
484 * Return the device instance.
486 public KM200Device getDevice() {
491 * The GetKM200Runnable class get the data from device to all items.
493 private class GetKM200Runnable implements Runnable {
495 private final KM200GatewayHandler gatewayHandler;
496 private final KM200Device remoteDevice;
497 private final Logger logger = LoggerFactory.getLogger(GetKM200Runnable.class);
498 private final Map<Channel, JsonObject> sendMap;
500 public GetKM200Runnable(Map<Channel, JsonObject> sendMap, KM200GatewayHandler gatewayHandler,
501 KM200Device remoteDevice) {
502 this.sendMap = sendMap;
503 this.gatewayHandler = gatewayHandler;
504 this.remoteDevice = remoteDevice;
509 logger.debug("GetKM200Runnable");
510 synchronized (remoteDevice) {
511 if (remoteDevice.getInited()) {
512 remoteDevice.resetAllUpdates(remoteDevice.serviceTreeMap);
513 updateChildren(sendMap, gatewayHandler, remoteDevice, null);
520 * The GetKM200Runnable class get the data from device for one channel.
522 private class GetSingleKM200Runnable implements Runnable {
524 private final Logger logger = LoggerFactory.getLogger(GetSingleKM200Runnable.class);
525 private final KM200GatewayHandler gatewayHandler;
526 private final KM200Device remoteDevice;
527 private final Channel channel;
528 private final Map<Channel, JsonObject> sendMap;
530 public GetSingleKM200Runnable(Map<Channel, JsonObject> sendMap, KM200GatewayHandler gatewayHandler,
531 KM200Device remoteDevice, Channel channel) {
532 this.gatewayHandler = gatewayHandler;
533 this.remoteDevice = remoteDevice;
534 this.channel = channel;
535 this.sendMap = sendMap;
540 logger.debug("GetKM200Runnable");
542 synchronized (remoteDevice) {
543 synchronized (sendMap) {
544 if (sendMap.containsKey(channel)) {
548 if (remoteDevice.getInited()) {
549 logger.trace("Checking: {} Root: {}", channel.getUID().getAsString(),
550 channel.getProperties().get("root"));
551 String chTypes = channel.getAcceptedItemType();
552 if (null == chTypes) {
553 logger.warn("Channel {} has not accepted item types", channel.getLabel());
556 String service = KM200Utils.checkParameterReplacement(channel, remoteDevice);
557 KM200ServiceObject object = remoteDevice.getServiceObject(service);
558 if (null != object) {
559 if (object.getVirtual() == 1) {
560 String parent = object.getParent();
561 updateChildren(sendMap, gatewayHandler, remoteDevice, parent);
563 object.setUpdated(false);
564 synchronized (sendMap) {
565 KM200ServiceObject serObjekt = remoteDevice.getServiceObject(service);
566 if (null != serObjekt) {
567 JsonObject obj = sendMap.get(channel);
569 state = dataHandler.parseJSONData(obj, serObjekt.getServiceType(), service,
570 chTypes, KM200Utils.getChannelConfigurationStrings(channel));
572 state = dataHandler.getProvidersState(service, chTypes,
573 KM200Utils.getChannelConfigurationStrings(channel));
578 gatewayHandler.updateState(channel.getUID(), state);
579 } catch (IllegalStateException e) {
580 logger.warn("Could not get updated item state", e);
592 * The sendKM200Thread class sends the data to the device.
594 private class SendKM200Runnable implements Runnable {
596 private final Logger logger = LoggerFactory.getLogger(SendKM200Runnable.class);
597 private final Map<Channel, JsonObject> newObject;
598 private final KM200Device remoteDevice;
600 public SendKM200Runnable(Map<Channel, JsonObject> newObject, KM200Device remoteDevice) {
601 this.newObject = newObject;
602 this.remoteDevice = remoteDevice;
607 logger.debug("Send-Executor started");
608 Map.Entry<Channel, JsonObject> nextEntry;
609 /* Check whether a new entry is availible, if yes then take and remove it */
612 synchronized (remoteDevice) {
613 synchronized (newObject) {
614 Iterator<Entry<Channel, JsonObject>> i = newObject.entrySet().iterator();
616 nextEntry = i.next();
620 if (nextEntry != null) {
621 /* Now send the data to the device */
622 Channel channel = nextEntry.getKey();
623 JsonObject newObject = nextEntry.getValue();
625 String service = KM200Utils.checkParameterReplacement(channel, remoteDevice);
626 KM200ServiceObject object = remoteDevice.getServiceObject(service);
627 if (null != object) {
628 if (object.getVirtual() == 0) {
629 remoteDevice.setServiceNode(service, newObject);
631 String parent = object.getParent();
632 if (null != parent) {
633 logger.trace("Sending: {} to : {}", newObject, service);
634 remoteDevice.setServiceNode(parent, newObject);
640 } while (nextEntry != null);