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