]> git.basschouten.com Git - openhab-addons.git/blob
47dc3355b8aed1eb4588d1e5fcb50eee9163c061
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2024 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.nikohomecontrol.internal.protocol.nhc2;
14
15 import static org.openhab.binding.nikohomecontrol.internal.protocol.NikoHomeControlConstants.*;
16
17 import java.lang.reflect.Type;
18 import java.net.InetAddress;
19 import java.security.cert.CertificateException;
20 import java.util.ArrayList;
21 import java.util.List;
22 import java.util.NoSuchElementException;
23 import java.util.Objects;
24 import java.util.Optional;
25 import java.util.concurrent.CompletableFuture;
26 import java.util.concurrent.CopyOnWriteArrayList;
27 import java.util.concurrent.ExecutionException;
28 import java.util.concurrent.ScheduledExecutorService;
29 import java.util.concurrent.TimeUnit;
30 import java.util.concurrent.TimeoutException;
31 import java.util.stream.Collectors;
32 import java.util.stream.IntStream;
33
34 import org.eclipse.jdt.annotation.NonNullByDefault;
35 import org.eclipse.jdt.annotation.Nullable;
36 import org.openhab.binding.nikohomecontrol.internal.protocol.NhcAccess;
37 import org.openhab.binding.nikohomecontrol.internal.protocol.NhcAction;
38 import org.openhab.binding.nikohomecontrol.internal.protocol.NhcAlarm;
39 import org.openhab.binding.nikohomecontrol.internal.protocol.NhcControllerEvent;
40 import org.openhab.binding.nikohomecontrol.internal.protocol.NhcMeter;
41 import org.openhab.binding.nikohomecontrol.internal.protocol.NhcThermostat;
42 import org.openhab.binding.nikohomecontrol.internal.protocol.NhcVideo;
43 import org.openhab.binding.nikohomecontrol.internal.protocol.NikoHomeControlCommunication;
44 import org.openhab.binding.nikohomecontrol.internal.protocol.NikoHomeControlConstants.AccessType;
45 import org.openhab.binding.nikohomecontrol.internal.protocol.NikoHomeControlConstants.ActionType;
46 import org.openhab.binding.nikohomecontrol.internal.protocol.NikoHomeControlConstants.MeterType;
47 import org.openhab.binding.nikohomecontrol.internal.protocol.nhc2.NhcDevice2.NhcParameter;
48 import org.openhab.binding.nikohomecontrol.internal.protocol.nhc2.NhcDevice2.NhcProperty;
49 import org.openhab.binding.nikohomecontrol.internal.protocol.nhc2.NhcDevice2.NhcTrait;
50 import org.openhab.binding.nikohomecontrol.internal.protocol.nhc2.NhcMessage2.NhcMessageParam;
51 import org.openhab.core.io.transport.mqtt.MqttConnectionObserver;
52 import org.openhab.core.io.transport.mqtt.MqttConnectionState;
53 import org.openhab.core.io.transport.mqtt.MqttException;
54 import org.openhab.core.io.transport.mqtt.MqttMessageSubscriber;
55 import org.slf4j.Logger;
56 import org.slf4j.LoggerFactory;
57
58 import com.google.gson.FieldNamingPolicy;
59 import com.google.gson.Gson;
60 import com.google.gson.GsonBuilder;
61 import com.google.gson.JsonSyntaxException;
62 import com.google.gson.reflect.TypeToken;
63
64 /**
65  * The {@link NikoHomeControlCommunication2} class is able to do the following tasks with Niko Home Control II
66  * systems:
67  * <ul>
68  * <li>Start and stop MQTT connection with Niko Home Control II Connected Controller.
69  * <li>Read all setup and status information from the Niko Home Control Controller.
70  * <li>Execute Niko Home Control commands.
71  * <li>Listen for events from Niko Home Control.
72  * </ul>
73  *
74  * @author Mark Herwege - Initial Contribution
75  */
76 @NonNullByDefault
77 public class NikoHomeControlCommunication2 extends NikoHomeControlCommunication
78         implements MqttMessageSubscriber, MqttConnectionObserver {
79
80     private final Logger logger = LoggerFactory.getLogger(NikoHomeControlCommunication2.class);
81
82     private final NhcMqttConnection2 mqttConnection;
83
84     private final List<NhcService2> services = new CopyOnWriteArrayList<>();
85
86     private volatile String profile = "";
87
88     private volatile @Nullable NhcSystemInfo2 nhcSystemInfo;
89     private volatile @Nullable NhcTimeInfo2 nhcTimeInfo;
90
91     private volatile boolean initStarted = false;
92     private volatile @Nullable CompletableFuture<Boolean> communicationStarted;
93
94     private final Gson gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.UPPER_CAMEL_CASE).create();
95
96     /**
97      * Constructor for Niko Home Control communication object, manages communication with
98      * Niko Home Control II Connected Controller.
99      *
100      * @throws CertificateException when the SSL context for MQTT communication cannot be created
101      * @throws java.net.UnknownHostException when the IP address is not provided
102      *
103      */
104     public NikoHomeControlCommunication2(NhcControllerEvent handler, String clientId,
105             ScheduledExecutorService scheduler) throws CertificateException {
106         super(handler, scheduler);
107         mqttConnection = new NhcMqttConnection2(clientId, this, this);
108     }
109
110     @Override
111     public synchronized void startCommunication() {
112         initStarted = false;
113         communicationStarted = new CompletableFuture<>();
114
115         InetAddress addr = handler.getAddr();
116         if (addr == null) {
117             logger.warn("IP address cannot be empty");
118             stopCommunication();
119             return;
120         }
121         String addrString = addr.getHostAddress();
122         int port = handler.getPort();
123         logger.debug("initializing for mqtt connection to CoCo on {}:{}", addrString, port);
124
125         profile = handler.getProfile();
126
127         String token = handler.getToken();
128         if (token.isEmpty()) {
129             logger.warn("JWT token cannot be empty");
130             stopCommunication();
131             return;
132         }
133
134         try {
135             mqttConnection.startConnection(addrString, port, profile, token);
136         } catch (MqttException e) {
137             logger.debug("error in mqtt communication");
138             handler.controllerOffline("@text/offline.communication-error");
139             scheduleRestartCommunication();
140         }
141     }
142
143     @Override
144     public synchronized void resetCommunication() {
145         CompletableFuture<Boolean> started = communicationStarted;
146         if (started != null) {
147             started.complete(false);
148         }
149         communicationStarted = null;
150         initStarted = false;
151
152         mqttConnection.stopConnection();
153     }
154
155     @Override
156     public boolean communicationActive() {
157         CompletableFuture<Boolean> started = communicationStarted;
158         if (started == null) {
159             return false;
160         }
161         try {
162             // Wait until we received all devices info to confirm we are active.
163             return started.get(5000, TimeUnit.MILLISECONDS);
164         } catch (InterruptedException | ExecutionException | TimeoutException e) {
165             logger.debug("exception waiting for connection start: {}", e.toString());
166             return false;
167         }
168     }
169
170     /**
171      * After setting up the communication with the Niko Home Control Connected Controller, send all initialization
172      * messages.
173      *
174      */
175     private synchronized void initialize() {
176         initStarted = true;
177
178         NhcMessage2 message = new NhcMessage2();
179
180         try {
181             message.method = "systeminfo.publish";
182             mqttConnection.connectionPublish(profile + "/system/cmd", gson.toJson(message));
183
184             message.method = "services.list";
185             mqttConnection.connectionPublish(profile + "/authentication/cmd", gson.toJson(message));
186
187             message.method = "devices.list";
188             mqttConnection.connectionPublish(profile + "/control/devices/cmd", gson.toJson(message));
189
190             message.method = "notifications.list";
191             mqttConnection.connectionPublish(profile + "/notification/cmd", gson.toJson(message));
192         } catch (MqttException e) {
193             initStarted = false;
194             logger.debug("error in mqtt communication during initialization");
195             resetCommunication();
196         }
197     }
198
199     private void connectionLost(String message) {
200         logger.debug("connection lost");
201         resetCommunication();
202         handler.controllerOffline(message);
203     }
204
205     private void systemEvt(String response) {
206         Type messageType = new TypeToken<NhcMessage2>() {
207         }.getType();
208         List<NhcTimeInfo2> timeInfo = null;
209         List<NhcSystemInfo2> systemInfo = null;
210         try {
211             NhcMessage2 message = gson.fromJson(response, messageType);
212             List<NhcMessageParam> messageParams = (message != null) ? message.params : null;
213             if (messageParams != null) {
214                 timeInfo = messageParams.stream().filter(p -> (p.timeInfo != null)).findFirst().get().timeInfo;
215                 systemInfo = messageParams.stream().filter(p -> (p.systemInfo != null)).findFirst().get().systemInfo;
216             }
217         } catch (JsonSyntaxException e) {
218             logger.debug("unexpected json {}", response);
219         } catch (NoSuchElementException ignore) {
220             // Ignore if timeInfo not present in response, this should not happen in a timeInfo response
221         }
222         if (timeInfo != null) {
223             nhcTimeInfo = timeInfo.get(0);
224         }
225         if (systemInfo != null) {
226             nhcSystemInfo = systemInfo.get(0);
227             handler.updatePropertiesEvent();
228         }
229     }
230
231     private void systeminfoPublishRsp(String response) {
232         Type messageType = new TypeToken<NhcMessage2>() {
233         }.getType();
234         List<NhcSystemInfo2> systemInfo = null;
235         try {
236             NhcMessage2 message = gson.fromJson(response, messageType);
237             List<NhcMessageParam> messageParams = (message != null) ? message.params : null;
238             if (messageParams != null) {
239                 systemInfo = messageParams.stream().filter(p -> (p.systemInfo != null)).findFirst().get().systemInfo;
240             }
241         } catch (JsonSyntaxException e) {
242             logger.debug("unexpected json {}", response);
243         } catch (NoSuchElementException ignore) {
244             // Ignore if systemInfo not present in response, this should not happen in a systemInfo response
245         }
246         if (systemInfo != null) {
247             nhcSystemInfo = systemInfo.get(0);
248         }
249     }
250
251     private void servicesListRsp(String response) {
252         Type messageType = new TypeToken<NhcMessage2>() {
253         }.getType();
254         List<NhcService2> serviceList = null;
255         try {
256             NhcMessage2 message = gson.fromJson(response, messageType);
257             List<NhcMessageParam> messageParams = (message != null) ? message.params : null;
258             if (messageParams != null) {
259                 serviceList = messageParams.stream().filter(p -> (p.services != null)).findFirst().get().services;
260             }
261         } catch (JsonSyntaxException e) {
262             logger.debug("unexpected json {}", response);
263         } catch (NoSuchElementException ignore) {
264             // Ignore if services not present in response, this should not happen in a services response
265         }
266         services.clear();
267         if (serviceList != null) {
268             services.addAll(serviceList);
269         }
270     }
271
272     private void devicesListRsp(String response) {
273         Type messageType = new TypeToken<NhcMessage2>() {
274         }.getType();
275         List<NhcDevice2> deviceList = null;
276         try {
277             NhcMessage2 message = gson.fromJson(response, messageType);
278             List<NhcMessageParam> messageParams = (message != null) ? message.params : null;
279             if (messageParams != null) {
280                 deviceList = messageParams.stream().filter(p -> (p.devices != null)).findFirst().get().devices;
281             }
282         } catch (JsonSyntaxException e) {
283             logger.debug("unexpected json {}", response);
284         } catch (NoSuchElementException ignore) {
285             // Ignore if devices not present in response, this should not happen in a devices response
286         }
287         if (deviceList == null) {
288             return;
289         }
290
291         for (NhcDevice2 device : deviceList) {
292             addDevice(device);
293             updateState(device);
294         }
295
296         // Once a devices list response is received, we know the communication is fully started.
297         logger.debug("Communication start complete.");
298         handler.controllerOnline();
299         CompletableFuture<Boolean> future = communicationStarted;
300         if (future != null) {
301             future.complete(true);
302         }
303     }
304
305     private void devicesEvt(String response) {
306         Type messageType = new TypeToken<NhcMessage2>() {
307         }.getType();
308         List<NhcDevice2> deviceList = null;
309         String method = null;
310         try {
311             NhcMessage2 message = gson.fromJson(response, messageType);
312             method = (message != null) ? message.method : null;
313             List<NhcMessageParam> messageParams = (message != null) ? message.params : null;
314             if (messageParams != null) {
315                 deviceList = messageParams.stream().filter(p -> (p.devices != null)).findFirst().get().devices;
316             }
317         } catch (JsonSyntaxException e) {
318             logger.debug("unexpected json {}", response);
319         } catch (NoSuchElementException ignore) {
320             // Ignore if devices not present in response, this should not happen in a devices event
321         }
322         if (deviceList == null) {
323             return;
324         }
325
326         if ("devices.removed".equals(method)) {
327             deviceList.forEach(this::removeDevice);
328             return;
329         } else if ("devices.added".equals(method)) {
330             deviceList.forEach(this::addDevice);
331         }
332
333         deviceList.forEach(this::updateState);
334     }
335
336     private void notificationEvt(String response) {
337         Type messageType = new TypeToken<NhcMessage2>() {
338         }.getType();
339         List<NhcNotification2> notificationList = null;
340         try {
341             NhcMessage2 message = gson.fromJson(response, messageType);
342             List<NhcMessageParam> messageParams = (message != null) ? message.params : null;
343             if (messageParams != null) {
344                 notificationList = messageParams.stream().filter(p -> (p.notifications != null)).findFirst()
345                         .get().notifications;
346             }
347         } catch (JsonSyntaxException e) {
348             logger.debug("unexpected json {}", response);
349         } catch (NoSuchElementException ignore) {
350             // Ignore if notifications not present in response, this should not happen in a notifications event
351         }
352         logger.debug("notifications {}", notificationList);
353         if (notificationList == null) {
354             return;
355         }
356
357         for (NhcNotification2 notification : notificationList) {
358             if ("new".equals(notification.status)) {
359                 String alarmText = notification.text;
360                 switch (notification.type) {
361                     case "alarm":
362                         handler.alarmEvent(alarmText);
363                         break;
364                     case "notification":
365                         handler.noticeEvent(alarmText);
366                         break;
367                     default:
368                         logger.debug("unexpected message type {}", notification.type);
369                 }
370             }
371         }
372     }
373
374     private void addDevice(NhcDevice2 device) {
375         String location = null;
376         List<NhcParameter> parameters = device.parameters;
377         if (parameters != null) {
378             location = parameters.stream().map(p -> p.locationName).filter(Objects::nonNull).findFirst().orElse(null);
379         }
380
381         if ("videodoorstation".equals(device.type) || "vds".equals(device.type)) {
382             addVideoDevice(device);
383         } else if ("accesscontrol".equals(device.model) || "bellbutton".equals(device.model)) {
384             addAccessDevice(device, location);
385         } else if ("alarms".equals(device.model)) {
386             addAlarmDevice(device, location);
387         } else if ("action".equals(device.type) || "virtual".equals(device.type)) {
388             addActionDevice(device, location);
389         } else if ("thermostat".equals(device.type)) {
390             addThermostatDevice(device, location);
391         } else if ("centralmeter".equals(device.type) || "energyhome".equals(device.type)) {
392             addMeterDevice(device, location);
393         } else {
394             logger.debug("device type {} and model {} not supported for {}, {}", device.type, device.model, device.uuid,
395                     device.name);
396         }
397     }
398
399     private void addActionDevice(NhcDevice2 device, @Nullable String location) {
400         ActionType actionType;
401         switch (device.model) {
402             case "generic":
403             case "pir":
404             case "simulation":
405             case "comfort":
406             case "alloff":
407             case "overallcomfort":
408             case "garagedoor":
409                 actionType = ActionType.TRIGGER;
410                 break;
411             case "light":
412             case "socket":
413             case "switched-generic":
414             case "switched-fan":
415             case "flag":
416                 actionType = ActionType.RELAY;
417                 break;
418             case "dimmer":
419                 actionType = ActionType.DIMMER;
420                 break;
421             case "rolldownshutter":
422             case "sunblind":
423             case "venetianblind":
424             case "gate":
425                 actionType = ActionType.ROLLERSHUTTER;
426                 break;
427             default:
428                 actionType = ActionType.GENERIC;
429                 logger.debug("device type {} and model {} not recognised for {}, {}, ignoring", device.type,
430                         device.model, device.uuid, device.name);
431                 return;
432         }
433
434         NhcAction nhcAction = actions.get(device.uuid);
435         if (nhcAction != null) {
436             // update name and location so discovery will see updated name and location
437             nhcAction.setName(device.name);
438             nhcAction.setLocation(location);
439         } else {
440             logger.debug("adding action device {} model {}, {}", device.uuid, device.model, device.name);
441             nhcAction = new NhcAction2(device.uuid, device.name, device.type, device.technology, device.model, location,
442                     actionType, this);
443         }
444         actions.put(device.uuid, nhcAction);
445     }
446
447     private void addThermostatDevice(NhcDevice2 device, @Nullable String location) {
448         NhcThermostat nhcThermostat = thermostats.get(device.uuid);
449         if (nhcThermostat != null) {
450             nhcThermostat.setName(device.name);
451             nhcThermostat.setLocation(location);
452         } else {
453             logger.debug("adding thermostat device {} model {}, {}", device.uuid, device.model, device.name);
454             nhcThermostat = new NhcThermostat2(device.uuid, device.name, device.type, device.technology, device.model,
455                     location, this);
456         }
457         thermostats.put(device.uuid, nhcThermostat);
458     }
459
460     private void addMeterDevice(NhcDevice2 device, @Nullable String location) {
461         NhcMeter nhcMeter = meters.get(device.uuid);
462         if (nhcMeter != null) {
463             nhcMeter.setName(device.name);
464             nhcMeter.setLocation(location);
465         } else {
466             logger.debug("adding energy meter device {} model {}, {}", device.uuid, device.model, device.name);
467             nhcMeter = new NhcMeter2(device.uuid, device.name, MeterType.ENERGY_LIVE, device.type, device.technology,
468                     device.model, null, location, this, scheduler);
469         }
470         meters.put(device.uuid, nhcMeter);
471     }
472
473     private void addAccessDevice(NhcDevice2 device, @Nullable String location) {
474         AccessType accessType = AccessType.BASE;
475         if ("bellbutton".equals(device.model)) {
476             accessType = AccessType.BELLBUTTON;
477         } else {
478             List<NhcProperty> properties = device.properties;
479             if (properties != null) {
480                 boolean hasBasicState = properties.stream().anyMatch(p -> (p.basicState != null));
481                 if (hasBasicState) {
482                     accessType = AccessType.RINGANDCOMEIN;
483                 }
484             }
485         }
486
487         NhcAccess2 nhcAccess = (NhcAccess2) accessDevices.get(device.uuid);
488         if (nhcAccess != null) {
489             nhcAccess.setName(device.name);
490             nhcAccess.setLocation(location);
491         } else {
492             String buttonId = null;
493             List<NhcParameter> parameters = device.parameters;
494             if (parameters != null) {
495                 buttonId = parameters.stream().map(p -> p.buttonId).filter(Objects::nonNull).findFirst().orElse(null);
496             }
497
498             logger.debug("adding access device {} model {} type {}, {}", device.uuid, device.model, accessType,
499                     device.name);
500             nhcAccess = new NhcAccess2(device.uuid, device.name, device.type, device.technology, device.model, location,
501                     accessType, buttonId, this);
502
503             if (buttonId != null) {
504                 NhcAccess2 access = nhcAccess;
505                 String macAddress = buttonId.split("_")[0];
506                 videoDevices.forEach((key, videoDevice) -> {
507                     if (macAddress.equals(videoDevice.getMacAddress())) {
508                         int buttonIndex = access.getButtonIndex();
509                         logger.debug("link access device {} to video device {} button {}", device.uuid,
510                                 videoDevice.getId(), buttonIndex);
511                         videoDevice.setNhcAccess(buttonIndex, access);
512                         access.setNhcVideo(videoDevice);
513                     }
514                 });
515             }
516         }
517         accessDevices.put(device.uuid, nhcAccess);
518     }
519
520     private void addVideoDevice(NhcDevice2 device) {
521         NhcVideo2 nhcVideo = (NhcVideo2) videoDevices.get(device.uuid);
522         if (nhcVideo != null) {
523             nhcVideo.setName(device.name);
524         } else {
525             String macAddress = null;
526             String ipAddress = null;
527             String mjpegUri = null;
528             String tnUri = null;
529             List<NhcTrait> traits = device.traits;
530             if (traits != null) {
531                 macAddress = traits.stream().map(t -> t.macAddress).filter(Objects::nonNull).findFirst().orElse(null);
532             }
533             List<NhcParameter> parameters = device.parameters;
534             if (parameters != null) {
535                 mjpegUri = parameters.stream().map(p -> p.mjpegUri).filter(Objects::nonNull).findFirst().orElse(null);
536                 tnUri = parameters.stream().map(p -> p.tnUri).filter(Objects::nonNull).findFirst().orElse(null);
537             }
538             List<NhcProperty> properties = device.properties;
539             if (properties != null) {
540                 ipAddress = properties.stream().map(p -> p.ipAddress).filter(Objects::nonNull).findFirst().orElse(null);
541             }
542
543             logger.debug("adding video device {} model {}, {}", device.uuid, device.model, device.name);
544             nhcVideo = new NhcVideo2(device.uuid, device.name, device.type, device.technology, device.model, macAddress,
545                     ipAddress, mjpegUri, tnUri, this);
546
547             if (macAddress != null) {
548                 NhcVideo2 video = nhcVideo;
549                 String mac = macAddress;
550                 accessDevices.forEach((key, accessDevice) -> {
551                     NhcAccess2 access = (NhcAccess2) accessDevice;
552                     String buttonMac = access.getButtonId();
553                     if (buttonMac != null) {
554                         buttonMac = buttonMac.split("_")[0];
555                         if (mac.equals(buttonMac)) {
556                             int buttonIndex = access.getButtonIndex();
557                             logger.debug("link access device {} to video device {} button {}", accessDevice.getId(),
558                                     device.uuid, buttonIndex);
559                             video.setNhcAccess(buttonIndex, access);
560                             access.setNhcVideo(video);
561                         }
562                     }
563                 });
564             }
565         }
566         videoDevices.put(device.uuid, nhcVideo);
567     }
568
569     private void addAlarmDevice(NhcDevice2 device, @Nullable String location) {
570         NhcAlarm nhcAlarm = alarmDevices.get(device.uuid);
571         if (nhcAlarm != null) {
572             nhcAlarm.setName(device.name);
573             nhcAlarm.setLocation(location);
574         } else {
575             logger.debug("adding alarm device {} model {}, {}", device.uuid, device.model, device.name);
576             nhcAlarm = new NhcAlarm2(device.uuid, device.name, device.type, device.technology, device.model, location,
577                     this);
578         }
579         alarmDevices.put(device.uuid, nhcAlarm);
580     }
581
582     private void removeDevice(NhcDevice2 device) {
583         NhcAction action = actions.get(device.uuid);
584         NhcThermostat thermostat = thermostats.get(device.uuid);
585         NhcMeter meter = meters.get(device.uuid);
586         NhcAccess access = accessDevices.get(device.uuid);
587         NhcVideo video = videoDevices.get(device.uuid);
588         NhcAlarm alarm = alarmDevices.get(device.uuid);
589         if (action != null) {
590             action.actionRemoved();
591             actions.remove(device.uuid);
592         } else if (thermostat != null) {
593             thermostat.thermostatRemoved();
594             thermostats.remove(device.uuid);
595         } else if (meter != null) {
596             meter.meterRemoved();
597             meters.remove(device.uuid);
598         } else if (access != null) {
599             access.accessDeviceRemoved();
600             accessDevices.remove(device.uuid);
601         } else if (video != null) {
602             video.videoDeviceRemoved();
603             videoDevices.remove(device.uuid);
604         } else if (alarm != null) {
605             alarm.alarmDeviceRemoved();
606             alarmDevices.remove(device.uuid);
607         }
608     }
609
610     private void updateState(NhcDevice2 device) {
611         List<NhcProperty> deviceProperties = device.properties;
612         if (deviceProperties == null) {
613             logger.debug("Cannot Update state for {} as no properties defined in device message", device.uuid);
614             return;
615         }
616
617         NhcAction action = actions.get(device.uuid);
618         NhcThermostat thermostat = thermostats.get(device.uuid);
619         NhcMeter meter = meters.get(device.uuid);
620         NhcAccess accessDevice = accessDevices.get(device.uuid);
621         NhcVideo videoDevice = videoDevices.get(device.uuid);
622         NhcAlarm alarm = alarmDevices.get(device.uuid);
623
624         if (action != null) {
625             updateActionState((NhcAction2) action, deviceProperties);
626         } else if (thermostat != null) {
627             updateThermostatState((NhcThermostat2) thermostat, deviceProperties);
628         } else if (meter != null) {
629             updateMeterState((NhcMeter2) meter, deviceProperties);
630         } else if (accessDevice != null) {
631             updateAccessState((NhcAccess2) accessDevice, deviceProperties);
632         } else if (videoDevice != null) {
633             updateVideoState((NhcVideo2) videoDevice, deviceProperties);
634         } else if (alarm != null) {
635             updateAlarmState((NhcAlarm2) alarm, deviceProperties);
636         } else {
637             logger.trace("No known device for {}", device.uuid);
638         }
639     }
640
641     private void updateActionState(NhcAction2 action, List<NhcProperty> deviceProperties) {
642         if (action.getType() == ActionType.ROLLERSHUTTER) {
643             updateRollershutterState(action, deviceProperties);
644         } else {
645             updateLightState(action, deviceProperties);
646         }
647     }
648
649     private void updateLightState(NhcAction2 action, List<NhcProperty> deviceProperties) {
650         Optional<NhcProperty> statusProperty = deviceProperties.stream().filter(p -> (p.status != null)).findFirst();
651         Optional<NhcProperty> dimmerProperty = deviceProperties.stream().filter(p -> (p.brightness != null))
652                 .findFirst();
653         Optional<NhcProperty> basicStateProperty = deviceProperties.stream().filter(p -> (p.basicState != null))
654                 .findFirst();
655
656         String booleanState = null;
657         if (statusProperty.isPresent()) {
658             booleanState = statusProperty.get().status;
659         } else if (basicStateProperty.isPresent()) {
660             booleanState = basicStateProperty.get().basicState;
661         }
662
663         if (NHCOFF.equals(booleanState) || NHCFALSE.equals(booleanState)) {
664             action.setBooleanState(false);
665             logger.debug("setting action {} internally to OFF", action.getId());
666         }
667
668         if (dimmerProperty.isPresent()) {
669             String brightness = dimmerProperty.get().brightness;
670             if (brightness != null) {
671                 try {
672                     logger.debug("setting action {} internally to {}", action.getId(), dimmerProperty.get().brightness);
673                     action.setState(Integer.parseInt(brightness));
674                 } catch (NumberFormatException e) {
675                     logger.debug("received invalid brightness value {} for dimmer {}", brightness, action.getId());
676                 }
677             }
678         }
679
680         if (NHCON.equals(booleanState) || NHCTRUE.equals(booleanState)) {
681             logger.debug("setting action {} internally to ON", action.getId());
682             action.setBooleanState(true);
683         }
684     }
685
686     private void updateRollershutterState(NhcAction2 action, List<NhcProperty> deviceProperties) {
687         deviceProperties.stream().map(p -> p.position).filter(Objects::nonNull).findFirst().ifPresent(position -> {
688             try {
689                 logger.debug("setting action {} internally to {}", action.getId(), position);
690                 action.setState(Integer.parseInt(position));
691             } catch (NumberFormatException e) {
692                 logger.trace("received empty or invalid rollershutter {} position info {}", action.getId(), position);
693             }
694         });
695     }
696
697     private void updateThermostatState(NhcThermostat2 thermostat, List<NhcProperty> deviceProperties) {
698         Optional<Boolean> overruleActiveProperty = deviceProperties.stream().map(p -> p.overruleActive)
699                 .filter(Objects::nonNull).map(t -> Boolean.parseBoolean(t)).findFirst();
700         Optional<Integer> overruleSetpointProperty = deviceProperties.stream().map(p -> p.overruleSetpoint)
701                 .map(s -> (!((s == null) || s.isEmpty())) ? Math.round(Float.parseFloat(s) * 10) : null)
702                 .filter(Objects::nonNull).findFirst();
703         Optional<Integer> overruleTimeProperty = deviceProperties.stream().map(p -> p.overruleTime)
704                 .map(s -> (!((s == null) || s.isEmpty())) ? Math.round(Float.parseFloat(s)) : null)
705                 .filter(Objects::nonNull).findFirst();
706         Optional<Integer> setpointTemperatureProperty = deviceProperties.stream().map(p -> p.setpointTemperature)
707                 .map(s -> (!((s == null) || s.isEmpty())) ? Math.round(Float.parseFloat(s) * 10) : null)
708                 .filter(Objects::nonNull).findFirst();
709         Optional<Boolean> ecoSaveProperty = deviceProperties.stream().map(p -> p.ecoSave)
710                 .map(s -> s != null ? Boolean.parseBoolean(s) : null).filter(Objects::nonNull).findFirst();
711         Optional<Integer> ambientTemperatureProperty = deviceProperties.stream().map(p -> p.ambientTemperature)
712                 .map(s -> (!((s == null) || s.isEmpty())) ? Math.round(Float.parseFloat(s) * 10) : null)
713                 .filter(Objects::nonNull).findFirst();
714         Optional<@Nullable String> demandProperty = deviceProperties.stream().map(p -> p.demand)
715                 .filter(Objects::nonNull).findFirst();
716         Optional<@Nullable String> operationModeProperty = deviceProperties.stream().map(p -> p.operationMode)
717                 .filter(Objects::nonNull).findFirst();
718
719         String modeString = deviceProperties.stream().map(p -> p.program).filter(Objects::nonNull).findFirst()
720                 .orElse("");
721         int mode = IntStream.range(0, THERMOSTATMODES.length).filter(i -> THERMOSTATMODES[i].equals(modeString))
722                 .findFirst().orElse(thermostat.getMode());
723
724         int measured = ambientTemperatureProperty.orElse(thermostat.getMeasured());
725         int setpoint = setpointTemperatureProperty.orElse(thermostat.getSetpoint());
726
727         int overrule = 0;
728         int overruletime = 0;
729         if (overruleActiveProperty.orElse(true)) {
730             overrule = overruleSetpointProperty.orElse(thermostat.getOverrule());
731             overruletime = overruleTimeProperty.orElse(thermostat.getRemainingOverruletime());
732         }
733
734         int ecosave = thermostat.getEcosave();
735         if (ecoSaveProperty.orElse(false)) {
736             ecosave = 1;
737         }
738
739         int demand = thermostat.getDemand();
740         String demandString = demandProperty.orElse(operationModeProperty.orElse(""));
741         demandString = demandString == null ? "" : demandString;
742         switch (demandString) {
743             case "None":
744                 demand = 0;
745                 break;
746             case "Heating":
747                 demand = 1;
748                 break;
749             case "Cooling":
750                 demand = -1;
751                 break;
752         }
753
754         logger.debug(
755                 "setting thermostat {} with measured {}, setpoint {}, mode {}, overrule {}, overruletime {}, ecosave {}, demand {}",
756                 thermostat.getId(), measured, setpoint, mode, overrule, overruletime, ecosave, demand);
757         thermostat.setState(measured, setpoint, mode, overrule, overruletime, ecosave, demand);
758     }
759
760     private void updateMeterState(NhcMeter2 meter, List<NhcProperty> deviceProperties) {
761         try {
762             Optional<Integer> electricalPower = deviceProperties.stream().map(p -> p.electricalPower)
763                     .map(s -> (!((s == null) || s.isEmpty())) ? Math.round(Float.parseFloat(s)) : null)
764                     .filter(Objects::nonNull).findFirst();
765             Optional<Integer> powerFromGrid = deviceProperties.stream().map(p -> p.electricalPowerFromGrid)
766                     .map(s -> (!((s == null) || s.isEmpty())) ? Math.round(Float.parseFloat(s)) : null)
767                     .filter(Objects::nonNull).findFirst();
768             Optional<Integer> powerToGrid = deviceProperties.stream().map(p -> p.electricalPowerToGrid)
769                     .map(s -> (!((s == null) || s.isEmpty())) ? Math.round(Float.parseFloat(s)) : null)
770                     .filter(Objects::nonNull).findFirst();
771             int power = electricalPower.orElse(powerFromGrid.orElse(0) - powerToGrid.orElse(0));
772             logger.trace("setting energy meter {} power to {}", meter.getId(), power);
773             meter.setPower(power);
774         } catch (NumberFormatException e) {
775             logger.trace("wrong format in energy meter {} power reading", meter.getId());
776             meter.setPower(null);
777         }
778     }
779
780     private void updateAccessState(NhcAccess2 accessDevice, List<NhcProperty> deviceProperties) {
781         Optional<NhcProperty> basicStateProperty = deviceProperties.stream().filter(p -> (p.basicState != null))
782                 .findFirst();
783         Optional<NhcProperty> doorLockProperty = deviceProperties.stream().filter(p -> (p.doorlock != null))
784                 .findFirst();
785
786         if (basicStateProperty.isPresent()) {
787             String basicState = basicStateProperty.get().basicState;
788             boolean state = false;
789             if (NHCON.equals(basicState) || NHCTRUE.equals(basicState)) {
790                 state = true;
791             }
792             switch (accessDevice.getType()) {
793                 case RINGANDCOMEIN:
794                     accessDevice.updateRingAndComeInState(state);
795                     logger.debug("setting access device {} ring and come in to {}", accessDevice.getId(), state);
796                     break;
797                 case BELLBUTTON:
798                     accessDevice.updateBellState(state);
799                     logger.debug("setting access device {} bell to {}", accessDevice.getId(), state);
800                     break;
801                 default:
802                     break;
803             }
804         }
805
806         if (doorLockProperty.isPresent()) {
807             String doorLockState = doorLockProperty.get().doorlock;
808             boolean state = false;
809             if (NHCCLOSED.equals(doorLockState)) {
810                 state = true;
811             }
812             logger.debug("setting access device {} doorlock to {}", accessDevice.getId(), state);
813             accessDevice.updateDoorLockState(state);
814         }
815     }
816
817     private void updateVideoState(NhcVideo2 videoDevice, List<NhcProperty> deviceProperties) {
818         String callStatus01 = deviceProperties.stream().map(p -> p.callStatus01).filter(Objects::nonNull).findFirst()
819                 .orElse(null);
820         String callStatus02 = deviceProperties.stream().map(p -> p.callStatus02).filter(Objects::nonNull).findFirst()
821                 .orElse(null);
822         String callStatus03 = deviceProperties.stream().map(p -> p.callStatus03).filter(Objects::nonNull).findFirst()
823                 .orElse(null);
824         String callStatus04 = deviceProperties.stream().map(p -> p.callStatus04).filter(Objects::nonNull).findFirst()
825                 .orElse(null);
826
827         logger.debug("setting video device {} call status to {}, {}, {}, {}", videoDevice.getId(), callStatus01,
828                 callStatus02, callStatus03, callStatus04);
829         videoDevice.updateState(callStatus01, callStatus02, callStatus03, callStatus04);
830     }
831
832     private void updateAlarmState(NhcAlarm2 alarmDevice, List<NhcProperty> deviceProperties) {
833         String state = deviceProperties.stream().map(p -> p.internalState).filter(Objects::nonNull).findFirst()
834                 .orElse(null);
835         if (state != null) {
836             logger.debug("setting alarm device {} state to {}", alarmDevice.getId(), state);
837             alarmDevice.setState(state);
838         }
839         String triggered = deviceProperties.stream().map(p -> p.alarmTriggered).filter(Objects::nonNull).findFirst()
840                 .orElse(null);
841         if (Boolean.valueOf(triggered)) {
842             logger.debug("triggering alarm device {}", alarmDevice.getId());
843             alarmDevice.triggerAlarm();
844         }
845     }
846
847     @Override
848     public void executeAction(String actionId, String value) {
849         NhcMessage2 message = new NhcMessage2();
850
851         message.method = "devices.control";
852         List<NhcMessageParam> params = new ArrayList<>();
853         NhcMessageParam param = new NhcMessageParam();
854         params.add(param);
855         message.params = params;
856         List<NhcDevice2> devices = new ArrayList<>();
857         NhcDevice2 device = new NhcDevice2();
858         devices.add(device);
859         param.devices = devices;
860         device.uuid = actionId;
861         List<NhcProperty> deviceProperties = new ArrayList<>();
862         NhcProperty property = new NhcProperty();
863         deviceProperties.add(property);
864         device.properties = deviceProperties;
865
866         NhcAction2 action = (NhcAction2) actions.get(actionId);
867         if (action == null) {
868             return;
869         }
870
871         switch (action.getType()) {
872             case GENERIC:
873             case TRIGGER:
874                 property.basicState = NHCTRIGGERED;
875                 break;
876             case RELAY:
877                 property.status = value;
878                 break;
879             case DIMMER:
880                 if (NHCON.equals(value)) {
881                     action.setBooleanState(true); // this will trigger sending the stored brightness value event out
882                     property.status = value;
883                 } else if (NHCOFF.equals(value)) {
884                     property.status = value;
885                 } else {
886                     try {
887                         action.setState(Integer.parseInt(value)); // set cached state to new brightness value to avoid
888                                                                   // switching on with old brightness value before
889                                                                   // updating
890                                                                   // to new value
891                     } catch (NumberFormatException e) {
892                         logger.debug("internal error, trying to set invalid brightness value {} for dimmer {}", value,
893                                 action.getId());
894                         return;
895                     }
896
897                     // If the light is off, turn the light on before sending the brightness value, needs to happen
898                     // in 2 separate messages.
899                     if (!action.booleanState()) {
900                         executeAction(actionId, NHCON);
901                     }
902                     property.brightness = value;
903                 }
904                 break;
905             case ROLLERSHUTTER:
906                 if (NHCSTOP.equals(value)) {
907                     property.action = value;
908                 } else if (NHCUP.equals(value)) {
909                     property.position = "100";
910                 } else if (NHCDOWN.equals(value)) {
911                     property.position = "0";
912                 } else {
913                     property.position = value;
914                 }
915                 break;
916         }
917
918         String topic = profile + "/control/devices/cmd";
919         String gsonMessage = gson.toJson(message);
920         sendDeviceMessage(topic, gsonMessage);
921     }
922
923     @Override
924     public void executeThermostat(String thermostatId, String mode) {
925         NhcMessage2 message = new NhcMessage2();
926
927         message.method = "devices.control";
928         List<NhcMessageParam> params = new ArrayList<>();
929         NhcMessageParam param = new NhcMessageParam();
930         params.add(param);
931         message.params = params;
932         List<NhcDevice2> devices = new ArrayList<>();
933         NhcDevice2 device = new NhcDevice2();
934         devices.add(device);
935         param.devices = devices;
936         device.uuid = thermostatId;
937         List<NhcProperty> deviceProperties = new ArrayList<>();
938
939         NhcProperty overruleActiveProp = new NhcProperty();
940         deviceProperties.add(overruleActiveProp);
941         overruleActiveProp.overruleActive = "False";
942
943         NhcProperty program = new NhcProperty();
944         deviceProperties.add(program);
945         program.program = mode;
946
947         device.properties = deviceProperties;
948
949         String topic = profile + "/control/devices/cmd";
950         String gsonMessage = gson.toJson(message);
951         sendDeviceMessage(topic, gsonMessage);
952     }
953
954     @Override
955     public void executeThermostat(String thermostatId, int overruleTemp, int overruleTime) {
956         NhcMessage2 message = new NhcMessage2();
957
958         message.method = "devices.control";
959         List<NhcMessageParam> params = new ArrayList<>();
960         NhcMessageParam param = new NhcMessageParam();
961         params.add(param);
962         message.params = params;
963         List<NhcDevice2> devices = new ArrayList<>();
964         NhcDevice2 device = new NhcDevice2();
965         devices.add(device);
966         param.devices = devices;
967         device.uuid = thermostatId;
968         List<NhcProperty> deviceProperties = new ArrayList<>();
969
970         if (overruleTime > 0) {
971             NhcProperty overruleActiveProp = new NhcProperty();
972             overruleActiveProp.overruleActive = "True";
973             deviceProperties.add(overruleActiveProp);
974
975             NhcProperty overruleSetpointProp = new NhcProperty();
976             overruleSetpointProp.overruleSetpoint = String.valueOf(overruleTemp / 10.0);
977             deviceProperties.add(overruleSetpointProp);
978
979             NhcProperty overruleTimeProp = new NhcProperty();
980             overruleTimeProp.overruleTime = String.valueOf(overruleTime);
981             deviceProperties.add(overruleTimeProp);
982         } else {
983             NhcProperty overruleActiveProp = new NhcProperty();
984             overruleActiveProp.overruleActive = "False";
985             deviceProperties.add(overruleActiveProp);
986         }
987         device.properties = deviceProperties;
988
989         String topic = profile + "/control/devices/cmd";
990         String gsonMessage = gson.toJson(message);
991         sendDeviceMessage(topic, gsonMessage);
992     }
993
994     @Override
995     public void executeMeter(String meterId) {
996         // Nothing to do, individual meter readings not supported in NHC II at this point in time
997     }
998
999     @Override
1000     public void retriggerMeterLive(String meterId) {
1001         NhcMessage2 message = new NhcMessage2();
1002
1003         message.method = "devices.control";
1004         List<NhcMessageParam> params = new ArrayList<>();
1005         NhcMessageParam param = new NhcMessageParam();
1006         params.add(param);
1007         message.params = params;
1008         List<NhcDevice2> devices = new ArrayList<>();
1009         NhcDevice2 device = new NhcDevice2();
1010         devices.add(device);
1011         param.devices = devices;
1012         device.uuid = meterId;
1013         List<NhcProperty> deviceProperties = new ArrayList<>();
1014
1015         NhcProperty reportInstantUsageProp = new NhcProperty();
1016         deviceProperties.add(reportInstantUsageProp);
1017         reportInstantUsageProp.reportInstantUsage = "True";
1018         device.properties = deviceProperties;
1019
1020         String topic = profile + "/control/devices/cmd";
1021         String gsonMessage = gson.toJson(message);
1022
1023         sendDeviceMessage(topic, gsonMessage);
1024     }
1025
1026     @Override
1027     public void executeAccessBell(String accessId) {
1028         executeAccess(accessId);
1029     }
1030
1031     @Override
1032     public void executeAccessRingAndComeIn(String accessId, boolean ringAndComeIn) {
1033         NhcAccess2 accessDevice = (NhcAccess2) accessDevices.get(accessId);
1034         if (accessDevice == null) {
1035             return;
1036         }
1037
1038         boolean current = accessDevice.getRingAndComeInState();
1039         if ((ringAndComeIn && !current) || (!ringAndComeIn && current)) {
1040             executeAccess(accessId);
1041         } else {
1042             logger.trace("Not updating ring and come in as state did not change");
1043         }
1044     }
1045
1046     private void executeAccess(String accessId) {
1047         NhcMessage2 message = new NhcMessage2();
1048
1049         message.method = "devices.control";
1050         List<NhcMessageParam> params = new ArrayList<>();
1051         NhcMessageParam param = new NhcMessageParam();
1052         params.add(param);
1053         message.params = params;
1054         List<NhcDevice2> devices = new ArrayList<>();
1055         NhcDevice2 device = new NhcDevice2();
1056         devices.add(device);
1057         param.devices = devices;
1058         device.uuid = accessId;
1059         List<NhcProperty> deviceProperties = new ArrayList<>();
1060         NhcProperty property = new NhcProperty();
1061         deviceProperties.add(property);
1062         device.properties = deviceProperties;
1063
1064         NhcAccess2 accessDevice = (NhcAccess2) accessDevices.get(accessId);
1065         if (accessDevice == null) {
1066             return;
1067         }
1068
1069         property.basicState = NHCTRIGGERED;
1070
1071         String topic = profile + "/control/devices/cmd";
1072         String gsonMessage = gson.toJson(message);
1073         sendDeviceMessage(topic, gsonMessage);
1074     }
1075
1076     @Override
1077     public void executeVideoBell(String accessId, int buttonIndex) {
1078         NhcMessage2 message = new NhcMessage2();
1079
1080         message.method = "devices.control";
1081         List<NhcMessageParam> params = new ArrayList<>();
1082         NhcMessageParam param = new NhcMessageParam();
1083         params.add(param);
1084         message.params = params;
1085         List<NhcDevice2> devices = new ArrayList<>();
1086         NhcDevice2 device = new NhcDevice2();
1087         devices.add(device);
1088         param.devices = devices;
1089         device.uuid = accessId;
1090         List<NhcProperty> deviceProperties = new ArrayList<>();
1091         NhcProperty property = new NhcProperty();
1092         deviceProperties.add(property);
1093         device.properties = deviceProperties;
1094
1095         NhcVideo videoDevice = videoDevices.get(accessId);
1096         if (videoDevice == null) {
1097             return;
1098         }
1099
1100         switch (buttonIndex) {
1101             case 1:
1102                 property.callStatus01 = NHCRINGING;
1103                 break;
1104             case 2:
1105                 property.callStatus02 = NHCRINGING;
1106                 break;
1107             case 3:
1108                 property.callStatus03 = NHCRINGING;
1109                 break;
1110             case 4:
1111                 property.callStatus04 = NHCRINGING;
1112                 break;
1113             default:
1114                 break;
1115         }
1116
1117         String topic = profile + "/control/devices/cmd";
1118         String gsonMessage = gson.toJson(message);
1119         sendDeviceMessage(topic, gsonMessage);
1120     }
1121
1122     @Override
1123     public void executeAccessUnlock(String accessId) {
1124         NhcMessage2 message = new NhcMessage2();
1125
1126         message.method = "devices.control";
1127         List<NhcMessageParam> params = new ArrayList<>();
1128         NhcMessageParam param = new NhcMessageParam();
1129         params.add(param);
1130         message.params = params;
1131         List<NhcDevice2> devices = new ArrayList<>();
1132         NhcDevice2 device = new NhcDevice2();
1133         devices.add(device);
1134         param.devices = devices;
1135         device.uuid = accessId;
1136         List<NhcProperty> deviceProperties = new ArrayList<>();
1137         NhcProperty property = new NhcProperty();
1138         deviceProperties.add(property);
1139         device.properties = deviceProperties;
1140
1141         NhcAccess2 accessDevice = (NhcAccess2) accessDevices.get(accessId);
1142         if (accessDevice == null) {
1143             return;
1144         }
1145
1146         property.doorlock = NHCOPEN;
1147
1148         String topic = profile + "/control/devices/cmd";
1149         String gsonMessage = gson.toJson(message);
1150         sendDeviceMessage(topic, gsonMessage);
1151     }
1152
1153     @Override
1154     public void executeArm(String alarmId) {
1155         executeAlarm(alarmId, NHCARM);
1156     }
1157
1158     @Override
1159     public void executeDisarm(String alarmId) {
1160         executeAlarm(alarmId, NHCDISARM);
1161     }
1162
1163     private void executeAlarm(String alarmId, String state) {
1164         NhcMessage2 message = new NhcMessage2();
1165
1166         message.method = "devices.control";
1167         List<NhcMessageParam> params = new ArrayList<>();
1168         NhcMessageParam param = new NhcMessageParam();
1169         params.add(param);
1170         message.params = params;
1171         List<NhcDevice2> devices = new ArrayList<>();
1172         NhcDevice2 device = new NhcDevice2();
1173         devices.add(device);
1174         param.devices = devices;
1175         device.uuid = alarmId;
1176         List<NhcProperty> deviceProperties = new ArrayList<>();
1177         NhcProperty property = new NhcProperty();
1178         deviceProperties.add(property);
1179         device.properties = deviceProperties;
1180
1181         NhcAlarm2 alarmDevice = (NhcAlarm2) alarmDevices.get(alarmId);
1182         if (alarmDevice == null) {
1183             return;
1184         }
1185
1186         property.control = state;
1187
1188         String topic = profile + "/control/devices/cmd";
1189         String gsonMessage = gson.toJson(message);
1190         sendDeviceMessage(topic, gsonMessage);
1191     }
1192
1193     private void sendDeviceMessage(String topic, String gsonMessage) {
1194         try {
1195             mqttConnection.connectionPublish(topic, gsonMessage);
1196
1197         } catch (MqttException e) {
1198             String message = e.getLocalizedMessage();
1199
1200             logger.debug("sending command failed, trying to restart communication");
1201             restartCommunication();
1202             // retry sending after restart
1203             try {
1204                 if (communicationActive()) {
1205                     mqttConnection.connectionPublish(topic, gsonMessage);
1206                 } else {
1207                     logger.debug("failed to restart communication");
1208                 }
1209             } catch (MqttException e1) {
1210                 message = e1.getLocalizedMessage();
1211
1212                 logger.debug("error resending device command");
1213             }
1214             if (!communicationActive()) {
1215                 message = (message != null) ? message : "@text/offline.communication-error";
1216                 connectionLost(message);
1217                 // Keep on trying to restart, but don't send message anymore
1218                 scheduleRestartCommunication();
1219             }
1220         }
1221     }
1222
1223     @Override
1224     public void processMessage(String topic, byte[] payload) {
1225         String message = new String(payload);
1226         if ((profile + "/system/evt").equals(topic)) {
1227             systemEvt(message);
1228         } else if ((profile + "/system/rsp").equals(topic)) {
1229             logger.debug("received topic {}, payload {}", topic, message);
1230             systeminfoPublishRsp(message);
1231         } else if ((profile + "/notification/evt").equals(topic)) {
1232             logger.debug("received topic {}, payload {}", topic, message);
1233             notificationEvt(message);
1234         } else if ((profile + "/control/devices/evt").equals(topic)) {
1235             logger.trace("received topic {}, payload {}", topic, message);
1236             devicesEvt(message);
1237         } else if ((profile + "/control/devices/rsp").equals(topic)) {
1238             logger.debug("received topic {}, payload {}", topic, message);
1239             devicesListRsp(message);
1240         } else if ((profile + "/authentication/rsp").equals(topic)) {
1241             logger.debug("received topic {}, payload {}", topic, message);
1242             servicesListRsp(message);
1243         } else if ((profile + "/control/devices.error").equals(topic)) {
1244             logger.warn("received error {}", message);
1245         } else {
1246             logger.trace("not acted on received message topic {}, payload {}", topic, message);
1247         }
1248     }
1249
1250     /**
1251      * @return system info retrieved from Connected Controller
1252      */
1253     public NhcSystemInfo2 getSystemInfo() {
1254         NhcSystemInfo2 systemInfo = nhcSystemInfo;
1255         if (systemInfo == null) {
1256             systemInfo = new NhcSystemInfo2();
1257         }
1258         return systemInfo;
1259     }
1260
1261     /**
1262      * @return time info retrieved from Connected Controller
1263      */
1264     public NhcTimeInfo2 getTimeInfo() {
1265         NhcTimeInfo2 timeInfo = nhcTimeInfo;
1266         if (timeInfo == null) {
1267             timeInfo = new NhcTimeInfo2();
1268         }
1269         return timeInfo;
1270     }
1271
1272     /**
1273      * @return comma separated list of services retrieved from Connected Controller
1274      */
1275     public String getServices() {
1276         return services.stream().map(NhcService2::name).collect(Collectors.joining(", "));
1277     }
1278
1279     @Override
1280     public void connectionStateChanged(MqttConnectionState state, @Nullable Throwable error) {
1281         // do in separate thread as this method needs to return early
1282         scheduler.submit(() -> {
1283             if (error != null) {
1284                 logger.debug("Connection state: {}, error", state, error);
1285                 String localizedMessage = error.getLocalizedMessage();
1286                 String message = (localizedMessage != null) ? localizedMessage : "@text/offline.communication-error";
1287                 connectionLost(message);
1288                 scheduleRestartCommunication();
1289             } else if ((state == MqttConnectionState.CONNECTED) && !initStarted) {
1290                 initialize();
1291             } else {
1292                 logger.trace("Connection state: {}", state);
1293             }
1294         });
1295     }
1296 }