2 * Copyright (c) 2010-2021 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.nikohomecontrol.internal.protocol.nhc2;
15 import static org.openhab.binding.nikohomecontrol.internal.protocol.NikoHomeControlConstants.*;
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;
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;
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;
58 * The {@link NikoHomeControlCommunication2} class is able to do the following tasks with Niko Home Control II
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.
67 * @author Mark Herwege - Initial Contribution
70 public class NikoHomeControlCommunication2 extends NikoHomeControlCommunication
71 implements MqttMessageSubscriber, MqttConnectionObserver {
73 private final Logger logger = LoggerFactory.getLogger(NikoHomeControlCommunication2.class);
75 private final NhcMqttConnection2 mqttConnection;
77 private final List<NhcService2> services = new CopyOnWriteArrayList<>();
79 private volatile String profile = "";
81 private volatile @Nullable NhcSystemInfo2 nhcSystemInfo;
82 private volatile @Nullable NhcTimeInfo2 nhcTimeInfo;
84 private volatile @Nullable CompletableFuture<Boolean> communicationStarted;
86 private ScheduledExecutorService scheduler;
88 private final Gson gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.UPPER_CAMEL_CASE).create();
91 * Constructor for Niko Home Control communication object, manages communication with
92 * Niko Home Control II Connected Controller.
94 * @throws CertificateException when the SSL context for MQTT communication cannot be created
95 * @throws UnknownHostException when the IP address is not provided
98 public NikoHomeControlCommunication2(NhcControllerEvent handler, String clientId,
99 ScheduledExecutorService scheduler) throws CertificateException {
101 mqttConnection = new NhcMqttConnection2(clientId, this, this);
102 this.scheduler = scheduler;
106 public synchronized void startCommunication() {
107 communicationStarted = new CompletableFuture<>();
109 InetAddress addr = handler.getAddr();
111 logger.warn("IP address cannot be empty");
115 String addrString = addr.getHostAddress();
116 int port = handler.getPort();
117 logger.debug("initializing for mqtt connection to CoCo on {}:{}", addrString, port);
119 profile = handler.getProfile();
121 String token = handler.getToken();
122 if (token.isEmpty()) {
123 logger.warn("JWT token cannot be empty");
129 mqttConnection.startConnection(addrString, port, profile, token);
131 } catch (MqttException e) {
132 logger.debug("error in mqtt communication");
138 public synchronized void stopCommunication() {
139 CompletableFuture<Boolean> started = communicationStarted;
140 if (started != null) {
141 started.complete(false);
143 communicationStarted = null;
144 mqttConnection.stopConnection();
148 public boolean communicationActive() {
149 CompletableFuture<Boolean> started = communicationStarted;
150 if (started == null) {
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");
163 * After setting up the communication with the Niko Home Control Connected Controller, send all initialization
167 private void initialize() throws MqttException {
168 NhcMessage2 message = new NhcMessage2();
170 message.method = "systeminfo.publish";
171 mqttConnection.connectionPublish(profile + "/system/cmd", gson.toJson(message));
173 message.method = "services.list";
174 mqttConnection.connectionPublish(profile + "/authentication/cmd", gson.toJson(message));
176 message.method = "devices.list";
177 mqttConnection.connectionPublish(profile + "/control/devices/cmd", gson.toJson(message));
179 message.method = "notifications.list";
180 mqttConnection.connectionPublish(profile + "/notification/cmd", gson.toJson(message));
183 private void connectionLost(String message) {
184 logger.debug("connection lost");
186 handler.controllerOffline(message);
189 private void systemEvt(String response) {
190 Type messageType = new TypeToken<NhcMessage2>() {
192 List<NhcTimeInfo2> timeInfo = null;
193 List<NhcSystemInfo2> systemInfo = null;
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;
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
206 if (timeInfo != null) {
207 nhcTimeInfo = timeInfo.get(0);
209 if (systemInfo != null) {
210 nhcSystemInfo = systemInfo.get(0);
211 handler.updatePropertiesEvent();
215 private void systeminfoPublishRsp(String response) {
216 Type messageType = new TypeToken<NhcMessage2>() {
218 List<NhcSystemInfo2> systemInfo = null;
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;
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
230 if (systemInfo != null) {
231 nhcSystemInfo = systemInfo.get(0);
235 private void servicesListRsp(String response) {
236 Type messageType = new TypeToken<NhcMessage2>() {
238 List<NhcService2> serviceList = null;
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;
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
251 if (serviceList != null) {
252 services.addAll(serviceList);
256 private void devicesListRsp(String response) {
257 Type messageType = new TypeToken<NhcMessage2>() {
259 List<NhcDevice2> deviceList = null;
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;
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
271 if (deviceList == null) {
275 for (NhcDevice2 device : deviceList) {
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);
289 private void devicesEvt(String response) {
290 Type messageType = new TypeToken<NhcMessage2>() {
292 List<NhcDevice2> deviceList = null;
293 String method = null;
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;
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
306 if (deviceList == null) {
310 if ("devices.removed".equals(method)) {
311 deviceList.forEach(this::removeDevice);
313 } else if ("devices.added".equals(method)) {
314 deviceList.forEach(this::addDevice);
317 deviceList.forEach(this::updateState);
320 private void notificationEvt(String response) {
321 Type messageType = new TypeToken<NhcMessage2>() {
323 List<NhcNotification2> notificationList = null;
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;
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
336 logger.debug("notifications {}", notificationList);
337 if (notificationList == null) {
341 for (NhcNotification2 notification : notificationList) {
342 if ("new".equals(notification.status)) {
343 String alarmText = notification.text;
344 switch (notification.type) {
346 handler.alarmEvent(alarmText);
349 handler.noticeEvent(alarmText);
352 logger.debug("unexpected message type {}", notification.type);
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()
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);
369 ActionType actionType;
370 switch (device.model) {
377 case "overallcomfort":
379 actionType = ActionType.TRIGGER;
383 case "switched-generic":
386 actionType = ActionType.RELAY;
389 actionType = ActionType.DIMMER;
391 case "rolldownshutter":
393 case "venetianblind":
395 actionType = ActionType.ROLLERSHUTTER;
398 actionType = ActionType.GENERIC;
399 logger.debug("device model {} not recognised, default to GENERIC action", device.model);
402 NhcAction2 nhcAction = new NhcAction2(device.uuid, device.name, device.model, device.technology,
403 actionType, location, this);
404 actions.put(device.uuid, nhcAction);
406 } else if ("thermostat".equals(device.type)) {
407 if (!thermostats.containsKey(device.uuid)) {
408 logger.debug("adding thermostat device {}, {}", device.uuid, device.name);
410 NhcThermostat2 nhcThermostat = new NhcThermostat2(device.uuid, device.name, device.model,
411 device.technology, location, this);
412 thermostats.put(device.uuid, nhcThermostat);
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);
422 logger.debug("device type {} not supported for {}, {}", device.type, device.uuid, device.name);
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);
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);
449 NhcAction action = actions.get(device.uuid);
450 NhcThermostat thermostat = thermostats.get(device.uuid);
451 NhcEnergyMeter energyMeter = energyMeters.get(device.uuid);
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);
462 private void updateActionState(NhcAction2 action, List<NhcProperty> deviceProperties) {
463 if (action.getType() == ActionType.ROLLERSHUTTER) {
464 updateRollershutterState(action, deviceProperties);
466 updateLightState(action, deviceProperties);
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))
474 Optional<NhcProperty> basicStateProperty = deviceProperties.stream().filter(p -> (p.basicState != null))
477 String booleanState = null;
478 if (statusProperty.isPresent()) {
479 booleanState = statusProperty.get().status;
480 } else if (basicStateProperty.isPresent()) {
481 booleanState = basicStateProperty.get().basicState;
484 if (NHCOFF.equals(booleanState) || NHCFALSE.equals(booleanState)) {
485 action.setBooleanState(false);
486 logger.debug("setting action {} internally to OFF", action.getId());
489 if (dimmerProperty.isPresent()) {
490 String brightness = dimmerProperty.get().brightness;
491 if (brightness != null) {
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());
501 if (NHCON.equals(booleanState) || NHCTRUE.equals(booleanState)) {
502 action.setBooleanState(true);
503 logger.debug("setting action {} internally to ON", action.getId());
507 private void updateRollershutterState(NhcAction2 action, List<NhcProperty> deviceProperties) {
508 deviceProperties.stream().map(p -> p.position).filter(Objects::nonNull).findFirst().ifPresent(position -> {
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);
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();
540 String modeString = deviceProperties.stream().map(p -> p.program).filter(Objects::nonNull).findFirst()
542 int mode = IntStream.range(0, THERMOSTATMODES.length).filter(i -> THERMOSTATMODES[i].equals(modeString))
543 .findFirst().orElse(thermostat.getMode());
545 int measured = ambientTemperatureProperty.orElse(thermostat.getMeasured());
546 int setpoint = setpointTemperatureProperty.orElse(thermostat.getSetpoint());
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);
555 int ecosave = thermostat.getEcosave();
556 if (ecoSaveProperty.orElse(false)) {
560 int demand = thermostat.getDemand();
561 String demandString = demandProperty.orElse(operationModeProperty.orElse(""));
562 demandString = demandString == null ? "" : demandString;
563 switch (demandString) {
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);
581 private void updateEnergyMeterState(NhcEnergyMeter2 energyMeter, List<NhcProperty> deviceProperties) {
582 deviceProperties.stream().map(p -> p.electricalPower).filter(Objects::nonNull).findFirst()
583 .ifPresent(electricalPower -> {
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());
597 public void executeAction(String actionId, String value) {
598 NhcMessage2 message = new NhcMessage2();
600 message.method = "devices.control";
601 ArrayList<NhcMessageParam> params = new ArrayList<>();
602 NhcMessageParam param = new NhcMessageParam();
604 message.params = params;
605 ArrayList<NhcDevice2> devices = new ArrayList<>();
606 NhcDevice2 device = new NhcDevice2();
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;
615 NhcAction2 action = (NhcAction2) actions.get(actionId);
616 if (action == null) {
620 switch (action.getType()) {
623 if (!NHCON.equals(value)) {
624 // Only trigger for ON
627 property.basicState = NHCTRIGGERED;
630 property.status = value;
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;
640 action.setState(Integer.parseInt(value)); // set cached state to new brightness value to avoid
641 // switching on with old brightness value before
644 } catch (NumberFormatException e) {
645 logger.debug("internal error, trying to set invalid brightness value {} for dimmer {}", value,
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);
655 property.brightness = value;
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";
666 property.position = value;
671 String topic = profile + "/control/devices/cmd";
672 String gsonMessage = gson.toJson(message);
673 sendDeviceMessage(topic, gsonMessage);
677 public void executeThermostat(String thermostatId, String mode) {
678 NhcMessage2 message = new NhcMessage2();
680 message.method = "devices.control";
681 ArrayList<NhcMessageParam> params = new ArrayList<>();
682 NhcMessageParam param = new NhcMessageParam();
684 message.params = params;
685 ArrayList<NhcDevice2> devices = new ArrayList<>();
686 NhcDevice2 device = new NhcDevice2();
688 param.devices = devices;
689 device.uuid = thermostatId;
690 ArrayList<NhcProperty> deviceProperties = new ArrayList<>();
692 NhcProperty overruleActiveProp = new NhcProperty();
693 deviceProperties.add(overruleActiveProp);
694 overruleActiveProp.overruleActive = "False";
696 NhcProperty program = new NhcProperty();
697 deviceProperties.add(program);
698 program.program = mode;
700 device.properties = deviceProperties;
702 String topic = profile + "/control/devices/cmd";
703 String gsonMessage = gson.toJson(message);
704 sendDeviceMessage(topic, gsonMessage);
708 public void executeThermostat(String thermostatId, int overruleTemp, int overruleTime) {
709 NhcMessage2 message = new NhcMessage2();
711 message.method = "devices.control";
712 ArrayList<NhcMessageParam> params = new ArrayList<>();
713 NhcMessageParam param = new NhcMessageParam();
715 message.params = params;
716 ArrayList<NhcDevice2> devices = new ArrayList<>();
717 NhcDevice2 device = new NhcDevice2();
719 param.devices = devices;
720 device.uuid = thermostatId;
721 ArrayList<NhcProperty> deviceProperties = new ArrayList<>();
723 if (overruleTime > 0) {
724 NhcProperty overruleActiveProp = new NhcProperty();
725 overruleActiveProp.overruleActive = "True";
726 deviceProperties.add(overruleActiveProp);
728 NhcProperty overruleSetpointProp = new NhcProperty();
729 overruleSetpointProp.overruleSetpoint = String.valueOf(overruleTemp / 10.0);
730 deviceProperties.add(overruleSetpointProp);
732 NhcProperty overruleTimeProp = new NhcProperty();
733 overruleTimeProp.overruleTime = String.valueOf(overruleTime);
734 deviceProperties.add(overruleTimeProp);
736 NhcProperty overruleActiveProp = new NhcProperty();
737 overruleActiveProp.overruleActive = "False";
738 deviceProperties.add(overruleActiveProp);
740 device.properties = deviceProperties;
742 String topic = profile + "/control/devices/cmd";
743 String gsonMessage = gson.toJson(message);
744 sendDeviceMessage(topic, gsonMessage);
748 public void startEnergyMeter(String energyMeterId) {
749 NhcMessage2 message = new NhcMessage2();
751 message.method = "devices.control";
752 ArrayList<NhcMessageParam> params = new ArrayList<>();
753 NhcMessageParam param = new NhcMessageParam();
755 message.params = params;
756 ArrayList<NhcDevice2> devices = new ArrayList<>();
757 NhcDevice2 device = new NhcDevice2();
759 param.devices = devices;
760 device.uuid = energyMeterId;
761 ArrayList<NhcProperty> deviceProperties = new ArrayList<>();
763 NhcProperty reportInstantUsageProp = new NhcProperty();
764 deviceProperties.add(reportInstantUsageProp);
765 reportInstantUsageProp.reportInstantUsage = "True";
766 device.properties = deviceProperties;
768 String topic = profile + "/control/devices/cmd";
769 String gsonMessage = gson.toJson(message);
771 NhcEnergyMeter2 energyMeter = (NhcEnergyMeter2) energyMeters.get(energyMeterId);
772 if (energyMeter != null) {
773 energyMeter.startEnergyMeter(topic, gsonMessage);
778 public void stopEnergyMeter(String energyMeterId) {
779 NhcEnergyMeter2 energyMeter = (NhcEnergyMeter2) energyMeters.get(energyMeterId);
780 if (energyMeter != null) {
781 energyMeter.stopEnergyMeter();
786 * Method called from the {@link NhcEnergyMeter2} object to send message to Niko Home Control.
791 public void executeEnergyMeter(String topic, String gsonMessage) {
792 sendDeviceMessage(topic, gsonMessage);
795 private void sendDeviceMessage(String topic, String gsonMessage) {
797 mqttConnection.connectionPublish(topic, gsonMessage);
799 } catch (MqttException e) {
800 String message = e.getLocalizedMessage();
802 logger.debug("sending command failed, trying to restart communication");
803 restartCommunication();
804 // retry sending after restart
806 if (communicationActive()) {
807 mqttConnection.connectionPublish(topic, gsonMessage);
809 logger.debug("failed to restart communication");
811 } catch (MqttException e1) {
812 message = e1.getLocalizedMessage();
814 logger.debug("error resending device command");
816 if (!communicationActive()) {
817 message = (message != null) ? message : "@text/offline.communication-error";
818 connectionLost(message);
824 public void processMessage(String topic, byte[] payload) {
825 String message = new String(payload);
826 if ((profile + "/system/evt").equals(topic)) {
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);
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);
846 logger.trace("not acted on received message topic {}, payload {}", topic, message);
851 * @return system info retrieved from Connected Controller
853 public NhcSystemInfo2 getSystemInfo() {
854 NhcSystemInfo2 systemInfo = nhcSystemInfo;
855 if (systemInfo == null) {
856 systemInfo = new NhcSystemInfo2();
862 * @return time info retrieved from Connected Controller
864 public NhcTimeInfo2 getTimeInfo() {
865 NhcTimeInfo2 timeInfo = nhcTimeInfo;
866 if (timeInfo == null) {
867 timeInfo = new NhcTimeInfo2();
873 * @return comma separated list of services retrieved from Connected Controller
875 public String getServices() {
876 return services.stream().map(NhcService2::name).collect(Collectors.joining(", "));
880 public void connectionStateChanged(MqttConnectionState state, @Nullable Throwable error) {
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();
889 if (!communicationActive()) {
890 connectionLost(message);
893 logger.trace("Connection state: {}", state);