2 * Copyright (c) 2010-2024 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.freeathomesystem.internal.handler;
15 import java.math.BigDecimal;
17 import java.net.URISyntaxException;
18 import java.util.ArrayList;
19 import java.util.HashMap;
20 import java.util.List;
21 import java.util.Locale;
24 import org.eclipse.jdt.annotation.NonNullByDefault;
25 import org.openhab.binding.freeathomesystem.internal.configuration.FreeAtHomeDeviceHandlerConfiguration;
26 import org.openhab.binding.freeathomesystem.internal.datamodel.FreeAtHomeDatapoint;
27 import org.openhab.binding.freeathomesystem.internal.datamodel.FreeAtHomeDatapointGroup;
28 import org.openhab.binding.freeathomesystem.internal.datamodel.FreeAtHomeDeviceChannel;
29 import org.openhab.binding.freeathomesystem.internal.datamodel.FreeAtHomeDeviceDescription;
30 import org.openhab.binding.freeathomesystem.internal.type.FreeAtHomeChannelTypeProvider;
31 import org.openhab.binding.freeathomesystem.internal.util.FreeAtHomeGeneralException;
32 import org.openhab.binding.freeathomesystem.internal.util.FreeAtHomeHttpCommunicationException;
33 import org.openhab.binding.freeathomesystem.internal.util.UidUtils;
34 import org.openhab.binding.freeathomesystem.internal.valuestateconverter.ValueStateConverter;
35 import org.openhab.core.i18n.LocaleProvider;
36 import org.openhab.core.i18n.TranslationProvider;
37 import org.openhab.core.library.types.StopMoveType;
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.ThingUID;
46 import org.openhab.core.thing.binding.BaseThingHandler;
47 import org.openhab.core.thing.binding.ThingHandler;
48 import org.openhab.core.thing.binding.builder.ChannelBuilder;
49 import org.openhab.core.thing.binding.builder.ThingBuilder;
50 import org.openhab.core.thing.type.AutoUpdatePolicy;
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.State;
58 import org.openhab.core.types.StateDescriptionFragmentBuilder;
59 import org.osgi.framework.Bundle;
60 import org.osgi.framework.FrameworkUtil;
61 import org.slf4j.Logger;
62 import org.slf4j.LoggerFactory;
65 * The {@link FreeAtHomeDeviceHandler} is responsible for handling the generic free@home device main communication
68 * @author Andras Uhrin - Initial contribution
72 public class FreeAtHomeDeviceHandler extends BaseThingHandler implements FreeAtHomeDeviceStateListener {
74 private static final String CHANNEL_URI = "channel-type:freeathomesystem:config";
76 private final Logger logger = LoggerFactory.getLogger(FreeAtHomeDeviceHandler.class);
77 private FreeAtHomeDeviceDescription device = new FreeAtHomeDeviceDescription();
78 private FreeAtHomeChannelTypeProvider channelTypeProvider;
79 private TranslationProvider i18nProvider;
80 private Locale locale;
81 private Bundle bundle;
83 private Map<ChannelUID, FreeAtHomeDatapointGroup> mapChannelUID = new HashMap<ChannelUID, FreeAtHomeDatapointGroup>();
84 private Map<String, ChannelUID> mapEventToChannelUID = new HashMap<String, ChannelUID>();
86 public FreeAtHomeDeviceHandler(Thing thing, FreeAtHomeChannelTypeProvider channelTypeProvider,
87 TranslationProvider i18nProvider, LocaleProvider localeProvider) {
90 this.channelTypeProvider = channelTypeProvider;
91 this.i18nProvider = i18nProvider;
92 this.bundle = FrameworkUtil.getBundle(getClass());
93 this.locale = localeProvider.getLocale();
97 public void initialize() {
98 updateStatus(ThingStatus.UNKNOWN);
100 scheduler.execute(() -> {
101 FreeAtHomeDeviceHandlerConfiguration config = getConfigAs(FreeAtHomeDeviceHandlerConfiguration.class);
103 Bridge bridge = this.getBridge();
104 String locDeviceId = config.deviceId;
106 if (bridge != null) {
107 ThingHandler handler = bridge.getHandler();
109 if (handler instanceof FreeAtHomeBridgeHandler bridgeHandler) {
110 if (!locDeviceId.isBlank()) {
112 device = bridgeHandler.getFreeatHomeDeviceDescription(locDeviceId);
115 } catch (FreeAtHomeHttpCommunicationException e) {
116 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
117 "@text/comm-error.error-in-sysap-com");
118 } catch (FreeAtHomeGeneralException e) {
119 logger.debug("General error in the binding - during initialization {}",
120 device.getDeviceId());
122 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
123 "@text/conf-error.general-binding-error");
126 // register device for status updates
127 bridgeHandler.registerDeviceStateListener(device.getDeviceId(), this);
129 updateStatus(ThingStatus.ONLINE);
131 logger.debug("Device created - device id: {}", device.getDeviceId());
133 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
134 "@text/conf-error.invalid-deviceconfig");
136 logger.debug("Device cannot be created: device ID is null!");
140 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
141 "@text/conf-error.bridge-not-configured");
143 logger.debug("Device cannot be created: no bridge is configured!");
150 public void dispose() {
151 Bridge bridge = this.getBridge();
153 // Unregister device and specific channel for event based state updated
154 if (bridge != null) {
155 ThingHandler handler = bridge.getHandler();
157 if (handler instanceof FreeAtHomeBridgeHandler bridgeHandler) {
158 bridgeHandler.unregisterDeviceStateListener(device.getDeviceId());
162 // Remove mapping tables
163 mapChannelUID.clear();
165 mapEventToChannelUID.clear();
167 logger.debug("Device removed - device id: {}", device.getDeviceId());
170 private void handleRefreshCommand(FreeAtHomeBridgeHandler freeAtHomeBridge, FreeAtHomeDatapointGroup dpg,
171 ChannelUID channelUID) {
172 String valueStr = "0";
173 String channelID = "ch000";
174 String datapointID = "0";
176 // Check whether it is a INPUT only datapoint group
178 if (dpg.getDirection() == FreeAtHomeDatapointGroup.DatapointGroupDirection.INPUT) {
179 FreeAtHomeDatapoint datapoint = dpg.getInputDatapoint();
181 if (datapoint != null) {
182 channelID = datapoint.channelId;
183 datapointID = datapoint.getDatapointId();
186 FreeAtHomeDatapoint datapoint = dpg.getOutputDatapoint();
188 if (datapoint != null) {
189 channelID = datapoint.channelId;
190 datapointID = datapoint.getDatapointId();
195 valueStr = freeAtHomeBridge.getDatapoint(device.getDeviceId(), channelID, datapointID);
197 ValueStateConverter vsc = dpg.getValueStateConverter();
199 updateState(channelUID, vsc.convertToState(valueStr));
200 } catch (FreeAtHomeHttpCommunicationException e) {
201 logger.debug("Communication error during refresh command {} - at channel {} - Error string {}",
202 device.getDeviceId(), channelUID.getAsString(), e.getMessage());
204 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
205 "@text/comm-error.error-in-sysap-com");
206 } catch (FreeAtHomeGeneralException e) {
207 logger.debug("General error in the binding - during REFRESH command {} - at channel {} - Error string {}",
208 device.getDeviceId(), channelUID.getAsString(), e.getMessage());
210 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
211 "@text/conf-error.general-binding-error");
215 private void handleSetCommand(FreeAtHomeBridgeHandler freeAtHomeBridge, FreeAtHomeDatapointGroup dpg,
216 ChannelUID channelUID, Command command) {
218 String valueString = "0";
220 // initial error handling. look for the data point group validity
221 FreeAtHomeDatapoint datapoint = dpg.getInputDatapoint();
223 if (datapoint == null) {
224 logger.debug("Invalid parameter in handleSetCommand - DeviceId - {} - at channel {}", device.getDeviceId(),
225 channelUID.getAsString());
231 ValueStateConverter vsc = dpg.getValueStateConverter();
233 if (command instanceof StopMoveType) {
236 state = ((State) command);
237 valueString = vsc.convertToValueString(state);
240 freeAtHomeBridge.setDatapoint(device.getDeviceId(), datapoint.channelId, datapoint.getDatapointId(),
243 if (!device.isScene()) {
245 updateState(channelUID, state);
247 updateState(channelUID, new StringType("STOP"));
250 } catch (FreeAtHomeHttpCommunicationException e) {
252 "Communication error during set command {} - at channel {} - full command {} - Error string {}",
253 device.getDeviceId(), channelUID.getAsString(), command.toFullString(), e.getMessage());
255 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
256 "@text/comm-error.error-in-sysap-com");
257 } catch (FreeAtHomeGeneralException e) {
258 logger.debug("General error in the binding - during SET command {} - at channel {} - Error string {}",
259 device.getDeviceId(), channelUID.getAsString(), e.getMessage());
261 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
262 "@text/conf-error.general-binding-error");
267 public void handleCommand(ChannelUID channelUID, Command command) {
268 FreeAtHomeBridgeHandler freeAtHomeBridge = null;
270 Bridge bridge = this.getBridge();
272 if (bridge != null) {
273 ThingHandler handler = bridge.getHandler();
275 if (handler instanceof FreeAtHomeBridgeHandler bridgeHandler) {
276 freeAtHomeBridge = bridgeHandler;
280 if (freeAtHomeBridge != null) {
281 updateStatus(ThingStatus.ONLINE);
283 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
284 "@text/conf-error.invalid-bridge");
288 FreeAtHomeDatapointGroup dpg = mapChannelUID.get(channelUID);
290 // is the datapointgroup invalid
292 logger.debug("Handle command for device (but invalid datapointgroup) {} - at channel {} - full command {}",
293 device.getDeviceId(), channelUID.getAsString(), command.toFullString());
295 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
296 "@text/conf-error.invalid-deviceconfig");
298 if (command instanceof RefreshType) {
299 handleRefreshCommand(freeAtHomeBridge, dpg, channelUID);
301 handleSetCommand(freeAtHomeBridge, dpg, channelUID, command);
304 logger.debug("Handle command for device {} - at channel {} - full command {}", device.getDeviceId(),
305 channelUID.getAsString(), command.toFullString());
309 public void onDeviceRemoved() {
310 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.GONE);
313 public void onDeviceStateChanged(String event, String valueString) {
314 // Get the channle UID belonging to this event
315 ChannelUID channelUID = mapEventToChannelUID.get(event);
318 if (channelUID != null) {
319 // get the value State Converter for the channel
320 FreeAtHomeDatapointGroup dpg = mapChannelUID.get(channelUID);
324 state = dpg.getValueStateConverter().convertToState(valueString);
326 // Handle state change
327 handleEventBasedUpdate(channelUID, state);
329 // if it is virtual device, give a feedback to free@home also
330 if (isThingHandlesVirtualDevice()) {
331 feedbackForVirtualDevice(channelUID, valueString);
335 } catch (FreeAtHomeGeneralException e) {
336 logger.debug("General error in the binding during onDeviceStateChange");
338 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
339 "@text/conf-error.general-binding-error");
343 private void handleEventBasedUpdate(ChannelUID channelUID, State state) {
344 this.updateState(channelUID, state);
347 private void feedbackForVirtualDevice(ChannelUID channelUID, String valueString) {
348 FreeAtHomeBridgeHandler freeAtHomeBridge = null;
350 FreeAtHomeDatapointGroup dpg = mapChannelUID.get(channelUID);
352 Bridge bridge = this.getBridge();
354 if (bridge != null) {
355 ThingHandler handler = bridge.getHandler();
357 if (handler instanceof FreeAtHomeBridgeHandler bridgeHandler) {
358 freeAtHomeBridge = bridgeHandler;
362 if (freeAtHomeBridge == null) {
363 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE, "@text/gen-error.no-bridge-avail");
368 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
369 "@text/conf-error.datapointgroup-invalid");
373 FreeAtHomeDatapoint inputDatapoint = dpg.getInputDatapoint();
375 if (inputDatapoint == null) {
376 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
377 "@text/conf-error.inputdatapoint-invalid");
381 if ((dpg.getDirection() != FreeAtHomeDatapointGroup.DatapointGroupDirection.INPUT)
382 || (dpg.getDirection() != FreeAtHomeDatapointGroup.DatapointGroupDirection.INPUTOUTPUT)) {
383 logger.debug("Handle feedback for virtual device {} - at channel {} - but wrong config",
384 device.getDeviceId(), channelUID.getAsString());
388 freeAtHomeBridge.setDatapoint(device.getDeviceId(), inputDatapoint.channelId,
389 inputDatapoint.getDatapointId(), valueString);
391 updateStatus(ThingStatus.ONLINE);
393 logger.debug("Handle feedback for virtual device {} - at channel {} - value {}", device.getDeviceId(),
394 channelUID.getAsString(), valueString);
395 } catch (FreeAtHomeHttpCommunicationException e) {
396 logger.debug("Communication error during set command {} - at channel {} - value {} - Error string {}",
397 device.getDeviceId(), channelUID.getAsString(), valueString, e.getMessage());
399 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
400 "@text/comm-error.not-able-open-httpconnection");
404 public ChannelTypeUID createChannelTypeForDatapointgroup(FreeAtHomeDatapointGroup dpg,
405 ChannelTypeUID channelTypeUID) throws FreeAtHomeGeneralException {
406 StateDescriptionFragmentBuilder stateFragment = StateDescriptionFragmentBuilder.create();
408 stateFragment.withReadOnly(dpg.isReadOnly());
409 stateFragment.withPattern(dpg.getTypePattern());
411 if (dpg.isDecimal() || dpg.isInteger()) {
412 BigDecimal min = new BigDecimal(dpg.getMin());
413 BigDecimal max = new BigDecimal(dpg.getMax());
414 stateFragment.withMinimum(min).withMaximum(max);
418 URI configDescriptionUriChannel = new URI(CHANNEL_URI);
420 ChannelTypeBuilder<?> channelTypeBuilder = ChannelTypeBuilder
421 .state(channelTypeUID,
422 String.format("%s-%s-%s-%s", dpg.getLabel(), dpg.getOpenHabItemType(),
423 dpg.getOpenHabCategory(), "type"),
424 dpg.getOpenHabItemType())
425 .withCategory(dpg.getOpenHabCategory()).withStateDescriptionFragment(stateFragment.build());
427 ChannelType channelType = channelTypeBuilder.isAdvanced(false)
428 .withConfigDescriptionURI(configDescriptionUriChannel)
429 .withDescription(String.format("Type for channel - %s ", dpg.getLabel())).build();
431 channelTypeProvider.addChannelType(channelType);
433 logger.debug("Channel type created {} - label: {} - category: {}", channelTypeUID.getAsString(),
434 dpg.getLabel(), dpg.getOpenHabCategory());
435 } catch (URISyntaxException e) {
436 logger.debug("Channel config URI cannot created for datapoint - datapoint group: {}", dpg.getLabel());
439 return channelTypeUID;
442 public void updateChannels() throws FreeAtHomeGeneralException {
443 // define update policy
444 AutoUpdatePolicy policy = AutoUpdatePolicy.DEFAULT;
446 if (device.isScene()) {
447 policy = AutoUpdatePolicy.VETO;
450 // Initialize channels
451 List<Channel> thingChannels = new ArrayList<>(this.getThing().getChannels());
453 if (thingChannels.isEmpty()) {
454 ThingBuilder thingBuilder = editThing();
456 ThingUID thingUID = thing.getUID();
458 for (int i = 0; i < device.getNumberOfChannels(); i++) {
459 FreeAtHomeDeviceChannel channel = device.getChannel(i);
461 for (int j = 0; j < channel.getNumberOfDatapointGroup(); j++) {
462 FreeAtHomeDatapointGroup dpg = channel.getDatapointGroup(j);
463 Map<String, String> channelProps = new HashMap<>();
465 FreeAtHomeDatapoint inputDatapoint = dpg.getInputDatapoint();
466 FreeAtHomeDatapoint outputDatapoint = dpg.getOutputDatapoint();
468 if (inputDatapoint != null) {
469 channelProps.put("input", inputDatapoint.getDatapointId());
472 if (outputDatapoint != null) {
473 channelProps.put("output", outputDatapoint.getDatapointId());
476 ChannelTypeUID channelTypeUID = UidUtils.generateChannelTypeUID(dpg.getValueType(),
479 if (channelTypeProvider.getChannelType(channelTypeUID, null) == null) {
480 channelTypeUID = createChannelTypeForDatapointgroup(dpg, channelTypeUID);
483 ChannelUID channelUID = new ChannelUID(thingUID, channel.getChannelId(),
484 dpg.getLabel().substring(4));
486 String channelLabel = String.format("%s",
487 i18nProvider.getText(bundle, dpg.getLabel(), "-", locale));
489 String channelDescription = String.format("(%s) %s", channel.getChannelLabel(),
490 i18nProvider.getText(bundle, dpg.getDescription(), "-", locale));
492 Channel thingChannel = ChannelBuilder.create(channelUID)
493 .withAcceptedItemType(dpg.getOpenHabItemType()).withKind(ChannelKind.STATE)
494 .withProperties(channelProps).withLabel(capitalizeWordsInLabel(channelLabel))
495 .withDescription(channelDescription).withType(channelTypeUID).withAutoUpdatePolicy(policy)
497 thingChannels.add(thingChannel);
499 logger.debug("Thing channel created - device: {} - channelUID: {} - channel label: {}",
500 device.getDeviceId() + device.getDeviceLabel(), channelUID.getAsString(), channelLabel);
502 // in case of output channel, register it for updates
503 if (outputDatapoint != null) {
504 String eventDatapointID = new String(device.getDeviceId() + "/" + channel.getChannelId() + "/"
505 + outputDatapoint.getDatapointId());
507 mapEventToChannelUID.put(eventDatapointID, channelUID);
510 // add the datapoint group to the mapping channel
511 mapChannelUID.put(channelUID, dpg);
513 if (dpg.getInputDatapoint() == null) {
515 "Thing channel registered - device: {} - channelUID: {} - channel label: {} - category: {}",
516 device.getDeviceId() + device.getDeviceLabel(), channelUID.getAsString(),
517 dpg.getLabel(), dpg.getOpenHabCategory());
520 "Thing channel registered - device: {} - channelUID: {} - channel label: {} - category: {}",
521 device.getDeviceId() + device.getDeviceLabel(), channelUID.getAsString(),
522 dpg.getLabel(), dpg.getOpenHabCategory());
526 thingBuilder.withChannels(thingChannels);
528 updateThing(thingBuilder.build());
531 reloadChannelTypes();
534 thingChannels.forEach(channel -> {
535 if (isLinked(channel.getUID())) {
536 channelLinked(channel.getUID());
541 private void reloadChannelTypes() throws FreeAtHomeGeneralException {
542 Bridge bridge = this.getBridge();
544 ThingUID thingUID = thing.getUID();
547 if (bridge != null) {
548 ThingHandler handler = bridge.getHandler();
550 if (handler instanceof FreeAtHomeBridgeHandler bridgeHandler) {
551 device = bridgeHandler.getFreeatHomeDeviceDescription(device.getDeviceId());
554 } catch (FreeAtHomeHttpCommunicationException e) {
555 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
556 "@text/comm-error.error-in-sysap-com");
559 for (int i = 0; i < device.getNumberOfChannels(); i++) {
560 FreeAtHomeDeviceChannel channel = device.getChannel(i);
562 for (int j = 0; j < channel.getNumberOfDatapointGroup(); j++) {
563 FreeAtHomeDatapointGroup dpg = channel.getDatapointGroup(j);
565 ChannelTypeUID channelTypeUID = UidUtils.generateChannelTypeUID(dpg.getValueType(), dpg.isReadOnly());
567 if (channelTypeProvider.getChannelType(channelTypeUID, null) == null) {
568 channelTypeUID = createChannelTypeForDatapointgroup(dpg, channelTypeUID);
571 ChannelUID channelUID = new ChannelUID(thingUID, channel.getChannelId());
573 FreeAtHomeDatapoint outputDatapoint = dpg.getOutputDatapoint();
575 // in case of output channel, register it for updates
576 if (outputDatapoint != null) {
577 String eventDatapointID = new String(device.getDeviceId() + "/" + channel.getChannelId() + "/"
578 + outputDatapoint.getDatapointId());
580 mapEventToChannelUID.put(eventDatapointID, channelUID);
583 // add the datapoint group to the mapping channel
584 mapChannelUID.put(channelUID, dpg);
586 logger.debug("Thing channelType reloaded - Device: {} - channelTypeUID: {}",
587 device.getDeviceId() + device.getDeviceLabel(), channelTypeUID.getAsString());
592 public void removeChannels() {
593 Bridge bridge = this.getBridge();
596 if (bridge != null) {
597 ThingHandler handler = bridge.getHandler();
599 if (handler instanceof FreeAtHomeBridgeHandler bridgeHandler) {
600 device = bridgeHandler.getFreeatHomeDeviceDescription(device.getDeviceId());
603 } catch (FreeAtHomeHttpCommunicationException e) {
604 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
605 "@text/comm-error.error-in-sysap-com");
608 mapChannelUID.clear();
610 mapEventToChannelUID.clear();
613 private String capitalizeWordsInLabel(String label) {
614 // splliting up words using split function
615 String[] words = label.split(" ");
617 for (int i = 0; i < words.length; i++) {
619 // taking letter individually from sentences
620 String firstLetter = words[i].substring(0, 1);
621 String restOfWord = words[i].substring(1);
623 // making first letter uppercase using toUpperCase function
624 firstLetter = firstLetter.toUpperCase();
625 words[i] = firstLetter + restOfWord;
628 // joining the words together to make a sentence
629 return String.join(" ", words);
632 private boolean isThingHandlesVirtualDevice() {
633 return device.isVirtual();