]> git.basschouten.com Git - openhab-addons.git/blob
36b18f30f19c92cf431a831e97478f44992a05a2
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2022 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.km200.internal.handler;
14
15 import static org.openhab.binding.km200.internal.KM200BindingConstants.*;
16
17 import java.math.BigDecimal;
18 import java.math.RoundingMode;
19 import java.net.URI;
20 import java.net.URISyntaxException;
21 import java.util.ArrayList;
22 import java.util.Collections;
23 import java.util.HashMap;
24 import java.util.HashSet;
25 import java.util.List;
26 import java.util.Map;
27 import java.util.Set;
28 import java.util.stream.Collectors;
29 import java.util.stream.Stream;
30
31 import org.eclipse.jdt.annotation.NonNullByDefault;
32 import org.eclipse.jdt.annotation.Nullable;
33 import org.openhab.binding.km200.internal.KM200ChannelTypeProvider;
34 import org.openhab.binding.km200.internal.KM200ServiceObject;
35 import org.openhab.binding.km200.internal.KM200ThingType;
36 import org.openhab.binding.km200.internal.KM200Utils;
37 import org.openhab.core.library.CoreItemFactory;
38 import org.openhab.core.library.types.DateTimeType;
39 import org.openhab.core.library.types.DecimalType;
40 import org.openhab.core.library.types.OnOffType;
41 import org.openhab.core.library.types.StringType;
42 import org.openhab.core.thing.Bridge;
43 import org.openhab.core.thing.Channel;
44 import org.openhab.core.thing.ChannelUID;
45 import org.openhab.core.thing.Thing;
46 import org.openhab.core.thing.ThingStatus;
47 import org.openhab.core.thing.ThingStatusDetail;
48 import org.openhab.core.thing.ThingTypeUID;
49 import org.openhab.core.thing.binding.BaseThingHandler;
50 import org.openhab.core.thing.binding.builder.ChannelBuilder;
51 import org.openhab.core.thing.binding.builder.ThingBuilder;
52 import org.openhab.core.thing.type.ChannelKind;
53 import org.openhab.core.thing.type.ChannelType;
54 import org.openhab.core.thing.type.ChannelTypeBuilder;
55 import org.openhab.core.thing.type.ChannelTypeUID;
56 import org.openhab.core.types.Command;
57 import org.openhab.core.types.RefreshType;
58 import org.openhab.core.types.StateDescriptionFragment;
59 import org.openhab.core.types.StateDescriptionFragmentBuilder;
60 import org.openhab.core.types.StateOption;
61 import org.slf4j.Logger;
62 import org.slf4j.LoggerFactory;
63
64 /**
65  * The {@link KM200ThingHandler} is responsible for handling commands, which are
66  * sent to one of the channels.
67  *
68  * @author Markus Eckhardt - Initial contribution
69  */
70 @NonNullByDefault
71 public class KM200ThingHandler extends BaseThingHandler {
72
73     private final Logger logger = LoggerFactory.getLogger(KM200ThingHandler.class);
74
75     public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections.unmodifiableSet(Stream
76             .of(THING_TYPE_DHW_CIRCUIT, THING_TYPE_HEATING_CIRCUIT, THING_TYPE_SOLAR_CIRCUIT, THING_TYPE_HEAT_SOURCE,
77                     THING_TYPE_SYSTEM_APPLIANCE, THING_TYPE_SYSTEM_HOLIDAYMODES, THING_TYPE_SYSTEM_SENSOR,
78                     THING_TYPE_GATEWAY, THING_TYPE_NOTIFICATION, THING_TYPE_SYSTEM, THING_TYPE_SYSTEMSTATES)
79             .collect(Collectors.toSet()));
80
81     private final KM200ChannelTypeProvider channelTypeProvider;
82
83     public KM200ThingHandler(Thing thing, KM200ChannelTypeProvider channelTypeProvider) {
84         super(thing);
85         this.channelTypeProvider = channelTypeProvider;
86     }
87
88     @Override
89     public void handleCommand(ChannelUID channelUID, Command command) {
90         Bridge bridge = this.getBridge();
91         if (bridge == null) {
92             return;
93         }
94         KM200GatewayHandler gateway = (KM200GatewayHandler) bridge.getHandler();
95         if (gateway == null) {
96             return;
97         }
98         Channel channel = getThing().getChannel(channelUID.getId());
99         if (null != channel) {
100             if (command instanceof DateTimeType || command instanceof DecimalType || command instanceof StringType
101                     || command instanceof OnOffType) {
102                 gateway.prepareMessage(this.getThing(), channel, command);
103             } else if (command instanceof RefreshType) {
104                 gateway.refreshChannel(channel);
105             } else {
106                 logger.warn("Unsupported Command: {} Class: {}", command.toFullString(), command.getClass());
107             }
108         }
109     }
110
111     /**
112      * Choose a tag for a channel
113      */
114     Set<String> checkTags(String unitOfMeasure, @Nullable Boolean readOnly) {
115         Set<String> tags = new HashSet<>();
116         if (unitOfMeasure.indexOf("°C") == 0 || unitOfMeasure.indexOf("K") == 0) {
117             if (null != readOnly) {
118                 if (readOnly) {
119                     tags.add("CurrentTemperature");
120                 } else {
121                     tags.add("TargetTemperature");
122                 }
123             }
124         }
125         return tags;
126     }
127
128     /**
129      * Choose a category for a channel
130      */
131     String checkCategory(String unitOfMeasure, String topCategory, @Nullable Boolean readOnly) {
132         String category = null;
133         if (unitOfMeasure.indexOf("°C") == 0 || unitOfMeasure.indexOf("K") == 0) {
134             if (null == readOnly) {
135                 category = topCategory;
136             } else {
137                 if (readOnly) {
138                     category = "Temperature";
139                 } else {
140                     category = "Heating";
141                 }
142             }
143         } else if (unitOfMeasure.indexOf("kW") == 0 || unitOfMeasure.indexOf("kWh") == 0) {
144             category = "Energy";
145         } else if (unitOfMeasure.indexOf("l/min") == 0 || unitOfMeasure.indexOf("l/h") == 0) {
146             category = "Flow";
147         } else if (unitOfMeasure.indexOf("Pascal") == 0 || unitOfMeasure.indexOf("bar") == 0) {
148             category = "Pressure";
149         } else if (unitOfMeasure.indexOf("rpm") == 0) {
150             category = "Flow";
151         } else if (unitOfMeasure.indexOf("mins") == 0 || unitOfMeasure.indexOf("minutes") == 0) {
152             category = "Time";
153         } else if (unitOfMeasure.indexOf("kg/l") == 0) {
154             category = "Oil";
155         } else if (unitOfMeasure.indexOf("%%") == 0) {
156             category = "Number";
157         } else {
158             category = topCategory;
159         }
160         return category;
161     }
162
163     /**
164      * Creates a new channel
165      */
166     @Nullable
167     Channel createChannel(ChannelTypeUID channelTypeUID, ChannelUID channelUID, String root, String type,
168             @Nullable String currentPathName, String description, String label, boolean addProperties,
169             boolean switchProgram, StateDescriptionFragment state, String unitOfMeasure) {
170         URI configDescriptionUriChannel = null;
171         Channel newChannel = null;
172         ChannelType channelType = null;
173         Map<String, String> chProperties = new HashMap<>();
174         String itemType = "";
175         String category = null;
176         if (CoreItemFactory.NUMBER.equals(type)) {
177             itemType = "NumberType";
178             category = "Number";
179         } else if (CoreItemFactory.STRING.equals(type)) {
180             itemType = "StringType";
181             category = "Text";
182         } else {
183             logger.info("Channeltype {} not supported", type);
184             return null;
185         }
186         try {
187             configDescriptionUriChannel = new URI(CONFIG_DESCRIPTION_URI_CHANNEL);
188             channelType = ChannelTypeBuilder.state(channelTypeUID, label, itemType) //
189                     .withDescription(description) //
190                     .withCategory(checkCategory(unitOfMeasure, category, state.isReadOnly())) //
191                     .withTags(checkTags(unitOfMeasure, state.isReadOnly())) //
192                     .withStateDescriptionFragment(state) //
193                     .withConfigDescriptionURI(configDescriptionUriChannel).build();
194         } catch (URISyntaxException ex) {
195             logger.warn("Can't create ConfigDescription URI '{}', ConfigDescription for channels not avilable!",
196                     CONFIG_DESCRIPTION_URI_CHANNEL);
197             channelType = ChannelTypeBuilder.state(channelTypeUID, label, itemType) //
198                     .withDescription(description) //
199                     .withCategory(checkCategory(unitOfMeasure, category, state.isReadOnly())) //
200                     .withTags(checkTags(unitOfMeasure, state.isReadOnly())).build();
201         }
202         channelTypeProvider.addChannelType(channelType);
203
204         chProperties.put("root", KM200Utils.translatesPathToName(root));
205         if (null != currentPathName && switchProgram) {
206             chProperties.put(SWITCH_PROGRAM_CURRENT_PATH_NAME, currentPathName);
207         }
208         if (addProperties) {
209             newChannel = ChannelBuilder.create(channelUID, type).withType(channelTypeUID).withDescription(description)
210                     .withLabel(label).withKind(ChannelKind.STATE).withProperties(chProperties).build();
211         } else {
212             newChannel = ChannelBuilder.create(channelUID, type).withType(channelTypeUID).withDescription(description)
213                     .withLabel(label).withKind(ChannelKind.STATE).build();
214         }
215         return newChannel;
216     }
217
218     @Override
219     public void initialize() {
220         Bridge bridge = this.getBridge();
221         if (bridge == null) {
222             logger.debug("Bridge not existing");
223             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
224             return;
225         }
226         logger.debug("initialize, Bridge: {}", bridge);
227         KM200GatewayHandler gateway = (KM200GatewayHandler) bridge.getHandler();
228         if (gateway == null) {
229             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
230             logger.debug("Gateway not existing: {}", bridge);
231             return;
232         }
233         String service = KM200Utils.translatesNameToPath(thing.getProperties().get("root"));
234         if (service == null) {
235             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "root property missing");
236             return;
237         }
238         synchronized (gateway.getDevice()) {
239             updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.CONFIGURATION_PENDING);
240             if (!gateway.getDevice().getInited()) {
241                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED);
242                 logger.debug("Bridge: not initialized: {}", bridge);
243                 return;
244             }
245             List<Channel> subChannels = new ArrayList<>();
246             if (gateway.getDevice().containsService(service)) {
247                 KM200ServiceObject serObj = gateway.getDevice().getServiceObject(service);
248                 if (null != serObj) {
249                     addChannels(serObj, thing, subChannels, "");
250                 }
251             } else if (service.contains(SWITCH_PROGRAM_PATH_NAME)) {
252                 String currentPathName = thing.getProperties().get(SWITCH_PROGRAM_CURRENT_PATH_NAME);
253                 StateDescriptionFragment state = StateDescriptionFragmentBuilder.create().withPattern("%s")
254                         .withOptions(KM200SwitchProgramServiceHandler.daysList).build();
255                 Channel newChannel = createChannel(new ChannelTypeUID(thing.getUID().getAsString() + ":" + "weekday"),
256                         new ChannelUID(thing.getUID(), "weekday"), service + "/" + "weekday", CoreItemFactory.STRING,
257                         currentPathName, "Current selected weekday for cycle selection", "Weekday", true, true, state,
258                         "");
259                 if (null == newChannel) {
260                     logger.warn("Creation of the channel {} was not possible", thing.getUID());
261                 } else {
262                     subChannels.add(newChannel);
263                 }
264
265                 state = StateDescriptionFragmentBuilder.create().withMinimum(BigDecimal.ZERO).withStep(BigDecimal.ONE)
266                         .withPattern("%d").withReadOnly(true).build();
267                 newChannel = createChannel(new ChannelTypeUID(thing.getUID().getAsString() + ":" + "nbrCycles"),
268                         new ChannelUID(thing.getUID(), "nbrCycles"), service + "/" + "nbrCycles",
269                         CoreItemFactory.NUMBER, currentPathName, "Number of switching cycles", "Number", true, true,
270                         state, "");
271                 if (null == newChannel) {
272                     logger.warn("Creation of the channel {} was not possible", thing.getUID());
273                 } else {
274                     subChannels.add(newChannel);
275                 }
276
277                 state = StateDescriptionFragmentBuilder.create().withMinimum(BigDecimal.ZERO).withStep(BigDecimal.ONE)
278                         .withPattern("%d").build();
279                 newChannel = createChannel(new ChannelTypeUID(thing.getUID().getAsString() + ":" + "cycle"),
280                         new ChannelUID(thing.getUID(), "cycle"), service + "/" + "cycle", CoreItemFactory.NUMBER,
281                         currentPathName, "Current selected cycle", "Cycle", true, true, state, "");
282                 if (null == newChannel) {
283                     logger.warn("Creation of the channel {} was not possible", thing.getUID());
284                 } else {
285                     subChannels.add(newChannel);
286                 }
287
288                 state = StateDescriptionFragmentBuilder.create().withMinimum(BigDecimal.ZERO).withStep(BigDecimal.ONE)
289                         .withPattern("%d minutes").build();
290                 String posName = thing.getProperties().get(SWITCH_PROGRAM_POSITIVE);
291                 if (posName == null) {
292                     newChannel = null;
293                 } else {
294                     newChannel = createChannel(new ChannelTypeUID(thing.getUID().getAsString() + ":" + posName),
295                             new ChannelUID(thing.getUID(), posName), service + "/" + posName, CoreItemFactory.NUMBER,
296                             currentPathName, "Positive switch of the cycle, like 'Day' 'On'", posName, true, true,
297                             state, "minutes");
298                 }
299                 if (null == newChannel) {
300                     logger.warn("Creation of the channel {} was not possible", thing.getUID());
301                 } else {
302                     subChannels.add(newChannel);
303                 }
304
305                 String negName = thing.getProperties().get(SWITCH_PROGRAM_NEGATIVE);
306                 if (negName == null) {
307                     newChannel = null;
308                 } else {
309                     newChannel = createChannel(new ChannelTypeUID(thing.getUID().getAsString() + ":" + negName),
310                             new ChannelUID(thing.getUID(), negName), service + "/" + negName, CoreItemFactory.NUMBER,
311                             currentPathName, "Negative switch of the cycle, like 'Night' 'Off'", negName, true, true,
312                             state, "minutes");
313                 }
314                 if (null == newChannel) {
315                     logger.warn("Creation of the channel {} was not possible", thing.getUID());
316                 } else {
317                     subChannels.add(newChannel);
318                 }
319             }
320             ThingBuilder thingBuilder = editThing();
321             List<Channel> actChannels = thing.getChannels();
322             for (Channel channel : actChannels) {
323                 thingBuilder.withoutChannel(channel.getUID());
324             }
325             thingBuilder.withChannels(subChannels);
326             updateThing(thingBuilder.build());
327             updateStatus(ThingStatus.ONLINE);
328         }
329     }
330
331     @Override
332     public void dispose() {
333         channelTypeProvider.removeChannelTypesForThing(getThing().getUID());
334     }
335
336     /**
337      * Checks whether a channel is linked to an item
338      */
339     public boolean checkLinked(Channel channel) {
340         return isLinked(channel.getUID().getId());
341     }
342
343     /**
344      * Search for services and add them to a list
345      */
346     private void addChannels(KM200ServiceObject serObj, Thing thing, List<Channel> subChannels, String subNameAddon) {
347         String service = serObj.getFullServiceName();
348         Set<String> subKeys = serObj.serviceTreeMap.keySet();
349         List<String> asProperties = null;
350         /* Some defines for dummy values, we will ignore such services */
351         final BigDecimal maxInt16AsFloat = new BigDecimal(+3276.8).setScale(6, RoundingMode.HALF_UP);
352         final BigDecimal minInt16AsFloat = new BigDecimal(-3276.8).setScale(6, RoundingMode.HALF_UP);
353         final BigDecimal maxInt16AsInt = new BigDecimal(3200).setScale(4, RoundingMode.HALF_UP);
354         for (KM200ThingType tType : KM200ThingType.values()) {
355             String root = tType.getRootPath();
356             if (root.compareTo(service) == 0) {
357                 asProperties = tType.asBridgeProperties();
358             }
359         }
360         for (String subKey : subKeys) {
361             if (asProperties != null) {
362                 if (asProperties.contains(subKey)) {
363                     continue;
364                 }
365             }
366             Map<String, String> properties = new HashMap<>(1);
367             String root = service + "/" + subKey;
368             properties.put("root", KM200Utils.translatesPathToName(root));
369             String subKeyType = serObj.serviceTreeMap.get(subKey).getServiceType();
370             boolean readOnly;
371             String unitOfMeasure = "";
372             StateDescriptionFragment state = null;
373             ChannelTypeUID channelTypeUID = new ChannelTypeUID(
374                     thing.getUID().getAsString() + ":" + subNameAddon + subKey);
375             Channel newChannel = null;
376             ChannelUID channelUID = new ChannelUID(thing.getUID(), subNameAddon + subKey);
377             if (serObj.serviceTreeMap.get(subKey).getWriteable() > 0) {
378                 readOnly = false;
379             } else {
380                 readOnly = true;
381             }
382             if ("temperatures".compareTo(thing.getUID().getId()) == 0) {
383                 unitOfMeasure = "°C";
384             }
385             logger.trace("Create things: {} id: {} channel: {}", thing.getUID(), subKey, thing.getUID().getId());
386             switch (subKeyType) {
387                 case DATA_TYPE_STRING_VALUE:
388                     /* Creating an new channel type with capabilities from service */
389                     List<StateOption> options = null;
390                     if (serObj.serviceTreeMap.get(subKey).getValueParameter() != null) {
391                         options = new ArrayList<>();
392                         // The type is definitely correct here
393                         @SuppressWarnings("unchecked")
394                         List<String> subValParas = (List<String>) serObj.serviceTreeMap.get(subKey).getValueParameter();
395                         if (null != subValParas) {
396                             for (String para : subValParas) {
397                                 StateOption stateOption = new StateOption(para, para);
398                                 options.add(stateOption);
399                             }
400                         }
401                     }
402                     StateDescriptionFragmentBuilder builder = StateDescriptionFragmentBuilder.create().withPattern("%s")
403                             .withReadOnly(readOnly);
404                     if (options != null && !options.isEmpty()) {
405                         builder.withOptions(options);
406                     }
407                     state = builder.build();
408                     newChannel = createChannel(channelTypeUID, channelUID, root, CoreItemFactory.STRING, null, subKey,
409                             subKey, true, false, state, unitOfMeasure);
410                     break;
411                 case DATA_TYPE_FLOAT_VALUE:
412                     /*
413                      * Check whether the value is a NaN. Usually all floats are BigDecimal. If it's a double then it's
414                      * Double.NaN. In this case we are ignoring this channel.
415                      */
416                     BigDecimal minVal = null;
417                     BigDecimal maxVal = null;
418                     BigDecimal step = null;
419                     final BigDecimal val;
420                     Object tmpVal = serObj.serviceTreeMap.get(subKey).getValue();
421                     if (tmpVal instanceof Double) {
422                         continue;
423                     }
424                     /* Check whether the value is a dummy (e.g. not connected sensor) */
425                     val = (BigDecimal) serObj.serviceTreeMap.get(subKey).getValue();
426                     if (val != null) {
427                         if (val.setScale(6, RoundingMode.HALF_UP).equals(maxInt16AsFloat)
428                                 || val.setScale(6, RoundingMode.HALF_UP).equals(minInt16AsFloat)
429                                 || val.setScale(4, RoundingMode.HALF_UP).equals(maxInt16AsInt)) {
430                             continue;
431                         }
432                     }
433                     /* Check the capabilities of this service */
434                     if (serObj.serviceTreeMap.get(subKey).getValueParameter() != null) {
435                         /* Creating an new channel type with capabilities from service */
436                         // The type is definitely correct here
437                         @SuppressWarnings("unchecked")
438                         List<Object> subValParas = (List<Object>) serObj.serviceTreeMap.get(subKey).getValueParameter();
439                         if (null != subValParas) {
440                             minVal = (BigDecimal) subValParas.get(0);
441                             maxVal = (BigDecimal) subValParas.get(1);
442                             if (subValParas.size() > 2) {
443                                 unitOfMeasure = (String) subValParas.get(2);
444                                 if ("C".equals(unitOfMeasure)) {
445                                     unitOfMeasure = "°C";
446                                 }
447                             }
448                             step = BigDecimal.valueOf(0.5);
449                         }
450                     }
451                     builder = StateDescriptionFragmentBuilder.create().withPattern("%.1f " + unitOfMeasure)
452                             .withReadOnly(readOnly);
453                     if (minVal != null) {
454                         builder.withMinimum(minVal);
455                     }
456                     if (maxVal != null) {
457                         builder.withMaximum(maxVal);
458                     }
459                     if (step != null) {
460                         builder.withStep(step);
461                     }
462                     state = builder.build();
463                     newChannel = createChannel(channelTypeUID, channelUID, root, CoreItemFactory.NUMBER, null, subKey,
464                             subKey, true, false, state, unitOfMeasure);
465                     break;
466                 case DATA_TYPE_REF_ENUM:
467                     /* Check whether the sub service should be ignored */
468                     boolean ignoreIt = false;
469                     for (KM200ThingType tType : KM200ThingType.values()) {
470                         if (tType.getThingTypeUID().equals(thing.getThingTypeUID())) {
471                             for (String ignore : tType.ignoreSubService()) {
472                                 if (ignore.equals(subKey)) {
473                                     ignoreIt = true;
474                                 }
475                             }
476                         }
477                     }
478                     if (ignoreIt) {
479                         continue;
480                     }
481                     /* Search for new services in sub path */
482                     KM200ServiceObject obj = serObj.serviceTreeMap.get(subKey);
483                     if (obj != null) {
484                         addChannels(obj, thing, subChannels, subKey + "_");
485                     }
486                     break;
487                 case DATA_TYPE_ERROR_LIST:
488                     if ("nbrErrors".equals(subKey) || "error".equals(subKey)) {
489                         state = StateDescriptionFragmentBuilder.create().withPattern("%.0f").withReadOnly(readOnly)
490                                 .build();
491                         newChannel = createChannel(new ChannelTypeUID(thing.getUID().getAsString() + ":" + subKey),
492                                 channelUID, root, CoreItemFactory.NUMBER, null, subKey, subKey, true, false, state,
493                                 unitOfMeasure);
494                     } else if ("errorString".equals(subKey)) {
495                         state = StateDescriptionFragmentBuilder.create().withPattern("%s").withReadOnly(readOnly)
496                                 .build();
497                         newChannel = createChannel(new ChannelTypeUID(thing.getUID().getAsString() + ":" + subKey),
498                                 channelUID, root, CoreItemFactory.STRING, null, "Error message", "Text", true, false,
499                                 state, unitOfMeasure);
500                     }
501                     break;
502             }
503             if (newChannel != null && state != null) {
504                 subChannels.add(newChannel);
505             }
506         }
507     }
508 }