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