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.nhc1;
15 import static org.openhab.binding.nikohomecontrol.internal.NikoHomeControlBindingConstants.THREAD_NAME_PREFIX;
17 import java.io.BufferedReader;
18 import java.io.IOException;
19 import java.io.InputStreamReader;
20 import java.io.PrintWriter;
21 import java.net.InetAddress;
22 import java.net.Socket;
23 import java.util.List;
25 import java.util.Optional;
26 import java.util.concurrent.ConcurrentHashMap;
27 import java.util.concurrent.ScheduledExecutorService;
28 import java.util.function.Consumer;
30 import org.eclipse.jdt.annotation.NonNullByDefault;
31 import org.eclipse.jdt.annotation.Nullable;
32 import org.openhab.binding.nikohomecontrol.internal.protocol.NhcAction;
33 import org.openhab.binding.nikohomecontrol.internal.protocol.NhcControllerEvent;
34 import org.openhab.binding.nikohomecontrol.internal.protocol.NhcThermostat;
35 import org.openhab.binding.nikohomecontrol.internal.protocol.NikoHomeControlCommunication;
36 import org.openhab.binding.nikohomecontrol.internal.protocol.NikoHomeControlConstants.ActionType;
37 import org.slf4j.Logger;
38 import org.slf4j.LoggerFactory;
40 import com.google.gson.Gson;
41 import com.google.gson.GsonBuilder;
42 import com.google.gson.JsonParseException;
45 * The {@link NikoHomeControlCommunication1} class is able to do the following tasks with Niko Home Control I
48 * <li>Start and stop TCP socket connection with Niko Home Control IP-interface.
49 * <li>Read all setup and status information from the Niko Home Control Controller.
50 * <li>Execute Niko Home Control commands.
51 * <li>Listen to events from Niko Home Control.
54 * @author Mark Herwege - Initial Contribution
57 public class NikoHomeControlCommunication1 extends NikoHomeControlCommunication {
59 private Logger logger = LoggerFactory.getLogger(NikoHomeControlCommunication1.class);
61 private String eventThreadName = THREAD_NAME_PREFIX;
63 private final NhcSystemInfo1 systemInfo = new NhcSystemInfo1();
64 private final Map<String, NhcLocation1> locations = new ConcurrentHashMap<>();
66 private @Nullable Socket nhcSocket;
67 private @Nullable PrintWriter nhcOut;
68 private @Nullable BufferedReader nhcIn;
70 private volatile boolean listenerStopped;
71 private volatile boolean nhcEventsRunning;
73 // We keep only 2 gson adapters used to serialize and deserialize all messages sent and received
74 protected final Gson gsonOut = new Gson();
75 protected Gson gsonIn;
78 * Constructor for Niko Home Control I communication object, manages communication with
79 * Niko Home Control IP-interface.
82 public NikoHomeControlCommunication1(NhcControllerEvent handler, ScheduledExecutorService scheduler,
83 String eventThreadName) {
84 super(handler, scheduler);
85 this.eventThreadName = eventThreadName;
87 // When we set up this object, we want to get the proper gson adapter set up once
88 GsonBuilder gsonBuilder = new GsonBuilder();
89 gsonBuilder.registerTypeAdapter(NhcMessageBase1.class, new NikoHomeControlMessageDeserializer1());
90 gsonIn = gsonBuilder.create();
94 public synchronized void startCommunication() {
96 for (int i = 1; nhcEventsRunning && (i <= 5); i++) {
97 // the events listener thread did not finish yet, so wait max 5000ms before restarting
100 if (nhcEventsRunning) {
101 logger.debug("starting but previous connection still active after 5000ms");
102 throw new IOException();
105 InetAddress addr = handler.getAddr();
106 int port = handler.getPort();
108 Socket socket = new Socket(addr, port);
110 nhcOut = new PrintWriter(socket.getOutputStream(), true);
111 nhcIn = new BufferedReader(new InputStreamReader(socket.getInputStream()));
112 logger.debug("connected via local port {}", socket.getLocalPort());
114 // initialize all info in local fields
117 // Start Niko Home Control event listener. This listener will act on all messages coming from
119 (new Thread(this::runNhcEvents, eventThreadName)).start();
121 handler.controllerOnline();
122 } catch (InterruptedException e) {
123 handler.controllerOffline("@text/offline.communication-error");
124 } catch (IOException e) {
125 handler.controllerOffline("@text/offline.communication-error");
126 scheduleRestartCommunication();
131 * Cleanup socket when the communication with Niko Home Control IP-interface is closed.
135 public synchronized void resetCommunication() {
136 listenerStopped = true;
138 Socket socket = nhcSocket;
139 if (socket != null) {
142 } catch (IOException ignore) {
143 // ignore IO Error when trying to close the socket if the intention is to close it anyway
148 logger.debug("communication stopped");
152 public boolean communicationActive() {
153 return (nhcSocket != null);
157 * Method that handles inbound communication from Niko Home Control, to be called on a separate thread.
159 * The thread listens to the TCP socket opened at instantiation of the {@link NikoHomeControlCommunication} class
160 * and interprets all inbound json messages. It triggers state updates for active channels linked to the Niko Home
161 * Control actions. It is started after initialization of the communication.
164 private void runNhcEvents() {
167 logger.debug("listening for events");
168 listenerStopped = false;
169 nhcEventsRunning = true;
172 BufferedReader in = nhcIn;
174 while (!listenerStopped && ((nhcMessage = in.readLine()) != null)) {
175 readMessage(nhcMessage);
178 } catch (IOException e) {
179 if (!listenerStopped) {
180 nhcEventsRunning = false;
181 // this is a socket error, not a communication stop triggered from outside this runnable
182 logger.debug("IO error in listener");
183 // the IO has stopped working, so we need to close cleanly and try to restart
184 scheduleRestartCommunication();
188 nhcEventsRunning = false;
191 nhcEventsRunning = false;
192 // this is a stop from outside the runnable, so just log it and stop
193 logger.debug("event listener thread stopped");
197 * After setting up the communication with the Niko Home Control IP-interface, send all initialization messages.
199 * Only at first initialization, also set the return values. Otherwise use the runnable to get updated values.
200 * While communication is set up for thermostats, tariff data and alarms, only info from locations and actions
201 * is used beyond this point in openHAB. All other elements are for future extensions.
203 * @throws IOException
205 private void initialize() throws IOException {
206 sendAndReadMessage("systeminfo");
207 sendAndReadMessage("startevents");
208 sendAndReadMessage("listlocations");
209 sendAndReadMessage("listactions");
210 sendAndReadMessage("listthermostat");
211 sendAndReadMessage("listthermostatHVAC");
212 sendAndReadMessage("readtariffdata");
213 sendAndReadMessage("getalarms");
216 private void sendAndReadMessage(String command) throws IOException {
217 BufferedReader in = nhcIn;
219 sendMessage(new NhcMessageCmd1(command));
220 readMessage(in.readLine());
225 * Called by other methods to send json cmd to Niko Home Control.
229 private synchronized void sendMessage(Object nhcMessage) {
230 String json = gsonOut.toJson(nhcMessage);
231 PrintWriter out = nhcOut;
233 logger.debug("send json {}", json);
235 if (out.checkError()) {
236 logger.debug("error sending message, trying to restart communication");
237 restartCommunication();
238 // retry sending after restart
239 logger.debug("resend json {}", json);
241 if (out.checkError()) {
242 handler.controllerOffline("@text/offline.communication-error");
243 // Keep on trying to restart, but don't send message anymore
244 scheduleRestartCommunication();
251 * Method that interprets all feedback from Niko Home Control and calls appropriate handling methods.
253 * @param nhcMessage message read from Niko Home Control.
255 private void readMessage(@Nullable String nhcMessage) {
256 logger.debug("received json {}", nhcMessage);
259 NhcMessageBase1 nhcMessageGson = gsonIn.fromJson(nhcMessage, NhcMessageBase1.class);
261 if (nhcMessageGson == null) {
264 String cmd = nhcMessageGson.getCmd();
265 String event = nhcMessageGson.getEvent();
267 if ("systeminfo".equals(cmd)) {
268 cmdSystemInfo(((NhcMessageMap1) nhcMessageGson).getData());
269 } else if ("startevents".equals(cmd)) {
270 cmdStartEvents(((NhcMessageMap1) nhcMessageGson).getData());
271 } else if ("listlocations".equals(cmd)) {
272 cmdListLocations(((NhcMessageListMap1) nhcMessageGson).getData());
273 } else if ("listactions".equals(cmd)) {
274 cmdListActions(((NhcMessageListMap1) nhcMessageGson).getData());
275 } else if (("listthermostat").equals(cmd)) {
276 cmdListThermostat(((NhcMessageListMap1) nhcMessageGson).getData());
277 } else if ("executeactions".equals(cmd)) {
278 cmdExecuteActions(((NhcMessageMap1) nhcMessageGson).getData());
279 } else if ("executethermostat".equals(cmd)) {
280 cmdExecuteThermostat(((NhcMessageMap1) nhcMessageGson).getData());
281 } else if ("listactions".equals(event)) {
282 eventListActions(((NhcMessageListMap1) nhcMessageGson).getData());
283 } else if ("listthermostat".equals(event)) {
284 eventListThermostat(((NhcMessageListMap1) nhcMessageGson).getData());
285 } else if ("getalarms".equals(event)) {
286 eventGetAlarms(((NhcMessageMap1) nhcMessageGson).getData());
288 logger.debug("not acted on json {}", nhcMessage);
290 } catch (JsonParseException e) {
291 logger.debug("not acted on unsupported json {}", nhcMessage);
295 private void setIfPresent(Map<String, String> data, String key, Consumer<String> consumer) {
296 String val = data.get(key);
298 consumer.accept(val);
302 private synchronized void cmdSystemInfo(Map<String, String> data) {
303 logger.debug("systeminfo");
305 setIfPresent(data, "swversion", systemInfo::setSwVersion);
306 setIfPresent(data, "api", systemInfo::setApi);
307 setIfPresent(data, "time", systemInfo::setTime);
308 setIfPresent(data, "language", systemInfo::setLanguage);
309 setIfPresent(data, "currency", systemInfo::setCurrency);
310 setIfPresent(data, "units", systemInfo::setUnits);
311 setIfPresent(data, "DST", systemInfo::setDst);
312 setIfPresent(data, "TZ", systemInfo::setTz);
313 setIfPresent(data, "lastenergyerase", systemInfo::setLastEnergyErase);
314 setIfPresent(data, "lastconfig", systemInfo::setLastConfig);
318 * Return the object with system info as read from the Niko Home Control controller.
320 * @return the systemInfo
322 public synchronized NhcSystemInfo1 getSystemInfo() {
326 private void cmdStartEvents(Map<String, String> data) {
327 String errorCodeString = data.get("error");
328 if (errorCodeString != null) {
329 int errorCode = Integer.parseInt(errorCodeString);
330 if (errorCode == 0) {
331 logger.debug("start events success");
333 logger.debug("error code {} returned on start events", errorCode);
336 logger.debug("could not determine error code returned on start events");
340 private void cmdListLocations(List<Map<String, String>> data) {
341 logger.debug("list locations");
345 for (Map<String, String> location : data) {
346 String id = location.get("id");
347 String name = location.get("name");
348 if (id == null || name == null) {
349 logger.debug("id or name null, ignoring entry");
352 NhcLocation1 nhcLocation1 = new NhcLocation1(name);
353 locations.put(id, nhcLocation1);
357 private void cmdListActions(List<Map<String, String>> data) {
358 logger.debug("list actions");
360 for (Map<String, String> action : data) {
361 String id = action.get("id");
363 logger.debug("id not found in action {}", action);
366 String value1 = action.get("value1");
367 int state = ((value1 == null) || value1.isEmpty() ? 0 : Integer.parseInt(value1));
368 String value2 = action.get("value2");
369 int closeTime = ((value2 == null) || value2.isEmpty() ? 0 : Integer.parseInt(value2));
370 String value3 = action.get("value3");
371 int openTime = ((value3 == null) || value3.isEmpty() ? 0 : Integer.parseInt(value3));
373 String name = action.get("name");
375 logger.debug("name not found in action {}", action);
378 String type = Optional.ofNullable(action.get("type")).orElse("");
379 ActionType actionType = ActionType.GENERIC;
382 actionType = ActionType.TRIGGER;
385 actionType = ActionType.RELAY;
388 actionType = ActionType.DIMMER;
392 actionType = ActionType.ROLLERSHUTTER;
395 logger.debug("unknown action type {} for action {}", type, id);
398 String locationId = action.get("location");
399 String location = "";
400 if (locationId != null && !locationId.isEmpty()) {
401 location = locations.getOrDefault(locationId, new NhcLocation1("")).getName();
403 if (!actions.containsKey(id)) {
404 // Initial instantiation of NhcAction class for action object
405 NhcAction nhcAction = new NhcAction1(id, name, actionType, location, this, scheduler);
406 if (actionType == ActionType.ROLLERSHUTTER) {
407 nhcAction.setShutterTimes(openTime, closeTime);
409 nhcAction.setState(state);
410 actions.put(id, nhcAction);
412 // Action object already exists, so only update state, name and location.
413 // If we would re-instantiate action, we would lose pointer back from action to thing handler that was
414 // set in thing handler initialize().
415 NhcAction nhcAction = actions.get(id);
416 if (nhcAction != null) {
417 nhcAction.setName(name);
418 nhcAction.setLocation(location);
419 nhcAction.setState(state);
425 private int parseIntOrThrow(@Nullable String str) throws IllegalArgumentException {
427 throw new IllegalArgumentException("String is null");
430 return Integer.parseInt(str);
431 } catch (NumberFormatException e) {
432 throw new IllegalArgumentException(e);
436 private void cmdListThermostat(List<Map<String, String>> data) {
437 logger.debug("list thermostats");
439 for (Map<String, String> thermostat : data) {
441 String id = thermostat.get("id");
443 logger.debug("skipping thermostat {}, id not found", thermostat);
446 int measured = parseIntOrThrow(thermostat.get("measured"));
447 int setpoint = parseIntOrThrow(thermostat.get("setpoint"));
448 int mode = parseIntOrThrow(thermostat.get("mode"));
449 int overrule = parseIntOrThrow(thermostat.get("overrule"));
450 // overruletime received in "HH:MM" format
451 String[] overruletimeStrings = thermostat.getOrDefault("overruletime", "").split(":");
452 int overruletime = 0;
453 if (overruletimeStrings.length == 2) {
454 overruletime = Integer.parseInt(overruletimeStrings[0]) * 60
455 + Integer.parseInt(overruletimeStrings[1]);
457 int ecosave = parseIntOrThrow(thermostat.get("ecosave"));
459 // For parity with NHC II, assume heating/cooling if thermostat is on and setpoint different from
461 int demand = (mode != 3) ? (setpoint > measured ? 1 : (setpoint < measured ? -1 : 0)) : 0;
463 String name = thermostat.get("name");
464 String locationId = thermostat.get("location");
465 NhcLocation1 nhcLocation = null;
466 if (!((locationId == null) || locationId.isEmpty())) {
467 nhcLocation = locations.get(locationId);
469 String location = (nhcLocation != null) ? nhcLocation.getName() : null;
470 NhcThermostat t = thermostats.computeIfAbsent(id, i -> {
471 // Initial instantiation of NhcThermostat class for thermostat object
473 return new NhcThermostat1(i, name, location, this);
475 throw new IllegalArgumentException();
481 t.setLocation(location);
482 t.updateState(measured, setpoint, mode, overrule, overruletime, ecosave, demand);
484 } catch (IllegalArgumentException e) {
490 private void cmdExecuteActions(Map<String, String> data) {
492 int errorCode = parseIntOrThrow(data.get("error"));
493 if (errorCode == 0) {
494 logger.debug("execute action success");
496 logger.debug("error code {} returned on command execution", errorCode);
498 } catch (IllegalArgumentException e) {
499 logger.debug("no error code returned on command execution");
503 private void cmdExecuteThermostat(Map<String, String> data) {
505 int errorCode = parseIntOrThrow(data.get("error"));
506 if (errorCode == 0) {
507 logger.debug("execute thermostats success");
509 logger.debug("error code {} returned on command execution", errorCode);
511 } catch (IllegalArgumentException e) {
512 logger.debug("no error code returned on command execution");
516 private void eventListActions(List<Map<String, String>> data) {
517 for (Map<String, String> action : data) {
518 String id = action.get("id");
519 if (id == null || !actions.containsKey(id)) {
520 logger.warn("action in controller not known {}", id);
523 String stateString = action.get("value1");
524 if (stateString != null) {
525 int state = Integer.parseInt(stateString);
526 logger.debug("event execute action {} with state {}", id, state);
527 NhcAction action1 = actions.get(id);
528 if (action1 != null) {
529 action1.setState(state);
535 private void eventListThermostat(List<Map<String, String>> data) {
536 for (Map<String, String> thermostat : data) {
538 String id = thermostat.get("id");
539 if (!thermostats.containsKey(id)) {
540 logger.warn("thermostat in controller not known {}", id);
544 int measured = parseIntOrThrow(thermostat.get("measured"));
545 int setpoint = parseIntOrThrow(thermostat.get("setpoint"));
546 int mode = parseIntOrThrow(thermostat.get("mode"));
547 int overrule = parseIntOrThrow(thermostat.get("overrule"));
548 // overruletime received in "HH:MM" format
549 String[] overruletimeStrings = thermostat.getOrDefault("overruletime", "").split(":");
550 int overruletime = 0;
551 if (overruletimeStrings.length == 2) {
552 overruletime = Integer.parseInt(overruletimeStrings[0]) * 60
553 + Integer.parseInt(overruletimeStrings[1]);
555 int ecosave = parseIntOrThrow(thermostat.get("ecosave"));
557 int demand = (mode != 3) ? (setpoint > measured ? 1 : (setpoint < measured ? -1 : 0)) : 0;
560 "Niko Home Control: event execute thermostat {} with measured {}, setpoint {}, mode {}, overrule {}, overruletime {}, ecosave {}, demand {}",
561 id, measured, setpoint, mode, overrule, overruletime, ecosave, demand);
562 NhcThermostat t = thermostats.get(id);
564 t.updateState(measured, setpoint, mode, overrule, overruletime, ecosave, demand);
566 } catch (IllegalArgumentException e) {
572 private void eventGetAlarms(Map<String, String> data) {
573 String alarmText = data.get("text");
574 if (alarmText == null) {
575 logger.debug("message does not contain alarmtext: {}", data);
578 switch (data.getOrDefault("type", "")) {
580 logger.debug("alarm - {}", alarmText);
581 handler.alarmEvent(alarmText);
584 logger.debug("notice - {}", alarmText);
585 handler.noticeEvent(alarmText);
588 logger.debug("unexpected message type {}", data.get("type"));
593 public void executeAction(String actionId, String value) {
594 NhcMessageCmd1 nhcCmd = new NhcMessageCmd1("executeactions", Integer.parseInt(actionId),
595 Integer.parseInt(value));
600 public void executeThermostat(String thermostatId, String mode) {
601 NhcMessageCmd1 nhcCmd = new NhcMessageCmd1("executethermostat", Integer.parseInt(thermostatId))
602 .withMode(Integer.parseInt(mode));
607 public void executeThermostat(String thermostatId, int overruleTemp, int overruleTime) {
608 String overruletimeString = String.format("%1$02d:%2$02d", overruleTime / 60, overruleTime % 60);
609 NhcMessageCmd1 nhcCmd = new NhcMessageCmd1("executethermostat", Integer.parseInt(thermostatId))
610 .withOverrule(overruleTemp).withOverruletime(overruletimeString);