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