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.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.projectfile.IhcEnumValue;
54 import org.openhab.binding.ihc.internal.ws.projectfile.ProjectFileUtils;
55 import org.openhab.binding.ihc.internal.ws.resourcevalues.WSBooleanValue;
56 import org.openhab.binding.ihc.internal.ws.resourcevalues.WSEnumValue;
57 import org.openhab.binding.ihc.internal.ws.resourcevalues.WSResourceValue;
58 import org.openhab.core.OpenHAB;
59 import org.openhab.core.library.types.DateTimeType;
60 import org.openhab.core.library.types.DecimalType;
61 import org.openhab.core.library.types.OnOffType;
62 import org.openhab.core.library.types.StringType;
63 import org.openhab.core.thing.Channel;
64 import org.openhab.core.thing.ChannelUID;
65 import org.openhab.core.thing.Thing;
66 import org.openhab.core.thing.ThingStatus;
67 import org.openhab.core.thing.ThingStatusDetail;
68 import org.openhab.core.thing.binding.BaseThingHandler;
69 import org.openhab.core.types.Command;
70 import org.openhab.core.types.RefreshType;
71 import org.openhab.core.types.State;
72 import org.openhab.core.types.Type;
73 import org.slf4j.Logger;
74 import org.slf4j.LoggerFactory;
75 import org.w3c.dom.Document;
78 * The {@link IhcHandler} is responsible for handling commands, which are
79 * sent to one of the channels.
81 * @author Pauli Anttila - Initial contribution
83 public class IhcHandler extends BaseThingHandler implements IhcEventListener {
84 private final Logger logger = LoggerFactory.getLogger(IhcHandler.class);
86 /** Maximum pulse width in milliseconds. */
87 private static final int MAX_PULSE_WIDTH_IN_MS = 4000;
89 /** Maximum long press time in milliseconds. */
90 private static final int MAX_LONG_PRESS_IN_MS = 5000;
92 /** Name of the local IHC / ELKO project file */
93 private static final String LOCAL_IHC_PROJECT_FILE_NAME_TEMPLATE = "ihc-project-file-%s.xml";
95 /** Holds runtime notification reorder timeout in milliseconds */
96 private static final int NOTIFICATIONS_REORDER_WAIT_TIME = 1000;
98 /** IHC / ELKO LS Controller client */
99 private IhcClient ihc;
102 * Reminder to slow down resource value notification ordering from
105 private ScheduledFuture<?> notificationsRequestReminder;
107 /** Holds local IHC / ELKO project file */
108 private Document projectFile;
111 * Store current state of the controller, use to recognize when controller
114 private String controllerState = "";
116 private IhcConfiguration conf;
117 private final Set<Integer> linkedResourceIds = Collections.synchronizedSet(new HashSet<>());
118 private Map<Integer, LocalDateTime> lastUpdate = new HashMap<>();
119 private EnumDictionary enumDictionary;
121 private boolean connecting = false;
122 private boolean reconnectRequest = false;
123 private boolean valueNotificationRequest = false;
125 private ScheduledFuture<?> controlJob;
126 private ScheduledFuture<?> pollingJobRf;
128 private Map<String, ScheduledFuture<?>> longPressFutures = new HashMap<>();
130 public IhcHandler(Thing thing) {
134 protected boolean isValueNotificationRequestActivated() {
135 synchronized (this) {
136 return valueNotificationRequest;
140 protected void setValueNotificationRequest(boolean valueNotificationRequest) {
141 synchronized (this) {
142 this.valueNotificationRequest = valueNotificationRequest;
146 protected boolean isReconnectRequestActivated() {
147 synchronized (this) {
148 return reconnectRequest;
152 protected void setReconnectRequest(boolean reconnect) {
153 synchronized (this) {
154 this.reconnectRequest = reconnect;
158 protected boolean isConnecting() {
159 synchronized (this) {
164 protected void setConnectingState(boolean value) {
165 synchronized (this) {
166 this.connecting = value;
170 private String getFilePathInUserDataFolder(String fileName) {
171 return OpenHAB.getUserDataFolder() + File.separator + fileName;
175 public void initialize() {
176 conf = getConfigAs(IhcConfiguration.class);
177 logger.debug("Using configuration: {}", conf);
179 linkedResourceIds.clear();
180 linkedResourceIds.addAll(getAllLinkedChannelsResourceIds());
181 logger.debug("Linked resources {}: {}", linkedResourceIds.size(), linkedResourceIds);
183 if (controlJob == null || controlJob.isCancelled()) {
184 logger.debug("Start control task, interval={}sec", 1);
185 controlJob = scheduler.scheduleWithFixedDelay(this::reconnectCheck, 0, 1, TimeUnit.SECONDS);
190 public void dispose() {
191 logger.debug("Stopping thing");
192 if (controlJob != null && !controlJob.isCancelled()) {
193 controlJob.cancel(true);
200 public void handleCommand(ChannelUID channelUID, Command command) {
201 logger.debug("Received channel: {}, command: {}", channelUID, command);
204 logger.warn("Connection is not initialized, abort resource value update for channel '{}'!", channelUID);
208 if (ihc.getConnectionState() != ConnectionState.CONNECTED) {
209 logger.warn("Connection to controller is not open, abort resource value update for channel '{}'!",
214 switch (channelUID.getId()) {
215 case CHANNEL_CONTROLLER_STATE:
216 if (command.equals(RefreshType.REFRESH)) {
217 updateControllerStateChannel();
221 case CHANNEL_CONTROLLER_UPTIME:
222 if (command.equals(RefreshType.REFRESH)) {
223 updateControllerInformationChannels();
227 case CHANNEL_CONTROLLER_TIME:
228 if (command.equals(RefreshType.REFRESH)) {
229 updateControllerTimeChannels();
234 if (command.equals(RefreshType.REFRESH)) {
235 refreshChannel(channelUID);
237 updateResourceChannel(channelUID, command);
243 private void refreshChannel(ChannelUID channelUID) {
244 logger.debug("REFRESH channel {}", channelUID);
245 Channel channel = thing.getChannel(channelUID.getId());
246 if (channel != null) {
248 ChannelParams params = new ChannelParams(channel);
249 logger.debug("Channel params: {}", params);
250 if (params.isDirectionWriteOnly()) {
251 logger.warn("Write only channel, skip refresh command to {}", channelUID);
254 WSResourceValue value = ihc.resourceQuery(params.getResourceId());
255 resourceValueUpdateReceived(value);
256 } catch (IhcExecption e) {
257 logger.warn("Can't update channel '{}' value, reason: {}", channelUID, e.getMessage(), e);
258 } catch (ConversionException e) {
259 logger.warn("Channel param error, reason: {}.", e.getMessage(), e);
264 private void updateControllerStateChannel() {
266 String state = ihc.getControllerState().getState();
270 case IhcClient.CONTROLLER_STATE_INITIALIZE:
271 value = "Initialize";
273 case IhcClient.CONTROLLER_STATE_READY:
277 value = "Unknown state: " + state;
280 updateState(new ChannelUID(getThing().getUID(), CHANNEL_CONTROLLER_STATE), new StringType(value));
281 } catch (IhcExecption e) {
282 logger.warn("Controller state information fetch failed, reason: {}", e.getMessage(), e);
286 private void updateControllerProperties() {
288 WSSystemInfo systemInfo = ihc.getSystemInfo();
289 logger.debug("Controller information: {}", systemInfo);
290 WSProjectInfo projectInfo = ihc.getProjectInfo();
291 logger.debug("Project information: {}", projectInfo);
293 Map<String, String> properties = editProperties();
294 properties.put(PROPERTY_MANUFACTURER, systemInfo.getBrand());
295 properties.put(PROPERTY_SERIALNUMBER, systemInfo.getSerialNumber());
296 properties.put(PROPERTY_SW_VERSION, systemInfo.getVersion());
297 properties.put(PROPERTY_FW_VERSION, systemInfo.getHwRevision());
298 properties.put(PROPERTY_APP_WITHOUT_VIEWER, Boolean.toString(systemInfo.getApplicationIsWithoutViewer()));
299 properties.put(PROPERTY_SW_DATE,
300 systemInfo.getSwDate().withZoneSameInstant(ZoneId.systemDefault()).toString());
301 properties.put(PROPERTY_PRODUCTION_DATE, systemInfo.getProductionDate());
302 if (!systemInfo.getDatalineVersion().isEmpty()) {
303 properties.put(PROPERTY_DATALINE_VERSION, systemInfo.getDatalineVersion());
305 if (!systemInfo.getRfModuleSerialNumber().isEmpty()) {
306 properties.put(PROPERTY_RF_MODULE_SERIALNUMBER, systemInfo.getRfModuleSerialNumber());
308 if (!systemInfo.getRfModuleSoftwareVersion().isEmpty()) {
309 properties.put(PROPERTY_RF_MODULE_VERSION, systemInfo.getRfModuleSoftwareVersion());
311 properties.put(PROPERTY_PROJECT_DATE,
312 projectInfo.getLastmodified().getAsLocalDateTime().atZone(ZoneId.systemDefault()).toString());
313 properties.put(PROPERTY_PROJECT_NUMBER, projectInfo.getProjectNumber());
314 updateProperties(properties);
315 } catch (IhcExecption e) {
316 logger.warn("Controller information fetch failed, reason: {}", e.getMessage(), e);
320 private void updateControllerInformationChannels() {
322 WSSystemInfo systemInfo = ihc.getSystemInfo();
323 logger.debug("Controller information: {}", systemInfo);
325 updateState(new ChannelUID(getThing().getUID(), CHANNEL_CONTROLLER_UPTIME),
326 new DecimalType((double) systemInfo.getUptime() / 1000));
327 } catch (IhcExecption e) {
328 logger.warn("Controller uptime information fetch failed, reason: {}.", e.getMessage(), e);
332 private void updateControllerTimeChannels() {
334 WSTimeManagerSettings timeSettings = ihc.getTimeSettings();
335 logger.debug("Controller time settings: {}", timeSettings);
337 ZonedDateTime time = timeSettings.getTimeAndDateInUTC().getAsZonedDateTime(ZoneId.of("Z"))
338 .withZoneSameInstant(ZoneId.systemDefault());
339 updateState(new ChannelUID(getThing().getUID(), CHANNEL_CONTROLLER_TIME), new DateTimeType(time));
340 } catch (IhcExecption e) {
341 logger.warn("Controller uptime information fetch failed, reason: {}.", e.getMessage(), e);
345 private void updateResourceChannel(ChannelUID channelUID, Command command) {
346 Channel channel = thing.getChannel(channelUID.getId());
347 if (channel != null) {
349 ChannelParams params = new ChannelParams(channel);
350 logger.debug("Channel params: {}", params);
351 if (params.isDirectionReadOnly()) {
352 logger.debug("Read only channel, skip the update to {}", channelUID);
355 updateChannel(channelUID, params, command);
356 } catch (IhcExecption e) {
357 logger.warn("Can't update channel '{}' value, cause {}", channelUID, e.getMessage());
358 } catch (ConversionException e) {
359 logger.debug("Conversion error for channel {}, reason: {}", channelUID, e.getMessage());
364 private void updateChannel(ChannelUID channelUID, ChannelParams params, Command command)
365 throws IhcExecption, ConversionException {
366 if (params.getCommandToReact() != null) {
367 if (command.toString().equals(params.getCommandToReact())) {
368 logger.debug("Command '{}' equal to channel reaction parameter '{}', execute it", command,
369 params.getCommandToReact());
371 logger.debug("Command '{}' doesn't equal to reaction trigger parameter '{}', skip it", command,
372 params.getCommandToReact());
376 WSResourceValue value = ihc.getResourceValueInformation(params.getResourceId());
378 if (params.getPulseWidth() != null) {
379 sendPulseCommand(channelUID, params, value, Math.min(params.getPulseWidth(), MAX_PULSE_WIDTH_IN_MS));
381 sendNormalCommand(channelUID, params, command, value);
386 private void sendNormalCommand(ChannelUID channelUID, ChannelParams params, Command command, WSResourceValue value)
387 throws IhcExecption, ConversionException {
388 logger.debug("Send command '{}' to resource '{}'", command, value.resourceID);
389 ConverterAdditionalInfo converterAdditionalInfo = new ConverterAdditionalInfo(getEnumValues(value),
390 params.isInverted(), getCommandLevels(params));
391 Converter<WSResourceValue, Type> converter = ConverterFactory.getInstance().getConverter(value.getClass(),
393 if (converter != null) {
394 WSResourceValue val = converter.convertFromOHType(command, value, converterAdditionalInfo);
395 logger.debug("Update resource value (inverted output={}): {}", params.isInverted(), val);
396 if (!updateResource(val)) {
397 logger.warn("Channel {} update to resource '{}' failed.", channelUID, val);
400 logger.debug("No converter implemented for {} <-> {}", value.getClass(), command.getClass());
404 private List<IhcEnumValue> getEnumValues(WSResourceValue value) {
405 if (value instanceof WSEnumValue) {
406 return enumDictionary.getEnumValues(((WSEnumValue) value).definitionTypeID);
411 private void sendPulseCommand(ChannelUID channelUID, ChannelParams params, WSResourceValue value,
412 Integer pulseWidth) throws IhcExecption, ConversionException {
413 logger.debug("Send {}ms pulse to resource: {}", pulseWidth, value.resourceID);
414 logger.debug("Channel params: {}", params);
415 Converter<WSResourceValue, Type> converter = ConverterFactory.getInstance().getConverter(value.getClass(),
418 if (converter != null) {
419 ConverterAdditionalInfo converterAdditionalInfo = new ConverterAdditionalInfo(null, params.isInverted(),
420 getCommandLevels(params));
422 WSResourceValue valOn = converter.convertFromOHType(OnOffType.ON, value, converterAdditionalInfo);
423 WSResourceValue valOff = converter.convertFromOHType(OnOffType.OFF, value, converterAdditionalInfo);
425 // set resource to ON
426 logger.debug("Update resource value (inverted output={}): {}", params.isInverted(), valOn);
427 if (updateResource(valOn)) {
428 logger.debug("Sleeping: {}ms", pulseWidth);
429 scheduler.schedule(new Runnable() {
432 // set resource back to OFF
433 logger.debug("Update resource value (inverted output={}): {}", params.isInverted(), valOff);
435 if (!updateResource(valOff)) {
436 logger.warn("Channel {} update to resource '{}' failed.", channelUID, valOff);
438 } catch (IhcExecption e) {
439 logger.warn("Can't update channel '{}' value, cause {}", channelUID, e.getMessage());
442 }, pulseWidth, TimeUnit.MILLISECONDS);
444 logger.warn("Channel {} update failed.", channelUID);
447 logger.debug("No converter implemented for {} <-> {}", value.getClass(), OnOffType.class);
452 * Update resource value to IHC controller.
454 private boolean updateResource(WSResourceValue value) throws IhcExecption {
455 boolean result = false;
457 result = ihc.resourceUpdate(value);
458 } catch (IhcExecption e) {
459 logger.warn("Value could not be updated - retrying one time: {}.", e.getMessage(), e);
460 result = ihc.resourceUpdate(value);
466 public void channelLinked(ChannelUID channelUID) {
467 logger.debug("channelLinked: {}", channelUID);
469 switch (channelUID.getId()) {
470 case CHANNEL_CONTROLLER_STATE:
471 updateControllerStateChannel();
474 case CHANNEL_CONTROLLER_UPTIME:
475 updateControllerInformationChannels();
478 case CHANNEL_CONTROLLER_TIME:
479 updateControllerTimeChannels();
483 Channel channel = thing.getChannel(channelUID.getId());
484 if (channel != null) {
486 ChannelParams params = new ChannelParams(channel);
487 if (params.getResourceId() != null) {
488 if (!linkedResourceIds.contains(params.getResourceId())) {
489 logger.debug("New channel '{}' found, resource id '{}'", channelUID.getAsString(),
490 params.getResourceId());
491 linkedResourceIds.add(params.getResourceId());
492 updateNotificationsRequestReminder();
495 } catch (ConversionException e) {
496 logger.warn("Channel param error, reason: {}.", e.getMessage(), e);
503 public void channelUnlinked(ChannelUID channelUID) {
504 logger.debug("channelUnlinked: {}", channelUID);
506 switch (channelUID.getId()) {
507 case CHANNEL_CONTROLLER_STATE:
508 case CHANNEL_CONTROLLER_UPTIME:
509 case CHANNEL_CONTROLLER_TIME:
513 Channel channel = thing.getChannel(channelUID.getId());
514 if (channel != null) {
516 ChannelParams params = new ChannelParams(channel);
517 if (params.getResourceId() != null) {
518 linkedResourceIds.removeIf(c -> c.equals(params.getResourceId()));
519 updateNotificationsRequestReminder();
521 } catch (ConversionException e) {
522 logger.warn("Channel param error, reason: {}.", e.getMessage(), e);
529 * Initialize IHC client and open connection to IHC / ELKO LS controller.
532 private void connect() throws IhcExecption {
534 setConnectingState(true);
535 logger.debug("Connecting to IHC / ELKO LS controller [hostname='{}', username='{}'].", conf.hostname,
537 ihc = new IhcClient(conf.hostname, conf.username, conf.password, conf.timeout);
538 ihc.openConnection();
539 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE,
540 "Initializing communication to the IHC / ELKO controller");
543 updateControllerProperties();
544 updateControllerStateChannel();
545 updateControllerInformationChannels();
546 updateControllerTimeChannels();
547 ihc.addEventListener(this);
548 ihc.startControllerEventListeners();
549 updateNotificationsRequestReminder();
551 updateStatus(ThingStatus.ONLINE);
553 setConnectingState(false);
557 private void loadProject() throws IhcExecption {
558 if (conf.loadProjectFile) {
559 String fileName = String.format(LOCAL_IHC_PROJECT_FILE_NAME_TEMPLATE, thing.getUID().getId());
560 String filePath = getFilePathInUserDataFolder(fileName);
561 boolean loadProject = false;
563 if (projectFile == null) {
564 // try first load project file from local cache file.
566 projectFile = ProjectFileUtils.readFromFile(filePath);
567 } catch (IhcExecption e) {
568 logger.debug("Error occured when read project file from file '{}', reason {}", filePath,
574 if (!ProjectFileUtils.projectEqualsToControllerProject(projectFile, ihc.getProjectInfo())) {
576 "Local project file is not same as in the controller, reload project file from controller!");
581 logger.debug("Loading IHC /ELKO LS project file from controller...");
582 byte[] data = ihc.getProjectFileFromController();
583 logger.debug("Saving project file to local file '{}'", filePath);
585 ProjectFileUtils.saveToFile(filePath, data);
586 } catch (IhcExecption e) {
587 logger.warn("Error occured when trying to write data to file '{}', reason {}", filePath,
590 projectFile = ProjectFileUtils.converteBytesToDocument(data);
594 enumDictionary = new EnumDictionary(ProjectFileUtils.parseEnums(projectFile));
597 private void createChannels() {
598 if (conf.loadProjectFile && conf.createChannelsAutomatically) {
599 logger.debug("Creating channels");
600 List<Channel> thingChannels = new ArrayList<>();
601 thingChannels.addAll(getThing().getChannels());
602 ChannelUtils.addControllerChannels(getThing(), thingChannels);
603 ChannelUtils.addChannelsFromProjectFile(getThing(), projectFile, thingChannels);
604 printChannels(thingChannels);
605 updateThing(editThing().withChannels(thingChannels).build());
607 logger.debug("Automatic channel creation disabled");
611 private void printChannels(List<Channel> thingChannels) {
612 if (logger.isDebugEnabled()) {
613 thingChannels.forEach(channel -> {
614 if (channel != null) {
617 Object id = channel.getConfiguration().get(PARAM_RESOURCE_ID);
618 resourceId = id != null ? "0x" + Integer.toHexString(((BigDecimal) id).intValue()) : "";
619 } catch (IllegalArgumentException e) {
623 String channelType = channel.getAcceptedItemType() != null ? channel.getAcceptedItemType() : "";
624 String channelLabel = channel.getLabel() != null ? channel.getLabel() : "";
626 logger.debug("Channel: {}", String.format("%-55s | %-10s | %-10s | %s", channel.getUID(),
627 resourceId, channelType, channelLabel));
633 private void startRFPolling() {
634 if (pollingJobRf == null || pollingJobRf.isCancelled()) {
635 logger.debug("Start RF device refresh task, interval={}sec", 60);
636 pollingJobRf = scheduler.scheduleWithFixedDelay(this::updateRfDeviceStates, 10, 60, TimeUnit.SECONDS);
641 * Disconnect connection to IHC / ELKO LS controller.
644 private void disconnect() {
645 cancelAllLongPressTasks();
646 if (pollingJobRf != null && !pollingJobRf.isCancelled()) {
647 pollingJobRf.cancel(true);
652 ihc.removeEventListener(this);
653 ihc.closeConnection();
655 } catch (IhcExecption e) {
656 logger.warn("Couldn't close connection to IHC controller", e);
659 clearLastUpdateTimeCache();
662 private void clearLastUpdateTimeCache() {
667 public void errorOccured(IhcExecption e) {
668 logger.warn("Error occurred on communication to IHC controller: {}", e.getMessage(), e);
669 logger.debug("Reconnection request");
670 setReconnectRequest(true);
674 public void statusUpdateReceived(WSControllerState newState) {
675 logger.debug("Controller state: {}", newState.getState());
677 if (!controllerState.equals(newState.getState())) {
678 logger.debug("Controller state change detected ({} -> {})", controllerState, newState.getState());
680 switch (newState.getState()) {
681 case IhcClient.CONTROLLER_STATE_INITIALIZE:
682 logger.info("Controller state changed to initializing state, waiting for ready state");
683 updateState(new ChannelUID(getThing().getUID(), CHANNEL_CONTROLLER_STATE),
684 new StringType("initialize"));
685 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE,
686 "Controller is in initializing state");
688 case IhcClient.CONTROLLER_STATE_READY:
689 logger.info("Controller state changed to ready state");
690 updateState(new ChannelUID(getThing().getUID(), CHANNEL_CONTROLLER_STATE), new StringType("ready"));
691 updateStatus(ThingStatus.ONLINE);
696 if (controllerState.equals(IhcClient.CONTROLLER_STATE_INITIALIZE)
697 && newState.getState().equals(IhcClient.CONTROLLER_STATE_READY)) {
698 logger.debug("Reconnection request");
700 setReconnectRequest(true);
704 controllerState = newState.getState();
708 public void resourceValueUpdateReceived(WSResourceValue value) {
709 logger.debug("resourceValueUpdateReceived: {}", value);
711 thing.getChannels().forEach(channel -> {
713 ChannelParams params = new ChannelParams(channel);
714 if (params.getResourceId() != null && params.getResourceId().intValue() == value.resourceID) {
715 updateChannelState(channel, params, value);
717 } catch (ConversionException e) {
718 logger.warn("Channel param error, reason: {}.", e.getMessage(), e);
719 } catch (RuntimeException e) {
720 logger.warn("Unknown error occured, reason: {}.", e.getMessage(), e);
724 checkPotentialButtonPresses(value);
727 private void updateChannelState(Channel channel, ChannelParams params, WSResourceValue value) {
728 if (params.isDirectionWriteOnly()) {
729 logger.debug("Write only channel, skip update to {}", channel.getUID());
731 if (params.getChannelTypeId() != null) {
732 switch (params.getChannelTypeId()) {
733 case CHANNEL_TYPE_PUSH_BUTTON_TRIGGER:
738 logger.debug("Update channel '{}' state, channel params: {}", channel.getUID(), params);
739 Converter<WSResourceValue, Type> converter = ConverterFactory.getInstance()
740 .getConverter(value.getClass(), channel.getAcceptedItemType());
741 if (converter != null) {
742 State state = (State) converter.convertFromResourceValue(value,
743 new ConverterAdditionalInfo(null, params.isInverted(),
744 getCommandLevels(params)));
745 updateState(channel.getUID(), state);
747 logger.debug("No converter implemented for {} <-> {}", value.getClass(),
748 channel.getAcceptedItemType());
750 } catch (ConversionException e) {
751 logger.debug("Can't convert resource value '{}' to item type {}, reason: {}.", value,
752 channel.getAcceptedItemType(), e.getMessage(), e);
759 private void checkPotentialButtonPresses(WSResourceValue value) {
760 if (value instanceof WSBooleanValue) {
761 if (((WSBooleanValue) value).value) {
762 // potential button press
763 lastUpdate.put(value.resourceID, LocalDateTime.now());
764 updateTriggers(value.resourceID, Duration.ZERO);
766 // potential button release
767 LocalDateTime lastUpdateTime = lastUpdate.get(value.resourceID);
768 if (lastUpdateTime != null) {
769 Duration duration = Duration.between(lastUpdateTime, LocalDateTime.now());
770 logger.debug("Time between uddates: {}", duration);
771 updateTriggers(value.resourceID, duration);
777 private void updateTriggers(int resourceId, Duration duration) {
778 thing.getChannels().forEach(channel -> {
780 ChannelParams params = new ChannelParams(channel);
781 if (params.getResourceId() != null && params.getResourceId().intValue() == resourceId) {
782 if (params.getChannelTypeId() != null) {
783 switch (params.getChannelTypeId()) {
784 case CHANNEL_TYPE_PUSH_BUTTON_TRIGGER:
785 logger.debug("Update trigger channel '{}', channel params: {}",
786 channel.getUID().getId(), params);
787 if (duration.toMillis() == 0) {
788 triggerChannel(channel.getUID().getId(), EVENT_PRESSED);
789 createLongPressTask(channel.getUID().getId(), params.getLongPressTime());
791 cancelLongPressTask(channel.getUID().getId());
792 triggerChannel(channel.getUID().getId(), EVENT_RELEASED);
793 triggerChannel(channel.getUID().getId(), String.valueOf(duration.toMillis()));
794 ButtonPressDurationDetector button = new ButtonPressDurationDetector(duration,
795 params.getLongPressTime(), MAX_LONG_PRESS_IN_MS);
796 logger.debug("resourceId={}, ButtonPressDurationDetector={}", resourceId, button);
797 if (button.isShortPress()) {
798 triggerChannel(channel.getUID().getId(), EVENT_SHORT_PRESS);
805 } catch (ConversionException e) {
806 logger.warn("Channel param error, reason: {}", e.getMessage(), e);
811 private void createLongPressTask(String channelId, long longPressTimeInMs) {
812 if (longPressFutures.containsKey(channelId)) {
813 cancelLongPressTask(channelId);
815 logger.debug("Create long press task for channel '{}'", channelId);
816 longPressFutures.put(channelId, scheduler.schedule(() -> triggerChannel(channelId, EVENT_LONG_PRESS),
817 longPressTimeInMs, TimeUnit.MILLISECONDS));
820 private void cancelLongPressTask(String channelId) {
821 if (longPressFutures.containsKey(channelId)) {
822 logger.debug("Cancel long press task for channel '{}'", channelId);
823 longPressFutures.get(channelId).cancel(false);
824 longPressFutures.remove(channelId);
828 private void cancelAllLongPressTasks() {
829 longPressFutures.entrySet().parallelStream().forEach(e -> e.getValue().cancel(true));
830 longPressFutures.clear();
833 private void updateRfDeviceStates() {
835 if (ihc.getConnectionState() != ConnectionState.CONNECTED) {
836 logger.debug("Controller is connecting, abort subscribe");
840 logger.debug("Update RF device data");
842 List<WSRFDevice> devs = ihc.getDetectedRFDevices();
843 logger.debug("RF data: {}", devs);
845 devs.forEach(dev -> {
846 thing.getChannels().forEach(channel -> {
848 ChannelParams params = new ChannelParams(channel);
849 if (params.getSerialNumber() != null
850 && params.getSerialNumber().longValue() == dev.getSerialNumber()) {
851 String channelId = channel.getUID().getId();
852 if (params.getChannelTypeId() != null) {
853 switch (params.getChannelTypeId()) {
854 case CHANNEL_TYPE_RF_LOW_BATTERY:
855 updateState(channelId,
856 dev.getBatteryLevel() == 1 ? OnOffType.OFF : OnOffType.ON);
858 case CHANNEL_TYPE_RF_SIGNAL_STRENGTH:
859 int signalLevel = new SignalLevelConverter(dev.getSignalStrength())
860 .getSystemWideSignalLevel();
861 updateState(channelId, new StringType(String.valueOf(signalLevel)));
866 } catch (ConversionException e) {
867 logger.warn("Channel param error, reason: {}", e.getMessage(), e);
871 } catch (IhcExecption e) {
872 logger.debug("Error occured when fetching RF device information, reason: : {} ", e.getMessage(), e);
878 private void reconnectCheck() {
879 if (ihc == null || isReconnectRequestActivated()) {
885 setReconnectRequest(false);
886 } catch (IhcExecption e) {
887 logger.debug("Can't open connection to controller {}", e.getMessage());
888 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
889 setReconnectRequest(true);
894 if (isValueNotificationRequestActivated() && !isConnecting()) {
896 enableResourceValueNotifications();
897 } catch (IhcExecption e) {
898 logger.warn("Can't enable resource value notifications from controller. ", e);
903 private Set<Integer> getAllLinkedChannelsResourceIds() {
904 Set<Integer> resourceIds = Collections.synchronizedSet(new HashSet<>());
905 resourceIds.addAll(this.getThing().getChannels().stream().filter(c -> isLinked(c.getUID())).map(c -> {
907 ChannelParams params = new ChannelParams(c);
908 logger.debug("Linked channel '{}' found, resource id '{}'", c.getUID().getAsString(),
909 params.getResourceId());
910 return params.getResourceId();
911 } catch (ConversionException e) {
912 logger.warn("Channel param error, reason: {}.", e.getMessage(), e);
915 }).filter(c -> c != null && c != 0).collect(Collectors.toSet()));
920 * Order resource value notifications from IHC controller.
922 private void enableResourceValueNotifications() throws IhcExecption {
923 logger.debug("Subscribe resource runtime value notifications");
926 if (ihc.getConnectionState() != ConnectionState.CONNECTED) {
927 logger.debug("Controller is connecting, abort subscribe");
930 setValueNotificationRequest(false);
931 Set<Integer> resourceIds = ChannelUtils.getAllTriggerChannelsResourceIds(getThing());
932 logger.debug("Enable runtime notfications for {} trigger(s)", resourceIds.size());
933 logger.debug("Enable runtime notfications for {} channel(s)", linkedResourceIds.size());
934 resourceIds.addAll(linkedResourceIds);
935 resourceIds.addAll(getAllLinkedChannelsResourceIds());
936 logger.debug("Enable runtime notfications for {} resources: {}", resourceIds.size(), resourceIds);
937 if (!resourceIds.isEmpty()) {
939 ihc.enableRuntimeValueNotifications(resourceIds);
940 } catch (IhcExecption e) {
941 logger.debug("Reconnection request");
942 setReconnectRequest(true);
946 logger.warn("Controller is not initialized!");
947 logger.debug("Reconnection request");
948 setReconnectRequest(true);
952 private synchronized void updateNotificationsRequestReminder() {
953 if (notificationsRequestReminder != null) {
954 notificationsRequestReminder.cancel(false);
957 logger.debug("Rechedule resource runtime value notifications order by {}ms", NOTIFICATIONS_REORDER_WAIT_TIME);
958 notificationsRequestReminder = scheduler.schedule(new Runnable() {
962 logger.debug("Delayed resource value notifications request is now enabled");
963 setValueNotificationRequest(true);
965 }, NOTIFICATIONS_REORDER_WAIT_TIME, TimeUnit.MILLISECONDS);
968 private Map<Command, Object> getCommandLevels(ChannelParams params) {
969 if (params.getOnLevel() != null) {
970 Map<Command, Object> commandLevels = new HashMap<>();
971 commandLevels.put(OnOffType.ON, params.getOnLevel());
972 return Collections.unmodifiableMap(commandLevels);