]> git.basschouten.com Git - openhab-addons.git/blob
8ab2d426950b310d86fa0c2b73f0d7ceda6dc532
[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             action.setState(Integer.parseInt(dimmerProperty.get().brightness));
490             logger.debug("Niko Home Control: setting action {} internally to {}", action.getId(),
491                     dimmerProperty.get().brightness);
492         }
493     }
494
495     private void updateRollershutterState(NhcAction2 action, List<NhcProperty> deviceProperties) {
496         deviceProperties.stream().map(p -> p.position).filter(Objects::nonNull).findFirst().ifPresent(position -> {
497             try {
498                 action.setState(Integer.parseInt(position));
499                 logger.debug("Niko Home Control: setting action {} internally to {}", action.getId(), position);
500             } catch (NumberFormatException e) {
501                 logger.trace("Niko Home Control: received empty rollershutter {} position info", action.getId());
502             }
503         });
504     }
505
506     private void updateThermostatState(NhcThermostat2 thermostat, List<NhcProperty> deviceProperties) {
507         Optional<Boolean> overruleActiveProperty = deviceProperties.stream().map(p -> p.overruleActive)
508                 .filter(Objects::nonNull).map(t -> Boolean.parseBoolean(t)).findFirst();
509         Optional<Integer> overruleSetpointProperty = deviceProperties.stream().map(p -> p.overruleSetpoint)
510                 .filter(s -> !((s == null) || s.isEmpty())).map(t -> Math.round(Float.parseFloat(t) * 10)).findFirst();
511         Optional<Integer> overruleTimeProperty = deviceProperties.stream().map(p -> p.overruleTime)
512                 .filter(s -> !((s == null) || s.isEmpty())).map(t -> Math.round(Float.parseFloat(t))).findFirst();
513         Optional<Integer> setpointTemperatureProperty = deviceProperties.stream().map(p -> p.setpointTemperature)
514                 .filter(s -> !((s == null) || s.isEmpty())).map(t -> Math.round(Float.parseFloat(t) * 10)).findFirst();
515         Optional<Boolean> ecoSaveProperty = deviceProperties.stream().map(p -> p.ecoSave).filter(Objects::nonNull)
516                 .map(t -> Boolean.parseBoolean(t)).findFirst();
517         Optional<Integer> ambientTemperatureProperty = deviceProperties.stream().map(p -> p.ambientTemperature)
518                 .filter(s -> !(s == null || s.isEmpty())).map(t -> Math.round(Float.parseFloat(t) * 10)).findFirst();
519         Optional<@Nullable String> demandProperty = deviceProperties.stream().map(p -> p.demand)
520                 .filter(Objects::nonNull).findFirst();
521         Optional<@Nullable String> operationModeProperty = deviceProperties.stream().map(p -> p.operationMode)
522                 .filter(Objects::nonNull).findFirst();
523
524         String modeString = deviceProperties.stream().map(p -> p.program).filter(Objects::nonNull).findFirst()
525                 .orElse("");
526         int mode = IntStream.range(0, THERMOSTATMODES.length).filter(i -> THERMOSTATMODES[i].equals(modeString))
527                 .findFirst().orElse(thermostat.getMode());
528
529         int measured = ambientTemperatureProperty.orElse(thermostat.getMeasured());
530         int setpoint = setpointTemperatureProperty.orElse(thermostat.getSetpoint());
531
532         int overrule = thermostat.getOverrule();
533         int overruletime = thermostat.getRemainingOverruletime();
534         if (overruleActiveProperty.orElse(false)) {
535             overrule = overruleSetpointProperty.orElse(0);
536             overruletime = overruleTimeProperty.orElse(0);
537         }
538
539         int ecosave = thermostat.getEcosave();
540         if (ecoSaveProperty.orElse(false)) {
541             ecosave = 1;
542         }
543
544         int demand = thermostat.getDemand();
545         String demandString = demandProperty.orElse(operationModeProperty.orElse(""));
546         demandString = demandString == null ? "" : demandString;
547         switch (demandString) {
548             case "None":
549                 demand = 0;
550                 break;
551             case "Heating":
552                 demand = 1;
553                 break;
554             case "Cooling":
555                 demand = -1;
556                 break;
557         }
558
559         logger.debug(
560                 "Niko Home Control: setting thermostat {} with measured {}, setpoint {}, mode {}, overrule {}, overruletime {}, ecosave {}, demand {}",
561                 thermostat.getId(), measured, setpoint, mode, overrule, overruletime, ecosave, demand);
562         thermostat.updateState(measured, setpoint, mode, overrule, overruletime, ecosave, demand);
563     }
564
565     private void updateEnergyMeterState(NhcEnergyMeter2 energyMeter, List<NhcProperty> deviceProperties) {
566         deviceProperties.stream().map(p -> p.electricalPower).filter(Objects::nonNull).findFirst()
567                 .ifPresent(electricalPower -> {
568                     try {
569                         energyMeter.setPower(Integer.parseInt(electricalPower));
570                         logger.trace("Niko Home Control: setting energy meter {} power to {}", energyMeter.getId(),
571                                 electricalPower);
572                     } catch (NumberFormatException e) {
573                         energyMeter.setPower(null);
574                         logger.trace("Niko Home Control: received empty energy meter {} power reading",
575                                 energyMeter.getId());
576                     }
577                 });
578     }
579
580     @Override
581     public void executeAction(String actionId, String value) {
582         NhcMessage2 message = new NhcMessage2();
583
584         message.method = "devices.control";
585         ArrayList<NhcMessageParam> params = new ArrayList<>();
586         NhcMessageParam param = new NhcMessageParam();
587         params.add(param);
588         message.params = params;
589         ArrayList<NhcDevice2> devices = new ArrayList<>();
590         NhcDevice2 device = new NhcDevice2();
591         devices.add(device);
592         param.devices = devices;
593         device.uuid = actionId;
594         ArrayList<NhcProperty> deviceProperties = new ArrayList<>();
595         NhcProperty property = new NhcProperty();
596         deviceProperties.add(property);
597         device.properties = deviceProperties;
598
599         NhcAction2 action = (NhcAction2) actions.get(actionId);
600
601         switch (action.getType()) {
602             case GENERIC:
603             case TRIGGER:
604                 property.basicState = NHCTRIGGERED;
605                 break;
606             case RELAY:
607                 property.status = value;
608                 break;
609             case DIMMER:
610                 if (NHCON.equals(value)) {
611                     action.setBooleanState(true); // this will trigger sending the stored brightness value event out
612                     property.status = value;
613                 } else if (NHCOFF.equals(value)) {
614                     property.status = value;
615                 } else {
616                     // If the light is off, turn the light on before sending the brightness value, needs to happen
617                     // in 2 separate messages.
618                     if (!action.booleanState()) {
619                         executeAction(actionId, NHCON);
620                     }
621                     property.brightness = value;
622                 }
623                 break;
624             case ROLLERSHUTTER:
625                 if (NHCSTOP.equals(value)) {
626                     property.action = value;
627                 } else if (NHCUP.equals(value)) {
628                     property.position = "100";
629                 } else if (NHCDOWN.equals(value)) {
630                     property.position = "0";
631                 } else {
632                     int position = 100 - Integer.parseInt(value);
633                     property.position = String.valueOf(position);
634                 }
635                 break;
636         }
637
638         String topic = profile + "/control/devices/cmd";
639         String gsonMessage = gson.toJson(message);
640         sendDeviceMessage(topic, gsonMessage);
641     }
642
643     @Override
644     public void executeThermostat(String thermostatId, String mode) {
645         NhcMessage2 message = new NhcMessage2();
646
647         message.method = "devices.control";
648         ArrayList<NhcMessageParam> params = new ArrayList<>();
649         NhcMessageParam param = new NhcMessageParam();
650         params.add(param);
651         message.params = params;
652         ArrayList<NhcDevice2> devices = new ArrayList<>();
653         NhcDevice2 device = new NhcDevice2();
654         devices.add(device);
655         param.devices = devices;
656         device.uuid = thermostatId;
657         ArrayList<NhcProperty> deviceProperties = new ArrayList<>();
658
659         NhcProperty overruleActiveProp = new NhcProperty();
660         deviceProperties.add(overruleActiveProp);
661         overruleActiveProp.overruleActive = "False";
662
663         NhcProperty program = new NhcProperty();
664         deviceProperties.add(program);
665         program.program = mode;
666
667         device.properties = deviceProperties;
668
669         String topic = profile + "/control/devices/cmd";
670         String gsonMessage = gson.toJson(message);
671         sendDeviceMessage(topic, gsonMessage);
672     }
673
674     @Override
675     public void executeThermostat(String thermostatId, int overruleTemp, int overruleTime) {
676         NhcMessage2 message = new NhcMessage2();
677
678         message.method = "devices.control";
679         ArrayList<NhcMessageParam> params = new ArrayList<>();
680         NhcMessageParam param = new NhcMessageParam();
681         params.add(param);
682         message.params = params;
683         ArrayList<NhcDevice2> devices = new ArrayList<>();
684         NhcDevice2 device = new NhcDevice2();
685         devices.add(device);
686         param.devices = devices;
687         device.uuid = thermostatId;
688         ArrayList<NhcProperty> deviceProperties = new ArrayList<>();
689
690         if (overruleTime > 0) {
691             NhcProperty overruleActiveProp = new NhcProperty();
692             overruleActiveProp.overruleActive = "True";
693             deviceProperties.add(overruleActiveProp);
694
695             NhcProperty overruleSetpointProp = new NhcProperty();
696             overruleSetpointProp.overruleSetpoint = String.valueOf(overruleTemp / 10.0);
697             deviceProperties.add(overruleSetpointProp);
698
699             NhcProperty overruleTimeProp = new NhcProperty();
700             overruleTimeProp.overruleTime = String.valueOf(overruleTime);
701             deviceProperties.add(overruleTimeProp);
702         } else {
703             NhcProperty overruleActiveProp = new NhcProperty();
704             overruleActiveProp.overruleActive = "False";
705             deviceProperties.add(overruleActiveProp);
706         }
707         device.properties = deviceProperties;
708
709         String topic = profile + "/control/devices/cmd";
710         String gsonMessage = gson.toJson(message);
711         sendDeviceMessage(topic, gsonMessage);
712     }
713
714     @Override
715     public void startEnergyMeter(String energyMeterId) {
716         NhcMessage2 message = new NhcMessage2();
717
718         message.method = "devices.control";
719         ArrayList<NhcMessageParam> params = new ArrayList<>();
720         NhcMessageParam param = new NhcMessageParam();
721         params.add(param);
722         message.params = params;
723         ArrayList<NhcDevice2> devices = new ArrayList<>();
724         NhcDevice2 device = new NhcDevice2();
725         devices.add(device);
726         param.devices = devices;
727         device.uuid = energyMeterId;
728         ArrayList<NhcProperty> deviceProperties = new ArrayList<>();
729
730         NhcProperty reportInstantUsageProp = new NhcProperty();
731         deviceProperties.add(reportInstantUsageProp);
732         reportInstantUsageProp.reportInstantUsage = "True";
733         device.properties = deviceProperties;
734
735         String topic = profile + "/control/devices/cmd";
736         String gsonMessage = gson.toJson(message);
737
738         ((NhcEnergyMeter2) energyMeters.get(energyMeterId)).startEnergyMeter(topic, gsonMessage);
739     }
740
741     @Override
742     public void stopEnergyMeter(String energyMeterId) {
743         ((NhcEnergyMeter2) energyMeters.get(energyMeterId)).stopEnergyMeter();
744     }
745
746     /**
747      * Method called from the {@link NhcEnergyMeter2} object to send message to Niko Home Control.
748      *
749      * @param topic
750      * @param gsonMessage
751      */
752     public void executeEnergyMeter(String topic, String gsonMessage) {
753         sendDeviceMessage(topic, gsonMessage);
754     }
755
756     private void sendDeviceMessage(String topic, String gsonMessage) {
757         try {
758             mqttConnection.connectionPublish(topic, gsonMessage);
759
760         } catch (MqttException e) {
761             logger.warn("Niko Home Control: sending command failed, trying to restart communication");
762             restartCommunication();
763             // retry sending after restart
764             try {
765                 if (communicationActive()) {
766                     mqttConnection.connectionPublish(topic, gsonMessage);
767                 } else {
768                     logger.warn("Niko Home Control: failed to restart communication");
769                     connectionLost();
770                 }
771             } catch (MqttException e1) {
772                 logger.warn("Niko Home Control: error resending device command");
773                 connectionLost();
774             }
775         }
776     }
777
778     @Override
779     public void processMessage(String topic, byte[] payload) {
780         String message = new String(payload);
781         if ((profile + "/system/evt").equals(topic)) {
782             systemEvt(message);
783         } else if ((profile + "/system/rsp").equals(topic)) {
784             logger.debug("Niko Home Control: received topic {}, payload {}", topic, message);
785             systeminfoPublishRsp(message);
786         } else if ((profile + "/notification/evt").equals(topic)) {
787             logger.debug("Niko Home Control: received topic {}, payload {}", topic, message);
788             notificationEvt(message);
789         } else if ((profile + "/control/devices/evt").equals(topic)) {
790             logger.trace("Niko Home Control: received topic {}, payload {}", topic, message);
791             devicesEvt(message);
792         } else if ((profile + "/control/devices/rsp").equals(topic)) {
793             logger.debug("Niko Home Control: received topic {}, payload {}", topic, message);
794             devicesListRsp(message);
795         } else if ((profile + "/authentication/rsp").equals(topic)) {
796             logger.debug("Niko Home Control: received topic {}, payload {}", topic, message);
797             servicesListRsp(message);
798         } else if ((profile + "/control/devices.error").equals(topic)) {
799             logger.warn("Niko Home Control: received error {}", message);
800         } else {
801             logger.trace("Niko Home Control: not acted on received message topic {}, payload {}", topic, message);
802         }
803     }
804
805     /**
806      * @return system info retrieved from Connected Controller
807      */
808     public NhcSystemInfo2 getSystemInfo() {
809         NhcSystemInfo2 systemInfo = nhcSystemInfo;
810         if (systemInfo == null) {
811             systemInfo = new NhcSystemInfo2();
812         }
813         return systemInfo;
814     }
815
816     /**
817      * @return time info retrieved from Connected Controller
818      */
819     public NhcTimeInfo2 getTimeInfo() {
820         NhcTimeInfo2 timeInfo = nhcTimeInfo;
821         if (timeInfo == null) {
822             timeInfo = new NhcTimeInfo2();
823         }
824         return timeInfo;
825     }
826
827     /**
828      * @return comma separated list of services retrieved from Connected Controller
829      */
830     public String getServices() {
831         return services.stream().map(NhcService2::name).collect(Collectors.joining(", "));
832     }
833
834     @Override
835     public void connectionStateChanged(MqttConnectionState state, @Nullable Throwable error) {
836         if (error != null) {
837             logger.debug("Connection state: {}", state, error);
838             restartCommunication();
839             if (!communicationActive()) {
840                 logger.warn("Niko Home Control: failed to restart communication");
841                 connectionLost();
842             }
843         } else {
844             logger.trace("Connection state: {}", state);
845         }
846     }
847 }