2 * Copyright (c) 2010-2022 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;
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;
28 import java.util.stream.Collectors;
29 import java.util.stream.Stream;
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;
65 * The {@link KM200ThingHandler} is responsible for handling commands, which are
66 * sent to one of the channels.
68 * @author Markus Eckhardt - Initial contribution
71 public class KM200ThingHandler extends BaseThingHandler {
73 private final Logger logger = LoggerFactory.getLogger(KM200ThingHandler.class);
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()));
81 private final KM200ChannelTypeProvider channelTypeProvider;
83 public KM200ThingHandler(Thing thing, KM200ChannelTypeProvider channelTypeProvider) {
85 this.channelTypeProvider = channelTypeProvider;
89 public void handleCommand(ChannelUID channelUID, Command command) {
90 Bridge bridge = this.getBridge();
94 KM200GatewayHandler gateway = (KM200GatewayHandler) bridge.getHandler();
95 if (gateway == null) {
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);
106 logger.warn("Unsupported Command: {} Class: {}", command.toFullString(), command.getClass());
112 * Choose a tag for a channel
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) {
119 tags.add("CurrentTemperature");
121 tags.add("TargetTemperature");
129 * Choose a category for a channel
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;
138 category = "Temperature";
140 category = "Heating";
143 } else if (unitOfMeasure.indexOf("kW") == 0 || unitOfMeasure.indexOf("kWh") == 0) {
145 } else if (unitOfMeasure.indexOf("l/min") == 0 || unitOfMeasure.indexOf("l/h") == 0) {
147 } else if (unitOfMeasure.indexOf("Pascal") == 0 || unitOfMeasure.indexOf("bar") == 0) {
148 category = "Pressure";
149 } else if (unitOfMeasure.indexOf("rpm") == 0) {
151 } else if (unitOfMeasure.indexOf("mins") == 0 || unitOfMeasure.indexOf("minutes") == 0) {
153 } else if (unitOfMeasure.indexOf("kg/l") == 0) {
155 } else if (unitOfMeasure.indexOf("%%") == 0) {
158 category = topCategory;
164 * Creates a new channel
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";
179 } else if (CoreItemFactory.STRING.equals(type)) {
180 itemType = "StringType";
183 logger.info("Channeltype {} not supported", type);
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();
202 channelTypeProvider.addChannelType(channelType);
204 chProperties.put("root", KM200Utils.translatesPathToName(root));
205 if (null != currentPathName && switchProgram) {
206 chProperties.put(SWITCH_PROGRAM_CURRENT_PATH_NAME, currentPathName);
209 newChannel = ChannelBuilder.create(channelUID, type).withType(channelTypeUID).withDescription(description)
210 .withLabel(label).withKind(ChannelKind.STATE).withProperties(chProperties).build();
212 newChannel = ChannelBuilder.create(channelUID, type).withType(channelTypeUID).withDescription(description)
213 .withLabel(label).withKind(ChannelKind.STATE).build();
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);
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);
233 String service = KM200Utils.translatesNameToPath(thing.getProperties().get("root"));
234 if (service == null) {
235 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "root property missing");
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);
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, "");
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,
259 if (null == newChannel) {
260 logger.warn("Creation of the channel {} was not possible", thing.getUID());
262 subChannels.add(newChannel);
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,
271 if (null == newChannel) {
272 logger.warn("Creation of the channel {} was not possible", thing.getUID());
274 subChannels.add(newChannel);
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());
285 subChannels.add(newChannel);
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) {
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,
299 if (null == newChannel) {
300 logger.warn("Creation of the channel {} was not possible", thing.getUID());
302 subChannels.add(newChannel);
305 String negName = thing.getProperties().get(SWITCH_PROGRAM_NEGATIVE);
306 if (negName == null) {
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,
314 if (null == newChannel) {
315 logger.warn("Creation of the channel {} was not possible", thing.getUID());
317 subChannels.add(newChannel);
320 ThingBuilder thingBuilder = editThing();
321 List<Channel> actChannels = thing.getChannels();
322 for (Channel channel : actChannels) {
323 thingBuilder.withoutChannel(channel.getUID());
325 thingBuilder.withChannels(subChannels);
326 updateThing(thingBuilder.build());
327 updateStatus(ThingStatus.ONLINE);
332 public void dispose() {
333 channelTypeProvider.removeChannelTypesForThing(getThing().getUID());
337 * Checks whether a channel is linked to an item
339 public boolean checkLinked(Channel channel) {
340 return isLinked(channel.getUID().getId());
344 * Search for services and add them to a list
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();
360 for (String subKey : subKeys) {
361 if (asProperties != null) {
362 if (asProperties.contains(subKey)) {
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();
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) {
382 if ("temperatures".compareTo(thing.getUID().getId()) == 0) {
383 unitOfMeasure = "°C";
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);
402 StateDescriptionFragmentBuilder builder = StateDescriptionFragmentBuilder.create().withPattern("%s")
403 .withReadOnly(readOnly);
404 if (options != null && !options.isEmpty()) {
405 builder.withOptions(options);
407 state = builder.build();
408 newChannel = createChannel(channelTypeUID, channelUID, root, CoreItemFactory.STRING, null, subKey,
409 subKey, true, false, state, unitOfMeasure);
411 case DATA_TYPE_FLOAT_VALUE:
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.
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) {
424 /* Check whether the value is a dummy (e.g. not connected sensor) */
425 val = (BigDecimal) serObj.serviceTreeMap.get(subKey).getValue();
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)) {
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";
448 step = BigDecimal.valueOf(0.5);
451 builder = StateDescriptionFragmentBuilder.create().withPattern("%.1f " + unitOfMeasure)
452 .withReadOnly(readOnly);
453 if (minVal != null) {
454 builder.withMinimum(minVal);
456 if (maxVal != null) {
457 builder.withMaximum(maxVal);
460 builder.withStep(step);
462 state = builder.build();
463 newChannel = createChannel(channelTypeUID, channelUID, root, CoreItemFactory.NUMBER, null, subKey,
464 subKey, true, false, state, unitOfMeasure);
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)) {
481 /* Search for new services in sub path */
482 KM200ServiceObject obj = serObj.serviceTreeMap.get(subKey);
484 addChannels(obj, thing, subChannels, subKey + "_");
487 case DATA_TYPE_ERROR_LIST:
488 if ("nbrErrors".equals(subKey) || "error".equals(subKey)) {
489 state = StateDescriptionFragmentBuilder.create().withPattern("%.0f").withReadOnly(readOnly)
491 newChannel = createChannel(new ChannelTypeUID(thing.getUID().getAsString() + ":" + subKey),
492 channelUID, root, CoreItemFactory.NUMBER, null, subKey, subKey, true, false, state,
494 } else if ("errorString".equals(subKey)) {
495 state = StateDescriptionFragmentBuilder.create().withPattern("%s").withReadOnly(readOnly)
497 newChannel = createChannel(new ChannelTypeUID(thing.getUID().getAsString() + ":" + subKey),
498 channelUID, root, CoreItemFactory.STRING, null, "Error message", "Text", true, false,
499 state, unitOfMeasure);
503 if (newChannel != null && state != null) {
504 subChannels.add(newChannel);