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.math.RoundingMode;
19 import java.time.ZonedDateTime;
20 import java.util.HashMap;
21 import java.util.List;
24 import org.eclipse.jdt.annotation.NonNullByDefault;
25 import org.eclipse.jdt.annotation.Nullable;
26 import org.openhab.binding.km200.internal.KM200Device;
27 import org.openhab.binding.km200.internal.KM200ServiceObject;
28 import org.openhab.core.library.CoreItemFactory;
29 import org.openhab.core.library.types.DateTimeType;
30 import org.openhab.core.library.types.DecimalType;
31 import org.openhab.core.library.types.OnOffType;
32 import org.openhab.core.library.types.StringType;
33 import org.openhab.core.types.Command;
34 import org.openhab.core.types.State;
35 import org.slf4j.Logger;
36 import org.slf4j.LoggerFactory;
38 import com.google.gson.JsonArray;
39 import com.google.gson.JsonObject;
40 import com.google.gson.JsonParseException;
41 import com.google.gson.JsonParser;
44 * The KM200DataHandler is managing the data handling between the device and items
46 * @author Markus Eckhardt - Initial contribution
49 public class KM200DataHandler {
50 private final Logger logger = LoggerFactory.getLogger(KM200DataHandler.class);
52 private final KM200Device remoteDevice;
54 public KM200DataHandler(KM200Device remoteDevice) {
55 this.remoteDevice = remoteDevice;
59 * This function checks the state of a service on the device
61 public @Nullable State getProvidersState(String service, String itemType, Map<String, String> itemPara) {
62 synchronized (remoteDevice) {
64 KM200ServiceObject object = null;
65 JsonObject jsonNode = null;
67 logger.trace("Check state of: {} item: {}", service, itemType);
68 if (remoteDevice.getBlacklistMap().contains(service)) {
69 logger.warn("Service on blacklist: {}", service);
72 if (remoteDevice.containsService(service)) {
73 object = remoteDevice.getServiceObject(service);
75 logger.warn("Serviceobject does not exist");
78 if (object.getReadable() == 0) {
79 logger.warn("Service is listed as protected (reading is not possible): {}", service);
82 type = object.getServiceType();
84 logger.warn("Service is not in the determined device service list: {}", service);
87 /* Needs to be updated? */
88 if (object.getVirtual() == 0) {
89 if (!object.getUpdated()) {
90 jsonNode = remoteDevice.getServiceNode(service);
91 if (jsonNode == null || jsonNode.isJsonNull()) {
92 logger.warn("Communication is not possible!");
95 object.setJSONData(jsonNode);
96 object.setUpdated(true);
98 /* If already updated then use the saved data */
99 jsonNode = object.getJSONData();
102 /* For using of virtual services only one receive on the parent service is needed */
103 String parent = object.getParent();
104 if (null != parent) {
105 KM200ServiceObject objParent = remoteDevice.getServiceObject(parent);
106 if (null != objParent) {
107 if (!objParent.getUpdated()) {
108 /* If it's a virtual service then receive the data from parent service */
109 jsonNode = remoteDevice.getServiceNode(parent);
110 if (jsonNode == null || jsonNode.isJsonNull()) {
111 logger.warn("Communication is not possible!");
114 objParent.setJSONData(jsonNode);
115 objParent.setUpdated(true);
116 object.setUpdated(true);
118 /* If already updated then use the saved data */
119 jsonNode = objParent.getJSONData();
124 if (null != jsonNode) {
125 return parseJSONData(jsonNode, type, service, itemType, itemPara);
133 * This function parses the receviced JSON Data and return the right state
135 public @Nullable State parseJSONData(JsonObject nodeRoot, String type, String service, String itemType,
136 Map<String, String> itemPara) {
138 KM200ServiceObject object = remoteDevice.getServiceObject(service);
139 if (null == object) {
142 String parent = object.getParent();
144 logger.trace("parseJSONData service: {}, data: {}", service, nodeRoot);
145 /* Now parsing of the JSON String depending on its type and the type of binding item */
147 if (nodeRoot.toString().length() == 2) {
148 logger.warn("Get empty reply");
152 case DATA_TYPE_STRING_VALUE: /* Check whether the type is a single value containing a string value */
153 logger.debug("parseJSONData type string value: {} Type: {}", nodeRoot, itemType);
154 String sVal = nodeRoot.get("value").getAsString();
155 object.setValue(sVal);
157 if ("Switch".equals(itemType)) {
158 // type is definitely correct here
159 Map<String, String> switchNames = itemPara;
160 if (switchNames.containsKey("on")) {
161 if (sVal.equals(switchNames.get("off"))) {
162 state = OnOffType.OFF;
163 } else if (sVal.equals(switchNames.get("on"))) {
164 state = OnOffType.ON;
166 } else if (switchNames.isEmpty()) {
167 logger.debug("No switch item configuration");
170 logger.warn("Switch-Item only on configured on/off string values: {}", nodeRoot);
173 /* NumberItem Binding */
174 } else if (CoreItemFactory.NUMBER.equals(itemType)) {
176 state = new DecimalType(Float.parseFloat(sVal));
177 } catch (NumberFormatException e) {
178 logger.warn("Conversion of the string value to Decimal wasn't possible, data: {} error: {}",
179 nodeRoot, e.getMessage());
182 /* DateTimeItem Binding */
183 } else if (CoreItemFactory.DATETIME.equals(itemType)) {
185 state = new DateTimeType(sVal);
186 } catch (IllegalArgumentException e) {
188 "Conversion of the string value to DateTime wasn't possible, data: {} error: {}",
189 nodeRoot, e.getMessage());
192 /* StringItem Binding */
193 } else if (CoreItemFactory.STRING.equals(itemType)) {
194 state = new StringType(sVal);
196 logger.info("Bindingtype not supported for string values: {}", itemType.getClass());
200 case DATA_TYPE_FLOAT_VALUE: /* Check whether the type is a single value containing a float value */
201 logger.trace("state of type float value: {}", nodeRoot);
204 bdVal = new BigDecimal(nodeRoot.get("value").getAsString()).setScale(1, RoundingMode.HALF_UP);
205 } catch (NumberFormatException e) {
208 object.setValue(bdVal);
209 /* NumberItem Binding */
210 if (CoreItemFactory.NUMBER.equals(itemType)) {
211 if (bdVal instanceof Double) { // Checking whether
212 state = new DecimalType((Double) bdVal);
214 state = new DecimalType(((Number) bdVal).doubleValue());
216 /* StringItem Binding */
217 } else if (CoreItemFactory.STRING.equals(itemType)) {
218 state = new StringType(bdVal.toString());
220 logger.info("Bindingtype not supported for float values: {}", itemType.getClass());
224 case DATA_TYPE_SWITCH_PROGRAM: /* Check whether the type is a switchProgram */
225 KM200SwitchProgramServiceHandler sPService = null;
226 logger.trace("state of type switchProgram: {}", nodeRoot);
227 if (null != parent) {
228 KM200ServiceObject objParent = remoteDevice.getServiceObject(parent);
229 if (null != objParent) {
230 /* Get the KM200SwitchProgramService class object with all specific parameters */
231 if (object.getVirtual() == 0) {
232 sPService = ((KM200SwitchProgramServiceHandler) object.getValueParameter());
234 sPService = ((KM200SwitchProgramServiceHandler) objParent.getValueParameter());
236 if (null != sPService) {
237 /* Update the switches inside the KM200SwitchProgramService */
238 sPService.updateSwitches(nodeRoot, remoteDevice);
240 /* the parsing of switch program-services have to be outside, using json in strings */
241 if (object.getVirtual() == 1) {
242 return this.getVirtualState(object, itemType, service);
245 * if access to the parent non virtual service the return the switchPoints jsonarray
247 if (CoreItemFactory.STRING.equals(itemType)) {
248 state = new StringType(
249 nodeRoot.get("switchPoints").getAsJsonArray().toString());
252 "Bindingtype not supported for switchProgram, only json over strings supported: {}",
253 itemType.getClass());
262 case DATA_TYPE_ERROR_LIST: /* Check whether the type is an errorList */
263 KM200ErrorServiceHandler eService = null;
264 logger.trace("state of type errorList: {}", nodeRoot);
265 if (null != parent) {
266 KM200ServiceObject objParent = remoteDevice.getServiceObject(parent);
267 if (null != objParent) {
268 /* Get the KM200ErrorService class object with all specific parameters */
269 if (object.getVirtual() == 0) {
270 eService = ((KM200ErrorServiceHandler) object.getValueParameter());
272 eService = ((KM200ErrorServiceHandler) objParent.getValueParameter());
274 if (null != eService) {
275 /* Update the switches inside the KM200SwitchProgramService */
276 eService.updateErrors(nodeRoot);
278 /* the parsing of switch program-services have to be outside, using json in strings */
279 if (object.getVirtual() == 1) {
280 return this.getVirtualState(object, itemType, service);
283 * if access to the parent non virtual service the return the switchPoints jsonarray
285 if (CoreItemFactory.STRING.equals(itemType)) {
286 state = new StringType(nodeRoot.get("values").getAsJsonArray().toString());
289 "Bindingtype not supported for error list, only json over strings is supported: {}",
290 itemType.getClass());
297 case DATA_TYPE_Y_RECORDING: /* Check whether the type is a yRecording */
298 logger.info("state of: type yRecording is not supported yet: {}", nodeRoot);
299 /* have to be completed */
301 case DATA_TYPE_SYSTEM_INFO: /* Check whether the type is a systeminfo */
302 logger.info("state of: type systeminfo is not supported yet: {}", nodeRoot);
303 /* have to be completed */
305 case DATA_TYPE_ARRAY_DATA: /* Check whether the type is an arrayData */
306 logger.info("state of: type arrayData is not supported yet: {}", nodeRoot);
307 /* have to be completed */
309 case DATA_TYPE_E_MONITORING_LIST: /* Check whether the type is an eMonitoringList */
310 logger.info("state of: type eMonitoringList is not supported yet: {}", nodeRoot);
311 /* have to be completed */
314 } catch (JsonParseException e) {
315 logger.warn("Parsingexception in JSON, data: {} error: {} ", nodeRoot, e.getMessage());
321 * This function checks the virtual state of a service
323 private @Nullable State getVirtualState(KM200ServiceObject object, String itemType, String service) {
325 String type = object.getServiceType();
326 String parent = object.getParent();
328 if (null != parent) {
329 KM200ServiceObject objParent = remoteDevice.getServiceObject(parent);
330 if (null != objParent) {
331 logger.trace("Check virtual state of: {} type: {} item: {}", service, type, itemType);
333 case DATA_TYPE_SWITCH_PROGRAM:
334 KM200SwitchProgramServiceHandler sPService = ((KM200SwitchProgramServiceHandler) objParent
335 .getValueParameter());
336 if (null != sPService) {
337 String[] servicePath = service.split("/");
338 String virtService = servicePath[servicePath.length - 1];
339 if ("weekday".equals(virtService)) {
340 if (CoreItemFactory.STRING.equals(itemType)) {
341 String actDay = sPService.getActiveDay();
342 state = new StringType(actDay);
344 logger.info("Bindingtype not supported for day service: {}", itemType.getClass());
347 } else if ("nbrCycles".equals(virtService)) {
348 if (CoreItemFactory.NUMBER.equals(itemType)) {
349 Integer nbrCycles = sPService.getNbrCycles();
350 state = new DecimalType(nbrCycles);
352 logger.info("Bindingtype not supported for nbrCycles service: {}",
353 itemType.getClass());
356 } else if ("cycle".equals(virtService)) {
357 if (CoreItemFactory.NUMBER.equals(itemType)) {
358 Integer cycle = sPService.getActiveCycle();
359 state = new DecimalType(cycle);
361 logger.info("Bindingtype not supported for cycle service: {}", itemType.getClass());
364 } else if (virtService.equals(sPService.getPositiveSwitch())) {
365 if (CoreItemFactory.NUMBER.equals(itemType)) {
366 Integer minutes = sPService.getActivePositiveSwitch();
367 state = new DecimalType(minutes);
368 } else if (CoreItemFactory.DATETIME.equals(itemType)) {
369 Integer minutes = sPService.getActivePositiveSwitch();
370 ZonedDateTime rightNow = ZonedDateTime.now();
371 rightNow.minusHours(rightNow.getHour());
372 rightNow.minusMinutes(rightNow.getMinute());
373 rightNow.plusSeconds(minutes * 60 - rightNow.getOffset().getTotalSeconds());
374 state = new DateTimeType(rightNow);
376 logger.info("Bindingtype not supported for cycle service: {}", itemType);
379 } else if (virtService.equals(sPService.getNegativeSwitch())) {
380 if (CoreItemFactory.NUMBER.equals(itemType)) {
381 Integer minutes = sPService.getActiveNegativeSwitch();
382 state = new DecimalType(minutes);
383 } else if (CoreItemFactory.DATETIME.equals(itemType)) {
384 Integer minutes = sPService.getActiveNegativeSwitch();
385 ZonedDateTime rightNow = ZonedDateTime.now();
386 rightNow.minusHours(rightNow.getHour());
387 rightNow.minusMinutes(rightNow.getMinute());
388 rightNow.plusSeconds(minutes * 60 - rightNow.getOffset().getTotalSeconds());
389 state = new DateTimeType(rightNow);
391 logger.info("Bindingtype not supported for cycle service: {}", itemType.getClass());
399 case DATA_TYPE_ERROR_LIST:
400 KM200ErrorServiceHandler eService = ((KM200ErrorServiceHandler) objParent.getValueParameter());
401 if (null != eService) {
402 String[] nServicePath = service.split("/");
403 String nVirtService = nServicePath[nServicePath.length - 1];
404 /* Go through the parameters and read the values */
405 switch (nVirtService) {
407 if (CoreItemFactory.NUMBER.equals(itemType)) {
408 Integer nbrErrors = eService.getNbrErrors();
409 state = new DecimalType(nbrErrors);
411 logger.info("Bindingtype not supported for error number service: {}",
412 itemType.getClass());
417 if (CoreItemFactory.NUMBER.equals(itemType)) {
418 Integer actError = eService.getActiveError();
419 state = new DecimalType(actError);
421 logger.info("Bindingtype not supported for error service: {}",
422 itemType.getClass());
427 if (CoreItemFactory.STRING.equals(itemType)) {
428 String errorString = eService.getErrorString();
429 if (errorString == null) {
432 state = new StringType(errorString);
434 logger.info("Bindingtype not supported for error string service: {}",
435 itemType.getClass());
451 * This function sets the state of a service on the device
453 public @Nullable JsonObject sendProvidersState(String service, Command command, String itemType, Object itemPara) {
454 synchronized (remoteDevice) {
456 KM200ServiceObject object;
457 JsonObject newObject;
459 logger.debug("Prepare item for send: {} zitem: {}", service, itemType);
460 if (remoteDevice.getBlacklistMap().contains(service)) {
461 logger.debug("Service on blacklist: {}", service);
464 if (remoteDevice.containsService(service)) {
465 object = remoteDevice.getServiceObject(service);
466 if (null == object) {
467 logger.debug("Object is null");
470 if (object.getWriteable() == 0) {
471 logger.warn("Service is listed as read-only: {}", service);
474 type = object.getServiceType();
476 logger.warn("Service is not in the determined device service list: {}", service);
479 /* The service is availible, set now the values depeding on the item and binding type */
480 logger.trace("state of: {} type: {}", command, type);
481 /* Binding is a NumberItem */
482 if (CoreItemFactory.NUMBER.equals(itemType)) {
483 BigDecimal bdVal = ((DecimalType) command).toBigDecimal();
484 /* Check the capabilities of this service */
485 if (object.getValueParameter() != null) {
486 // type is definitely correct here
487 @SuppressWarnings("unchecked")
488 List<BigDecimal> valParas = (List<BigDecimal>) object.getValueParameter();
489 if (null != valParas) {
490 BigDecimal minVal = valParas.get(0);
491 BigDecimal maxVal = valParas.get(1);
492 if (bdVal.compareTo(minVal) < 0) {
495 if (bdVal.compareTo(maxVal) > 0) {
500 newObject = new JsonObject();
501 if (DATA_TYPE_FLOAT_VALUE.equals(type)) {
502 newObject.addProperty("value", bdVal);
503 } else if (DATA_TYPE_STRING_VALUE.equals(type)) {
504 newObject.addProperty("value", bdVal.toString());
505 } else if (DATA_TYPE_SWITCH_PROGRAM.equals(type) && object.getVirtual() == 1) {
506 /* A switchProgram as NumberItem is always virtual */
507 newObject = sendVirtualState(object, service, command, itemType);
508 } else if (DATA_TYPE_ERROR_LIST.equals(type) && object.getVirtual() == 1) {
509 /* An errorList as NumberItem is always virtual */
510 newObject = sendVirtualState(object, service, command, itemType);
512 logger.info("Not supported type for numberItem: {}", type);
514 /* Binding is a StringItem */
515 } else if (CoreItemFactory.STRING.equals(itemType)) {
516 String val = ((StringType) command).toString();
517 newObject = new JsonObject();
518 /* Check the capabilities of this service */
519 if (object.getValueParameter() != null) {
520 // type is definitely correct here
521 @SuppressWarnings("unchecked")
522 List<String> valParas = (List<String>) object.getValueParameter();
523 if (null != valParas) {
524 if (!valParas.contains(val)) {
525 logger.warn("Parameter is not in the service parameterlist: {}", val);
530 if (DATA_TYPE_STRING_VALUE.equals(type)) {
531 newObject.addProperty("value", val);
532 } else if (DATA_TYPE_FLOAT_VALUE.equals(type)) {
533 newObject.addProperty("value", Float.parseFloat(val));
534 } else if (DATA_TYPE_SWITCH_PROGRAM.equals(type)) {
535 if (object.getVirtual() == 1) {
536 newObject = sendVirtualState(object, service, command, itemType);
538 /* The JSONArray of switch items can be send directly */
540 /* Check whether this input string is a valid JSONArray */
541 JsonArray userArray = (JsonArray) JsonParser.parseString(val);
542 newObject = userArray.getAsJsonObject();
543 } catch (JsonParseException e) {
544 logger.warn("The input for the switchProgram is not a valid JSONArray : {}",
550 logger.info("Not supported type for stringItem: {}", type);
552 /* Binding is a DateTimeItem */
553 } else if (CoreItemFactory.DATETIME.equals(itemType)) {
554 String val = ((DateTimeType) command).toString();
555 newObject = new JsonObject();
556 if (DATA_TYPE_STRING_VALUE.equals(type)) {
557 newObject.addProperty("value", val);
558 } else if (DATA_TYPE_SWITCH_PROGRAM.equals(type)) {
559 newObject = sendVirtualState(object, service, command, itemType);
561 logger.info("Not supported type for dateTimeItem: {}", type);
563 } else if ("Switch".equals(itemType)) {
565 newObject = new JsonObject();
566 // type is definitely correct here
567 @SuppressWarnings("unchecked")
568 Map<String, String> switchNames = (HashMap<String, String>) itemPara;
569 if (switchNames.containsKey("on")) {
570 if (command == OnOffType.OFF) {
571 val = switchNames.get("off");
572 } else if (command == OnOffType.ON) {
573 val = switchNames.get("on");
575 // type is definitely correct here
576 @SuppressWarnings("unchecked")
577 List<String> valParas = (List<String>) object.getValueParameter();
578 if (null != valParas) {
579 if (!valParas.contains(val)) {
580 logger.warn("Parameter is not in the service parameterlist: {}", val);
584 } else if (switchNames.isEmpty()) {
585 logger.debug("No switch item configuration");
588 logger.info("Switch-Item only on configured on/off string values {}", command);
591 if (DATA_TYPE_STRING_VALUE.equals(type)) {
592 newObject.addProperty("value", val);
594 logger.info("Not supported type for SwitchItem:{}", type);
597 logger.info("Bindingtype not supported: {}", itemType.getClass());
600 /* If some data is availible then we have to send it to device */
601 if (newObject != null && newObject.toString().length() > 2) {
602 logger.trace("Send Data: {}", newObject);
611 * This function sets the state of a virtual service
613 public @Nullable JsonObject sendVirtualState(KM200ServiceObject object, String service, Command command,
615 JsonObject newObject = null;
617 logger.trace("Check virtual state of: {} type: {} item: {}", service, type, itemType);
618 String parent = object.getParent();
619 if (null != parent) {
620 KM200ServiceObject objParent = remoteDevice.getServiceObject(parent);
621 if (null != objParent) {
622 type = object.getServiceType();
623 /* Binding is a StringItem */
624 if (CoreItemFactory.STRING.equals(itemType)) {
625 String val = ((StringType) command).toString();
627 case DATA_TYPE_SWITCH_PROGRAM:
628 KM200SwitchProgramServiceHandler sPService = ((KM200SwitchProgramServiceHandler) objParent
629 .getValueParameter());
630 if (null != sPService) {
631 String[] servicePath = service.split("/");
632 String virtService = servicePath[servicePath.length - 1];
633 if ("weekday".equals(virtService)) {
634 /* Only parameter changing without communication to device */
635 sPService.setActiveDay(val);
640 /* Binding is a NumberItem */
641 } else if (CoreItemFactory.NUMBER.equals(itemType)) {
642 Integer val = ((DecimalType) command).intValue();
644 case DATA_TYPE_SWITCH_PROGRAM:
645 KM200SwitchProgramServiceHandler sPService = ((KM200SwitchProgramServiceHandler) objParent
646 .getValueParameter());
647 if (null != sPService) {
648 String[] servicePath = service.split("/");
649 String virtService = servicePath[servicePath.length - 1];
650 if ("cycle".equals(virtService)) {
651 /* Only parameter changing without communication to device */
652 sPService.setActiveCycle(val);
653 } else if (virtService.equals(sPService.getPositiveSwitch())) {
654 sPService.setActivePositiveSwitch(val);
655 /* Create a JSON Array from current switch configuration */
656 newObject = sPService.getUpdatedJSONData(objParent);
657 } else if (virtService.equals(sPService.getNegativeSwitch())) {
658 sPService.setActiveNegativeSwitch(val);
659 /* Create a JSON Array from current switch configuration */
660 newObject = sPService.getUpdatedJSONData(objParent);
664 case DATA_TYPE_ERROR_LIST:
665 KM200ErrorServiceHandler eService = ((KM200ErrorServiceHandler) objParent
666 .getValueParameter());
667 if (null != eService) {
668 String[] nServicePath = service.split("/");
669 String nVirtService = nServicePath[nServicePath.length - 1];
670 if ("error".equals(nVirtService)) {
671 /* Only parameter changing without communication to device */
672 eService.setActiveError(val);
677 } else if (CoreItemFactory.DATETIME.equals(itemType)) {
678 ZonedDateTime swTime = ((DateTimeType) command).getZonedDateTime();
679 KM200SwitchProgramServiceHandler sPService = ((KM200SwitchProgramServiceHandler) objParent
680 .getValueParameter());
681 if (null != sPService) {
682 String[] servicePath = service.split("/");
683 String virtService = servicePath[servicePath.length - 1];
684 Integer minutes = swTime.getHour() * 60 + swTime.getMinute()
685 + swTime.getOffset().getTotalSeconds() % 60;
686 minutes = (minutes % sPService.getSwitchPointTimeRaster())
687 * sPService.getSwitchPointTimeRaster();
688 if (virtService.equals(sPService.getPositiveSwitch())) {
689 sPService.setActivePositiveSwitch(minutes);
691 if (virtService.equals(sPService.getNegativeSwitch())) {
692 sPService.setActiveNegativeSwitch(minutes);
694 /* Create a JSON Array from current switch configuration */
695 newObject = sPService.getUpdatedJSONData(objParent);