2 * Copyright (c) 2010-2020 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 java.io.BufferedReader;
16 import java.io.IOException;
17 import java.io.InputStreamReader;
18 import java.io.PrintWriter;
19 import java.net.InetAddress;
20 import java.net.Socket;
21 import java.util.List;
23 import java.util.concurrent.ConcurrentHashMap;
24 import java.util.concurrent.ScheduledExecutorService;
25 import java.util.function.Consumer;
27 import org.eclipse.jdt.annotation.NonNullByDefault;
28 import org.eclipse.jdt.annotation.Nullable;
29 import org.openhab.binding.nikohomecontrol.internal.protocol.NhcAction;
30 import org.openhab.binding.nikohomecontrol.internal.protocol.NhcControllerEvent;
31 import org.openhab.binding.nikohomecontrol.internal.protocol.NhcThermostat;
32 import org.openhab.binding.nikohomecontrol.internal.protocol.NikoHomeControlCommunication;
33 import org.openhab.binding.nikohomecontrol.internal.protocol.NikoHomeControlConstants.ActionType;
34 import org.slf4j.Logger;
35 import org.slf4j.LoggerFactory;
37 import com.google.gson.Gson;
38 import com.google.gson.GsonBuilder;
39 import com.google.gson.JsonParseException;
42 * The {@link NikoHomeControlCommunication1} class is able to do the following tasks with Niko Home Control I
45 * <li>Start and stop TCP socket connection with Niko Home Control IP-interface.
46 * <li>Read all setup and status information from the Niko Home Control Controller.
47 * <li>Execute Niko Home Control commands.
48 * <li>Listen to events from Niko Home Control.
51 * @author Mark Herwege - Initial Contribution
54 public class NikoHomeControlCommunication1 extends NikoHomeControlCommunication {
56 private Logger logger = LoggerFactory.getLogger(NikoHomeControlCommunication1.class);
58 private final NhcSystemInfo1 systemInfo = new NhcSystemInfo1();
59 private final Map<String, NhcLocation1> locations = new ConcurrentHashMap<>();
61 private @Nullable Socket nhcSocket;
62 private @Nullable PrintWriter nhcOut;
63 private @Nullable BufferedReader nhcIn;
65 private volatile boolean listenerStopped;
66 private volatile boolean nhcEventsRunning;
68 private ScheduledExecutorService scheduler;
70 // We keep only 2 gson adapters used to serialize and deserialize all messages sent and received
71 protected final Gson gsonOut = new Gson();
72 protected Gson gsonIn;
75 * Constructor for Niko Home Control I communication object, manages communication with
76 * Niko Home Control IP-interface.
79 public NikoHomeControlCommunication1(NhcControllerEvent handler, ScheduledExecutorService scheduler) {
81 this.scheduler = scheduler;
83 // When we set up this object, we want to get the proper gson adapter set up once
84 GsonBuilder gsonBuilder = new GsonBuilder();
85 gsonBuilder.registerTypeAdapter(NhcMessageBase1.class, new NikoHomeControlMessageDeserializer1());
86 gsonIn = gsonBuilder.create();
90 public synchronized void startCommunication() {
92 for (int i = 1; nhcEventsRunning && (i <= 5); i++) {
93 // the events listener thread did not finish yet, so wait max 5000ms before restarting
96 if (nhcEventsRunning) {
97 logger.debug("Niko Home Control: starting but previous connection still active after 5000ms");
98 throw new IOException();
101 InetAddress addr = handler.getAddr();
102 int port = handler.getPort();
104 Socket socket = new Socket(addr, port);
106 nhcOut = new PrintWriter(socket.getOutputStream(), true);
107 nhcIn = new BufferedReader(new InputStreamReader(socket.getInputStream()));
108 logger.debug("Niko Home Control: connected via local port {}", socket.getLocalPort());
110 // initialize all info in local fields
113 // Start Niko Home Control event listener. This listener will act on all messages coming from
115 (new Thread(this::runNhcEvents)).start();
117 } catch (IOException | InterruptedException e) {
118 logger.warn("Niko Home Control: error initializing communication");
120 handler.controllerOffline();
125 * Cleanup socket when the communication with Niko Home Control IP-interface is closed.
129 public synchronized void stopCommunication() {
130 listenerStopped = true;
132 Socket socket = nhcSocket;
133 if (socket != null) {
136 } catch (IOException ignore) {
137 // ignore IO Error when trying to close the socket if the intention is to close it anyway
142 logger.debug("Niko Home Control: communication stopped");
146 public boolean communicationActive() {
147 return (nhcSocket != null);
151 * Method that handles inbound communication from Niko Home Control, to be called on a separate thread.
153 * The thread listens to the TCP socket opened at instantiation of the {@link NikoHomeControlCommunication} class
154 * and interprets all inbound json messages. It triggers state updates for active channels linked to the Niko Home
155 * Control actions. It is started after initialization of the communication.
158 private void runNhcEvents() {
161 logger.debug("Niko Home Control: listening for events");
162 listenerStopped = false;
163 nhcEventsRunning = true;
166 while (!listenerStopped & (nhcIn != null) & ((nhcMessage = nhcIn.readLine()) != null)) {
167 readMessage(nhcMessage);
169 } catch (IOException e) {
170 if (!listenerStopped) {
171 nhcEventsRunning = false;
172 // this is a socket error, not a communication stop triggered from outside this runnable
173 logger.warn("Niko Home Control: IO error in listener");
174 // the IO has stopped working, so we need to close cleanly and try to restart
175 restartCommunication();
179 nhcEventsRunning = false;
182 nhcEventsRunning = false;
183 // this is a stop from outside the runnable, so just log it and stop
184 logger.debug("Niko Home Control: event listener thread stopped");
188 * After setting up the communication with the Niko Home Control IP-interface, send all initialization messages.
190 * Only at first initialization, also set the return values. Otherwise use the runnable to get updated values.
191 * While communication is set up for thermostats, tariff data and alarms, only info from locations and actions
192 * is used beyond this point in openHAB. All other elements are for future extensions.
194 * @throws IOException
196 private void initialize() throws IOException {
197 sendAndReadMessage("systeminfo");
198 sendAndReadMessage("startevents");
199 sendAndReadMessage("listlocations");
200 sendAndReadMessage("listactions");
201 sendAndReadMessage("listthermostat");
202 sendAndReadMessage("listthermostatHVAC");
203 sendAndReadMessage("readtariffdata");
204 sendAndReadMessage("getalarms");
207 @SuppressWarnings("null")
208 private void sendAndReadMessage(String command) throws IOException {
209 sendMessage(new NhcMessageCmd1(command));
210 readMessage(nhcIn.readLine());
214 * Called by other methods to send json cmd to Niko Home Control.
218 @SuppressWarnings("null")
219 private synchronized void sendMessage(Object nhcMessage) {
220 String json = gsonOut.toJson(nhcMessage);
221 logger.debug("Niko Home Control: send json {}", json);
222 nhcOut.println(json);
223 if (nhcOut.checkError()) {
224 logger.warn("Niko Home Control: error sending message, trying to restart communication");
225 restartCommunication();
226 // retry sending after restart
227 logger.debug("Niko Home Control: resend json {}", json);
228 nhcOut.println(json);
229 if (nhcOut.checkError()) {
230 logger.warn("Niko Home Control: error resending message");
231 handler.controllerOffline();
237 * Method that interprets all feedback from Niko Home Control and calls appropriate handling methods.
239 * @param nhcMessage message read from Niko Home Control.
241 private void readMessage(@Nullable String nhcMessage) {
242 logger.debug("Niko Home Control: received json {}", nhcMessage);
245 NhcMessageBase1 nhcMessageGson = gsonIn.fromJson(nhcMessage, NhcMessageBase1.class);
247 String cmd = nhcMessageGson.getCmd();
248 String event = nhcMessageGson.getEvent();
250 if ("systeminfo".equals(cmd)) {
251 cmdSystemInfo(((NhcMessageMap1) nhcMessageGson).getData());
252 } else if ("startevents".equals(cmd)) {
253 cmdStartEvents(((NhcMessageMap1) nhcMessageGson).getData());
254 } else if ("listlocations".equals(cmd)) {
255 cmdListLocations(((NhcMessageListMap1) nhcMessageGson).getData());
256 } else if ("listactions".equals(cmd)) {
257 cmdListActions(((NhcMessageListMap1) nhcMessageGson).getData());
258 } else if (("listthermostat").equals(cmd)) {
259 cmdListThermostat(((NhcMessageListMap1) nhcMessageGson).getData());
260 } else if ("executeactions".equals(cmd)) {
261 cmdExecuteActions(((NhcMessageMap1) nhcMessageGson).getData());
262 } else if ("executethermostat".equals(cmd)) {
263 cmdExecuteThermostat(((NhcMessageMap1) nhcMessageGson).getData());
264 } else if ("listactions".equals(event)) {
265 eventListActions(((NhcMessageListMap1) nhcMessageGson).getData());
266 } else if ("listthermostat".equals(event)) {
267 eventListThermostat(((NhcMessageListMap1) nhcMessageGson).getData());
268 } else if ("getalarms".equals(event)) {
269 eventGetAlarms(((NhcMessageMap1) nhcMessageGson).getData());
271 logger.debug("Niko Home Control: not acted on json {}", nhcMessage);
273 } catch (JsonParseException e) {
274 logger.debug("Niko Home Control: not acted on unsupported json {}", nhcMessage);
278 private void setIfPresent(Map<String, String> data, String key, Consumer<String> consumer) {
279 String val = data.get(key);
281 consumer.accept(val);
285 private synchronized void cmdSystemInfo(Map<String, String> data) {
286 logger.debug("Niko Home Control: systeminfo");
288 setIfPresent(data, "swversion", systemInfo::setSwVersion);
289 setIfPresent(data, "api", systemInfo::setApi);
290 setIfPresent(data, "time", systemInfo::setTime);
291 setIfPresent(data, "language", systemInfo::setLanguage);
292 setIfPresent(data, "currency", systemInfo::setCurrency);
293 setIfPresent(data, "units", systemInfo::setUnits);
294 setIfPresent(data, "DST", systemInfo::setDst);
295 setIfPresent(data, "TZ", systemInfo::setTz);
296 setIfPresent(data, "lastenergyerase", systemInfo::setLastEnergyErase);
297 setIfPresent(data, "lastconfig", systemInfo::setLastConfig);
301 * Return the object with system info as read from the Niko Home Control controller.
303 * @return the systemInfo
305 public synchronized NhcSystemInfo1 getSystemInfo() {
309 private void cmdStartEvents(Map<String, String> data) {
310 String errorCodeString = data.get("error");
311 if (errorCodeString != null) {
312 int errorCode = Integer.parseInt(errorCodeString);
313 if (errorCode == 0) {
314 logger.debug("Niko Home Control: start events success");
316 logger.warn("Niko Home Control: error code {} returned on start events", errorCode);
319 logger.warn("Niko Home Control: could not determine error code returned on start events");
323 private void cmdListLocations(List<Map<String, String>> data) {
324 logger.debug("Niko Home Control: list locations");
328 for (Map<String, String> location : data) {
329 String id = location.get("id");
330 String name = location.get("name");
331 if (id == null || name == null) {
332 logger.debug("id or name null, ignoring entry");
335 NhcLocation1 nhcLocation1 = new NhcLocation1(name);
336 locations.put(id, nhcLocation1);
340 private void cmdListActions(List<Map<String, String>> data) {
341 logger.debug("Niko Home Control: list actions");
343 for (Map<String, String> action : data) {
344 String id = action.get("id");
346 logger.debug("id not found in action {}", action);
349 String value1 = action.get("value1");
350 int state = ((value1 == null) || value1.isEmpty() ? 0 : Integer.parseInt(value1));
351 String value2 = action.get("value2");
352 int closeTime = ((value2 == null) || value2.isEmpty() ? 0 : Integer.parseInt(value2));
353 String value3 = action.get("value3");
354 int openTime = ((value3 == null) || value3.isEmpty() ? 0 : Integer.parseInt(value3));
356 if (!actions.containsKey(id)) {
357 // Initial instantiation of NhcAction class for action object
358 String name = action.get("name");
360 logger.debug("name not found in action {}", action);
363 String type = action.get("type");
364 ActionType actionType = ActionType.GENERIC;
367 actionType = ActionType.TRIGGER;
370 actionType = ActionType.RELAY;
373 actionType = ActionType.DIMMER;
377 actionType = ActionType.ROLLERSHUTTER;
380 logger.debug("Niko Home Control: unknown action type {} for action {}", type, id);
383 String locationId = action.get("location");
384 String location = "";
385 if (locationId != null && !locationId.isEmpty()) {
386 location = locations.getOrDefault(locationId, new NhcLocation1("")).getName();
388 NhcAction nhcAction = new NhcAction1(id, name, actionType, location, this, scheduler);
389 if (actionType == ActionType.ROLLERSHUTTER) {
390 nhcAction.setShutterTimes(openTime, closeTime);
392 nhcAction.setState(state);
393 actions.put(id, nhcAction);
395 // Action object already exists, so only update state.
396 // If we would re-instantiate action, we would lose pointer back from action to thing handler that was
397 // set in thing handler initialize().
398 actions.get(id).setState(state);
403 private int parseIntOrThrow(@Nullable String str) throws IllegalArgumentException {
405 throw new IllegalArgumentException("String is null");
407 return Integer.parseInt(str);
408 } catch (NumberFormatException e) {
409 throw new IllegalArgumentException(e);
413 private void cmdListThermostat(List<Map<String, String>> data) {
414 logger.debug("Niko Home Control: list thermostats");
416 for (Map<String, String> thermostat : data) {
418 String id = thermostat.get("id");
420 logger.debug("skipping thermostat {}, id not found", thermostat);
423 int measured = parseIntOrThrow(thermostat.get("measured"));
424 int setpoint = parseIntOrThrow(thermostat.get("setpoint"));
425 int mode = parseIntOrThrow(thermostat.get("mode"));
426 int overrule = parseIntOrThrow(thermostat.get("overrule"));
427 // overruletime received in "HH:MM" format
428 String[] overruletimeStrings = thermostat.getOrDefault("overruletime", "").split(":");
429 int overruletime = 0;
430 if (overruletimeStrings.length == 2) {
431 overruletime = Integer.parseInt(overruletimeStrings[0]) * 60
432 + Integer.parseInt(overruletimeStrings[1]);
434 int ecosave = parseIntOrThrow(thermostat.get("ecosave"));
436 // For parity with NHC II, assume heating/cooling if thermostat is on and setpoint different from
438 int demand = (mode != 3) ? (setpoint > measured ? 1 : (setpoint < measured ? -1 : 0)) : 0;
440 NhcThermostat t = thermostats.computeIfAbsent(id, i -> {
441 // Initial instantiation of NhcThermostat class for thermostat object
442 String name = thermostat.get("name");
443 String locationId = thermostat.get("location");
444 String location = "";
445 if (!locationId.isEmpty()) {
446 location = locations.get(locationId).getName();
449 return new NhcThermostat1(i, name, location, this);
451 throw new IllegalArgumentException();
454 t.updateState(measured, setpoint, mode, overrule, overruletime, ecosave, demand);
456 } catch (IllegalArgumentException e) {
462 private void cmdExecuteActions(Map<String, String> data) {
464 int errorCode = parseIntOrThrow(data.get("error"));
465 if (errorCode == 0) {
466 logger.debug("Niko Home Control: execute action success");
468 logger.warn("Niko Home Control: error code {} returned on command execution", errorCode);
470 } catch (IllegalArgumentException e) {
471 logger.warn("Niko Home Control: no error code returned on command execution");
475 private void cmdExecuteThermostat(Map<String, String> data) {
477 int errorCode = parseIntOrThrow(data.get("error"));
478 if (errorCode == 0) {
479 logger.debug("Niko Home Control: execute thermostats success");
481 logger.warn("Niko Home Control: error code {} returned on command execution", errorCode);
483 } catch (IllegalArgumentException e) {
484 logger.warn("Niko Home Control: no error code returned on command execution");
488 private void eventListActions(List<Map<String, String>> data) {
489 for (Map<String, String> action : data) {
490 String id = action.get("id");
491 if (id == null || !actions.containsKey(id)) {
492 logger.warn("Niko Home Control: action in controller not known {}", id);
495 String stateString = action.get("value1");
496 if (stateString != null) {
497 int state = Integer.parseInt(stateString);
498 logger.debug("Niko Home Control: event execute action {} with state {}", id, state);
499 NhcAction action1 = actions.get(id);
500 if (action1 != null) {
501 action1.setState(state);
507 private void eventListThermostat(List<Map<String, String>> data) {
508 for (Map<String, String> thermostat : data) {
510 String id = thermostat.get("id");
511 if (!thermostats.containsKey(id)) {
512 logger.warn("Niko Home Control: thermostat in controller not known {}", id);
516 int measured = parseIntOrThrow(thermostat.get("measured"));
517 int setpoint = parseIntOrThrow(thermostat.get("setpoint"));
518 int mode = parseIntOrThrow(thermostat.get("mode"));
519 int overrule = parseIntOrThrow(thermostat.get("overrule"));
520 // overruletime received in "HH:MM" format
521 String[] overruletimeStrings = thermostat.getOrDefault("overruletime", "").split(":");
522 int overruletime = 0;
523 if (overruletimeStrings.length == 2) {
524 overruletime = Integer.parseInt(overruletimeStrings[0]) * 60
525 + Integer.parseInt(overruletimeStrings[1]);
527 int ecosave = parseIntOrThrow(thermostat.get("ecosave"));
529 int demand = (mode != 3) ? (setpoint > measured ? 1 : (setpoint < measured ? -1 : 0)) : 0;
532 "Niko Home Control: event execute thermostat {} with measured {}, setpoint {}, mode {}, overrule {}, overruletime {}, ecosave {}, demand {}",
533 id, measured, setpoint, mode, overrule, overruletime, ecosave, demand);
534 NhcThermostat t = thermostats.get(id);
536 t.updateState(measured, setpoint, mode, overrule, overruletime, ecosave, demand);
538 } catch (IllegalArgumentException e) {
544 private void eventGetAlarms(Map<String, String> data) {
545 String alarmText = data.get("text");
546 if (alarmText == null) {
547 logger.debug("message does not contain alarmtext: {}", data);
550 switch (data.getOrDefault("type", "")) {
552 logger.debug("Niko Home Control: alarm - {}", alarmText);
553 handler.alarmEvent(alarmText);
556 logger.debug("Niko Home Control: notice - {}", alarmText);
557 handler.noticeEvent(alarmText);
560 logger.debug("Niko Home Control: unexpected message type {}", data.get("type"));
565 public void executeAction(String actionId, String value) {
566 NhcMessageCmd1 nhcCmd = new NhcMessageCmd1("executeactions", Integer.parseInt(actionId),
567 Integer.parseInt(value));
572 public void executeThermostat(String thermostatId, String mode) {
573 NhcMessageCmd1 nhcCmd = new NhcMessageCmd1("executethermostat", Integer.parseInt(thermostatId))
574 .withMode(Integer.parseInt(mode));
579 public void executeThermostat(String thermostatId, int overruleTemp, int overruleTime) {
580 String overruletimeString = String.format("%1$02d:%2$02d", overruleTime / 60, overruleTime % 60);
581 NhcMessageCmd1 nhcCmd = new NhcMessageCmd1("executethermostat", Integer.parseInt(thermostatId))
582 .withOverrule(overruleTemp).withOverruletime(overruletimeString);