]> git.basschouten.com Git - openhab-addons.git/blob
fca29161c3967be6a9d4c9dece0c0700fcecf30e
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2020 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.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;
75
76 /**
77  * The {@link IhcHandler} is responsible for handling commands, which are
78  * sent to one of the channels.
79  *
80  * @author Pauli Anttila - Initial contribution
81  */
82 public class IhcHandler extends BaseThingHandler implements IhcEventListener {
83     private final Logger logger = LoggerFactory.getLogger(IhcHandler.class);
84
85     /** Maximum pulse width in milliseconds. */
86     private static final int MAX_PULSE_WIDTH_IN_MS = 4000;
87
88     /** Maximum long press time in milliseconds. */
89     private static final int MAX_LONG_PRESS_IN_MS = 5000;
90
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";
93
94     /** Holds runtime notification reorder timeout in milliseconds */
95     private static final int NOTIFICATIONS_REORDER_WAIT_TIME = 1000;
96
97     /** IHC / ELKO LS Controller client */
98     private IhcClient ihc;
99
100     /**
101      * Reminder to slow down resource value notification ordering from
102      * controller.
103      */
104     private ScheduledFuture<?> notificationsRequestReminder;
105
106     /** Holds local IHC / ELKO project file */
107     private Document projectFile;
108
109     /**
110      * Store current state of the controller, use to recognize when controller
111      * state is changed
112      */
113     private String controllerState = "";
114
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;
119
120     private boolean connecting = false;
121     private boolean reconnectRequest = false;
122     private boolean valueNotificationRequest = false;
123
124     private ScheduledFuture<?> controlJob;
125     private ScheduledFuture<?> pollingJobRf;
126
127     private Map<String, ScheduledFuture<?>> longPressFutures = new HashMap<>();
128
129     public IhcHandler(Thing thing) {
130         super(thing);
131     }
132
133     protected boolean isValueNotificationRequestActivated() {
134         synchronized (this) {
135             return valueNotificationRequest;
136         }
137     }
138
139     protected void setValueNotificationRequest(boolean valueNotificationRequest) {
140         synchronized (this) {
141             this.valueNotificationRequest = valueNotificationRequest;
142         }
143     }
144
145     protected boolean isReconnectRequestActivated() {
146         synchronized (this) {
147             return reconnectRequest;
148         }
149     }
150
151     protected void setReconnectRequest(boolean reconnect) {
152         synchronized (this) {
153             this.reconnectRequest = reconnect;
154         }
155     }
156
157     protected boolean isConnecting() {
158         synchronized (this) {
159             return connecting;
160         }
161     }
162
163     protected void setConnectingState(boolean value) {
164         synchronized (this) {
165             this.connecting = value;
166         }
167     }
168
169     private String getFilePathInUserDataFolder(String fileName) {
170         String progArg = System.getProperty("smarthome.userdata");
171         if (progArg != null) {
172             return progArg + File.separator + fileName;
173         }
174         return fileName;
175     }
176
177     @Override
178     public void initialize() {
179         conf = getConfigAs(IhcConfiguration.class);
180         logger.debug("Using configuration: {}", conf);
181
182         linkedResourceIds.clear();
183         linkedResourceIds.addAll(getAllLinkedChannelsResourceIds());
184         logger.debug("Linked resources {}: {}", linkedResourceIds.size(), linkedResourceIds);
185
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);
189         }
190     }
191
192     @Override
193     public void dispose() {
194         logger.debug("Stopping thing");
195         if (controlJob != null && !controlJob.isCancelled()) {
196             controlJob.cancel(true);
197             controlJob = null;
198         }
199         disconnect();
200     }
201
202     @Override
203     public void handleCommand(ChannelUID channelUID, Command command) {
204         logger.debug("Received channel: {}, command: {}", channelUID, command);
205
206         if (ihc == null) {
207             logger.warn("Connection is not initialized, abort resource value update for channel '{}'!", channelUID);
208             return;
209         }
210
211         if (ihc.getConnectionState() != ConnectionState.CONNECTED) {
212             logger.warn("Connection to controller is not open, abort resource value update for channel '{}'!",
213                     channelUID);
214             return;
215         }
216
217         switch (channelUID.getId()) {
218             case CHANNEL_CONTROLLER_STATE:
219                 if (command.equals(RefreshType.REFRESH)) {
220                     updateControllerStateChannel();
221                 }
222                 break;
223
224             case CHANNEL_CONTROLLER_UPTIME:
225                 if (command.equals(RefreshType.REFRESH)) {
226                     updateControllerInformationChannels();
227                 }
228                 break;
229
230             case CHANNEL_CONTROLLER_TIME:
231                 if (command.equals(RefreshType.REFRESH)) {
232                     updateControllerTimeChannels();
233                 }
234                 break;
235
236             default:
237                 if (command.equals(RefreshType.REFRESH)) {
238                     refreshChannel(channelUID);
239                 } else {
240                     updateResourceChannel(channelUID, command);
241                 }
242                 break;
243         }
244     }
245
246     private void refreshChannel(ChannelUID channelUID) {
247         logger.debug("REFRESH channel {}", channelUID);
248         Channel channel = thing.getChannel(channelUID.getId());
249         if (channel != null) {
250             try {
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);
255                     return;
256                 }
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);
263             }
264         }
265     }
266
267     private void updateControllerStateChannel() {
268         try {
269             String state = ihc.getControllerState().getState();
270             String value;
271
272             switch (state) {
273                 case IhcClient.CONTROLLER_STATE_INITIALIZE:
274                     value = "Initialize";
275                     break;
276                 case IhcClient.CONTROLLER_STATE_READY:
277                     value = "Ready";
278                     break;
279                 default:
280                     value = "Unknown state: " + state;
281             }
282
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);
286         }
287     }
288
289     private void updateControllerProperties() {
290         try {
291             WSSystemInfo systemInfo = ihc.getSystemInfo();
292             logger.debug("Controller information: {}", systemInfo);
293             WSProjectInfo projectInfo = ihc.getProjectInfo();
294             logger.debug("Project information: {}", projectInfo);
295
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());
307             }
308             if (!systemInfo.getRfModuleSerialNumber().isEmpty()) {
309                 properties.put(PROPERTY_RF_MODULE_SERIALNUMBER, systemInfo.getRfModuleSerialNumber());
310             }
311             if (!systemInfo.getRfModuleSoftwareVersion().isEmpty()) {
312                 properties.put(PROPERTY_RF_MODULE_VERSION, systemInfo.getRfModuleSoftwareVersion());
313             }
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);
320         }
321     }
322
323     private void updateControllerInformationChannels() {
324         try {
325             WSSystemInfo systemInfo = ihc.getSystemInfo();
326             logger.debug("Controller information: {}", systemInfo);
327
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);
332         }
333     }
334
335     private void updateControllerTimeChannels() {
336         try {
337             WSTimeManagerSettings timeSettings = ihc.getTimeSettings();
338             logger.debug("Controller time settings: {}", timeSettings);
339
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);
345         }
346     }
347
348     private void updateResourceChannel(ChannelUID channelUID, Command command) {
349         Channel channel = thing.getChannel(channelUID.getId());
350         if (channel != null) {
351             try {
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);
356                     return;
357                 }
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());
363             }
364         }
365     }
366
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());
373             } else {
374                 logger.debug("Command '{}' doesn't equal to reaction trigger parameter '{}', skip it", command,
375                         params.getCommandToReact());
376                 return;
377             }
378         }
379         WSResourceValue value = ihc.getResourceValueInformation(params.getResourceId());
380         if (value != null) {
381             if (params.getPulseWidth() != null) {
382                 sendPulseCommand(channelUID, params, value, Math.min(params.getPulseWidth(), MAX_PULSE_WIDTH_IN_MS));
383             } else {
384                 sendNormalCommand(channelUID, params, command, value);
385             }
386         }
387     }
388
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(),
395                 command.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);
401             }
402         } else {
403             logger.debug("No converter implemented for {} <-> {}", value.getClass(), command.getClass());
404         }
405     }
406
407     private List<IhcEnumValue> getEnumValues(WSResourceValue value) {
408         if (value instanceof WSEnumValue) {
409             return enumDictionary.getEnumValues(((WSEnumValue) value).definitionTypeID);
410         }
411         return null;
412     }
413
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(),
419                 OnOffType.class);
420
421         if (converter != null) {
422             ConverterAdditionalInfo converterAdditionalInfo = new ConverterAdditionalInfo(null, params.isInverted(),
423                     getCommandLevels(params));
424
425             WSResourceValue valOn = converter.convertFromOHType(OnOffType.ON, value, converterAdditionalInfo);
426             WSResourceValue valOff = converter.convertFromOHType(OnOffType.OFF, value, converterAdditionalInfo);
427
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() {
433                     @Override
434                     public void run() {
435                         // set resource back to OFF
436                         logger.debug("Update resource value (inverted output={}): {}", params.isInverted(), valOff);
437                         try {
438                             if (!updateResource(valOff)) {
439                                 logger.warn("Channel {} update to resource '{}' failed.", channelUID, valOff);
440                             }
441                         } catch (IhcExecption e) {
442                             logger.warn("Can't update channel '{}' value, cause {}", channelUID, e.getMessage());
443                         }
444                     }
445                 }, pulseWidth, TimeUnit.MILLISECONDS);
446             } else {
447                 logger.warn("Channel {} update failed.", channelUID);
448             }
449         } else {
450             logger.debug("No converter implemented for {} <-> {}", value.getClass(), OnOffType.class);
451         }
452     }
453
454     /**
455      * Update resource value to IHC controller.
456      */
457     private boolean updateResource(WSResourceValue value) throws IhcExecption {
458         boolean result = false;
459         try {
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);
464         }
465         return result;
466     }
467
468     @Override
469     public void channelLinked(ChannelUID channelUID) {
470         logger.debug("channelLinked: {}", channelUID);
471
472         switch (channelUID.getId()) {
473             case CHANNEL_CONTROLLER_STATE:
474                 updateControllerStateChannel();
475                 break;
476
477             case CHANNEL_CONTROLLER_UPTIME:
478                 updateControllerInformationChannels();
479                 break;
480
481             case CHANNEL_CONTROLLER_TIME:
482                 updateControllerTimeChannels();
483                 break;
484
485             default:
486                 Channel channel = thing.getChannel(channelUID.getId());
487                 if (channel != null) {
488                     try {
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();
496                             }
497                         }
498                     } catch (ConversionException e) {
499                         logger.warn("Channel param error, reason: {}.", e.getMessage(), e);
500                     }
501                 }
502         }
503     }
504
505     @Override
506     public void channelUnlinked(ChannelUID channelUID) {
507         logger.debug("channelUnlinked: {}", channelUID);
508
509         switch (channelUID.getId()) {
510             case CHANNEL_CONTROLLER_STATE:
511             case CHANNEL_CONTROLLER_UPTIME:
512             case CHANNEL_CONTROLLER_TIME:
513                 break;
514
515             default:
516                 Channel channel = thing.getChannel(channelUID.getId());
517                 if (channel != null) {
518                     try {
519                         ChannelParams params = new ChannelParams(channel);
520                         if (params.getResourceId() != null) {
521                             linkedResourceIds.removeIf(c -> c.equals(params.getResourceId()));
522                             updateNotificationsRequestReminder();
523                         }
524                     } catch (ConversionException e) {
525                         logger.warn("Channel param error, reason: {}.", e.getMessage(), e);
526                     }
527                 }
528         }
529     }
530
531     /**
532      * Initialize IHC client and open connection to IHC / ELKO LS controller.
533      *
534      */
535     private void connect() throws IhcExecption {
536         try {
537             setConnectingState(true);
538             logger.debug("Connecting to IHC / ELKO LS controller [hostname='{}', username='{}'].", conf.hostname,
539                     conf.username);
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");
544             loadProject();
545             createChannels();
546             updateControllerProperties();
547             updateControllerStateChannel();
548             updateControllerInformationChannels();
549             updateControllerTimeChannels();
550             ihc.addEventListener(this);
551             ihc.startControllerEventListeners();
552             updateNotificationsRequestReminder();
553             startRFPolling();
554             updateStatus(ThingStatus.ONLINE);
555         } finally {
556             setConnectingState(false);
557         }
558     }
559
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;
565
566             if (projectFile == null) {
567                 // try first load project file from local cache file.
568                 try {
569                     projectFile = ProjectFileUtils.readFromFile(filePath);
570                 } catch (IhcExecption e) {
571                     logger.debug("Error occured when read project file from file '{}', reason {}", filePath,
572                             e.getMessage(), e);
573                     loadProject = true;
574                 }
575             }
576
577             if (!ProjectFileUtils.projectEqualsToControllerProject(projectFile, ihc.getProjectInfo())) {
578                 logger.debug(
579                         "Local project file is not same as in the controller, reload project file from controller!");
580                 loadProject = true;
581             }
582
583             if (loadProject) {
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);
587                 try {
588                     ProjectFileUtils.saveToFile(filePath, data);
589                 } catch (IhcExecption e) {
590                     logger.warn("Error occured when trying to write data to file '{}', reason {}", filePath,
591                             e.getMessage(), e);
592                 }
593                 projectFile = ProjectFileUtils.converteBytesToDocument(data);
594             }
595         }
596
597         enumDictionary = new EnumDictionary(ProjectFileUtils.parseEnums(projectFile));
598     }
599
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());
609         } else {
610             logger.debug("Automatic channel creation disabled");
611         }
612     }
613
614     private void printChannels(List<Channel> thingChannels) {
615         if (logger.isDebugEnabled()) {
616             thingChannels.forEach(channel -> {
617                 if (channel != null) {
618                     String resourceId;
619                     try {
620                         Object id = channel.getConfiguration().get(PARAM_RESOURCE_ID);
621                         resourceId = id != null ? "0x" + Integer.toHexString(((BigDecimal) id).intValue()) : "";
622                     } catch (IllegalArgumentException e) {
623                         resourceId = "";
624                     }
625
626                     String channelType = channel.getAcceptedItemType() != null ? channel.getAcceptedItemType() : "";
627                     String channelLabel = channel.getLabel() != null ? channel.getLabel() : "";
628
629                     logger.debug("Channel: {}", String.format("%-55s | %-10s | %-10s | %s", channel.getUID(),
630                             resourceId, channelType, channelLabel));
631                 }
632             });
633         }
634     }
635
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);
640         }
641     }
642
643     /**
644      * Disconnect connection to IHC / ELKO LS controller.
645      *
646      */
647     private void disconnect() {
648         cancelAllLongPressTasks();
649         if (pollingJobRf != null && !pollingJobRf.isCancelled()) {
650             pollingJobRf.cancel(true);
651             pollingJobRf = null;
652         }
653         if (ihc != null) {
654             try {
655                 ihc.removeEventListener(this);
656                 ihc.closeConnection();
657                 ihc = null;
658             } catch (IhcExecption e) {
659                 logger.warn("Couldn't close connection to IHC controller", e);
660             }
661         }
662         clearLastUpdateTimeCache();
663     }
664
665     private void clearLastUpdateTimeCache() {
666         lastUpdate.clear();
667     }
668
669     @Override
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);
674     }
675
676     @Override
677     public void statusUpdateReceived(WSControllerState newState) {
678         logger.debug("Controller state: {}", newState.getState());
679
680         if (!controllerState.equals(newState.getState())) {
681             logger.debug("Controller state change detected ({} -> {})", controllerState, newState.getState());
682
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");
690                     break;
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);
695                     break;
696                 default:
697             }
698
699             if (controllerState.equals(IhcClient.CONTROLLER_STATE_INITIALIZE)
700                     && newState.getState().equals(IhcClient.CONTROLLER_STATE_READY)) {
701                 logger.debug("Reconnection request");
702                 projectFile = null;
703                 setReconnectRequest(true);
704             }
705         }
706
707         controllerState = newState.getState();
708     }
709
710     @Override
711     public void resourceValueUpdateReceived(WSResourceValue value) {
712         logger.debug("resourceValueUpdateReceived: {}", value);
713
714         thing.getChannels().forEach(channel -> {
715             try {
716                 ChannelParams params = new ChannelParams(channel);
717                 if (params.getResourceId() != null && params.getResourceId().intValue() == value.resourceID) {
718                     updateChannelState(channel, params, value);
719                 }
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);
724             }
725         });
726
727         checkPotentialButtonPresses(value);
728     }
729
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());
733         } else {
734             if (params.getChannelTypeId() != null) {
735                 switch (params.getChannelTypeId()) {
736                     case CHANNEL_TYPE_PUSH_BUTTON_TRIGGER:
737                         break;
738
739                     default:
740                         try {
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);
749                             } else {
750                                 logger.debug("No converter implemented for {} <-> {}", value.getClass(),
751                                         channel.getAcceptedItemType());
752                             }
753                         } catch (ConversionException e) {
754                             logger.debug("Can't convert resource value '{}' to item type {}, reason: {}.", value,
755                                     channel.getAcceptedItemType(), e.getMessage(), e);
756                         }
757                 }
758             }
759         }
760     }
761
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);
768             } else {
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);
775                 }
776             }
777         }
778     }
779
780     private void updateTriggers(int resourceId, Duration duration) {
781         thing.getChannels().forEach(channel -> {
782             try {
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());
793                                 } else {
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);
802                                     }
803                                     break;
804                                 }
805                         }
806                     }
807                 }
808             } catch (ConversionException e) {
809                 logger.warn("Channel param error, reason:  {}", e.getMessage(), e);
810             }
811         });
812     }
813
814     private void createLongPressTask(String channelId, long longPressTimeInMs) {
815         if (longPressFutures.containsKey(channelId)) {
816             cancelLongPressTask(channelId);
817         }
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));
821     }
822
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);
828         }
829     }
830
831     private void cancelAllLongPressTasks() {
832         longPressFutures.entrySet().parallelStream().forEach(e -> e.getValue().cancel(true));
833         longPressFutures.clear();
834     }
835
836     private void updateRfDeviceStates() {
837         if (ihc != null) {
838             if (ihc.getConnectionState() != ConnectionState.CONNECTED) {
839                 logger.debug("Controller is connecting, abort subscribe");
840                 return;
841             }
842
843             logger.debug("Update RF device data");
844             try {
845                 List<WSRFDevice> devs = ihc.getDetectedRFDevices();
846                 logger.debug("RF data: {}", devs);
847
848                 devs.forEach(dev -> {
849                     thing.getChannels().forEach(channel -> {
850                         try {
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);
860                                             break;
861                                         case CHANNEL_TYPE_RF_SIGNAL_STRENGTH:
862                                             int signalLevel = new SignalLevelConverter(dev.getSignalStrength())
863                                                     .getSystemWideSignalLevel();
864                                             updateState(channelId, new StringType(String.valueOf(signalLevel)));
865                                             break;
866                                     }
867                                 }
868                             }
869                         } catch (ConversionException e) {
870                             logger.warn("Channel param error, reason:  {}", e.getMessage(), e);
871                         }
872                     });
873                 });
874             } catch (IhcExecption e) {
875                 logger.debug("Error occured when fetching RF device information, reason: : {} ", e.getMessage(), e);
876                 return;
877             }
878         }
879     }
880
881     private void reconnectCheck() {
882         if (ihc == null || isReconnectRequestActivated()) {
883             try {
884                 if (ihc != null) {
885                     disconnect();
886                 }
887                 connect();
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);
893                 return;
894             }
895         }
896
897         if (isValueNotificationRequestActivated() && !isConnecting()) {
898             try {
899                 enableResourceValueNotifications();
900             } catch (IhcExecption e) {
901                 logger.warn("Can't enable resource value notifications from controller. ", e);
902             }
903         }
904     }
905
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 -> {
909             try {
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);
916                 return null;
917             }
918         }).filter(c -> c != null && c != 0).collect(Collectors.toSet()));
919         return resourceIds;
920     }
921
922     /**
923      * Order resource value notifications from IHC controller.
924      */
925     private void enableResourceValueNotifications() throws IhcExecption {
926         logger.debug("Subscribe resource runtime value notifications");
927
928         if (ihc != null) {
929             if (ihc.getConnectionState() != ConnectionState.CONNECTED) {
930                 logger.debug("Controller is connecting, abort subscribe");
931                 return;
932             }
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()) {
941                 try {
942                     ihc.enableRuntimeValueNotifications(resourceIds);
943                 } catch (IhcExecption e) {
944                     logger.debug("Reconnection request");
945                     setReconnectRequest(true);
946                 }
947             }
948         } else {
949             logger.warn("Controller is not initialized!");
950             logger.debug("Reconnection request");
951             setReconnectRequest(true);
952         }
953     }
954
955     private synchronized void updateNotificationsRequestReminder() {
956         if (notificationsRequestReminder != null) {
957             notificationsRequestReminder.cancel(false);
958         }
959
960         logger.debug("Rechedule resource runtime value notifications order by {}ms", NOTIFICATIONS_REORDER_WAIT_TIME);
961         notificationsRequestReminder = scheduler.schedule(new Runnable() {
962
963             @Override
964             public void run() {
965                 logger.debug("Delayed resource value notifications request is now enabled");
966                 setValueNotificationRequest(true);
967             }
968         }, NOTIFICATIONS_REORDER_WAIT_TIME, TimeUnit.MILLISECONDS);
969     }
970
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);
976         }
977         return null;
978     }
979 }