]> git.basschouten.com Git - openhab-addons.git/blob
a2c8b7bd409083da1575556b0b8b7d565040cf20
[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.Optional;
24 import java.util.concurrent.ConcurrentHashMap;
25 import java.util.concurrent.ScheduledExecutorService;
26 import java.util.function.Consumer;
27
28 import org.eclipse.jdt.annotation.NonNullByDefault;
29 import org.eclipse.jdt.annotation.Nullable;
30 import org.openhab.binding.nikohomecontrol.internal.protocol.NhcAction;
31 import org.openhab.binding.nikohomecontrol.internal.protocol.NhcControllerEvent;
32 import org.openhab.binding.nikohomecontrol.internal.protocol.NhcThermostat;
33 import org.openhab.binding.nikohomecontrol.internal.protocol.NikoHomeControlCommunication;
34 import org.openhab.binding.nikohomecontrol.internal.protocol.NikoHomeControlConstants.ActionType;
35 import org.slf4j.Logger;
36 import org.slf4j.LoggerFactory;
37
38 import com.google.gson.Gson;
39 import com.google.gson.GsonBuilder;
40 import com.google.gson.JsonParseException;
41
42 /**
43  * The {@link NikoHomeControlCommunication1} class is able to do the following tasks with Niko Home Control I
44  * systems:
45  * <ul>
46  * <li>Start and stop TCP socket connection with Niko Home Control IP-interface.
47  * <li>Read all setup and status information from the Niko Home Control Controller.
48  * <li>Execute Niko Home Control commands.
49  * <li>Listen to events from Niko Home Control.
50  * </ul>
51  *
52  * @author Mark Herwege - Initial Contribution
53  */
54 @NonNullByDefault
55 public class NikoHomeControlCommunication1 extends NikoHomeControlCommunication {
56
57     private Logger logger = LoggerFactory.getLogger(NikoHomeControlCommunication1.class);
58
59     private final NhcSystemInfo1 systemInfo = new NhcSystemInfo1();
60     private final Map<String, NhcLocation1> locations = new ConcurrentHashMap<>();
61
62     private @Nullable Socket nhcSocket;
63     private @Nullable PrintWriter nhcOut;
64     private @Nullable BufferedReader nhcIn;
65
66     private volatile boolean listenerStopped;
67     private volatile boolean nhcEventsRunning;
68
69     private ScheduledExecutorService scheduler;
70
71     // We keep only 2 gson adapters used to serialize and deserialize all messages sent and received
72     protected final Gson gsonOut = new Gson();
73     protected Gson gsonIn;
74
75     /**
76      * Constructor for Niko Home Control I communication object, manages communication with
77      * Niko Home Control IP-interface.
78      *
79      */
80     public NikoHomeControlCommunication1(NhcControllerEvent handler, ScheduledExecutorService scheduler) {
81         super(handler);
82         this.scheduler = scheduler;
83
84         // When we set up this object, we want to get the proper gson adapter set up once
85         GsonBuilder gsonBuilder = new GsonBuilder();
86         gsonBuilder.registerTypeAdapter(NhcMessageBase1.class, new NikoHomeControlMessageDeserializer1());
87         gsonIn = gsonBuilder.create();
88     }
89
90     @Override
91     public synchronized void startCommunication() {
92         try {
93             for (int i = 1; nhcEventsRunning && (i <= 5); i++) {
94                 // the events listener thread did not finish yet, so wait max 5000ms before restarting
95                 Thread.sleep(1000);
96             }
97             if (nhcEventsRunning) {
98                 logger.debug("starting but previous connection still active after 5000ms");
99                 throw new IOException();
100             }
101
102             InetAddress addr = handler.getAddr();
103             int port = handler.getPort();
104
105             Socket socket = new Socket(addr, port);
106             nhcSocket = socket;
107             nhcOut = new PrintWriter(socket.getOutputStream(), true);
108             nhcIn = new BufferedReader(new InputStreamReader(socket.getInputStream()));
109             logger.debug("connected via local port {}", socket.getLocalPort());
110
111             // initialize all info in local fields
112             initialize();
113
114             // Start Niko Home Control event listener. This listener will act on all messages coming from
115             // IP-interface.
116             (new Thread(this::runNhcEvents)).start();
117
118         } catch (IOException | InterruptedException e) {
119             stopCommunication();
120             handler.controllerOffline("Error initializing communication");
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("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("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.debug("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("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("send json {}", json);
222         nhcOut.println(json);
223         if (nhcOut.checkError()) {
224             logger.debug("error sending message, trying to restart communication");
225             restartCommunication();
226             // retry sending after restart
227             logger.debug("resend json {}", json);
228             nhcOut.println(json);
229             if (nhcOut.checkError()) {
230                 handler.controllerOffline("Error resending message");
231             }
232         }
233     }
234
235     /**
236      * Method that interprets all feedback from Niko Home Control and calls appropriate handling methods.
237      *
238      * @param nhcMessage message read from Niko Home Control.
239      */
240     private void readMessage(@Nullable String nhcMessage) {
241         logger.debug("received json {}", nhcMessage);
242
243         try {
244             NhcMessageBase1 nhcMessageGson = gsonIn.fromJson(nhcMessage, NhcMessageBase1.class);
245
246             if (nhcMessageGson == null) {
247                 return;
248             }
249             String cmd = nhcMessageGson.getCmd();
250             String event = nhcMessageGson.getEvent();
251
252             if ("systeminfo".equals(cmd)) {
253                 cmdSystemInfo(((NhcMessageMap1) nhcMessageGson).getData());
254             } else if ("startevents".equals(cmd)) {
255                 cmdStartEvents(((NhcMessageMap1) nhcMessageGson).getData());
256             } else if ("listlocations".equals(cmd)) {
257                 cmdListLocations(((NhcMessageListMap1) nhcMessageGson).getData());
258             } else if ("listactions".equals(cmd)) {
259                 cmdListActions(((NhcMessageListMap1) nhcMessageGson).getData());
260             } else if (("listthermostat").equals(cmd)) {
261                 cmdListThermostat(((NhcMessageListMap1) nhcMessageGson).getData());
262             } else if ("executeactions".equals(cmd)) {
263                 cmdExecuteActions(((NhcMessageMap1) nhcMessageGson).getData());
264             } else if ("executethermostat".equals(cmd)) {
265                 cmdExecuteThermostat(((NhcMessageMap1) nhcMessageGson).getData());
266             } else if ("listactions".equals(event)) {
267                 eventListActions(((NhcMessageListMap1) nhcMessageGson).getData());
268             } else if ("listthermostat".equals(event)) {
269                 eventListThermostat(((NhcMessageListMap1) nhcMessageGson).getData());
270             } else if ("getalarms".equals(event)) {
271                 eventGetAlarms(((NhcMessageMap1) nhcMessageGson).getData());
272             } else {
273                 logger.debug("not acted on json {}", nhcMessage);
274             }
275         } catch (JsonParseException e) {
276             logger.debug("not acted on unsupported json {}", nhcMessage);
277         }
278     }
279
280     private void setIfPresent(Map<String, String> data, String key, Consumer<String> consumer) {
281         String val = data.get(key);
282         if (val != null) {
283             consumer.accept(val);
284         }
285     }
286
287     private synchronized void cmdSystemInfo(Map<String, String> data) {
288         logger.debug("systeminfo");
289
290         setIfPresent(data, "swversion", systemInfo::setSwVersion);
291         setIfPresent(data, "api", systemInfo::setApi);
292         setIfPresent(data, "time", systemInfo::setTime);
293         setIfPresent(data, "language", systemInfo::setLanguage);
294         setIfPresent(data, "currency", systemInfo::setCurrency);
295         setIfPresent(data, "units", systemInfo::setUnits);
296         setIfPresent(data, "DST", systemInfo::setDst);
297         setIfPresent(data, "TZ", systemInfo::setTz);
298         setIfPresent(data, "lastenergyerase", systemInfo::setLastEnergyErase);
299         setIfPresent(data, "lastconfig", systemInfo::setLastConfig);
300     }
301
302     /**
303      * Return the object with system info as read from the Niko Home Control controller.
304      *
305      * @return the systemInfo
306      */
307     public synchronized NhcSystemInfo1 getSystemInfo() {
308         return systemInfo;
309     }
310
311     private void cmdStartEvents(Map<String, String> data) {
312         String errorCodeString = data.get("error");
313         if (errorCodeString != null) {
314             int errorCode = Integer.parseInt(errorCodeString);
315             if (errorCode == 0) {
316                 logger.debug("start events success");
317             } else {
318                 logger.debug("error code {} returned on start events", errorCode);
319             }
320         } else {
321             logger.debug("could not determine error code returned on start events");
322         }
323     }
324
325     private void cmdListLocations(List<Map<String, String>> data) {
326         logger.debug("list locations");
327
328         locations.clear();
329
330         for (Map<String, String> location : data) {
331             String id = location.get("id");
332             String name = location.get("name");
333             if (id == null || name == null) {
334                 logger.debug("id or name null, ignoring entry");
335                 continue;
336             }
337             NhcLocation1 nhcLocation1 = new NhcLocation1(name);
338             locations.put(id, nhcLocation1);
339         }
340     }
341
342     private void cmdListActions(List<Map<String, String>> data) {
343         logger.debug("list actions");
344
345         for (Map<String, String> action : data) {
346             String id = action.get("id");
347             if (id == null) {
348                 logger.debug("id not found in action {}", action);
349                 continue;
350             }
351             String value1 = action.get("value1");
352             int state = ((value1 == null) || value1.isEmpty() ? 0 : Integer.parseInt(value1));
353             String value2 = action.get("value2");
354             int closeTime = ((value2 == null) || value2.isEmpty() ? 0 : Integer.parseInt(value2));
355             String value3 = action.get("value3");
356             int openTime = ((value3 == null) || value3.isEmpty() ? 0 : Integer.parseInt(value3));
357
358             if (!actions.containsKey(id)) {
359                 // Initial instantiation of NhcAction class for action object
360                 String name = action.get("name");
361                 if (name == null) {
362                     logger.debug("name not found in action {}", action);
363                     continue;
364                 }
365                 String type = Optional.ofNullable(action.get("type")).orElse("");
366                 ActionType actionType = ActionType.GENERIC;
367                 switch (type) {
368                     case "0":
369                         actionType = ActionType.TRIGGER;
370                         break;
371                     case "1":
372                         actionType = ActionType.RELAY;
373                         break;
374                     case "2":
375                         actionType = ActionType.DIMMER;
376                         break;
377                     case "4":
378                     case "5":
379                         actionType = ActionType.ROLLERSHUTTER;
380                         break;
381                     default:
382                         logger.debug("unknown action type {} for action {}", type, id);
383                         continue;
384                 }
385                 String locationId = action.get("location");
386                 String location = "";
387                 if (locationId != null && !locationId.isEmpty()) {
388                     location = locations.getOrDefault(locationId, new NhcLocation1("")).getName();
389                 }
390                 NhcAction nhcAction = new NhcAction1(id, name, actionType, location, this, scheduler);
391                 if (actionType == ActionType.ROLLERSHUTTER) {
392                     nhcAction.setShutterTimes(openTime, closeTime);
393                 }
394                 nhcAction.setState(state);
395                 actions.put(id, nhcAction);
396             } else {
397                 // Action object already exists, so only update state.
398                 // If we would re-instantiate action, we would lose pointer back from action to thing handler that was
399                 // set in thing handler initialize().
400                 NhcAction nhcAction = actions.get(id);
401                 if (nhcAction != null) {
402                     nhcAction.setState(state);
403                 }
404             }
405         }
406     }
407
408     private int parseIntOrThrow(@Nullable String str) throws IllegalArgumentException {
409         if (str == null) {
410             throw new IllegalArgumentException("String is null");
411         }
412         try {
413             return Integer.parseInt(str);
414         } catch (NumberFormatException e) {
415             throw new IllegalArgumentException(e);
416         }
417     }
418
419     private void cmdListThermostat(List<Map<String, String>> data) {
420         logger.debug("list thermostats");
421
422         for (Map<String, String> thermostat : data) {
423             try {
424                 String id = thermostat.get("id");
425                 if (id == null) {
426                     logger.debug("skipping thermostat {}, id not found", thermostat);
427                     continue;
428                 }
429                 int measured = parseIntOrThrow(thermostat.get("measured"));
430                 int setpoint = parseIntOrThrow(thermostat.get("setpoint"));
431                 int mode = parseIntOrThrow(thermostat.get("mode"));
432                 int overrule = parseIntOrThrow(thermostat.get("overrule"));
433                 // overruletime received in "HH:MM" format
434                 String[] overruletimeStrings = thermostat.getOrDefault("overruletime", "").split(":");
435                 int overruletime = 0;
436                 if (overruletimeStrings.length == 2) {
437                     overruletime = Integer.parseInt(overruletimeStrings[0]) * 60
438                             + Integer.parseInt(overruletimeStrings[1]);
439                 }
440                 int ecosave = parseIntOrThrow(thermostat.get("ecosave"));
441
442                 // For parity with NHC II, assume heating/cooling if thermostat is on and setpoint different from
443                 // measured
444                 int demand = (mode != 3) ? (setpoint > measured ? 1 : (setpoint < measured ? -1 : 0)) : 0;
445
446                 NhcThermostat t = thermostats.computeIfAbsent(id, i -> {
447                     // Initial instantiation of NhcThermostat class for thermostat object
448                     String name = thermostat.get("name");
449                     String locationId = thermostat.get("location");
450                     String location = "";
451                     if (!((locationId == null) || locationId.isEmpty())) {
452                         NhcLocation1 nhcLocation = locations.get(locationId);
453                         if (nhcLocation != null) {
454                             location = nhcLocation.getName();
455                         }
456                     }
457                     if (name != null) {
458                         return new NhcThermostat1(i, name, location, this);
459                     }
460                     throw new IllegalArgumentException();
461                 });
462                 if (t != null) {
463                     t.updateState(measured, setpoint, mode, overrule, overruletime, ecosave, demand);
464                 }
465             } catch (IllegalArgumentException e) {
466                 // do nothing
467             }
468         }
469     }
470
471     private void cmdExecuteActions(Map<String, String> data) {
472         try {
473             int errorCode = parseIntOrThrow(data.get("error"));
474             if (errorCode == 0) {
475                 logger.debug("execute action success");
476             } else {
477                 logger.debug("error code {} returned on command execution", errorCode);
478             }
479         } catch (IllegalArgumentException e) {
480             logger.debug("no error code returned on command execution");
481         }
482     }
483
484     private void cmdExecuteThermostat(Map<String, String> data) {
485         try {
486             int errorCode = parseIntOrThrow(data.get("error"));
487             if (errorCode == 0) {
488                 logger.debug("execute thermostats success");
489             } else {
490                 logger.debug("error code {} returned on command execution", errorCode);
491             }
492         } catch (IllegalArgumentException e) {
493             logger.debug("no error code returned on command execution");
494         }
495     }
496
497     private void eventListActions(List<Map<String, String>> data) {
498         for (Map<String, String> action : data) {
499             String id = action.get("id");
500             if (id == null || !actions.containsKey(id)) {
501                 logger.warn("action in controller not known {}", id);
502                 return;
503             }
504             String stateString = action.get("value1");
505             if (stateString != null) {
506                 int state = Integer.parseInt(stateString);
507                 logger.debug("event execute action {} with state {}", id, state);
508                 NhcAction action1 = actions.get(id);
509                 if (action1 != null) {
510                     action1.setState(state);
511                 }
512             }
513         }
514     }
515
516     private void eventListThermostat(List<Map<String, String>> data) {
517         for (Map<String, String> thermostat : data) {
518             try {
519                 String id = thermostat.get("id");
520                 if (!thermostats.containsKey(id)) {
521                     logger.warn("thermostat in controller not known {}", id);
522                     return;
523                 }
524
525                 int measured = parseIntOrThrow(thermostat.get("measured"));
526                 int setpoint = parseIntOrThrow(thermostat.get("setpoint"));
527                 int mode = parseIntOrThrow(thermostat.get("mode"));
528                 int overrule = parseIntOrThrow(thermostat.get("overrule"));
529                 // overruletime received in "HH:MM" format
530                 String[] overruletimeStrings = thermostat.getOrDefault("overruletime", "").split(":");
531                 int overruletime = 0;
532                 if (overruletimeStrings.length == 2) {
533                     overruletime = Integer.parseInt(overruletimeStrings[0]) * 60
534                             + Integer.parseInt(overruletimeStrings[1]);
535                 }
536                 int ecosave = parseIntOrThrow(thermostat.get("ecosave"));
537
538                 int demand = (mode != 3) ? (setpoint > measured ? 1 : (setpoint < measured ? -1 : 0)) : 0;
539
540                 logger.debug(
541                         "Niko Home Control: event execute thermostat {} with measured {}, setpoint {}, mode {}, overrule {}, overruletime {}, ecosave {}, demand {}",
542                         id, measured, setpoint, mode, overrule, overruletime, ecosave, demand);
543                 NhcThermostat t = thermostats.get(id);
544                 if (t != null) {
545                     t.updateState(measured, setpoint, mode, overrule, overruletime, ecosave, demand);
546                 }
547             } catch (IllegalArgumentException e) {
548                 // do nothing
549             }
550         }
551     }
552
553     private void eventGetAlarms(Map<String, String> data) {
554         String alarmText = data.get("text");
555         if (alarmText == null) {
556             logger.debug("message does not contain alarmtext: {}", data);
557             return;
558         }
559         switch (data.getOrDefault("type", "")) {
560             case "0":
561                 logger.debug("alarm - {}", alarmText);
562                 handler.alarmEvent(alarmText);
563                 break;
564             case "1":
565                 logger.debug("notice - {}", alarmText);
566                 handler.noticeEvent(alarmText);
567                 break;
568             default:
569                 logger.debug("unexpected message type {}", data.get("type"));
570         }
571     }
572
573     @Override
574     public void executeAction(String actionId, String value) {
575         NhcMessageCmd1 nhcCmd = new NhcMessageCmd1("executeactions", Integer.parseInt(actionId),
576                 Integer.parseInt(value));
577         sendMessage(nhcCmd);
578     }
579
580     @Override
581     public void executeThermostat(String thermostatId, String mode) {
582         NhcMessageCmd1 nhcCmd = new NhcMessageCmd1("executethermostat", Integer.parseInt(thermostatId))
583                 .withMode(Integer.parseInt(mode));
584         sendMessage(nhcCmd);
585     }
586
587     @Override
588     public void executeThermostat(String thermostatId, int overruleTemp, int overruleTime) {
589         String overruletimeString = String.format("%1$02d:%2$02d", overruleTime / 60, overruleTime % 60);
590         NhcMessageCmd1 nhcCmd = new NhcMessageCmd1("executethermostat", Integer.parseInt(thermostatId))
591                 .withOverrule(overruleTemp).withOverruletime(overruletimeString);
592         sendMessage(nhcCmd);
593     }
594 }