]> git.basschouten.com Git - openhab-addons.git/blob
6fb5b936afbcb6f84300fe9b941ff5cebe2d6af1
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.ihc.internal.handler;
14
15 import static org.openhab.binding.ihc.internal.IhcBindingConstants.*;
16
17 import java.io.File;
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;
28 import java.util.Map;
29 import java.util.Set;
30 import java.util.concurrent.ScheduledFuture;
31 import java.util.concurrent.TimeUnit;
32 import java.util.stream.Collectors;
33
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;
77
78 /**
79  * The {@link IhcHandler} is responsible for handling commands, which are
80  * sent to one of the channels.
81  *
82  * @author Pauli Anttila - Initial contribution
83  */
84 public class IhcHandler extends BaseThingHandler implements IhcEventListener {
85     private final Logger logger = LoggerFactory.getLogger(IhcHandler.class);
86
87     /** Maximum pulse width in milliseconds. */
88     private static final int MAX_PULSE_WIDTH_IN_MS = 4000;
89
90     /** Maximum long press time in milliseconds. */
91     private static final int MAX_LONG_PRESS_IN_MS = 5000;
92
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";
95
96     /** Holds runtime notification reorder timeout in milliseconds */
97     private static final int NOTIFICATIONS_REORDER_WAIT_TIME = 1000;
98
99     /** IHC / ELKO LS Controller client */
100     private IhcClient ihc;
101
102     /**
103      * Reminder to slow down resource value notification ordering from
104      * controller.
105      */
106     private ScheduledFuture<?> notificationsRequestReminder;
107
108     /** Holds local IHC / ELKO project file */
109     private Document projectFile;
110
111     /**
112      * Store current state of the controller, use to recognize when controller
113      * state is changed
114      */
115     private String controllerState = "";
116
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;
121
122     private boolean connecting = false;
123     private boolean reconnectRequest = false;
124     private boolean valueNotificationRequest = false;
125
126     private ScheduledFuture<?> controlJob;
127     private ScheduledFuture<?> pollingJobRf;
128
129     private Map<String, ScheduledFuture<?>> longPressFutures = new HashMap<>();
130
131     public IhcHandler(Thing thing) {
132         super(thing);
133     }
134
135     protected boolean isValueNotificationRequestActivated() {
136         synchronized (this) {
137             return valueNotificationRequest;
138         }
139     }
140
141     protected void setValueNotificationRequest(boolean valueNotificationRequest) {
142         synchronized (this) {
143             this.valueNotificationRequest = valueNotificationRequest;
144         }
145     }
146
147     protected boolean isReconnectRequestActivated() {
148         synchronized (this) {
149             return reconnectRequest;
150         }
151     }
152
153     protected void setReconnectRequest(boolean reconnect) {
154         synchronized (this) {
155             this.reconnectRequest = reconnect;
156         }
157     }
158
159     protected boolean isConnecting() {
160         synchronized (this) {
161             return connecting;
162         }
163     }
164
165     protected void setConnectingState(boolean value) {
166         synchronized (this) {
167             this.connecting = value;
168         }
169     }
170
171     private String getFilePathInUserDataFolder(String fileName) {
172         return OpenHAB.getUserDataFolder() + File.separator + fileName;
173     }
174
175     @Override
176     public void initialize() {
177         conf = getConfigAs(IhcConfiguration.class);
178         logger.debug("Using configuration: {}", conf);
179
180         linkedResourceIds.clear();
181         linkedResourceIds.addAll(getAllLinkedChannelsResourceIds());
182         logger.debug("Linked resources {}: {}", linkedResourceIds.size(), linkedResourceIds);
183
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);
187         }
188     }
189
190     @Override
191     public void dispose() {
192         logger.debug("Stopping thing");
193         if (controlJob != null && !controlJob.isCancelled()) {
194             controlJob.cancel(true);
195             controlJob = null;
196         }
197         disconnect();
198     }
199
200     @Override
201     public void handleCommand(ChannelUID channelUID, Command command) {
202         logger.debug("Received channel: {}, command: {}", channelUID, command);
203
204         if (ihc == null) {
205             logger.debug("Connection is not initialized, aborting resource value update for channel '{}'!", channelUID);
206             return;
207         }
208
209         if (ihc.getConnectionState() != ConnectionState.CONNECTED) {
210             logger.debug("Connection to controller is not open, aborting resource value update for channel '{}'!",
211                     channelUID);
212             return;
213         }
214
215         if (thing.getStatus() != ThingStatus.ONLINE) {
216             logger.debug("Controller is not ONLINE, aborting resource value update for channel '{}'!", channelUID);
217             return;
218         }
219
220         switch (channelUID.getId()) {
221             case CHANNEL_CONTROLLER_STATE:
222                 if (command.equals(RefreshType.REFRESH)) {
223                     updateControllerStateChannel();
224                 }
225                 break;
226
227             case CHANNEL_CONTROLLER_UPTIME:
228                 if (command.equals(RefreshType.REFRESH)) {
229                     updateControllerInformationChannels();
230                 }
231                 break;
232
233             case CHANNEL_CONTROLLER_TIME:
234                 if (command.equals(RefreshType.REFRESH)) {
235                     updateControllerTimeChannels();
236                 }
237                 break;
238
239             default:
240                 if (command.equals(RefreshType.REFRESH)) {
241                     refreshChannel(channelUID);
242                 } else {
243                     updateResourceChannel(channelUID, command);
244                 }
245                 break;
246         }
247     }
248
249     private void refreshChannel(ChannelUID channelUID) {
250         logger.debug("REFRESH channel {}", channelUID);
251         Channel channel = thing.getChannel(channelUID.getId());
252         if (channel != null) {
253             try {
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);
258                     return;
259                 }
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);
266             }
267         }
268     }
269
270     private void updateControllerStateChannel() {
271         try {
272             String state = ihc.getControllerState().getState();
273             String value;
274
275             switch (state) {
276                 case IhcClient.CONTROLLER_STATE_INITIALIZE:
277                     value = "Initialize";
278                     break;
279                 case IhcClient.CONTROLLER_STATE_READY:
280                     value = "Ready";
281                     break;
282                 default:
283                     value = "Unknown state: " + state;
284             }
285
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);
289         }
290     }
291
292     private void updateControllerProperties() {
293         try {
294             WSSystemInfo systemInfo = ihc.getSystemInfo();
295             logger.debug("Controller information: {}", systemInfo);
296             WSProjectInfo projectInfo = ihc.getProjectInfo();
297             logger.debug("Project information: {}", projectInfo);
298
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());
310             }
311             if (!systemInfo.getRfModuleSerialNumber().isEmpty()) {
312                 properties.put(PROPERTY_RF_MODULE_SERIALNUMBER, systemInfo.getRfModuleSerialNumber());
313             }
314             if (!systemInfo.getRfModuleSoftwareVersion().isEmpty()) {
315                 properties.put(PROPERTY_RF_MODULE_VERSION, systemInfo.getRfModuleSoftwareVersion());
316             }
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);
323         }
324     }
325
326     private void updateControllerInformationChannels() {
327         try {
328             WSSystemInfo systemInfo = ihc.getSystemInfo();
329             logger.debug("Controller information: {}", systemInfo);
330
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);
335         }
336     }
337
338     private void updateControllerTimeChannels() {
339         try {
340             WSTimeManagerSettings timeSettings = ihc.getTimeSettings();
341             logger.debug("Controller time settings: {}", timeSettings);
342
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);
348         }
349     }
350
351     private void updateResourceChannel(ChannelUID channelUID, Command command) {
352         Channel channel = thing.getChannel(channelUID.getId());
353         if (channel != null) {
354             try {
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);
359                     return;
360                 }
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());
366             }
367         }
368     }
369
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());
376             } else {
377                 logger.debug("Command '{}' doesn't equal to reaction trigger parameter '{}', skip it", command,
378                         params.getCommandToReact());
379                 return;
380             }
381         }
382         WSResourceValue value = ihc.getResourceValueInformation(params.getResourceId());
383         if (value != null) {
384             if (params.getPulseWidth() != null) {
385                 sendPulseCommand(channelUID, params, value, Math.min(params.getPulseWidth(), MAX_PULSE_WIDTH_IN_MS));
386             } else {
387                 sendNormalCommand(channelUID, params, command, value);
388             }
389         }
390     }
391
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(),
398                 command.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);
404             }
405         } else {
406             logger.debug("No converter implemented for {} <-> {}", value.getClass(), command.getClass());
407         }
408     }
409
410     private List<IhcEnumValue> getEnumValues(WSResourceValue value) {
411         if (value instanceof WSEnumValue) {
412             return enumDictionary.getEnumValues(((WSEnumValue) value).definitionTypeID);
413         }
414         return null;
415     }
416
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(),
422                 OnOffType.class);
423
424         if (converter != null) {
425             ConverterAdditionalInfo converterAdditionalInfo = new ConverterAdditionalInfo(null, params.isInverted(),
426                     getCommandLevels(params));
427
428             WSResourceValue valOn = converter.convertFromOHType(OnOffType.ON, value, converterAdditionalInfo);
429             WSResourceValue valOff = converter.convertFromOHType(OnOffType.OFF, value, converterAdditionalInfo);
430
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() {
436                     @Override
437                     public void run() {
438                         // set resource back to OFF
439                         logger.debug("Update resource value (inverted output={}): {}", params.isInverted(), valOff);
440                         try {
441                             if (!updateResource(valOff)) {
442                                 logger.warn("Channel {} update to resource '{}' failed.", channelUID, valOff);
443                             }
444                         } catch (IhcExecption e) {
445                             logger.warn("Can't update channel '{}' value, cause {}", channelUID, e.getMessage());
446                         }
447                     }
448                 }, pulseWidth, TimeUnit.MILLISECONDS);
449             } else {
450                 logger.warn("Channel {} update failed.", channelUID);
451             }
452         } else {
453             logger.debug("No converter implemented for {} <-> {}", value.getClass(), OnOffType.class);
454         }
455     }
456
457     /**
458      * Update resource value to IHC controller.
459      */
460     private boolean updateResource(WSResourceValue value) throws IhcExecption {
461         boolean result = false;
462         try {
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);
467         }
468         return result;
469     }
470
471     @Override
472     public void channelLinked(ChannelUID channelUID) {
473         logger.debug("channelLinked: {}", channelUID);
474
475         switch (channelUID.getId()) {
476             case CHANNEL_CONTROLLER_STATE:
477                 updateControllerStateChannel();
478                 break;
479
480             case CHANNEL_CONTROLLER_UPTIME:
481                 updateControllerInformationChannels();
482                 break;
483
484             case CHANNEL_CONTROLLER_TIME:
485                 updateControllerTimeChannels();
486                 break;
487
488             default:
489                 Channel channel = thing.getChannel(channelUID.getId());
490                 if (channel != null) {
491                     try {
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();
499                             }
500                         }
501                     } catch (ConversionException e) {
502                         logger.warn("Channel param error, reason: {}.", e.getMessage(), e);
503                     }
504                 }
505         }
506     }
507
508     @Override
509     public void channelUnlinked(ChannelUID channelUID) {
510         logger.debug("channelUnlinked: {}", channelUID);
511
512         switch (channelUID.getId()) {
513             case CHANNEL_CONTROLLER_STATE:
514             case CHANNEL_CONTROLLER_UPTIME:
515             case CHANNEL_CONTROLLER_TIME:
516                 break;
517
518             default:
519                 Channel channel = thing.getChannel(channelUID.getId());
520                 if (channel != null) {
521                     try {
522                         ChannelParams params = new ChannelParams(channel);
523                         if (params.getResourceId() != null) {
524                             linkedResourceIds.removeIf(c -> c.equals(params.getResourceId()));
525                             updateNotificationsRequestReminder();
526                         }
527                     } catch (ConversionException e) {
528                         logger.warn("Channel param error, reason: {}.", e.getMessage(), e);
529                     }
530                 }
531         }
532     }
533
534     /**
535      * Initialize IHC client and open connection to IHC / ELKO LS controller.
536      *
537      */
538     private void connect() throws IhcExecption {
539         try {
540             setConnectingState(true);
541             logger.debug("Connecting to IHC / ELKO LS controller [hostname='{}', username='{}'].", conf.hostname,
542                     conf.username);
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");
547             loadProject();
548             createChannels();
549             updateControllerProperties();
550             updateControllerStateChannel();
551             updateControllerInformationChannels();
552             updateControllerTimeChannels();
553             ihc.addEventListener(this);
554             ihc.startControllerEventListeners();
555             updateNotificationsRequestReminder();
556             startRFPolling();
557             updateStatus(ThingStatus.ONLINE);
558         } finally {
559             setConnectingState(false);
560         }
561     }
562
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;
568
569             if (projectFile == null) {
570                 // try first load project file from local cache file.
571                 try {
572                     projectFile = ProjectFileUtils.readFromFile(filePath);
573                 } catch (IhcExecption e) {
574                     logger.debug("Error occured when read project file from file '{}', reason {}", filePath,
575                             e.getMessage(), e);
576                     loadProject = true;
577                 }
578             }
579
580             if (!ProjectFileUtils.projectEqualsToControllerProject(projectFile, ihc.getProjectInfo())) {
581                 logger.debug(
582                         "Local project file is not same as in the controller, reload project file from controller!");
583                 loadProject = true;
584             }
585
586             if (loadProject) {
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);
590                 try {
591                     ProjectFileUtils.saveToFile(filePath, data);
592                 } catch (IhcExecption e) {
593                     logger.warn("Error occured when trying to write data to file '{}', reason {}", filePath,
594                             e.getMessage(), e);
595                 }
596                 projectFile = ProjectFileUtils.converteBytesToDocument(data);
597             }
598         }
599
600         enumDictionary = new EnumDictionary(ProjectFileUtils.parseEnums(projectFile));
601     }
602
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());
612         } else {
613             logger.debug("Automatic channel creation disabled");
614         }
615     }
616
617     private void printChannels(List<Channel> thingChannels) {
618         if (logger.isDebugEnabled()) {
619             thingChannels.forEach(channel -> {
620                 if (channel != null) {
621                     String resourceId;
622                     try {
623                         Object id = channel.getConfiguration().get(PARAM_RESOURCE_ID);
624                         resourceId = id != null ? "0x" + Integer.toHexString(((BigDecimal) id).intValue()) : "";
625                     } catch (IllegalArgumentException e) {
626                         resourceId = "";
627                     }
628
629                     String channelType = channel.getAcceptedItemType() != null ? channel.getAcceptedItemType() : "";
630                     String channelLabel = channel.getLabel() != null ? channel.getLabel() : "";
631
632                     logger.debug("Channel: {}", String.format("%-55s | %-10s | %-10s | %s", channel.getUID(),
633                             resourceId, channelType, channelLabel));
634                 }
635             });
636         }
637     }
638
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);
643         }
644     }
645
646     /**
647      * Disconnect connection to IHC / ELKO LS controller.
648      *
649      */
650     private void disconnect() {
651         cancelAllLongPressTasks();
652         if (pollingJobRf != null && !pollingJobRf.isCancelled()) {
653             pollingJobRf.cancel(true);
654             pollingJobRf = null;
655         }
656         if (ihc != null) {
657             try {
658                 ihc.removeEventListener(this);
659                 ihc.closeConnection();
660                 ihc = null;
661             } catch (IhcExecption e) {
662                 logger.warn("Couldn't close connection to IHC controller", e);
663             }
664         }
665         clearLastUpdateTimeCache();
666     }
667
668     private void clearLastUpdateTimeCache() {
669         lastUpdate.clear();
670     }
671
672     @Override
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);
677     }
678
679     @Override
680     public void statusUpdateReceived(WSControllerState newState) {
681         logger.debug("Controller state: {}", newState.getState());
682
683         if (!controllerState.equals(newState.getState())) {
684             logger.debug("Controller state change detected ({} -> {})", controllerState, newState.getState());
685
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");
693                     break;
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);
698                     break;
699                 default:
700             }
701
702             if (controllerState.equals(IhcClient.CONTROLLER_STATE_INITIALIZE)
703                     && newState.getState().equals(IhcClient.CONTROLLER_STATE_READY)) {
704                 logger.debug("Reconnection request");
705                 projectFile = null;
706                 setReconnectRequest(true);
707             }
708         }
709
710         controllerState = newState.getState();
711     }
712
713     @Override
714     public void resourceValueUpdateReceived(WSResourceValue value) {
715         logger.debug("resourceValueUpdateReceived: {}", value);
716
717         thing.getChannels().forEach(channel -> {
718             try {
719                 ChannelParams params = new ChannelParams(channel);
720                 if (params.getResourceId() != null && params.getResourceId().intValue() == value.resourceID) {
721                     updateChannelState(channel, params, value);
722                 }
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);
727             }
728         });
729
730         checkPotentialButtonPresses(value);
731     }
732
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());
736         } else {
737             if (params.getChannelTypeId() != null) {
738                 switch (params.getChannelTypeId()) {
739                     case CHANNEL_TYPE_PUSH_BUTTON_TRIGGER:
740                         break;
741
742                     default:
743                         try {
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);
752                             } else {
753                                 logger.debug("No converter implemented for {} <-> {}", value.getClass(),
754                                         channel.getAcceptedItemType());
755                             }
756                         } catch (ConversionException e) {
757                             logger.debug("Can't convert resource value '{}' to item type {}, reason: {}.", value,
758                                     channel.getAcceptedItemType(), e.getMessage(), e);
759                         }
760                 }
761             }
762         }
763     }
764
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);
771             } else {
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);
778                 }
779             }
780         }
781     }
782
783     private void updateTriggers(int resourceId, Duration duration) {
784         thing.getChannels().forEach(channel -> {
785             try {
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());
796                                 } else {
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);
805                                     }
806                                     break;
807                                 }
808                         }
809                     }
810                 }
811             } catch (ConversionException e) {
812                 logger.warn("Channel param error, reason:  {}", e.getMessage(), e);
813             }
814         });
815     }
816
817     private void createLongPressTask(String channelId, long longPressTimeInMs) {
818         if (longPressFutures.containsKey(channelId)) {
819             cancelLongPressTask(channelId);
820         }
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));
824     }
825
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);
831         }
832     }
833
834     private void cancelAllLongPressTasks() {
835         longPressFutures.entrySet().parallelStream().forEach(e -> e.getValue().cancel(true));
836         longPressFutures.clear();
837     }
838
839     private void updateRfDeviceStates() {
840         if (ihc != null) {
841             if (ihc.getConnectionState() != ConnectionState.CONNECTED) {
842                 logger.debug("Controller is connecting, abort subscribe");
843                 return;
844             }
845
846             logger.debug("Update RF device data");
847             try {
848                 List<WSRFDevice> devs = ihc.getDetectedRFDevices();
849                 logger.debug("RF data: {}", devs);
850
851                 devs.forEach(dev -> {
852                     thing.getChannels().forEach(channel -> {
853                         try {
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);
863                                             break;
864                                         case CHANNEL_TYPE_RF_SIGNAL_STRENGTH:
865                                             int signalLevel = new SignalLevelConverter(dev.getSignalStrength())
866                                                     .getSystemWideSignalLevel();
867                                             updateState(channelId, new StringType(String.valueOf(signalLevel)));
868                                             break;
869                                     }
870                                 }
871                             }
872                         } catch (ConversionException e) {
873                             logger.warn("Channel param error, reason:  {}", e.getMessage(), e);
874                         }
875                     });
876                 });
877             } catch (IhcExecption e) {
878                 logger.debug("Error occured when fetching RF device information, reason: : {} ", e.getMessage(), e);
879                 return;
880             }
881         }
882     }
883
884     private void reconnectCheck() {
885         if (ihc == null || isReconnectRequestActivated()) {
886             try {
887                 if (ihc != null) {
888                     disconnect();
889                 }
890                 connect();
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);
896                 return;
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);
901                 return;
902             }
903         }
904
905         if (isValueNotificationRequestActivated() && !isConnecting()) {
906             try {
907                 enableResourceValueNotifications();
908             } catch (IhcExecption e) {
909                 logger.warn("Can't enable resource value notifications from controller. ", e);
910             }
911         }
912     }
913
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 -> {
917             try {
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);
924                 return null;
925             }
926         }).filter(c -> c != null && c != 0).collect(Collectors.toSet()));
927         return resourceIds;
928     }
929
930     /**
931      * Order resource value notifications from IHC controller.
932      */
933     private void enableResourceValueNotifications() throws IhcExecption {
934         logger.debug("Subscribe resource runtime value notifications");
935
936         if (ihc != null) {
937             if (ihc.getConnectionState() != ConnectionState.CONNECTED) {
938                 logger.debug("Controller is connecting, abort subscribe");
939                 return;
940             }
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()) {
949                 try {
950                     ihc.enableRuntimeValueNotifications(resourceIds);
951                 } catch (IhcExecption e) {
952                     logger.debug("Reconnection request");
953                     setReconnectRequest(true);
954                 }
955             }
956         } else {
957             logger.warn("Controller is not initialized!");
958             logger.debug("Reconnection request");
959             setReconnectRequest(true);
960         }
961     }
962
963     private synchronized void updateNotificationsRequestReminder() {
964         if (notificationsRequestReminder != null) {
965             notificationsRequestReminder.cancel(false);
966         }
967
968         logger.debug("Rechedule resource runtime value notifications order by {}ms", NOTIFICATIONS_REORDER_WAIT_TIME);
969         notificationsRequestReminder = scheduler.schedule(new Runnable() {
970
971             @Override
972             public void run() {
973                 logger.debug("Delayed resource value notifications request is now enabled");
974                 setValueNotificationRequest(true);
975             }
976         }, NOTIFICATIONS_REORDER_WAIT_TIME, TimeUnit.MILLISECONDS);
977     }
978
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);
984         }
985         return null;
986     }
987 }