2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.max.internal.handler;
15 import static org.openhab.binding.max.internal.MaxBindingConstants.*;
16 import static org.openhab.core.library.unit.SIUnits.CELSIUS;
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;
38 import java.util.Map.Entry;
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;
50 import javax.measure.quantity.Temperature;
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;
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.
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
112 public class MaxCubeBridgeHandler extends BaseBridgeHandler {
114 private enum BackupState {
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");
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);
138 private SendCommand lastCommandId;
139 private long refreshInterval = 30;
140 private String ipAddress;
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;
150 private final MessageProcessor messageProcessor = new MessageProcessor();
151 private final ReentrantLock dutyCycleLock = new ReentrantLock();
152 private final Condition excessDutyCycle = dutyCycleLock.newCondition();
155 * Duty cycle of the cube
157 private int dutyCycle;
160 * The available memory slots of the cube
162 private int freeMemorySlots;
165 * connection socket and reader/writer for execute method
167 private Socket socket;
168 private BufferedReader reader;
169 private OutputStreamWriter writer;
171 private boolean previousOnline;
173 private final Set<DeviceStatusListener> deviceStatusListeners = new CopyOnWriteArraySet<>();
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.");
182 private ScheduledFuture<?> pollingJob;
183 private Thread queueConsumerThread;
184 private BackupState backup = BackupState.REQUESTED;
185 private MaxBackupUtils backupUtil;
187 public MaxCubeBridgeHandler(Bridge br) {
192 public void handleCommand(ChannelUID channelUID, Command command) {
193 if (command instanceof RefreshType) {
194 logger.debug("Refresh command received.");
195 refreshCache.getValue();
197 logger.warn("No bridge commands defined. Cannot process '{}'.", command);
202 public void dispose() {
203 logger.debug("Handler disposed.");
205 stopAutomaticRefresh();
206 } catch (InterruptedException e) {
207 logger.error("Could not stop automatic refresh", e);
208 Thread.currentThread().interrupt();
217 public void initialize() {
218 logger.debug("Initializing MAX! Cube bridge handler.");
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);
234 previousOnline = true; // To trigger offline in case no connection @ startup
235 backupUtil = new MaxBackupUtils();
236 startAutomaticRefresh();
240 public void handleConfigurationUpdate(Map<String, Object> configurationParameters) {
241 boolean refresh = true;
242 logger.debug("MAX! Cube {}: Configuration update received", getThing().getThingTypeUID());
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) {
254 if (configurationParameter.getKey().startsWith("action-")) {
255 if (configurationParameter.getValue().toString().equals(BUTTON_ACTION_VALUE)) {
256 if (configurationParameter.getKey().equals(ACTION_CUBE_REBOOT)) {
259 if (configurationParameter.getKey().equals(ACTION_CUBE_RESET)) {
264 configuration.put(configurationParameter.getKey(), BigDecimal.valueOf(BUTTON_NOACTION_VALUE));
266 configuration.put(configurationParameter.getKey(), configurationParameter.getValue());
270 // Persist changes and restart with new parameters
271 updateConfiguration(configuration);
275 stopAutomaticRefresh();
276 } catch (InterruptedException e) {
277 logger.error("Could not stop automatic refresh", e);
278 Thread.currentThread().interrupt();
287 public Collection<Class<? extends ThingHandlerService>> getServices() {
288 return Collections.unmodifiableSet(
289 Stream.of(MaxDeviceDiscoveryService.class, MaxCubeActions.class).collect(Collectors.toSet()));
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) {
298 deviceStatusListener.onDeviceRemoved(this, di);
299 } catch (Exception e) {
300 logger.error("An exception occurred while calling the DeviceStatusListener", e);
301 unregisterDeviceStatusListener(deviceStatusListener);
306 propertiesSet = false;
307 roomPropertiesSet = false;
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);
316 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "Rebooting");
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");
324 socket.setSoTimeout(80000);
325 if (!sendCubeCommand(new NCommand())) {
326 logger.debug("Error during Inclusion mode");
328 logger.debug("End MAX! inclusion mode");
329 socket.setSoTimeout(NETWORK_TIMEOUT);
330 } catch (SocketException e) {
331 logger.debug("Timeout during MAX! inclusion mode");
334 logger.debug("Need to be online to start inclusion mode");
338 private synchronized void startAutomaticRefresh() {
339 if (pollingJob == null || pollingJob.isCancelled()) {
340 pollingJob = scheduler.scheduleWithFixedDelay(this::refreshData, 0, refreshInterval, TimeUnit.SECONDS);
342 if (queueConsumerThread == null || !queueConsumerThread.isAlive()) {
343 queueConsumerThread = new Thread(new QueueConsumer(commandQueue), "max-queue-consumer");
344 queueConsumerThread.setDaemon(true);
345 queueConsumerThread.start();
350 * stops the refreshing jobs
352 * @throws InterruptedException
354 private void stopAutomaticRefresh() throws InterruptedException {
355 if (pollingJob != null && !pollingJob.isCancelled()) {
356 pollingJob.cancel(true);
360 if (queueConsumerThread != null && queueConsumerThread.isAlive()) {
361 queueConsumerThread.interrupt();
362 queueConsumerThread.join(1000);
366 public class QueueConsumer implements Runnable {
367 private final BlockingQueue<SendCommand> commandQueue;
369 public QueueConsumer(final BlockingQueue<SendCommand> commandQueue) {
370 this.commandQueue = commandQueue;
374 * Keeps taking commands from the command queue and send it to
375 * {@link sendCubeCommand} for execution.
380 while (!Thread.currentThread().isInterrupted()) {
381 waitForNormalDutyCycle();
382 final SendCommand sendCommand = commandQueue.take();
383 CubeCommand cmd = sendCommand.getCubeCommand();
385 cmd = getCommand(sendCommand);
388 // Actual sending of the data to the Max! Cube Lan Gateway
389 logger.debug("Command {} sent to MAX! Cube at IP: {}", sendCommand, ipAddress);
391 if (sendCubeCommand(cmd)) {
392 logger.trace("Command {} completed for MAX! Cube at IP: {}", sendCommand, ipAddress);
394 logger.debug("Error sending command {} to MAX! Cube at IP: {}", sendCommand, ipAddress);
399 } catch (InterruptedException e) {
400 logger.debug("Stopping queueConsumer");
401 } catch (Exception e) {
402 logger.error("Unexpected exception occurred during run of queueConsumer", e);
406 private void waitForNormalDutyCycle() throws InterruptedException {
407 dutyCycleLock.lock();
409 while (hasExcessDutyCycle()) {
411 if (socket != null && !socket.isClosed()) {
414 } catch (IOException e) {
415 logger.debug("Could not close socket", e);
417 logger.debug("Found to have excess duty cycle, waiting for better times...");
418 excessDutyCycle.await(1, TimeUnit.MINUTES);
421 dutyCycleLock.unlock();
427 * Processes device command and sends it to the MAX! Cube Lan Gateway.
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
434 private CubeCommand getCommand(SendCommand sendCommand) {
435 String serialNumber = sendCommand.getDeviceSerial();
436 ChannelUID channelUID = sendCommand.getChannelUID();
437 Command command = sendCommand.getCommand();
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);
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;
456 return new SCommand(device.getRFAddress(), device.getRoomId(), device.getMode(), setTemp);
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);
474 logger.debug("Only updates to AUTOMATIC & BOOST & MANUAL supported, received value: '{}'",
483 * initiates read data from the MAX! Cube bridge
485 private void refreshData() {
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) {
494 deviceStatusListener.onDeviceStateChanged(getThing().getUID(), di);
495 } catch (Exception e) {
496 logger.error("An exception occurred while calling the DeviceStatusListener", e);
497 unregisterDeviceStatusListener(deviceStatusListener);
501 // New device, not seen before, pass to Discovery
503 for (DeviceStatusListener deviceStatusListener : deviceStatusListeners) {
505 deviceStatusListener.onDeviceAdded(getThing(), di);
507 deviceStatusListener.onDeviceStateChanged(getThing().getUID(), di);
508 } catch (Exception e) {
509 logger.error("An exception occurred while calling the DeviceStatusListener", e);
511 lastActiveDevices.add(di.getSerialNumber());
515 } else if (previousOnline) {
519 } catch (Exception e) {
520 logger.debug("Unexpected exception occurred during execution: {}", e.getMessage(), e);
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);
531 public void onConnection() {
532 logger.debug("Bridge connected. Updating thing status to ONLINE.");
533 updateStatus(ThingStatus.ONLINE);
536 public boolean registerDeviceStatusListener(DeviceStatusListener deviceStatusListener) {
537 if (deviceStatusListener == null) {
538 throw new IllegalArgumentException("It's not allowed to pass a null deviceStatusListener.");
540 return deviceStatusListeners.add(deviceStatusListener);
543 public boolean unregisterDeviceStatusListener(DeviceStatusListener deviceStatusListener) {
544 if (deviceStatusListener == null) {
545 throw new IllegalArgumentException("It's not allowed to pass a null deviceStatusListener.");
547 boolean result = deviceStatusListeners.remove(deviceStatusListener);
554 public void clearDeviceList() {
555 lastActiveDevices.clear();
559 * Connects to the Max! Cube Lan gateway and send a command to Cube
560 * and process the message
562 * @param {@link CubeCommand}
563 * @return boolean success
565 private synchronized boolean sendCubeCommand(CubeCommand command) {
567 if (socket == null || socket.isClosed()) {
568 this.socketConnect();
569 } else if (maxRequestsPerConnection > 0 && requestCount >= maxRequestsPerConnection) {
570 logger.debug("maxRequestsPerConnection reached, reconnecting.");
572 this.socketConnect();
575 if (requestCount == 0) {
576 logger.debug("Connect to MAX! Cube");
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();
586 writer.write(command.getCommandString());
587 logger.trace("Write string to Max! Cube {}: {}", ipAddress, command.getCommandString());
589 if (!command.getReturnStrings().isEmpty()) {
590 readLines(command.getReturnStrings());
599 } catch (ConnectException e) {
600 logger.debug("Connection timed out on {} port {}", ipAddress, port);
601 socketClose(); // reconnect on next execution
603 } catch (UnknownHostException e) {
604 logger.debug("Host error occurred during execution: {}", e.getMessage());
605 socketClose(); // reconnect on next execution
607 } catch (IOException e) {
608 logger.debug("IO error occurred during execution: {}", e.getMessage());
609 socketClose(); // reconnect on next execution
611 } catch (Exception e) {
612 logger.debug("Exception occurred during execution", e);
613 socketClose(); // reconnect on next execution
623 * Read line from the Cube and process the message.
625 * @param terminator String with ending messagetype e.g. L:
626 * @throws IOException
628 private void readLines(String terminator) throws IOException {
629 if (terminator == null) {
634 String raw = reader.readLine();
636 if (backup != BackupState.NO_BACKUP) {
637 backupUtil.buildBackup(raw);
639 logger.trace("message block: '{}'", raw);
641 this.messageProcessor.addReceivedLine(raw);
642 if (this.messageProcessor.isMessageAvailable()) {
643 Message message = this.messageProcessor.pull();
644 processMessage(message);
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();
652 logger.info("Message could not be processed: '{}' from MAX! Cube lan gateway: {}:", raw,
654 this.messageProcessor.reset();
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();
661 if (raw.startsWith(terminator)) {
671 * Processes the message
674 * the decoded message data
676 private void processMessage(Message message) {
677 if (message == null) {
681 message.debug(logger);
682 switch (message.getType()) {
684 // Nothing to do with A Messages.
687 processCMessage((CMessage) message);
690 setProperties((FMessage) message);
693 processHMessage((HMessage) message);
694 if (backup == BackupState.REQUESTED) {
695 backup = BackupState.IN_PROGRESS;
699 ((LMessage) message).updateDevices(devices, configurations);
700 logger.trace("{} devices found.", devices.size());
701 if (backup == BackupState.IN_PROGRESS) {
702 backup = BackupState.NO_BACKUP;
706 processMMessage((MMessage) message);
709 processNMessage((NMessage) message);
712 processSMessage((SMessage) message);
719 private void processCMessage(CMessage cMessage) {
720 DeviceConfiguration c = null;
721 for (DeviceConfiguration conf : configurations) {
722 if (conf.getSerialNumber().equalsIgnoreCase(cMessage.getSerialNumber())) {
729 configurations.add(DeviceConfiguration.create(cMessage));
731 c.setValues(cMessage);
732 Device di = getDevice(cMessage.getSerialNumber());
734 di.setProperties(cMessage.getProperties());
738 for (DeviceStatusListener deviceStatusListener : deviceStatusListeners) {
740 Device di = getDevice(cMessage.getSerialNumber());
742 deviceStatusListener.onDeviceConfigUpdate(getThing(), di);
744 } catch (NullPointerException e) {
745 logger.debug("Unexpected NPE cought. Please report stacktrace", e);
747 } catch (Exception e) {
748 logger.error("An exception occurred while calling the DeviceStatusListener", e);
749 unregisterDeviceStatusListener(deviceStatusListener);
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);
764 if (!propertiesSet) {
765 setProperties(hMessage);
767 new SendCommand("Cube(" + getThing().getUID().getId() + ")", new FCommand(), "Request NTP info"));
771 private void processMMessage(MMessage msg) {
772 rooms = new ArrayList<>(msg.rooms);
774 if (!roomPropertiesSet) {
777 for (DeviceInformation di : msg.devices) {
778 DeviceConfiguration c = null;
779 for (DeviceConfiguration conf : configurations) {
780 if (conf.getSerialNumber().equalsIgnoreCase(di.getSerialNumber())) {
787 configurations.remove(c);
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();
799 c.setRoomName(roomName);
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));
813 private void processSMessage(SMessage sMessage) {
814 setDutyCycle(sMessage.getDutyCycle());
815 freeMemorySlots = sMessage.getFreeMemorySlots();
817 if (sMessage.isCommandDiscarded()) {
818 logger.warn("Last Send Command discarded. Duty Cycle: {}, Free Memory Slots: {}", dutyCycle,
821 logger.debug("S message. Duty Cycle: {}, Free Memory Slots: {}", dutyCycle, freeMemorySlots);
825 private void setDutyCycle(int dutyCycleMsg) {
826 dutyCycleLock.lock();
828 dutyCycle = dutyCycleMsg;
829 if (!hasExcessDutyCycle()) {
830 excessDutyCycle.signalAll();
832 logger.debug("Duty cycle at {}, will not release other thread", dutyCycle);
835 dutyCycleLock.unlock();
840 * Set the properties for this device
844 private void setProperties(HMessage message) {
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.");
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");
865 propertiesSet = true;
866 } catch (Exception e) {
867 logger.debug("Exception occurred during property update: {}", e.getMessage(), e);
872 * Set the properties for this device
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());
882 updateConfiguration(configuration);
883 logger.debug("Room properties updated");
884 roomPropertiesSet = true;
888 * Set the properties for this device
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");
902 private Device getDevice(String serialNumber, List<Device> devices) {
903 for (Device device : devices) {
904 if (device.getSerialNumber().toUpperCase().equals(serialNumber)) {
912 * Returns the MAX! Device decoded during the last refreshData
914 * @param serialNumber
915 * the serial number of the device as String
916 * @return device the {@link Device} information decoded in last refreshData
919 public Device getDevice(String serialNumber) {
920 return getDevice(serialNumber, devices);
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.
931 * the SendCommand containing the serial number of the device as
932 * String the channelUID used to send the command and the the
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());
943 lastCommandId = sendCommand;
944 logger.debug("Command queued id {} ({}:{}).", sendCommand.getId(), sendCommand.getKey(),
945 sendCommand.getCommandText());
948 logger.debug("Command queued full dropping command id {} ({}).", sendCommand.getId(), sendCommand.getKey());
953 * Updates the room information by sending M command
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);
961 logger.debug("No devices to build room & device update message. Try later");
966 * Delete a devices from the cube and updates the room information
968 * @param maxDeviceSerial Serial
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);
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();
990 if (configurationParameter.getKey().equals(PROPERTY_NTP_SERVER2)) {
991 ntpServer2 = (String) configurationParameter.getValue();
994 queueCommand(new SendCommand("Cube(" + getThing().getUID().getId() + ")", new FCommand(ntpServer1, ntpServer2),
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);
1008 private void socketClose() {
1011 } catch (Exception e) {
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));
1021 public boolean hasExcessDutyCycle() {
1022 return dutyCycle >= MAX_DUTY_CYCLE;
1025 public void backup() {
1026 this.backup = BackupState.REQUESTED;
1027 this.backupUtil = new MaxBackupUtils(
1028 new Date().toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime().format(formatter));