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