2 * Copyright (c) 2010-2024 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.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;
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;
59 * The {@link NikoHomeControlCommunication2} class is able to do the following tasks with Niko Home Control II
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.
68 * @author Mark Herwege - Initial Contribution
71 public class NikoHomeControlCommunication2 extends NikoHomeControlCommunication
72 implements MqttMessageSubscriber, MqttConnectionObserver {
74 private final Logger logger = LoggerFactory.getLogger(NikoHomeControlCommunication2.class);
76 private final NhcMqttConnection2 mqttConnection;
78 private final List<NhcService2> services = new CopyOnWriteArrayList<>();
80 private volatile String profile = "";
82 private volatile @Nullable NhcSystemInfo2 nhcSystemInfo;
83 private volatile @Nullable NhcTimeInfo2 nhcTimeInfo;
85 private volatile boolean initStarted = false;
86 private volatile @Nullable CompletableFuture<Boolean> communicationStarted;
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 java.net.UnknownHostException when the IP address is not provided
98 public NikoHomeControlCommunication2(NhcControllerEvent handler, String clientId,
99 ScheduledExecutorService scheduler) throws CertificateException {
100 super(handler, scheduler);
101 mqttConnection = new NhcMqttConnection2(clientId, this, this);
105 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);
130 } catch (MqttException e) {
131 logger.debug("error in mqtt communication");
132 handler.controllerOffline("@text/offline.communication-error");
133 scheduleRestartCommunication();
138 public synchronized void resetCommunication() {
139 CompletableFuture<Boolean> started = communicationStarted;
140 if (started != null) {
141 started.complete(false);
143 communicationStarted = null;
146 mqttConnection.stopConnection();
150 public boolean communicationActive() {
151 CompletableFuture<Boolean> started = communicationStarted;
152 if (started == null) {
156 // Wait until we received all devices info to confirm we are active.
157 return started.get(5000, TimeUnit.MILLISECONDS);
158 } catch (InterruptedException | ExecutionException | TimeoutException e) {
159 logger.debug("exception waiting for connection start");
165 * After setting up the communication with the Niko Home Control Connected Controller, send all initialization
169 private synchronized void initialize() {
172 NhcMessage2 message = new NhcMessage2();
175 message.method = "systeminfo.publish";
176 mqttConnection.connectionPublish(profile + "/system/cmd", gson.toJson(message));
178 message.method = "services.list";
179 mqttConnection.connectionPublish(profile + "/authentication/cmd", gson.toJson(message));
181 message.method = "devices.list";
182 mqttConnection.connectionPublish(profile + "/control/devices/cmd", gson.toJson(message));
184 message.method = "notifications.list";
185 mqttConnection.connectionPublish(profile + "/notification/cmd", gson.toJson(message));
186 } catch (MqttException e) {
188 logger.debug("error in mqtt communication during initialization");
189 resetCommunication();
193 private void connectionLost(String message) {
194 logger.debug("connection lost");
195 resetCommunication();
196 handler.controllerOffline(message);
199 private void systemEvt(String response) {
200 Type messageType = new TypeToken<NhcMessage2>() {
202 List<NhcTimeInfo2> timeInfo = null;
203 List<NhcSystemInfo2> systemInfo = null;
205 NhcMessage2 message = gson.fromJson(response, messageType);
206 List<NhcMessageParam> messageParams = (message != null) ? message.params : null;
207 if (messageParams != null) {
208 timeInfo = messageParams.stream().filter(p -> (p.timeInfo != null)).findFirst().get().timeInfo;
209 systemInfo = messageParams.stream().filter(p -> (p.systemInfo != null)).findFirst().get().systemInfo;
211 } catch (JsonSyntaxException e) {
212 logger.debug("unexpected json {}", response);
213 } catch (NoSuchElementException ignore) {
214 // Ignore if timeInfo not present in response, this should not happen in a timeInfo response
216 if (timeInfo != null) {
217 nhcTimeInfo = timeInfo.get(0);
219 if (systemInfo != null) {
220 nhcSystemInfo = systemInfo.get(0);
221 handler.updatePropertiesEvent();
225 private void systeminfoPublishRsp(String response) {
226 Type messageType = new TypeToken<NhcMessage2>() {
228 List<NhcSystemInfo2> systemInfo = null;
230 NhcMessage2 message = gson.fromJson(response, messageType);
231 List<NhcMessageParam> messageParams = (message != null) ? message.params : null;
232 if (messageParams != null) {
233 systemInfo = messageParams.stream().filter(p -> (p.systemInfo != null)).findFirst().get().systemInfo;
235 } catch (JsonSyntaxException e) {
236 logger.debug("unexpected json {}", response);
237 } catch (NoSuchElementException ignore) {
238 // Ignore if systemInfo not present in response, this should not happen in a systemInfo response
240 if (systemInfo != null) {
241 nhcSystemInfo = systemInfo.get(0);
245 private void servicesListRsp(String response) {
246 Type messageType = new TypeToken<NhcMessage2>() {
248 List<NhcService2> serviceList = null;
250 NhcMessage2 message = gson.fromJson(response, messageType);
251 List<NhcMessageParam> messageParams = (message != null) ? message.params : null;
252 if (messageParams != null) {
253 serviceList = messageParams.stream().filter(p -> (p.services != null)).findFirst().get().services;
255 } catch (JsonSyntaxException e) {
256 logger.debug("unexpected json {}", response);
257 } catch (NoSuchElementException ignore) {
258 // Ignore if services not present in response, this should not happen in a services response
261 if (serviceList != null) {
262 services.addAll(serviceList);
266 private void devicesListRsp(String response) {
267 Type messageType = new TypeToken<NhcMessage2>() {
269 List<NhcDevice2> deviceList = null;
271 NhcMessage2 message = gson.fromJson(response, messageType);
272 List<NhcMessageParam> messageParams = (message != null) ? message.params : null;
273 if (messageParams != null) {
274 deviceList = messageParams.stream().filter(p -> (p.devices != null)).findFirst().get().devices;
276 } catch (JsonSyntaxException e) {
277 logger.debug("unexpected json {}", response);
278 } catch (NoSuchElementException ignore) {
279 // Ignore if devices not present in response, this should not happen in a devices response
281 if (deviceList == null) {
285 for (NhcDevice2 device : deviceList) {
290 // Once a devices list response is received, we know the communication is fully started.
291 logger.debug("Communication start complete.");
292 handler.controllerOnline();
293 CompletableFuture<Boolean> future = communicationStarted;
294 if (future != null) {
295 future.complete(true);
299 private void devicesEvt(String response) {
300 Type messageType = new TypeToken<NhcMessage2>() {
302 List<NhcDevice2> deviceList = null;
303 String method = null;
305 NhcMessage2 message = gson.fromJson(response, messageType);
306 method = (message != null) ? message.method : null;
307 List<NhcMessageParam> messageParams = (message != null) ? message.params : null;
308 if (messageParams != null) {
309 deviceList = messageParams.stream().filter(p -> (p.devices != null)).findFirst().get().devices;
311 } catch (JsonSyntaxException e) {
312 logger.debug("unexpected json {}", response);
313 } catch (NoSuchElementException ignore) {
314 // Ignore if devices not present in response, this should not happen in a devices event
316 if (deviceList == null) {
320 if ("devices.removed".equals(method)) {
321 deviceList.forEach(this::removeDevice);
323 } else if ("devices.added".equals(method)) {
324 deviceList.forEach(this::addDevice);
327 deviceList.forEach(this::updateState);
330 private void notificationEvt(String response) {
331 Type messageType = new TypeToken<NhcMessage2>() {
333 List<NhcNotification2> notificationList = null;
335 NhcMessage2 message = gson.fromJson(response, messageType);
336 List<NhcMessageParam> messageParams = (message != null) ? message.params : null;
337 if (messageParams != null) {
338 notificationList = messageParams.stream().filter(p -> (p.notifications != null)).findFirst()
339 .get().notifications;
341 } catch (JsonSyntaxException e) {
342 logger.debug("unexpected json {}", response);
343 } catch (NoSuchElementException ignore) {
344 // Ignore if notifications not present in response, this should not happen in a notifications event
346 logger.debug("notifications {}", notificationList);
347 if (notificationList == null) {
351 for (NhcNotification2 notification : notificationList) {
352 if ("new".equals(notification.status)) {
353 String alarmText = notification.text;
354 switch (notification.type) {
356 handler.alarmEvent(alarmText);
359 handler.noticeEvent(alarmText);
362 logger.debug("unexpected message type {}", notification.type);
368 private void addDevice(NhcDevice2 device) {
369 String location = null;
370 List<NhcParameter> parameters = device.parameters;
371 if (parameters != null) {
372 location = parameters.stream().map(p -> p.locationName).filter(Objects::nonNull).findFirst().orElse(null);
375 if ("action".equals(device.type) || "virtual".equals(device.type)) {
376 ActionType actionType;
377 switch (device.model) {
384 case "overallcomfort":
386 actionType = ActionType.TRIGGER;
390 case "switched-generic":
393 actionType = ActionType.RELAY;
396 actionType = ActionType.DIMMER;
398 case "rolldownshutter":
400 case "venetianblind":
402 actionType = ActionType.ROLLERSHUTTER;
405 actionType = ActionType.GENERIC;
406 logger.debug("device type {} and model {} not recognised for {}, {}, ignoring", device.type,
407 device.model, device.uuid, device.name);
411 NhcAction nhcAction = actions.get(device.uuid);
412 if (nhcAction != null) {
413 // update name and location so discovery will see updated name and location
414 nhcAction.setName(device.name);
415 nhcAction.setLocation(location);
417 logger.debug("adding action device {} model {}, {}", device.uuid, device.model, device.name);
418 nhcAction = new NhcAction2(device.uuid, device.name, device.type, device.technology, device.model,
419 location, actionType, this);
421 actions.put(device.uuid, nhcAction);
422 } else if ("thermostat".equals(device.type)) {
423 NhcThermostat nhcThermostat = thermostats.get(device.uuid);
424 if (nhcThermostat != null) {
425 nhcThermostat.setName(device.name);
426 nhcThermostat.setLocation(location);
428 logger.debug("adding thermostat device {} model {}, {}", device.uuid, device.model, device.name);
429 nhcThermostat = new NhcThermostat2(device.uuid, device.name, device.type, device.technology,
430 device.model, location, this);
432 thermostats.put(device.uuid, nhcThermostat);
433 } else if ("centralmeter".equals(device.type) || "energyhome".equals(device.type)) {
434 NhcEnergyMeter nhcEnergyMeter = energyMeters.get(device.uuid);
435 if (nhcEnergyMeter != null) {
436 nhcEnergyMeter.setName(device.name);
437 nhcEnergyMeter.setLocation(location);
439 logger.debug("adding energy meter device {} model {}, {}", device.uuid, device.model, device.name);
440 nhcEnergyMeter = new NhcEnergyMeter2(device.uuid, device.name, device.type, device.technology,
441 device.model, location, this, scheduler);
443 energyMeters.put(device.uuid, nhcEnergyMeter);
445 logger.debug("device type {} and model {} not supported for {}, {}", device.type, device.model, device.uuid,
450 private void removeDevice(NhcDevice2 device) {
451 NhcAction action = actions.get(device.uuid);
452 NhcThermostat thermostat = thermostats.get(device.uuid);
453 NhcEnergyMeter energyMeter = energyMeters.get(device.uuid);
454 if (action != null) {
455 action.actionRemoved();
456 actions.remove(device.uuid);
457 } else if (thermostat != null) {
458 thermostat.thermostatRemoved();
459 thermostats.remove(device.uuid);
460 } else if (energyMeter != null) {
461 energyMeter.energyMeterRemoved();
462 energyMeters.remove(device.uuid);
466 private void updateState(NhcDevice2 device) {
467 List<NhcProperty> deviceProperties = device.properties;
468 if (deviceProperties == null) {
469 logger.debug("Cannot Update state for {} as no properties defined in device message", device.uuid);
473 NhcAction action = actions.get(device.uuid);
474 NhcThermostat thermostat = thermostats.get(device.uuid);
475 NhcEnergyMeter energyMeter = energyMeters.get(device.uuid);
477 if (action != null) {
478 updateActionState((NhcAction2) action, deviceProperties);
479 } else if (thermostat != null) {
480 updateThermostatState((NhcThermostat2) thermostat, deviceProperties);
481 } else if (energyMeter != null) {
482 updateEnergyMeterState((NhcEnergyMeter2) energyMeter, deviceProperties);
486 private void updateActionState(NhcAction2 action, List<NhcProperty> deviceProperties) {
487 if (action.getType() == ActionType.ROLLERSHUTTER) {
488 updateRollershutterState(action, deviceProperties);
490 updateLightState(action, deviceProperties);
494 private void updateLightState(NhcAction2 action, List<NhcProperty> deviceProperties) {
495 Optional<NhcProperty> statusProperty = deviceProperties.stream().filter(p -> (p.status != null)).findFirst();
496 Optional<NhcProperty> dimmerProperty = deviceProperties.stream().filter(p -> (p.brightness != null))
498 Optional<NhcProperty> basicStateProperty = deviceProperties.stream().filter(p -> (p.basicState != null))
501 String booleanState = null;
502 if (statusProperty.isPresent()) {
503 booleanState = statusProperty.get().status;
504 } else if (basicStateProperty.isPresent()) {
505 booleanState = basicStateProperty.get().basicState;
508 if (NHCOFF.equals(booleanState) || NHCFALSE.equals(booleanState)) {
509 action.setBooleanState(false);
510 logger.debug("setting action {} internally to OFF", action.getId());
513 if (dimmerProperty.isPresent()) {
514 String brightness = dimmerProperty.get().brightness;
515 if (brightness != null) {
517 action.setState(Integer.parseInt(brightness));
518 logger.debug("setting action {} internally to {}", action.getId(), dimmerProperty.get().brightness);
519 } catch (NumberFormatException e) {
520 logger.debug("received invalid brightness value {} for dimmer {}", brightness, action.getId());
525 if (NHCON.equals(booleanState) || NHCTRUE.equals(booleanState)) {
526 action.setBooleanState(true);
527 logger.debug("setting action {} internally to ON", action.getId());
531 private void updateRollershutterState(NhcAction2 action, List<NhcProperty> deviceProperties) {
532 deviceProperties.stream().map(p -> p.position).filter(Objects::nonNull).findFirst().ifPresent(position -> {
534 action.setState(Integer.parseInt(position));
535 logger.debug("setting action {} internally to {}", action.getId(), position);
536 } catch (NumberFormatException e) {
537 logger.trace("received empty or invalid rollershutter {} position info {}", action.getId(), position);
542 private void updateThermostatState(NhcThermostat2 thermostat, List<NhcProperty> deviceProperties) {
543 Optional<Boolean> overruleActiveProperty = deviceProperties.stream().map(p -> p.overruleActive)
544 .filter(Objects::nonNull).map(t -> Boolean.parseBoolean(t)).findFirst();
545 Optional<Integer> overruleSetpointProperty = deviceProperties.stream().map(p -> p.overruleSetpoint)
546 .map(s -> (!((s == null) || s.isEmpty())) ? Math.round(Float.parseFloat(s) * 10) : null)
547 .filter(Objects::nonNull).findFirst();
548 Optional<Integer> overruleTimeProperty = deviceProperties.stream().map(p -> p.overruleTime)
549 .map(s -> (!((s == null) || s.isEmpty())) ? Math.round(Float.parseFloat(s)) : null)
550 .filter(Objects::nonNull).findFirst();
551 Optional<Integer> setpointTemperatureProperty = deviceProperties.stream().map(p -> p.setpointTemperature)
552 .map(s -> (!((s == null) || s.isEmpty())) ? Math.round(Float.parseFloat(s) * 10) : null)
553 .filter(Objects::nonNull).findFirst();
554 Optional<Boolean> ecoSaveProperty = deviceProperties.stream().map(p -> p.ecoSave)
555 .map(s -> s != null ? Boolean.parseBoolean(s) : null).filter(Objects::nonNull).findFirst();
556 Optional<Integer> ambientTemperatureProperty = deviceProperties.stream().map(p -> p.ambientTemperature)
557 .map(s -> (!((s == null) || s.isEmpty())) ? Math.round(Float.parseFloat(s) * 10) : null)
558 .filter(Objects::nonNull).findFirst();
559 Optional<@Nullable String> demandProperty = deviceProperties.stream().map(p -> p.demand)
560 .filter(Objects::nonNull).findFirst();
561 Optional<@Nullable String> operationModeProperty = deviceProperties.stream().map(p -> p.operationMode)
562 .filter(Objects::nonNull).findFirst();
564 String modeString = deviceProperties.stream().map(p -> p.program).filter(Objects::nonNull).findFirst()
566 int mode = IntStream.range(0, THERMOSTATMODES.length).filter(i -> THERMOSTATMODES[i].equals(modeString))
567 .findFirst().orElse(thermostat.getMode());
569 int measured = ambientTemperatureProperty.orElse(thermostat.getMeasured());
570 int setpoint = setpointTemperatureProperty.orElse(thermostat.getSetpoint());
573 int overruletime = 0;
574 if (overruleActiveProperty.orElse(true)) {
575 overrule = overruleSetpointProperty.orElse(thermostat.getOverrule());
576 overruletime = overruleTimeProperty.orElse(thermostat.getRemainingOverruletime());
579 int ecosave = thermostat.getEcosave();
580 if (ecoSaveProperty.orElse(false)) {
584 int demand = thermostat.getDemand();
585 String demandString = demandProperty.orElse(operationModeProperty.orElse(""));
586 demandString = demandString == null ? "" : demandString;
587 switch (demandString) {
600 "Niko Home Control: setting thermostat {} with measured {}, setpoint {}, mode {}, overrule {}, overruletime {}, ecosave {}, demand {}",
601 thermostat.getId(), measured, setpoint, mode, overrule, overruletime, ecosave, demand);
602 thermostat.updateState(measured, setpoint, mode, overrule, overruletime, ecosave, demand);
605 private void updateEnergyMeterState(NhcEnergyMeter2 energyMeter, List<NhcProperty> deviceProperties) {
607 Optional<Integer> electricalPower = deviceProperties.stream().map(p -> p.electricalPower)
608 .map(s -> (!((s == null) || s.isEmpty())) ? Math.round(Float.parseFloat(s)) : null)
609 .filter(Objects::nonNull).findFirst();
610 Optional<Integer> powerFromGrid = deviceProperties.stream().map(p -> p.electricalPowerFromGrid)
611 .map(s -> (!((s == null) || s.isEmpty())) ? Math.round(Float.parseFloat(s)) : null)
612 .filter(Objects::nonNull).findFirst();
613 Optional<Integer> powerToGrid = deviceProperties.stream().map(p -> p.electricalPowerToGrid)
614 .map(s -> (!((s == null) || s.isEmpty())) ? Math.round(Float.parseFloat(s)) : null)
615 .filter(Objects::nonNull).findFirst();
616 int power = electricalPower.orElse(powerFromGrid.orElse(0) - powerToGrid.orElse(0));
617 logger.trace("setting energy meter {} power to {}", energyMeter.getId(), electricalPower);
618 energyMeter.setPower(power);
619 } catch (NumberFormatException e) {
620 energyMeter.setPower(null);
621 logger.trace("wrong format in energy meter {} power reading", energyMeter.getId());
626 public void executeAction(String actionId, String value) {
627 NhcMessage2 message = new NhcMessage2();
629 message.method = "devices.control";
630 ArrayList<NhcMessageParam> params = new ArrayList<>();
631 NhcMessageParam param = new NhcMessageParam();
633 message.params = params;
634 ArrayList<NhcDevice2> devices = new ArrayList<>();
635 NhcDevice2 device = new NhcDevice2();
637 param.devices = devices;
638 device.uuid = actionId;
639 ArrayList<NhcProperty> deviceProperties = new ArrayList<>();
640 NhcProperty property = new NhcProperty();
641 deviceProperties.add(property);
642 device.properties = deviceProperties;
644 NhcAction2 action = (NhcAction2) actions.get(actionId);
645 if (action == null) {
649 switch (action.getType()) {
652 property.basicState = NHCTRIGGERED;
655 property.status = value;
658 if (NHCON.equals(value)) {
659 action.setBooleanState(true); // this will trigger sending the stored brightness value event out
660 property.status = value;
661 } else if (NHCOFF.equals(value)) {
662 property.status = value;
665 action.setState(Integer.parseInt(value)); // set cached state to new brightness value to avoid
666 // switching on with old brightness value before
669 } catch (NumberFormatException e) {
670 logger.debug("internal error, trying to set invalid brightness value {} for dimmer {}", value,
675 // If the light is off, turn the light on before sending the brightness value, needs to happen
676 // in 2 separate messages.
677 if (!action.booleanState()) {
678 executeAction(actionId, NHCON);
680 property.brightness = value;
684 if (NHCSTOP.equals(value)) {
685 property.action = value;
686 } else if (NHCUP.equals(value)) {
687 property.position = "100";
688 } else if (NHCDOWN.equals(value)) {
689 property.position = "0";
691 property.position = value;
696 String topic = profile + "/control/devices/cmd";
697 String gsonMessage = gson.toJson(message);
698 sendDeviceMessage(topic, gsonMessage);
702 public void executeThermostat(String thermostatId, String mode) {
703 NhcMessage2 message = new NhcMessage2();
705 message.method = "devices.control";
706 ArrayList<NhcMessageParam> params = new ArrayList<>();
707 NhcMessageParam param = new NhcMessageParam();
709 message.params = params;
710 ArrayList<NhcDevice2> devices = new ArrayList<>();
711 NhcDevice2 device = new NhcDevice2();
713 param.devices = devices;
714 device.uuid = thermostatId;
715 ArrayList<NhcProperty> deviceProperties = new ArrayList<>();
717 NhcProperty overruleActiveProp = new NhcProperty();
718 deviceProperties.add(overruleActiveProp);
719 overruleActiveProp.overruleActive = "False";
721 NhcProperty program = new NhcProperty();
722 deviceProperties.add(program);
723 program.program = mode;
725 device.properties = deviceProperties;
727 String topic = profile + "/control/devices/cmd";
728 String gsonMessage = gson.toJson(message);
729 sendDeviceMessage(topic, gsonMessage);
733 public void executeThermostat(String thermostatId, int overruleTemp, int overruleTime) {
734 NhcMessage2 message = new NhcMessage2();
736 message.method = "devices.control";
737 ArrayList<NhcMessageParam> params = new ArrayList<>();
738 NhcMessageParam param = new NhcMessageParam();
740 message.params = params;
741 ArrayList<NhcDevice2> devices = new ArrayList<>();
742 NhcDevice2 device = new NhcDevice2();
744 param.devices = devices;
745 device.uuid = thermostatId;
746 ArrayList<NhcProperty> deviceProperties = new ArrayList<>();
748 if (overruleTime > 0) {
749 NhcProperty overruleActiveProp = new NhcProperty();
750 overruleActiveProp.overruleActive = "True";
751 deviceProperties.add(overruleActiveProp);
753 NhcProperty overruleSetpointProp = new NhcProperty();
754 overruleSetpointProp.overruleSetpoint = String.valueOf(overruleTemp / 10.0);
755 deviceProperties.add(overruleSetpointProp);
757 NhcProperty overruleTimeProp = new NhcProperty();
758 overruleTimeProp.overruleTime = String.valueOf(overruleTime);
759 deviceProperties.add(overruleTimeProp);
761 NhcProperty overruleActiveProp = new NhcProperty();
762 overruleActiveProp.overruleActive = "False";
763 deviceProperties.add(overruleActiveProp);
765 device.properties = deviceProperties;
767 String topic = profile + "/control/devices/cmd";
768 String gsonMessage = gson.toJson(message);
769 sendDeviceMessage(topic, gsonMessage);
773 public void startEnergyMeter(String energyMeterId) {
774 NhcMessage2 message = new NhcMessage2();
776 message.method = "devices.control";
777 ArrayList<NhcMessageParam> params = new ArrayList<>();
778 NhcMessageParam param = new NhcMessageParam();
780 message.params = params;
781 ArrayList<NhcDevice2> devices = new ArrayList<>();
782 NhcDevice2 device = new NhcDevice2();
784 param.devices = devices;
785 device.uuid = energyMeterId;
786 ArrayList<NhcProperty> deviceProperties = new ArrayList<>();
788 NhcProperty reportInstantUsageProp = new NhcProperty();
789 deviceProperties.add(reportInstantUsageProp);
790 reportInstantUsageProp.reportInstantUsage = "True";
791 device.properties = deviceProperties;
793 String topic = profile + "/control/devices/cmd";
794 String gsonMessage = gson.toJson(message);
796 NhcEnergyMeter2 energyMeter = (NhcEnergyMeter2) energyMeters.get(energyMeterId);
797 if (energyMeter != null) {
798 energyMeter.startEnergyMeter(topic, gsonMessage);
803 public void stopEnergyMeter(String energyMeterId) {
804 NhcEnergyMeter2 energyMeter = (NhcEnergyMeter2) energyMeters.get(energyMeterId);
805 if (energyMeter != null) {
806 energyMeter.stopEnergyMeter();
811 * Method called from the {@link NhcEnergyMeter2} object to send message to Niko Home Control.
816 public void executeEnergyMeter(String topic, String gsonMessage) {
817 sendDeviceMessage(topic, gsonMessage);
820 private void sendDeviceMessage(String topic, String gsonMessage) {
822 mqttConnection.connectionPublish(topic, gsonMessage);
824 } catch (MqttException e) {
825 String message = e.getLocalizedMessage();
827 logger.debug("sending command failed, trying to restart communication");
828 restartCommunication();
829 // retry sending after restart
831 if (communicationActive()) {
832 mqttConnection.connectionPublish(topic, gsonMessage);
834 logger.debug("failed to restart communication");
836 } catch (MqttException e1) {
837 message = e1.getLocalizedMessage();
839 logger.debug("error resending device command");
841 if (!communicationActive()) {
842 message = (message != null) ? message : "@text/offline.communication-error";
843 connectionLost(message);
844 // Keep on trying to restart, but don't send message anymore
845 scheduleRestartCommunication();
851 public void processMessage(String topic, byte[] payload) {
852 String message = new String(payload);
853 if ((profile + "/system/evt").equals(topic)) {
855 } else if ((profile + "/system/rsp").equals(topic)) {
856 logger.debug("received topic {}, payload {}", topic, message);
857 systeminfoPublishRsp(message);
858 } else if ((profile + "/notification/evt").equals(topic)) {
859 logger.debug("received topic {}, payload {}", topic, message);
860 notificationEvt(message);
861 } else if ((profile + "/control/devices/evt").equals(topic)) {
862 logger.trace("received topic {}, payload {}", topic, message);
864 } else if ((profile + "/control/devices/rsp").equals(topic)) {
865 logger.debug("received topic {}, payload {}", topic, message);
866 devicesListRsp(message);
867 } else if ((profile + "/authentication/rsp").equals(topic)) {
868 logger.debug("received topic {}, payload {}", topic, message);
869 servicesListRsp(message);
870 } else if ((profile + "/control/devices.error").equals(topic)) {
871 logger.warn("received error {}", message);
873 logger.trace("not acted on received message topic {}, payload {}", topic, message);
878 * @return system info retrieved from Connected Controller
880 public NhcSystemInfo2 getSystemInfo() {
881 NhcSystemInfo2 systemInfo = nhcSystemInfo;
882 if (systemInfo == null) {
883 systemInfo = new NhcSystemInfo2();
889 * @return time info retrieved from Connected Controller
891 public NhcTimeInfo2 getTimeInfo() {
892 NhcTimeInfo2 timeInfo = nhcTimeInfo;
893 if (timeInfo == null) {
894 timeInfo = new NhcTimeInfo2();
900 * @return comma separated list of services retrieved from Connected Controller
902 public String getServices() {
903 return services.stream().map(NhcService2::name).collect(Collectors.joining(", "));
907 public void connectionStateChanged(MqttConnectionState state, @Nullable Throwable error) {
908 // do in separate thread as this method needs to return early
909 scheduler.submit(() -> {
911 logger.debug("Connection state: {}, error", state, error);
912 String localizedMessage = error.getLocalizedMessage();
913 String message = (localizedMessage != null) ? localizedMessage : "@text/offline.communication-error";
914 connectionLost(message);
915 scheduleRestartCommunication();
916 } else if ((state == MqttConnectionState.CONNECTED) && !initStarted) {
919 logger.trace("Connection state: {}", state);