2 * Copyright (c) 2010-2020 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.homematic.internal.handler;
15 import static org.openhab.binding.homematic.internal.HomematicBindingConstants.*;
16 import static org.openhab.binding.homematic.internal.misc.HomematicConstants.*;
18 import java.io.IOException;
19 import java.math.BigDecimal;
20 import java.util.ArrayList;
21 import java.util.HashMap;
22 import java.util.Iterator;
23 import java.util.List;
25 import java.util.Map.Entry;
26 import java.util.concurrent.Future;
28 import org.apache.commons.lang.ObjectUtils;
29 import org.apache.commons.lang.StringUtils;
30 import org.apache.commons.lang.math.NumberUtils;
31 import org.openhab.binding.homematic.internal.HomematicBindingConstants;
32 import org.openhab.binding.homematic.internal.common.HomematicConfig;
33 import org.openhab.binding.homematic.internal.communicator.HomematicGateway;
34 import org.openhab.binding.homematic.internal.converter.ConverterException;
35 import org.openhab.binding.homematic.internal.converter.ConverterFactory;
36 import org.openhab.binding.homematic.internal.converter.ConverterTypeException;
37 import org.openhab.binding.homematic.internal.converter.TypeConverter;
38 import org.openhab.binding.homematic.internal.misc.HomematicClientException;
39 import org.openhab.binding.homematic.internal.misc.HomematicConstants;
40 import org.openhab.binding.homematic.internal.model.HmChannel;
41 import org.openhab.binding.homematic.internal.model.HmDatapoint;
42 import org.openhab.binding.homematic.internal.model.HmDatapointConfig;
43 import org.openhab.binding.homematic.internal.model.HmDatapointInfo;
44 import org.openhab.binding.homematic.internal.model.HmDevice;
45 import org.openhab.binding.homematic.internal.model.HmParamsetType;
46 import org.openhab.binding.homematic.internal.type.HomematicTypeGeneratorImpl;
47 import org.openhab.binding.homematic.internal.type.MetadataUtils;
48 import org.openhab.binding.homematic.internal.type.UidUtils;
49 import org.openhab.core.config.core.Configuration;
50 import org.openhab.core.config.core.validation.ConfigValidationException;
51 import org.openhab.core.library.types.OnOffType;
52 import org.openhab.core.library.types.StopMoveType;
53 import org.openhab.core.thing.Bridge;
54 import org.openhab.core.thing.Channel;
55 import org.openhab.core.thing.ChannelUID;
56 import org.openhab.core.thing.Thing;
57 import org.openhab.core.thing.ThingStatus;
58 import org.openhab.core.thing.ThingStatusDetail;
59 import org.openhab.core.thing.binding.BaseThingHandler;
60 import org.openhab.core.thing.binding.ThingHandler;
61 import org.openhab.core.thing.binding.builder.ChannelBuilder;
62 import org.openhab.core.types.Command;
63 import org.openhab.core.types.RefreshType;
64 import org.openhab.core.types.State;
65 import org.slf4j.Logger;
66 import org.slf4j.LoggerFactory;
69 * The {@link HomematicThingHandler} is responsible for handling commands, which are sent to one of the channels.
71 * @author Gerhard Riegler - Initial contribution
73 public class HomematicThingHandler extends BaseThingHandler {
74 private final Logger logger = LoggerFactory.getLogger(HomematicThingHandler.class);
75 private Future<?> initFuture;
76 private final Object initLock = new Object();
77 private volatile boolean deviceDeletionPending = false;
79 public HomematicThingHandler(Thing thing) {
84 public void initialize() {
85 if (initFuture != null) {
89 initFuture = scheduler.submit(() -> {
92 synchronized (initLock) {
93 doInitializeInBackground();
95 } catch (HomematicClientException ex) {
96 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, ex.getMessage());
97 } catch (IOException ex) {
98 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, ex.getMessage());
99 } catch (GatewayNotAvailableException ex) {
100 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE, ex.getMessage());
101 } catch (Exception ex) {
102 logger.error("{}", ex.getMessage(), ex);
103 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, ex.getMessage());
108 private void doInitializeInBackground() throws GatewayNotAvailableException, HomematicClientException, IOException {
109 HomematicGateway gateway = getHomematicGateway();
110 HmDevice device = gateway.getDevice(UidUtils.getHomematicAddress(getThing()));
111 HmChannel channelZero = device.getChannel(0);
112 loadHomematicChannelValues(channelZero);
113 updateStatus(device);
114 logger.debug("Initializing thing '{}' from gateway '{}'", getThing().getUID(), gateway.getId());
117 Map<String, String> properties = editProperties();
118 setProperty(properties, channelZero, PROPERTY_BATTERY_TYPE, VIRTUAL_DATAPOINT_NAME_BATTERY_TYPE);
119 setProperty(properties, channelZero, Thing.PROPERTY_FIRMWARE_VERSION, VIRTUAL_DATAPOINT_NAME_FIRMWARE);
120 setProperty(properties, channelZero, Thing.PROPERTY_SERIAL_NUMBER, device.getAddress());
121 setProperty(properties, channelZero, PROPERTY_AES_KEY, DATAPOINT_NAME_AES_KEY);
122 updateProperties(properties);
124 // update data point list for reconfigurable channels
125 for (HmChannel channel : device.getChannels()) {
126 if (channel.isReconfigurable()) {
127 loadHomematicChannelValues(channel);
128 if (channel.checkForChannelFunctionChange()) {
129 gateway.updateChannelValueDatapoints(channel);
134 // update configurations
135 Configuration config = editConfiguration();
136 for (HmChannel channel : device.getChannels()) {
137 loadHomematicChannelValues(channel);
138 for (HmDatapoint dp : channel.getDatapoints()) {
139 if (dp.getParamsetType() == HmParamsetType.MASTER) {
140 config.put(MetadataUtils.getParameterName(dp),
141 dp.isEnumType() ? dp.getOptionValue() : dp.getValue());
145 updateConfiguration(config);
147 // update thing channel list for reconfigurable channels (relies on the new value of the
148 // CHANNEL_FUNCTION datapoint fetched during configuration update)
149 List<Channel> thingChannels = new ArrayList<>(getThing().getChannels());
150 if (updateDynamicChannelList(device, thingChannels)) {
151 updateThing(editThing().withChannels(thingChannels).build());
156 * Update the given thing channel list to reflect the device's current datapoint set
158 * @return true if the list was modified, false if it was not modified
160 private boolean updateDynamicChannelList(HmDevice device, List<Channel> thingChannels) {
161 boolean changed = false;
162 for (HmChannel channel : device.getChannels()) {
163 if (!channel.isReconfigurable()) {
166 final String expectedFunction = channel
167 .getDatapoint(HmParamsetType.MASTER, HomematicConstants.DATAPOINT_NAME_CHANNEL_FUNCTION)
169 final String propertyName = String.format(PROPERTY_DYNAMIC_FUNCTION_FORMAT, channel.getNumber());
171 // remove thing channels that were configured for a different function
172 Iterator<Channel> channelIter = thingChannels.iterator();
173 while (channelIter.hasNext()) {
174 Map<String, String> properties = channelIter.next().getProperties();
175 String function = properties.get(propertyName);
176 if (function != null && !function.equals(expectedFunction)) {
177 channelIter.remove();
181 for (HmDatapoint dp : channel.getDatapoints()) {
182 if (HomematicTypeGeneratorImpl.isIgnoredDatapoint(dp)
183 || dp.getParamsetType() != HmParamsetType.VALUES) {
186 ChannelUID channelUID = UidUtils.generateChannelUID(dp, getThing().getUID());
187 if (containsChannel(thingChannels, channelUID)) {
188 // Channel is already present -> channel configuration likely hasn't changed
192 Map<String, String> channelProps = new HashMap<>();
193 channelProps.put(propertyName, expectedFunction);
195 Channel thingChannel = ChannelBuilder.create(channelUID, MetadataUtils.getItemType(dp))
196 .withProperties(channelProps).withLabel(MetadataUtils.getLabel(dp))
197 .withDescription(MetadataUtils.getDatapointDescription(dp))
198 .withType(UidUtils.generateChannelTypeUID(dp)).build();
199 thingChannels.add(thingChannel);
208 * Checks whether the given list includes a channel with the given UID
210 private static boolean containsChannel(List<Channel> channels, ChannelUID channelUID) {
211 for (Channel channel : channels) {
212 ChannelUID uid = channel.getUID();
213 if (StringUtils.equals(channelUID.getGroupId(), uid.getGroupId())
214 && StringUtils.equals(channelUID.getId(), uid.getId())) {
222 * Sets a thing property with a datapoint value.
224 private void setProperty(Map<String, String> properties, HmChannel channelZero, String propertyName,
225 String datapointName) {
226 HmDatapoint dp = channelZero
227 .getDatapoint(new HmDatapointInfo(HmParamsetType.VALUES, channelZero, datapointName));
229 properties.put(propertyName, ObjectUtils.toString(dp.getValue()));
234 public void channelLinked(ChannelUID channelUID) {
235 handleRefresh(channelUID);
239 * Updates the state of the given channel.
241 protected void handleRefresh(ChannelUID channelUID) {
243 if (thing.getStatus() == ThingStatus.ONLINE) {
244 logger.debug("Updating channel '{}' from thing id '{}'", channelUID, getThing().getUID().getId());
245 updateChannelState(channelUID);
247 } catch (Exception ex) {
248 logger.warn("{}", ex.getMessage());
253 public void handleCommand(ChannelUID channelUID, Command command) {
254 logger.debug("Received command '{}' for channel '{}'", command, channelUID);
255 HmDatapoint dp = null;
257 HomematicGateway gateway = getHomematicGateway();
258 HmDatapointInfo dpInfo = UidUtils.createHmDatapointInfo(channelUID);
259 if (RefreshType.REFRESH == command) {
260 logger.debug("Refreshing {}", dpInfo);
261 dpInfo = new HmDatapointInfo(dpInfo.getAddress(), HmParamsetType.VALUES, 0,
262 VIRTUAL_DATAPOINT_NAME_RELOAD_FROM_GATEWAY);
263 dp = gateway.getDatapoint(dpInfo);
264 sendDatapoint(dp, new HmDatapointConfig(), Boolean.TRUE);
266 Channel channel = getThing().getChannel(channelUID.getId());
267 if (channel == null) {
268 logger.warn("Channel '{}' not found in thing '{}' on gateway '{}'", channelUID, getThing().getUID(),
271 if (StopMoveType.STOP == command && DATAPOINT_NAME_LEVEL.equals(dpInfo.getName())) {
272 // special case with stop type (rollershutter)
273 dpInfo.setName(DATAPOINT_NAME_STOP);
274 HmDatapoint stopDp = gateway.getDatapoint(dpInfo);
275 ChannelUID stopChannelUID = UidUtils.generateChannelUID(stopDp, getThing().getUID());
276 handleCommand(stopChannelUID, OnOffType.ON);
278 dp = gateway.getDatapoint(dpInfo);
279 TypeConverter<?> converter = ConverterFactory.createConverter(channel.getAcceptedItemType());
280 Object newValue = converter.convertToBinding(command, dp);
281 HmDatapointConfig config = getChannelConfig(channel, dp);
282 sendDatapoint(dp, config, newValue);
286 } catch (HomematicClientException | GatewayNotAvailableException ex) {
287 logger.warn("{}", ex.getMessage());
288 } catch (IOException ex) {
289 if (dp != null && dp.getChannel().getDevice().isOffline()) {
290 logger.warn("Device '{}' is OFFLINE, can't send command '{}' for channel '{}'",
291 dp.getChannel().getDevice().getAddress(), command, channelUID);
292 logger.trace("{}", ex.getMessage(), ex);
294 logger.error("{}", ex.getMessage(), ex);
296 } catch (ConverterTypeException ex) {
297 logger.warn("{}, please check the item type and the commands in your scripts", ex.getMessage());
298 } catch (Exception ex) {
299 logger.error("{}", ex.getMessage(), ex);
303 private void sendDatapoint(HmDatapoint dp, HmDatapointConfig config, Object newValue)
304 throws IOException, HomematicClientException, GatewayNotAvailableException {
305 String rxMode = getRxModeForDatapointTransmission(dp.getName(), dp.getValue(), newValue);
306 getHomematicGateway().sendDatapoint(dp, config, newValue, rxMode);
310 * Returns the rx mode that shall be used for transmitting a new value of a datapoint to the device. The
311 * HomematicThingHandler always uses the default rx mode; custom thing handlers can override this method to
312 * adjust the rx mode.
314 * @param datapointName The datapoint that will be updated on the device
315 * @param currentValue The current value of the datapoint
316 * @param newValue The value that will be sent to the device
317 * @return The rxMode ({@link HomematicBindingConstants#RX_BURST_MODE "BURST"} for burst mode,
318 * {@link HomematicBindingConstants#RX_WAKEUP_MODE "WAKEUP"} for wakeup mode, or null for the default mode)
320 protected String getRxModeForDatapointTransmission(String datapointName, Object currentValue, Object newValue) {
325 * Evaluates the channel and datapoint for this channelUID and updates the state of the channel.
327 private void updateChannelState(ChannelUID channelUID)
328 throws GatewayNotAvailableException, HomematicClientException, IOException, ConverterException {
329 HomematicGateway gateway = getHomematicGateway();
330 HmDatapointInfo dpInfo = UidUtils.createHmDatapointInfo(channelUID);
331 HmDatapoint dp = gateway.getDatapoint(dpInfo);
332 Channel channel = getThing().getChannel(channelUID.getId());
333 updateChannelState(dp, channel);
337 * Sets the configuration or evaluates the channel for this datapoint and updates the state of the channel.
339 protected void updateDatapointState(HmDatapoint dp) {
341 updateStatus(dp.getChannel().getDevice());
343 if (dp.getParamsetType() == HmParamsetType.MASTER) {
344 // update configuration
345 Configuration config = editConfiguration();
346 config.put(MetadataUtils.getParameterName(dp), dp.isEnumType() ? dp.getOptionValue() : dp.getValue());
347 updateConfiguration(config);
348 } else if (!HomematicTypeGeneratorImpl.isIgnoredDatapoint(dp)) {
350 ChannelUID channelUID = UidUtils.generateChannelUID(dp, thing.getUID());
351 Channel channel = thing.getChannel(channelUID.getId());
352 if (channel != null) {
353 updateChannelState(dp, channel);
355 logger.warn("Channel not found for datapoint '{}'", new HmDatapointInfo(dp));
358 } catch (GatewayNotAvailableException ex) {
360 } catch (Exception ex) {
361 logger.error("{}", ex.getMessage(), ex);
366 * Converts the value of the datapoint to a State, updates the channel and also sets the thing status if necessary.
368 private void updateChannelState(final HmDatapoint dp, Channel channel)
369 throws IOException, GatewayNotAvailableException, ConverterException {
370 if (dp.isTrigger()) {
371 if (dp.getValue() != null) {
372 triggerChannel(channel.getUID(), ObjectUtils.toString(dp.getValue()));
374 } else if (isLinked(channel)) {
375 loadHomematicChannelValues(dp.getChannel());
377 TypeConverter<?> converter = ConverterFactory.createConverter(channel.getAcceptedItemType());
378 State state = converter.convertFromBinding(dp);
380 updateState(channel.getUID(), state);
382 logger.debug("Failed to get converted state from datapoint '{}'", dp.getName());
388 * Loads all values for the given Homematic channel if it is not initialized.
390 private void loadHomematicChannelValues(HmChannel hmChannel) throws GatewayNotAvailableException, IOException {
391 if (!hmChannel.isInitialized()) {
392 synchronized (this) {
393 if (!hmChannel.isInitialized()) {
395 getHomematicGateway().loadChannelValues(hmChannel);
396 } catch (IOException ex) {
397 if (hmChannel.getDevice().isOffline()) {
398 logger.warn("Device '{}' is OFFLINE, can't update channel '{}'",
399 hmChannel.getDevice().getAddress(), hmChannel.getNumber());
410 * Updates the thing status based on device status.
412 private void updateStatus(HmDevice device) throws GatewayNotAvailableException, IOException {
413 loadHomematicChannelValues(device.getChannel(0));
415 ThingStatus oldStatus = thing.getStatus();
416 ThingStatus newStatus = ThingStatus.ONLINE;
417 ThingStatusDetail newDetail = ThingStatusDetail.NONE;
419 if ((getBridge() != null) && (getBridge().getStatus() == ThingStatus.OFFLINE)) {
420 newStatus = ThingStatus.OFFLINE;
421 newDetail = ThingStatusDetail.BRIDGE_OFFLINE;
422 } else if (device.isFirmwareUpdating()) {
423 newStatus = ThingStatus.OFFLINE;
424 newDetail = ThingStatusDetail.FIRMWARE_UPDATING;
425 } else if (device.isUnreach()) {
426 newStatus = ThingStatus.OFFLINE;
427 newDetail = ThingStatusDetail.COMMUNICATION_ERROR;
428 } else if (device.isConfigPending() || device.isUpdatePending()) {
429 newDetail = ThingStatusDetail.CONFIGURATION_PENDING;
432 if (thing.getStatus() != newStatus || thing.getStatusInfo().getStatusDetail() != newDetail) {
433 updateStatus(newStatus, newDetail);
435 if (oldStatus == ThingStatus.OFFLINE && newStatus == ThingStatus.ONLINE) {
441 * Returns true, if the channel is linked at least to one item.
443 private boolean isLinked(Channel channel) {
444 return channel != null && super.isLinked(channel.getUID().getId());
448 * Returns the channel config for the given datapoint.
450 protected HmDatapointConfig getChannelConfig(HmDatapoint dp) {
451 ChannelUID channelUid = UidUtils.generateChannelUID(dp, getThing().getUID());
452 Channel channel = getThing().getChannel(channelUid.getId());
453 return channel != null ? getChannelConfig(channel, dp) : new HmDatapointConfig();
457 * Returns the config for a channel.
459 private HmDatapointConfig getChannelConfig(Channel channel, HmDatapoint dp) {
460 return channel.getConfiguration().as(HmDatapointConfig.class);
464 * Returns the Homematic gateway if the bridge is available.
466 private HomematicGateway getHomematicGateway() throws GatewayNotAvailableException {
467 final Bridge bridge = getBridge();
468 if (bridge != null) {
469 HomematicBridgeHandler bridgeHandler = (HomematicBridgeHandler) bridge.getHandler();
470 if (bridgeHandler != null && bridgeHandler.getGateway() != null) {
471 return bridgeHandler.getGateway();
475 throw new GatewayNotAvailableException("HomematicGateway not yet available!");
479 public void handleConfigurationUpdate(Map<String, Object> configurationParameters)
480 throws ConfigValidationException {
481 super.handleConfigurationUpdate(configurationParameters);
484 HomematicGateway gateway = getHomematicGateway();
485 HmDevice device = gateway.getDevice(UidUtils.getHomematicAddress(getThing()));
487 for (Entry<String, Object> configurationParameter : configurationParameters.entrySet()) {
488 String key = configurationParameter.getKey();
489 Object newValue = configurationParameter.getValue();
491 if (key.startsWith("HMP_")) {
492 key = StringUtils.removeStart(key, "HMP_");
493 Integer channelNumber = NumberUtils.toInt(StringUtils.substringBefore(key, "_"));
494 String dpName = StringUtils.substringAfter(key, "_");
496 HmDatapointInfo dpInfo = new HmDatapointInfo(device.getAddress(), HmParamsetType.MASTER,
497 channelNumber, dpName);
498 HmDatapoint dp = device.getChannel(channelNumber).getDatapoint(dpInfo);
502 if (newValue != null) {
503 if (newValue instanceof BigDecimal) {
504 final BigDecimal decimal = (BigDecimal) newValue;
505 if (dp.isIntegerType()) {
506 newValue = decimal.intValue();
507 } else if (dp.isFloatType()) {
508 newValue = decimal.doubleValue();
511 if (ObjectUtils.notEqual(dp.isEnumType() ? dp.getOptionValue() : dp.getValue(),
513 sendDatapoint(dp, new HmDatapointConfig(), newValue);
516 } catch (IOException ex) {
517 logger.error("Error setting thing property {}: {}", dpInfo, ex.getMessage());
520 logger.error("Can't find datapoint for thing property {}", dpInfo);
524 gateway.triggerDeviceValuesReload(device);
525 } catch (HomematicClientException | GatewayNotAvailableException ex) {
526 logger.error("Error setting thing properties: {}", ex.getMessage(), ex);
530 @SuppressWarnings("null")
532 public synchronized void handleRemoval() {
534 final ThingHandler handler;
536 if ((bridge = getBridge()) == null || (handler = bridge.getHandler()) == null) {
537 super.handleRemoval();
541 final HomematicConfig config = bridge.getConfiguration().as(HomematicConfig.class);
542 final boolean factoryResetOnDeletion = config.isFactoryResetOnDeletion();
543 final boolean unpairOnDeletion = factoryResetOnDeletion || config.isUnpairOnDeletion();
545 if (unpairOnDeletion) {
546 deviceDeletionPending = true;
547 ((HomematicBridgeHandler) handler).deleteFromGateway(UidUtils.getHomematicAddress(thing),
548 factoryResetOnDeletion, false, true);
550 super.handleRemoval();
555 * Called by the bridgeHandler when this device has been removed from the gateway.
557 public synchronized void deviceRemoved() {
558 deviceDeletionPending = false;
559 if (getThing().getStatus() == ThingStatus.REMOVING) {
560 // thing removal was initiated on ESH side
561 updateStatus(ThingStatus.REMOVED);
563 // device removal was initiated on homematic side, thing is not removed
564 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.GONE);
569 * Called by the bridgeHandler when the device for this thing has been added to the gateway.
570 * This is used to reconnect a device that was previously unpaired.
572 * @param device The device that has been added to the gateway
574 public void deviceLoaded(HmDevice device) {
576 updateStatus(device);
577 } catch (GatewayNotAvailableException ex) {
579 } catch (IOException ex) {
580 logger.warn("Could not reinitialize the device '{}': {}", device.getAddress(), ex.getMessage(), ex);
585 * Returns whether the device deletion is pending.
587 * @return true, if the deletion of this device on its gateway has been triggered but has not yet completed
589 public synchronized boolean isDeletionPending() {
590 return deviceDeletionPending;