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)) {
386 addAlarmDevice(device, location);
387 } else if ("action".equals(device.type) || "virtual".equals(device.type)) {
388 addActionDevice(device, location);
389 } else if ("thermostat".equals(device.type)) {
390 addThermostatDevice(device, location);
391 } else if ("centralmeter".equals(device.type) || "energyhome".equals(device.type)) {
392 addMeterDevice(device, location);
394 logger.debug("device type {} and model {} not supported for {}, {}", device.type, device.model, device.uuid,
399 private void addActionDevice(NhcDevice2 device, @Nullable String location) {
400 ActionType actionType;
401 switch (device.model) {
407 case "overallcomfort":
409 actionType = ActionType.TRIGGER;
413 case "switched-generic":
416 actionType = ActionType.RELAY;
419 actionType = ActionType.DIMMER;
421 case "rolldownshutter":
423 case "venetianblind":
425 actionType = ActionType.ROLLERSHUTTER;
428 actionType = ActionType.GENERIC;
429 logger.debug("device type {} and model {} not recognised for {}, {}, ignoring", device.type,
430 device.model, device.uuid, device.name);
434 NhcAction nhcAction = actions.get(device.uuid);
435 if (nhcAction != null) {
436 // update name and location so discovery will see updated name and location
437 nhcAction.setName(device.name);
438 nhcAction.setLocation(location);
440 logger.debug("adding action device {} model {}, {}", device.uuid, device.model, device.name);
441 nhcAction = new NhcAction2(device.uuid, device.name, device.type, device.technology, device.model, location,
444 actions.put(device.uuid, nhcAction);
447 private void addThermostatDevice(NhcDevice2 device, @Nullable String location) {
448 NhcThermostat nhcThermostat = thermostats.get(device.uuid);
449 if (nhcThermostat != null) {
450 nhcThermostat.setName(device.name);
451 nhcThermostat.setLocation(location);
453 logger.debug("adding thermostat device {} model {}, {}", device.uuid, device.model, device.name);
454 nhcThermostat = new NhcThermostat2(device.uuid, device.name, device.type, device.technology, device.model,
457 thermostats.put(device.uuid, nhcThermostat);
460 private void addMeterDevice(NhcDevice2 device, @Nullable String location) {
461 NhcMeter nhcMeter = meters.get(device.uuid);
462 if (nhcMeter != null) {
463 nhcMeter.setName(device.name);
464 nhcMeter.setLocation(location);
466 logger.debug("adding energy meter device {} model {}, {}", device.uuid, device.model, device.name);
467 nhcMeter = new NhcMeter2(device.uuid, device.name, MeterType.ENERGY_LIVE, device.type, device.technology,
468 device.model, null, location, this, scheduler);
470 meters.put(device.uuid, nhcMeter);
473 private void addAccessDevice(NhcDevice2 device, @Nullable String location) {
474 AccessType accessType = AccessType.BASE;
475 if ("bellbutton".equals(device.model)) {
476 accessType = AccessType.BELLBUTTON;
478 List<NhcProperty> properties = device.properties;
479 if (properties != null) {
480 boolean hasBasicState = properties.stream().anyMatch(p -> (p.basicState != null));
482 accessType = AccessType.RINGANDCOMEIN;
487 NhcAccess2 nhcAccess = (NhcAccess2) accessDevices.get(device.uuid);
488 if (nhcAccess != null) {
489 nhcAccess.setName(device.name);
490 nhcAccess.setLocation(location);
492 String buttonId = null;
493 List<NhcParameter> parameters = device.parameters;
494 if (parameters != null) {
495 buttonId = parameters.stream().map(p -> p.buttonId).filter(Objects::nonNull).findFirst().orElse(null);
498 logger.debug("adding access device {} model {} type {}, {}", device.uuid, device.model, accessType,
500 nhcAccess = new NhcAccess2(device.uuid, device.name, device.type, device.technology, device.model, location,
501 accessType, buttonId, this);
503 if (buttonId != null) {
504 NhcAccess2 access = nhcAccess;
505 String macAddress = buttonId.split("_")[0];
506 videoDevices.forEach((key, videoDevice) -> {
507 if (macAddress.equals(videoDevice.getMacAddress())) {
508 int buttonIndex = access.getButtonIndex();
509 logger.debug("link access device {} to video device {} button {}", device.uuid,
510 videoDevice.getId(), buttonIndex);
511 videoDevice.setNhcAccess(buttonIndex, access);
512 access.setNhcVideo(videoDevice);
517 accessDevices.put(device.uuid, nhcAccess);
520 private void addVideoDevice(NhcDevice2 device) {
521 NhcVideo2 nhcVideo = (NhcVideo2) videoDevices.get(device.uuid);
522 if (nhcVideo != null) {
523 nhcVideo.setName(device.name);
525 String macAddress = null;
526 String ipAddress = null;
527 String mjpegUri = null;
529 List<NhcTrait> traits = device.traits;
530 if (traits != null) {
531 macAddress = traits.stream().map(t -> t.macAddress).filter(Objects::nonNull).findFirst().orElse(null);
533 List<NhcParameter> parameters = device.parameters;
534 if (parameters != null) {
535 mjpegUri = parameters.stream().map(p -> p.mjpegUri).filter(Objects::nonNull).findFirst().orElse(null);
536 tnUri = parameters.stream().map(p -> p.tnUri).filter(Objects::nonNull).findFirst().orElse(null);
538 List<NhcProperty> properties = device.properties;
539 if (properties != null) {
540 ipAddress = properties.stream().map(p -> p.ipAddress).filter(Objects::nonNull).findFirst().orElse(null);
543 logger.debug("adding video device {} model {}, {}", device.uuid, device.model, device.name);
544 nhcVideo = new NhcVideo2(device.uuid, device.name, device.type, device.technology, device.model, macAddress,
545 ipAddress, mjpegUri, tnUri, this);
547 if (macAddress != null) {
548 NhcVideo2 video = nhcVideo;
549 String mac = macAddress;
550 accessDevices.forEach((key, accessDevice) -> {
551 NhcAccess2 access = (NhcAccess2) accessDevice;
552 String buttonMac = access.getButtonId();
553 if (buttonMac != null) {
554 buttonMac = buttonMac.split("_")[0];
555 if (mac.equals(buttonMac)) {
556 int buttonIndex = access.getButtonIndex();
557 logger.debug("link access device {} to video device {} button {}", accessDevice.getId(),
558 device.uuid, buttonIndex);
559 video.setNhcAccess(buttonIndex, access);
560 access.setNhcVideo(video);
566 videoDevices.put(device.uuid, nhcVideo);
569 private void addAlarmDevice(NhcDevice2 device, @Nullable String location) {
570 NhcAlarm nhcAlarm = alarmDevices.get(device.uuid);
571 if (nhcAlarm != null) {
572 nhcAlarm.setName(device.name);
573 nhcAlarm.setLocation(location);
575 logger.debug("adding alarm device {} model {}, {}", device.uuid, device.model, device.name);
576 nhcAlarm = new NhcAlarm2(device.uuid, device.name, device.type, device.technology, device.model, location,
579 alarmDevices.put(device.uuid, nhcAlarm);
582 private void removeDevice(NhcDevice2 device) {
583 NhcAction action = actions.get(device.uuid);
584 NhcThermostat thermostat = thermostats.get(device.uuid);
585 NhcMeter meter = meters.get(device.uuid);
586 NhcAccess access = accessDevices.get(device.uuid);
587 NhcVideo video = videoDevices.get(device.uuid);
588 NhcAlarm alarm = alarmDevices.get(device.uuid);
589 if (action != null) {
590 action.actionRemoved();
591 actions.remove(device.uuid);
592 } else if (thermostat != null) {
593 thermostat.thermostatRemoved();
594 thermostats.remove(device.uuid);
595 } else if (meter != null) {
596 meter.meterRemoved();
597 meters.remove(device.uuid);
598 } else if (access != null) {
599 access.accessDeviceRemoved();
600 accessDevices.remove(device.uuid);
601 } else if (video != null) {
602 video.videoDeviceRemoved();
603 videoDevices.remove(device.uuid);
604 } else if (alarm != null) {
605 alarm.alarmDeviceRemoved();
606 alarmDevices.remove(device.uuid);
610 private void updateState(NhcDevice2 device) {
611 List<NhcProperty> deviceProperties = device.properties;
612 if (deviceProperties == null) {
613 logger.debug("Cannot Update state for {} as no properties defined in device message", device.uuid);
617 NhcAction action = actions.get(device.uuid);
618 NhcThermostat thermostat = thermostats.get(device.uuid);
619 NhcMeter meter = meters.get(device.uuid);
620 NhcAccess accessDevice = accessDevices.get(device.uuid);
621 NhcVideo videoDevice = videoDevices.get(device.uuid);
622 NhcAlarm alarm = alarmDevices.get(device.uuid);
624 if (action != null) {
625 updateActionState((NhcAction2) action, deviceProperties);
626 } else if (thermostat != null) {
627 updateThermostatState((NhcThermostat2) thermostat, deviceProperties);
628 } else if (meter != null) {
629 updateMeterState((NhcMeter2) meter, deviceProperties);
630 } else if (accessDevice != null) {
631 updateAccessState((NhcAccess2) accessDevice, deviceProperties);
632 } else if (videoDevice != null) {
633 updateVideoState((NhcVideo2) videoDevice, deviceProperties);
634 } else if (alarm != null) {
635 updateAlarmState((NhcAlarm2) alarm, deviceProperties);
637 logger.trace("No known device for {}", device.uuid);
641 private void updateActionState(NhcAction2 action, List<NhcProperty> deviceProperties) {
642 if (action.getType() == ActionType.ROLLERSHUTTER) {
643 updateRollershutterState(action, deviceProperties);
645 updateLightState(action, deviceProperties);
649 private void updateLightState(NhcAction2 action, List<NhcProperty> deviceProperties) {
650 Optional<NhcProperty> statusProperty = deviceProperties.stream().filter(p -> (p.status != null)).findFirst();
651 Optional<NhcProperty> dimmerProperty = deviceProperties.stream().filter(p -> (p.brightness != null))
653 Optional<NhcProperty> basicStateProperty = deviceProperties.stream().filter(p -> (p.basicState != null))
656 String booleanState = null;
657 if (statusProperty.isPresent()) {
658 booleanState = statusProperty.get().status;
659 } else if (basicStateProperty.isPresent()) {
660 booleanState = basicStateProperty.get().basicState;
663 if (NHCOFF.equals(booleanState) || NHCFALSE.equals(booleanState)) {
664 action.setBooleanState(false);
665 logger.debug("setting action {} internally to OFF", action.getId());
668 if (dimmerProperty.isPresent()) {
669 String brightness = dimmerProperty.get().brightness;
670 if (brightness != null) {
672 logger.debug("setting action {} internally to {}", action.getId(), dimmerProperty.get().brightness);
673 action.setState(Integer.parseInt(brightness));
674 } catch (NumberFormatException e) {
675 logger.debug("received invalid brightness value {} for dimmer {}", brightness, action.getId());
680 if (NHCON.equals(booleanState) || NHCTRUE.equals(booleanState)) {
681 logger.debug("setting action {} internally to ON", action.getId());
682 action.setBooleanState(true);
686 private void updateRollershutterState(NhcAction2 action, List<NhcProperty> deviceProperties) {
687 deviceProperties.stream().map(p -> p.position).filter(Objects::nonNull).findFirst().ifPresent(position -> {
689 logger.debug("setting action {} internally to {}", action.getId(), position);
690 action.setState(Integer.parseInt(position));
691 } catch (NumberFormatException e) {
692 logger.trace("received empty or invalid rollershutter {} position info {}", action.getId(), position);
697 private void updateThermostatState(NhcThermostat2 thermostat, List<NhcProperty> deviceProperties) {
698 Optional<Boolean> overruleActiveProperty = deviceProperties.stream().map(p -> p.overruleActive)
699 .filter(Objects::nonNull).map(t -> Boolean.parseBoolean(t)).findFirst();
700 Optional<Integer> overruleSetpointProperty = deviceProperties.stream().map(p -> p.overruleSetpoint)
701 .map(s -> (!((s == null) || s.isEmpty())) ? Math.round(Float.parseFloat(s) * 10) : null)
702 .filter(Objects::nonNull).findFirst();
703 Optional<Integer> overruleTimeProperty = deviceProperties.stream().map(p -> p.overruleTime)
704 .map(s -> (!((s == null) || s.isEmpty())) ? Math.round(Float.parseFloat(s)) : null)
705 .filter(Objects::nonNull).findFirst();
706 Optional<Integer> setpointTemperatureProperty = deviceProperties.stream().map(p -> p.setpointTemperature)
707 .map(s -> (!((s == null) || s.isEmpty())) ? Math.round(Float.parseFloat(s) * 10) : null)
708 .filter(Objects::nonNull).findFirst();
709 Optional<Boolean> ecoSaveProperty = deviceProperties.stream().map(p -> p.ecoSave)
710 .map(s -> s != null ? Boolean.parseBoolean(s) : null).filter(Objects::nonNull).findFirst();
711 Optional<Integer> ambientTemperatureProperty = deviceProperties.stream().map(p -> p.ambientTemperature)
712 .map(s -> (!((s == null) || s.isEmpty())) ? Math.round(Float.parseFloat(s) * 10) : null)
713 .filter(Objects::nonNull).findFirst();
714 Optional<@Nullable String> demandProperty = deviceProperties.stream().map(p -> p.demand)
715 .filter(Objects::nonNull).findFirst();
716 Optional<@Nullable String> operationModeProperty = deviceProperties.stream().map(p -> p.operationMode)
717 .filter(Objects::nonNull).findFirst();
719 String modeString = deviceProperties.stream().map(p -> p.program).filter(Objects::nonNull).findFirst()
721 int mode = IntStream.range(0, THERMOSTATMODES.length).filter(i -> THERMOSTATMODES[i].equals(modeString))
722 .findFirst().orElse(thermostat.getMode());
724 int measured = ambientTemperatureProperty.orElse(thermostat.getMeasured());
725 int setpoint = setpointTemperatureProperty.orElse(thermostat.getSetpoint());
728 int overruletime = 0;
729 if (overruleActiveProperty.orElse(true)) {
730 overrule = overruleSetpointProperty.orElse(thermostat.getOverrule());
731 overruletime = overruleTimeProperty.orElse(thermostat.getRemainingOverruletime());
734 int ecosave = thermostat.getEcosave();
735 if (ecoSaveProperty.orElse(false)) {
739 int demand = thermostat.getDemand();
740 String demandString = demandProperty.orElse(operationModeProperty.orElse(""));
741 demandString = demandString == null ? "" : demandString;
742 switch (demandString) {
755 "setting thermostat {} with measured {}, setpoint {}, mode {}, overrule {}, overruletime {}, ecosave {}, demand {}",
756 thermostat.getId(), measured, setpoint, mode, overrule, overruletime, ecosave, demand);
757 thermostat.setState(measured, setpoint, mode, overrule, overruletime, ecosave, demand);
760 private void updateMeterState(NhcMeter2 meter, List<NhcProperty> deviceProperties) {
762 Optional<Integer> electricalPower = deviceProperties.stream().map(p -> p.electricalPower)
763 .map(s -> (!((s == null) || s.isEmpty())) ? Math.round(Float.parseFloat(s)) : null)
764 .filter(Objects::nonNull).findFirst();
765 Optional<Integer> powerFromGrid = deviceProperties.stream().map(p -> p.electricalPowerFromGrid)
766 .map(s -> (!((s == null) || s.isEmpty())) ? Math.round(Float.parseFloat(s)) : null)
767 .filter(Objects::nonNull).findFirst();
768 Optional<Integer> powerToGrid = deviceProperties.stream().map(p -> p.electricalPowerToGrid)
769 .map(s -> (!((s == null) || s.isEmpty())) ? Math.round(Float.parseFloat(s)) : null)
770 .filter(Objects::nonNull).findFirst();
771 int power = electricalPower.orElse(powerFromGrid.orElse(0) - powerToGrid.orElse(0));
772 logger.trace("setting energy meter {} power to {}", meter.getId(), power);
773 meter.setPower(power);
774 } catch (NumberFormatException e) {
775 logger.trace("wrong format in energy meter {} power reading", meter.getId());
776 meter.setPower(null);
780 private void updateAccessState(NhcAccess2 accessDevice, List<NhcProperty> deviceProperties) {
781 Optional<NhcProperty> basicStateProperty = deviceProperties.stream().filter(p -> (p.basicState != null))
783 Optional<NhcProperty> doorLockProperty = deviceProperties.stream().filter(p -> (p.doorlock != null))
786 if (basicStateProperty.isPresent()) {
787 String basicState = basicStateProperty.get().basicState;
788 boolean state = false;
789 if (NHCON.equals(basicState) || NHCTRUE.equals(basicState)) {
792 switch (accessDevice.getType()) {
794 accessDevice.updateRingAndComeInState(state);
795 logger.debug("setting access device {} ring and come in to {}", accessDevice.getId(), state);
798 accessDevice.updateBellState(state);
799 logger.debug("setting access device {} bell to {}", accessDevice.getId(), state);
806 if (doorLockProperty.isPresent()) {
807 String doorLockState = doorLockProperty.get().doorlock;
808 boolean state = false;
809 if (NHCCLOSED.equals(doorLockState)) {
812 logger.debug("setting access device {} doorlock to {}", accessDevice.getId(), state);
813 accessDevice.updateDoorLockState(state);
817 private void updateVideoState(NhcVideo2 videoDevice, List<NhcProperty> deviceProperties) {
818 String callStatus01 = deviceProperties.stream().map(p -> p.callStatus01).filter(Objects::nonNull).findFirst()
820 String callStatus02 = deviceProperties.stream().map(p -> p.callStatus02).filter(Objects::nonNull).findFirst()
822 String callStatus03 = deviceProperties.stream().map(p -> p.callStatus03).filter(Objects::nonNull).findFirst()
824 String callStatus04 = deviceProperties.stream().map(p -> p.callStatus04).filter(Objects::nonNull).findFirst()
827 logger.debug("setting video device {} call status to {}, {}, {}, {}", videoDevice.getId(), callStatus01,
828 callStatus02, callStatus03, callStatus04);
829 videoDevice.updateState(callStatus01, callStatus02, callStatus03, callStatus04);
832 private void updateAlarmState(NhcAlarm2 alarmDevice, List<NhcProperty> deviceProperties) {
833 String state = deviceProperties.stream().map(p -> p.internalState).filter(Objects::nonNull).findFirst()
836 logger.debug("setting alarm device {} state to {}", alarmDevice.getId(), state);
837 alarmDevice.setState(state);
839 String triggered = deviceProperties.stream().map(p -> p.alarmTriggered).filter(Objects::nonNull).findFirst()
841 if (Boolean.valueOf(triggered)) {
842 logger.debug("triggering alarm device {}", alarmDevice.getId());
843 alarmDevice.triggerAlarm();
848 public void executeAction(String actionId, String value) {
849 NhcMessage2 message = new NhcMessage2();
851 message.method = "devices.control";
852 List<NhcMessageParam> params = new ArrayList<>();
853 NhcMessageParam param = new NhcMessageParam();
855 message.params = params;
856 List<NhcDevice2> devices = new ArrayList<>();
857 NhcDevice2 device = new NhcDevice2();
859 param.devices = devices;
860 device.uuid = actionId;
861 List<NhcProperty> deviceProperties = new ArrayList<>();
862 NhcProperty property = new NhcProperty();
863 deviceProperties.add(property);
864 device.properties = deviceProperties;
866 NhcAction2 action = (NhcAction2) actions.get(actionId);
867 if (action == null) {
871 switch (action.getType()) {
874 property.basicState = NHCTRIGGERED;
877 property.status = value;
880 if (NHCON.equals(value)) {
881 action.setBooleanState(true); // this will trigger sending the stored brightness value event out
882 property.status = value;
883 } else if (NHCOFF.equals(value)) {
884 property.status = value;
887 action.setState(Integer.parseInt(value)); // set cached state to new brightness value to avoid
888 // switching on with old brightness value before
891 } catch (NumberFormatException e) {
892 logger.debug("internal error, trying to set invalid brightness value {} for dimmer {}", value,
897 // If the light is off, turn the light on before sending the brightness value, needs to happen
898 // in 2 separate messages.
899 if (!action.booleanState()) {
900 executeAction(actionId, NHCON);
902 property.brightness = value;
906 if (NHCSTOP.equals(value)) {
907 property.action = value;
908 } else if (NHCUP.equals(value)) {
909 property.position = "100";
910 } else if (NHCDOWN.equals(value)) {
911 property.position = "0";
913 property.position = value;
918 String topic = profile + "/control/devices/cmd";
919 String gsonMessage = gson.toJson(message);
920 sendDeviceMessage(topic, gsonMessage);
924 public void executeThermostat(String thermostatId, String mode) {
925 NhcMessage2 message = new NhcMessage2();
927 message.method = "devices.control";
928 List<NhcMessageParam> params = new ArrayList<>();
929 NhcMessageParam param = new NhcMessageParam();
931 message.params = params;
932 List<NhcDevice2> devices = new ArrayList<>();
933 NhcDevice2 device = new NhcDevice2();
935 param.devices = devices;
936 device.uuid = thermostatId;
937 List<NhcProperty> deviceProperties = new ArrayList<>();
939 NhcProperty overruleActiveProp = new NhcProperty();
940 deviceProperties.add(overruleActiveProp);
941 overruleActiveProp.overruleActive = "False";
943 NhcProperty program = new NhcProperty();
944 deviceProperties.add(program);
945 program.program = mode;
947 device.properties = deviceProperties;
949 String topic = profile + "/control/devices/cmd";
950 String gsonMessage = gson.toJson(message);
951 sendDeviceMessage(topic, gsonMessage);
955 public void executeThermostat(String thermostatId, int overruleTemp, int overruleTime) {
956 NhcMessage2 message = new NhcMessage2();
958 message.method = "devices.control";
959 List<NhcMessageParam> params = new ArrayList<>();
960 NhcMessageParam param = new NhcMessageParam();
962 message.params = params;
963 List<NhcDevice2> devices = new ArrayList<>();
964 NhcDevice2 device = new NhcDevice2();
966 param.devices = devices;
967 device.uuid = thermostatId;
968 List<NhcProperty> deviceProperties = new ArrayList<>();
970 if (overruleTime > 0) {
971 NhcProperty overruleActiveProp = new NhcProperty();
972 overruleActiveProp.overruleActive = "True";
973 deviceProperties.add(overruleActiveProp);
975 NhcProperty overruleSetpointProp = new NhcProperty();
976 overruleSetpointProp.overruleSetpoint = String.valueOf(overruleTemp / 10.0);
977 deviceProperties.add(overruleSetpointProp);
979 NhcProperty overruleTimeProp = new NhcProperty();
980 overruleTimeProp.overruleTime = String.valueOf(overruleTime);
981 deviceProperties.add(overruleTimeProp);
983 NhcProperty overruleActiveProp = new NhcProperty();
984 overruleActiveProp.overruleActive = "False";
985 deviceProperties.add(overruleActiveProp);
987 device.properties = deviceProperties;
989 String topic = profile + "/control/devices/cmd";
990 String gsonMessage = gson.toJson(message);
991 sendDeviceMessage(topic, gsonMessage);
995 public void executeMeter(String meterId) {
996 // Nothing to do, individual meter readings not supported in NHC II at this point in time
1000 public void retriggerMeterLive(String meterId) {
1001 NhcMessage2 message = new NhcMessage2();
1003 message.method = "devices.control";
1004 List<NhcMessageParam> params = new ArrayList<>();
1005 NhcMessageParam param = new NhcMessageParam();
1007 message.params = params;
1008 List<NhcDevice2> devices = new ArrayList<>();
1009 NhcDevice2 device = new NhcDevice2();
1010 devices.add(device);
1011 param.devices = devices;
1012 device.uuid = meterId;
1013 List<NhcProperty> deviceProperties = new ArrayList<>();
1015 NhcProperty reportInstantUsageProp = new NhcProperty();
1016 deviceProperties.add(reportInstantUsageProp);
1017 reportInstantUsageProp.reportInstantUsage = "True";
1018 device.properties = deviceProperties;
1020 String topic = profile + "/control/devices/cmd";
1021 String gsonMessage = gson.toJson(message);
1023 sendDeviceMessage(topic, gsonMessage);
1027 public void executeAccessBell(String accessId) {
1028 executeAccess(accessId);
1032 public void executeAccessRingAndComeIn(String accessId, boolean ringAndComeIn) {
1033 NhcAccess2 accessDevice = (NhcAccess2) accessDevices.get(accessId);
1034 if (accessDevice == null) {
1038 boolean current = accessDevice.getRingAndComeInState();
1039 if ((ringAndComeIn && !current) || (!ringAndComeIn && current)) {
1040 executeAccess(accessId);
1042 logger.trace("Not updating ring and come in as state did not change");
1046 private void executeAccess(String accessId) {
1047 NhcMessage2 message = new NhcMessage2();
1049 message.method = "devices.control";
1050 List<NhcMessageParam> params = new ArrayList<>();
1051 NhcMessageParam param = new NhcMessageParam();
1053 message.params = params;
1054 List<NhcDevice2> devices = new ArrayList<>();
1055 NhcDevice2 device = new NhcDevice2();
1056 devices.add(device);
1057 param.devices = devices;
1058 device.uuid = accessId;
1059 List<NhcProperty> deviceProperties = new ArrayList<>();
1060 NhcProperty property = new NhcProperty();
1061 deviceProperties.add(property);
1062 device.properties = deviceProperties;
1064 NhcAccess2 accessDevice = (NhcAccess2) accessDevices.get(accessId);
1065 if (accessDevice == null) {
1069 property.basicState = NHCTRIGGERED;
1071 String topic = profile + "/control/devices/cmd";
1072 String gsonMessage = gson.toJson(message);
1073 sendDeviceMessage(topic, gsonMessage);
1077 public void executeVideoBell(String accessId, int buttonIndex) {
1078 NhcMessage2 message = new NhcMessage2();
1080 message.method = "devices.control";
1081 List<NhcMessageParam> params = new ArrayList<>();
1082 NhcMessageParam param = new NhcMessageParam();
1084 message.params = params;
1085 List<NhcDevice2> devices = new ArrayList<>();
1086 NhcDevice2 device = new NhcDevice2();
1087 devices.add(device);
1088 param.devices = devices;
1089 device.uuid = accessId;
1090 List<NhcProperty> deviceProperties = new ArrayList<>();
1091 NhcProperty property = new NhcProperty();
1092 deviceProperties.add(property);
1093 device.properties = deviceProperties;
1095 NhcVideo videoDevice = videoDevices.get(accessId);
1096 if (videoDevice == null) {
1100 switch (buttonIndex) {
1102 property.callStatus01 = NHCRINGING;
1105 property.callStatus02 = NHCRINGING;
1108 property.callStatus03 = NHCRINGING;
1111 property.callStatus04 = NHCRINGING;
1117 String topic = profile + "/control/devices/cmd";
1118 String gsonMessage = gson.toJson(message);
1119 sendDeviceMessage(topic, gsonMessage);
1123 public void executeAccessUnlock(String accessId) {
1124 NhcMessage2 message = new NhcMessage2();
1126 message.method = "devices.control";
1127 List<NhcMessageParam> params = new ArrayList<>();
1128 NhcMessageParam param = new NhcMessageParam();
1130 message.params = params;
1131 List<NhcDevice2> devices = new ArrayList<>();
1132 NhcDevice2 device = new NhcDevice2();
1133 devices.add(device);
1134 param.devices = devices;
1135 device.uuid = accessId;
1136 List<NhcProperty> deviceProperties = new ArrayList<>();
1137 NhcProperty property = new NhcProperty();
1138 deviceProperties.add(property);
1139 device.properties = deviceProperties;
1141 NhcAccess2 accessDevice = (NhcAccess2) accessDevices.get(accessId);
1142 if (accessDevice == null) {
1146 property.doorlock = NHCOPEN;
1148 String topic = profile + "/control/devices/cmd";
1149 String gsonMessage = gson.toJson(message);
1150 sendDeviceMessage(topic, gsonMessage);
1154 public void executeArm(String alarmId) {
1155 executeAlarm(alarmId, NHCARM);
1159 public void executeDisarm(String alarmId) {
1160 executeAlarm(alarmId, NHCDISARM);
1163 private void executeAlarm(String alarmId, String state) {
1164 NhcMessage2 message = new NhcMessage2();
1166 message.method = "devices.control";
1167 List<NhcMessageParam> params = new ArrayList<>();
1168 NhcMessageParam param = new NhcMessageParam();
1170 message.params = params;
1171 List<NhcDevice2> devices = new ArrayList<>();
1172 NhcDevice2 device = new NhcDevice2();
1173 devices.add(device);
1174 param.devices = devices;
1175 device.uuid = alarmId;
1176 List<NhcProperty> deviceProperties = new ArrayList<>();
1177 NhcProperty property = new NhcProperty();
1178 deviceProperties.add(property);
1179 device.properties = deviceProperties;
1181 NhcAlarm2 alarmDevice = (NhcAlarm2) alarmDevices.get(alarmId);
1182 if (alarmDevice == null) {
1186 property.control = state;
1188 String topic = profile + "/control/devices/cmd";
1189 String gsonMessage = gson.toJson(message);
1190 sendDeviceMessage(topic, gsonMessage);
1193 private void sendDeviceMessage(String topic, String gsonMessage) {
1195 mqttConnection.connectionPublish(topic, gsonMessage);
1197 } catch (MqttException e) {
1198 String message = e.getLocalizedMessage();
1200 logger.debug("sending command failed, trying to restart communication");
1201 restartCommunication();
1202 // retry sending after restart
1204 if (communicationActive()) {
1205 mqttConnection.connectionPublish(topic, gsonMessage);
1207 logger.debug("failed to restart communication");
1209 } catch (MqttException e1) {
1210 message = e1.getLocalizedMessage();
1212 logger.debug("error resending device command");
1214 if (!communicationActive()) {
1215 message = (message != null) ? message : "@text/offline.communication-error";
1216 connectionLost(message);
1217 // Keep on trying to restart, but don't send message anymore
1218 scheduleRestartCommunication();
1224 public void processMessage(String topic, byte[] payload) {
1225 String message = new String(payload);
1226 if ((profile + "/system/evt").equals(topic)) {
1228 } else if ((profile + "/system/rsp").equals(topic)) {
1229 logger.debug("received topic {}, payload {}", topic, message);
1230 systeminfoPublishRsp(message);
1231 } else if ((profile + "/notification/evt").equals(topic)) {
1232 logger.debug("received topic {}, payload {}", topic, message);
1233 notificationEvt(message);
1234 } else if ((profile + "/control/devices/evt").equals(topic)) {
1235 logger.trace("received topic {}, payload {}", topic, message);
1236 devicesEvt(message);
1237 } else if ((profile + "/control/devices/rsp").equals(topic)) {
1238 logger.debug("received topic {}, payload {}", topic, message);
1239 devicesListRsp(message);
1240 } else if ((profile + "/authentication/rsp").equals(topic)) {
1241 logger.debug("received topic {}, payload {}", topic, message);
1242 servicesListRsp(message);
1243 } else if ((profile + "/control/devices.error").equals(topic)) {
1244 logger.warn("received error {}", message);
1246 logger.trace("not acted on received message topic {}, payload {}", topic, message);
1251 * @return system info retrieved from Connected Controller
1253 public NhcSystemInfo2 getSystemInfo() {
1254 NhcSystemInfo2 systemInfo = nhcSystemInfo;
1255 if (systemInfo == null) {
1256 systemInfo = new NhcSystemInfo2();
1262 * @return time info retrieved from Connected Controller
1264 public NhcTimeInfo2 getTimeInfo() {
1265 NhcTimeInfo2 timeInfo = nhcTimeInfo;
1266 if (timeInfo == null) {
1267 timeInfo = new NhcTimeInfo2();
1273 * @return comma separated list of services retrieved from Connected Controller
1275 public String getServices() {
1276 return services.stream().map(NhcService2::name).collect(Collectors.joining(", "));
1280 public void connectionStateChanged(MqttConnectionState state, @Nullable Throwable error) {
1281 // do in separate thread as this method needs to return early
1282 scheduler.submit(() -> {
1283 if (error != null) {
1284 logger.debug("Connection state: {}, error", state, error);
1285 String localizedMessage = error.getLocalizedMessage();
1286 String message = (localizedMessage != null) ? localizedMessage : "@text/offline.communication-error";
1287 connectionLost(message);
1288 scheduleRestartCommunication();
1289 } else if ((state == MqttConnectionState.CONNECTED) && !initStarted) {
1292 logger.trace("Connection state: {}", state);