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