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