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.NhcControllerEvent;
39 import org.openhab.binding.nikohomecontrol.internal.protocol.NhcMeter;
40 import org.openhab.binding.nikohomecontrol.internal.protocol.NhcThermostat;
41 import org.openhab.binding.nikohomecontrol.internal.protocol.NhcVideo;
42 import org.openhab.binding.nikohomecontrol.internal.protocol.NikoHomeControlCommunication;
43 import org.openhab.binding.nikohomecontrol.internal.protocol.NikoHomeControlConstants.AccessType;
44 import org.openhab.binding.nikohomecontrol.internal.protocol.NikoHomeControlConstants.ActionType;
45 import org.openhab.binding.nikohomecontrol.internal.protocol.NikoHomeControlConstants.MeterType;
46 import org.openhab.binding.nikohomecontrol.internal.protocol.nhc2.NhcDevice2.NhcParameter;
47 import org.openhab.binding.nikohomecontrol.internal.protocol.nhc2.NhcDevice2.NhcProperty;
48 import org.openhab.binding.nikohomecontrol.internal.protocol.nhc2.NhcDevice2.NhcTrait;
49 import org.openhab.binding.nikohomecontrol.internal.protocol.nhc2.NhcMessage2.NhcMessageParam;
50 import org.openhab.core.io.transport.mqtt.MqttConnectionObserver;
51 import org.openhab.core.io.transport.mqtt.MqttConnectionState;
52 import org.openhab.core.io.transport.mqtt.MqttException;
53 import org.openhab.core.io.transport.mqtt.MqttMessageSubscriber;
54 import org.slf4j.Logger;
55 import org.slf4j.LoggerFactory;
57 import com.google.gson.FieldNamingPolicy;
58 import com.google.gson.Gson;
59 import com.google.gson.GsonBuilder;
60 import com.google.gson.JsonSyntaxException;
61 import com.google.gson.reflect.TypeToken;
64 * The {@link NikoHomeControlCommunication2} class is able to do the following tasks with Niko Home Control II
67 * <li>Start and stop MQTT connection with Niko Home Control II Connected Controller.
68 * <li>Read all setup and status information from the Niko Home Control Controller.
69 * <li>Execute Niko Home Control commands.
70 * <li>Listen for events from Niko Home Control.
73 * @author Mark Herwege - Initial Contribution
76 public class NikoHomeControlCommunication2 extends NikoHomeControlCommunication
77 implements MqttMessageSubscriber, MqttConnectionObserver {
79 private final Logger logger = LoggerFactory.getLogger(NikoHomeControlCommunication2.class);
81 private final NhcMqttConnection2 mqttConnection;
83 private final List<NhcService2> services = new CopyOnWriteArrayList<>();
85 private volatile String profile = "";
87 private volatile @Nullable NhcSystemInfo2 nhcSystemInfo;
88 private volatile @Nullable NhcTimeInfo2 nhcTimeInfo;
90 private volatile boolean initStarted = false;
91 private volatile @Nullable CompletableFuture<Boolean> communicationStarted;
93 private final Gson gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.UPPER_CAMEL_CASE).create();
96 * Constructor for Niko Home Control communication object, manages communication with
97 * Niko Home Control II Connected Controller.
99 * @throws CertificateException when the SSL context for MQTT communication cannot be created
100 * @throws java.net.UnknownHostException when the IP address is not provided
103 public NikoHomeControlCommunication2(NhcControllerEvent handler, String clientId,
104 ScheduledExecutorService scheduler) throws CertificateException {
105 super(handler, scheduler);
106 mqttConnection = new NhcMqttConnection2(clientId, this, this);
110 public synchronized void startCommunication() {
112 communicationStarted = new CompletableFuture<>();
114 InetAddress addr = handler.getAddr();
116 logger.warn("IP address cannot be empty");
120 String addrString = addr.getHostAddress();
121 int port = handler.getPort();
122 logger.debug("initializing for mqtt connection to CoCo on {}:{}", addrString, port);
124 profile = handler.getProfile();
126 String token = handler.getToken();
127 if (token.isEmpty()) {
128 logger.warn("JWT token cannot be empty");
134 mqttConnection.startConnection(addrString, port, profile, token);
135 } catch (MqttException e) {
136 logger.debug("error in mqtt communication");
137 handler.controllerOffline("@text/offline.communication-error");
138 scheduleRestartCommunication();
143 public synchronized void resetCommunication() {
144 CompletableFuture<Boolean> started = communicationStarted;
145 if (started != null) {
146 started.complete(false);
148 communicationStarted = null;
151 mqttConnection.stopConnection();
155 public boolean communicationActive() {
156 CompletableFuture<Boolean> started = communicationStarted;
157 if (started == null) {
161 // Wait until we received all devices info to confirm we are active.
162 return started.get(5000, TimeUnit.MILLISECONDS);
163 } catch (InterruptedException | ExecutionException | TimeoutException e) {
164 logger.debug("exception waiting for connection start: {}", e.toString());
170 * After setting up the communication with the Niko Home Control Connected Controller, send all initialization
174 private synchronized void initialize() {
177 NhcMessage2 message = new NhcMessage2();
180 message.method = "systeminfo.publish";
181 mqttConnection.connectionPublish(profile + "/system/cmd", gson.toJson(message));
183 message.method = "services.list";
184 mqttConnection.connectionPublish(profile + "/authentication/cmd", gson.toJson(message));
186 message.method = "devices.list";
187 mqttConnection.connectionPublish(profile + "/control/devices/cmd", gson.toJson(message));
189 message.method = "notifications.list";
190 mqttConnection.connectionPublish(profile + "/notification/cmd", gson.toJson(message));
191 } catch (MqttException e) {
193 logger.debug("error in mqtt communication during initialization");
194 resetCommunication();
198 private void connectionLost(String message) {
199 logger.debug("connection lost");
200 resetCommunication();
201 handler.controllerOffline(message);
204 private void systemEvt(String response) {
205 Type messageType = new TypeToken<NhcMessage2>() {
207 List<NhcTimeInfo2> timeInfo = null;
208 List<NhcSystemInfo2> systemInfo = null;
210 NhcMessage2 message = gson.fromJson(response, messageType);
211 List<NhcMessageParam> messageParams = (message != null) ? message.params : null;
212 if (messageParams != null) {
213 timeInfo = messageParams.stream().filter(p -> (p.timeInfo != null)).findFirst().get().timeInfo;
214 systemInfo = messageParams.stream().filter(p -> (p.systemInfo != null)).findFirst().get().systemInfo;
216 } catch (JsonSyntaxException e) {
217 logger.debug("unexpected json {}", response);
218 } catch (NoSuchElementException ignore) {
219 // Ignore if timeInfo not present in response, this should not happen in a timeInfo response
221 if (timeInfo != null) {
222 nhcTimeInfo = timeInfo.get(0);
224 if (systemInfo != null) {
225 nhcSystemInfo = systemInfo.get(0);
226 handler.updatePropertiesEvent();
230 private void systeminfoPublishRsp(String response) {
231 Type messageType = new TypeToken<NhcMessage2>() {
233 List<NhcSystemInfo2> systemInfo = null;
235 NhcMessage2 message = gson.fromJson(response, messageType);
236 List<NhcMessageParam> messageParams = (message != null) ? message.params : null;
237 if (messageParams != null) {
238 systemInfo = messageParams.stream().filter(p -> (p.systemInfo != null)).findFirst().get().systemInfo;
240 } catch (JsonSyntaxException e) {
241 logger.debug("unexpected json {}", response);
242 } catch (NoSuchElementException ignore) {
243 // Ignore if systemInfo not present in response, this should not happen in a systemInfo response
245 if (systemInfo != null) {
246 nhcSystemInfo = systemInfo.get(0);
250 private void servicesListRsp(String response) {
251 Type messageType = new TypeToken<NhcMessage2>() {
253 List<NhcService2> serviceList = null;
255 NhcMessage2 message = gson.fromJson(response, messageType);
256 List<NhcMessageParam> messageParams = (message != null) ? message.params : null;
257 if (messageParams != null) {
258 serviceList = messageParams.stream().filter(p -> (p.services != null)).findFirst().get().services;
260 } catch (JsonSyntaxException e) {
261 logger.debug("unexpected json {}", response);
262 } catch (NoSuchElementException ignore) {
263 // Ignore if services not present in response, this should not happen in a services response
266 if (serviceList != null) {
267 services.addAll(serviceList);
271 private void devicesListRsp(String response) {
272 Type messageType = new TypeToken<NhcMessage2>() {
274 List<NhcDevice2> deviceList = null;
276 NhcMessage2 message = gson.fromJson(response, messageType);
277 List<NhcMessageParam> messageParams = (message != null) ? message.params : null;
278 if (messageParams != null) {
279 deviceList = messageParams.stream().filter(p -> (p.devices != null)).findFirst().get().devices;
281 } catch (JsonSyntaxException e) {
282 logger.debug("unexpected json {}", response);
283 } catch (NoSuchElementException ignore) {
284 // Ignore if devices not present in response, this should not happen in a devices response
286 if (deviceList == null) {
290 for (NhcDevice2 device : deviceList) {
295 // Once a devices list response is received, we know the communication is fully started.
296 logger.debug("Communication start complete.");
297 handler.controllerOnline();
298 CompletableFuture<Boolean> future = communicationStarted;
299 if (future != null) {
300 future.complete(true);
304 private void devicesEvt(String response) {
305 Type messageType = new TypeToken<NhcMessage2>() {
307 List<NhcDevice2> deviceList = null;
308 String method = null;
310 NhcMessage2 message = gson.fromJson(response, messageType);
311 method = (message != null) ? message.method : null;
312 List<NhcMessageParam> messageParams = (message != null) ? message.params : null;
313 if (messageParams != null) {
314 deviceList = messageParams.stream().filter(p -> (p.devices != null)).findFirst().get().devices;
316 } catch (JsonSyntaxException e) {
317 logger.debug("unexpected json {}", response);
318 } catch (NoSuchElementException ignore) {
319 // Ignore if devices not present in response, this should not happen in a devices event
321 if (deviceList == null) {
325 if ("devices.removed".equals(method)) {
326 deviceList.forEach(this::removeDevice);
328 } else if ("devices.added".equals(method)) {
329 deviceList.forEach(this::addDevice);
332 deviceList.forEach(this::updateState);
335 private void notificationEvt(String response) {
336 Type messageType = new TypeToken<NhcMessage2>() {
338 List<NhcNotification2> notificationList = null;
340 NhcMessage2 message = gson.fromJson(response, messageType);
341 List<NhcMessageParam> messageParams = (message != null) ? message.params : null;
342 if (messageParams != null) {
343 notificationList = messageParams.stream().filter(p -> (p.notifications != null)).findFirst()
344 .get().notifications;
346 } catch (JsonSyntaxException e) {
347 logger.debug("unexpected json {}", response);
348 } catch (NoSuchElementException ignore) {
349 // Ignore if notifications not present in response, this should not happen in a notifications event
351 logger.debug("notifications {}", notificationList);
352 if (notificationList == null) {
356 for (NhcNotification2 notification : notificationList) {
357 if ("new".equals(notification.status)) {
358 String alarmText = notification.text;
359 switch (notification.type) {
361 handler.alarmEvent(alarmText);
364 handler.noticeEvent(alarmText);
367 logger.debug("unexpected message type {}", notification.type);
373 private void addDevice(NhcDevice2 device) {
374 String location = null;
375 List<NhcParameter> parameters = device.parameters;
376 if (parameters != null) {
377 location = parameters.stream().map(p -> p.locationName).filter(Objects::nonNull).findFirst().orElse(null);
380 if ("videodoorstation".equals(device.type) || "vds".equals(device.type)) {
381 addVideoDevice(device);
382 } else if ("accesscontrol".equals(device.model) || "bellbutton".equals(device.model)) {
383 addAccessDevice(device, location);
384 } else if ("action".equals(device.type) || "virtual".equals(device.type)) {
385 addActionDevice(device, location);
386 } else if ("thermostat".equals(device.type)) {
387 addThermostatDevice(device, location);
388 } else if ("centralmeter".equals(device.type) || "energyhome".equals(device.type)) {
389 addMeterDevice(device, location);
391 logger.debug("device type {} and model {} not supported for {}, {}", device.type, device.model, device.uuid,
396 private void addActionDevice(NhcDevice2 device, @Nullable String location) {
397 ActionType actionType;
398 switch (device.model) {
405 case "overallcomfort":
407 actionType = ActionType.TRIGGER;
411 case "switched-generic":
414 actionType = ActionType.RELAY;
417 actionType = ActionType.DIMMER;
419 case "rolldownshutter":
421 case "venetianblind":
423 actionType = ActionType.ROLLERSHUTTER;
426 actionType = ActionType.GENERIC;
427 logger.debug("device type {} and model {} not recognised for {}, {}, ignoring", device.type,
428 device.model, device.uuid, device.name);
432 NhcAction nhcAction = actions.get(device.uuid);
433 if (nhcAction != null) {
434 // update name and location so discovery will see updated name and location
435 nhcAction.setName(device.name);
436 nhcAction.setLocation(location);
438 logger.debug("adding action device {} model {}, {}", device.uuid, device.model, device.name);
439 nhcAction = new NhcAction2(device.uuid, device.name, device.type, device.technology, device.model, location,
442 actions.put(device.uuid, nhcAction);
445 private void addThermostatDevice(NhcDevice2 device, @Nullable String location) {
446 NhcThermostat nhcThermostat = thermostats.get(device.uuid);
447 if (nhcThermostat != null) {
448 nhcThermostat.setName(device.name);
449 nhcThermostat.setLocation(location);
451 logger.debug("adding thermostat device {} model {}, {}", device.uuid, device.model, device.name);
452 nhcThermostat = new NhcThermostat2(device.uuid, device.name, device.type, device.technology, device.model,
455 thermostats.put(device.uuid, nhcThermostat);
458 private void addMeterDevice(NhcDevice2 device, @Nullable String location) {
459 NhcMeter nhcMeter = meters.get(device.uuid);
460 if (nhcMeter != null) {
461 nhcMeter.setName(device.name);
462 nhcMeter.setLocation(location);
464 logger.debug("adding energy meter device {} model {}, {}", device.uuid, device.model, device.name);
465 nhcMeter = new NhcMeter2(device.uuid, device.name, MeterType.ENERGY_LIVE, device.type, device.technology,
466 device.model, null, location, this, scheduler);
468 meters.put(device.uuid, nhcMeter);
471 private void addAccessDevice(NhcDevice2 device, @Nullable String location) {
472 AccessType accessType = AccessType.BASE;
473 if ("bellbutton".equals(device.model)) {
474 accessType = AccessType.BELLBUTTON;
476 List<NhcProperty> properties = device.properties;
477 if (properties != null) {
478 boolean hasBasicState = properties.stream().anyMatch(p -> (p.basicState != null));
480 accessType = AccessType.RINGANDCOMEIN;
485 NhcAccess2 nhcAccess = (NhcAccess2) accessDevices.get(device.uuid);
486 if (nhcAccess != null) {
487 nhcAccess.setName(device.name);
488 nhcAccess.setLocation(location);
490 String buttonId = null;
491 List<NhcParameter> parameters = device.parameters;
492 if (parameters != null) {
493 buttonId = parameters.stream().map(p -> p.buttonId).filter(Objects::nonNull).findFirst().orElse(null);
496 logger.debug("adding access device {} model {} type {}, {}", device.uuid, device.model, accessType,
498 nhcAccess = new NhcAccess2(device.uuid, device.name, device.type, device.technology, device.model, location,
499 accessType, buttonId, this);
501 if (buttonId != null) {
502 NhcAccess2 access = nhcAccess;
503 String macAddress = buttonId.split("_")[0];
504 videoDevices.forEach((key, videoDevice) -> {
505 if (macAddress.equals(videoDevice.getMacAddress())) {
506 int buttonIndex = access.getButtonIndex();
507 logger.debug("link access device {} to video device {} button {}", device.uuid,
508 videoDevice.getId(), buttonIndex);
509 videoDevice.setNhcAccess(buttonIndex, access);
510 access.setNhcVideo(videoDevice);
515 accessDevices.put(device.uuid, nhcAccess);
518 private void addVideoDevice(NhcDevice2 device) {
519 NhcVideo2 nhcVideo = (NhcVideo2) videoDevices.get(device.uuid);
520 if (nhcVideo != null) {
521 nhcVideo.setName(device.name);
523 String macAddress = null;
524 String ipAddress = null;
525 String mjpegUri = null;
527 List<NhcTrait> traits = device.traits;
528 if (traits != null) {
529 macAddress = traits.stream().map(t -> t.macAddress).filter(Objects::nonNull).findFirst().orElse(null);
531 List<NhcParameter> parameters = device.parameters;
532 if (parameters != null) {
533 mjpegUri = parameters.stream().map(p -> p.mjpegUri).filter(Objects::nonNull).findFirst().orElse(null);
534 tnUri = parameters.stream().map(p -> p.tnUri).filter(Objects::nonNull).findFirst().orElse(null);
536 List<NhcProperty> properties = device.properties;
537 if (properties != null) {
538 ipAddress = properties.stream().map(p -> p.ipAddress).filter(Objects::nonNull).findFirst().orElse(null);
541 logger.debug("adding video device {} model {}, {}", device.uuid, device.model, device.name);
542 nhcVideo = new NhcVideo2(device.uuid, device.name, device.type, device.technology, device.model, macAddress,
543 ipAddress, mjpegUri, tnUri, this);
545 if (macAddress != null) {
546 NhcVideo2 video = nhcVideo;
547 String mac = macAddress;
548 accessDevices.forEach((key, accessDevice) -> {
549 NhcAccess2 access = (NhcAccess2) accessDevice;
550 String buttonMac = access.getButtonId();
551 if (buttonMac != null) {
552 buttonMac = buttonMac.split("_")[0];
553 if (mac.equals(buttonMac)) {
554 int buttonIndex = access.getButtonIndex();
555 logger.debug("link access device {} to video device {} button {}", accessDevice.getId(),
556 device.uuid, buttonIndex);
557 video.setNhcAccess(buttonIndex, access);
558 access.setNhcVideo(video);
564 videoDevices.put(device.uuid, nhcVideo);
567 private void removeDevice(NhcDevice2 device) {
568 NhcAction action = actions.get(device.uuid);
569 NhcThermostat thermostat = thermostats.get(device.uuid);
570 NhcMeter meter = meters.get(device.uuid);
571 NhcAccess access = accessDevices.get(device.uuid);
572 NhcVideo video = videoDevices.get(device.uuid);
573 if (action != null) {
574 action.actionRemoved();
575 actions.remove(device.uuid);
576 } else if (thermostat != null) {
577 thermostat.thermostatRemoved();
578 thermostats.remove(device.uuid);
579 } else if (meter != null) {
580 meter.meterRemoved();
581 meters.remove(device.uuid);
582 } else if (access != null) {
583 access.accessDeviceRemoved();
584 accessDevices.remove(device.uuid);
585 } else if (video != null) {
586 video.videoDeviceRemoved();
587 videoDevices.remove(device.uuid);
591 private void updateState(NhcDevice2 device) {
592 List<NhcProperty> deviceProperties = device.properties;
593 if (deviceProperties == null) {
594 logger.debug("Cannot Update state for {} as no properties defined in device message", device.uuid);
598 NhcAction action = actions.get(device.uuid);
599 NhcThermostat thermostat = thermostats.get(device.uuid);
600 NhcMeter meter = meters.get(device.uuid);
601 NhcAccess accessDevice = accessDevices.get(device.uuid);
602 NhcVideo videoDevice = videoDevices.get(device.uuid);
604 if (action != null) {
605 updateActionState((NhcAction2) action, deviceProperties);
606 } else if (thermostat != null) {
607 updateThermostatState((NhcThermostat2) thermostat, deviceProperties);
608 } else if (meter != null) {
609 updateMeterState((NhcMeter2) meter, deviceProperties);
610 } else if (accessDevice != null) {
611 updateAccessState((NhcAccess2) accessDevice, deviceProperties);
612 } else if (videoDevice != null) {
613 updateVideoState((NhcVideo2) videoDevice, deviceProperties);
615 logger.trace("No known device for {}", device.uuid);
619 private void updateActionState(NhcAction2 action, List<NhcProperty> deviceProperties) {
620 if (action.getType() == ActionType.ROLLERSHUTTER) {
621 updateRollershutterState(action, deviceProperties);
623 updateLightState(action, deviceProperties);
627 private void updateLightState(NhcAction2 action, List<NhcProperty> deviceProperties) {
628 Optional<NhcProperty> statusProperty = deviceProperties.stream().filter(p -> (p.status != null)).findFirst();
629 Optional<NhcProperty> dimmerProperty = deviceProperties.stream().filter(p -> (p.brightness != null))
631 Optional<NhcProperty> basicStateProperty = deviceProperties.stream().filter(p -> (p.basicState != null))
634 String booleanState = null;
635 if (statusProperty.isPresent()) {
636 booleanState = statusProperty.get().status;
637 } else if (basicStateProperty.isPresent()) {
638 booleanState = basicStateProperty.get().basicState;
641 if (NHCOFF.equals(booleanState) || NHCFALSE.equals(booleanState)) {
642 action.setBooleanState(false);
643 logger.debug("setting action {} internally to OFF", action.getId());
646 if (dimmerProperty.isPresent()) {
647 String brightness = dimmerProperty.get().brightness;
648 if (brightness != null) {
650 logger.debug("setting action {} internally to {}", action.getId(), dimmerProperty.get().brightness);
651 action.setState(Integer.parseInt(brightness));
652 } catch (NumberFormatException e) {
653 logger.debug("received invalid brightness value {} for dimmer {}", brightness, action.getId());
658 if (NHCON.equals(booleanState) || NHCTRUE.equals(booleanState)) {
659 logger.debug("setting action {} internally to ON", action.getId());
660 action.setBooleanState(true);
664 private void updateRollershutterState(NhcAction2 action, List<NhcProperty> deviceProperties) {
665 deviceProperties.stream().map(p -> p.position).filter(Objects::nonNull).findFirst().ifPresent(position -> {
667 logger.debug("setting action {} internally to {}", action.getId(), position);
668 action.setState(Integer.parseInt(position));
669 } catch (NumberFormatException e) {
670 logger.trace("received empty or invalid rollershutter {} position info {}", action.getId(), position);
675 private void updateThermostatState(NhcThermostat2 thermostat, List<NhcProperty> deviceProperties) {
676 Optional<Boolean> overruleActiveProperty = deviceProperties.stream().map(p -> p.overruleActive)
677 .filter(Objects::nonNull).map(t -> Boolean.parseBoolean(t)).findFirst();
678 Optional<Integer> overruleSetpointProperty = deviceProperties.stream().map(p -> p.overruleSetpoint)
679 .map(s -> (!((s == null) || s.isEmpty())) ? Math.round(Float.parseFloat(s) * 10) : null)
680 .filter(Objects::nonNull).findFirst();
681 Optional<Integer> overruleTimeProperty = deviceProperties.stream().map(p -> p.overruleTime)
682 .map(s -> (!((s == null) || s.isEmpty())) ? Math.round(Float.parseFloat(s)) : null)
683 .filter(Objects::nonNull).findFirst();
684 Optional<Integer> setpointTemperatureProperty = deviceProperties.stream().map(p -> p.setpointTemperature)
685 .map(s -> (!((s == null) || s.isEmpty())) ? Math.round(Float.parseFloat(s) * 10) : null)
686 .filter(Objects::nonNull).findFirst();
687 Optional<Boolean> ecoSaveProperty = deviceProperties.stream().map(p -> p.ecoSave)
688 .map(s -> s != null ? Boolean.parseBoolean(s) : null).filter(Objects::nonNull).findFirst();
689 Optional<Integer> ambientTemperatureProperty = deviceProperties.stream().map(p -> p.ambientTemperature)
690 .map(s -> (!((s == null) || s.isEmpty())) ? Math.round(Float.parseFloat(s) * 10) : null)
691 .filter(Objects::nonNull).findFirst();
692 Optional<@Nullable String> demandProperty = deviceProperties.stream().map(p -> p.demand)
693 .filter(Objects::nonNull).findFirst();
694 Optional<@Nullable String> operationModeProperty = deviceProperties.stream().map(p -> p.operationMode)
695 .filter(Objects::nonNull).findFirst();
697 String modeString = deviceProperties.stream().map(p -> p.program).filter(Objects::nonNull).findFirst()
699 int mode = IntStream.range(0, THERMOSTATMODES.length).filter(i -> THERMOSTATMODES[i].equals(modeString))
700 .findFirst().orElse(thermostat.getMode());
702 int measured = ambientTemperatureProperty.orElse(thermostat.getMeasured());
703 int setpoint = setpointTemperatureProperty.orElse(thermostat.getSetpoint());
706 int overruletime = 0;
707 if (overruleActiveProperty.orElse(true)) {
708 overrule = overruleSetpointProperty.orElse(thermostat.getOverrule());
709 overruletime = overruleTimeProperty.orElse(thermostat.getRemainingOverruletime());
712 int ecosave = thermostat.getEcosave();
713 if (ecoSaveProperty.orElse(false)) {
717 int demand = thermostat.getDemand();
718 String demandString = demandProperty.orElse(operationModeProperty.orElse(""));
719 demandString = demandString == null ? "" : demandString;
720 switch (demandString) {
733 "setting thermostat {} with measured {}, setpoint {}, mode {}, overrule {}, overruletime {}, ecosave {}, demand {}",
734 thermostat.getId(), measured, setpoint, mode, overrule, overruletime, ecosave, demand);
735 thermostat.setState(measured, setpoint, mode, overrule, overruletime, ecosave, demand);
738 private void updateMeterState(NhcMeter2 meter, List<NhcProperty> deviceProperties) {
740 Optional<Integer> electricalPower = deviceProperties.stream().map(p -> p.electricalPower)
741 .map(s -> (!((s == null) || s.isEmpty())) ? Math.round(Float.parseFloat(s)) : null)
742 .filter(Objects::nonNull).findFirst();
743 Optional<Integer> powerFromGrid = deviceProperties.stream().map(p -> p.electricalPowerFromGrid)
744 .map(s -> (!((s == null) || s.isEmpty())) ? Math.round(Float.parseFloat(s)) : null)
745 .filter(Objects::nonNull).findFirst();
746 Optional<Integer> powerToGrid = deviceProperties.stream().map(p -> p.electricalPowerToGrid)
747 .map(s -> (!((s == null) || s.isEmpty())) ? Math.round(Float.parseFloat(s)) : null)
748 .filter(Objects::nonNull).findFirst();
749 int power = electricalPower.orElse(powerFromGrid.orElse(0) - powerToGrid.orElse(0));
750 logger.trace("setting energy meter {} power to {}", meter.getId(), power);
751 meter.setPower(power);
752 } catch (NumberFormatException e) {
753 logger.trace("wrong format in energy meter {} power reading", meter.getId());
754 meter.setPower(null);
758 private void updateAccessState(NhcAccess2 accessDevice, List<NhcProperty> deviceProperties) {
759 Optional<NhcProperty> basicStateProperty = deviceProperties.stream().filter(p -> (p.basicState != null))
761 Optional<NhcProperty> doorLockProperty = deviceProperties.stream().filter(p -> (p.doorlock != null))
764 if (basicStateProperty.isPresent()) {
765 String basicState = basicStateProperty.get().basicState;
766 boolean state = false;
767 if (NHCON.equals(basicState) || NHCTRUE.equals(basicState)) {
770 switch (accessDevice.getType()) {
772 accessDevice.updateRingAndComeInState(state);
773 logger.debug("setting access device {} ring and come in to {}", accessDevice.getId(), state);
776 accessDevice.updateBellState(state);
777 logger.debug("setting access device {} bell to {}", accessDevice.getId(), state);
784 if (doorLockProperty.isPresent()) {
785 String doorLockState = doorLockProperty.get().doorlock;
786 boolean state = false;
787 if (NHCCLOSED.equals(doorLockState)) {
790 logger.debug("setting access device {} doorlock to {}", accessDevice.getId(), state);
791 accessDevice.updateDoorLockState(state);
795 private void updateVideoState(NhcVideo2 videoDevice, List<NhcProperty> deviceProperties) {
796 String callStatus01 = deviceProperties.stream().map(p -> p.callStatus01).filter(Objects::nonNull).findFirst()
798 String callStatus02 = deviceProperties.stream().map(p -> p.callStatus02).filter(Objects::nonNull).findFirst()
800 String callStatus03 = deviceProperties.stream().map(p -> p.callStatus03).filter(Objects::nonNull).findFirst()
802 String callStatus04 = deviceProperties.stream().map(p -> p.callStatus04).filter(Objects::nonNull).findFirst()
805 logger.debug("setting video device {} call status to {}, {}, {}, {}", videoDevice.getId(), callStatus01,
806 callStatus02, callStatus03, callStatus04);
807 videoDevice.updateState(callStatus01, callStatus02, callStatus03, callStatus04);
811 public void executeAction(String actionId, String value) {
812 NhcMessage2 message = new NhcMessage2();
814 message.method = "devices.control";
815 ArrayList<NhcMessageParam> params = new ArrayList<>();
816 NhcMessageParam param = new NhcMessageParam();
818 message.params = params;
819 ArrayList<NhcDevice2> devices = new ArrayList<>();
820 NhcDevice2 device = new NhcDevice2();
822 param.devices = devices;
823 device.uuid = actionId;
824 ArrayList<NhcProperty> deviceProperties = new ArrayList<>();
825 NhcProperty property = new NhcProperty();
826 deviceProperties.add(property);
827 device.properties = deviceProperties;
829 NhcAction2 action = (NhcAction2) actions.get(actionId);
830 if (action == null) {
834 switch (action.getType()) {
837 property.basicState = NHCTRIGGERED;
840 property.status = value;
843 if (NHCON.equals(value)) {
844 action.setBooleanState(true); // this will trigger sending the stored brightness value event out
845 property.status = value;
846 } else if (NHCOFF.equals(value)) {
847 property.status = value;
850 action.setState(Integer.parseInt(value)); // set cached state to new brightness value to avoid
851 // switching on with old brightness value before
854 } catch (NumberFormatException e) {
855 logger.debug("internal error, trying to set invalid brightness value {} for dimmer {}", value,
860 // If the light is off, turn the light on before sending the brightness value, needs to happen
861 // in 2 separate messages.
862 if (!action.booleanState()) {
863 executeAction(actionId, NHCON);
865 property.brightness = value;
869 if (NHCSTOP.equals(value)) {
870 property.action = value;
871 } else if (NHCUP.equals(value)) {
872 property.position = "100";
873 } else if (NHCDOWN.equals(value)) {
874 property.position = "0";
876 property.position = value;
881 String topic = profile + "/control/devices/cmd";
882 String gsonMessage = gson.toJson(message);
883 sendDeviceMessage(topic, gsonMessage);
887 public void executeThermostat(String thermostatId, String mode) {
888 NhcMessage2 message = new NhcMessage2();
890 message.method = "devices.control";
891 ArrayList<NhcMessageParam> params = new ArrayList<>();
892 NhcMessageParam param = new NhcMessageParam();
894 message.params = params;
895 ArrayList<NhcDevice2> devices = new ArrayList<>();
896 NhcDevice2 device = new NhcDevice2();
898 param.devices = devices;
899 device.uuid = thermostatId;
900 ArrayList<NhcProperty> deviceProperties = new ArrayList<>();
902 NhcProperty overruleActiveProp = new NhcProperty();
903 deviceProperties.add(overruleActiveProp);
904 overruleActiveProp.overruleActive = "False";
906 NhcProperty program = new NhcProperty();
907 deviceProperties.add(program);
908 program.program = mode;
910 device.properties = deviceProperties;
912 String topic = profile + "/control/devices/cmd";
913 String gsonMessage = gson.toJson(message);
914 sendDeviceMessage(topic, gsonMessage);
918 public void executeThermostat(String thermostatId, int overruleTemp, int overruleTime) {
919 NhcMessage2 message = new NhcMessage2();
921 message.method = "devices.control";
922 ArrayList<NhcMessageParam> params = new ArrayList<>();
923 NhcMessageParam param = new NhcMessageParam();
925 message.params = params;
926 ArrayList<NhcDevice2> devices = new ArrayList<>();
927 NhcDevice2 device = new NhcDevice2();
929 param.devices = devices;
930 device.uuid = thermostatId;
931 ArrayList<NhcProperty> deviceProperties = new ArrayList<>();
933 if (overruleTime > 0) {
934 NhcProperty overruleActiveProp = new NhcProperty();
935 overruleActiveProp.overruleActive = "True";
936 deviceProperties.add(overruleActiveProp);
938 NhcProperty overruleSetpointProp = new NhcProperty();
939 overruleSetpointProp.overruleSetpoint = String.valueOf(overruleTemp / 10.0);
940 deviceProperties.add(overruleSetpointProp);
942 NhcProperty overruleTimeProp = new NhcProperty();
943 overruleTimeProp.overruleTime = String.valueOf(overruleTime);
944 deviceProperties.add(overruleTimeProp);
946 NhcProperty overruleActiveProp = new NhcProperty();
947 overruleActiveProp.overruleActive = "False";
948 deviceProperties.add(overruleActiveProp);
950 device.properties = deviceProperties;
952 String topic = profile + "/control/devices/cmd";
953 String gsonMessage = gson.toJson(message);
954 sendDeviceMessage(topic, gsonMessage);
958 public void executeMeter(String meterId) {
959 // Nothing to do, meter readings not supported in NHC II at this point in time
963 public void retriggerMeterLive(String meterId) {
964 NhcMessage2 message = new NhcMessage2();
966 message.method = "devices.control";
967 ArrayList<NhcMessageParam> params = new ArrayList<>();
968 NhcMessageParam param = new NhcMessageParam();
970 message.params = params;
971 ArrayList<NhcDevice2> devices = new ArrayList<>();
972 NhcDevice2 device = new NhcDevice2();
974 param.devices = devices;
975 device.uuid = meterId;
976 ArrayList<NhcProperty> deviceProperties = new ArrayList<>();
978 NhcProperty reportInstantUsageProp = new NhcProperty();
979 deviceProperties.add(reportInstantUsageProp);
980 reportInstantUsageProp.reportInstantUsage = "True";
981 device.properties = deviceProperties;
983 String topic = profile + "/control/devices/cmd";
984 String gsonMessage = gson.toJson(message);
986 sendDeviceMessage(topic, gsonMessage);
990 public void executeAccessBell(String accessId) {
991 executeAccess(accessId);
995 public void executeAccessRingAndComeIn(String accessId, boolean ringAndComeIn) {
996 NhcAccess2 accessDevice = (NhcAccess2) accessDevices.get(accessId);
997 if (accessDevice == null) {
1001 boolean current = accessDevice.getRingAndComeInState();
1002 if ((ringAndComeIn && !current) || (!ringAndComeIn && current)) {
1003 executeAccess(accessId);
1005 logger.trace("Not updating ring and come in as state did not change");
1009 private void executeAccess(String accessId) {
1010 NhcMessage2 message = new NhcMessage2();
1012 message.method = "devices.control";
1013 List<NhcMessageParam> params = new ArrayList<>();
1014 NhcMessageParam param = new NhcMessageParam();
1016 message.params = params;
1017 List<NhcDevice2> devices = new ArrayList<>();
1018 NhcDevice2 device = new NhcDevice2();
1019 devices.add(device);
1020 param.devices = devices;
1021 device.uuid = accessId;
1022 List<NhcProperty> deviceProperties = new ArrayList<>();
1023 NhcProperty property = new NhcProperty();
1024 deviceProperties.add(property);
1025 device.properties = deviceProperties;
1027 NhcAccess2 accessDevice = (NhcAccess2) accessDevices.get(accessId);
1028 if (accessDevice == null) {
1032 property.basicState = NHCTRIGGERED;
1034 String topic = profile + "/control/devices/cmd";
1035 String gsonMessage = gson.toJson(message);
1036 sendDeviceMessage(topic, gsonMessage);
1040 public void executeVideoBell(String accessId, int buttonIndex) {
1041 NhcMessage2 message = new NhcMessage2();
1043 message.method = "devices.control";
1044 List<NhcMessageParam> params = new ArrayList<>();
1045 NhcMessageParam param = new NhcMessageParam();
1047 message.params = params;
1048 List<NhcDevice2> devices = new ArrayList<>();
1049 NhcDevice2 device = new NhcDevice2();
1050 devices.add(device);
1051 param.devices = devices;
1052 device.uuid = accessId;
1053 List<NhcProperty> deviceProperties = new ArrayList<>();
1054 NhcProperty property = new NhcProperty();
1055 deviceProperties.add(property);
1056 device.properties = deviceProperties;
1058 NhcVideo videoDevice = videoDevices.get(accessId);
1059 if (videoDevice == null) {
1063 switch (buttonIndex) {
1065 property.callStatus01 = NHCRINGING;
1068 property.callStatus02 = NHCRINGING;
1071 property.callStatus03 = NHCRINGING;
1074 property.callStatus04 = NHCRINGING;
1080 String topic = profile + "/control/devices/cmd";
1081 String gsonMessage = gson.toJson(message);
1082 sendDeviceMessage(topic, gsonMessage);
1086 public void executeAccessUnlock(String accessId) {
1087 NhcMessage2 message = new NhcMessage2();
1089 message.method = "devices.control";
1090 List<NhcMessageParam> params = new ArrayList<>();
1091 NhcMessageParam param = new NhcMessageParam();
1093 message.params = params;
1094 ArrayList<NhcDevice2> devices = new ArrayList<>();
1095 NhcDevice2 device = new NhcDevice2();
1096 devices.add(device);
1097 param.devices = devices;
1098 device.uuid = accessId;
1099 ArrayList<NhcProperty> deviceProperties = new ArrayList<>();
1100 NhcProperty property = new NhcProperty();
1101 deviceProperties.add(property);
1102 device.properties = deviceProperties;
1104 NhcAccess2 accessDevice = (NhcAccess2) accessDevices.get(accessId);
1105 if (accessDevice == null) {
1109 property.doorlock = NHCOPEN;
1111 String topic = profile + "/control/devices/cmd";
1112 String gsonMessage = gson.toJson(message);
1113 sendDeviceMessage(topic, gsonMessage);
1116 private void sendDeviceMessage(String topic, String gsonMessage) {
1118 mqttConnection.connectionPublish(topic, gsonMessage);
1120 } catch (MqttException e) {
1121 String message = e.getLocalizedMessage();
1123 logger.debug("sending command failed, trying to restart communication");
1124 restartCommunication();
1125 // retry sending after restart
1127 if (communicationActive()) {
1128 mqttConnection.connectionPublish(topic, gsonMessage);
1130 logger.debug("failed to restart communication");
1132 } catch (MqttException e1) {
1133 message = e1.getLocalizedMessage();
1135 logger.debug("error resending device command");
1137 if (!communicationActive()) {
1138 message = (message != null) ? message : "@text/offline.communication-error";
1139 connectionLost(message);
1140 // Keep on trying to restart, but don't send message anymore
1141 scheduleRestartCommunication();
1147 public void processMessage(String topic, byte[] payload) {
1148 String message = new String(payload);
1149 if ((profile + "/system/evt").equals(topic)) {
1151 } else if ((profile + "/system/rsp").equals(topic)) {
1152 logger.debug("received topic {}, payload {}", topic, message);
1153 systeminfoPublishRsp(message);
1154 } else if ((profile + "/notification/evt").equals(topic)) {
1155 logger.debug("received topic {}, payload {}", topic, message);
1156 notificationEvt(message);
1157 } else if ((profile + "/control/devices/evt").equals(topic)) {
1158 logger.trace("received topic {}, payload {}", topic, message);
1159 devicesEvt(message);
1160 } else if ((profile + "/control/devices/rsp").equals(topic)) {
1161 logger.debug("received topic {}, payload {}", topic, message);
1162 devicesListRsp(message);
1163 } else if ((profile + "/authentication/rsp").equals(topic)) {
1164 logger.debug("received topic {}, payload {}", topic, message);
1165 servicesListRsp(message);
1166 } else if ((profile + "/control/devices.error").equals(topic)) {
1167 logger.warn("received error {}", message);
1169 logger.trace("not acted on received message topic {}, payload {}", topic, message);
1174 * @return system info retrieved from Connected Controller
1176 public NhcSystemInfo2 getSystemInfo() {
1177 NhcSystemInfo2 systemInfo = nhcSystemInfo;
1178 if (systemInfo == null) {
1179 systemInfo = new NhcSystemInfo2();
1185 * @return time info retrieved from Connected Controller
1187 public NhcTimeInfo2 getTimeInfo() {
1188 NhcTimeInfo2 timeInfo = nhcTimeInfo;
1189 if (timeInfo == null) {
1190 timeInfo = new NhcTimeInfo2();
1196 * @return comma separated list of services retrieved from Connected Controller
1198 public String getServices() {
1199 return services.stream().map(NhcService2::name).collect(Collectors.joining(", "));
1203 public void connectionStateChanged(MqttConnectionState state, @Nullable Throwable error) {
1204 // do in separate thread as this method needs to return early
1205 scheduler.submit(() -> {
1206 if (error != null) {
1207 logger.debug("Connection state: {}, error", state, error);
1208 String localizedMessage = error.getLocalizedMessage();
1209 String message = (localizedMessage != null) ? localizedMessage : "@text/offline.communication-error";
1210 connectionLost(message);
1211 scheduleRestartCommunication();
1212 } else if ((state == MqttConnectionState.CONNECTED) && !initStarted) {
1215 logger.trace("Connection state: {}", state);