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