2 * Copyright (c) 2010-2020 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.apache.commons.lang.StringUtils;
34 import org.eclipse.jdt.annotation.NonNullByDefault;
35 import org.eclipse.jdt.annotation.Nullable;
36 import org.eclipse.jetty.client.HttpClient;
37 import org.openhab.binding.km200.internal.KM200Device;
38 import org.openhab.binding.km200.internal.KM200ServiceObject;
39 import org.openhab.binding.km200.internal.KM200ThingType;
40 import org.openhab.binding.km200.internal.KM200Utils;
41 import org.openhab.core.common.NamedThreadFactory;
42 import org.openhab.core.config.core.Configuration;
43 import org.openhab.core.library.types.DateTimeType;
44 import org.openhab.core.library.types.DecimalType;
45 import org.openhab.core.library.types.StringType;
46 import org.openhab.core.thing.Bridge;
47 import org.openhab.core.thing.Channel;
48 import org.openhab.core.thing.ChannelUID;
49 import org.openhab.core.thing.Thing;
50 import org.openhab.core.thing.ThingStatus;
51 import org.openhab.core.thing.ThingStatusDetail;
52 import org.openhab.core.thing.ThingStatusInfo;
53 import org.openhab.core.thing.ThingTypeUID;
54 import org.openhab.core.thing.binding.BaseBridgeHandler;
55 import org.openhab.core.types.Command;
56 import org.openhab.core.types.State;
57 import org.slf4j.Logger;
58 import org.slf4j.LoggerFactory;
60 import com.google.gson.JsonObject;
61 import com.google.gson.JsonParseException;
64 * The {@link KM200GatewayHandler} is responsible for handling commands, which are
65 * sent to one of the channels.
67 * @author Markus Eckhardt - Initial contribution
70 public class KM200GatewayHandler extends BaseBridgeHandler {
72 private final Logger logger = LoggerFactory.getLogger(KM200GatewayHandler.class);
74 public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections.singleton(THING_TYPE_KMDEVICE);
76 private final Map<Channel, JsonObject> sendMap = Collections.synchronizedMap(new LinkedHashMap<>());
78 private List<KM200GatewayStatusListener> listeners = new CopyOnWriteArrayList<>();
81 * shared instance of HTTP client for (a)synchronous calls
83 private ScheduledExecutorService executor;
84 private final KM200Device remoteDevice;
85 private final KM200DataHandler dataHandler;
86 private int readDelay;
87 private int refreshInterval;
89 public KM200GatewayHandler(Bridge bridge, HttpClient httpClient) {
91 refreshInterval = 120;
93 updateStatus(ThingStatus.UNINITIALIZED, ThingStatusDetail.CONFIGURATION_PENDING);
94 remoteDevice = new KM200Device(httpClient);
95 dataHandler = new KM200DataHandler(remoteDevice);
96 executor = Executors.newScheduledThreadPool(2, new NamedThreadFactory("org.openhab.binding.km200", true));
100 public void handleCommand(ChannelUID channelUID, Command command) {
101 Channel channel = getThing().getChannel(channelUID.getId());
102 if (null != channel) {
103 if (command instanceof DateTimeType || command instanceof DecimalType || command instanceof StringType) {
104 prepareMessage(thing, channel, command);
106 logger.warn("Unsupported Command: {} Class: {}", command.toFullString(), command.getClass());
112 public void initialize() {
113 if (!getDevice().getInited()) {
114 logger.info("Update KM50/100/200 gateway configuration, it takes a minute....");
116 if (getDevice().isConfigured()) {
117 if (!checkConfiguration()) {
120 /* configuration and communication seems to be ok */
122 updateStatus(ThingStatus.ONLINE);
124 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "No bridge configured");
125 logger.debug("The KM50/100/200 gateway configuration is not complete");
129 SendKM200Runnable sendRunnable = new SendKM200Runnable(sendMap, getDevice());
130 GetKM200Runnable receivingRunnable = new GetKM200Runnable(sendMap, this, getDevice());
131 if (!executor.isTerminated()) {
132 executor = Executors.newScheduledThreadPool(2,
133 new NamedThreadFactory("org.openhab.binding.km200", true));
134 executor.scheduleWithFixedDelay(receivingRunnable, 30, refreshInterval, TimeUnit.SECONDS);
135 executor.scheduleWithFixedDelay(sendRunnable, 60, refreshInterval * 2, TimeUnit.SECONDS);
141 public void dispose() {
144 if (!executor.awaitTermination(60000, TimeUnit.SECONDS)) {
145 logger.debug("Services didn't finish in 60000 seconds!");
147 } catch (InterruptedException e) {
148 executor.shutdownNow();
150 synchronized (getDevice()) {
151 getDevice().setInited(false);
152 getDevice().setIP4Address("");
153 getDevice().setCryptKeyPriv("");
154 getDevice().setMD5Salt("");
155 getDevice().setGatewayPassword("");
156 getDevice().setPrivatePassword("");
157 getDevice().serviceTreeMap.clear();
159 updateStatus(ThingStatus.OFFLINE);
163 public void handleRemoval() {
164 for (Thing actThing : getThing().getThings()) {
165 actThing.setStatusInfo(new ThingStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE, ""));
167 this.updateStatus(ThingStatus.REMOVED);
171 * Gets bridges configuration
173 private void getConfiguration() {
174 Configuration configuration = getConfig();
175 for (String key : configuration.keySet()) {
176 logger.trace("initialize Key: {} Value: {}", key, configuration.get(key));
179 String ip = (String) configuration.get("ip4Address");
180 if (StringUtils.isNotBlank(ip)) {
182 InetAddress.getByName(ip);
183 } catch (UnknownHostException e) {
184 logger.warn("IP4_address is not valid!: {}", ip);
186 getDevice().setIP4Address(ip);
188 logger.debug("No ip4_address configured!");
192 String privateKey = (String) configuration.get("privateKey");
193 if (StringUtils.isNotBlank(privateKey)) {
194 getDevice().setCryptKeyPriv(privateKey);
198 String md5Salt = (String) configuration.get("md5Salt");
199 if (StringUtils.isNotBlank(md5Salt)) {
200 getDevice().setMD5Salt(md5Salt);
203 case "gatewayPassword":
204 String gatewayPassword = (String) configuration.get("gatewayPassword");
205 if (StringUtils.isNotBlank(gatewayPassword)) {
206 getDevice().setGatewayPassword(gatewayPassword);
209 case "privatePassword":
210 String privatePassword = (String) configuration.get("privatePassword");
211 if (StringUtils.isNotBlank(privatePassword)) {
212 getDevice().setPrivatePassword(privatePassword);
215 case "refreshInterval":
216 refreshInterval = ((BigDecimal) configuration.get("refreshInterval")).intValue();
217 logger.debug("Set refresh interval to: {} seconds.", refreshInterval);
220 readDelay = ((BigDecimal) configuration.get("readDelay")).intValue();
221 logger.debug("Set read delay to: {} seconds.", readDelay);
223 case "maxNbrRepeats":
224 Integer maxNbrRepeats = ((BigDecimal) configuration.get("maxNbrRepeats")).intValue();
225 logger.debug("Set max. number of repeats to: {} seconds.", maxNbrRepeats);
226 remoteDevice.setMaxNbrRepeats(maxNbrRepeats);
233 * Checks bridges configuration
235 private boolean checkConfiguration() {
236 /* Get HTTP Data from device */
237 JsonObject nodeRoot = remoteDevice.getServiceNode("/gateway/DateTime");
238 if (nodeRoot == null || nodeRoot.isJsonNull()) {
239 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
240 "No communication possible with gateway");
243 logger.debug("Test of the communication to the gateway was successful..");
245 /* Testing the received data, is decryption working? */
247 nodeRoot.get("type").getAsString();
248 nodeRoot.get("id").getAsString();
249 } catch (JsonParseException e) {
250 logger.debug("The data is not readable, check the key and password configuration! {}", e.getMessage());
251 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Wrong gateway configuration");
258 * Reads the devices capabilities and sets the data structures
260 private void readCapabilities() {
261 KM200VirtualServiceHandler virtualServiceHandler;
262 /* Checking of the device specific services and creating of a service list */
263 for (KM200ThingType thing : KM200ThingType.values()) {
264 String rootPath = thing.getRootPath();
265 if (!rootPath.isEmpty() && (rootPath.indexOf("/", 0) == rootPath.lastIndexOf("/", rootPath.length() - 1))) {
266 if (remoteDevice.getBlacklistMap().contains(thing.getRootPath())) {
267 logger.debug("Service on blacklist: {}", thing.getRootPath());
270 KM200ServiceHandler serviceHandler = new KM200ServiceHandler(thing.getRootPath(), null, remoteDevice);
271 serviceHandler.initObject();
274 /* Now init the virtual services */
275 virtualServiceHandler = new KM200VirtualServiceHandler(remoteDevice);
276 virtualServiceHandler.initVirtualObjects();
277 /* Output all available services in the log file */
278 getDevice().listAllServices();
279 updateBridgeProperties();
280 getDevice().setInited(true);
284 * Adds a GatewayConnectedListener
286 public void addGatewayStatusListener(KM200GatewayStatusListener listener) {
287 listeners.add(listener);
288 listener.gatewayStatusChanged(getThing().getStatus());
292 * Removes a GatewayConnectedListener
294 public void removeHubStatusListener(KM200GatewayStatusListener listener) {
295 listeners.remove(listener);
299 * Refreshes a channel
301 public void refreshChannel(Channel channel) {
302 GetSingleKM200Runnable runnable = new GetSingleKM200Runnable(sendMap, this, getDevice(), channel);
303 logger.debug("starting single runnable.");
304 scheduler.submit(runnable);
308 * Updates bridges properties
310 private void updateBridgeProperties() {
311 List<String> propertyServices = new ArrayList<>();
312 propertyServices.add(KM200ThingType.GATEWAY.getRootPath());
313 propertyServices.add(KM200ThingType.SYSTEM.getRootPath());
314 Map<String, String> bridgeProperties = editProperties();
316 for (KM200ThingType tType : KM200ThingType.values()) {
317 List<String> asProperties = tType.asBridgeProperties();
318 String rootPath = tType.getRootPath();
319 if (rootPath.isEmpty()) {
322 KM200ServiceObject serObj = getDevice().getServiceObject(rootPath);
323 if (null != serObj) {
324 for (String subKey : asProperties) {
325 if (serObj.serviceTreeMap.containsKey(subKey)) {
326 KM200ServiceObject subKeyObj = serObj.serviceTreeMap.get(subKey);
327 String subKeyType = subKeyObj.getServiceType();
328 if (!DATA_TYPE_STRING_VALUE.equals(subKeyType) && !DATA_TYPE_FLOAT_VALUE.equals(subKeyType)) {
331 if (bridgeProperties.containsKey(subKey)) {
332 bridgeProperties.remove(subKey);
334 Object value = subKeyObj.getValue();
335 logger.trace("Add Property: {} :{}", subKey, value);
337 bridgeProperties.put(subKey, value.toString());
343 updateProperties(bridgeProperties);
347 * Prepares a message for sending
349 public void prepareMessage(Thing thing, Channel channel, Command command) {
350 if (getDevice().getInited()) {
351 JsonObject newObject = null;
353 String service = KM200Utils.checkParameterReplacement(channel, getDevice());
354 String chTypes = channel.getAcceptedItemType();
355 if (null == chTypes) {
356 logger.warn("Channel {} has not accepted item types", channel.getLabel());
359 logger.trace("handleCommand channel: {} service: {}", channel.getLabel(), service);
360 newObject = dataHandler.sendProvidersState(service, command, chTypes,
361 KM200Utils.getChannelConfigurationStrings(channel));
362 synchronized (getDevice()) {
363 KM200ServiceObject serObjekt = getDevice().getServiceObject(service);
364 if (null != serObjekt) {
365 if (newObject != null) {
366 sendMap.put(channel, newObject);
367 } else if (getDevice().containsService(service) && serObjekt.getVirtual() == 1) {
368 String parent = serObjekt.getParent();
369 for (Thing actThing : getThing().getThings()) {
370 logger.trace("Checking: {}", actThing.getUID().getAsString());
371 for (Channel tmpChannel : actThing.getChannels()) {
372 String tmpChTypes = tmpChannel.getAcceptedItemType();
373 if (null == tmpChTypes) {
374 logger.warn("Channel {} has not accepted item types", tmpChannel.getLabel());
377 String actService = KM200Utils.checkParameterReplacement(tmpChannel, getDevice());
378 KM200ServiceObject actSerObjekt = getDevice().getServiceObject(actService);
379 if (null != actSerObjekt) {
380 String actParent = actSerObjekt.getParent();
381 if (actParent != null && actParent.equals(parent)) {
382 state = dataHandler.getProvidersState(actService, tmpChTypes,
383 KM200Utils.getChannelConfigurationStrings(tmpChannel));
386 updateState(tmpChannel.getUID(), state);
387 } catch (IllegalStateException e) {
388 logger.warn("Could not get updated item state", e);
396 logger.debug("Service is not availible: {}", service);
404 * Update the children
406 // Every thing has here a handler
407 private void updateChildren(Map<Channel, JsonObject> sendMap, KM200GatewayHandler gatewayHandler,
408 KM200Device remoteDevice, @Nullable String parent) {
410 synchronized (remoteDevice) {
411 if (parent != null) {
412 KM200ServiceObject serParObjekt = remoteDevice.getServiceObject(parent);
413 if (null != serParObjekt) {
414 serParObjekt.setUpdated(false);
417 for (Thing actThing : gatewayHandler.getThing().getThings()) {
418 for (Channel actChannel : actThing.getChannels()) {
419 String actChTypes = actChannel.getAcceptedItemType();
420 if (null == actChTypes) {
421 logger.warn("Channel {} has not accepted item types", actChannel.getLabel());
424 logger.trace("Checking: {} Root: {}", actChannel.getUID().getAsString(),
425 actChannel.getProperties().get("root"));
426 KM200ThingHandler actHandler = (KM200ThingHandler) actThing.getHandler();
427 if (actHandler != null) {
428 if (!actHandler.checkLinked(actChannel)) {
434 String tmpService = KM200Utils.checkParameterReplacement(actChannel, remoteDevice);
435 KM200ServiceObject tmpSerObjekt = remoteDevice.getServiceObject(tmpService);
436 if (null != tmpSerObjekt) {
437 if (parent == null || parent.equals(tmpSerObjekt.getParent())) {
438 synchronized (sendMap) {
439 JsonObject obj = sendMap.get(actChannel);
441 state = dataHandler.parseJSONData(obj, tmpSerObjekt.getServiceType(), tmpService,
442 actChTypes, KM200Utils.getChannelConfigurationStrings(actChannel));
444 state = dataHandler.getProvidersState(tmpService, actChTypes,
445 KM200Utils.getChannelConfigurationStrings(actChannel));
450 gatewayHandler.updateState(actChannel.getUID(), state);
451 } catch (IllegalStateException e) {
452 logger.warn("Could not get updated item state", e);
457 Thread.sleep(readDelay);
458 } catch (InterruptedException e) {
468 * Return the device instance.
470 public KM200Device getDevice() {
475 * The GetKM200Runnable class get the data from device to all items.
477 private class GetKM200Runnable implements Runnable {
479 private final KM200GatewayHandler gatewayHandler;
480 private final KM200Device remoteDevice;
481 private final Logger logger = LoggerFactory.getLogger(GetKM200Runnable.class);
482 private final Map<Channel, JsonObject> sendMap;
484 public GetKM200Runnable(Map<Channel, JsonObject> sendMap, KM200GatewayHandler gatewayHandler,
485 KM200Device remoteDevice) {
486 this.sendMap = sendMap;
487 this.gatewayHandler = gatewayHandler;
488 this.remoteDevice = remoteDevice;
493 logger.debug("GetKM200Runnable");
494 synchronized (remoteDevice) {
495 if (remoteDevice.getInited()) {
496 remoteDevice.resetAllUpdates(remoteDevice.serviceTreeMap);
497 updateChildren(sendMap, gatewayHandler, remoteDevice, null);
504 * The GetKM200Runnable class get the data from device for one channel.
506 private class GetSingleKM200Runnable implements Runnable {
508 private final Logger logger = LoggerFactory.getLogger(GetSingleKM200Runnable.class);
509 private final KM200GatewayHandler gatewayHandler;
510 private final KM200Device remoteDevice;
511 private final Channel channel;
512 private final Map<Channel, JsonObject> sendMap;
514 public GetSingleKM200Runnable(Map<Channel, JsonObject> sendMap, KM200GatewayHandler gatewayHandler,
515 KM200Device remoteDevice, Channel channel) {
516 this.gatewayHandler = gatewayHandler;
517 this.remoteDevice = remoteDevice;
518 this.channel = channel;
519 this.sendMap = sendMap;
524 logger.debug("GetKM200Runnable");
526 synchronized (remoteDevice) {
527 synchronized (sendMap) {
528 if (sendMap.containsKey(channel)) {
532 if (remoteDevice.getInited()) {
533 logger.trace("Checking: {} Root: {}", channel.getUID().getAsString(),
534 channel.getProperties().get("root"));
535 String chTypes = channel.getAcceptedItemType();
536 if (null == chTypes) {
537 logger.warn("Channel {} has not accepted item types", channel.getLabel());
540 String service = KM200Utils.checkParameterReplacement(channel, remoteDevice);
541 KM200ServiceObject object = remoteDevice.getServiceObject(service);
542 if (null != object) {
543 if (object.getVirtual() == 1) {
544 String parent = object.getParent();
545 updateChildren(sendMap, gatewayHandler, remoteDevice, parent);
547 object.setUpdated(false);
548 synchronized (sendMap) {
549 KM200ServiceObject serObjekt = remoteDevice.getServiceObject(service);
550 if (null != serObjekt) {
551 JsonObject obj = sendMap.get(channel);
553 state = dataHandler.parseJSONData(obj, serObjekt.getServiceType(), service,
554 chTypes, KM200Utils.getChannelConfigurationStrings(channel));
556 state = dataHandler.getProvidersState(service, chTypes,
557 KM200Utils.getChannelConfigurationStrings(channel));
562 gatewayHandler.updateState(channel.getUID(), state);
563 } catch (IllegalStateException e) {
564 logger.warn("Could not get updated item state", e);
576 * The sendKM200Thread class sends the data to the device.
578 private class SendKM200Runnable implements Runnable {
580 private final Logger logger = LoggerFactory.getLogger(SendKM200Runnable.class);
581 private final Map<Channel, JsonObject> newObject;
582 private final KM200Device remoteDevice;
584 public SendKM200Runnable(Map<Channel, JsonObject> newObject, KM200Device remoteDevice) {
585 this.newObject = newObject;
586 this.remoteDevice = remoteDevice;
591 logger.debug("Send-Executor started");
592 Map.Entry<Channel, JsonObject> nextEntry;
593 /* Check whether a new entry is availible, if yes then take and remove it */
596 synchronized (remoteDevice) {
597 synchronized (newObject) {
598 Iterator<Entry<Channel, JsonObject>> i = newObject.entrySet().iterator();
600 nextEntry = i.next();
604 if (nextEntry != null) {
605 /* Now send the data to the device */
606 Channel channel = nextEntry.getKey();
607 JsonObject newObject = nextEntry.getValue();
609 String service = KM200Utils.checkParameterReplacement(channel, remoteDevice);
610 KM200ServiceObject object = remoteDevice.getServiceObject(service);
611 if (null != object) {
612 if (object.getVirtual() == 0) {
613 remoteDevice.setServiceNode(service, newObject);
615 String parent = object.getParent();
616 if (null != parent) {
617 logger.trace("Sending: {} to : {}", newObject, service);
618 remoteDevice.setServiceNode(parent, newObject);
624 } while (nextEntry != null);