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.library.types.DateTimeType;
59 import org.openhab.core.library.types.DecimalType;
60 import org.openhab.core.library.types.OnOffType;
61 import org.openhab.core.library.types.StringType;
62 import org.openhab.core.thing.Channel;
63 import org.openhab.core.thing.ChannelUID;
64 import org.openhab.core.thing.Thing;
65 import org.openhab.core.thing.ThingStatus;
66 import org.openhab.core.thing.ThingStatusDetail;
67 import org.openhab.core.thing.binding.BaseThingHandler;
68 import org.openhab.core.types.Command;
69 import org.openhab.core.types.RefreshType;
70 import org.openhab.core.types.State;
71 import org.openhab.core.types.Type;
72 import org.slf4j.Logger;
73 import org.slf4j.LoggerFactory;
74 import org.w3c.dom.Document;
77 * The {@link IhcHandler} is responsible for handling commands, which are
78 * sent to one of the channels.
80 * @author Pauli Anttila - Initial contribution
82 public class IhcHandler extends BaseThingHandler implements IhcEventListener {
83 private final Logger logger = LoggerFactory.getLogger(IhcHandler.class);
85 /** Maximum pulse width in milliseconds. */
86 private static final int MAX_PULSE_WIDTH_IN_MS = 4000;
88 /** Maximum long press time in milliseconds. */
89 private static final int MAX_LONG_PRESS_IN_MS = 5000;
91 /** Name of the local IHC / ELKO project file */
92 private static final String LOCAL_IHC_PROJECT_FILE_NAME_TEMPLATE = "ihc-project-file-%s.xml";
94 /** Holds runtime notification reorder timeout in milliseconds */
95 private static final int NOTIFICATIONS_REORDER_WAIT_TIME = 1000;
97 /** IHC / ELKO LS Controller client */
98 private IhcClient ihc;
101 * Reminder to slow down resource value notification ordering from
104 private ScheduledFuture<?> notificationsRequestReminder;
106 /** Holds local IHC / ELKO project file */
107 private Document projectFile;
110 * Store current state of the controller, use to recognize when controller
113 private String controllerState = "";
115 private IhcConfiguration conf;
116 private final Set<Integer> linkedResourceIds = Collections.synchronizedSet(new HashSet<>());
117 private Map<Integer, LocalDateTime> lastUpdate = new HashMap<>();
118 private EnumDictionary enumDictionary;
120 private boolean connecting = false;
121 private boolean reconnectRequest = false;
122 private boolean valueNotificationRequest = false;
124 private ScheduledFuture<?> controlJob;
125 private ScheduledFuture<?> pollingJobRf;
127 private Map<String, ScheduledFuture<?>> longPressFutures = new HashMap<>();
129 public IhcHandler(Thing thing) {
133 protected boolean isValueNotificationRequestActivated() {
134 synchronized (this) {
135 return valueNotificationRequest;
139 protected void setValueNotificationRequest(boolean valueNotificationRequest) {
140 synchronized (this) {
141 this.valueNotificationRequest = valueNotificationRequest;
145 protected boolean isReconnectRequestActivated() {
146 synchronized (this) {
147 return reconnectRequest;
151 protected void setReconnectRequest(boolean reconnect) {
152 synchronized (this) {
153 this.reconnectRequest = reconnect;
157 protected boolean isConnecting() {
158 synchronized (this) {
163 protected void setConnectingState(boolean value) {
164 synchronized (this) {
165 this.connecting = value;
169 private String getFilePathInUserDataFolder(String fileName) {
170 String progArg = System.getProperty("smarthome.userdata");
171 if (progArg != null) {
172 return progArg + File.separator + fileName;
178 public void initialize() {
179 conf = getConfigAs(IhcConfiguration.class);
180 logger.debug("Using configuration: {}", conf);
182 linkedResourceIds.clear();
183 linkedResourceIds.addAll(getAllLinkedChannelsResourceIds());
184 logger.debug("Linked resources {}: {}", linkedResourceIds.size(), linkedResourceIds);
186 if (controlJob == null || controlJob.isCancelled()) {
187 logger.debug("Start control task, interval={}sec", 1);
188 controlJob = scheduler.scheduleWithFixedDelay(this::reconnectCheck, 0, 1, TimeUnit.SECONDS);
193 public void dispose() {
194 logger.debug("Stopping thing");
195 if (controlJob != null && !controlJob.isCancelled()) {
196 controlJob.cancel(true);
203 public void handleCommand(ChannelUID channelUID, Command command) {
204 logger.debug("Received channel: {}, command: {}", channelUID, command);
207 logger.warn("Connection is not initialized, abort resource value update for channel '{}'!", channelUID);
211 if (ihc.getConnectionState() != ConnectionState.CONNECTED) {
212 logger.warn("Connection to controller is not open, abort resource value update for channel '{}'!",
217 switch (channelUID.getId()) {
218 case CHANNEL_CONTROLLER_STATE:
219 if (command.equals(RefreshType.REFRESH)) {
220 updateControllerStateChannel();
224 case CHANNEL_CONTROLLER_UPTIME:
225 if (command.equals(RefreshType.REFRESH)) {
226 updateControllerInformationChannels();
230 case CHANNEL_CONTROLLER_TIME:
231 if (command.equals(RefreshType.REFRESH)) {
232 updateControllerTimeChannels();
237 if (command.equals(RefreshType.REFRESH)) {
238 refreshChannel(channelUID);
240 updateResourceChannel(channelUID, command);
246 private void refreshChannel(ChannelUID channelUID) {
247 logger.debug("REFRESH channel {}", channelUID);
248 Channel channel = thing.getChannel(channelUID.getId());
249 if (channel != null) {
251 ChannelParams params = new ChannelParams(channel);
252 logger.debug("Channel params: {}", params);
253 if (params.isDirectionWriteOnly()) {
254 logger.warn("Write only channel, skip refresh command to {}", channelUID);
257 WSResourceValue value = ihc.resourceQuery(params.getResourceId());
258 resourceValueUpdateReceived(value);
259 } catch (IhcExecption e) {
260 logger.warn("Can't update channel '{}' value, reason: {}", channelUID, e.getMessage(), e);
261 } catch (ConversionException e) {
262 logger.warn("Channel param error, reason: {}.", e.getMessage(), e);
267 private void updateControllerStateChannel() {
269 String state = ihc.getControllerState().getState();
273 case IhcClient.CONTROLLER_STATE_INITIALIZE:
274 value = "Initialize";
276 case IhcClient.CONTROLLER_STATE_READY:
280 value = "Unknown state: " + state;
283 updateState(new ChannelUID(getThing().getUID(), CHANNEL_CONTROLLER_STATE), new StringType(value));
284 } catch (IhcExecption e) {
285 logger.warn("Controller state information fetch failed, reason: {}", e.getMessage(), e);
289 private void updateControllerProperties() {
291 WSSystemInfo systemInfo = ihc.getSystemInfo();
292 logger.debug("Controller information: {}", systemInfo);
293 WSProjectInfo projectInfo = ihc.getProjectInfo();
294 logger.debug("Project information: {}", projectInfo);
296 Map<String, String> properties = editProperties();
297 properties.put(PROPERTY_MANUFACTURER, systemInfo.getBrand());
298 properties.put(PROPERTY_SERIALNUMBER, systemInfo.getSerialNumber());
299 properties.put(PROPERTY_SW_VERSION, systemInfo.getVersion());
300 properties.put(PROPERTY_FW_VERSION, systemInfo.getHwRevision());
301 properties.put(PROPERTY_APP_WITHOUT_VIEWER, Boolean.toString(systemInfo.getApplicationIsWithoutViewer()));
302 properties.put(PROPERTY_SW_DATE,
303 systemInfo.getSwDate().withZoneSameInstant(ZoneId.systemDefault()).toString());
304 properties.put(PROPERTY_PRODUCTION_DATE, systemInfo.getProductionDate());
305 if (!systemInfo.getDatalineVersion().isEmpty()) {
306 properties.put(PROPERTY_DATALINE_VERSION, systemInfo.getDatalineVersion());
308 if (!systemInfo.getRfModuleSerialNumber().isEmpty()) {
309 properties.put(PROPERTY_RF_MODULE_SERIALNUMBER, systemInfo.getRfModuleSerialNumber());
311 if (!systemInfo.getRfModuleSoftwareVersion().isEmpty()) {
312 properties.put(PROPERTY_RF_MODULE_VERSION, systemInfo.getRfModuleSoftwareVersion());
314 properties.put(PROPERTY_PROJECT_DATE,
315 projectInfo.getLastmodified().getAsLocalDateTime().atZone(ZoneId.systemDefault()).toString());
316 properties.put(PROPERTY_PROJECT_NUMBER, projectInfo.getProjectNumber());
317 updateProperties(properties);
318 } catch (IhcExecption e) {
319 logger.warn("Controller information fetch failed, reason: {}", e.getMessage(), e);
323 private void updateControllerInformationChannels() {
325 WSSystemInfo systemInfo = ihc.getSystemInfo();
326 logger.debug("Controller information: {}", systemInfo);
328 updateState(new ChannelUID(getThing().getUID(), CHANNEL_CONTROLLER_UPTIME),
329 new DecimalType((double) systemInfo.getUptime() / 1000));
330 } catch (IhcExecption e) {
331 logger.warn("Controller uptime information fetch failed, reason: {}.", e.getMessage(), e);
335 private void updateControllerTimeChannels() {
337 WSTimeManagerSettings timeSettings = ihc.getTimeSettings();
338 logger.debug("Controller time settings: {}", timeSettings);
340 ZonedDateTime time = timeSettings.getTimeAndDateInUTC().getAsZonedDateTime(ZoneId.of("Z"))
341 .withZoneSameInstant(ZoneId.systemDefault());
342 updateState(new ChannelUID(getThing().getUID(), CHANNEL_CONTROLLER_TIME), new DateTimeType(time));
343 } catch (IhcExecption e) {
344 logger.warn("Controller uptime information fetch failed, reason: {}.", e.getMessage(), e);
348 private void updateResourceChannel(ChannelUID channelUID, Command command) {
349 Channel channel = thing.getChannel(channelUID.getId());
350 if (channel != null) {
352 ChannelParams params = new ChannelParams(channel);
353 logger.debug("Channel params: {}", params);
354 if (params.isDirectionReadOnly()) {
355 logger.debug("Read only channel, skip the update to {}", channelUID);
358 updateChannel(channelUID, params, command);
359 } catch (IhcExecption e) {
360 logger.warn("Can't update channel '{}' value, cause {}", channelUID, e.getMessage());
361 } catch (ConversionException e) {
362 logger.debug("Conversion error for channel {}, reason: {}", channelUID, e.getMessage());
367 private void updateChannel(ChannelUID channelUID, ChannelParams params, Command command)
368 throws IhcExecption, ConversionException {
369 if (params.getCommandToReact() != null) {
370 if (command.toString().equals(params.getCommandToReact())) {
371 logger.debug("Command '{}' equal to channel reaction parameter '{}', execute it", command,
372 params.getCommandToReact());
374 logger.debug("Command '{}' doesn't equal to reaction trigger parameter '{}', skip it", command,
375 params.getCommandToReact());
379 WSResourceValue value = ihc.getResourceValueInformation(params.getResourceId());
381 if (params.getPulseWidth() != null) {
382 sendPulseCommand(channelUID, params, value, Math.min(params.getPulseWidth(), MAX_PULSE_WIDTH_IN_MS));
384 sendNormalCommand(channelUID, params, command, value);
389 private void sendNormalCommand(ChannelUID channelUID, ChannelParams params, Command command, WSResourceValue value)
390 throws IhcExecption, ConversionException {
391 logger.debug("Send command '{}' to resource '{}'", command, value.resourceID);
392 ConverterAdditionalInfo converterAdditionalInfo = new ConverterAdditionalInfo(getEnumValues(value),
393 params.isInverted(), getCommandLevels(params));
394 Converter<WSResourceValue, Type> converter = ConverterFactory.getInstance().getConverter(value.getClass(),
396 if (converter != null) {
397 WSResourceValue val = converter.convertFromOHType(command, value, converterAdditionalInfo);
398 logger.debug("Update resource value (inverted output={}): {}", params.isInverted(), val);
399 if (!updateResource(val)) {
400 logger.warn("Channel {} update to resource '{}' failed.", channelUID, val);
403 logger.debug("No converter implemented for {} <-> {}", value.getClass(), command.getClass());
407 private List<IhcEnumValue> getEnumValues(WSResourceValue value) {
408 if (value instanceof WSEnumValue) {
409 return enumDictionary.getEnumValues(((WSEnumValue) value).definitionTypeID);
414 private void sendPulseCommand(ChannelUID channelUID, ChannelParams params, WSResourceValue value,
415 Integer pulseWidth) throws IhcExecption, ConversionException {
416 logger.debug("Send {}ms pulse to resource: {}", pulseWidth, value.resourceID);
417 logger.debug("Channel params: {}", params);
418 Converter<WSResourceValue, Type> converter = ConverterFactory.getInstance().getConverter(value.getClass(),
421 if (converter != null) {
422 ConverterAdditionalInfo converterAdditionalInfo = new ConverterAdditionalInfo(null, params.isInverted(),
423 getCommandLevels(params));
425 WSResourceValue valOn = converter.convertFromOHType(OnOffType.ON, value, converterAdditionalInfo);
426 WSResourceValue valOff = converter.convertFromOHType(OnOffType.OFF, value, converterAdditionalInfo);
428 // set resource to ON
429 logger.debug("Update resource value (inverted output={}): {}", params.isInverted(), valOn);
430 if (updateResource(valOn)) {
431 logger.debug("Sleeping: {}ms", pulseWidth);
432 scheduler.schedule(new Runnable() {
435 // set resource back to OFF
436 logger.debug("Update resource value (inverted output={}): {}", params.isInverted(), valOff);
438 if (!updateResource(valOff)) {
439 logger.warn("Channel {} update to resource '{}' failed.", channelUID, valOff);
441 } catch (IhcExecption e) {
442 logger.warn("Can't update channel '{}' value, cause {}", channelUID, e.getMessage());
445 }, pulseWidth, TimeUnit.MILLISECONDS);
447 logger.warn("Channel {} update failed.", channelUID);
450 logger.debug("No converter implemented for {} <-> {}", value.getClass(), OnOffType.class);
455 * Update resource value to IHC controller.
457 private boolean updateResource(WSResourceValue value) throws IhcExecption {
458 boolean result = false;
460 result = ihc.resourceUpdate(value);
461 } catch (IhcExecption e) {
462 logger.warn("Value could not be updated - retrying one time: {}.", e.getMessage(), e);
463 result = ihc.resourceUpdate(value);
469 public void channelLinked(ChannelUID channelUID) {
470 logger.debug("channelLinked: {}", channelUID);
472 switch (channelUID.getId()) {
473 case CHANNEL_CONTROLLER_STATE:
474 updateControllerStateChannel();
477 case CHANNEL_CONTROLLER_UPTIME:
478 updateControllerInformationChannels();
481 case CHANNEL_CONTROLLER_TIME:
482 updateControllerTimeChannels();
486 Channel channel = thing.getChannel(channelUID.getId());
487 if (channel != null) {
489 ChannelParams params = new ChannelParams(channel);
490 if (params.getResourceId() != null) {
491 if (!linkedResourceIds.contains(params.getResourceId())) {
492 logger.debug("New channel '{}' found, resource id '{}'", channelUID.getAsString(),
493 params.getResourceId());
494 linkedResourceIds.add(params.getResourceId());
495 updateNotificationsRequestReminder();
498 } catch (ConversionException e) {
499 logger.warn("Channel param error, reason: {}.", e.getMessage(), e);
506 public void channelUnlinked(ChannelUID channelUID) {
507 logger.debug("channelUnlinked: {}", channelUID);
509 switch (channelUID.getId()) {
510 case CHANNEL_CONTROLLER_STATE:
511 case CHANNEL_CONTROLLER_UPTIME:
512 case CHANNEL_CONTROLLER_TIME:
516 Channel channel = thing.getChannel(channelUID.getId());
517 if (channel != null) {
519 ChannelParams params = new ChannelParams(channel);
520 if (params.getResourceId() != null) {
521 linkedResourceIds.removeIf(c -> c.equals(params.getResourceId()));
522 updateNotificationsRequestReminder();
524 } catch (ConversionException e) {
525 logger.warn("Channel param error, reason: {}.", e.getMessage(), e);
532 * Initialize IHC client and open connection to IHC / ELKO LS controller.
535 private void connect() throws IhcExecption {
537 setConnectingState(true);
538 logger.debug("Connecting to IHC / ELKO LS controller [hostname='{}', username='{}'].", conf.hostname,
540 ihc = new IhcClient(conf.hostname, conf.username, conf.password, conf.timeout);
541 ihc.openConnection();
542 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE,
543 "Initializing communication to the IHC / ELKO controller");
546 updateControllerProperties();
547 updateControllerStateChannel();
548 updateControllerInformationChannels();
549 updateControllerTimeChannels();
550 ihc.addEventListener(this);
551 ihc.startControllerEventListeners();
552 updateNotificationsRequestReminder();
554 updateStatus(ThingStatus.ONLINE);
556 setConnectingState(false);
560 private void loadProject() throws IhcExecption {
561 if (conf.loadProjectFile) {
562 String fileName = String.format(LOCAL_IHC_PROJECT_FILE_NAME_TEMPLATE, thing.getUID().getId());
563 String filePath = getFilePathInUserDataFolder(fileName);
564 boolean loadProject = false;
566 if (projectFile == null) {
567 // try first load project file from local cache file.
569 projectFile = ProjectFileUtils.readFromFile(filePath);
570 } catch (IhcExecption e) {
571 logger.debug("Error occured when read project file from file '{}', reason {}", filePath,
577 if (!ProjectFileUtils.projectEqualsToControllerProject(projectFile, ihc.getProjectInfo())) {
579 "Local project file is not same as in the controller, reload project file from controller!");
584 logger.debug("Loading IHC /ELKO LS project file from controller...");
585 byte[] data = ihc.getProjectFileFromController();
586 logger.debug("Saving project file to local file '{}'", filePath);
588 ProjectFileUtils.saveToFile(filePath, data);
589 } catch (IhcExecption e) {
590 logger.warn("Error occured when trying to write data to file '{}', reason {}", filePath,
593 projectFile = ProjectFileUtils.converteBytesToDocument(data);
597 enumDictionary = new EnumDictionary(ProjectFileUtils.parseEnums(projectFile));
600 private void createChannels() {
601 if (conf.loadProjectFile && conf.createChannelsAutomatically) {
602 logger.debug("Creating channels");
603 List<Channel> thingChannels = new ArrayList<>();
604 thingChannels.addAll(getThing().getChannels());
605 ChannelUtils.addControllerChannels(getThing(), thingChannels);
606 ChannelUtils.addChannelsFromProjectFile(getThing(), projectFile, thingChannels);
607 printChannels(thingChannels);
608 updateThing(editThing().withChannels(thingChannels).build());
610 logger.debug("Automatic channel creation disabled");
614 private void printChannels(List<Channel> thingChannels) {
615 if (logger.isDebugEnabled()) {
616 thingChannels.forEach(channel -> {
617 if (channel != null) {
620 Object id = channel.getConfiguration().get(PARAM_RESOURCE_ID);
621 resourceId = id != null ? "0x" + Integer.toHexString(((BigDecimal) id).intValue()) : "";
622 } catch (IllegalArgumentException e) {
626 String channelType = channel.getAcceptedItemType() != null ? channel.getAcceptedItemType() : "";
627 String channelLabel = channel.getLabel() != null ? channel.getLabel() : "";
629 logger.debug("Channel: {}", String.format("%-55s | %-10s | %-10s | %s", channel.getUID(),
630 resourceId, channelType, channelLabel));
636 private void startRFPolling() {
637 if (pollingJobRf == null || pollingJobRf.isCancelled()) {
638 logger.debug("Start RF device refresh task, interval={}sec", 60);
639 pollingJobRf = scheduler.scheduleWithFixedDelay(this::updateRfDeviceStates, 10, 60, TimeUnit.SECONDS);
644 * Disconnect connection to IHC / ELKO LS controller.
647 private void disconnect() {
648 cancelAllLongPressTasks();
649 if (pollingJobRf != null && !pollingJobRf.isCancelled()) {
650 pollingJobRf.cancel(true);
655 ihc.removeEventListener(this);
656 ihc.closeConnection();
658 } catch (IhcExecption e) {
659 logger.warn("Couldn't close connection to IHC controller", e);
662 clearLastUpdateTimeCache();
665 private void clearLastUpdateTimeCache() {
670 public void errorOccured(IhcExecption e) {
671 logger.warn("Error occurred on communication to IHC controller: {}", e.getMessage(), e);
672 logger.debug("Reconnection request");
673 setReconnectRequest(true);
677 public void statusUpdateReceived(WSControllerState newState) {
678 logger.debug("Controller state: {}", newState.getState());
680 if (!controllerState.equals(newState.getState())) {
681 logger.debug("Controller state change detected ({} -> {})", controllerState, newState.getState());
683 switch (newState.getState()) {
684 case IhcClient.CONTROLLER_STATE_INITIALIZE:
685 logger.info("Controller state changed to initializing state, waiting for ready state");
686 updateState(new ChannelUID(getThing().getUID(), CHANNEL_CONTROLLER_STATE),
687 new StringType("initialize"));
688 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE,
689 "Controller is in initializing state");
691 case IhcClient.CONTROLLER_STATE_READY:
692 logger.info("Controller state changed to ready state");
693 updateState(new ChannelUID(getThing().getUID(), CHANNEL_CONTROLLER_STATE), new StringType("ready"));
694 updateStatus(ThingStatus.ONLINE);
699 if (controllerState.equals(IhcClient.CONTROLLER_STATE_INITIALIZE)
700 && newState.getState().equals(IhcClient.CONTROLLER_STATE_READY)) {
701 logger.debug("Reconnection request");
703 setReconnectRequest(true);
707 controllerState = newState.getState();
711 public void resourceValueUpdateReceived(WSResourceValue value) {
712 logger.debug("resourceValueUpdateReceived: {}", value);
714 thing.getChannels().forEach(channel -> {
716 ChannelParams params = new ChannelParams(channel);
717 if (params.getResourceId() != null && params.getResourceId().intValue() == value.resourceID) {
718 updateChannelState(channel, params, value);
720 } catch (ConversionException e) {
721 logger.warn("Channel param error, reason: {}.", e.getMessage(), e);
722 } catch (RuntimeException e) {
723 logger.warn("Unknown error occured, reason: {}.", e.getMessage(), e);
727 checkPotentialButtonPresses(value);
730 private void updateChannelState(Channel channel, ChannelParams params, WSResourceValue value) {
731 if (params.isDirectionWriteOnly()) {
732 logger.debug("Write only channel, skip update to {}", channel.getUID());
734 if (params.getChannelTypeId() != null) {
735 switch (params.getChannelTypeId()) {
736 case CHANNEL_TYPE_PUSH_BUTTON_TRIGGER:
741 logger.debug("Update channel '{}' state, channel params: {}", channel.getUID(), params);
742 Converter<WSResourceValue, Type> converter = ConverterFactory.getInstance()
743 .getConverter(value.getClass(), channel.getAcceptedItemType());
744 if (converter != null) {
745 State state = (State) converter.convertFromResourceValue(value,
746 new ConverterAdditionalInfo(null, params.isInverted(),
747 getCommandLevels(params)));
748 updateState(channel.getUID(), state);
750 logger.debug("No converter implemented for {} <-> {}", value.getClass(),
751 channel.getAcceptedItemType());
753 } catch (ConversionException e) {
754 logger.debug("Can't convert resource value '{}' to item type {}, reason: {}.", value,
755 channel.getAcceptedItemType(), e.getMessage(), e);
762 private void checkPotentialButtonPresses(WSResourceValue value) {
763 if (value instanceof WSBooleanValue) {
764 if (((WSBooleanValue) value).value) {
765 // potential button press
766 lastUpdate.put(value.resourceID, LocalDateTime.now());
767 updateTriggers(value.resourceID, Duration.ZERO);
769 // potential button release
770 LocalDateTime lastUpdateTime = lastUpdate.get(value.resourceID);
771 if (lastUpdateTime != null) {
772 Duration duration = Duration.between(lastUpdateTime, LocalDateTime.now());
773 logger.debug("Time between uddates: {}", duration);
774 updateTriggers(value.resourceID, duration);
780 private void updateTriggers(int resourceId, Duration duration) {
781 thing.getChannels().forEach(channel -> {
783 ChannelParams params = new ChannelParams(channel);
784 if (params.getResourceId() != null && params.getResourceId().intValue() == resourceId) {
785 if (params.getChannelTypeId() != null) {
786 switch (params.getChannelTypeId()) {
787 case CHANNEL_TYPE_PUSH_BUTTON_TRIGGER:
788 logger.debug("Update trigger channel '{}', channel params: {}",
789 channel.getUID().getId(), params);
790 if (duration.toMillis() == 0) {
791 triggerChannel(channel.getUID().getId(), EVENT_PRESSED);
792 createLongPressTask(channel.getUID().getId(), params.getLongPressTime());
794 cancelLongPressTask(channel.getUID().getId());
795 triggerChannel(channel.getUID().getId(), EVENT_RELEASED);
796 triggerChannel(channel.getUID().getId(), String.valueOf(duration.toMillis()));
797 ButtonPressDurationDetector button = new ButtonPressDurationDetector(duration,
798 params.getLongPressTime(), MAX_LONG_PRESS_IN_MS);
799 logger.debug("resourceId={}, ButtonPressDurationDetector={}", resourceId, button);
800 if (button.isShortPress()) {
801 triggerChannel(channel.getUID().getId(), EVENT_SHORT_PRESS);
808 } catch (ConversionException e) {
809 logger.warn("Channel param error, reason: {}", e.getMessage(), e);
814 private void createLongPressTask(String channelId, long longPressTimeInMs) {
815 if (longPressFutures.containsKey(channelId)) {
816 cancelLongPressTask(channelId);
818 logger.debug("Create long press task for channel '{}'", channelId);
819 longPressFutures.put(channelId, scheduler.schedule(() -> triggerChannel(channelId, EVENT_LONG_PRESS),
820 longPressTimeInMs, TimeUnit.MILLISECONDS));
823 private void cancelLongPressTask(String channelId) {
824 if (longPressFutures.containsKey(channelId)) {
825 logger.debug("Cancel long press task for channel '{}'", channelId);
826 longPressFutures.get(channelId).cancel(false);
827 longPressFutures.remove(channelId);
831 private void cancelAllLongPressTasks() {
832 longPressFutures.entrySet().parallelStream().forEach(e -> e.getValue().cancel(true));
833 longPressFutures.clear();
836 private void updateRfDeviceStates() {
838 if (ihc.getConnectionState() != ConnectionState.CONNECTED) {
839 logger.debug("Controller is connecting, abort subscribe");
843 logger.debug("Update RF device data");
845 List<WSRFDevice> devs = ihc.getDetectedRFDevices();
846 logger.debug("RF data: {}", devs);
848 devs.forEach(dev -> {
849 thing.getChannels().forEach(channel -> {
851 ChannelParams params = new ChannelParams(channel);
852 if (params.getSerialNumber() != null
853 && params.getSerialNumber().longValue() == dev.getSerialNumber()) {
854 String channelId = channel.getUID().getId();
855 if (params.getChannelTypeId() != null) {
856 switch (params.getChannelTypeId()) {
857 case CHANNEL_TYPE_RF_LOW_BATTERY:
858 updateState(channelId,
859 dev.getBatteryLevel() == 1 ? OnOffType.OFF : OnOffType.ON);
861 case CHANNEL_TYPE_RF_SIGNAL_STRENGTH:
862 int signalLevel = new SignalLevelConverter(dev.getSignalStrength())
863 .getSystemWideSignalLevel();
864 updateState(channelId, new StringType(String.valueOf(signalLevel)));
869 } catch (ConversionException e) {
870 logger.warn("Channel param error, reason: {}", e.getMessage(), e);
874 } catch (IhcExecption e) {
875 logger.debug("Error occured when fetching RF device information, reason: : {} ", e.getMessage(), e);
881 private void reconnectCheck() {
882 if (ihc == null || isReconnectRequestActivated()) {
888 setReconnectRequest(false);
889 } catch (IhcExecption e) {
890 logger.debug("Can't open connection to controller {}", e.getMessage());
891 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
892 setReconnectRequest(true);
897 if (isValueNotificationRequestActivated() && !isConnecting()) {
899 enableResourceValueNotifications();
900 } catch (IhcExecption e) {
901 logger.warn("Can't enable resource value notifications from controller. ", e);
906 private Set<Integer> getAllLinkedChannelsResourceIds() {
907 Set<Integer> resourceIds = Collections.synchronizedSet(new HashSet<>());
908 resourceIds.addAll(this.getThing().getChannels().stream().filter(c -> isLinked(c.getUID())).map(c -> {
910 ChannelParams params = new ChannelParams(c);
911 logger.debug("Linked channel '{}' found, resource id '{}'", c.getUID().getAsString(),
912 params.getResourceId());
913 return params.getResourceId();
914 } catch (ConversionException e) {
915 logger.warn("Channel param error, reason: {}.", e.getMessage(), e);
918 }).filter(c -> c != null && c != 0).collect(Collectors.toSet()));
923 * Order resource value notifications from IHC controller.
925 private void enableResourceValueNotifications() throws IhcExecption {
926 logger.debug("Subscribe resource runtime value notifications");
929 if (ihc.getConnectionState() != ConnectionState.CONNECTED) {
930 logger.debug("Controller is connecting, abort subscribe");
933 setValueNotificationRequest(false);
934 Set<Integer> resourceIds = ChannelUtils.getAllTriggerChannelsResourceIds(getThing());
935 logger.debug("Enable runtime notfications for {} trigger(s)", resourceIds.size());
936 logger.debug("Enable runtime notfications for {} channel(s)", linkedResourceIds.size());
937 resourceIds.addAll(linkedResourceIds);
938 resourceIds.addAll(getAllLinkedChannelsResourceIds());
939 logger.debug("Enable runtime notfications for {} resources: {}", resourceIds.size(), resourceIds);
940 if (!resourceIds.isEmpty()) {
942 ihc.enableRuntimeValueNotifications(resourceIds);
943 } catch (IhcExecption e) {
944 logger.debug("Reconnection request");
945 setReconnectRequest(true);
949 logger.warn("Controller is not initialized!");
950 logger.debug("Reconnection request");
951 setReconnectRequest(true);
955 private synchronized void updateNotificationsRequestReminder() {
956 if (notificationsRequestReminder != null) {
957 notificationsRequestReminder.cancel(false);
960 logger.debug("Rechedule resource runtime value notifications order by {}ms", NOTIFICATIONS_REORDER_WAIT_TIME);
961 notificationsRequestReminder = scheduler.schedule(new Runnable() {
965 logger.debug("Delayed resource value notifications request is now enabled");
966 setValueNotificationRequest(true);
968 }, NOTIFICATIONS_REORDER_WAIT_TIME, TimeUnit.MILLISECONDS);
971 private Map<Command, Object> getCommandLevels(ChannelParams params) {
972 if (params.getOnLevel() != null) {
973 Map<Command, Object> commandLevels = new HashMap<>();
974 commandLevels.put(OnOffType.ON, params.getOnLevel());
975 return Collections.unmodifiableMap(commandLevels);