]> git.basschouten.com Git - openhab-addons.git/blob
d46af6f57a7b9fd29143433e76c12630a53b4108
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2022 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.NhcAction;
37 import org.openhab.binding.nikohomecontrol.internal.protocol.NhcControllerEvent;
38 import org.openhab.binding.nikohomecontrol.internal.protocol.NhcEnergyMeter;
39 import org.openhab.binding.nikohomecontrol.internal.protocol.NhcThermostat;
40 import org.openhab.binding.nikohomecontrol.internal.protocol.NikoHomeControlCommunication;
41 import org.openhab.binding.nikohomecontrol.internal.protocol.NikoHomeControlConstants.ActionType;
42 import org.openhab.binding.nikohomecontrol.internal.protocol.nhc2.NhcDevice2.NhcParameter;
43 import org.openhab.binding.nikohomecontrol.internal.protocol.nhc2.NhcDevice2.NhcProperty;
44 import org.openhab.binding.nikohomecontrol.internal.protocol.nhc2.NhcMessage2.NhcMessageParam;
45 import org.openhab.core.io.transport.mqtt.MqttConnectionObserver;
46 import org.openhab.core.io.transport.mqtt.MqttConnectionState;
47 import org.openhab.core.io.transport.mqtt.MqttException;
48 import org.openhab.core.io.transport.mqtt.MqttMessageSubscriber;
49 import org.slf4j.Logger;
50 import org.slf4j.LoggerFactory;
51
52 import com.google.gson.FieldNamingPolicy;
53 import com.google.gson.Gson;
54 import com.google.gson.GsonBuilder;
55 import com.google.gson.JsonSyntaxException;
56 import com.google.gson.reflect.TypeToken;
57
58 /**
59  * The {@link NikoHomeControlCommunication2} class is able to do the following tasks with Niko Home Control II
60  * systems:
61  * <ul>
62  * <li>Start and stop MQTT connection with Niko Home Control II Connected Controller.
63  * <li>Read all setup and status information from the Niko Home Control Controller.
64  * <li>Execute Niko Home Control commands.
65  * <li>Listen for events from Niko Home Control.
66  * </ul>
67  *
68  * @author Mark Herwege - Initial Contribution
69  */
70 @NonNullByDefault
71 public class NikoHomeControlCommunication2 extends NikoHomeControlCommunication
72         implements MqttMessageSubscriber, MqttConnectionObserver {
73
74     private final Logger logger = LoggerFactory.getLogger(NikoHomeControlCommunication2.class);
75
76     private final NhcMqttConnection2 mqttConnection;
77
78     private final List<NhcService2> services = new CopyOnWriteArrayList<>();
79
80     private volatile String profile = "";
81
82     private volatile @Nullable NhcSystemInfo2 nhcSystemInfo;
83     private volatile @Nullable NhcTimeInfo2 nhcTimeInfo;
84
85     private volatile @Nullable CompletableFuture<Boolean> communicationStarted;
86
87     private ScheduledExecutorService scheduler;
88
89     private final Gson gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.UPPER_CAMEL_CASE).create();
90
91     /**
92      * Constructor for Niko Home Control communication object, manages communication with
93      * Niko Home Control II Connected Controller.
94      *
95      * @throws CertificateException when the SSL context for MQTT communication cannot be created
96      * @throws UnknownHostException when the IP address is not provided
97      *
98      */
99     public NikoHomeControlCommunication2(NhcControllerEvent handler, String clientId,
100             ScheduledExecutorService scheduler) throws CertificateException {
101         super(handler);
102         mqttConnection = new NhcMqttConnection2(clientId, this, this);
103         this.scheduler = scheduler;
104     }
105
106     @Override
107     public synchronized void startCommunication() {
108         communicationStarted = new CompletableFuture<>();
109
110         InetAddress addr = handler.getAddr();
111         if (addr == null) {
112             logger.warn("IP address cannot be empty");
113             stopCommunication();
114             return;
115         }
116         String addrString = addr.getHostAddress();
117         int port = handler.getPort();
118         logger.debug("initializing for mqtt connection to CoCo on {}:{}", addrString, port);
119
120         profile = handler.getProfile();
121
122         String token = handler.getToken();
123         if (token.isEmpty()) {
124             logger.warn("JWT token cannot be empty");
125             stopCommunication();
126             return;
127         }
128
129         try {
130             mqttConnection.startConnection(addrString, port, profile, token);
131             initialize();
132         } catch (MqttException e) {
133             logger.debug("error in mqtt communication");
134             stopCommunication();
135         }
136     }
137
138     @Override
139     public synchronized void stopCommunication() {
140         CompletableFuture<Boolean> started = communicationStarted;
141         if (started != null) {
142             started.complete(false);
143         }
144         communicationStarted = null;
145         mqttConnection.stopConnection();
146     }
147
148     @Override
149     public boolean communicationActive() {
150         CompletableFuture<Boolean> started = communicationStarted;
151         if (started == null) {
152             return false;
153         }
154         try {
155             // Wait until we received all devices info to confirm we are active.
156             return started.get(5000, TimeUnit.MILLISECONDS);
157         } catch (InterruptedException | ExecutionException | TimeoutException e) {
158             logger.debug("exception waiting for connection start");
159             return false;
160         }
161     }
162
163     /**
164      * After setting up the communication with the Niko Home Control Connected Controller, send all initialization
165      * messages.
166      *
167      */
168     private void initialize() throws MqttException {
169         NhcMessage2 message = new NhcMessage2();
170
171         message.method = "systeminfo.publish";
172         mqttConnection.connectionPublish(profile + "/system/cmd", gson.toJson(message));
173
174         message.method = "services.list";
175         mqttConnection.connectionPublish(profile + "/authentication/cmd", gson.toJson(message));
176
177         message.method = "devices.list";
178         mqttConnection.connectionPublish(profile + "/control/devices/cmd", gson.toJson(message));
179
180         message.method = "notifications.list";
181         mqttConnection.connectionPublish(profile + "/notification/cmd", gson.toJson(message));
182     }
183
184     private void connectionLost(String message) {
185         logger.debug("connection lost");
186         stopCommunication();
187         handler.controllerOffline(message);
188     }
189
190     private void systemEvt(String response) {
191         Type messageType = new TypeToken<NhcMessage2>() {
192         }.getType();
193         List<NhcTimeInfo2> timeInfo = null;
194         List<NhcSystemInfo2> systemInfo = null;
195         try {
196             NhcMessage2 message = gson.fromJson(response, messageType);
197             List<NhcMessageParam> messageParams = (message != null) ? message.params : null;
198             if (messageParams != null) {
199                 timeInfo = messageParams.stream().filter(p -> (p.timeInfo != null)).findFirst().get().timeInfo;
200                 systemInfo = messageParams.stream().filter(p -> (p.systemInfo != null)).findFirst().get().systemInfo;
201             }
202         } catch (JsonSyntaxException e) {
203             logger.debug("unexpected json {}", response);
204         } catch (NoSuchElementException ignore) {
205             // Ignore if timeInfo not present in response, this should not happen in a timeInfo response
206         }
207         if (timeInfo != null) {
208             nhcTimeInfo = timeInfo.get(0);
209         }
210         if (systemInfo != null) {
211             nhcSystemInfo = systemInfo.get(0);
212             handler.updatePropertiesEvent();
213         }
214     }
215
216     private void systeminfoPublishRsp(String response) {
217         Type messageType = new TypeToken<NhcMessage2>() {
218         }.getType();
219         List<NhcSystemInfo2> systemInfo = null;
220         try {
221             NhcMessage2 message = gson.fromJson(response, messageType);
222             List<NhcMessageParam> messageParams = (message != null) ? message.params : null;
223             if (messageParams != null) {
224                 systemInfo = messageParams.stream().filter(p -> (p.systemInfo != null)).findFirst().get().systemInfo;
225             }
226         } catch (JsonSyntaxException e) {
227             logger.debug("unexpected json {}", response);
228         } catch (NoSuchElementException ignore) {
229             // Ignore if systemInfo not present in response, this should not happen in a systemInfo response
230         }
231         if (systemInfo != null) {
232             nhcSystemInfo = systemInfo.get(0);
233         }
234     }
235
236     private void servicesListRsp(String response) {
237         Type messageType = new TypeToken<NhcMessage2>() {
238         }.getType();
239         List<NhcService2> serviceList = null;
240         try {
241             NhcMessage2 message = gson.fromJson(response, messageType);
242             List<NhcMessageParam> messageParams = (message != null) ? message.params : null;
243             if (messageParams != null) {
244                 serviceList = messageParams.stream().filter(p -> (p.services != null)).findFirst().get().services;
245             }
246         } catch (JsonSyntaxException e) {
247             logger.debug("unexpected json {}", response);
248         } catch (NoSuchElementException ignore) {
249             // Ignore if services not present in response, this should not happen in a services response
250         }
251         services.clear();
252         if (serviceList != null) {
253             services.addAll(serviceList);
254         }
255     }
256
257     private void devicesListRsp(String response) {
258         Type messageType = new TypeToken<NhcMessage2>() {
259         }.getType();
260         List<NhcDevice2> deviceList = null;
261         try {
262             NhcMessage2 message = gson.fromJson(response, messageType);
263             List<NhcMessageParam> messageParams = (message != null) ? message.params : null;
264             if (messageParams != null) {
265                 deviceList = messageParams.stream().filter(p -> (p.devices != null)).findFirst().get().devices;
266             }
267         } catch (JsonSyntaxException e) {
268             logger.debug("unexpected json {}", response);
269         } catch (NoSuchElementException ignore) {
270             // Ignore if devices not present in response, this should not happen in a devices response
271         }
272         if (deviceList == null) {
273             return;
274         }
275
276         for (NhcDevice2 device : deviceList) {
277             addDevice(device);
278             updateState(device);
279         }
280
281         // Once a devices list response is received, we know the communication is fully started.
282         logger.debug("Communication start complete.");
283         handler.controllerOnline();
284         CompletableFuture<Boolean> future = communicationStarted;
285         if (future != null) {
286             future.complete(true);
287         }
288     }
289
290     private void devicesEvt(String response) {
291         Type messageType = new TypeToken<NhcMessage2>() {
292         }.getType();
293         List<NhcDevice2> deviceList = null;
294         String method = null;
295         try {
296             NhcMessage2 message = gson.fromJson(response, messageType);
297             method = (message != null) ? message.method : null;
298             List<NhcMessageParam> messageParams = (message != null) ? message.params : null;
299             if (messageParams != null) {
300                 deviceList = messageParams.stream().filter(p -> (p.devices != null)).findFirst().get().devices;
301             }
302         } catch (JsonSyntaxException e) {
303             logger.debug("unexpected json {}", response);
304         } catch (NoSuchElementException ignore) {
305             // Ignore if devices not present in response, this should not happen in a devices event
306         }
307         if (deviceList == null) {
308             return;
309         }
310
311         if ("devices.removed".equals(method)) {
312             deviceList.forEach(this::removeDevice);
313             return;
314         } else if ("devices.added".equals(method)) {
315             deviceList.forEach(this::addDevice);
316         }
317
318         deviceList.forEach(this::updateState);
319     }
320
321     private void notificationEvt(String response) {
322         Type messageType = new TypeToken<NhcMessage2>() {
323         }.getType();
324         List<NhcNotification2> notificationList = null;
325         try {
326             NhcMessage2 message = gson.fromJson(response, messageType);
327             List<NhcMessageParam> messageParams = (message != null) ? message.params : null;
328             if (messageParams != null) {
329                 notificationList = messageParams.stream().filter(p -> (p.notifications != null)).findFirst()
330                         .get().notifications;
331             }
332         } catch (JsonSyntaxException e) {
333             logger.debug("unexpected json {}", response);
334         } catch (NoSuchElementException ignore) {
335             // Ignore if notifications not present in response, this should not happen in a notifications event
336         }
337         logger.debug("notifications {}", notificationList);
338         if (notificationList == null) {
339             return;
340         }
341
342         for (NhcNotification2 notification : notificationList) {
343             if ("new".equals(notification.status)) {
344                 String alarmText = notification.text;
345                 switch (notification.type) {
346                     case "alarm":
347                         handler.alarmEvent(alarmText);
348                         break;
349                     case "notification":
350                         handler.noticeEvent(alarmText);
351                         break;
352                     default:
353                         logger.debug("unexpected message type {}", notification.type);
354                 }
355             }
356         }
357     }
358
359     private void addDevice(NhcDevice2 device) {
360         String location = null;
361         List<NhcParameter> parameters = device.parameters;
362         if (parameters != null) {
363             location = parameters.stream().map(p -> p.locationName).filter(Objects::nonNull).findFirst().orElse(null);
364         }
365
366         if ("action".equals(device.type) || "virtual".equals(device.type)) {
367             ActionType actionType;
368             switch (device.model) {
369                 case "generic":
370                 case "pir":
371                 case "simulation":
372                 case "comfort":
373                 case "alarms":
374                 case "alloff":
375                 case "overallcomfort":
376                 case "garagedoor":
377                     actionType = ActionType.TRIGGER;
378                     break;
379                 case "light":
380                 case "socket":
381                 case "switched-generic":
382                 case "switched-fan":
383                 case "flag":
384                     actionType = ActionType.RELAY;
385                     break;
386                 case "dimmer":
387                     actionType = ActionType.DIMMER;
388                     break;
389                 case "rolldownshutter":
390                 case "sunblind":
391                 case "venetianblind":
392                 case "gate":
393                     actionType = ActionType.ROLLERSHUTTER;
394                     break;
395                 default:
396                     actionType = ActionType.GENERIC;
397                     logger.debug("device type {} and model {} not recognised for {}, {}, ignoring", device.type,
398                             device.model, device.uuid, device.name);
399                     return;
400             }
401
402             NhcAction nhcAction = actions.get(device.uuid);
403             if (nhcAction != null) {
404                 // update name and location so discovery will see updated name and location
405                 nhcAction.setName(device.name);
406                 nhcAction.setLocation(location);
407             } else {
408                 logger.debug("adding action device {} model {}, {}", device.uuid, device.model, device.name);
409                 nhcAction = new NhcAction2(device.uuid, device.name, device.type, device.technology, device.model,
410                         location, actionType, this);
411             }
412             actions.put(device.uuid, nhcAction);
413         } else if ("thermostat".equals(device.type)) {
414             NhcThermostat nhcThermostat = thermostats.get(device.uuid);
415             if (nhcThermostat != null) {
416                 nhcThermostat.setName(device.name);
417                 nhcThermostat.setLocation(location);
418             } else {
419                 logger.debug("adding thermostat device {} model {}, {}", device.uuid, device.model, device.name);
420                 nhcThermostat = new NhcThermostat2(device.uuid, device.name, device.type, device.technology,
421                         device.model, location, this);
422             }
423             thermostats.put(device.uuid, nhcThermostat);
424         } else if ("centralmeter".equals(device.type) || "energyhome".equals(device.type)) {
425             NhcEnergyMeter nhcEnergyMeter = energyMeters.get(device.uuid);
426             if (nhcEnergyMeter != null) {
427                 nhcEnergyMeter.setName(device.name);
428                 nhcEnergyMeter.setLocation(location);
429             } else {
430                 logger.debug("adding energy meter device {} model {}, {}", device.uuid, device.model, device.name);
431                 nhcEnergyMeter = new NhcEnergyMeter2(device.uuid, device.name, device.type, device.technology,
432                         device.model, location, this, scheduler);
433             }
434             energyMeters.put(device.uuid, nhcEnergyMeter);
435         } else {
436             logger.debug("device type {} and model {} not supported for {}, {}", device.type, device.model, device.uuid,
437                     device.name);
438         }
439     }
440
441     private void removeDevice(NhcDevice2 device) {
442         NhcAction action = actions.get(device.uuid);
443         NhcThermostat thermostat = thermostats.get(device.uuid);
444         NhcEnergyMeter energyMeter = energyMeters.get(device.uuid);
445         if (action != null) {
446             action.actionRemoved();
447             actions.remove(device.uuid);
448         } else if (thermostat != null) {
449             thermostat.thermostatRemoved();
450             thermostats.remove(device.uuid);
451         } else if (energyMeter != null) {
452             energyMeter.energyMeterRemoved();
453             energyMeters.remove(device.uuid);
454         }
455     }
456
457     private void updateState(NhcDevice2 device) {
458         List<NhcProperty> deviceProperties = device.properties;
459         if (deviceProperties == null) {
460             logger.debug("Cannot Update state for {} as no properties defined in device message", device.uuid);
461             return;
462         }
463
464         NhcAction action = actions.get(device.uuid);
465         NhcThermostat thermostat = thermostats.get(device.uuid);
466         NhcEnergyMeter energyMeter = energyMeters.get(device.uuid);
467
468         if (action != null) {
469             updateActionState((NhcAction2) action, deviceProperties);
470         } else if (thermostat != null) {
471             updateThermostatState((NhcThermostat2) thermostat, deviceProperties);
472         } else if (energyMeter != null) {
473             updateEnergyMeterState((NhcEnergyMeter2) energyMeter, deviceProperties);
474         }
475     }
476
477     private void updateActionState(NhcAction2 action, List<NhcProperty> deviceProperties) {
478         if (action.getType() == ActionType.ROLLERSHUTTER) {
479             updateRollershutterState(action, deviceProperties);
480         } else {
481             updateLightState(action, deviceProperties);
482         }
483     }
484
485     private void updateLightState(NhcAction2 action, List<NhcProperty> deviceProperties) {
486         Optional<NhcProperty> statusProperty = deviceProperties.stream().filter(p -> (p.status != null)).findFirst();
487         Optional<NhcProperty> dimmerProperty = deviceProperties.stream().filter(p -> (p.brightness != null))
488                 .findFirst();
489         Optional<NhcProperty> basicStateProperty = deviceProperties.stream().filter(p -> (p.basicState != null))
490                 .findFirst();
491
492         String booleanState = null;
493         if (statusProperty.isPresent()) {
494             booleanState = statusProperty.get().status;
495         } else if (basicStateProperty.isPresent()) {
496             booleanState = basicStateProperty.get().basicState;
497         }
498
499         if (NHCOFF.equals(booleanState) || NHCFALSE.equals(booleanState)) {
500             action.setBooleanState(false);
501             logger.debug("setting action {} internally to OFF", action.getId());
502         }
503
504         if (dimmerProperty.isPresent()) {
505             String brightness = dimmerProperty.get().brightness;
506             if (brightness != null) {
507                 try {
508                     action.setState(Integer.parseInt(brightness));
509                     logger.debug("setting action {} internally to {}", action.getId(), dimmerProperty.get().brightness);
510                 } catch (NumberFormatException e) {
511                     logger.debug("received invalid brightness value {} for dimmer {}", brightness, action.getId());
512                 }
513             }
514         }
515
516         if (NHCON.equals(booleanState) || NHCTRUE.equals(booleanState)) {
517             action.setBooleanState(true);
518             logger.debug("setting action {} internally to ON", action.getId());
519         }
520     }
521
522     private void updateRollershutterState(NhcAction2 action, List<NhcProperty> deviceProperties) {
523         deviceProperties.stream().map(p -> p.position).filter(Objects::nonNull).findFirst().ifPresent(position -> {
524             try {
525                 action.setState(Integer.parseInt(position));
526                 logger.debug("setting action {} internally to {}", action.getId(), position);
527             } catch (NumberFormatException e) {
528                 logger.trace("received empty or invalid rollershutter {} position info {}", action.getId(), position);
529             }
530         });
531     }
532
533     private void updateThermostatState(NhcThermostat2 thermostat, List<NhcProperty> deviceProperties) {
534         Optional<Boolean> overruleActiveProperty = deviceProperties.stream().map(p -> p.overruleActive)
535                 .filter(Objects::nonNull).map(t -> Boolean.parseBoolean(t)).findFirst();
536         Optional<Integer> overruleSetpointProperty = deviceProperties.stream().map(p -> p.overruleSetpoint)
537                 .map(s -> (!((s == null) || s.isEmpty())) ? Math.round(Float.parseFloat(s) * 10) : null)
538                 .filter(Objects::nonNull).findFirst();
539         Optional<Integer> overruleTimeProperty = deviceProperties.stream().map(p -> p.overruleTime)
540                 .map(s -> (!((s == null) || s.isEmpty())) ? Math.round(Float.parseFloat(s)) : null)
541                 .filter(Objects::nonNull).findFirst();
542         Optional<Integer> setpointTemperatureProperty = deviceProperties.stream().map(p -> p.setpointTemperature)
543                 .map(s -> (!((s == null) || s.isEmpty())) ? Math.round(Float.parseFloat(s) * 10) : null)
544                 .filter(Objects::nonNull).findFirst();
545         Optional<Boolean> ecoSaveProperty = deviceProperties.stream().map(p -> p.ecoSave)
546                 .map(s -> s != null ? Boolean.parseBoolean(s) : null).filter(Objects::nonNull).findFirst();
547         Optional<Integer> ambientTemperatureProperty = deviceProperties.stream().map(p -> p.ambientTemperature)
548                 .map(s -> (!((s == null) || s.isEmpty())) ? Math.round(Float.parseFloat(s) * 10) : null)
549                 .filter(Objects::nonNull).findFirst();
550         Optional<@Nullable String> demandProperty = deviceProperties.stream().map(p -> p.demand)
551                 .filter(Objects::nonNull).findFirst();
552         Optional<@Nullable String> operationModeProperty = deviceProperties.stream().map(p -> p.operationMode)
553                 .filter(Objects::nonNull).findFirst();
554
555         String modeString = deviceProperties.stream().map(p -> p.program).filter(Objects::nonNull).findFirst()
556                 .orElse("");
557         int mode = IntStream.range(0, THERMOSTATMODES.length).filter(i -> THERMOSTATMODES[i].equals(modeString))
558                 .findFirst().orElse(thermostat.getMode());
559
560         int measured = ambientTemperatureProperty.orElse(thermostat.getMeasured());
561         int setpoint = setpointTemperatureProperty.orElse(thermostat.getSetpoint());
562
563         int overrule = thermostat.getOverrule();
564         int overruletime = thermostat.getRemainingOverruletime();
565         if (overruleActiveProperty.orElse(false)) {
566             overrule = overruleSetpointProperty.orElse(0);
567             overruletime = overruleTimeProperty.orElse(0);
568         }
569
570         int ecosave = thermostat.getEcosave();
571         if (ecoSaveProperty.orElse(false)) {
572             ecosave = 1;
573         }
574
575         int demand = thermostat.getDemand();
576         String demandString = demandProperty.orElse(operationModeProperty.orElse(""));
577         demandString = demandString == null ? "" : demandString;
578         switch (demandString) {
579             case "None":
580                 demand = 0;
581                 break;
582             case "Heating":
583                 demand = 1;
584                 break;
585             case "Cooling":
586                 demand = -1;
587                 break;
588         }
589
590         logger.debug(
591                 "Niko Home Control: setting thermostat {} with measured {}, setpoint {}, mode {}, overrule {}, overruletime {}, ecosave {}, demand {}",
592                 thermostat.getId(), measured, setpoint, mode, overrule, overruletime, ecosave, demand);
593         thermostat.updateState(measured, setpoint, mode, overrule, overruletime, ecosave, demand);
594     }
595
596     private void updateEnergyMeterState(NhcEnergyMeter2 energyMeter, List<NhcProperty> deviceProperties) {
597         try {
598             Optional<Integer> electricalPower = deviceProperties.stream().map(p -> p.electricalPower)
599                     .map(s -> (!((s == null) || s.isEmpty())) ? Math.round(Float.parseFloat(s)) : null)
600                     .filter(Objects::nonNull).findFirst();
601             Optional<Integer> powerFromGrid = deviceProperties.stream().map(p -> p.electricalPowerFromGrid)
602                     .map(s -> (!((s == null) || s.isEmpty())) ? Math.round(Float.parseFloat(s)) : null)
603                     .filter(Objects::nonNull).findFirst();
604             Optional<Integer> powerToGrid = deviceProperties.stream().map(p -> p.electricalPowerToGrid)
605                     .map(s -> (!((s == null) || s.isEmpty())) ? Math.round(Float.parseFloat(s)) : null)
606                     .filter(Objects::nonNull).findFirst();
607             int power = electricalPower.orElse(powerFromGrid.orElse(0) - powerToGrid.orElse(0));
608             logger.trace("setting energy meter {} power to {}", energyMeter.getId(), electricalPower);
609             energyMeter.setPower(power);
610         } catch (NumberFormatException e) {
611             energyMeter.setPower(null);
612             logger.trace("received empty energy meter {} power reading", energyMeter.getId());
613         }
614     }
615
616     @Override
617     public void executeAction(String actionId, String value) {
618         NhcMessage2 message = new NhcMessage2();
619
620         message.method = "devices.control";
621         ArrayList<NhcMessageParam> params = new ArrayList<>();
622         NhcMessageParam param = new NhcMessageParam();
623         params.add(param);
624         message.params = params;
625         ArrayList<NhcDevice2> devices = new ArrayList<>();
626         NhcDevice2 device = new NhcDevice2();
627         devices.add(device);
628         param.devices = devices;
629         device.uuid = actionId;
630         ArrayList<NhcProperty> deviceProperties = new ArrayList<>();
631         NhcProperty property = new NhcProperty();
632         deviceProperties.add(property);
633         device.properties = deviceProperties;
634
635         NhcAction2 action = (NhcAction2) actions.get(actionId);
636         if (action == null) {
637             return;
638         }
639
640         switch (action.getType()) {
641             case GENERIC:
642             case TRIGGER:
643                 property.basicState = NHCTRIGGERED;
644                 break;
645             case RELAY:
646                 property.status = value;
647                 break;
648             case DIMMER:
649                 if (NHCON.equals(value)) {
650                     action.setBooleanState(true); // this will trigger sending the stored brightness value event out
651                     property.status = value;
652                 } else if (NHCOFF.equals(value)) {
653                     property.status = value;
654                 } else {
655                     try {
656                         action.setState(Integer.parseInt(value)); // set cached state to new brightness value to avoid
657                                                                   // switching on with old brightness value before
658                                                                   // updating
659                                                                   // to new value
660                     } catch (NumberFormatException e) {
661                         logger.debug("internal error, trying to set invalid brightness value {} for dimmer {}", value,
662                                 action.getId());
663                         return;
664                     }
665
666                     // If the light is off, turn the light on before sending the brightness value, needs to happen
667                     // in 2 separate messages.
668                     if (!action.booleanState()) {
669                         executeAction(actionId, NHCON);
670                     }
671                     property.brightness = value;
672                 }
673                 break;
674             case ROLLERSHUTTER:
675                 if (NHCSTOP.equals(value)) {
676                     property.action = value;
677                 } else if (NHCUP.equals(value)) {
678                     property.position = "100";
679                 } else if (NHCDOWN.equals(value)) {
680                     property.position = "0";
681                 } else {
682                     property.position = value;
683                 }
684                 break;
685         }
686
687         String topic = profile + "/control/devices/cmd";
688         String gsonMessage = gson.toJson(message);
689         sendDeviceMessage(topic, gsonMessage);
690     }
691
692     @Override
693     public void executeThermostat(String thermostatId, String mode) {
694         NhcMessage2 message = new NhcMessage2();
695
696         message.method = "devices.control";
697         ArrayList<NhcMessageParam> params = new ArrayList<>();
698         NhcMessageParam param = new NhcMessageParam();
699         params.add(param);
700         message.params = params;
701         ArrayList<NhcDevice2> devices = new ArrayList<>();
702         NhcDevice2 device = new NhcDevice2();
703         devices.add(device);
704         param.devices = devices;
705         device.uuid = thermostatId;
706         ArrayList<NhcProperty> deviceProperties = new ArrayList<>();
707
708         NhcProperty overruleActiveProp = new NhcProperty();
709         deviceProperties.add(overruleActiveProp);
710         overruleActiveProp.overruleActive = "False";
711
712         NhcProperty program = new NhcProperty();
713         deviceProperties.add(program);
714         program.program = mode;
715
716         device.properties = deviceProperties;
717
718         String topic = profile + "/control/devices/cmd";
719         String gsonMessage = gson.toJson(message);
720         sendDeviceMessage(topic, gsonMessage);
721     }
722
723     @Override
724     public void executeThermostat(String thermostatId, int overruleTemp, int overruleTime) {
725         NhcMessage2 message = new NhcMessage2();
726
727         message.method = "devices.control";
728         ArrayList<NhcMessageParam> params = new ArrayList<>();
729         NhcMessageParam param = new NhcMessageParam();
730         params.add(param);
731         message.params = params;
732         ArrayList<NhcDevice2> devices = new ArrayList<>();
733         NhcDevice2 device = new NhcDevice2();
734         devices.add(device);
735         param.devices = devices;
736         device.uuid = thermostatId;
737         ArrayList<NhcProperty> deviceProperties = new ArrayList<>();
738
739         if (overruleTime > 0) {
740             NhcProperty overruleActiveProp = new NhcProperty();
741             overruleActiveProp.overruleActive = "True";
742             deviceProperties.add(overruleActiveProp);
743
744             NhcProperty overruleSetpointProp = new NhcProperty();
745             overruleSetpointProp.overruleSetpoint = String.valueOf(overruleTemp / 10.0);
746             deviceProperties.add(overruleSetpointProp);
747
748             NhcProperty overruleTimeProp = new NhcProperty();
749             overruleTimeProp.overruleTime = String.valueOf(overruleTime);
750             deviceProperties.add(overruleTimeProp);
751         } else {
752             NhcProperty overruleActiveProp = new NhcProperty();
753             overruleActiveProp.overruleActive = "False";
754             deviceProperties.add(overruleActiveProp);
755         }
756         device.properties = deviceProperties;
757
758         String topic = profile + "/control/devices/cmd";
759         String gsonMessage = gson.toJson(message);
760         sendDeviceMessage(topic, gsonMessage);
761     }
762
763     @Override
764     public void startEnergyMeter(String energyMeterId) {
765         NhcMessage2 message = new NhcMessage2();
766
767         message.method = "devices.control";
768         ArrayList<NhcMessageParam> params = new ArrayList<>();
769         NhcMessageParam param = new NhcMessageParam();
770         params.add(param);
771         message.params = params;
772         ArrayList<NhcDevice2> devices = new ArrayList<>();
773         NhcDevice2 device = new NhcDevice2();
774         devices.add(device);
775         param.devices = devices;
776         device.uuid = energyMeterId;
777         ArrayList<NhcProperty> deviceProperties = new ArrayList<>();
778
779         NhcProperty reportInstantUsageProp = new NhcProperty();
780         deviceProperties.add(reportInstantUsageProp);
781         reportInstantUsageProp.reportInstantUsage = "True";
782         device.properties = deviceProperties;
783
784         String topic = profile + "/control/devices/cmd";
785         String gsonMessage = gson.toJson(message);
786
787         NhcEnergyMeter2 energyMeter = (NhcEnergyMeter2) energyMeters.get(energyMeterId);
788         if (energyMeter != null) {
789             energyMeter.startEnergyMeter(topic, gsonMessage);
790         }
791     }
792
793     @Override
794     public void stopEnergyMeter(String energyMeterId) {
795         NhcEnergyMeter2 energyMeter = (NhcEnergyMeter2) energyMeters.get(energyMeterId);
796         if (energyMeter != null) {
797             energyMeter.stopEnergyMeter();
798         }
799     }
800
801     /**
802      * Method called from the {@link NhcEnergyMeter2} object to send message to Niko Home Control.
803      *
804      * @param topic
805      * @param gsonMessage
806      */
807     public void executeEnergyMeter(String topic, String gsonMessage) {
808         sendDeviceMessage(topic, gsonMessage);
809     }
810
811     private void sendDeviceMessage(String topic, String gsonMessage) {
812         try {
813             mqttConnection.connectionPublish(topic, gsonMessage);
814
815         } catch (MqttException e) {
816             String message = e.getLocalizedMessage();
817
818             logger.debug("sending command failed, trying to restart communication");
819             restartCommunication();
820             // retry sending after restart
821             try {
822                 if (communicationActive()) {
823                     mqttConnection.connectionPublish(topic, gsonMessage);
824                 } else {
825                     logger.debug("failed to restart communication");
826                 }
827             } catch (MqttException e1) {
828                 message = e1.getLocalizedMessage();
829
830                 logger.debug("error resending device command");
831             }
832             if (!communicationActive()) {
833                 message = (message != null) ? message : "@text/offline.communication-error";
834                 connectionLost(message);
835             }
836         }
837     }
838
839     @Override
840     public void processMessage(String topic, byte[] payload) {
841         String message = new String(payload);
842         if ((profile + "/system/evt").equals(topic)) {
843             systemEvt(message);
844         } else if ((profile + "/system/rsp").equals(topic)) {
845             logger.debug("received topic {}, payload {}", topic, message);
846             systeminfoPublishRsp(message);
847         } else if ((profile + "/notification/evt").equals(topic)) {
848             logger.debug("received topic {}, payload {}", topic, message);
849             notificationEvt(message);
850         } else if ((profile + "/control/devices/evt").equals(topic)) {
851             logger.trace("received topic {}, payload {}", topic, message);
852             devicesEvt(message);
853         } else if ((profile + "/control/devices/rsp").equals(topic)) {
854             logger.debug("received topic {}, payload {}", topic, message);
855             devicesListRsp(message);
856         } else if ((profile + "/authentication/rsp").equals(topic)) {
857             logger.debug("received topic {}, payload {}", topic, message);
858             servicesListRsp(message);
859         } else if ((profile + "/control/devices.error").equals(topic)) {
860             logger.warn("received error {}", message);
861         } else {
862             logger.trace("not acted on received message topic {}, payload {}", topic, message);
863         }
864     }
865
866     /**
867      * @return system info retrieved from Connected Controller
868      */
869     public NhcSystemInfo2 getSystemInfo() {
870         NhcSystemInfo2 systemInfo = nhcSystemInfo;
871         if (systemInfo == null) {
872             systemInfo = new NhcSystemInfo2();
873         }
874         return systemInfo;
875     }
876
877     /**
878      * @return time info retrieved from Connected Controller
879      */
880     public NhcTimeInfo2 getTimeInfo() {
881         NhcTimeInfo2 timeInfo = nhcTimeInfo;
882         if (timeInfo == null) {
883             timeInfo = new NhcTimeInfo2();
884         }
885         return timeInfo;
886     }
887
888     /**
889      * @return comma separated list of services retrieved from Connected Controller
890      */
891     public String getServices() {
892         return services.stream().map(NhcService2::name).collect(Collectors.joining(", "));
893     }
894
895     @Override
896     public void connectionStateChanged(MqttConnectionState state, @Nullable Throwable error) {
897         if (error != null) {
898             logger.debug("Connection state: {}, error", state, error);
899             String message = error.getLocalizedMessage();
900             message = (message != null) ? message : "@text/offline.communication-error";
901             if (!MqttConnectionState.CONNECTING.equals(state)) {
902                 // This is a connection loss, try to restart
903                 restartCommunication();
904             }
905             if (!communicationActive()) {
906                 connectionLost(message);
907             }
908         } else {
909             logger.trace("Connection state: {}", state);
910         }
911     }
912 }