]> git.basschouten.com Git - openhab-addons.git/blob
0eb1adb653aa071eb81d06116ecb0ef44883a30e
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.nikohomecontrol.internal.protocol.nhc1;
14
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;
22 import java.util.Map;
23 import java.util.concurrent.ConcurrentHashMap;
24 import java.util.concurrent.ScheduledExecutorService;
25 import java.util.function.Consumer;
26
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;
36
37 import com.google.gson.Gson;
38 import com.google.gson.GsonBuilder;
39 import com.google.gson.JsonParseException;
40
41 /**
42  * The {@link NikoHomeControlCommunication1} class is able to do the following tasks with Niko Home Control I
43  * systems:
44  * <ul>
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.
49  * </ul>
50  *
51  * @author Mark Herwege - Initial Contribution
52  */
53 @NonNullByDefault
54 public class NikoHomeControlCommunication1 extends NikoHomeControlCommunication {
55
56     private Logger logger = LoggerFactory.getLogger(NikoHomeControlCommunication1.class);
57
58     private final NhcSystemInfo1 systemInfo = new NhcSystemInfo1();
59     private final Map<String, NhcLocation1> locations = new ConcurrentHashMap<>();
60
61     private @Nullable Socket nhcSocket;
62     private @Nullable PrintWriter nhcOut;
63     private @Nullable BufferedReader nhcIn;
64
65     private volatile boolean listenerStopped;
66     private volatile boolean nhcEventsRunning;
67
68     private ScheduledExecutorService scheduler;
69
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;
73
74     /**
75      * Constructor for Niko Home Control I communication object, manages communication with
76      * Niko Home Control IP-interface.
77      *
78      */
79     public NikoHomeControlCommunication1(NhcControllerEvent handler, ScheduledExecutorService scheduler) {
80         super(handler);
81         this.scheduler = scheduler;
82
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();
87     }
88
89     @Override
90     public synchronized void startCommunication() {
91         try {
92             for (int i = 1; nhcEventsRunning && (i <= 5); i++) {
93                 // the events listener thread did not finish yet, so wait max 5000ms before restarting
94                 Thread.sleep(1000);
95             }
96             if (nhcEventsRunning) {
97                 logger.debug("Niko Home Control: starting but previous connection still active after 5000ms");
98                 throw new IOException();
99             }
100
101             InetAddress addr = handler.getAddr();
102             int port = handler.getPort();
103
104             Socket socket = new Socket(addr, port);
105             nhcSocket = socket;
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());
109
110             // initialize all info in local fields
111             initialize();
112
113             // Start Niko Home Control event listener. This listener will act on all messages coming from
114             // IP-interface.
115             (new Thread(this::runNhcEvents)).start();
116
117         } catch (IOException | InterruptedException e) {
118             logger.warn("Niko Home Control: error initializing communication");
119             stopCommunication();
120             handler.controllerOffline();
121         }
122     }
123
124     /**
125      * Cleanup socket when the communication with Niko Home Control IP-interface is closed.
126      *
127      */
128     @Override
129     public synchronized void stopCommunication() {
130         listenerStopped = true;
131
132         Socket socket = nhcSocket;
133         if (socket != null) {
134             try {
135                 socket.close();
136             } catch (IOException ignore) {
137                 // ignore IO Error when trying to close the socket if the intention is to close it anyway
138             }
139         }
140         nhcSocket = null;
141
142         logger.debug("Niko Home Control: communication stopped");
143     }
144
145     @Override
146     public boolean communicationActive() {
147         return (nhcSocket != null);
148     }
149
150     /**
151      * Method that handles inbound communication from Niko Home Control, to be called on a separate thread.
152      * <p>
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.
156      *
157      */
158     private void runNhcEvents() {
159         String nhcMessage;
160
161         logger.debug("Niko Home Control: listening for events");
162         listenerStopped = false;
163         nhcEventsRunning = true;
164
165         try {
166             while (!listenerStopped & (nhcIn != null) & ((nhcMessage = nhcIn.readLine()) != null)) {
167                 readMessage(nhcMessage);
168             }
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();
176                 return;
177             }
178         } finally {
179             nhcEventsRunning = false;
180         }
181
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");
185     }
186
187     /**
188      * After setting up the communication with the Niko Home Control IP-interface, send all initialization messages.
189      * <p>
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.
193      *
194      * @throws IOException
195      */
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");
205     }
206
207     @SuppressWarnings("null")
208     private void sendAndReadMessage(String command) throws IOException {
209         sendMessage(new NhcMessageCmd1(command));
210         readMessage(nhcIn.readLine());
211     }
212
213     /**
214      * Called by other methods to send json cmd to Niko Home Control.
215      *
216      * @param nhcMessage
217      */
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();
232             }
233         }
234     }
235
236     /**
237      * Method that interprets all feedback from Niko Home Control and calls appropriate handling methods.
238      *
239      * @param nhcMessage message read from Niko Home Control.
240      */
241     private void readMessage(@Nullable String nhcMessage) {
242         logger.debug("Niko Home Control: received json {}", nhcMessage);
243
244         try {
245             NhcMessageBase1 nhcMessageGson = gsonIn.fromJson(nhcMessage, NhcMessageBase1.class);
246
247             String cmd = nhcMessageGson.getCmd();
248             String event = nhcMessageGson.getEvent();
249
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());
270             } else {
271                 logger.debug("Niko Home Control: not acted on json {}", nhcMessage);
272             }
273         } catch (JsonParseException e) {
274             logger.debug("Niko Home Control: not acted on unsupported json {}", nhcMessage);
275         }
276     }
277
278     private void setIfPresent(Map<String, String> data, String key, Consumer<String> consumer) {
279         String val = data.get(key);
280         if (val != null) {
281             consumer.accept(val);
282         }
283     }
284
285     private synchronized void cmdSystemInfo(Map<String, String> data) {
286         logger.debug("Niko Home Control: systeminfo");
287
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);
298     }
299
300     /**
301      * Return the object with system info as read from the Niko Home Control controller.
302      *
303      * @return the systemInfo
304      */
305     public synchronized NhcSystemInfo1 getSystemInfo() {
306         return systemInfo;
307     }
308
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");
315             } else {
316                 logger.warn("Niko Home Control: error code {} returned on start events", errorCode);
317             }
318         } else {
319             logger.warn("Niko Home Control: could not determine error code returned on start events");
320         }
321     }
322
323     private void cmdListLocations(List<Map<String, String>> data) {
324         logger.debug("Niko Home Control: list locations");
325
326         locations.clear();
327
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");
333                 continue;
334             }
335             NhcLocation1 nhcLocation1 = new NhcLocation1(name);
336             locations.put(id, nhcLocation1);
337         }
338     }
339
340     private void cmdListActions(List<Map<String, String>> data) {
341         logger.debug("Niko Home Control: list actions");
342
343         for (Map<String, String> action : data) {
344             String id = action.get("id");
345             if (id == null) {
346                 logger.debug("id not found in action {}", action);
347                 continue;
348             }
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));
355
356             if (!actions.containsKey(id)) {
357                 // Initial instantiation of NhcAction class for action object
358                 String name = action.get("name");
359                 if (name == null) {
360                     logger.debug("name not found in action {}", action);
361                     continue;
362                 }
363                 String type = action.get("type");
364                 ActionType actionType = ActionType.GENERIC;
365                 switch (type) {
366                     case "0":
367                         actionType = ActionType.TRIGGER;
368                         break;
369                     case "1":
370                         actionType = ActionType.RELAY;
371                         break;
372                     case "2":
373                         actionType = ActionType.DIMMER;
374                         break;
375                     case "4":
376                     case "5":
377                         actionType = ActionType.ROLLERSHUTTER;
378                         break;
379                     default:
380                         logger.debug("Niko Home Control: unknown action type {} for action {}", type, id);
381                         continue;
382                 }
383                 String locationId = action.get("location");
384                 String location = "";
385                 if (locationId != null && !locationId.isEmpty()) {
386                     location = locations.getOrDefault(locationId, new NhcLocation1("")).getName();
387                 }
388                 NhcAction nhcAction = new NhcAction1(id, name, actionType, location, this, scheduler);
389                 if (actionType == ActionType.ROLLERSHUTTER) {
390                     nhcAction.setShutterTimes(openTime, closeTime);
391                 }
392                 nhcAction.setState(state);
393                 actions.put(id, nhcAction);
394             } else {
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);
399             }
400         }
401     }
402
403     private int parseIntOrThrow(@Nullable String str) throws IllegalArgumentException {
404         if (str == null)
405             throw new IllegalArgumentException("String is null");
406         try {
407             return Integer.parseInt(str);
408         } catch (NumberFormatException e) {
409             throw new IllegalArgumentException(e);
410         }
411     }
412
413     private void cmdListThermostat(List<Map<String, String>> data) {
414         logger.debug("Niko Home Control: list thermostats");
415
416         for (Map<String, String> thermostat : data) {
417             try {
418                 String id = thermostat.get("id");
419                 if (id == null) {
420                     logger.debug("skipping thermostat {}, id not found", thermostat);
421                     continue;
422                 }
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]);
433                 }
434                 int ecosave = parseIntOrThrow(thermostat.get("ecosave"));
435
436                 // For parity with NHC II, assume heating/cooling if thermostat is on and setpoint different from
437                 // measured
438                 int demand = (mode != 3) ? (setpoint > measured ? 1 : (setpoint < measured ? -1 : 0)) : 0;
439
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();
447                     }
448                     if (name != null) {
449                         return new NhcThermostat1(i, name, location, this);
450                     }
451                     throw new IllegalArgumentException();
452                 });
453                 if (t != null) {
454                     t.updateState(measured, setpoint, mode, overrule, overruletime, ecosave, demand);
455                 }
456             } catch (IllegalArgumentException e) {
457                 // do nothing
458             }
459         }
460     }
461
462     private void cmdExecuteActions(Map<String, String> data) {
463         try {
464             int errorCode = parseIntOrThrow(data.get("error"));
465             if (errorCode == 0) {
466                 logger.debug("Niko Home Control: execute action success");
467             } else {
468                 logger.warn("Niko Home Control: error code {} returned on command execution", errorCode);
469             }
470         } catch (IllegalArgumentException e) {
471             logger.warn("Niko Home Control: no error code returned on command execution");
472         }
473     }
474
475     private void cmdExecuteThermostat(Map<String, String> data) {
476         try {
477             int errorCode = parseIntOrThrow(data.get("error"));
478             if (errorCode == 0) {
479                 logger.debug("Niko Home Control: execute thermostats success");
480             } else {
481                 logger.warn("Niko Home Control: error code {} returned on command execution", errorCode);
482             }
483         } catch (IllegalArgumentException e) {
484             logger.warn("Niko Home Control: no error code returned on command execution");
485         }
486     }
487
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);
493                 return;
494             }
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);
502                 }
503             }
504         }
505     }
506
507     private void eventListThermostat(List<Map<String, String>> data) {
508         for (Map<String, String> thermostat : data) {
509             try {
510                 String id = thermostat.get("id");
511                 if (!thermostats.containsKey(id)) {
512                     logger.warn("Niko Home Control: thermostat in controller not known {}", id);
513                     return;
514                 }
515
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]);
526                 }
527                 int ecosave = parseIntOrThrow(thermostat.get("ecosave"));
528
529                 int demand = (mode != 3) ? (setpoint > measured ? 1 : (setpoint < measured ? -1 : 0)) : 0;
530
531                 logger.debug(
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);
535                 if (t != null) {
536                     t.updateState(measured, setpoint, mode, overrule, overruletime, ecosave, demand);
537                 }
538             } catch (IllegalArgumentException e) {
539                 // do nothing
540             }
541         }
542     }
543
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);
548             return;
549         }
550         switch (data.getOrDefault("type", "")) {
551             case "0":
552                 logger.debug("Niko Home Control: alarm - {}", alarmText);
553                 handler.alarmEvent(alarmText);
554                 break;
555             case "1":
556                 logger.debug("Niko Home Control: notice - {}", alarmText);
557                 handler.noticeEvent(alarmText);
558                 break;
559             default:
560                 logger.debug("Niko Home Control: unexpected message type {}", data.get("type"));
561         }
562     }
563
564     @Override
565     public void executeAction(String actionId, String value) {
566         NhcMessageCmd1 nhcCmd = new NhcMessageCmd1("executeactions", Integer.parseInt(actionId),
567                 Integer.parseInt(value));
568         sendMessage(nhcCmd);
569     }
570
571     @Override
572     public void executeThermostat(String thermostatId, String mode) {
573         NhcMessageCmd1 nhcCmd = new NhcMessageCmd1("executethermostat", Integer.parseInt(thermostatId))
574                 .withMode(Integer.parseInt(mode));
575         sendMessage(nhcCmd);
576     }
577
578     @Override
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);
583         sendMessage(nhcCmd);
584     }
585 }