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.NhcAccess;
37 import org.openhab.binding.nikohomecontrol.internal.protocol.NhcAction;
38 import org.openhab.binding.nikohomecontrol.internal.protocol.NhcAlarm;
39 import org.openhab.binding.nikohomecontrol.internal.protocol.NhcControllerEvent;
40 import org.openhab.binding.nikohomecontrol.internal.protocol.NhcMeter;
41 import org.openhab.binding.nikohomecontrol.internal.protocol.NhcThermostat;
42 import org.openhab.binding.nikohomecontrol.internal.protocol.NhcVideo;
43 import org.openhab.binding.nikohomecontrol.internal.protocol.NikoHomeControlCommunication;
44 import org.openhab.binding.nikohomecontrol.internal.protocol.NikoHomeControlConstants.AccessType;
45 import org.openhab.binding.nikohomecontrol.internal.protocol.NikoHomeControlConstants.ActionType;
46 import org.openhab.binding.nikohomecontrol.internal.protocol.NikoHomeControlConstants.MeterType;
47 import org.openhab.binding.nikohomecontrol.internal.protocol.nhc2.NhcDevice2.NhcParameter;
48 import org.openhab.binding.nikohomecontrol.internal.protocol.nhc2.NhcDevice2.NhcProperty;
49 import org.openhab.binding.nikohomecontrol.internal.protocol.nhc2.NhcDevice2.NhcTrait;
50 import org.openhab.binding.nikohomecontrol.internal.protocol.nhc2.NhcMessage2.NhcMessageParam;
51 import org.openhab.core.io.transport.mqtt.MqttConnectionObserver;
52 import org.openhab.core.io.transport.mqtt.MqttConnectionState;
53 import org.openhab.core.io.transport.mqtt.MqttException;
54 import org.openhab.core.io.transport.mqtt.MqttMessageSubscriber;
55 import org.slf4j.Logger;
56 import org.slf4j.LoggerFactory;
58 import com.google.gson.FieldNamingPolicy;
59 import com.google.gson.Gson;
60 import com.google.gson.GsonBuilder;
61 import com.google.gson.JsonSyntaxException;
62 import com.google.gson.reflect.TypeToken;
65 * The {@link NikoHomeControlCommunication2} class is able to do the following tasks with Niko Home Control II
68 * <li>Start and stop MQTT connection with Niko Home Control II Connected Controller.
69 * <li>Read all setup and status information from the Niko Home Control Controller.
70 * <li>Execute Niko Home Control commands.
71 * <li>Listen for events from Niko Home Control.
74 * @author Mark Herwege - Initial Contribution
77 public class NikoHomeControlCommunication2 extends NikoHomeControlCommunication
78 implements MqttMessageSubscriber, MqttConnectionObserver {
80 private final Logger logger = LoggerFactory.getLogger(NikoHomeControlCommunication2.class);
82 private final NhcMqttConnection2 mqttConnection;
84 private final List<NhcService2> services = new CopyOnWriteArrayList<>();
86 private volatile String profile = "";
88 private volatile @Nullable NhcSystemInfo2 nhcSystemInfo;
89 private volatile @Nullable NhcTimeInfo2 nhcTimeInfo;
91 private volatile boolean initStarted = false;
92 private volatile @Nullable CompletableFuture<Boolean> communicationStarted;
94 private final Gson gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.UPPER_CAMEL_CASE).create();
97 * Constructor for Niko Home Control communication object, manages communication with
98 * Niko Home Control II Connected Controller.
100 * @throws CertificateException when the SSL context for MQTT communication cannot be created
101 * @throws java.net.UnknownHostException when the IP address is not provided
104 public NikoHomeControlCommunication2(NhcControllerEvent handler, String clientId,
105 ScheduledExecutorService scheduler) throws CertificateException {
106 super(handler, scheduler);
107 mqttConnection = new NhcMqttConnection2(clientId, this, this);
111 public synchronized void startCommunication() {
113 communicationStarted = new CompletableFuture<>();
115 InetAddress addr = handler.getAddr();
117 logger.warn("IP address cannot be empty");
121 String addrString = addr.getHostAddress();
122 int port = handler.getPort();
123 logger.debug("initializing for mqtt connection to CoCo on {}:{}", addrString, port);
125 profile = handler.getProfile();
127 String token = handler.getToken();
128 if (token.isEmpty()) {
129 logger.warn("JWT token cannot be empty");
135 mqttConnection.startConnection(addrString, port, profile, token);
136 } catch (MqttException e) {
137 logger.debug("error in mqtt communication");
138 handler.controllerOffline("@text/offline.communication-error");
139 scheduleRestartCommunication();
144 public synchronized void resetCommunication() {
145 CompletableFuture<Boolean> started = communicationStarted;
146 if (started != null) {
147 started.complete(false);
149 communicationStarted = null;
152 mqttConnection.stopConnection();
156 public boolean communicationActive() {
157 CompletableFuture<Boolean> started = communicationStarted;
158 if (started == null) {
162 // Wait until we received all devices info to confirm we are active.
163 return started.get(5000, TimeUnit.MILLISECONDS);
164 } catch (InterruptedException | ExecutionException | TimeoutException e) {
165 logger.debug("exception waiting for connection start: {}", e.toString());
171 * After setting up the communication with the Niko Home Control Connected Controller, send all initialization
175 private synchronized void initialize() {
178 NhcMessage2 message = new NhcMessage2();
181 message.method = "systeminfo.publish";
182 mqttConnection.connectionPublish(profile + "/system/cmd", gson.toJson(message));
184 message.method = "services.list";
185 mqttConnection.connectionPublish(profile + "/authentication/cmd", gson.toJson(message));
187 message.method = "devices.list";
188 mqttConnection.connectionPublish(profile + "/control/devices/cmd", gson.toJson(message));
190 message.method = "notifications.list";
191 mqttConnection.connectionPublish(profile + "/notification/cmd", gson.toJson(message));
192 } catch (MqttException e) {
194 logger.debug("error in mqtt communication during initialization");
195 resetCommunication();
199 private void connectionLost(String message) {
200 logger.debug("connection lost");
201 resetCommunication();
202 handler.controllerOffline(message);
205 private void systemEvt(String response) {
206 Type messageType = new TypeToken<NhcMessage2>() {
208 List<NhcTimeInfo2> timeInfo = null;
209 List<NhcSystemInfo2> systemInfo = null;
211 NhcMessage2 message = gson.fromJson(response, messageType);
212 List<NhcMessageParam> messageParams = (message != null) ? message.params : null;
213 if (messageParams != null) {
214 timeInfo = messageParams.stream().filter(p -> (p.timeInfo != null)).findFirst().get().timeInfo;
215 systemInfo = messageParams.stream().filter(p -> (p.systemInfo != null)).findFirst().get().systemInfo;
217 } catch (JsonSyntaxException e) {
218 logger.debug("unexpected json {}", response);
219 } catch (NoSuchElementException ignore) {
220 // Ignore if timeInfo not present in response, this should not happen in a timeInfo response
222 if (timeInfo != null) {
223 nhcTimeInfo = timeInfo.get(0);
225 if (systemInfo != null) {
226 nhcSystemInfo = systemInfo.get(0);
227 handler.updatePropertiesEvent();
231 private void systeminfoPublishRsp(String response) {
232 Type messageType = new TypeToken<NhcMessage2>() {
234 List<NhcSystemInfo2> systemInfo = null;
236 NhcMessage2 message = gson.fromJson(response, messageType);
237 List<NhcMessageParam> messageParams = (message != null) ? message.params : null;
238 if (messageParams != null) {
239 systemInfo = messageParams.stream().filter(p -> (p.systemInfo != null)).findFirst().get().systemInfo;
241 } catch (JsonSyntaxException e) {
242 logger.debug("unexpected json {}", response);
243 } catch (NoSuchElementException ignore) {
244 // Ignore if systemInfo not present in response, this should not happen in a systemInfo response
246 if (systemInfo != null) {
247 nhcSystemInfo = systemInfo.get(0);
251 private void servicesListRsp(String response) {
252 Type messageType = new TypeToken<NhcMessage2>() {
254 List<NhcService2> serviceList = null;
256 NhcMessage2 message = gson.fromJson(response, messageType);
257 List<NhcMessageParam> messageParams = (message != null) ? message.params : null;
258 if (messageParams != null) {
259 serviceList = messageParams.stream().filter(p -> (p.services != null)).findFirst().get().services;
261 } catch (JsonSyntaxException e) {
262 logger.debug("unexpected json {}", response);
263 } catch (NoSuchElementException ignore) {
264 // Ignore if services not present in response, this should not happen in a services response
267 if (serviceList != null) {
268 services.addAll(serviceList);
272 private void devicesListRsp(String response) {
273 Type messageType = new TypeToken<NhcMessage2>() {
275 List<NhcDevice2> deviceList = null;
277 NhcMessage2 message = gson.fromJson(response, messageType);
278 List<NhcMessageParam> messageParams = (message != null) ? message.params : null;
279 if (messageParams != null) {
280 deviceList = messageParams.stream().filter(p -> (p.devices != null)).findFirst().get().devices;
282 } catch (JsonSyntaxException e) {
283 logger.debug("unexpected json {}", response);
284 } catch (NoSuchElementException ignore) {
285 // Ignore if devices not present in response, this should not happen in a devices response
287 if (deviceList == null) {
291 for (NhcDevice2 device : deviceList) {
296 // Once a devices list response is received, we know the communication is fully started.
297 logger.debug("Communication start complete.");
298 handler.controllerOnline();
299 CompletableFuture<Boolean> future = communicationStarted;
300 if (future != null) {
301 future.complete(true);
305 private void devicesEvt(String response) {
306 Type messageType = new TypeToken<NhcMessage2>() {
308 List<NhcDevice2> deviceList = null;
309 String method = null;
311 NhcMessage2 message = gson.fromJson(response, messageType);
312 method = (message != null) ? message.method : null;
313 List<NhcMessageParam> messageParams = (message != null) ? message.params : null;
314 if (messageParams != null) {
315 deviceList = messageParams.stream().filter(p -> (p.devices != null)).findFirst().get().devices;
317 } catch (JsonSyntaxException e) {
318 logger.debug("unexpected json {}", response);
319 } catch (NoSuchElementException ignore) {
320 // Ignore if devices not present in response, this should not happen in a devices event
322 if (deviceList == null) {
326 if ("devices.removed".equals(method)) {
327 deviceList.forEach(this::removeDevice);
329 } else if ("devices.added".equals(method)) {
330 deviceList.forEach(this::addDevice);
333 deviceList.forEach(this::updateState);
336 private void notificationEvt(String response) {
337 Type messageType = new TypeToken<NhcMessage2>() {
339 List<NhcNotification2> notificationList = null;
341 NhcMessage2 message = gson.fromJson(response, messageType);
342 List<NhcMessageParam> messageParams = (message != null) ? message.params : null;
343 if (messageParams != null) {
344 notificationList = messageParams.stream().filter(p -> (p.notifications != null)).findFirst()
345 .get().notifications;
347 } catch (JsonSyntaxException e) {
348 logger.debug("unexpected json {}", response);
349 } catch (NoSuchElementException ignore) {
350 // Ignore if notifications not present in response, this should not happen in a notifications event
352 logger.debug("notifications {}", notificationList);
353 if (notificationList == null) {
357 for (NhcNotification2 notification : notificationList) {
358 if ("new".equals(notification.status)) {
359 String alarmText = notification.text;
360 switch (notification.type) {
362 handler.alarmEvent(alarmText);
365 handler.noticeEvent(alarmText);
368 logger.debug("unexpected message type {}", notification.type);
374 private void addDevice(NhcDevice2 device) {
375 String location = null;
376 List<NhcParameter> parameters = device.parameters;
377 if (parameters != null) {
378 location = parameters.stream().map(p -> p.locationName).filter(Objects::nonNull).findFirst().orElse(null);
381 if ("videodoorstation".equals(device.type) || "vds".equals(device.type)) {
382 addVideoDevice(device);
383 } else if ("accesscontrol".equals(device.model) || "bellbutton".equals(device.model)) {
384 addAccessDevice(device, location);
385 } else if ("alarms".equals(device.model) && (device.properties != null)
386 && (device.properties.stream().anyMatch(p -> (p.alarmActive != null)))) {
387 addAlarmDevice(device, location);
388 } else if ("action".equals(device.type) || "virtual".equals(device.type)) {
389 addActionDevice(device, location);
390 } else if ("thermostat".equals(device.type)) {
391 addThermostatDevice(device, location);
392 } else if ("centralmeter".equals(device.type) || "energyhome".equals(device.type)) {
393 addMeterDevice(device, location);
395 logger.debug("device type {} and model {} not supported for {}, {}", device.type, device.model, device.uuid,
400 private void addActionDevice(NhcDevice2 device, @Nullable String location) {
401 ActionType actionType;
402 switch (device.model) {
409 case "overallcomfort":
411 actionType = ActionType.TRIGGER;
415 case "switched-generic":
418 actionType = ActionType.RELAY;
421 actionType = ActionType.DIMMER;
423 case "rolldownshutter":
425 case "venetianblind":
427 actionType = ActionType.ROLLERSHUTTER;
430 actionType = ActionType.GENERIC;
431 logger.debug("device type {} and model {} not recognised for {}, {}, ignoring", device.type,
432 device.model, device.uuid, device.name);
436 NhcAction nhcAction = actions.get(device.uuid);
437 if (nhcAction != null) {
438 // update name and location so discovery will see updated name and location
439 nhcAction.setName(device.name);
440 nhcAction.setLocation(location);
442 logger.debug("adding action device {} model {}, {}", device.uuid, device.model, device.name);
443 nhcAction = new NhcAction2(device.uuid, device.name, device.type, device.technology, device.model, location,
446 actions.put(device.uuid, nhcAction);
449 private void addThermostatDevice(NhcDevice2 device, @Nullable String location) {
450 NhcThermostat nhcThermostat = thermostats.get(device.uuid);
451 if (nhcThermostat != null) {
452 nhcThermostat.setName(device.name);
453 nhcThermostat.setLocation(location);
455 logger.debug("adding thermostat device {} model {}, {}", device.uuid, device.model, device.name);
456 nhcThermostat = new NhcThermostat2(device.uuid, device.name, device.type, device.technology, device.model,
459 thermostats.put(device.uuid, nhcThermostat);
462 private void addMeterDevice(NhcDevice2 device, @Nullable String location) {
463 NhcMeter nhcMeter = meters.get(device.uuid);
464 if (nhcMeter != null) {
465 nhcMeter.setName(device.name);
466 nhcMeter.setLocation(location);
468 logger.debug("adding energy meter device {} model {}, {}", device.uuid, device.model, device.name);
469 nhcMeter = new NhcMeter2(device.uuid, device.name, MeterType.ENERGY_LIVE, device.type, device.technology,
470 device.model, null, location, this, scheduler);
472 meters.put(device.uuid, nhcMeter);
475 private void addAccessDevice(NhcDevice2 device, @Nullable String location) {
476 AccessType accessType = AccessType.BASE;
477 if ("bellbutton".equals(device.model)) {
478 accessType = AccessType.BELLBUTTON;
480 List<NhcProperty> properties = device.properties;
481 if (properties != null) {
482 boolean hasBasicState = properties.stream().anyMatch(p -> (p.basicState != null));
484 accessType = AccessType.RINGANDCOMEIN;
489 NhcAccess2 nhcAccess = (NhcAccess2) accessDevices.get(device.uuid);
490 if (nhcAccess != null) {
491 nhcAccess.setName(device.name);
492 nhcAccess.setLocation(location);
494 String buttonId = null;
495 List<NhcParameter> parameters = device.parameters;
496 if (parameters != null) {
497 buttonId = parameters.stream().map(p -> p.buttonId).filter(Objects::nonNull).findFirst().orElse(null);
500 logger.debug("adding access device {} model {} type {}, {}", device.uuid, device.model, accessType,
502 nhcAccess = new NhcAccess2(device.uuid, device.name, device.type, device.technology, device.model, location,
503 accessType, buttonId, this);
505 if (buttonId != null) {
506 NhcAccess2 access = nhcAccess;
507 String macAddress = buttonId.split("_")[0];
508 videoDevices.forEach((key, videoDevice) -> {
509 if (macAddress.equals(videoDevice.getMacAddress())) {
510 int buttonIndex = access.getButtonIndex();
511 logger.debug("link access device {} to video device {} button {}", device.uuid,
512 videoDevice.getId(), buttonIndex);
513 videoDevice.setNhcAccess(buttonIndex, access);
514 access.setNhcVideo(videoDevice);
519 accessDevices.put(device.uuid, nhcAccess);
522 private void addVideoDevice(NhcDevice2 device) {
523 NhcVideo2 nhcVideo = (NhcVideo2) videoDevices.get(device.uuid);
524 if (nhcVideo != null) {
525 nhcVideo.setName(device.name);
527 String macAddress = null;
528 String ipAddress = null;
529 String mjpegUri = null;
531 List<NhcTrait> traits = device.traits;
532 if (traits != null) {
533 macAddress = traits.stream().map(t -> t.macAddress).filter(Objects::nonNull).findFirst().orElse(null);
535 List<NhcParameter> parameters = device.parameters;
536 if (parameters != null) {
537 mjpegUri = parameters.stream().map(p -> p.mjpegUri).filter(Objects::nonNull).findFirst().orElse(null);
538 tnUri = parameters.stream().map(p -> p.tnUri).filter(Objects::nonNull).findFirst().orElse(null);
540 List<NhcProperty> properties = device.properties;
541 if (properties != null) {
542 ipAddress = properties.stream().map(p -> p.ipAddress).filter(Objects::nonNull).findFirst().orElse(null);
545 logger.debug("adding video device {} model {}, {}", device.uuid, device.model, device.name);
546 nhcVideo = new NhcVideo2(device.uuid, device.name, device.type, device.technology, device.model, macAddress,
547 ipAddress, mjpegUri, tnUri, this);
549 if (macAddress != null) {
550 NhcVideo2 video = nhcVideo;
551 String mac = macAddress;
552 accessDevices.forEach((key, accessDevice) -> {
553 NhcAccess2 access = (NhcAccess2) accessDevice;
554 String buttonMac = access.getButtonId();
555 if (buttonMac != null) {
556 buttonMac = buttonMac.split("_")[0];
557 if (mac.equals(buttonMac)) {
558 int buttonIndex = access.getButtonIndex();
559 logger.debug("link access device {} to video device {} button {}", accessDevice.getId(),
560 device.uuid, buttonIndex);
561 video.setNhcAccess(buttonIndex, access);
562 access.setNhcVideo(video);
568 videoDevices.put(device.uuid, nhcVideo);
571 private void addAlarmDevice(NhcDevice2 device, @Nullable String location) {
572 NhcAlarm nhcAlarm = alarmDevices.get(device.uuid);
573 if (nhcAlarm != null) {
574 nhcAlarm.setName(device.name);
575 nhcAlarm.setLocation(location);
577 logger.debug("adding alarm device {} model {}, {}", device.uuid, device.model, device.name);
578 nhcAlarm = new NhcAlarm2(device.uuid, device.name, device.type, device.technology, device.model, location,
581 alarmDevices.put(device.uuid, nhcAlarm);
584 private void removeDevice(NhcDevice2 device) {
585 NhcAction action = actions.get(device.uuid);
586 NhcThermostat thermostat = thermostats.get(device.uuid);
587 NhcMeter meter = meters.get(device.uuid);
588 NhcAccess access = accessDevices.get(device.uuid);
589 NhcVideo video = videoDevices.get(device.uuid);
590 NhcAlarm alarm = alarmDevices.get(device.uuid);
591 if (action != null) {
592 action.actionRemoved();
593 actions.remove(device.uuid);
594 } else if (thermostat != null) {
595 thermostat.thermostatRemoved();
596 thermostats.remove(device.uuid);
597 } else if (meter != null) {
598 meter.meterRemoved();
599 meters.remove(device.uuid);
600 } else if (access != null) {
601 access.accessDeviceRemoved();
602 accessDevices.remove(device.uuid);
603 } else if (video != null) {
604 video.videoDeviceRemoved();
605 videoDevices.remove(device.uuid);
606 } else if (alarm != null) {
607 alarm.alarmDeviceRemoved();
608 alarmDevices.remove(device.uuid);
612 private void updateState(NhcDevice2 device) {
613 List<NhcProperty> deviceProperties = device.properties;
614 if (deviceProperties == null) {
615 logger.debug("Cannot Update state for {} as no properties defined in device message", device.uuid);
619 NhcAction action = actions.get(device.uuid);
620 NhcThermostat thermostat = thermostats.get(device.uuid);
621 NhcMeter meter = meters.get(device.uuid);
622 NhcAccess accessDevice = accessDevices.get(device.uuid);
623 NhcVideo videoDevice = videoDevices.get(device.uuid);
624 NhcAlarm alarm = alarmDevices.get(device.uuid);
626 if (action != null) {
627 updateActionState((NhcAction2) action, deviceProperties);
628 } else if (thermostat != null) {
629 updateThermostatState((NhcThermostat2) thermostat, deviceProperties);
630 } else if (meter != null) {
631 updateMeterState((NhcMeter2) meter, deviceProperties);
632 } else if (accessDevice != null) {
633 updateAccessState((NhcAccess2) accessDevice, deviceProperties);
634 } else if (videoDevice != null) {
635 updateVideoState((NhcVideo2) videoDevice, deviceProperties);
636 } else if (alarm != null) {
637 updateAlarmState((NhcAlarm2) alarm, deviceProperties);
639 logger.trace("No known device for {}", device.uuid);
643 private void updateActionState(NhcAction2 action, List<NhcProperty> deviceProperties) {
644 if (action.getType() == ActionType.ROLLERSHUTTER) {
645 updateRollershutterState(action, deviceProperties);
647 updateLightState(action, deviceProperties);
651 private void updateLightState(NhcAction2 action, List<NhcProperty> deviceProperties) {
652 Optional<NhcProperty> statusProperty = deviceProperties.stream().filter(p -> (p.status != null)).findFirst();
653 Optional<NhcProperty> dimmerProperty = deviceProperties.stream().filter(p -> (p.brightness != null))
655 Optional<NhcProperty> basicStateProperty = deviceProperties.stream().filter(p -> (p.basicState != null))
658 String booleanState = null;
659 if (statusProperty.isPresent()) {
660 booleanState = statusProperty.get().status;
661 } else if (basicStateProperty.isPresent()) {
662 booleanState = basicStateProperty.get().basicState;
665 if (NHCOFF.equals(booleanState) || NHCFALSE.equals(booleanState)) {
666 action.setBooleanState(false);
667 logger.debug("setting action {} internally to OFF", action.getId());
670 if (dimmerProperty.isPresent()) {
671 String brightness = dimmerProperty.get().brightness;
672 if (brightness != null) {
674 logger.debug("setting action {} internally to {}", action.getId(), dimmerProperty.get().brightness);
675 action.setState(Integer.parseInt(brightness));
676 } catch (NumberFormatException e) {
677 logger.debug("received invalid brightness value {} for dimmer {}", brightness, action.getId());
682 if (NHCON.equals(booleanState) || NHCTRUE.equals(booleanState)) {
683 logger.debug("setting action {} internally to ON", action.getId());
684 action.setBooleanState(true);
688 private void updateRollershutterState(NhcAction2 action, List<NhcProperty> deviceProperties) {
689 deviceProperties.stream().map(p -> p.position).filter(Objects::nonNull).findFirst().ifPresent(position -> {
691 logger.debug("setting action {} internally to {}", action.getId(), position);
692 action.setState(Integer.parseInt(position));
693 } catch (NumberFormatException e) {
694 logger.trace("received empty or invalid rollershutter {} position info {}", action.getId(), position);
699 private void updateThermostatState(NhcThermostat2 thermostat, List<NhcProperty> deviceProperties) {
700 Optional<Boolean> overruleActiveProperty = deviceProperties.stream().map(p -> p.overruleActive)
701 .filter(Objects::nonNull).map(t -> Boolean.parseBoolean(t)).findFirst();
702 Optional<Integer> overruleSetpointProperty = deviceProperties.stream().map(p -> p.overruleSetpoint)
703 .map(s -> (!((s == null) || s.isEmpty())) ? Math.round(Float.parseFloat(s) * 10) : null)
704 .filter(Objects::nonNull).findFirst();
705 Optional<Integer> overruleTimeProperty = deviceProperties.stream().map(p -> p.overruleTime)
706 .map(s -> (!((s == null) || s.isEmpty())) ? Math.round(Float.parseFloat(s)) : null)
707 .filter(Objects::nonNull).findFirst();
708 Optional<Integer> setpointTemperatureProperty = deviceProperties.stream().map(p -> p.setpointTemperature)
709 .map(s -> (!((s == null) || s.isEmpty())) ? Math.round(Float.parseFloat(s) * 10) : null)
710 .filter(Objects::nonNull).findFirst();
711 Optional<Boolean> ecoSaveProperty = deviceProperties.stream().map(p -> p.ecoSave)
712 .map(s -> s != null ? Boolean.parseBoolean(s) : null).filter(Objects::nonNull).findFirst();
713 Optional<Integer> ambientTemperatureProperty = deviceProperties.stream().map(p -> p.ambientTemperature)
714 .map(s -> (!((s == null) || s.isEmpty())) ? Math.round(Float.parseFloat(s) * 10) : null)
715 .filter(Objects::nonNull).findFirst();
716 Optional<@Nullable String> demandProperty = deviceProperties.stream().map(p -> p.demand)
717 .filter(Objects::nonNull).findFirst();
718 Optional<@Nullable String> operationModeProperty = deviceProperties.stream().map(p -> p.operationMode)
719 .filter(Objects::nonNull).findFirst();
721 String modeString = deviceProperties.stream().map(p -> p.program).filter(Objects::nonNull).findFirst()
723 int mode = IntStream.range(0, THERMOSTATMODES.length).filter(i -> THERMOSTATMODES[i].equals(modeString))
724 .findFirst().orElse(thermostat.getMode());
726 int measured = ambientTemperatureProperty.orElse(thermostat.getMeasured());
727 int setpoint = setpointTemperatureProperty.orElse(thermostat.getSetpoint());
730 int overruletime = 0;
731 if (overruleActiveProperty.orElse(true)) {
732 overrule = overruleSetpointProperty.orElse(thermostat.getOverrule());
733 overruletime = overruleTimeProperty.orElse(thermostat.getRemainingOverruletime());
736 int ecosave = thermostat.getEcosave();
737 if (ecoSaveProperty.orElse(false)) {
741 int demand = thermostat.getDemand();
742 String demandString = demandProperty.orElse(operationModeProperty.orElse(""));
743 demandString = demandString == null ? "" : demandString;
744 switch (demandString) {
757 "setting thermostat {} with measured {}, setpoint {}, mode {}, overrule {}, overruletime {}, ecosave {}, demand {}",
758 thermostat.getId(), measured, setpoint, mode, overrule, overruletime, ecosave, demand);
759 thermostat.setState(measured, setpoint, mode, overrule, overruletime, ecosave, demand);
762 private void updateMeterState(NhcMeter2 meter, List<NhcProperty> deviceProperties) {
764 Optional<Integer> electricalPower = deviceProperties.stream().map(p -> p.electricalPower)
765 .map(s -> (!((s == null) || s.isEmpty())) ? Math.round(Float.parseFloat(s)) : null)
766 .filter(Objects::nonNull).findFirst();
767 Optional<Integer> powerFromGrid = deviceProperties.stream().map(p -> p.electricalPowerFromGrid)
768 .map(s -> (!((s == null) || s.isEmpty())) ? Math.round(Float.parseFloat(s)) : null)
769 .filter(Objects::nonNull).findFirst();
770 Optional<Integer> powerToGrid = deviceProperties.stream().map(p -> p.electricalPowerToGrid)
771 .map(s -> (!((s == null) || s.isEmpty())) ? Math.round(Float.parseFloat(s)) : null)
772 .filter(Objects::nonNull).findFirst();
773 int power = electricalPower.orElse(powerFromGrid.orElse(0) - powerToGrid.orElse(0));
774 logger.trace("setting energy meter {} power to {}", meter.getId(), power);
775 meter.setPower(power);
776 } catch (NumberFormatException e) {
777 logger.trace("wrong format in energy meter {} power reading", meter.getId());
778 meter.setPower(null);
782 private void updateAccessState(NhcAccess2 accessDevice, List<NhcProperty> deviceProperties) {
783 Optional<NhcProperty> basicStateProperty = deviceProperties.stream().filter(p -> (p.basicState != null))
785 Optional<NhcProperty> doorLockProperty = deviceProperties.stream().filter(p -> (p.doorlock != null))
788 if (basicStateProperty.isPresent()) {
789 String basicState = basicStateProperty.get().basicState;
790 boolean state = false;
791 if (NHCON.equals(basicState) || NHCTRUE.equals(basicState)) {
794 switch (accessDevice.getType()) {
796 accessDevice.updateRingAndComeInState(state);
797 logger.debug("setting access device {} ring and come in to {}", accessDevice.getId(), state);
800 accessDevice.updateBellState(state);
801 logger.debug("setting access device {} bell to {}", accessDevice.getId(), state);
808 if (doorLockProperty.isPresent()) {
809 String doorLockState = doorLockProperty.get().doorlock;
810 boolean state = false;
811 if (NHCCLOSED.equals(doorLockState)) {
814 logger.debug("setting access device {} doorlock to {}", accessDevice.getId(), state);
815 accessDevice.updateDoorLockState(state);
819 private void updateVideoState(NhcVideo2 videoDevice, List<NhcProperty> deviceProperties) {
820 String callStatus01 = deviceProperties.stream().map(p -> p.callStatus01).filter(Objects::nonNull).findFirst()
822 String callStatus02 = deviceProperties.stream().map(p -> p.callStatus02).filter(Objects::nonNull).findFirst()
824 String callStatus03 = deviceProperties.stream().map(p -> p.callStatus03).filter(Objects::nonNull).findFirst()
826 String callStatus04 = deviceProperties.stream().map(p -> p.callStatus04).filter(Objects::nonNull).findFirst()
829 logger.debug("setting video device {} call status to {}, {}, {}, {}", videoDevice.getId(), callStatus01,
830 callStatus02, callStatus03, callStatus04);
831 videoDevice.updateState(callStatus01, callStatus02, callStatus03, callStatus04);
834 private void updateAlarmState(NhcAlarm2 alarmDevice, List<NhcProperty> deviceProperties) {
835 String state = deviceProperties.stream().map(p -> p.internalState).filter(Objects::nonNull).findFirst()
838 logger.debug("setting alarm device {} state to {}", alarmDevice.getId(), state);
839 alarmDevice.setState(state);
841 String triggered = deviceProperties.stream().map(p -> p.alarmTriggered).filter(Objects::nonNull).findFirst()
843 if (Boolean.valueOf(triggered)) {
844 logger.debug("triggering alarm device {}", alarmDevice.getId());
845 alarmDevice.triggerAlarm();
850 public void executeAction(String actionId, String value) {
851 NhcMessage2 message = new NhcMessage2();
853 message.method = "devices.control";
854 List<NhcMessageParam> params = new ArrayList<>();
855 NhcMessageParam param = new NhcMessageParam();
857 message.params = params;
858 List<NhcDevice2> devices = new ArrayList<>();
859 NhcDevice2 device = new NhcDevice2();
861 param.devices = devices;
862 device.uuid = actionId;
863 List<NhcProperty> deviceProperties = new ArrayList<>();
864 NhcProperty property = new NhcProperty();
865 deviceProperties.add(property);
866 device.properties = deviceProperties;
868 NhcAction2 action = (NhcAction2) actions.get(actionId);
869 if (action == null) {
873 switch (action.getType()) {
876 property.basicState = NHCTRIGGERED;
879 property.status = value;
882 if (NHCON.equals(value)) {
883 action.setBooleanState(true); // this will trigger sending the stored brightness value event out
884 property.status = value;
885 } else if (NHCOFF.equals(value)) {
886 property.status = value;
889 action.setState(Integer.parseInt(value)); // set cached state to new brightness value to avoid
890 // switching on with old brightness value before
893 } catch (NumberFormatException e) {
894 logger.debug("internal error, trying to set invalid brightness value {} for dimmer {}", value,
899 // If the light is off, turn the light on before sending the brightness value, needs to happen
900 // in 2 separate messages.
901 if (!action.booleanState()) {
902 executeAction(actionId, NHCON);
904 property.brightness = value;
908 if (NHCSTOP.equals(value)) {
909 property.action = value;
910 } else if (NHCUP.equals(value)) {
911 property.position = "100";
912 } else if (NHCDOWN.equals(value)) {
913 property.position = "0";
915 property.position = value;
920 String topic = profile + "/control/devices/cmd";
921 String gsonMessage = gson.toJson(message);
922 sendDeviceMessage(topic, gsonMessage);
926 public void executeThermostat(String thermostatId, String mode) {
927 NhcMessage2 message = new NhcMessage2();
929 message.method = "devices.control";
930 List<NhcMessageParam> params = new ArrayList<>();
931 NhcMessageParam param = new NhcMessageParam();
933 message.params = params;
934 List<NhcDevice2> devices = new ArrayList<>();
935 NhcDevice2 device = new NhcDevice2();
937 param.devices = devices;
938 device.uuid = thermostatId;
939 List<NhcProperty> deviceProperties = new ArrayList<>();
941 NhcProperty overruleActiveProp = new NhcProperty();
942 deviceProperties.add(overruleActiveProp);
943 overruleActiveProp.overruleActive = "False";
945 NhcProperty program = new NhcProperty();
946 deviceProperties.add(program);
947 program.program = mode;
949 device.properties = deviceProperties;
951 String topic = profile + "/control/devices/cmd";
952 String gsonMessage = gson.toJson(message);
953 sendDeviceMessage(topic, gsonMessage);
957 public void executeThermostat(String thermostatId, int overruleTemp, int overruleTime) {
958 NhcMessage2 message = new NhcMessage2();
960 message.method = "devices.control";
961 List<NhcMessageParam> params = new ArrayList<>();
962 NhcMessageParam param = new NhcMessageParam();
964 message.params = params;
965 List<NhcDevice2> devices = new ArrayList<>();
966 NhcDevice2 device = new NhcDevice2();
968 param.devices = devices;
969 device.uuid = thermostatId;
970 List<NhcProperty> deviceProperties = new ArrayList<>();
972 if (overruleTime > 0) {
973 NhcProperty overruleActiveProp = new NhcProperty();
974 overruleActiveProp.overruleActive = "True";
975 deviceProperties.add(overruleActiveProp);
977 NhcProperty overruleSetpointProp = new NhcProperty();
978 overruleSetpointProp.overruleSetpoint = String.valueOf(overruleTemp / 10.0);
979 deviceProperties.add(overruleSetpointProp);
981 NhcProperty overruleTimeProp = new NhcProperty();
982 overruleTimeProp.overruleTime = String.valueOf(overruleTime);
983 deviceProperties.add(overruleTimeProp);
985 NhcProperty overruleActiveProp = new NhcProperty();
986 overruleActiveProp.overruleActive = "False";
987 deviceProperties.add(overruleActiveProp);
989 device.properties = deviceProperties;
991 String topic = profile + "/control/devices/cmd";
992 String gsonMessage = gson.toJson(message);
993 sendDeviceMessage(topic, gsonMessage);
997 public void executeMeter(String meterId) {
998 // Nothing to do, individual meter readings not supported in NHC II at this point in time
1002 public void retriggerMeterLive(String meterId) {
1003 NhcMessage2 message = new NhcMessage2();
1005 message.method = "devices.control";
1006 List<NhcMessageParam> params = new ArrayList<>();
1007 NhcMessageParam param = new NhcMessageParam();
1009 message.params = params;
1010 List<NhcDevice2> devices = new ArrayList<>();
1011 NhcDevice2 device = new NhcDevice2();
1012 devices.add(device);
1013 param.devices = devices;
1014 device.uuid = meterId;
1015 List<NhcProperty> deviceProperties = new ArrayList<>();
1017 NhcProperty reportInstantUsageProp = new NhcProperty();
1018 deviceProperties.add(reportInstantUsageProp);
1019 reportInstantUsageProp.reportInstantUsage = "True";
1020 device.properties = deviceProperties;
1022 String topic = profile + "/control/devices/cmd";
1023 String gsonMessage = gson.toJson(message);
1025 sendDeviceMessage(topic, gsonMessage);
1029 public void executeAccessBell(String accessId) {
1030 executeAccess(accessId);
1034 public void executeAccessRingAndComeIn(String accessId, boolean ringAndComeIn) {
1035 NhcAccess2 accessDevice = (NhcAccess2) accessDevices.get(accessId);
1036 if (accessDevice == null) {
1040 boolean current = accessDevice.getRingAndComeInState();
1041 if ((ringAndComeIn && !current) || (!ringAndComeIn && current)) {
1042 executeAccess(accessId);
1044 logger.trace("Not updating ring and come in as state did not change");
1048 private void executeAccess(String accessId) {
1049 NhcMessage2 message = new NhcMessage2();
1051 message.method = "devices.control";
1052 List<NhcMessageParam> params = new ArrayList<>();
1053 NhcMessageParam param = new NhcMessageParam();
1055 message.params = params;
1056 List<NhcDevice2> devices = new ArrayList<>();
1057 NhcDevice2 device = new NhcDevice2();
1058 devices.add(device);
1059 param.devices = devices;
1060 device.uuid = accessId;
1061 List<NhcProperty> deviceProperties = new ArrayList<>();
1062 NhcProperty property = new NhcProperty();
1063 deviceProperties.add(property);
1064 device.properties = deviceProperties;
1066 NhcAccess2 accessDevice = (NhcAccess2) accessDevices.get(accessId);
1067 if (accessDevice == null) {
1071 property.basicState = NHCTRIGGERED;
1073 String topic = profile + "/control/devices/cmd";
1074 String gsonMessage = gson.toJson(message);
1075 sendDeviceMessage(topic, gsonMessage);
1079 public void executeVideoBell(String accessId, int buttonIndex) {
1080 NhcMessage2 message = new NhcMessage2();
1082 message.method = "devices.control";
1083 List<NhcMessageParam> params = new ArrayList<>();
1084 NhcMessageParam param = new NhcMessageParam();
1086 message.params = params;
1087 List<NhcDevice2> devices = new ArrayList<>();
1088 NhcDevice2 device = new NhcDevice2();
1089 devices.add(device);
1090 param.devices = devices;
1091 device.uuid = accessId;
1092 List<NhcProperty> deviceProperties = new ArrayList<>();
1093 NhcProperty property = new NhcProperty();
1094 deviceProperties.add(property);
1095 device.properties = deviceProperties;
1097 NhcVideo videoDevice = videoDevices.get(accessId);
1098 if (videoDevice == null) {
1102 switch (buttonIndex) {
1104 property.callStatus01 = NHCRINGING;
1107 property.callStatus02 = NHCRINGING;
1110 property.callStatus03 = NHCRINGING;
1113 property.callStatus04 = NHCRINGING;
1119 String topic = profile + "/control/devices/cmd";
1120 String gsonMessage = gson.toJson(message);
1121 sendDeviceMessage(topic, gsonMessage);
1125 public void executeAccessUnlock(String accessId) {
1126 NhcMessage2 message = new NhcMessage2();
1128 message.method = "devices.control";
1129 List<NhcMessageParam> params = new ArrayList<>();
1130 NhcMessageParam param = new NhcMessageParam();
1132 message.params = params;
1133 List<NhcDevice2> devices = new ArrayList<>();
1134 NhcDevice2 device = new NhcDevice2();
1135 devices.add(device);
1136 param.devices = devices;
1137 device.uuid = accessId;
1138 List<NhcProperty> deviceProperties = new ArrayList<>();
1139 NhcProperty property = new NhcProperty();
1140 deviceProperties.add(property);
1141 device.properties = deviceProperties;
1143 NhcAccess2 accessDevice = (NhcAccess2) accessDevices.get(accessId);
1144 if (accessDevice == null) {
1148 property.doorlock = NHCOPEN;
1150 String topic = profile + "/control/devices/cmd";
1151 String gsonMessage = gson.toJson(message);
1152 sendDeviceMessage(topic, gsonMessage);
1156 public void executeArm(String alarmId) {
1157 executeAlarm(alarmId, NHCARM);
1161 public void executeDisarm(String alarmId) {
1162 executeAlarm(alarmId, NHCDISARM);
1165 private void executeAlarm(String alarmId, String state) {
1166 NhcMessage2 message = new NhcMessage2();
1168 message.method = "devices.control";
1169 List<NhcMessageParam> params = new ArrayList<>();
1170 NhcMessageParam param = new NhcMessageParam();
1172 message.params = params;
1173 List<NhcDevice2> devices = new ArrayList<>();
1174 NhcDevice2 device = new NhcDevice2();
1175 devices.add(device);
1176 param.devices = devices;
1177 device.uuid = alarmId;
1178 List<NhcProperty> deviceProperties = new ArrayList<>();
1179 NhcProperty property = new NhcProperty();
1180 deviceProperties.add(property);
1181 device.properties = deviceProperties;
1183 NhcAlarm2 alarmDevice = (NhcAlarm2) alarmDevices.get(alarmId);
1184 if (alarmDevice == null) {
1188 property.control = state;
1190 String topic = profile + "/control/devices/cmd";
1191 String gsonMessage = gson.toJson(message);
1192 sendDeviceMessage(topic, gsonMessage);
1195 private void sendDeviceMessage(String topic, String gsonMessage) {
1197 mqttConnection.connectionPublish(topic, gsonMessage);
1199 } catch (MqttException e) {
1200 String message = e.getLocalizedMessage();
1202 logger.debug("sending command failed, trying to restart communication");
1203 restartCommunication();
1204 // retry sending after restart
1206 if (communicationActive()) {
1207 mqttConnection.connectionPublish(topic, gsonMessage);
1209 logger.debug("failed to restart communication");
1211 } catch (MqttException e1) {
1212 message = e1.getLocalizedMessage();
1214 logger.debug("error resending device command");
1216 if (!communicationActive()) {
1217 message = (message != null) ? message : "@text/offline.communication-error";
1218 connectionLost(message);
1219 // Keep on trying to restart, but don't send message anymore
1220 scheduleRestartCommunication();
1226 public void processMessage(String topic, byte[] payload) {
1227 String message = new String(payload);
1228 if ((profile + "/system/evt").equals(topic)) {
1230 } else if ((profile + "/system/rsp").equals(topic)) {
1231 logger.debug("received topic {}, payload {}", topic, message);
1232 systeminfoPublishRsp(message);
1233 } else if ((profile + "/notification/evt").equals(topic)) {
1234 logger.debug("received topic {}, payload {}", topic, message);
1235 notificationEvt(message);
1236 } else if ((profile + "/control/devices/evt").equals(topic)) {
1237 logger.trace("received topic {}, payload {}", topic, message);
1238 devicesEvt(message);
1239 } else if ((profile + "/control/devices/rsp").equals(topic)) {
1240 logger.debug("received topic {}, payload {}", topic, message);
1241 devicesListRsp(message);
1242 } else if ((profile + "/authentication/rsp").equals(topic)) {
1243 logger.debug("received topic {}, payload {}", topic, message);
1244 servicesListRsp(message);
1245 } else if ((profile + "/control/devices.error").equals(topic)) {
1246 logger.warn("received error {}", message);
1248 logger.trace("not acted on received message topic {}, payload {}", topic, message);
1253 * @return system info retrieved from Connected Controller
1255 public NhcSystemInfo2 getSystemInfo() {
1256 NhcSystemInfo2 systemInfo = nhcSystemInfo;
1257 if (systemInfo == null) {
1258 systemInfo = new NhcSystemInfo2();
1264 * @return time info retrieved from Connected Controller
1266 public NhcTimeInfo2 getTimeInfo() {
1267 NhcTimeInfo2 timeInfo = nhcTimeInfo;
1268 if (timeInfo == null) {
1269 timeInfo = new NhcTimeInfo2();
1275 * @return comma separated list of services retrieved from Connected Controller
1277 public String getServices() {
1278 return services.stream().map(NhcService2::name).collect(Collectors.joining(", "));
1282 public void connectionStateChanged(MqttConnectionState state, @Nullable Throwable error) {
1283 // do in separate thread as this method needs to return early
1284 scheduler.submit(() -> {
1285 if (error != null) {
1286 logger.debug("Connection state: {}, error", state, error);
1287 String localizedMessage = error.getLocalizedMessage();
1288 String message = (localizedMessage != null) ? localizedMessage : "@text/offline.communication-error";
1289 connectionLost(message);
1290 scheduleRestartCommunication();
1291 } else if ((state == MqttConnectionState.CONNECTED) && !initStarted) {
1294 logger.trace("Connection state: {}", state);