]> git.basschouten.com Git - openhab-addons.git/blob
a3fdbff332714cf65a2ff6210adec610dcbefb8c
[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.max.internal.handler;
14
15 import static org.openhab.binding.max.internal.MaxBindingConstants.*;
16 import static org.openhab.core.library.unit.SIUnits.CELSIUS;
17
18 import java.io.BufferedReader;
19 import java.io.IOException;
20 import java.io.InputStreamReader;
21 import java.io.OutputStreamWriter;
22 import java.math.BigDecimal;
23 import java.math.RoundingMode;
24 import java.net.ConnectException;
25 import java.net.Socket;
26 import java.net.SocketException;
27 import java.net.UnknownHostException;
28 import java.nio.charset.StandardCharsets;
29 import java.time.ZoneId;
30 import java.time.format.DateTimeFormatter;
31 import java.util.ArrayList;
32 import java.util.Collection;
33 import java.util.Collections;
34 import java.util.Date;
35 import java.util.HashSet;
36 import java.util.List;
37 import java.util.Map;
38 import java.util.Map.Entry;
39 import java.util.Set;
40 import java.util.concurrent.ArrayBlockingQueue;
41 import java.util.concurrent.BlockingQueue;
42 import java.util.concurrent.CopyOnWriteArraySet;
43 import java.util.concurrent.ScheduledFuture;
44 import java.util.concurrent.TimeUnit;
45 import java.util.concurrent.locks.Condition;
46 import java.util.concurrent.locks.ReentrantLock;
47 import java.util.stream.Collectors;
48 import java.util.stream.Stream;
49
50 import javax.measure.quantity.Temperature;
51
52 import org.openhab.binding.max.internal.MaxBackupUtils;
53 import org.openhab.binding.max.internal.MaxBindingConstants;
54 import org.openhab.binding.max.internal.actions.MaxCubeActions;
55 import org.openhab.binding.max.internal.command.ACommand;
56 import org.openhab.binding.max.internal.command.CCommand;
57 import org.openhab.binding.max.internal.command.CubeCommand;
58 import org.openhab.binding.max.internal.command.FCommand;
59 import org.openhab.binding.max.internal.command.LCommand;
60 import org.openhab.binding.max.internal.command.MCommand;
61 import org.openhab.binding.max.internal.command.NCommand;
62 import org.openhab.binding.max.internal.command.QCommand;
63 import org.openhab.binding.max.internal.command.SCommand;
64 import org.openhab.binding.max.internal.command.TCommand;
65 import org.openhab.binding.max.internal.command.UdpCubeCommand;
66 import org.openhab.binding.max.internal.config.MaxCubeBridgeConfiguration;
67 import org.openhab.binding.max.internal.device.Device;
68 import org.openhab.binding.max.internal.device.DeviceConfiguration;
69 import org.openhab.binding.max.internal.device.DeviceInformation;
70 import org.openhab.binding.max.internal.device.DeviceType;
71 import org.openhab.binding.max.internal.device.HeatingThermostat;
72 import org.openhab.binding.max.internal.device.RoomInformation;
73 import org.openhab.binding.max.internal.device.ThermostatModeType;
74 import org.openhab.binding.max.internal.discovery.MaxDeviceDiscoveryService;
75 import org.openhab.binding.max.internal.exceptions.UnprocessableMessageException;
76 import org.openhab.binding.max.internal.message.CMessage;
77 import org.openhab.binding.max.internal.message.FMessage;
78 import org.openhab.binding.max.internal.message.HMessage;
79 import org.openhab.binding.max.internal.message.LMessage;
80 import org.openhab.binding.max.internal.message.MMessage;
81 import org.openhab.binding.max.internal.message.Message;
82 import org.openhab.binding.max.internal.message.MessageProcessor;
83 import org.openhab.binding.max.internal.message.NMessage;
84 import org.openhab.binding.max.internal.message.SMessage;
85 import org.openhab.core.cache.ExpiringCache;
86 import org.openhab.core.config.core.Configuration;
87 import org.openhab.core.library.types.DecimalType;
88 import org.openhab.core.library.types.OnOffType;
89 import org.openhab.core.library.types.QuantityType;
90 import org.openhab.core.library.types.StringType;
91 import org.openhab.core.thing.Bridge;
92 import org.openhab.core.thing.ChannelUID;
93 import org.openhab.core.thing.Thing;
94 import org.openhab.core.thing.ThingStatus;
95 import org.openhab.core.thing.ThingStatusDetail;
96 import org.openhab.core.thing.binding.BaseBridgeHandler;
97 import org.openhab.core.thing.binding.ThingHandlerService;
98 import org.openhab.core.types.Command;
99 import org.openhab.core.types.RefreshType;
100 import org.slf4j.Logger;
101 import org.slf4j.LoggerFactory;
102
103 /**
104  * {@link MaxCubeBridgeHandler} is the handler for a MAX! Cube and connects it
105  * to the framework. All {@link MaxDevicesHandler}s use the
106  * {@link MaxCubeBridgeHandler} to execute the actual commands.
107  *
108  * @author Andreas Heil (info@aheil.de) - Initial contribution
109  * @author Marcel Verpaalen - Initial contribution OH2 version
110  * @author Bernd Michael Helm (bernd.helm at helmundwalter.de) - Exclusive mode
111  */
112 public class MaxCubeBridgeHandler extends BaseBridgeHandler {
113
114     private enum BackupState {
115         NO_BACKUP,
116         REQUESTED,
117         IN_PROGRESS
118     }
119
120     /** timeout on network connection **/
121     private static final int NETWORK_TIMEOUT = 10000;
122     /** MAX! Thermostat default off temperature */
123     private static final double DEFAULT_OFF_TEMPERATURE = 4.5;
124     /** MAX! Thermostat default on temperature */
125     private static final double DEFAULT_ON_TEMPERATURE = 30.5;
126     /** maximum queue size that we're allowing */
127     private static final int MAX_COMMANDS = 50;
128     private static final int MAX_DUTY_CYCLE = 80;
129     private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMdd-HHmm");
130
131     private final Logger logger = LoggerFactory.getLogger(MaxCubeBridgeHandler.class);
132     private final List<Device> devices = new ArrayList<>();
133     private List<RoomInformation> rooms;
134     private final Set<String> lastActiveDevices = new HashSet<>();
135     private final List<DeviceConfiguration> configurations = new ArrayList<>();
136     private final BlockingQueue<SendCommand> commandQueue = new ArrayBlockingQueue<>(MAX_COMMANDS);
137
138     private SendCommand lastCommandId;
139     private long refreshInterval = 30;
140     private String ipAddress;
141     private int port;
142     private boolean exclusive;
143     private int maxRequestsPerConnection;
144     private String ntpServer1;
145     private String ntpServer2;
146     private int requestCount;
147     private boolean propertiesSet;
148     private boolean roomPropertiesSet;
149
150     private final MessageProcessor messageProcessor = new MessageProcessor();
151     private final ReentrantLock dutyCycleLock = new ReentrantLock();
152     private final Condition excessDutyCycle = dutyCycleLock.newCondition();
153
154     /**
155      * Duty cycle of the cube
156      */
157     private int dutyCycle;
158
159     /**
160      * The available memory slots of the cube
161      */
162     private int freeMemorySlots;
163
164     /**
165      * connection socket and reader/writer for execute method
166      */
167     private Socket socket;
168     private BufferedReader reader;
169     private OutputStreamWriter writer;
170
171     private boolean previousOnline;
172
173     private final Set<DeviceStatusListener> deviceStatusListeners = new CopyOnWriteArraySet<>();
174
175     private static final long CACHE_EXPIRY = TimeUnit.SECONDS.toMillis(10);
176     private final ExpiringCache<Boolean> refreshCache = new ExpiringCache<>(CACHE_EXPIRY, () -> {
177         logger.debug("Refreshing.");
178         refreshData();
179         return true;
180     });
181
182     private ScheduledFuture<?> pollingJob;
183     private Thread queueConsumerThread;
184     private BackupState backup = BackupState.REQUESTED;
185     private MaxBackupUtils backupUtil;
186
187     public MaxCubeBridgeHandler(Bridge br) {
188         super(br);
189     }
190
191     @Override
192     public void handleCommand(ChannelUID channelUID, Command command) {
193         if (command instanceof RefreshType) {
194             logger.debug("Refresh command received.");
195             refreshCache.getValue();
196         } else {
197             logger.warn("No bridge commands defined. Cannot process '{}'.", command);
198         }
199     }
200
201     @Override
202     public void dispose() {
203         logger.debug("Handler disposed.");
204         try {
205             stopAutomaticRefresh();
206         } catch (InterruptedException e) {
207             logger.error("Could not stop automatic refresh", e);
208             Thread.currentThread().interrupt();
209         }
210         clearDeviceList();
211
212         socketClose();
213         super.dispose();
214     }
215
216     @Override
217     public void initialize() {
218         logger.debug("Initializing MAX! Cube bridge handler.");
219
220         MaxCubeBridgeConfiguration configuration = getConfigAs(MaxCubeBridgeConfiguration.class);
221         port = configuration.port;
222         ipAddress = configuration.ipAddress;
223         refreshInterval = configuration.refreshInterval;
224         exclusive = configuration.exclusive;
225         maxRequestsPerConnection = configuration.maxRequestsPerConnection;
226         ntpServer1 = configuration.ntpServer1;
227         ntpServer2 = configuration.ntpServer2;
228         logger.debug("Cube IP         {}.", ipAddress);
229         logger.debug("Port            {}.", port);
230         logger.debug("RefreshInterval {}.", refreshInterval);
231         logger.debug("Exclusive mode  {}.", exclusive);
232         logger.debug("Max Requests    {}.", maxRequestsPerConnection);
233
234         previousOnline = true; // To trigger offline in case no connection @ startup
235         backupUtil = new MaxBackupUtils();
236         startAutomaticRefresh();
237     }
238
239     @Override
240     public void handleConfigurationUpdate(Map<String, Object> configurationParameters) {
241         boolean refresh = true;
242         logger.debug("MAX! Cube {}: Configuration update received", getThing().getThingTypeUID());
243
244         Configuration configuration = editConfiguration();
245         for (Entry<String, Object> configurationParameter : configurationParameters.entrySet()) {
246             logger.debug("MAX! Cube {}: Configuration update {} to {}", getThing().getThingTypeUID(),
247                     configurationParameter.getKey(), configurationParameter.getValue());
248             if (configurationParameter.getKey().startsWith("ntp")) {
249                 sendNtpUpdate(configurationParameters);
250                 if (configurationParameters.size() == 1) {
251                     refresh = false;
252                 }
253             }
254             if (configurationParameter.getKey().startsWith("action-")) {
255                 if (configurationParameter.getValue().toString().equals(BUTTON_ACTION_VALUE)) {
256                     if (configurationParameter.getKey().equals(ACTION_CUBE_REBOOT)) {
257                         cubeReboot();
258                     }
259                     if (configurationParameter.getKey().equals(ACTION_CUBE_RESET)) {
260                         cubeConfigReset();
261                         refresh = false;
262                     }
263                 }
264                 configuration.put(configurationParameter.getKey(), BigDecimal.valueOf(BUTTON_NOACTION_VALUE));
265             } else {
266                 configuration.put(configurationParameter.getKey(), configurationParameter.getValue());
267             }
268         }
269
270         // Persist changes and restart with new parameters
271         updateConfiguration(configuration);
272
273         if (refresh) {
274             try {
275                 stopAutomaticRefresh();
276             } catch (InterruptedException e) {
277                 logger.error("Could not stop automatic refresh", e);
278                 Thread.currentThread().interrupt();
279             }
280             clearDeviceList();
281             socketClose();
282             initialize();
283         }
284     }
285
286     @Override
287     public Collection<Class<? extends ThingHandlerService>> getServices() {
288         return Collections.unmodifiableSet(
289                 Stream.of(MaxDeviceDiscoveryService.class, MaxCubeActions.class).collect(Collectors.toSet()));
290     }
291
292     public void cubeConfigReset() {
293         logger.debug("Resetting configuration for MAX! Cube {}", getThing().getUID());
294         sendCubeCommand(new ACommand());
295         for (Device di : devices) {
296             for (DeviceStatusListener deviceStatusListener : deviceStatusListeners) {
297                 try {
298                     deviceStatusListener.onDeviceRemoved(this, di);
299                 } catch (Exception e) {
300                     logger.error("An exception occurred while calling the DeviceStatusListener", e);
301                     unregisterDeviceStatusListener(deviceStatusListener);
302                 }
303             }
304         }
305         clearDeviceList();
306         propertiesSet = false;
307         roomPropertiesSet = false;
308     }
309
310     public void cubeReboot() {
311         logger.info("Rebooting MAX! Cube {}", getThing().getUID());
312         MaxCubeBridgeConfiguration maxConfiguration = getConfigAs(MaxCubeBridgeConfiguration.class);
313         UdpCubeCommand reboot = new UdpCubeCommand(UdpCubeCommand.UdpCommandType.REBOOT, maxConfiguration.serialNumber);
314         reboot.setIpAddress(maxConfiguration.ipAddress);
315         reboot.send();
316         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "Rebooting");
317     }
318
319     public void deviceInclusion() {
320         if (previousOnline && socket != null) {
321             updateStatus(ThingStatus.ONLINE, ThingStatusDetail.CONFIGURATION_PENDING, "Inclusion");
322             logger.debug("Start MAX! inclusion mode for 60 seconds");
323             try {
324                 socket.setSoTimeout(80000);
325                 if (!sendCubeCommand(new NCommand())) {
326                     logger.debug("Error during Inclusion mode");
327                 }
328                 logger.debug("End MAX! inclusion mode");
329                 socket.setSoTimeout(NETWORK_TIMEOUT);
330             } catch (SocketException e) {
331                 logger.debug("Timeout during MAX! inclusion mode");
332             }
333         } else {
334             logger.debug("Need to be online to start inclusion mode");
335         }
336     }
337
338     private synchronized void startAutomaticRefresh() {
339         if (pollingJob == null || pollingJob.isCancelled()) {
340             pollingJob = scheduler.scheduleWithFixedDelay(this::refreshData, 0, refreshInterval, TimeUnit.SECONDS);
341         }
342         if (queueConsumerThread == null || !queueConsumerThread.isAlive()) {
343             queueConsumerThread = new Thread(new QueueConsumer(commandQueue), "max-queue-consumer");
344             queueConsumerThread.setDaemon(true);
345             queueConsumerThread.start();
346         }
347     }
348
349     /**
350      * stops the refreshing jobs
351      *
352      * @throws InterruptedException
353      */
354     private void stopAutomaticRefresh() throws InterruptedException {
355         if (pollingJob != null && !pollingJob.isCancelled()) {
356             pollingJob.cancel(true);
357             pollingJob = null;
358         }
359
360         if (queueConsumerThread != null && queueConsumerThread.isAlive()) {
361             queueConsumerThread.interrupt();
362             queueConsumerThread.join(1000);
363         }
364     }
365
366     public class QueueConsumer implements Runnable {
367         private final BlockingQueue<SendCommand> commandQueue;
368
369         public QueueConsumer(final BlockingQueue<SendCommand> commandQueue) {
370             this.commandQueue = commandQueue;
371         }
372
373         /**
374          * Keeps taking commands from the command queue and send it to
375          * {@link sendCubeCommand} for execution.
376          */
377         @Override
378         public void run() {
379             try {
380                 while (!Thread.currentThread().isInterrupted()) {
381                     waitForNormalDutyCycle();
382                     final SendCommand sendCommand = commandQueue.take();
383                     CubeCommand cmd = sendCommand.getCubeCommand();
384                     if (cmd == null) {
385                         cmd = getCommand(sendCommand);
386                     }
387                     if (cmd != null) {
388                         // Actual sending of the data to the Max! Cube Lan Gateway
389                         logger.debug("Command {} sent to MAX! Cube at IP: {}", sendCommand, ipAddress);
390
391                         if (sendCubeCommand(cmd)) {
392                             logger.trace("Command {} completed for MAX! Cube at IP: {}", sendCommand, ipAddress);
393                         } else {
394                             logger.debug("Error sending command {} to MAX! Cube at IP: {}", sendCommand, ipAddress);
395                         }
396                     }
397                     Thread.sleep(5000);
398                 }
399             } catch (InterruptedException e) {
400                 logger.debug("Stopping queueConsumer");
401             } catch (Exception e) {
402                 logger.error("Unexpected exception occurred during run of queueConsumer", e);
403             }
404         }
405
406         private void waitForNormalDutyCycle() throws InterruptedException {
407             dutyCycleLock.lock();
408             try {
409                 while (hasExcessDutyCycle()) {
410                     try {
411                         if (socket != null && !socket.isClosed()) {
412                             socket.close();
413                         }
414                     } catch (IOException e) {
415                         logger.debug("Could not close socket", e);
416                     }
417                     logger.debug("Found to have excess duty cycle, waiting for better times...");
418                     excessDutyCycle.await(1, TimeUnit.MINUTES);
419                 }
420             } finally {
421                 dutyCycleLock.unlock();
422             }
423         }
424     }
425
426     /**
427      * Processes device command and sends it to the MAX! Cube Lan Gateway.
428      *
429      * @param {@link SendCommand}
430      *            the SendCommand containing the serial number of the device as
431      *            String the channelUID used to send the command and the the
432      *            command data
433      */
434     private CubeCommand getCommand(SendCommand sendCommand) {
435         String serialNumber = sendCommand.getDeviceSerial();
436         ChannelUID channelUID = sendCommand.getChannelUID();
437         Command command = sendCommand.getCommand();
438
439         // send command to MAX! Cube LAN Gateway
440         HeatingThermostat device = (HeatingThermostat) getDevice(serialNumber, devices);
441         if (device == null) {
442             logger.debug("Cannot send command to device with serial number '{}', device not listed.", serialNumber);
443             return null;
444         }
445
446         // Temperature setting
447         if (channelUID.getId().equals(CHANNEL_SETTEMP)) {
448             if (command instanceof QuantityType || command instanceof OnOffType) {
449                 double setTemp = DEFAULT_OFF_TEMPERATURE;
450                 if (command instanceof QuantityType) {
451                     setTemp = ((QuantityType<Temperature>) command).toUnit(CELSIUS).toBigDecimal()
452                             .setScale(1, RoundingMode.HALF_UP).doubleValue();
453                 } else if (command instanceof OnOffType) {
454                     setTemp = OnOffType.ON.equals(command) ? DEFAULT_ON_TEMPERATURE : DEFAULT_OFF_TEMPERATURE;
455                 }
456                 return new SCommand(device.getRFAddress(), device.getRoomId(), device.getMode(), setTemp);
457             }
458             // Mode setting
459         } else if (channelUID.getId().equals(CHANNEL_MODE)) {
460             if (command instanceof StringType) {
461                 String commandContent = command.toString().trim().toUpperCase();
462                 double setTemp = device.getTemperatureSetpoint();
463                 if (commandContent.contentEquals(ThermostatModeType.AUTOMATIC.toString())) {
464                     device.setMode(ThermostatModeType.AUTOMATIC);
465                     return new SCommand(device.getRFAddress(), device.getRoomId(), ThermostatModeType.AUTOMATIC, 0D);
466                 } else if (commandContent.contentEquals(ThermostatModeType.BOOST.toString())) {
467                     device.setMode(ThermostatModeType.BOOST);
468                     return new SCommand(device.getRFAddress(), device.getRoomId(), ThermostatModeType.BOOST, setTemp);
469                 } else if (commandContent.contentEquals(ThermostatModeType.MANUAL.toString())) {
470                     device.setMode(ThermostatModeType.MANUAL);
471                     logger.debug("updates to MANUAL mode with temperature '{}'", setTemp);
472                     return new SCommand(device.getRFAddress(), device.getRoomId(), ThermostatModeType.MANUAL, setTemp);
473                 } else {
474                     logger.debug("Only updates to AUTOMATIC & BOOST & MANUAL supported, received value: '{}'",
475                             commandContent);
476                 }
477             }
478         }
479         return null;
480     }
481
482     /**
483      * initiates read data from the MAX! Cube bridge
484      */
485     private void refreshData() {
486         try {
487             if (sendCubeCommand(new LCommand())) {
488                 updateStatus(ThingStatus.ONLINE);
489                 previousOnline = true;
490                 for (Device di : devices) {
491                     if (lastActiveDevices != null && lastActiveDevices.contains(di.getSerialNumber())) {
492                         for (DeviceStatusListener deviceStatusListener : deviceStatusListeners) {
493                             try {
494                                 deviceStatusListener.onDeviceStateChanged(getThing().getUID(), di);
495                             } catch (Exception e) {
496                                 logger.error("An exception occurred while calling the DeviceStatusListener", e);
497                                 unregisterDeviceStatusListener(deviceStatusListener);
498                             }
499                         }
500                     }
501                     // New device, not seen before, pass to Discovery
502                     else {
503                         for (DeviceStatusListener deviceStatusListener : deviceStatusListeners) {
504                             try {
505                                 deviceStatusListener.onDeviceAdded(getThing(), di);
506                                 di.setUpdated(true);
507                                 deviceStatusListener.onDeviceStateChanged(getThing().getUID(), di);
508                             } catch (Exception e) {
509                                 logger.error("An exception occurred while calling the DeviceStatusListener", e);
510                             }
511                             lastActiveDevices.add(di.getSerialNumber());
512                         }
513                     }
514                 }
515             } else if (previousOnline) {
516                 onConnectionLost();
517             }
518
519         } catch (Exception e) {
520             logger.debug("Unexpected exception occurred during execution: {}", e.getMessage(), e);
521         }
522     }
523
524     public void onConnectionLost() {
525         logger.debug("Bridge connection lost. Updating thing status to OFFLINE.");
526         previousOnline = false;
527         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR);
528         clearDeviceList();
529     }
530
531     public void onConnection() {
532         logger.debug("Bridge connected. Updating thing status to ONLINE.");
533         updateStatus(ThingStatus.ONLINE);
534     }
535
536     public boolean registerDeviceStatusListener(DeviceStatusListener deviceStatusListener) {
537         if (deviceStatusListener == null) {
538             throw new IllegalArgumentException("It's not allowed to pass a null deviceStatusListener.");
539         }
540         return deviceStatusListeners.add(deviceStatusListener);
541     }
542
543     public boolean unregisterDeviceStatusListener(DeviceStatusListener deviceStatusListener) {
544         if (deviceStatusListener == null) {
545             throw new IllegalArgumentException("It's not allowed to pass a null deviceStatusListener.");
546         }
547         boolean result = deviceStatusListeners.remove(deviceStatusListener);
548         if (result) {
549             clearDeviceList();
550         }
551         return result;
552     }
553
554     public void clearDeviceList() {
555         lastActiveDevices.clear();
556     }
557
558     /**
559      * Connects to the Max! Cube Lan gateway and send a command to Cube
560      * and process the message
561      *
562      * @param {@link CubeCommand}
563      * @return boolean success
564      */
565     private synchronized boolean sendCubeCommand(CubeCommand command) {
566         try {
567             if (socket == null || socket.isClosed()) {
568                 this.socketConnect();
569             } else if (maxRequestsPerConnection > 0 && requestCount >= maxRequestsPerConnection) {
570                 logger.debug("maxRequestsPerConnection reached, reconnecting.");
571                 socket.close();
572                 this.socketConnect();
573             }
574
575             if (requestCount == 0) {
576                 logger.debug("Connect to MAX! Cube");
577                 readLines("L:");
578             }
579             if (!(requestCount == 0 && command instanceof LCommand)) {
580                 logger.debug("Sending request #{} to MAX! Cube", this.requestCount);
581                 if (writer == null) {
582                     logger.warn("Can't write to MAX! Cube");
583                     this.socketConnect();
584                 }
585
586                 writer.write(command.getCommandString());
587                 logger.trace("Write string to Max! Cube {}: {}", ipAddress, command.getCommandString());
588                 writer.flush();
589                 if (!command.getReturnStrings().isEmpty()) {
590                     readLines(command.getReturnStrings());
591                 } else {
592                     socketClose();
593                 }
594             }
595
596             requestCount++;
597             return true;
598
599         } catch (ConnectException e) {
600             logger.debug("Connection timed out on {} port {}", ipAddress, port);
601             socketClose(); // reconnect on next execution
602             return false;
603         } catch (UnknownHostException e) {
604             logger.debug("Host error occurred during execution: {}", e.getMessage());
605             socketClose(); // reconnect on next execution
606             return false;
607         } catch (IOException e) {
608             logger.debug("IO error occurred during execution: {}", e.getMessage());
609             socketClose(); // reconnect on next execution
610             return false;
611         } catch (Exception e) {
612             logger.debug("Exception occurred during execution", e);
613             socketClose(); // reconnect on next execution
614             return false;
615         } finally {
616             if (!exclusive) {
617                 socketClose();
618             }
619         }
620     }
621
622     /**
623      * Read line from the Cube and process the message.
624      *
625      * @param terminator String with ending messagetype e.g. L:
626      * @throws IOException
627      */
628     private void readLines(String terminator) throws IOException {
629         if (terminator == null) {
630             return;
631         }
632         boolean cont = true;
633         while (cont) {
634             String raw = reader.readLine();
635             if (raw != null) {
636                 if (backup != BackupState.NO_BACKUP) {
637                     backupUtil.buildBackup(raw);
638                 }
639                 logger.trace("message block: '{}'", raw);
640                 try {
641                     this.messageProcessor.addReceivedLine(raw);
642                     if (this.messageProcessor.isMessageAvailable()) {
643                         Message message = this.messageProcessor.pull();
644                         processMessage(message);
645
646                     }
647                 } catch (UnprocessableMessageException e) {
648                     if (raw.contentEquals("M:")) {
649                         logger.info("No Rooms information found. Configure your MAX! Cube: {}", ipAddress);
650                         this.messageProcessor.reset();
651                     } else {
652                         logger.info("Message could not be processed: '{}' from MAX! Cube lan gateway: {}:", raw,
653                                 ipAddress);
654                         this.messageProcessor.reset();
655                     }
656                 } catch (Exception e) {
657                     logger.debug("Error while handling message block: '{}' from MAX! Cube lan gateway: {}: {}", raw,
658                             ipAddress, e.getMessage(), e);
659                     this.messageProcessor.reset();
660                 }
661                 if (raw.startsWith(terminator)) {
662                     cont = false;
663                 }
664             } else {
665                 cont = false;
666             }
667         }
668     }
669
670     /**
671      * Processes the message
672      *
673      * @param Message
674      *            the decoded message data
675      */
676     private void processMessage(Message message) {
677         if (message == null) {
678             return;
679         }
680
681         message.debug(logger);
682         switch (message.getType()) {
683             case A:
684                 // Nothing to do with A Messages.
685                 break;
686             case C:
687                 processCMessage((CMessage) message);
688                 break;
689             case F:
690                 setProperties((FMessage) message);
691                 break;
692             case H:
693                 processHMessage((HMessage) message);
694                 if (backup == BackupState.REQUESTED) {
695                     backup = BackupState.IN_PROGRESS;
696                 }
697                 break;
698             case L:
699                 ((LMessage) message).updateDevices(devices, configurations);
700                 logger.trace("{} devices found.", devices.size());
701                 if (backup == BackupState.IN_PROGRESS) {
702                     backup = BackupState.NO_BACKUP;
703                 }
704                 break;
705             case M:
706                 processMMessage((MMessage) message);
707                 break;
708             case N:
709                 processNMessage((NMessage) message);
710                 break;
711             case S:
712                 processSMessage((SMessage) message);
713                 break;
714             default:
715                 break;
716         }
717     }
718
719     private void processCMessage(CMessage cMessage) {
720         DeviceConfiguration c = null;
721         for (DeviceConfiguration conf : configurations) {
722             if (conf.getSerialNumber().equalsIgnoreCase(cMessage.getSerialNumber())) {
723                 c = conf;
724                 break;
725             }
726         }
727
728         if (c == null) {
729             configurations.add(DeviceConfiguration.create(cMessage));
730         } else {
731             c.setValues(cMessage);
732             Device di = getDevice(cMessage.getSerialNumber());
733             if (di != null) {
734                 di.setProperties(cMessage.getProperties());
735             }
736         }
737         if (exclusive) {
738             for (DeviceStatusListener deviceStatusListener : deviceStatusListeners) {
739                 try {
740                     Device di = getDevice(cMessage.getSerialNumber());
741                     if (di != null) {
742                         deviceStatusListener.onDeviceConfigUpdate(getThing(), di);
743                     }
744                 } catch (NullPointerException e) {
745                     logger.debug("Unexpected NPE cought. Please report stacktrace", e);
746                     // ignore
747                 } catch (Exception e) {
748                     logger.error("An exception occurred while calling the DeviceStatusListener", e);
749                     unregisterDeviceStatusListener(deviceStatusListener);
750                 }
751             }
752         }
753     }
754
755     private void processHMessage(HMessage hMessage) {
756         int freeMemorySlotsMsg = hMessage.getFreeMemorySlots();
757         int dutyCycleMsg = hMessage.getDutyCycle();
758         if (freeMemorySlotsMsg != freeMemorySlots || dutyCycleMsg != dutyCycle) {
759             freeMemorySlots = freeMemorySlotsMsg;
760             setDutyCycle(dutyCycleMsg);
761
762             updateCubeState();
763         }
764         if (!propertiesSet) {
765             setProperties(hMessage);
766             queueCommand(
767                     new SendCommand("Cube(" + getThing().getUID().getId() + ")", new FCommand(), "Request NTP info"));
768         }
769     }
770
771     private void processMMessage(MMessage msg) {
772         rooms = new ArrayList<>(msg.rooms);
773
774         if (!roomPropertiesSet) {
775             setProperties(msg);
776         }
777         for (DeviceInformation di : msg.devices) {
778             DeviceConfiguration c = null;
779             for (DeviceConfiguration conf : configurations) {
780                 if (conf.getSerialNumber().equalsIgnoreCase(di.getSerialNumber())) {
781                     c = conf;
782                     break;
783                 }
784             }
785
786             if (c != null) {
787                 configurations.remove(c);
788             }
789
790             c = DeviceConfiguration.create(di);
791             configurations.add(c);
792             c.setRoomId(di.getRoomId());
793             String roomName = "";
794             for (RoomInformation room : msg.rooms) {
795                 if (room.getPosition() == di.getRoomId()) {
796                     roomName = room.getName();
797                 }
798             }
799             c.setRoomName(roomName);
800         }
801     }
802
803     private void processNMessage(NMessage nMessage) {
804         if (!nMessage.getRfAddress().isEmpty()) {
805             logger.debug("New {} found. Serial: {}, rfaddress: {}", nMessage.getDeviceType(),
806                     nMessage.getSerialNumber(), nMessage.getRfAddress());
807             // Send C command to get the configuration so it will be added to discovery
808             String newSerial = nMessage.getSerialNumber();
809             queueCommand(new SendCommand(newSerial, new CCommand(nMessage.getRfAddress()), "Refresh " + newSerial));
810         }
811     }
812
813     private void processSMessage(SMessage sMessage) {
814         setDutyCycle(sMessage.getDutyCycle());
815         freeMemorySlots = sMessage.getFreeMemorySlots();
816         updateCubeState();
817         if (sMessage.isCommandDiscarded()) {
818             logger.warn("Last Send Command discarded. Duty Cycle: {}, Free Memory Slots: {}", dutyCycle,
819                     freeMemorySlots);
820         } else {
821             logger.debug("S message. Duty Cycle: {}, Free Memory Slots: {}", dutyCycle, freeMemorySlots);
822         }
823     }
824
825     private void setDutyCycle(int dutyCycleMsg) {
826         dutyCycleLock.lock();
827         try {
828             dutyCycle = dutyCycleMsg;
829             if (!hasExcessDutyCycle()) {
830                 excessDutyCycle.signalAll();
831             } else {
832                 logger.debug("Duty cycle at {}, will not release other thread", dutyCycle);
833             }
834         } finally {
835             dutyCycleLock.unlock();
836         }
837     }
838
839     /**
840      * Set the properties for this device
841      *
842      * @param HMessage
843      */
844     private void setProperties(HMessage message) {
845         try {
846             logger.debug("MAX! Cube properties update");
847             Map<String, String> properties = editProperties();
848             properties.put(Thing.PROPERTY_MODEL_ID, DeviceType.Cube.toString());
849             properties.put(Thing.PROPERTY_FIRMWARE_VERSION, message.getFirmwareVersion());
850             properties.put(Thing.PROPERTY_SERIAL_NUMBER, message.getSerialNumber());
851             properties.put(Thing.PROPERTY_VENDOR, MaxBindingConstants.PROPERTY_VENDOR_NAME);
852             updateProperties(properties);
853             if (message.getRFAddress()
854                     .equalsIgnoreCase((String) getConfig().get(MaxBindingConstants.PROPERTY_RFADDRESS))
855                     && message.getSerialNumber()
856                             .equalsIgnoreCase((String) getConfig().get(Thing.PROPERTY_SERIAL_NUMBER))) {
857                 logger.debug("MAX! Cube config already up2date.");
858             } else {
859                 Configuration configuration = editConfiguration();
860                 configuration.put(MaxBindingConstants.PROPERTY_RFADDRESS, message.getRFAddress());
861                 configuration.put(Thing.PROPERTY_SERIAL_NUMBER, message.getSerialNumber());
862                 updateConfiguration(configuration);
863                 logger.debug("MAX! Cube config updated");
864             }
865             propertiesSet = true;
866         } catch (Exception e) {
867             logger.debug("Exception occurred during property update: {}", e.getMessage(), e);
868         }
869     }
870
871     /**
872      * Set the properties for this device
873      *
874      * @param MMessage
875      */
876     private void setProperties(MMessage message) {
877         Configuration configuration = editConfiguration();
878         for (RoomInformation room : message.rooms) {
879             configuration.put("room" + Integer.toString(room.getPosition()), room.getName());
880             logger.trace("Room '{}' name='{}'", "Room" + Integer.toString(room.getPosition()), room.getName());
881         }
882         updateConfiguration(configuration);
883         logger.debug("Room properties updated");
884         roomPropertiesSet = true;
885     }
886
887     /**
888      * Set the properties for this device
889      *
890      * @param FMessage
891      */
892     private void setProperties(FMessage message) {
893         ntpServer1 = message.getNtpServer1();
894         ntpServer2 = message.getNtpServer2();
895         Configuration configuration = editConfiguration();
896         configuration.put(PROPERTY_NTP_SERVER1, ntpServer1);
897         configuration.put(PROPERTY_NTP_SERVER2, ntpServer2);
898         updateConfiguration(configuration);
899         logger.debug("NTP properties updated");
900     }
901
902     private Device getDevice(String serialNumber, List<Device> devices) {
903         for (Device device : devices) {
904             if (device.getSerialNumber().toUpperCase().equals(serialNumber)) {
905                 return device;
906             }
907         }
908         return null;
909     }
910
911     /**
912      * Returns the MAX! Device decoded during the last refreshData
913      *
914      * @param serialNumber
915      *            the serial number of the device as String
916      * @return device the {@link Device} information decoded in last refreshData
917      */
918
919     public Device getDevice(String serialNumber) {
920         return getDevice(serialNumber, devices);
921     }
922
923     /**
924      * Takes the device command and puts it on the command queue to be processed
925      * by the MAX! Cube Lan Gateway. Note that if multiple commands for the same
926      * item-channel combination are send prior that they are processed by the
927      * Max! Cube, they will be removed from the queue as they would not be
928      * meaningful. This will improve the behavior when using sliders in the GUI.
929      *
930      * @param sendCommand
931      *            the SendCommand containing the serial number of the device as
932      *            String the channelUID used to send the command and the the
933      *            command data
934      */
935     public void queueCommand(SendCommand sendCommand) {
936         if (commandQueue.offer(sendCommand)) {
937             if (lastCommandId != null && lastCommandId.getKey().equals(sendCommand.getKey())) {
938                 if (commandQueue.remove(lastCommandId)) {
939                     logger.debug("Removed Command id {} ({}) from queue. Superceeded by {}", lastCommandId.getId(),
940                             lastCommandId.getKey(), sendCommand.getId());
941                 }
942             }
943             lastCommandId = sendCommand;
944             logger.debug("Command queued id {} ({}:{}).", sendCommand.getId(), sendCommand.getKey(),
945                     sendCommand.getCommandText());
946
947         } else {
948             logger.debug("Command queued full dropping command id {} ({}).", sendCommand.getId(), sendCommand.getKey());
949         }
950     }
951
952     /**
953      * Updates the room information by sending M command
954      */
955     public void sendDeviceAndRoomNameUpdate(String comment) {
956         if (!devices.isEmpty()) {
957             SendCommand sendCommand = new SendCommand("Cube(" + getThing().getUID().getId() + ")",
958                     new MCommand(devices, rooms), comment);
959             queueCommand(sendCommand);
960         } else {
961             logger.debug("No devices to build room & device update message. Try later");
962         }
963     }
964
965     /**
966      * Delete a devices from the cube and updates the room information
967      *
968      * @param maxDeviceSerial Serial
969      */
970     public void sendDeviceDelete(String maxDeviceSerial) {
971         Device device = getDevice(maxDeviceSerial);
972         if (device != null) {
973             SendCommand sendCommand = new SendCommand(maxDeviceSerial, new TCommand(device.getRFAddress(), true),
974                     "Delete device " + maxDeviceSerial + " from Cube!");
975             queueCommand(sendCommand);
976             devices.remove(device);
977             sendDeviceAndRoomNameUpdate("Remove name entry for " + maxDeviceSerial);
978             sendCommand = new SendCommand(maxDeviceSerial, new QCommand(), "Reload Data");
979             queueCommand(sendCommand);
980         }
981     }
982
983     private void sendNtpUpdate(Map<String, Object> configurationParameters) {
984         String ntpServer1 = this.ntpServer1;
985         String ntpServer2 = this.ntpServer2;
986         for (Entry<String, Object> configurationParameter : configurationParameters.entrySet()) {
987             if (configurationParameter.getKey().equals(PROPERTY_NTP_SERVER1)) {
988                 ntpServer1 = (String) configurationParameter.getValue();
989             }
990             if (configurationParameter.getKey().equals(PROPERTY_NTP_SERVER2)) {
991                 ntpServer2 = (String) configurationParameter.getValue();
992             }
993         }
994         queueCommand(new SendCommand("Cube(" + getThing().getUID().getId() + ")", new FCommand(ntpServer1, ntpServer2),
995                 "Update NTP info"));
996     }
997
998     private boolean socketConnect() throws UnknownHostException, IOException {
999         socket = new Socket(ipAddress, port);
1000         socket.setSoTimeout((NETWORK_TIMEOUT));
1001         logger.debug("Open new connection... to {} port {}", ipAddress, port);
1002         reader = new BufferedReader(new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8));
1003         writer = new OutputStreamWriter(socket.getOutputStream(), StandardCharsets.UTF_8);
1004         requestCount = 0;
1005         return true;
1006     }
1007
1008     private void socketClose() {
1009         try {
1010             socket.close();
1011         } catch (Exception e) {
1012         }
1013         socket = null;
1014     }
1015
1016     private void updateCubeState() {
1017         updateState(new ChannelUID(getThing().getUID(), CHANNEL_FREE_MEMORY), new DecimalType(freeMemorySlots));
1018         updateState(new ChannelUID(getThing().getUID(), CHANNEL_DUTY_CYCLE), new DecimalType(dutyCycle));
1019     }
1020
1021     public boolean hasExcessDutyCycle() {
1022         return dutyCycle >= MAX_DUTY_CYCLE;
1023     }
1024
1025     public void backup() {
1026         this.backup = BackupState.REQUESTED;
1027         this.backupUtil = new MaxBackupUtils(
1028                 new Date().toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime().format(formatter));
1029         socketClose();
1030     }
1031 }