2 * Copyright (c) 2010-2022 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.util.ArrayList;
21 import java.util.Collections;
22 import java.util.Iterator;
23 import java.util.LinkedHashMap;
24 import java.util.List;
26 import java.util.Map.Entry;
28 import java.util.concurrent.CopyOnWriteArrayList;
29 import java.util.concurrent.Executors;
30 import java.util.concurrent.ScheduledExecutorService;
31 import java.util.concurrent.TimeUnit;
33 import org.eclipse.jdt.annotation.NonNullByDefault;
34 import org.eclipse.jdt.annotation.Nullable;
35 import org.eclipse.jetty.client.HttpClient;
36 import org.openhab.binding.km200.internal.KM200Device;
37 import org.openhab.binding.km200.internal.KM200ServiceObject;
38 import org.openhab.binding.km200.internal.KM200ThingType;
39 import org.openhab.binding.km200.internal.KM200Utils;
40 import org.openhab.core.common.NamedThreadFactory;
41 import org.openhab.core.config.core.Configuration;
42 import org.openhab.core.library.types.DateTimeType;
43 import org.openhab.core.library.types.DecimalType;
44 import org.openhab.core.library.types.StringType;
45 import org.openhab.core.thing.Bridge;
46 import org.openhab.core.thing.Channel;
47 import org.openhab.core.thing.ChannelUID;
48 import org.openhab.core.thing.Thing;
49 import org.openhab.core.thing.ThingStatus;
50 import org.openhab.core.thing.ThingStatusDetail;
51 import org.openhab.core.thing.ThingStatusInfo;
52 import org.openhab.core.thing.ThingTypeUID;
53 import org.openhab.core.thing.binding.BaseBridgeHandler;
54 import org.openhab.core.types.Command;
55 import org.openhab.core.types.State;
56 import org.slf4j.Logger;
57 import org.slf4j.LoggerFactory;
59 import com.google.gson.JsonObject;
60 import com.google.gson.JsonParseException;
63 * The {@link KM200GatewayHandler} is responsible for handling commands, which are
64 * sent to one of the channels.
66 * @author Markus Eckhardt - Initial contribution
69 public class KM200GatewayHandler extends BaseBridgeHandler {
71 private final Logger logger = LoggerFactory.getLogger(KM200GatewayHandler.class);
73 public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections.singleton(THING_TYPE_KMDEVICE);
75 private final Map<Channel, JsonObject> sendMap = Collections.synchronizedMap(new LinkedHashMap<>());
77 private List<KM200GatewayStatusListener> listeners = new CopyOnWriteArrayList<>();
80 * shared instance of HTTP client for (a)synchronous calls
82 private ScheduledExecutorService executor;
83 private final KM200Device remoteDevice;
84 private final KM200DataHandler dataHandler;
85 private int readDelay;
86 private int refreshInterval;
88 public KM200GatewayHandler(Bridge bridge, HttpClient httpClient) {
90 refreshInterval = 120;
92 updateStatus(ThingStatus.UNINITIALIZED, ThingStatusDetail.CONFIGURATION_PENDING);
93 remoteDevice = new KM200Device(httpClient);
94 dataHandler = new KM200DataHandler(remoteDevice);
95 executor = Executors.newScheduledThreadPool(2, new NamedThreadFactory("org.openhab.binding.km200", true));
99 public void handleCommand(ChannelUID channelUID, Command command) {
100 Channel channel = getThing().getChannel(channelUID.getId());
101 if (null != channel) {
102 if (command instanceof DateTimeType || command instanceof DecimalType || command instanceof StringType) {
103 prepareMessage(thing, channel, command);
105 logger.warn("Unsupported Command: {} Class: {}", command.toFullString(), command.getClass());
111 public void initialize() {
112 if (!getDevice().getInited()) {
113 logger.info("Update KM50/100/200 gateway configuration, it takes a minute....");
115 if (getDevice().isConfigured()) {
116 if (!checkConfiguration()) {
119 /* configuration and communication seems to be ok */
121 updateStatus(ThingStatus.ONLINE);
123 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "No bridge configured");
124 logger.debug("The KM50/100/200 gateway configuration is not complete");
128 SendKM200Runnable sendRunnable = new SendKM200Runnable(sendMap, getDevice());
129 GetKM200Runnable receivingRunnable = new GetKM200Runnable(sendMap, this, getDevice());
130 if (!executor.isTerminated()) {
131 executor = Executors.newScheduledThreadPool(2,
132 new NamedThreadFactory("org.openhab.binding.km200", true));
133 executor.scheduleWithFixedDelay(receivingRunnable, 30, refreshInterval, TimeUnit.SECONDS);
134 executor.scheduleWithFixedDelay(sendRunnable, 60, refreshInterval * 2, TimeUnit.SECONDS);
140 public void dispose() {
143 if (!executor.awaitTermination(60000, TimeUnit.SECONDS)) {
144 logger.debug("Services didn't finish in 60000 seconds!");
146 } catch (InterruptedException e) {
147 executor.shutdownNow();
149 synchronized (getDevice()) {
150 getDevice().setInited(false);
151 getDevice().setIP4Address("");
152 getDevice().setCryptKeyPriv("");
153 getDevice().setMD5Salt("");
154 getDevice().setGatewayPassword("");
155 getDevice().setPrivatePassword("");
156 getDevice().serviceTreeMap.clear();
158 updateStatus(ThingStatus.OFFLINE);
162 public void handleRemoval() {
163 for (Thing actThing : getThing().getThings()) {
164 actThing.setStatusInfo(new ThingStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE, ""));
166 this.updateStatus(ThingStatus.REMOVED);
170 * Gets bridges configuration
172 private void getConfiguration() {
173 Configuration configuration = getConfig();
174 for (String key : configuration.keySet()) {
175 logger.trace("initialize Key: {} Value: {}", key, configuration.get(key));
178 String ip = (String) configuration.get("ip4Address");
179 if (ip != null && !ip.isBlank()) {
181 InetAddress.getByName(ip);
182 } catch (UnknownHostException e) {
183 logger.warn("IP4_address is not valid!: {}", ip);
185 getDevice().setIP4Address(ip);
187 logger.debug("No ip4_address configured!");
191 String privateKey = (String) configuration.get("privateKey");
192 if (privateKey != null && !privateKey.isBlank()) {
193 getDevice().setCryptKeyPriv(privateKey);
197 String md5Salt = (String) configuration.get("md5Salt");
198 if (md5Salt != null && !md5Salt.isBlank()) {
199 getDevice().setMD5Salt(md5Salt);
202 case "gatewayPassword":
203 String gatewayPassword = (String) configuration.get("gatewayPassword");
204 if (gatewayPassword != null && !gatewayPassword.isBlank()) {
205 getDevice().setGatewayPassword(gatewayPassword);
208 case "privatePassword":
209 String privatePassword = (String) configuration.get("privatePassword");
210 if (privatePassword != null && !privatePassword.isBlank()) {
211 getDevice().setPrivatePassword(privatePassword);
214 case "refreshInterval":
215 refreshInterval = ((BigDecimal) configuration.get("refreshInterval")).intValue();
216 logger.debug("Set refresh interval to: {} seconds.", refreshInterval);
219 readDelay = ((BigDecimal) configuration.get("readDelay")).intValue();
220 logger.debug("Set read delay to: {} seconds.", readDelay);
222 case "maxNbrRepeats":
223 Integer maxNbrRepeats = ((BigDecimal) configuration.get("maxNbrRepeats")).intValue();
224 logger.debug("Set max. number of repeats to: {} seconds.", maxNbrRepeats);
225 remoteDevice.setMaxNbrRepeats(maxNbrRepeats);
232 * Checks bridges configuration
234 private boolean checkConfiguration() {
235 /* Get HTTP Data from device */
236 JsonObject nodeRoot = remoteDevice.getServiceNode("/gateway/DateTime");
237 if (nodeRoot == null || nodeRoot.isJsonNull()) {
238 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
239 "No communication possible with gateway");
242 logger.debug("Test of the communication to the gateway was successful..");
244 /* Testing the received data, is decryption working? */
246 nodeRoot.get("type").getAsString();
247 nodeRoot.get("id").getAsString();
248 } catch (JsonParseException e) {
249 logger.debug("The data is not readable, check the key and password configuration! {}", e.getMessage());
250 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Wrong gateway configuration");
257 * Reads the devices capabilities and sets the data structures
259 private void readCapabilities() {
260 KM200VirtualServiceHandler virtualServiceHandler;
261 /* Checking of the device specific services and creating of a service list */
262 for (KM200ThingType thing : KM200ThingType.values()) {
263 String rootPath = thing.getRootPath();
264 if (!rootPath.isEmpty() && (rootPath.indexOf("/", 0) == rootPath.lastIndexOf("/", rootPath.length() - 1))) {
265 if (remoteDevice.getBlacklistMap().contains(thing.getRootPath())) {
266 logger.debug("Service on blacklist: {}", thing.getRootPath());
269 KM200ServiceHandler serviceHandler = new KM200ServiceHandler(thing.getRootPath(), null, remoteDevice);
270 serviceHandler.initObject();
273 /* Now init the virtual services */
274 virtualServiceHandler = new KM200VirtualServiceHandler(remoteDevice);
275 virtualServiceHandler.initVirtualObjects();
276 /* Output all available services in the log file */
277 getDevice().listAllServices();
278 updateBridgeProperties();
279 getDevice().setInited(true);
283 * Adds a GatewayConnectedListener
285 public void addGatewayStatusListener(KM200GatewayStatusListener listener) {
286 listeners.add(listener);
287 listener.gatewayStatusChanged(getThing().getStatus());
291 * Removes a GatewayConnectedListener
293 public void removeHubStatusListener(KM200GatewayStatusListener listener) {
294 listeners.remove(listener);
298 * Refreshes a channel
300 public void refreshChannel(Channel channel) {
301 GetSingleKM200Runnable runnable = new GetSingleKM200Runnable(sendMap, this, getDevice(), channel);
302 logger.debug("starting single runnable.");
303 scheduler.submit(runnable);
307 * Updates bridges properties
309 private void updateBridgeProperties() {
310 List<String> propertyServices = new ArrayList<>();
311 propertyServices.add(KM200ThingType.GATEWAY.getRootPath());
312 propertyServices.add(KM200ThingType.SYSTEM.getRootPath());
313 Map<String, String> bridgeProperties = editProperties();
315 for (KM200ThingType tType : KM200ThingType.values()) {
316 List<String> asProperties = tType.asBridgeProperties();
317 String rootPath = tType.getRootPath();
318 if (rootPath.isEmpty()) {
321 KM200ServiceObject serObj = getDevice().getServiceObject(rootPath);
322 if (null != serObj) {
323 for (String subKey : asProperties) {
324 if (serObj.serviceTreeMap.containsKey(subKey)) {
325 KM200ServiceObject subKeyObj = serObj.serviceTreeMap.get(subKey);
326 String subKeyType = subKeyObj.getServiceType();
327 if (!DATA_TYPE_STRING_VALUE.equals(subKeyType) && !DATA_TYPE_FLOAT_VALUE.equals(subKeyType)) {
330 if (bridgeProperties.containsKey(subKey)) {
331 bridgeProperties.remove(subKey);
333 Object value = subKeyObj.getValue();
334 logger.trace("Add Property: {} :{}", subKey, value);
336 bridgeProperties.put(subKey, value.toString());
342 updateProperties(bridgeProperties);
346 * Prepares a message for sending
348 public void prepareMessage(Thing thing, Channel channel, Command command) {
349 if (getDevice().getInited()) {
350 JsonObject newObject = null;
352 String service = KM200Utils.checkParameterReplacement(channel, getDevice());
353 String chTypes = channel.getAcceptedItemType();
354 if (null == chTypes) {
355 logger.warn("Channel {} has not accepted item types", channel.getLabel());
358 logger.trace("handleCommand channel: {} service: {}", channel.getLabel(), service);
359 newObject = dataHandler.sendProvidersState(service, command, chTypes,
360 KM200Utils.getChannelConfigurationStrings(channel));
361 synchronized (getDevice()) {
362 KM200ServiceObject serObjekt = getDevice().getServiceObject(service);
363 if (null != serObjekt) {
364 if (newObject != null) {
365 sendMap.put(channel, newObject);
366 } else if (getDevice().containsService(service) && serObjekt.getVirtual() == 1) {
367 String parent = serObjekt.getParent();
368 for (Thing actThing : getThing().getThings()) {
369 logger.trace("Checking: {}", actThing.getUID().getAsString());
370 for (Channel tmpChannel : actThing.getChannels()) {
371 String tmpChTypes = tmpChannel.getAcceptedItemType();
372 if (null == tmpChTypes) {
373 logger.warn("Channel {} has not accepted item types", tmpChannel.getLabel());
376 String actService = KM200Utils.checkParameterReplacement(tmpChannel, getDevice());
377 KM200ServiceObject actSerObjekt = getDevice().getServiceObject(actService);
378 if (null != actSerObjekt) {
379 String actParent = actSerObjekt.getParent();
380 if (actParent != null && actParent.equals(parent)) {
381 state = dataHandler.getProvidersState(actService, tmpChTypes,
382 KM200Utils.getChannelConfigurationStrings(tmpChannel));
385 updateState(tmpChannel.getUID(), state);
386 } catch (IllegalStateException e) {
387 logger.warn("Could not get updated item state", e);
395 logger.debug("Service is not availible: {}", service);
403 * Update the children
405 // Every thing has here a handler
406 private void updateChildren(Map<Channel, JsonObject> sendMap, KM200GatewayHandler gatewayHandler,
407 KM200Device remoteDevice, @Nullable String parent) {
409 synchronized (remoteDevice) {
410 if (parent != null) {
411 KM200ServiceObject serParObjekt = remoteDevice.getServiceObject(parent);
412 if (null != serParObjekt) {
413 serParObjekt.setUpdated(false);
416 for (Thing actThing : gatewayHandler.getThing().getThings()) {
417 for (Channel actChannel : actThing.getChannels()) {
418 String actChTypes = actChannel.getAcceptedItemType();
419 if (null == actChTypes) {
420 logger.warn("Channel {} has not accepted item types", actChannel.getLabel());
423 logger.trace("Checking: {} Root: {}", actChannel.getUID().getAsString(),
424 actChannel.getProperties().get("root"));
425 KM200ThingHandler actHandler = (KM200ThingHandler) actThing.getHandler();
426 if (actHandler != null) {
427 if (!actHandler.checkLinked(actChannel)) {
433 String tmpService = KM200Utils.checkParameterReplacement(actChannel, remoteDevice);
434 KM200ServiceObject tmpSerObjekt = remoteDevice.getServiceObject(tmpService);
435 if (null != tmpSerObjekt) {
436 if (parent == null || parent.equals(tmpSerObjekt.getParent())) {
437 synchronized (sendMap) {
438 JsonObject obj = sendMap.get(actChannel);
440 state = dataHandler.parseJSONData(obj, tmpSerObjekt.getServiceType(), tmpService,
441 actChTypes, KM200Utils.getChannelConfigurationStrings(actChannel));
443 state = dataHandler.getProvidersState(tmpService, actChTypes,
444 KM200Utils.getChannelConfigurationStrings(actChannel));
449 gatewayHandler.updateState(actChannel.getUID(), state);
450 } catch (IllegalStateException e) {
451 logger.warn("Could not get updated item state", e);
456 Thread.sleep(readDelay);
457 } catch (InterruptedException e) {
467 * Return the device instance.
469 public KM200Device getDevice() {
474 * The GetKM200Runnable class get the data from device to all items.
476 private class GetKM200Runnable implements Runnable {
478 private final KM200GatewayHandler gatewayHandler;
479 private final KM200Device remoteDevice;
480 private final Logger logger = LoggerFactory.getLogger(GetKM200Runnable.class);
481 private final Map<Channel, JsonObject> sendMap;
483 public GetKM200Runnable(Map<Channel, JsonObject> sendMap, KM200GatewayHandler gatewayHandler,
484 KM200Device remoteDevice) {
485 this.sendMap = sendMap;
486 this.gatewayHandler = gatewayHandler;
487 this.remoteDevice = remoteDevice;
492 logger.debug("GetKM200Runnable");
493 synchronized (remoteDevice) {
494 if (remoteDevice.getInited()) {
495 remoteDevice.resetAllUpdates(remoteDevice.serviceTreeMap);
496 updateChildren(sendMap, gatewayHandler, remoteDevice, null);
503 * The GetKM200Runnable class get the data from device for one channel.
505 private class GetSingleKM200Runnable implements Runnable {
507 private final Logger logger = LoggerFactory.getLogger(GetSingleKM200Runnable.class);
508 private final KM200GatewayHandler gatewayHandler;
509 private final KM200Device remoteDevice;
510 private final Channel channel;
511 private final Map<Channel, JsonObject> sendMap;
513 public GetSingleKM200Runnable(Map<Channel, JsonObject> sendMap, KM200GatewayHandler gatewayHandler,
514 KM200Device remoteDevice, Channel channel) {
515 this.gatewayHandler = gatewayHandler;
516 this.remoteDevice = remoteDevice;
517 this.channel = channel;
518 this.sendMap = sendMap;
523 logger.debug("GetKM200Runnable");
525 synchronized (remoteDevice) {
526 synchronized (sendMap) {
527 if (sendMap.containsKey(channel)) {
531 if (remoteDevice.getInited()) {
532 logger.trace("Checking: {} Root: {}", channel.getUID().getAsString(),
533 channel.getProperties().get("root"));
534 String chTypes = channel.getAcceptedItemType();
535 if (null == chTypes) {
536 logger.warn("Channel {} has not accepted item types", channel.getLabel());
539 String service = KM200Utils.checkParameterReplacement(channel, remoteDevice);
540 KM200ServiceObject object = remoteDevice.getServiceObject(service);
541 if (null != object) {
542 if (object.getVirtual() == 1) {
543 String parent = object.getParent();
544 updateChildren(sendMap, gatewayHandler, remoteDevice, parent);
546 object.setUpdated(false);
547 synchronized (sendMap) {
548 KM200ServiceObject serObjekt = remoteDevice.getServiceObject(service);
549 if (null != serObjekt) {
550 JsonObject obj = sendMap.get(channel);
552 state = dataHandler.parseJSONData(obj, serObjekt.getServiceType(), service,
553 chTypes, KM200Utils.getChannelConfigurationStrings(channel));
555 state = dataHandler.getProvidersState(service, chTypes,
556 KM200Utils.getChannelConfigurationStrings(channel));
561 gatewayHandler.updateState(channel.getUID(), state);
562 } catch (IllegalStateException e) {
563 logger.warn("Could not get updated item state", e);
575 * The sendKM200Thread class sends the data to the device.
577 private class SendKM200Runnable implements Runnable {
579 private final Logger logger = LoggerFactory.getLogger(SendKM200Runnable.class);
580 private final Map<Channel, JsonObject> newObject;
581 private final KM200Device remoteDevice;
583 public SendKM200Runnable(Map<Channel, JsonObject> newObject, KM200Device remoteDevice) {
584 this.newObject = newObject;
585 this.remoteDevice = remoteDevice;
590 logger.debug("Send-Executor started");
591 Map.Entry<Channel, JsonObject> nextEntry;
592 /* Check whether a new entry is availible, if yes then take and remove it */
595 synchronized (remoteDevice) {
596 synchronized (newObject) {
597 Iterator<Entry<Channel, JsonObject>> i = newObject.entrySet().iterator();
599 nextEntry = i.next();
603 if (nextEntry != null) {
604 /* Now send the data to the device */
605 Channel channel = nextEntry.getKey();
606 JsonObject newObject = nextEntry.getValue();
608 String service = KM200Utils.checkParameterReplacement(channel, remoteDevice);
609 KM200ServiceObject object = remoteDevice.getServiceObject(service);
610 if (null != object) {
611 if (object.getVirtual() == 0) {
612 remoteDevice.setServiceNode(service, newObject);
614 String parent = object.getParent();
615 if (null != parent) {
616 logger.trace("Sending: {} to : {}", newObject, service);
617 remoteDevice.setServiceNode(parent, newObject);
623 } while (nextEntry != null);