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