2 * Copyright (c) 2010-2023 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.ihc.internal.handler;
15 import static org.openhab.binding.ihc.internal.IhcBindingConstants.*;
18 import java.math.BigDecimal;
19 import java.time.Duration;
20 import java.time.LocalDateTime;
21 import java.time.ZoneId;
22 import java.time.ZonedDateTime;
23 import java.util.ArrayList;
24 import java.util.Collections;
25 import java.util.HashMap;
26 import java.util.HashSet;
27 import java.util.List;
30 import java.util.concurrent.ScheduledFuture;
31 import java.util.concurrent.TimeUnit;
32 import java.util.stream.Collectors;
34 import org.openhab.binding.ihc.internal.ButtonPressDurationDetector;
35 import org.openhab.binding.ihc.internal.ChannelUtils;
36 import org.openhab.binding.ihc.internal.EnumDictionary;
37 import org.openhab.binding.ihc.internal.SignalLevelConverter;
38 import org.openhab.binding.ihc.internal.config.ChannelParams;
39 import org.openhab.binding.ihc.internal.config.IhcConfiguration;
40 import org.openhab.binding.ihc.internal.converters.Converter;
41 import org.openhab.binding.ihc.internal.converters.ConverterAdditionalInfo;
42 import org.openhab.binding.ihc.internal.converters.ConverterFactory;
43 import org.openhab.binding.ihc.internal.ws.IhcClient;
44 import org.openhab.binding.ihc.internal.ws.IhcClient.ConnectionState;
45 import org.openhab.binding.ihc.internal.ws.IhcEventListener;
46 import org.openhab.binding.ihc.internal.ws.datatypes.WSControllerState;
47 import org.openhab.binding.ihc.internal.ws.datatypes.WSProjectInfo;
48 import org.openhab.binding.ihc.internal.ws.datatypes.WSRFDevice;
49 import org.openhab.binding.ihc.internal.ws.datatypes.WSSystemInfo;
50 import org.openhab.binding.ihc.internal.ws.datatypes.WSTimeManagerSettings;
51 import org.openhab.binding.ihc.internal.ws.exeptions.ConversionException;
52 import org.openhab.binding.ihc.internal.ws.exeptions.IhcExecption;
53 import org.openhab.binding.ihc.internal.ws.exeptions.IhcFatalExecption;
54 import org.openhab.binding.ihc.internal.ws.projectfile.IhcEnumValue;
55 import org.openhab.binding.ihc.internal.ws.projectfile.ProjectFileUtils;
56 import org.openhab.binding.ihc.internal.ws.resourcevalues.WSBooleanValue;
57 import org.openhab.binding.ihc.internal.ws.resourcevalues.WSEnumValue;
58 import org.openhab.binding.ihc.internal.ws.resourcevalues.WSResourceValue;
59 import org.openhab.core.OpenHAB;
60 import org.openhab.core.library.types.DateTimeType;
61 import org.openhab.core.library.types.DecimalType;
62 import org.openhab.core.library.types.OnOffType;
63 import org.openhab.core.library.types.StringType;
64 import org.openhab.core.thing.Channel;
65 import org.openhab.core.thing.ChannelUID;
66 import org.openhab.core.thing.Thing;
67 import org.openhab.core.thing.ThingStatus;
68 import org.openhab.core.thing.ThingStatusDetail;
69 import org.openhab.core.thing.binding.BaseThingHandler;
70 import org.openhab.core.types.Command;
71 import org.openhab.core.types.RefreshType;
72 import org.openhab.core.types.State;
73 import org.openhab.core.types.Type;
74 import org.slf4j.Logger;
75 import org.slf4j.LoggerFactory;
76 import org.w3c.dom.Document;
79 * The {@link IhcHandler} is responsible for handling commands, which are
80 * sent to one of the channels.
82 * @author Pauli Anttila - Initial contribution
84 public class IhcHandler extends BaseThingHandler implements IhcEventListener {
85 private final Logger logger = LoggerFactory.getLogger(IhcHandler.class);
87 /** Maximum pulse width in milliseconds. */
88 private static final int MAX_PULSE_WIDTH_IN_MS = 4000;
90 /** Maximum long press time in milliseconds. */
91 private static final int MAX_LONG_PRESS_IN_MS = 5000;
93 /** Name of the local IHC / ELKO project file */
94 private static final String LOCAL_IHC_PROJECT_FILE_NAME_TEMPLATE = "ihc-project-file-%s.xml";
96 /** Holds runtime notification reorder timeout in milliseconds */
97 private static final int NOTIFICATIONS_REORDER_WAIT_TIME = 1000;
99 /** IHC / ELKO LS Controller client */
100 private IhcClient ihc;
103 * Reminder to slow down resource value notification ordering from
106 private ScheduledFuture<?> notificationsRequestReminder;
108 /** Holds local IHC / ELKO project file */
109 private Document projectFile;
112 * Store current state of the controller, use to recognize when controller
115 private String controllerState = "";
117 private IhcConfiguration conf;
118 private final Set<Integer> linkedResourceIds = Collections.synchronizedSet(new HashSet<>());
119 private Map<Integer, LocalDateTime> lastUpdate = new HashMap<>();
120 private EnumDictionary enumDictionary;
122 private boolean connecting = false;
123 private boolean reconnectRequest = false;
124 private boolean valueNotificationRequest = false;
126 private ScheduledFuture<?> controlJob;
127 private ScheduledFuture<?> pollingJobRf;
129 private Map<String, ScheduledFuture<?>> longPressFutures = new HashMap<>();
131 public IhcHandler(Thing thing) {
135 protected boolean isValueNotificationRequestActivated() {
136 synchronized (this) {
137 return valueNotificationRequest;
141 protected void setValueNotificationRequest(boolean valueNotificationRequest) {
142 synchronized (this) {
143 this.valueNotificationRequest = valueNotificationRequest;
147 protected boolean isReconnectRequestActivated() {
148 synchronized (this) {
149 return reconnectRequest;
153 protected void setReconnectRequest(boolean reconnect) {
154 synchronized (this) {
155 this.reconnectRequest = reconnect;
159 protected boolean isConnecting() {
160 synchronized (this) {
165 protected void setConnectingState(boolean value) {
166 synchronized (this) {
167 this.connecting = value;
171 private String getFilePathInUserDataFolder(String fileName) {
172 return OpenHAB.getUserDataFolder() + File.separator + fileName;
176 public void initialize() {
177 conf = getConfigAs(IhcConfiguration.class);
178 logger.debug("Using configuration: {}", conf);
180 linkedResourceIds.clear();
181 linkedResourceIds.addAll(getAllLinkedChannelsResourceIds());
182 logger.debug("Linked resources {}: {}", linkedResourceIds.size(), linkedResourceIds);
184 if (controlJob == null || controlJob.isCancelled()) {
185 logger.debug("Start control task, interval={}sec", 1);
186 controlJob = scheduler.scheduleWithFixedDelay(this::reconnectCheck, 0, 1, TimeUnit.SECONDS);
191 public void dispose() {
192 logger.debug("Stopping thing");
193 if (controlJob != null && !controlJob.isCancelled()) {
194 controlJob.cancel(true);
201 public void handleCommand(ChannelUID channelUID, Command command) {
202 logger.debug("Received channel: {}, command: {}", channelUID, command);
205 logger.debug("Connection is not initialized, aborting resource value update for channel '{}'!", channelUID);
209 if (ihc.getConnectionState() != ConnectionState.CONNECTED) {
210 logger.debug("Connection to controller is not open, aborting resource value update for channel '{}'!",
215 if (thing.getStatus() != ThingStatus.ONLINE) {
216 logger.debug("Controller is not ONLINE, aborting resource value update for channel '{}'!", channelUID);
220 switch (channelUID.getId()) {
221 case CHANNEL_CONTROLLER_STATE:
222 if (command.equals(RefreshType.REFRESH)) {
223 updateControllerStateChannel();
227 case CHANNEL_CONTROLLER_UPTIME:
228 if (command.equals(RefreshType.REFRESH)) {
229 updateControllerInformationChannels();
233 case CHANNEL_CONTROLLER_TIME:
234 if (command.equals(RefreshType.REFRESH)) {
235 updateControllerTimeChannels();
240 if (command.equals(RefreshType.REFRESH)) {
241 refreshChannel(channelUID);
243 updateResourceChannel(channelUID, command);
249 private void refreshChannel(ChannelUID channelUID) {
250 logger.debug("REFRESH channel {}", channelUID);
251 Channel channel = thing.getChannel(channelUID.getId());
252 if (channel != null) {
254 ChannelParams params = new ChannelParams(channel);
255 logger.debug("Channel params: {}", params);
256 if (params.isDirectionWriteOnly()) {
257 logger.warn("Write only channel, skip refresh command to {}", channelUID);
260 WSResourceValue value = ihc.resourceQuery(params.getResourceId());
261 resourceValueUpdateReceived(value);
262 } catch (IhcExecption e) {
263 logger.warn("Can't update channel '{}' value, reason: {}", channelUID, e.getMessage(), e);
264 } catch (ConversionException e) {
265 logger.warn("Channel param error, reason: {}.", e.getMessage(), e);
270 private void updateControllerStateChannel() {
272 String state = ihc.getControllerState().getState();
276 case IhcClient.CONTROLLER_STATE_INITIALIZE:
277 value = "Initialize";
279 case IhcClient.CONTROLLER_STATE_READY:
283 value = "Unknown state: " + state;
286 updateState(new ChannelUID(getThing().getUID(), CHANNEL_CONTROLLER_STATE), new StringType(value));
287 } catch (IhcExecption e) {
288 logger.warn("Controller state information fetch failed, reason: {}", e.getMessage(), e);
292 private void updateControllerProperties() {
294 WSSystemInfo systemInfo = ihc.getSystemInfo();
295 logger.debug("Controller information: {}", systemInfo);
296 WSProjectInfo projectInfo = ihc.getProjectInfo();
297 logger.debug("Project information: {}", projectInfo);
299 Map<String, String> properties = editProperties();
300 properties.put(PROPERTY_MANUFACTURER, systemInfo.getBrand());
301 properties.put(PROPERTY_SERIALNUMBER, systemInfo.getSerialNumber());
302 properties.put(PROPERTY_SW_VERSION, systemInfo.getVersion());
303 properties.put(PROPERTY_FW_VERSION, systemInfo.getHwRevision());
304 properties.put(PROPERTY_APP_WITHOUT_VIEWER, Boolean.toString(systemInfo.getApplicationIsWithoutViewer()));
305 properties.put(PROPERTY_SW_DATE,
306 systemInfo.getSwDate().withZoneSameInstant(ZoneId.systemDefault()).toString());
307 properties.put(PROPERTY_PRODUCTION_DATE, systemInfo.getProductionDate());
308 if (!systemInfo.getDatalineVersion().isEmpty()) {
309 properties.put(PROPERTY_DATALINE_VERSION, systemInfo.getDatalineVersion());
311 if (!systemInfo.getRfModuleSerialNumber().isEmpty()) {
312 properties.put(PROPERTY_RF_MODULE_SERIALNUMBER, systemInfo.getRfModuleSerialNumber());
314 if (!systemInfo.getRfModuleSoftwareVersion().isEmpty()) {
315 properties.put(PROPERTY_RF_MODULE_VERSION, systemInfo.getRfModuleSoftwareVersion());
317 properties.put(PROPERTY_PROJECT_DATE,
318 projectInfo.getLastmodified().getAsLocalDateTime().atZone(ZoneId.systemDefault()).toString());
319 properties.put(PROPERTY_PROJECT_NUMBER, projectInfo.getProjectNumber());
320 updateProperties(properties);
321 } catch (IhcExecption e) {
322 logger.warn("Controller information fetch failed, reason: {}", e.getMessage(), e);
326 private void updateControllerInformationChannels() {
328 WSSystemInfo systemInfo = ihc.getSystemInfo();
329 logger.debug("Controller information: {}", systemInfo);
331 updateState(new ChannelUID(getThing().getUID(), CHANNEL_CONTROLLER_UPTIME),
332 new DecimalType((double) systemInfo.getUptime() / 1000));
333 } catch (IhcExecption e) {
334 logger.warn("Controller uptime information fetch failed, reason: {}.", e.getMessage(), e);
338 private void updateControllerTimeChannels() {
340 WSTimeManagerSettings timeSettings = ihc.getTimeSettings();
341 logger.debug("Controller time settings: {}", timeSettings);
343 ZonedDateTime time = timeSettings.getTimeAndDateInUTC().getAsZonedDateTime(ZoneId.of("Z"))
344 .withZoneSameInstant(ZoneId.systemDefault());
345 updateState(new ChannelUID(getThing().getUID(), CHANNEL_CONTROLLER_TIME), new DateTimeType(time));
346 } catch (IhcExecption e) {
347 logger.warn("Controller uptime information fetch failed, reason: {}.", e.getMessage(), e);
351 private void updateResourceChannel(ChannelUID channelUID, Command command) {
352 Channel channel = thing.getChannel(channelUID.getId());
353 if (channel != null) {
355 ChannelParams params = new ChannelParams(channel);
356 logger.debug("Channel params: {}", params);
357 if (params.isDirectionReadOnly()) {
358 logger.debug("Read only channel, skip the update to {}", channelUID);
361 updateChannel(channelUID, params, command);
362 } catch (IhcExecption e) {
363 logger.warn("Can't update channel '{}' value, cause {}", channelUID, e.getMessage());
364 } catch (ConversionException e) {
365 logger.debug("Conversion error for channel {}, reason: {}", channelUID, e.getMessage());
370 private void updateChannel(ChannelUID channelUID, ChannelParams params, Command command)
371 throws IhcExecption, ConversionException {
372 if (params.getCommandToReact() != null) {
373 if (command.toString().equals(params.getCommandToReact())) {
374 logger.debug("Command '{}' equal to channel reaction parameter '{}', execute it", command,
375 params.getCommandToReact());
377 logger.debug("Command '{}' doesn't equal to reaction trigger parameter '{}', skip it", command,
378 params.getCommandToReact());
382 WSResourceValue value = ihc.getResourceValueInformation(params.getResourceId());
384 if (params.getPulseWidth() != null) {
385 sendPulseCommand(channelUID, params, value, Math.min(params.getPulseWidth(), MAX_PULSE_WIDTH_IN_MS));
387 sendNormalCommand(channelUID, params, command, value);
392 private void sendNormalCommand(ChannelUID channelUID, ChannelParams params, Command command, WSResourceValue value)
393 throws IhcExecption, ConversionException {
394 logger.debug("Send command '{}' to resource '{}'", command, value.resourceID);
395 ConverterAdditionalInfo converterAdditionalInfo = new ConverterAdditionalInfo(getEnumValues(value),
396 params.isInverted(), getCommandLevels(params));
397 Converter<WSResourceValue, Type> converter = ConverterFactory.getInstance().getConverter(value.getClass(),
399 if (converter != null) {
400 WSResourceValue val = converter.convertFromOHType(command, value, converterAdditionalInfo);
401 logger.debug("Update resource value (inverted output={}): {}", params.isInverted(), val);
402 if (!updateResource(val)) {
403 logger.warn("Channel {} update to resource '{}' failed.", channelUID, val);
406 logger.debug("No converter implemented for {} <-> {}", value.getClass(), command.getClass());
410 private List<IhcEnumValue> getEnumValues(WSResourceValue value) {
411 if (value instanceof WSEnumValue) {
412 return enumDictionary.getEnumValues(((WSEnumValue) value).definitionTypeID);
417 private void sendPulseCommand(ChannelUID channelUID, ChannelParams params, WSResourceValue value,
418 Integer pulseWidth) throws IhcExecption, ConversionException {
419 logger.debug("Send {}ms pulse to resource: {}", pulseWidth, value.resourceID);
420 logger.debug("Channel params: {}", params);
421 Converter<WSResourceValue, Type> converter = ConverterFactory.getInstance().getConverter(value.getClass(),
424 if (converter != null) {
425 ConverterAdditionalInfo converterAdditionalInfo = new ConverterAdditionalInfo(null, params.isInverted(),
426 getCommandLevels(params));
428 WSResourceValue valOn = converter.convertFromOHType(OnOffType.ON, value, converterAdditionalInfo);
429 WSResourceValue valOff = converter.convertFromOHType(OnOffType.OFF, value, converterAdditionalInfo);
431 // set resource to ON
432 logger.debug("Update resource value (inverted output={}): {}", params.isInverted(), valOn);
433 if (updateResource(valOn)) {
434 logger.debug("Sleeping: {}ms", pulseWidth);
435 scheduler.schedule(new Runnable() {
438 // set resource back to OFF
439 logger.debug("Update resource value (inverted output={}): {}", params.isInverted(), valOff);
441 if (!updateResource(valOff)) {
442 logger.warn("Channel {} update to resource '{}' failed.", channelUID, valOff);
444 } catch (IhcExecption e) {
445 logger.warn("Can't update channel '{}' value, cause {}", channelUID, e.getMessage());
448 }, pulseWidth, TimeUnit.MILLISECONDS);
450 logger.warn("Channel {} update failed.", channelUID);
453 logger.debug("No converter implemented for {} <-> {}", value.getClass(), OnOffType.class);
458 * Update resource value to IHC controller.
460 private boolean updateResource(WSResourceValue value) throws IhcExecption {
461 boolean result = false;
463 result = ihc.resourceUpdate(value);
464 } catch (IhcExecption e) {
465 logger.warn("Value could not be updated - retrying one time: {}.", e.getMessage(), e);
466 result = ihc.resourceUpdate(value);
472 public void channelLinked(ChannelUID channelUID) {
473 logger.debug("channelLinked: {}", channelUID);
475 switch (channelUID.getId()) {
476 case CHANNEL_CONTROLLER_STATE:
477 updateControllerStateChannel();
480 case CHANNEL_CONTROLLER_UPTIME:
481 updateControllerInformationChannels();
484 case CHANNEL_CONTROLLER_TIME:
485 updateControllerTimeChannels();
489 Channel channel = thing.getChannel(channelUID.getId());
490 if (channel != null) {
492 ChannelParams params = new ChannelParams(channel);
493 if (params.getResourceId() != null) {
494 if (!linkedResourceIds.contains(params.getResourceId())) {
495 logger.debug("New channel '{}' found, resource id '{}'", channelUID.getAsString(),
496 params.getResourceId());
497 linkedResourceIds.add(params.getResourceId());
498 updateNotificationsRequestReminder();
501 } catch (ConversionException e) {
502 logger.warn("Channel param error, reason: {}.", e.getMessage(), e);
509 public void channelUnlinked(ChannelUID channelUID) {
510 logger.debug("channelUnlinked: {}", channelUID);
512 switch (channelUID.getId()) {
513 case CHANNEL_CONTROLLER_STATE:
514 case CHANNEL_CONTROLLER_UPTIME:
515 case CHANNEL_CONTROLLER_TIME:
519 Channel channel = thing.getChannel(channelUID.getId());
520 if (channel != null) {
522 ChannelParams params = new ChannelParams(channel);
523 if (params.getResourceId() != null) {
524 linkedResourceIds.removeIf(c -> c.equals(params.getResourceId()));
525 updateNotificationsRequestReminder();
527 } catch (ConversionException e) {
528 logger.warn("Channel param error, reason: {}.", e.getMessage(), e);
535 * Initialize IHC client and open connection to IHC / ELKO LS controller.
538 private void connect() throws IhcExecption {
540 setConnectingState(true);
541 logger.debug("Connecting to IHC / ELKO LS controller [hostname='{}', username='{}'].", conf.hostname,
543 ihc = new IhcClient(conf.hostname, conf.username, conf.password, conf.timeout, conf.tlsVersion);
544 ihc.openConnection();
545 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE,
546 "Initializing communication to the IHC / ELKO controller");
549 updateControllerProperties();
550 updateControllerStateChannel();
551 updateControllerInformationChannels();
552 updateControllerTimeChannels();
553 ihc.addEventListener(this);
554 ihc.startControllerEventListeners();
555 updateNotificationsRequestReminder();
557 updateStatus(ThingStatus.ONLINE);
559 setConnectingState(false);
563 private void loadProject() throws IhcExecption {
564 if (conf.loadProjectFile) {
565 String fileName = String.format(LOCAL_IHC_PROJECT_FILE_NAME_TEMPLATE, thing.getUID().getId());
566 String filePath = getFilePathInUserDataFolder(fileName);
567 boolean loadProject = false;
569 if (projectFile == null) {
570 // try first load project file from local cache file.
572 projectFile = ProjectFileUtils.readFromFile(filePath);
573 } catch (IhcExecption e) {
574 logger.debug("Error occured when read project file from file '{}', reason {}", filePath,
580 if (!ProjectFileUtils.projectEqualsToControllerProject(projectFile, ihc.getProjectInfo())) {
582 "Local project file is not same as in the controller, reload project file from controller!");
587 logger.debug("Loading IHC /ELKO LS project file from controller...");
588 byte[] data = ihc.getProjectFileFromController();
589 logger.debug("Saving project file to local file '{}'", filePath);
591 ProjectFileUtils.saveToFile(filePath, data);
592 } catch (IhcExecption e) {
593 logger.warn("Error occured when trying to write data to file '{}', reason {}", filePath,
596 projectFile = ProjectFileUtils.converteBytesToDocument(data);
600 enumDictionary = new EnumDictionary(ProjectFileUtils.parseEnums(projectFile));
603 private void createChannels() {
604 if (conf.loadProjectFile && conf.createChannelsAutomatically) {
605 logger.debug("Creating channels");
606 List<Channel> thingChannels = new ArrayList<>();
607 thingChannels.addAll(getThing().getChannels());
608 ChannelUtils.addControllerChannels(getThing(), thingChannels);
609 ChannelUtils.addChannelsFromProjectFile(getThing(), projectFile, thingChannels);
610 printChannels(thingChannels);
611 updateThing(editThing().withChannels(thingChannels).build());
613 logger.debug("Automatic channel creation disabled");
617 private void printChannels(List<Channel> thingChannels) {
618 if (logger.isDebugEnabled()) {
619 thingChannels.forEach(channel -> {
620 if (channel != null) {
623 Object id = channel.getConfiguration().get(PARAM_RESOURCE_ID);
624 resourceId = id != null ? "0x" + Integer.toHexString(((BigDecimal) id).intValue()) : "";
625 } catch (IllegalArgumentException e) {
629 String channelType = channel.getAcceptedItemType() != null ? channel.getAcceptedItemType() : "";
630 String channelLabel = channel.getLabel() != null ? channel.getLabel() : "";
632 logger.debug("Channel: {}", String.format("%-55s | %-10s | %-10s | %s", channel.getUID(),
633 resourceId, channelType, channelLabel));
639 private void startRFPolling() {
640 if (pollingJobRf == null || pollingJobRf.isCancelled()) {
641 logger.debug("Start RF device refresh task, interval={}sec", 60);
642 pollingJobRf = scheduler.scheduleWithFixedDelay(this::updateRfDeviceStates, 10, 60, TimeUnit.SECONDS);
647 * Disconnect connection to IHC / ELKO LS controller.
650 private void disconnect() {
651 cancelAllLongPressTasks();
652 if (pollingJobRf != null && !pollingJobRf.isCancelled()) {
653 pollingJobRf.cancel(true);
658 ihc.removeEventListener(this);
659 ihc.closeConnection();
661 } catch (IhcExecption e) {
662 logger.warn("Couldn't close connection to IHC controller", e);
665 clearLastUpdateTimeCache();
668 private void clearLastUpdateTimeCache() {
673 public void errorOccured(IhcExecption e) {
674 logger.warn("Error occurred on communication to IHC controller: {}", e.getMessage(), e);
675 logger.debug("Reconnection request");
676 setReconnectRequest(true);
680 public void statusUpdateReceived(WSControllerState newState) {
681 logger.debug("Controller state: {}", newState.getState());
683 if (!controllerState.equals(newState.getState())) {
684 logger.debug("Controller state change detected ({} -> {})", controllerState, newState.getState());
686 switch (newState.getState()) {
687 case IhcClient.CONTROLLER_STATE_INITIALIZE:
688 logger.info("Controller state changed to initializing state, waiting for ready state");
689 updateState(new ChannelUID(getThing().getUID(), CHANNEL_CONTROLLER_STATE),
690 new StringType("initialize"));
691 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE,
692 "Controller is in initializing state");
694 case IhcClient.CONTROLLER_STATE_READY:
695 logger.info("Controller state changed to ready state");
696 updateState(new ChannelUID(getThing().getUID(), CHANNEL_CONTROLLER_STATE), new StringType("ready"));
697 updateStatus(ThingStatus.ONLINE);
702 if (controllerState.equals(IhcClient.CONTROLLER_STATE_INITIALIZE)
703 && newState.getState().equals(IhcClient.CONTROLLER_STATE_READY)) {
704 logger.debug("Reconnection request");
706 setReconnectRequest(true);
710 controllerState = newState.getState();
714 public void resourceValueUpdateReceived(WSResourceValue value) {
715 logger.debug("resourceValueUpdateReceived: {}", value);
717 thing.getChannels().forEach(channel -> {
719 ChannelParams params = new ChannelParams(channel);
720 if (params.getResourceId() != null && params.getResourceId().intValue() == value.resourceID) {
721 updateChannelState(channel, params, value);
723 } catch (ConversionException e) {
724 logger.warn("Channel param error, reason: {}.", e.getMessage(), e);
725 } catch (RuntimeException e) {
726 logger.warn("Unknown error occured, reason: {}.", e.getMessage(), e);
730 checkPotentialButtonPresses(value);
733 private void updateChannelState(Channel channel, ChannelParams params, WSResourceValue value) {
734 if (params.isDirectionWriteOnly()) {
735 logger.debug("Write only channel, skip update to {}", channel.getUID());
737 if (params.getChannelTypeId() != null) {
738 switch (params.getChannelTypeId()) {
739 case CHANNEL_TYPE_PUSH_BUTTON_TRIGGER:
744 logger.debug("Update channel '{}' state, channel params: {}", channel.getUID(), params);
745 Converter<WSResourceValue, Type> converter = ConverterFactory.getInstance()
746 .getConverter(value.getClass(), channel.getAcceptedItemType());
747 if (converter != null) {
748 State state = (State) converter.convertFromResourceValue(value,
749 new ConverterAdditionalInfo(null, params.isInverted(),
750 getCommandLevels(params)));
751 updateState(channel.getUID(), state);
753 logger.debug("No converter implemented for {} <-> {}", value.getClass(),
754 channel.getAcceptedItemType());
756 } catch (ConversionException e) {
757 logger.debug("Can't convert resource value '{}' to item type {}, reason: {}.", value,
758 channel.getAcceptedItemType(), e.getMessage(), e);
765 private void checkPotentialButtonPresses(WSResourceValue value) {
766 if (value instanceof WSBooleanValue) {
767 if (((WSBooleanValue) value).value) {
768 // potential button press
769 lastUpdate.put(value.resourceID, LocalDateTime.now());
770 updateTriggers(value.resourceID, Duration.ZERO);
772 // potential button release
773 LocalDateTime lastUpdateTime = lastUpdate.get(value.resourceID);
774 if (lastUpdateTime != null) {
775 Duration duration = Duration.between(lastUpdateTime, LocalDateTime.now());
776 logger.debug("Time between uddates: {}", duration);
777 updateTriggers(value.resourceID, duration);
783 private void updateTriggers(int resourceId, Duration duration) {
784 thing.getChannels().forEach(channel -> {
786 ChannelParams params = new ChannelParams(channel);
787 if (params.getResourceId() != null && params.getResourceId().intValue() == resourceId) {
788 if (params.getChannelTypeId() != null) {
789 switch (params.getChannelTypeId()) {
790 case CHANNEL_TYPE_PUSH_BUTTON_TRIGGER:
791 logger.debug("Update trigger channel '{}', channel params: {}",
792 channel.getUID().getId(), params);
793 if (duration.toMillis() == 0) {
794 triggerChannel(channel.getUID().getId(), EVENT_PRESSED);
795 createLongPressTask(channel.getUID().getId(), params.getLongPressTime());
797 cancelLongPressTask(channel.getUID().getId());
798 triggerChannel(channel.getUID().getId(), EVENT_RELEASED);
799 triggerChannel(channel.getUID().getId(), String.valueOf(duration.toMillis()));
800 ButtonPressDurationDetector button = new ButtonPressDurationDetector(duration,
801 params.getLongPressTime(), MAX_LONG_PRESS_IN_MS);
802 logger.debug("resourceId={}, ButtonPressDurationDetector={}", resourceId, button);
803 if (button.isShortPress()) {
804 triggerChannel(channel.getUID().getId(), EVENT_SHORT_PRESS);
811 } catch (ConversionException e) {
812 logger.warn("Channel param error, reason: {}", e.getMessage(), e);
817 private void createLongPressTask(String channelId, long longPressTimeInMs) {
818 if (longPressFutures.containsKey(channelId)) {
819 cancelLongPressTask(channelId);
821 logger.debug("Create long press task for channel '{}'", channelId);
822 longPressFutures.put(channelId, scheduler.schedule(() -> triggerChannel(channelId, EVENT_LONG_PRESS),
823 longPressTimeInMs, TimeUnit.MILLISECONDS));
826 private void cancelLongPressTask(String channelId) {
827 if (longPressFutures.containsKey(channelId)) {
828 logger.debug("Cancel long press task for channel '{}'", channelId);
829 longPressFutures.get(channelId).cancel(false);
830 longPressFutures.remove(channelId);
834 private void cancelAllLongPressTasks() {
835 longPressFutures.entrySet().parallelStream().forEach(e -> e.getValue().cancel(true));
836 longPressFutures.clear();
839 private void updateRfDeviceStates() {
841 if (ihc.getConnectionState() != ConnectionState.CONNECTED) {
842 logger.debug("Controller is connecting, abort subscribe");
846 logger.debug("Update RF device data");
848 List<WSRFDevice> devs = ihc.getDetectedRFDevices();
849 logger.debug("RF data: {}", devs);
851 devs.forEach(dev -> {
852 thing.getChannels().forEach(channel -> {
854 ChannelParams params = new ChannelParams(channel);
855 if (params.getSerialNumber() != null
856 && params.getSerialNumber().longValue() == dev.getSerialNumber()) {
857 String channelId = channel.getUID().getId();
858 if (params.getChannelTypeId() != null) {
859 switch (params.getChannelTypeId()) {
860 case CHANNEL_TYPE_RF_LOW_BATTERY:
861 updateState(channelId,
862 dev.getBatteryLevel() == 1 ? OnOffType.OFF : OnOffType.ON);
864 case CHANNEL_TYPE_RF_SIGNAL_STRENGTH:
865 int signalLevel = new SignalLevelConverter(dev.getSignalStrength())
866 .getSystemWideSignalLevel();
867 updateState(channelId, new StringType(String.valueOf(signalLevel)));
872 } catch (ConversionException e) {
873 logger.warn("Channel param error, reason: {}", e.getMessage(), e);
877 } catch (IhcExecption e) {
878 logger.debug("Error occured when fetching RF device information, reason: : {} ", e.getMessage(), e);
884 private void reconnectCheck() {
885 if (ihc == null || isReconnectRequestActivated()) {
891 setReconnectRequest(false);
892 } catch (IhcFatalExecption e) {
893 logger.warn("Can't open connection to controller {}", e.getMessage());
894 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
895 setReconnectRequest(false);
897 } catch (IhcExecption e) {
898 logger.debug("Can't open connection to controller {}", e.getMessage());
899 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
900 setReconnectRequest(true);
905 if (isValueNotificationRequestActivated() && !isConnecting()) {
907 enableResourceValueNotifications();
908 } catch (IhcExecption e) {
909 logger.warn("Can't enable resource value notifications from controller. ", e);
914 private Set<Integer> getAllLinkedChannelsResourceIds() {
915 Set<Integer> resourceIds = Collections.synchronizedSet(new HashSet<>());
916 resourceIds.addAll(this.getThing().getChannels().stream().filter(c -> isLinked(c.getUID())).map(c -> {
918 ChannelParams params = new ChannelParams(c);
919 logger.debug("Linked channel '{}' found, resource id '{}'", c.getUID().getAsString(),
920 params.getResourceId());
921 return params.getResourceId();
922 } catch (ConversionException e) {
923 logger.warn("Channel param error, reason: {}.", e.getMessage(), e);
926 }).filter(c -> c != null && c != 0).collect(Collectors.toSet()));
931 * Order resource value notifications from IHC controller.
933 private void enableResourceValueNotifications() throws IhcExecption {
934 logger.debug("Subscribe resource runtime value notifications");
937 if (ihc.getConnectionState() != ConnectionState.CONNECTED) {
938 logger.debug("Controller is connecting, abort subscribe");
941 setValueNotificationRequest(false);
942 Set<Integer> resourceIds = ChannelUtils.getAllTriggerChannelsResourceIds(getThing());
943 logger.debug("Enable runtime notfications for {} trigger(s)", resourceIds.size());
944 logger.debug("Enable runtime notfications for {} channel(s)", linkedResourceIds.size());
945 resourceIds.addAll(linkedResourceIds);
946 resourceIds.addAll(getAllLinkedChannelsResourceIds());
947 logger.debug("Enable runtime notfications for {} resources: {}", resourceIds.size(), resourceIds);
948 if (!resourceIds.isEmpty()) {
950 ihc.enableRuntimeValueNotifications(resourceIds);
951 } catch (IhcExecption e) {
952 logger.debug("Reconnection request");
953 setReconnectRequest(true);
957 logger.warn("Controller is not initialized!");
958 logger.debug("Reconnection request");
959 setReconnectRequest(true);
963 private synchronized void updateNotificationsRequestReminder() {
964 if (notificationsRequestReminder != null) {
965 notificationsRequestReminder.cancel(false);
968 logger.debug("Rechedule resource runtime value notifications order by {}ms", NOTIFICATIONS_REORDER_WAIT_TIME);
969 notificationsRequestReminder = scheduler.schedule(new Runnable() {
973 logger.debug("Delayed resource value notifications request is now enabled");
974 setValueNotificationRequest(true);
976 }, NOTIFICATIONS_REORDER_WAIT_TIME, TimeUnit.MILLISECONDS);
979 private Map<Command, Object> getCommandLevels(ChannelParams params) {
980 if (params.getOnLevel() != null) {
981 Map<Command, Object> commandLevels = new HashMap<>();
982 commandLevels.put(OnOffType.ON, params.getOnLevel());
983 return Collections.unmodifiableMap(commandLevels);