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;
26 import org.eclipse.jdt.annotation.NonNullByDefault;
27 import org.eclipse.jdt.annotation.Nullable;
28 import org.openhab.binding.nikohomecontrol.internal.protocol.NhcAction;
29 import org.openhab.binding.nikohomecontrol.internal.protocol.NhcControllerEvent;
30 import org.openhab.binding.nikohomecontrol.internal.protocol.NhcThermostat;
31 import org.openhab.binding.nikohomecontrol.internal.protocol.NikoHomeControlCommunication;
32 import org.openhab.binding.nikohomecontrol.internal.protocol.NikoHomeControlConstants.ActionType;
33 import org.slf4j.Logger;
34 import org.slf4j.LoggerFactory;
36 import com.google.gson.Gson;
37 import com.google.gson.GsonBuilder;
38 import com.google.gson.JsonParseException;
41 * The {@link NikoHomeControlCommunication1} class is able to do the following tasks with Niko Home Control I
44 * <li>Start and stop TCP socket connection with Niko Home Control IP-interface.
45 * <li>Read all setup and status information from the Niko Home Control Controller.
46 * <li>Execute Niko Home Control commands.
47 * <li>Listen to events from Niko Home Control.
50 * @author Mark Herwege - Initial Contribution
53 public class NikoHomeControlCommunication1 extends NikoHomeControlCommunication {
55 private Logger logger = LoggerFactory.getLogger(NikoHomeControlCommunication1.class);
57 private final NhcSystemInfo1 systemInfo = new NhcSystemInfo1();
58 private final Map<String, NhcLocation1> locations = new ConcurrentHashMap<>();
60 private @Nullable Socket nhcSocket;
61 private @Nullable PrintWriter nhcOut;
62 private @Nullable BufferedReader nhcIn;
64 private volatile boolean listenerStopped;
65 private volatile boolean nhcEventsRunning;
67 private ScheduledExecutorService scheduler;
69 // We keep only 2 gson adapters used to serialize and deserialize all messages sent and received
70 protected final Gson gsonOut = new Gson();
71 protected Gson gsonIn;
74 * Constructor for Niko Home Control I communication object, manages communication with
75 * Niko Home Control IP-interface.
78 public NikoHomeControlCommunication1(NhcControllerEvent handler, ScheduledExecutorService scheduler) {
80 this.scheduler = scheduler;
82 // When we set up this object, we want to get the proper gson adapter set up once
83 GsonBuilder gsonBuilder = new GsonBuilder();
84 gsonBuilder.registerTypeAdapter(NhcMessageBase1.class, new NikoHomeControlMessageDeserializer1());
85 gsonIn = gsonBuilder.create();
89 public synchronized void startCommunication() {
91 for (int i = 1; nhcEventsRunning && (i <= 5); i++) {
92 // the events listener thread did not finish yet, so wait max 5000ms before restarting
95 if (nhcEventsRunning) {
96 logger.debug("Niko Home Control: starting but previous connection still active after 5000ms");
97 throw new IOException();
100 InetAddress addr = handler.getAddr();
101 int port = handler.getPort();
103 Socket socket = new Socket(addr, port);
105 nhcOut = new PrintWriter(socket.getOutputStream(), true);
106 nhcIn = new BufferedReader(new InputStreamReader(socket.getInputStream()));
107 logger.debug("Niko Home Control: connected via local port {}", socket.getLocalPort());
109 // initialize all info in local fields
112 // Start Niko Home Control event listener. This listener will act on all messages coming from
114 (new Thread(this::runNhcEvents)).start();
116 } catch (IOException | InterruptedException e) {
117 logger.warn("Niko Home Control: error initializing communication");
119 handler.controllerOffline();
124 * Cleanup socket when the communication with Niko Home Control IP-interface is closed.
128 public synchronized void stopCommunication() {
129 listenerStopped = true;
131 Socket socket = nhcSocket;
132 if (socket != null) {
135 } catch (IOException ignore) {
136 // ignore IO Error when trying to close the socket if the intention is to close it anyway
141 logger.debug("Niko Home Control: communication stopped");
145 public boolean communicationActive() {
146 return (nhcSocket != null);
150 * Method that handles inbound communication from Niko Home Control, to be called on a separate thread.
152 * The thread listens to the TCP socket opened at instantiation of the {@link NikoHomeControlCommunication} class
153 * and interprets all inbound json messages. It triggers state updates for active channels linked to the Niko Home
154 * Control actions. It is started after initialization of the communication.
157 private void runNhcEvents() {
160 logger.debug("Niko Home Control: listening for events");
161 listenerStopped = false;
162 nhcEventsRunning = true;
165 while (!listenerStopped & (nhcIn != null) & ((nhcMessage = nhcIn.readLine()) != null)) {
166 readMessage(nhcMessage);
168 } catch (IOException e) {
169 if (!listenerStopped) {
170 nhcEventsRunning = false;
171 // this is a socket error, not a communication stop triggered from outside this runnable
172 logger.warn("Niko Home Control: IO error in listener");
173 // the IO has stopped working, so we need to close cleanly and try to restart
174 restartCommunication();
178 nhcEventsRunning = false;
181 nhcEventsRunning = false;
182 // this is a stop from outside the runnable, so just log it and stop
183 logger.debug("Niko Home Control: event listener thread stopped");
187 * After setting up the communication with the Niko Home Control IP-interface, send all initialization messages.
189 * Only at first initialization, also set the return values. Otherwise use the runnable to get updated values.
190 * While communication is set up for thermostats, tariff data and alarms, only info from locations and actions
191 * is used beyond this point in openHAB. All other elements are for future extensions.
193 * @throws IOException
195 private void initialize() throws IOException {
196 sendAndReadMessage("systeminfo");
197 sendAndReadMessage("startevents");
198 sendAndReadMessage("listlocations");
199 sendAndReadMessage("listactions");
200 sendAndReadMessage("listthermostat");
201 sendAndReadMessage("listthermostatHVAC");
202 sendAndReadMessage("readtariffdata");
203 sendAndReadMessage("getalarms");
206 @SuppressWarnings("null")
207 private void sendAndReadMessage(String command) throws IOException {
208 sendMessage(new NhcMessageCmd1(command));
209 readMessage(nhcIn.readLine());
213 * Called by other methods to send json cmd to Niko Home Control.
217 @SuppressWarnings("null")
218 private synchronized void sendMessage(Object nhcMessage) {
219 String json = gsonOut.toJson(nhcMessage);
220 logger.debug("Niko Home Control: send json {}", json);
221 nhcOut.println(json);
222 if (nhcOut.checkError()) {
223 logger.warn("Niko Home Control: error sending message, trying to restart communication");
224 restartCommunication();
225 // retry sending after restart
226 logger.debug("Niko Home Control: resend json {}", json);
227 nhcOut.println(json);
228 if (nhcOut.checkError()) {
229 logger.warn("Niko Home Control: error resending message");
230 handler.controllerOffline();
236 * Method that interprets all feedback from Niko Home Control and calls appropriate handling methods.
238 * @param nhcMessage message read from Niko Home Control.
240 private void readMessage(@Nullable String nhcMessage) {
241 logger.debug("Niko Home Control: received json {}", nhcMessage);
244 NhcMessageBase1 nhcMessageGson = gsonIn.fromJson(nhcMessage, NhcMessageBase1.class);
246 String cmd = nhcMessageGson.getCmd();
247 String event = nhcMessageGson.getEvent();
249 if ("systeminfo".equals(cmd)) {
250 cmdSystemInfo(((NhcMessageMap1) nhcMessageGson).getData());
251 } else if ("startevents".equals(cmd)) {
252 cmdStartEvents(((NhcMessageMap1) nhcMessageGson).getData());
253 } else if ("listlocations".equals(cmd)) {
254 cmdListLocations(((NhcMessageListMap1) nhcMessageGson).getData());
255 } else if ("listactions".equals(cmd)) {
256 cmdListActions(((NhcMessageListMap1) nhcMessageGson).getData());
257 } else if (("listthermostat").equals(cmd)) {
258 cmdListThermostat(((NhcMessageListMap1) nhcMessageGson).getData());
259 } else if ("executeactions".equals(cmd)) {
260 cmdExecuteActions(((NhcMessageMap1) nhcMessageGson).getData());
261 } else if ("executethermostat".equals(cmd)) {
262 cmdExecuteThermostat(((NhcMessageMap1) nhcMessageGson).getData());
263 } else if ("listactions".equals(event)) {
264 eventListActions(((NhcMessageListMap1) nhcMessageGson).getData());
265 } else if ("listthermostat".equals(event)) {
266 eventListThermostat(((NhcMessageListMap1) nhcMessageGson).getData());
267 } else if ("getalarms".equals(event)) {
268 eventGetAlarms(((NhcMessageMap1) nhcMessageGson).getData());
270 logger.debug("Niko Home Control: not acted on json {}", nhcMessage);
272 } catch (JsonParseException e) {
273 logger.debug("Niko Home Control: not acted on unsupported json {}", nhcMessage);
277 private synchronized void cmdSystemInfo(Map<String, String> data) {
278 logger.debug("Niko Home Control: systeminfo");
280 if (data.containsKey("swversion")) {
281 systemInfo.setSwVersion(data.get("swversion"));
283 if (data.containsKey("api")) {
284 systemInfo.setApi(data.get("api"));
286 if (data.containsKey("time")) {
287 systemInfo.setTime(data.get("time"));
289 if (data.containsKey("language")) {
290 systemInfo.setLanguage(data.get("language"));
292 if (data.containsKey("currency")) {
293 systemInfo.setCurrency(data.get("currency"));
295 if (data.containsKey("units")) {
296 systemInfo.setUnits(data.get("units"));
298 if (data.containsKey("DST")) {
299 systemInfo.setDst(data.get("DST"));
301 if (data.containsKey("TZ")) {
302 systemInfo.setTz(data.get("TZ"));
304 if (data.containsKey("lastenergyerase")) {
305 systemInfo.setLastEnergyErase(data.get("lastenergyerase"));
307 if (data.containsKey("lastconfig")) {
308 systemInfo.setLastConfig(data.get("lastconfig"));
313 * Return the object with system info as read from the Niko Home Control controller.
315 * @return the systemInfo
317 public synchronized NhcSystemInfo1 getSystemInfo() {
321 private void cmdStartEvents(Map<String, String> data) {
322 int errorCode = Integer.parseInt(data.get("error"));
324 if (errorCode == 0) {
325 logger.debug("Niko Home Control: start events success");
327 logger.warn("Niko Home Control: error code {} returned on start events", errorCode);
331 private void cmdListLocations(List<Map<String, String>> data) {
332 logger.debug("Niko Home Control: list locations");
336 for (Map<String, String> location : data) {
337 String id = location.get("id");
338 String name = location.get("name");
339 NhcLocation1 nhcLocation1 = new NhcLocation1(name);
340 locations.put(id, nhcLocation1);
344 private void cmdListActions(List<Map<String, String>> data) {
345 logger.debug("Niko Home Control: list actions");
347 for (Map<String, String> action : data) {
349 String id = action.get("id");
350 int state = Integer.parseInt(action.get("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");
359 String type = action.get("type");
360 ActionType actionType = ActionType.GENERIC;
363 actionType = ActionType.TRIGGER;
366 actionType = ActionType.RELAY;
369 actionType = ActionType.DIMMER;
373 actionType = ActionType.ROLLERSHUTTER;
376 logger.debug("Niko Home Control: unknown action type {} for action {}", type, id);
379 String locationId = action.get("location");
380 String location = "";
381 if (!locationId.isEmpty()) {
382 location = locations.get(locationId).getName();
384 NhcAction nhcAction = new NhcAction1(id, name, actionType, location, this, scheduler);
385 if (actionType == ActionType.ROLLERSHUTTER) {
386 nhcAction.setShutterTimes(openTime, closeTime);
388 nhcAction.setState(state);
389 actions.put(id, nhcAction);
391 // Action object already exists, so only update state.
392 // If we would re-instantiate action, we would lose pointer back from action to thing handler that was
393 // set in thing handler initialize().
394 actions.get(id).setState(state);
399 private void cmdListThermostat(List<Map<String, String>> data) {
400 logger.debug("Niko Home Control: list thermostats");
402 for (Map<String, String> thermostat : data) {
404 String id = thermostat.get("id");
405 int measured = Integer.parseInt(thermostat.get("measured"));
406 int setpoint = Integer.parseInt(thermostat.get("setpoint"));
407 int mode = Integer.parseInt(thermostat.get("mode"));
408 int overrule = Integer.parseInt(thermostat.get("overrule"));
409 // overruletime received in "HH:MM" format
410 String[] overruletimeStrings = thermostat.get("overruletime").split(":");
411 int overruletime = 0;
412 if (overruletimeStrings.length == 2) {
413 overruletime = Integer.parseInt(overruletimeStrings[0]) * 60 + Integer.parseInt(overruletimeStrings[1]);
415 int ecosave = Integer.parseInt(thermostat.get("ecosave"));
417 // For parity with NHC II, assume heating/cooling if thermostat is on and setpoint different from measured
418 int demand = (mode != 3) ? (setpoint > measured ? 1 : (setpoint < measured ? -1 : 0)) : 0;
420 if (!thermostats.containsKey(id)) {
421 // Initial instantiation of NhcThermostat class for thermostat object
422 String name = thermostat.get("name");
423 String locationId = thermostat.get("location");
424 String location = "";
425 if (!locationId.isEmpty()) {
426 location = locations.get(locationId).getName();
428 NhcThermostat nhcThermostat = new NhcThermostat1(id, name, location, this);
429 nhcThermostat.updateState(measured, setpoint, mode, overrule, overruletime, ecosave, demand);
430 thermostats.put(id, nhcThermostat);
432 // Thermostat object already exists, so only update state.
433 // If we would re-instantiate thermostat, we would lose pointer back from thermostat to thing handler
434 // that was set in thing handler initialize().
435 thermostats.get(id).updateState(measured, setpoint, mode, overrule, overruletime, ecosave, demand);
440 private void cmdExecuteActions(Map<String, String> data) {
441 int errorCode = Integer.parseInt(data.get("error"));
442 if (errorCode == 0) {
443 logger.debug("Niko Home Control: execute action success");
445 logger.warn("Niko Home Control: error code {} returned on command execution", errorCode);
449 private void cmdExecuteThermostat(Map<String, String> data) {
450 int errorCode = Integer.parseInt(data.get("error"));
451 if (errorCode == 0) {
452 logger.debug("Niko Home Control: execute thermostats success");
454 logger.warn("Niko Home Control: error code {} returned on command execution", errorCode);
458 private void eventListActions(List<Map<String, String>> data) {
459 for (Map<String, String> action : data) {
460 String id = action.get("id");
461 if (!actions.containsKey(id)) {
462 logger.warn("Niko Home Control: action in controller not known {}", id);
465 int state = Integer.parseInt(action.get("value1"));
466 logger.debug("Niko Home Control: event execute action {} with state {}", id, state);
467 actions.get(id).setState(state);
471 private void eventListThermostat(List<Map<String, String>> data) {
472 for (Map<String, String> thermostat : data) {
473 String id = thermostat.get("id");
474 if (!thermostats.containsKey(id)) {
475 logger.warn("Niko Home Control: thermostat in controller not known {}", id);
479 int measured = Integer.parseInt(thermostat.get("measured"));
480 int setpoint = Integer.parseInt(thermostat.get("setpoint"));
481 int mode = Integer.parseInt(thermostat.get("mode"));
482 int overrule = Integer.parseInt(thermostat.get("overrule"));
483 // overruletime received in "HH:MM" format
484 String[] overruletimeStrings = thermostat.get("overruletime").split(":");
485 int overruletime = 0;
486 if (overruletimeStrings.length == 2) {
487 overruletime = Integer.parseInt(overruletimeStrings[0]) * 60 + Integer.parseInt(overruletimeStrings[1]);
489 int ecosave = Integer.parseInt(thermostat.get("ecosave"));
491 int demand = (mode != 3) ? (setpoint > measured ? 1 : (setpoint < measured ? -1 : 0)) : 0;
494 "Niko Home Control: event execute thermostat {} with measured {}, setpoint {}, mode {}, overrule {}, overruletime {}, ecosave {}, demand {}",
495 id, measured, setpoint, mode, overrule, overruletime, ecosave, demand);
496 thermostats.get(id).updateState(measured, setpoint, mode, overrule, overruletime, ecosave, demand);
500 private void eventGetAlarms(Map<String, String> data) {
501 int type = Integer.parseInt(data.get("type"));
502 String alarmText = data.get("text");
505 logger.debug("Niko Home Control: alarm - {}", alarmText);
506 handler.alarmEvent(alarmText);
509 logger.debug("Niko Home Control: notice - {}", alarmText);
510 handler.noticeEvent(alarmText);
513 logger.debug("Niko Home Control: unexpected message type {}", type);
518 public void executeAction(String actionId, String value) {
519 NhcMessageCmd1 nhcCmd = new NhcMessageCmd1("executeactions", Integer.parseInt(actionId),
520 Integer.parseInt(value));
525 public void executeThermostat(String thermostatId, String mode) {
526 NhcMessageCmd1 nhcCmd = new NhcMessageCmd1("executethermostat", Integer.parseInt(thermostatId))
527 .withMode(Integer.parseInt(mode));
532 public void executeThermostat(String thermostatId, int overruleTemp, int overruleTime) {
533 String overruletimeString = String.format("%1$02d:%2$02d", overruleTime / 60, overruleTime % 60);
534 NhcMessageCmd1 nhcCmd = new NhcMessageCmd1("executethermostat", Integer.parseInt(thermostatId))
535 .withOverrule(overruleTemp).withOverruletime(overruletimeString);