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