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