]> git.basschouten.com Git - openhab-addons.git/blob
d307b859db2899e55831e6e24807ddd7fe1c4288
[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) && (device.properties != null)
386                 && (device.properties.stream().anyMatch(p -> (p.alarmActive != null)))) {
387             addAlarmDevice(device, location);
388         } else if ("action".equals(device.type) || "virtual".equals(device.type)) {
389             addActionDevice(device, location);
390         } else if ("thermostat".equals(device.type)) {
391             addThermostatDevice(device, location);
392         } else if ("centralmeter".equals(device.type) || "energyhome".equals(device.type)) {
393             addMeterDevice(device, location);
394         } else {
395             logger.debug("device type {} and model {} not supported for {}, {}", device.type, device.model, device.uuid,
396                     device.name);
397         }
398     }
399
400     private void addActionDevice(NhcDevice2 device, @Nullable String location) {
401         ActionType actionType;
402         switch (device.model) {
403             case "generic":
404             case "pir":
405             case "simulation":
406             case "comfort":
407             case "alarms":
408             case "alloff":
409             case "overallcomfort":
410             case "garagedoor":
411                 actionType = ActionType.TRIGGER;
412                 break;
413             case "light":
414             case "socket":
415             case "switched-generic":
416             case "switched-fan":
417             case "flag":
418                 actionType = ActionType.RELAY;
419                 break;
420             case "dimmer":
421                 actionType = ActionType.DIMMER;
422                 break;
423             case "rolldownshutter":
424             case "sunblind":
425             case "venetianblind":
426             case "gate":
427                 actionType = ActionType.ROLLERSHUTTER;
428                 break;
429             default:
430                 actionType = ActionType.GENERIC;
431                 logger.debug("device type {} and model {} not recognised for {}, {}, ignoring", device.type,
432                         device.model, device.uuid, device.name);
433                 return;
434         }
435
436         NhcAction nhcAction = actions.get(device.uuid);
437         if (nhcAction != null) {
438             // update name and location so discovery will see updated name and location
439             nhcAction.setName(device.name);
440             nhcAction.setLocation(location);
441         } else {
442             logger.debug("adding action device {} model {}, {}", device.uuid, device.model, device.name);
443             nhcAction = new NhcAction2(device.uuid, device.name, device.type, device.technology, device.model, location,
444                     actionType, this);
445         }
446         actions.put(device.uuid, nhcAction);
447     }
448
449     private void addThermostatDevice(NhcDevice2 device, @Nullable String location) {
450         NhcThermostat nhcThermostat = thermostats.get(device.uuid);
451         if (nhcThermostat != null) {
452             nhcThermostat.setName(device.name);
453             nhcThermostat.setLocation(location);
454         } else {
455             logger.debug("adding thermostat device {} model {}, {}", device.uuid, device.model, device.name);
456             nhcThermostat = new NhcThermostat2(device.uuid, device.name, device.type, device.technology, device.model,
457                     location, this);
458         }
459         thermostats.put(device.uuid, nhcThermostat);
460     }
461
462     private void addMeterDevice(NhcDevice2 device, @Nullable String location) {
463         NhcMeter nhcMeter = meters.get(device.uuid);
464         if (nhcMeter != null) {
465             nhcMeter.setName(device.name);
466             nhcMeter.setLocation(location);
467         } else {
468             logger.debug("adding energy meter device {} model {}, {}", device.uuid, device.model, device.name);
469             nhcMeter = new NhcMeter2(device.uuid, device.name, MeterType.ENERGY_LIVE, device.type, device.technology,
470                     device.model, null, location, this, scheduler);
471         }
472         meters.put(device.uuid, nhcMeter);
473     }
474
475     private void addAccessDevice(NhcDevice2 device, @Nullable String location) {
476         AccessType accessType = AccessType.BASE;
477         if ("bellbutton".equals(device.model)) {
478             accessType = AccessType.BELLBUTTON;
479         } else {
480             List<NhcProperty> properties = device.properties;
481             if (properties != null) {
482                 boolean hasBasicState = properties.stream().anyMatch(p -> (p.basicState != null));
483                 if (hasBasicState) {
484                     accessType = AccessType.RINGANDCOMEIN;
485                 }
486             }
487         }
488
489         NhcAccess2 nhcAccess = (NhcAccess2) accessDevices.get(device.uuid);
490         if (nhcAccess != null) {
491             nhcAccess.setName(device.name);
492             nhcAccess.setLocation(location);
493         } else {
494             String buttonId = null;
495             List<NhcParameter> parameters = device.parameters;
496             if (parameters != null) {
497                 buttonId = parameters.stream().map(p -> p.buttonId).filter(Objects::nonNull).findFirst().orElse(null);
498             }
499
500             logger.debug("adding access device {} model {} type {}, {}", device.uuid, device.model, accessType,
501                     device.name);
502             nhcAccess = new NhcAccess2(device.uuid, device.name, device.type, device.technology, device.model, location,
503                     accessType, buttonId, this);
504
505             if (buttonId != null) {
506                 NhcAccess2 access = nhcAccess;
507                 String macAddress = buttonId.split("_")[0];
508                 videoDevices.forEach((key, videoDevice) -> {
509                     if (macAddress.equals(videoDevice.getMacAddress())) {
510                         int buttonIndex = access.getButtonIndex();
511                         logger.debug("link access device {} to video device {} button {}", device.uuid,
512                                 videoDevice.getId(), buttonIndex);
513                         videoDevice.setNhcAccess(buttonIndex, access);
514                         access.setNhcVideo(videoDevice);
515                     }
516                 });
517             }
518         }
519         accessDevices.put(device.uuid, nhcAccess);
520     }
521
522     private void addVideoDevice(NhcDevice2 device) {
523         NhcVideo2 nhcVideo = (NhcVideo2) videoDevices.get(device.uuid);
524         if (nhcVideo != null) {
525             nhcVideo.setName(device.name);
526         } else {
527             String macAddress = null;
528             String ipAddress = null;
529             String mjpegUri = null;
530             String tnUri = null;
531             List<NhcTrait> traits = device.traits;
532             if (traits != null) {
533                 macAddress = traits.stream().map(t -> t.macAddress).filter(Objects::nonNull).findFirst().orElse(null);
534             }
535             List<NhcParameter> parameters = device.parameters;
536             if (parameters != null) {
537                 mjpegUri = parameters.stream().map(p -> p.mjpegUri).filter(Objects::nonNull).findFirst().orElse(null);
538                 tnUri = parameters.stream().map(p -> p.tnUri).filter(Objects::nonNull).findFirst().orElse(null);
539             }
540             List<NhcProperty> properties = device.properties;
541             if (properties != null) {
542                 ipAddress = properties.stream().map(p -> p.ipAddress).filter(Objects::nonNull).findFirst().orElse(null);
543             }
544
545             logger.debug("adding video device {} model {}, {}", device.uuid, device.model, device.name);
546             nhcVideo = new NhcVideo2(device.uuid, device.name, device.type, device.technology, device.model, macAddress,
547                     ipAddress, mjpegUri, tnUri, this);
548
549             if (macAddress != null) {
550                 NhcVideo2 video = nhcVideo;
551                 String mac = macAddress;
552                 accessDevices.forEach((key, accessDevice) -> {
553                     NhcAccess2 access = (NhcAccess2) accessDevice;
554                     String buttonMac = access.getButtonId();
555                     if (buttonMac != null) {
556                         buttonMac = buttonMac.split("_")[0];
557                         if (mac.equals(buttonMac)) {
558                             int buttonIndex = access.getButtonIndex();
559                             logger.debug("link access device {} to video device {} button {}", accessDevice.getId(),
560                                     device.uuid, buttonIndex);
561                             video.setNhcAccess(buttonIndex, access);
562                             access.setNhcVideo(video);
563                         }
564                     }
565                 });
566             }
567         }
568         videoDevices.put(device.uuid, nhcVideo);
569     }
570
571     private void addAlarmDevice(NhcDevice2 device, @Nullable String location) {
572         NhcAlarm nhcAlarm = alarmDevices.get(device.uuid);
573         if (nhcAlarm != null) {
574             nhcAlarm.setName(device.name);
575             nhcAlarm.setLocation(location);
576         } else {
577             logger.debug("adding alarm device {} model {}, {}", device.uuid, device.model, device.name);
578             nhcAlarm = new NhcAlarm2(device.uuid, device.name, device.type, device.technology, device.model, location,
579                     this);
580         }
581         alarmDevices.put(device.uuid, nhcAlarm);
582     }
583
584     private void removeDevice(NhcDevice2 device) {
585         NhcAction action = actions.get(device.uuid);
586         NhcThermostat thermostat = thermostats.get(device.uuid);
587         NhcMeter meter = meters.get(device.uuid);
588         NhcAccess access = accessDevices.get(device.uuid);
589         NhcVideo video = videoDevices.get(device.uuid);
590         NhcAlarm alarm = alarmDevices.get(device.uuid);
591         if (action != null) {
592             action.actionRemoved();
593             actions.remove(device.uuid);
594         } else if (thermostat != null) {
595             thermostat.thermostatRemoved();
596             thermostats.remove(device.uuid);
597         } else if (meter != null) {
598             meter.meterRemoved();
599             meters.remove(device.uuid);
600         } else if (access != null) {
601             access.accessDeviceRemoved();
602             accessDevices.remove(device.uuid);
603         } else if (video != null) {
604             video.videoDeviceRemoved();
605             videoDevices.remove(device.uuid);
606         } else if (alarm != null) {
607             alarm.alarmDeviceRemoved();
608             alarmDevices.remove(device.uuid);
609         }
610     }
611
612     private void updateState(NhcDevice2 device) {
613         List<NhcProperty> deviceProperties = device.properties;
614         if (deviceProperties == null) {
615             logger.debug("Cannot Update state for {} as no properties defined in device message", device.uuid);
616             return;
617         }
618
619         NhcAction action = actions.get(device.uuid);
620         NhcThermostat thermostat = thermostats.get(device.uuid);
621         NhcMeter meter = meters.get(device.uuid);
622         NhcAccess accessDevice = accessDevices.get(device.uuid);
623         NhcVideo videoDevice = videoDevices.get(device.uuid);
624         NhcAlarm alarm = alarmDevices.get(device.uuid);
625
626         if (action != null) {
627             updateActionState((NhcAction2) action, deviceProperties);
628         } else if (thermostat != null) {
629             updateThermostatState((NhcThermostat2) thermostat, deviceProperties);
630         } else if (meter != null) {
631             updateMeterState((NhcMeter2) meter, deviceProperties);
632         } else if (accessDevice != null) {
633             updateAccessState((NhcAccess2) accessDevice, deviceProperties);
634         } else if (videoDevice != null) {
635             updateVideoState((NhcVideo2) videoDevice, deviceProperties);
636         } else if (alarm != null) {
637             updateAlarmState((NhcAlarm2) alarm, deviceProperties);
638         } else {
639             logger.trace("No known device for {}", device.uuid);
640         }
641     }
642
643     private void updateActionState(NhcAction2 action, List<NhcProperty> deviceProperties) {
644         if (action.getType() == ActionType.ROLLERSHUTTER) {
645             updateRollershutterState(action, deviceProperties);
646         } else {
647             updateLightState(action, deviceProperties);
648         }
649     }
650
651     private void updateLightState(NhcAction2 action, List<NhcProperty> deviceProperties) {
652         Optional<NhcProperty> statusProperty = deviceProperties.stream().filter(p -> (p.status != null)).findFirst();
653         Optional<NhcProperty> dimmerProperty = deviceProperties.stream().filter(p -> (p.brightness != null))
654                 .findFirst();
655         Optional<NhcProperty> basicStateProperty = deviceProperties.stream().filter(p -> (p.basicState != null))
656                 .findFirst();
657
658         String booleanState = null;
659         if (statusProperty.isPresent()) {
660             booleanState = statusProperty.get().status;
661         } else if (basicStateProperty.isPresent()) {
662             booleanState = basicStateProperty.get().basicState;
663         }
664
665         if (NHCOFF.equals(booleanState) || NHCFALSE.equals(booleanState)) {
666             action.setBooleanState(false);
667             logger.debug("setting action {} internally to OFF", action.getId());
668         }
669
670         if (dimmerProperty.isPresent()) {
671             String brightness = dimmerProperty.get().brightness;
672             if (brightness != null) {
673                 try {
674                     logger.debug("setting action {} internally to {}", action.getId(), dimmerProperty.get().brightness);
675                     action.setState(Integer.parseInt(brightness));
676                 } catch (NumberFormatException e) {
677                     logger.debug("received invalid brightness value {} for dimmer {}", brightness, action.getId());
678                 }
679             }
680         }
681
682         if (NHCON.equals(booleanState) || NHCTRUE.equals(booleanState)) {
683             logger.debug("setting action {} internally to ON", action.getId());
684             action.setBooleanState(true);
685         }
686     }
687
688     private void updateRollershutterState(NhcAction2 action, List<NhcProperty> deviceProperties) {
689         deviceProperties.stream().map(p -> p.position).filter(Objects::nonNull).findFirst().ifPresent(position -> {
690             try {
691                 logger.debug("setting action {} internally to {}", action.getId(), position);
692                 action.setState(Integer.parseInt(position));
693             } catch (NumberFormatException e) {
694                 logger.trace("received empty or invalid rollershutter {} position info {}", action.getId(), position);
695             }
696         });
697     }
698
699     private void updateThermostatState(NhcThermostat2 thermostat, List<NhcProperty> deviceProperties) {
700         Optional<Boolean> overruleActiveProperty = deviceProperties.stream().map(p -> p.overruleActive)
701                 .filter(Objects::nonNull).map(t -> Boolean.parseBoolean(t)).findFirst();
702         Optional<Integer> overruleSetpointProperty = deviceProperties.stream().map(p -> p.overruleSetpoint)
703                 .map(s -> (!((s == null) || s.isEmpty())) ? Math.round(Float.parseFloat(s) * 10) : null)
704                 .filter(Objects::nonNull).findFirst();
705         Optional<Integer> overruleTimeProperty = deviceProperties.stream().map(p -> p.overruleTime)
706                 .map(s -> (!((s == null) || s.isEmpty())) ? Math.round(Float.parseFloat(s)) : null)
707                 .filter(Objects::nonNull).findFirst();
708         Optional<Integer> setpointTemperatureProperty = deviceProperties.stream().map(p -> p.setpointTemperature)
709                 .map(s -> (!((s == null) || s.isEmpty())) ? Math.round(Float.parseFloat(s) * 10) : null)
710                 .filter(Objects::nonNull).findFirst();
711         Optional<Boolean> ecoSaveProperty = deviceProperties.stream().map(p -> p.ecoSave)
712                 .map(s -> s != null ? Boolean.parseBoolean(s) : null).filter(Objects::nonNull).findFirst();
713         Optional<Integer> ambientTemperatureProperty = deviceProperties.stream().map(p -> p.ambientTemperature)
714                 .map(s -> (!((s == null) || s.isEmpty())) ? Math.round(Float.parseFloat(s) * 10) : null)
715                 .filter(Objects::nonNull).findFirst();
716         Optional<@Nullable String> demandProperty = deviceProperties.stream().map(p -> p.demand)
717                 .filter(Objects::nonNull).findFirst();
718         Optional<@Nullable String> operationModeProperty = deviceProperties.stream().map(p -> p.operationMode)
719                 .filter(Objects::nonNull).findFirst();
720
721         String modeString = deviceProperties.stream().map(p -> p.program).filter(Objects::nonNull).findFirst()
722                 .orElse("");
723         int mode = IntStream.range(0, THERMOSTATMODES.length).filter(i -> THERMOSTATMODES[i].equals(modeString))
724                 .findFirst().orElse(thermostat.getMode());
725
726         int measured = ambientTemperatureProperty.orElse(thermostat.getMeasured());
727         int setpoint = setpointTemperatureProperty.orElse(thermostat.getSetpoint());
728
729         int overrule = 0;
730         int overruletime = 0;
731         if (overruleActiveProperty.orElse(true)) {
732             overrule = overruleSetpointProperty.orElse(thermostat.getOverrule());
733             overruletime = overruleTimeProperty.orElse(thermostat.getRemainingOverruletime());
734         }
735
736         int ecosave = thermostat.getEcosave();
737         if (ecoSaveProperty.orElse(false)) {
738             ecosave = 1;
739         }
740
741         int demand = thermostat.getDemand();
742         String demandString = demandProperty.orElse(operationModeProperty.orElse(""));
743         demandString = demandString == null ? "" : demandString;
744         switch (demandString) {
745             case "None":
746                 demand = 0;
747                 break;
748             case "Heating":
749                 demand = 1;
750                 break;
751             case "Cooling":
752                 demand = -1;
753                 break;
754         }
755
756         logger.debug(
757                 "setting thermostat {} with measured {}, setpoint {}, mode {}, overrule {}, overruletime {}, ecosave {}, demand {}",
758                 thermostat.getId(), measured, setpoint, mode, overrule, overruletime, ecosave, demand);
759         thermostat.setState(measured, setpoint, mode, overrule, overruletime, ecosave, demand);
760     }
761
762     private void updateMeterState(NhcMeter2 meter, List<NhcProperty> deviceProperties) {
763         try {
764             Optional<Integer> electricalPower = deviceProperties.stream().map(p -> p.electricalPower)
765                     .map(s -> (!((s == null) || s.isEmpty())) ? Math.round(Float.parseFloat(s)) : null)
766                     .filter(Objects::nonNull).findFirst();
767             Optional<Integer> powerFromGrid = deviceProperties.stream().map(p -> p.electricalPowerFromGrid)
768                     .map(s -> (!((s == null) || s.isEmpty())) ? Math.round(Float.parseFloat(s)) : null)
769                     .filter(Objects::nonNull).findFirst();
770             Optional<Integer> powerToGrid = deviceProperties.stream().map(p -> p.electricalPowerToGrid)
771                     .map(s -> (!((s == null) || s.isEmpty())) ? Math.round(Float.parseFloat(s)) : null)
772                     .filter(Objects::nonNull).findFirst();
773             int power = electricalPower.orElse(powerFromGrid.orElse(0) - powerToGrid.orElse(0));
774             logger.trace("setting energy meter {} power to {}", meter.getId(), power);
775             meter.setPower(power);
776         } catch (NumberFormatException e) {
777             logger.trace("wrong format in energy meter {} power reading", meter.getId());
778             meter.setPower(null);
779         }
780     }
781
782     private void updateAccessState(NhcAccess2 accessDevice, List<NhcProperty> deviceProperties) {
783         Optional<NhcProperty> basicStateProperty = deviceProperties.stream().filter(p -> (p.basicState != null))
784                 .findFirst();
785         Optional<NhcProperty> doorLockProperty = deviceProperties.stream().filter(p -> (p.doorlock != null))
786                 .findFirst();
787
788         if (basicStateProperty.isPresent()) {
789             String basicState = basicStateProperty.get().basicState;
790             boolean state = false;
791             if (NHCON.equals(basicState) || NHCTRUE.equals(basicState)) {
792                 state = true;
793             }
794             switch (accessDevice.getType()) {
795                 case RINGANDCOMEIN:
796                     accessDevice.updateRingAndComeInState(state);
797                     logger.debug("setting access device {} ring and come in to {}", accessDevice.getId(), state);
798                     break;
799                 case BELLBUTTON:
800                     accessDevice.updateBellState(state);
801                     logger.debug("setting access device {} bell to {}", accessDevice.getId(), state);
802                     break;
803                 default:
804                     break;
805             }
806         }
807
808         if (doorLockProperty.isPresent()) {
809             String doorLockState = doorLockProperty.get().doorlock;
810             boolean state = false;
811             if (NHCCLOSED.equals(doorLockState)) {
812                 state = true;
813             }
814             logger.debug("setting access device {} doorlock to {}", accessDevice.getId(), state);
815             accessDevice.updateDoorLockState(state);
816         }
817     }
818
819     private void updateVideoState(NhcVideo2 videoDevice, List<NhcProperty> deviceProperties) {
820         String callStatus01 = deviceProperties.stream().map(p -> p.callStatus01).filter(Objects::nonNull).findFirst()
821                 .orElse(null);
822         String callStatus02 = deviceProperties.stream().map(p -> p.callStatus02).filter(Objects::nonNull).findFirst()
823                 .orElse(null);
824         String callStatus03 = deviceProperties.stream().map(p -> p.callStatus03).filter(Objects::nonNull).findFirst()
825                 .orElse(null);
826         String callStatus04 = deviceProperties.stream().map(p -> p.callStatus04).filter(Objects::nonNull).findFirst()
827                 .orElse(null);
828
829         logger.debug("setting video device {} call status to {}, {}, {}, {}", videoDevice.getId(), callStatus01,
830                 callStatus02, callStatus03, callStatus04);
831         videoDevice.updateState(callStatus01, callStatus02, callStatus03, callStatus04);
832     }
833
834     private void updateAlarmState(NhcAlarm2 alarmDevice, List<NhcProperty> deviceProperties) {
835         String state = deviceProperties.stream().map(p -> p.internalState).filter(Objects::nonNull).findFirst()
836                 .orElse(null);
837         if (state != null) {
838             logger.debug("setting alarm device {} state to {}", alarmDevice.getId(), state);
839             alarmDevice.setState(state);
840         }
841         String triggered = deviceProperties.stream().map(p -> p.alarmTriggered).filter(Objects::nonNull).findFirst()
842                 .orElse(null);
843         if (Boolean.valueOf(triggered)) {
844             logger.debug("triggering alarm device {}", alarmDevice.getId());
845             alarmDevice.triggerAlarm();
846         }
847     }
848
849     @Override
850     public void executeAction(String actionId, String value) {
851         NhcMessage2 message = new NhcMessage2();
852
853         message.method = "devices.control";
854         List<NhcMessageParam> params = new ArrayList<>();
855         NhcMessageParam param = new NhcMessageParam();
856         params.add(param);
857         message.params = params;
858         List<NhcDevice2> devices = new ArrayList<>();
859         NhcDevice2 device = new NhcDevice2();
860         devices.add(device);
861         param.devices = devices;
862         device.uuid = actionId;
863         List<NhcProperty> deviceProperties = new ArrayList<>();
864         NhcProperty property = new NhcProperty();
865         deviceProperties.add(property);
866         device.properties = deviceProperties;
867
868         NhcAction2 action = (NhcAction2) actions.get(actionId);
869         if (action == null) {
870             return;
871         }
872
873         switch (action.getType()) {
874             case GENERIC:
875             case TRIGGER:
876                 property.basicState = NHCTRIGGERED;
877                 break;
878             case RELAY:
879                 property.status = value;
880                 break;
881             case DIMMER:
882                 if (NHCON.equals(value)) {
883                     action.setBooleanState(true); // this will trigger sending the stored brightness value event out
884                     property.status = value;
885                 } else if (NHCOFF.equals(value)) {
886                     property.status = value;
887                 } else {
888                     try {
889                         action.setState(Integer.parseInt(value)); // set cached state to new brightness value to avoid
890                                                                   // switching on with old brightness value before
891                                                                   // updating
892                                                                   // to new value
893                     } catch (NumberFormatException e) {
894                         logger.debug("internal error, trying to set invalid brightness value {} for dimmer {}", value,
895                                 action.getId());
896                         return;
897                     }
898
899                     // If the light is off, turn the light on before sending the brightness value, needs to happen
900                     // in 2 separate messages.
901                     if (!action.booleanState()) {
902                         executeAction(actionId, NHCON);
903                     }
904                     property.brightness = value;
905                 }
906                 break;
907             case ROLLERSHUTTER:
908                 if (NHCSTOP.equals(value)) {
909                     property.action = value;
910                 } else if (NHCUP.equals(value)) {
911                     property.position = "100";
912                 } else if (NHCDOWN.equals(value)) {
913                     property.position = "0";
914                 } else {
915                     property.position = value;
916                 }
917                 break;
918         }
919
920         String topic = profile + "/control/devices/cmd";
921         String gsonMessage = gson.toJson(message);
922         sendDeviceMessage(topic, gsonMessage);
923     }
924
925     @Override
926     public void executeThermostat(String thermostatId, String mode) {
927         NhcMessage2 message = new NhcMessage2();
928
929         message.method = "devices.control";
930         List<NhcMessageParam> params = new ArrayList<>();
931         NhcMessageParam param = new NhcMessageParam();
932         params.add(param);
933         message.params = params;
934         List<NhcDevice2> devices = new ArrayList<>();
935         NhcDevice2 device = new NhcDevice2();
936         devices.add(device);
937         param.devices = devices;
938         device.uuid = thermostatId;
939         List<NhcProperty> deviceProperties = new ArrayList<>();
940
941         NhcProperty overruleActiveProp = new NhcProperty();
942         deviceProperties.add(overruleActiveProp);
943         overruleActiveProp.overruleActive = "False";
944
945         NhcProperty program = new NhcProperty();
946         deviceProperties.add(program);
947         program.program = mode;
948
949         device.properties = deviceProperties;
950
951         String topic = profile + "/control/devices/cmd";
952         String gsonMessage = gson.toJson(message);
953         sendDeviceMessage(topic, gsonMessage);
954     }
955
956     @Override
957     public void executeThermostat(String thermostatId, int overruleTemp, int overruleTime) {
958         NhcMessage2 message = new NhcMessage2();
959
960         message.method = "devices.control";
961         List<NhcMessageParam> params = new ArrayList<>();
962         NhcMessageParam param = new NhcMessageParam();
963         params.add(param);
964         message.params = params;
965         List<NhcDevice2> devices = new ArrayList<>();
966         NhcDevice2 device = new NhcDevice2();
967         devices.add(device);
968         param.devices = devices;
969         device.uuid = thermostatId;
970         List<NhcProperty> deviceProperties = new ArrayList<>();
971
972         if (overruleTime > 0) {
973             NhcProperty overruleActiveProp = new NhcProperty();
974             overruleActiveProp.overruleActive = "True";
975             deviceProperties.add(overruleActiveProp);
976
977             NhcProperty overruleSetpointProp = new NhcProperty();
978             overruleSetpointProp.overruleSetpoint = String.valueOf(overruleTemp / 10.0);
979             deviceProperties.add(overruleSetpointProp);
980
981             NhcProperty overruleTimeProp = new NhcProperty();
982             overruleTimeProp.overruleTime = String.valueOf(overruleTime);
983             deviceProperties.add(overruleTimeProp);
984         } else {
985             NhcProperty overruleActiveProp = new NhcProperty();
986             overruleActiveProp.overruleActive = "False";
987             deviceProperties.add(overruleActiveProp);
988         }
989         device.properties = deviceProperties;
990
991         String topic = profile + "/control/devices/cmd";
992         String gsonMessage = gson.toJson(message);
993         sendDeviceMessage(topic, gsonMessage);
994     }
995
996     @Override
997     public void executeMeter(String meterId) {
998         // Nothing to do, individual meter readings not supported in NHC II at this point in time
999     }
1000
1001     @Override
1002     public void retriggerMeterLive(String meterId) {
1003         NhcMessage2 message = new NhcMessage2();
1004
1005         message.method = "devices.control";
1006         List<NhcMessageParam> params = new ArrayList<>();
1007         NhcMessageParam param = new NhcMessageParam();
1008         params.add(param);
1009         message.params = params;
1010         List<NhcDevice2> devices = new ArrayList<>();
1011         NhcDevice2 device = new NhcDevice2();
1012         devices.add(device);
1013         param.devices = devices;
1014         device.uuid = meterId;
1015         List<NhcProperty> deviceProperties = new ArrayList<>();
1016
1017         NhcProperty reportInstantUsageProp = new NhcProperty();
1018         deviceProperties.add(reportInstantUsageProp);
1019         reportInstantUsageProp.reportInstantUsage = "True";
1020         device.properties = deviceProperties;
1021
1022         String topic = profile + "/control/devices/cmd";
1023         String gsonMessage = gson.toJson(message);
1024
1025         sendDeviceMessage(topic, gsonMessage);
1026     }
1027
1028     @Override
1029     public void executeAccessBell(String accessId) {
1030         executeAccess(accessId);
1031     }
1032
1033     @Override
1034     public void executeAccessRingAndComeIn(String accessId, boolean ringAndComeIn) {
1035         NhcAccess2 accessDevice = (NhcAccess2) accessDevices.get(accessId);
1036         if (accessDevice == null) {
1037             return;
1038         }
1039
1040         boolean current = accessDevice.getRingAndComeInState();
1041         if ((ringAndComeIn && !current) || (!ringAndComeIn && current)) {
1042             executeAccess(accessId);
1043         } else {
1044             logger.trace("Not updating ring and come in as state did not change");
1045         }
1046     }
1047
1048     private void executeAccess(String accessId) {
1049         NhcMessage2 message = new NhcMessage2();
1050
1051         message.method = "devices.control";
1052         List<NhcMessageParam> params = new ArrayList<>();
1053         NhcMessageParam param = new NhcMessageParam();
1054         params.add(param);
1055         message.params = params;
1056         List<NhcDevice2> devices = new ArrayList<>();
1057         NhcDevice2 device = new NhcDevice2();
1058         devices.add(device);
1059         param.devices = devices;
1060         device.uuid = accessId;
1061         List<NhcProperty> deviceProperties = new ArrayList<>();
1062         NhcProperty property = new NhcProperty();
1063         deviceProperties.add(property);
1064         device.properties = deviceProperties;
1065
1066         NhcAccess2 accessDevice = (NhcAccess2) accessDevices.get(accessId);
1067         if (accessDevice == null) {
1068             return;
1069         }
1070
1071         property.basicState = NHCTRIGGERED;
1072
1073         String topic = profile + "/control/devices/cmd";
1074         String gsonMessage = gson.toJson(message);
1075         sendDeviceMessage(topic, gsonMessage);
1076     }
1077
1078     @Override
1079     public void executeVideoBell(String accessId, int buttonIndex) {
1080         NhcMessage2 message = new NhcMessage2();
1081
1082         message.method = "devices.control";
1083         List<NhcMessageParam> params = new ArrayList<>();
1084         NhcMessageParam param = new NhcMessageParam();
1085         params.add(param);
1086         message.params = params;
1087         List<NhcDevice2> devices = new ArrayList<>();
1088         NhcDevice2 device = new NhcDevice2();
1089         devices.add(device);
1090         param.devices = devices;
1091         device.uuid = accessId;
1092         List<NhcProperty> deviceProperties = new ArrayList<>();
1093         NhcProperty property = new NhcProperty();
1094         deviceProperties.add(property);
1095         device.properties = deviceProperties;
1096
1097         NhcVideo videoDevice = videoDevices.get(accessId);
1098         if (videoDevice == null) {
1099             return;
1100         }
1101
1102         switch (buttonIndex) {
1103             case 1:
1104                 property.callStatus01 = NHCRINGING;
1105                 break;
1106             case 2:
1107                 property.callStatus02 = NHCRINGING;
1108                 break;
1109             case 3:
1110                 property.callStatus03 = NHCRINGING;
1111                 break;
1112             case 4:
1113                 property.callStatus04 = NHCRINGING;
1114                 break;
1115             default:
1116                 break;
1117         }
1118
1119         String topic = profile + "/control/devices/cmd";
1120         String gsonMessage = gson.toJson(message);
1121         sendDeviceMessage(topic, gsonMessage);
1122     }
1123
1124     @Override
1125     public void executeAccessUnlock(String accessId) {
1126         NhcMessage2 message = new NhcMessage2();
1127
1128         message.method = "devices.control";
1129         List<NhcMessageParam> params = new ArrayList<>();
1130         NhcMessageParam param = new NhcMessageParam();
1131         params.add(param);
1132         message.params = params;
1133         List<NhcDevice2> devices = new ArrayList<>();
1134         NhcDevice2 device = new NhcDevice2();
1135         devices.add(device);
1136         param.devices = devices;
1137         device.uuid = accessId;
1138         List<NhcProperty> deviceProperties = new ArrayList<>();
1139         NhcProperty property = new NhcProperty();
1140         deviceProperties.add(property);
1141         device.properties = deviceProperties;
1142
1143         NhcAccess2 accessDevice = (NhcAccess2) accessDevices.get(accessId);
1144         if (accessDevice == null) {
1145             return;
1146         }
1147
1148         property.doorlock = NHCOPEN;
1149
1150         String topic = profile + "/control/devices/cmd";
1151         String gsonMessage = gson.toJson(message);
1152         sendDeviceMessage(topic, gsonMessage);
1153     }
1154
1155     @Override
1156     public void executeArm(String alarmId) {
1157         executeAlarm(alarmId, NHCARM);
1158     }
1159
1160     @Override
1161     public void executeDisarm(String alarmId) {
1162         executeAlarm(alarmId, NHCDISARM);
1163     }
1164
1165     private void executeAlarm(String alarmId, String state) {
1166         NhcMessage2 message = new NhcMessage2();
1167
1168         message.method = "devices.control";
1169         List<NhcMessageParam> params = new ArrayList<>();
1170         NhcMessageParam param = new NhcMessageParam();
1171         params.add(param);
1172         message.params = params;
1173         List<NhcDevice2> devices = new ArrayList<>();
1174         NhcDevice2 device = new NhcDevice2();
1175         devices.add(device);
1176         param.devices = devices;
1177         device.uuid = alarmId;
1178         List<NhcProperty> deviceProperties = new ArrayList<>();
1179         NhcProperty property = new NhcProperty();
1180         deviceProperties.add(property);
1181         device.properties = deviceProperties;
1182
1183         NhcAlarm2 alarmDevice = (NhcAlarm2) alarmDevices.get(alarmId);
1184         if (alarmDevice == null) {
1185             return;
1186         }
1187
1188         property.control = state;
1189
1190         String topic = profile + "/control/devices/cmd";
1191         String gsonMessage = gson.toJson(message);
1192         sendDeviceMessage(topic, gsonMessage);
1193     }
1194
1195     private void sendDeviceMessage(String topic, String gsonMessage) {
1196         try {
1197             mqttConnection.connectionPublish(topic, gsonMessage);
1198
1199         } catch (MqttException e) {
1200             String message = e.getLocalizedMessage();
1201
1202             logger.debug("sending command failed, trying to restart communication");
1203             restartCommunication();
1204             // retry sending after restart
1205             try {
1206                 if (communicationActive()) {
1207                     mqttConnection.connectionPublish(topic, gsonMessage);
1208                 } else {
1209                     logger.debug("failed to restart communication");
1210                 }
1211             } catch (MqttException e1) {
1212                 message = e1.getLocalizedMessage();
1213
1214                 logger.debug("error resending device command");
1215             }
1216             if (!communicationActive()) {
1217                 message = (message != null) ? message : "@text/offline.communication-error";
1218                 connectionLost(message);
1219                 // Keep on trying to restart, but don't send message anymore
1220                 scheduleRestartCommunication();
1221             }
1222         }
1223     }
1224
1225     @Override
1226     public void processMessage(String topic, byte[] payload) {
1227         String message = new String(payload);
1228         if ((profile + "/system/evt").equals(topic)) {
1229             systemEvt(message);
1230         } else if ((profile + "/system/rsp").equals(topic)) {
1231             logger.debug("received topic {}, payload {}", topic, message);
1232             systeminfoPublishRsp(message);
1233         } else if ((profile + "/notification/evt").equals(topic)) {
1234             logger.debug("received topic {}, payload {}", topic, message);
1235             notificationEvt(message);
1236         } else if ((profile + "/control/devices/evt").equals(topic)) {
1237             logger.trace("received topic {}, payload {}", topic, message);
1238             devicesEvt(message);
1239         } else if ((profile + "/control/devices/rsp").equals(topic)) {
1240             logger.debug("received topic {}, payload {}", topic, message);
1241             devicesListRsp(message);
1242         } else if ((profile + "/authentication/rsp").equals(topic)) {
1243             logger.debug("received topic {}, payload {}", topic, message);
1244             servicesListRsp(message);
1245         } else if ((profile + "/control/devices.error").equals(topic)) {
1246             logger.warn("received error {}", message);
1247         } else {
1248             logger.trace("not acted on received message topic {}, payload {}", topic, message);
1249         }
1250     }
1251
1252     /**
1253      * @return system info retrieved from Connected Controller
1254      */
1255     public NhcSystemInfo2 getSystemInfo() {
1256         NhcSystemInfo2 systemInfo = nhcSystemInfo;
1257         if (systemInfo == null) {
1258             systemInfo = new NhcSystemInfo2();
1259         }
1260         return systemInfo;
1261     }
1262
1263     /**
1264      * @return time info retrieved from Connected Controller
1265      */
1266     public NhcTimeInfo2 getTimeInfo() {
1267         NhcTimeInfo2 timeInfo = nhcTimeInfo;
1268         if (timeInfo == null) {
1269             timeInfo = new NhcTimeInfo2();
1270         }
1271         return timeInfo;
1272     }
1273
1274     /**
1275      * @return comma separated list of services retrieved from Connected Controller
1276      */
1277     public String getServices() {
1278         return services.stream().map(NhcService2::name).collect(Collectors.joining(", "));
1279     }
1280
1281     @Override
1282     public void connectionStateChanged(MqttConnectionState state, @Nullable Throwable error) {
1283         // do in separate thread as this method needs to return early
1284         scheduler.submit(() -> {
1285             if (error != null) {
1286                 logger.debug("Connection state: {}, error", state, error);
1287                 String localizedMessage = error.getLocalizedMessage();
1288                 String message = (localizedMessage != null) ? localizedMessage : "@text/offline.communication-error";
1289                 connectionLost(message);
1290                 scheduleRestartCommunication();
1291             } else if ((state == MqttConnectionState.CONNECTED) && !initStarted) {
1292                 initialize();
1293             } else {
1294                 logger.trace("Connection state: {}", state);
1295             }
1296         });
1297     }
1298 }