]> git.basschouten.com Git - openhab-addons.git/blob
c0b369f1855b11af996664593f61bd2624fb6e20
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2020 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             NhcLocation1 nhcLocation1 = new NhcLocation1(name);
332             locations.put(id, nhcLocation1);
333         }
334     }
335
336     private void cmdListActions(List<Map<String, String>> data) {
337         logger.debug("Niko Home Control: list actions");
338
339         for (Map<String, String> action : data) {
340
341             String id = action.get("id");
342             String value1 = action.get("value1");
343             int state = ((value1 == null) || value1.isEmpty() ? 0 : Integer.parseInt(value1));
344             String value2 = action.get("value2");
345             int closeTime = ((value2 == null) || value2.isEmpty() ? 0 : Integer.parseInt(value2));
346             String value3 = action.get("value3");
347             int openTime = ((value3 == null) || value3.isEmpty() ? 0 : Integer.parseInt(value3));
348
349             if (!actions.containsKey(id)) {
350                 // Initial instantiation of NhcAction class for action object
351                 String name = action.get("name");
352                 String type = action.get("type");
353                 ActionType actionType = ActionType.GENERIC;
354                 switch (type) {
355                     case "0":
356                         actionType = ActionType.TRIGGER;
357                         break;
358                     case "1":
359                         actionType = ActionType.RELAY;
360                         break;
361                     case "2":
362                         actionType = ActionType.DIMMER;
363                         break;
364                     case "4":
365                     case "5":
366                         actionType = ActionType.ROLLERSHUTTER;
367                         break;
368                     default:
369                         logger.debug("Niko Home Control: unknown action type {} for action {}", type, id);
370                         continue;
371                 }
372                 String locationId = action.get("location");
373                 String location = "";
374                 if (!locationId.isEmpty()) {
375                     location = locations.get(locationId).getName();
376                 }
377                 NhcAction nhcAction = new NhcAction1(id, name, actionType, location, this, scheduler);
378                 if (actionType == ActionType.ROLLERSHUTTER) {
379                     nhcAction.setShutterTimes(openTime, closeTime);
380                 }
381                 nhcAction.setState(state);
382                 actions.put(id, nhcAction);
383             } else {
384                 // Action object already exists, so only update state.
385                 // If we would re-instantiate action, we would lose pointer back from action to thing handler that was
386                 // set in thing handler initialize().
387                 actions.get(id).setState(state);
388             }
389         }
390     }
391
392     private int parseIntOrThrow(@Nullable String str) throws IllegalArgumentException {
393         if (str == null)
394             throw new IllegalArgumentException("String is null");
395         try {
396             return Integer.parseInt(str);
397         } catch (NumberFormatException e) {
398             throw new IllegalArgumentException(e);
399         }
400     }
401
402     private void cmdListThermostat(List<Map<String, String>> data) {
403         logger.debug("Niko Home Control: list thermostats");
404
405         for (Map<String, String> thermostat : data) {
406             try {
407                 String id = thermostat.get("id");
408                 int measured = parseIntOrThrow(thermostat.get("measured"));
409                 int setpoint = parseIntOrThrow(thermostat.get("setpoint"));
410                 int mode = parseIntOrThrow(thermostat.get("mode"));
411                 int overrule = parseIntOrThrow(thermostat.get("overrule"));
412                 // overruletime received in "HH:MM" format
413                 String[] overruletimeStrings = thermostat.getOrDefault("overruletime", "").split(":");
414                 int overruletime = 0;
415                 if (overruletimeStrings.length == 2) {
416                     overruletime = Integer.parseInt(overruletimeStrings[0]) * 60
417                             + Integer.parseInt(overruletimeStrings[1]);
418                 }
419                 int ecosave = parseIntOrThrow(thermostat.get("ecosave"));
420
421                 // For parity with NHC II, assume heating/cooling if thermostat is on and setpoint different from
422                 // measured
423                 int demand = (mode != 3) ? (setpoint > measured ? 1 : (setpoint < measured ? -1 : 0)) : 0;
424
425                 NhcThermostat t = thermostats.computeIfAbsent(id, i -> {
426                     // Initial instantiation of NhcThermostat class for thermostat object
427                     String name = thermostat.get("name");
428                     String locationId = thermostat.get("location");
429                     String location = "";
430                     if (!locationId.isEmpty()) {
431                         location = locations.get(locationId).getName();
432                     }
433                     if (name != null) {
434                         return new NhcThermostat1(i, name, location, this);
435                     }
436                     throw new IllegalArgumentException();
437                 });
438                 if (t != null) {
439                     t.updateState(measured, setpoint, mode, overrule, overruletime, ecosave, demand);
440                 }
441             } catch (IllegalArgumentException e) {
442                 // do nothing
443             }
444         }
445     }
446
447     private void cmdExecuteActions(Map<String, String> data) {
448         try {
449             int errorCode = parseIntOrThrow(data.get("error"));
450             if (errorCode == 0) {
451                 logger.debug("Niko Home Control: execute action success");
452             } else {
453                 logger.warn("Niko Home Control: error code {} returned on command execution", errorCode);
454             }
455         } catch (IllegalArgumentException e) {
456             logger.warn("Niko Home Control: no error code returned on command execution");
457         }
458     }
459
460     private void cmdExecuteThermostat(Map<String, String> data) {
461         try {
462             int errorCode = parseIntOrThrow(data.get("error"));
463             if (errorCode == 0) {
464                 logger.debug("Niko Home Control: execute thermostats success");
465             } else {
466                 logger.warn("Niko Home Control: error code {} returned on command execution", errorCode);
467             }
468         } catch (IllegalArgumentException e) {
469             logger.warn("Niko Home Control: no error code returned on command execution");
470         }
471     }
472
473     private void eventListActions(List<Map<String, String>> data) {
474         for (Map<String, String> action : data) {
475             String id = action.get("id");
476             if (id == null || !actions.containsKey(id)) {
477                 logger.warn("Niko Home Control: action in controller not known {}", id);
478                 return;
479             }
480             String stateString = action.get("value1");
481             if (stateString != null) {
482                 int state = Integer.parseInt(stateString);
483                 logger.debug("Niko Home Control: event execute action {} with state {}", id, state);
484                 NhcAction action1 = actions.get(id);
485                 if (action1 != null) {
486                     action1.setState(state);
487                 }
488             }
489         }
490     }
491
492     private void eventListThermostat(List<Map<String, String>> data) {
493         for (Map<String, String> thermostat : data) {
494             try {
495                 String id = thermostat.get("id");
496                 if (!thermostats.containsKey(id)) {
497                     logger.warn("Niko Home Control: thermostat in controller not known {}", id);
498                     return;
499                 }
500
501                 int measured = parseIntOrThrow(thermostat.get("measured"));
502                 int setpoint = parseIntOrThrow(thermostat.get("setpoint"));
503                 int mode = parseIntOrThrow(thermostat.get("mode"));
504                 int overrule = parseIntOrThrow(thermostat.get("overrule"));
505                 // overruletime received in "HH:MM" format
506                 String[] overruletimeStrings = thermostat.getOrDefault("overruletime", "").split(":");
507                 int overruletime = 0;
508                 if (overruletimeStrings.length == 2) {
509                     overruletime = Integer.parseInt(overruletimeStrings[0]) * 60
510                             + Integer.parseInt(overruletimeStrings[1]);
511                 }
512                 int ecosave = parseIntOrThrow(thermostat.get("ecosave"));
513
514                 int demand = (mode != 3) ? (setpoint > measured ? 1 : (setpoint < measured ? -1 : 0)) : 0;
515
516                 logger.debug(
517                         "Niko Home Control: event execute thermostat {} with measured {}, setpoint {}, mode {}, overrule {}, overruletime {}, ecosave {}, demand {}",
518                         id, measured, setpoint, mode, overrule, overruletime, ecosave, demand);
519                 NhcThermostat t = thermostats.get(id);
520                 if (t != null) {
521                     t.updateState(measured, setpoint, mode, overrule, overruletime, ecosave, demand);
522                 }
523             } catch (IllegalArgumentException e) {
524                 // do nothing
525             }
526         }
527     }
528
529     private void eventGetAlarms(Map<String, String> data) {
530         String alarmText = data.get("text");
531         switch (data.getOrDefault("type", "")) {
532             case "0":
533                 logger.debug("Niko Home Control: alarm - {}", alarmText);
534                 handler.alarmEvent(alarmText);
535                 break;
536             case "1":
537                 logger.debug("Niko Home Control: notice - {}", alarmText);
538                 handler.noticeEvent(alarmText);
539                 break;
540             default:
541                 logger.debug("Niko Home Control: unexpected message type {}", data.get("type"));
542         }
543     }
544
545     @Override
546     public void executeAction(String actionId, String value) {
547         NhcMessageCmd1 nhcCmd = new NhcMessageCmd1("executeactions", Integer.parseInt(actionId),
548                 Integer.parseInt(value));
549         sendMessage(nhcCmd);
550     }
551
552     @Override
553     public void executeThermostat(String thermostatId, String mode) {
554         NhcMessageCmd1 nhcCmd = new NhcMessageCmd1("executethermostat", Integer.parseInt(thermostatId))
555                 .withMode(Integer.parseInt(mode));
556         sendMessage(nhcCmd);
557     }
558
559     @Override
560     public void executeThermostat(String thermostatId, int overruleTemp, int overruleTime) {
561         String overruletimeString = String.format("%1$02d:%2$02d", overruleTime / 60, overruleTime % 60);
562         NhcMessageCmd1 nhcCmd = new NhcMessageCmd1("executethermostat", Integer.parseInt(thermostatId))
563                 .withOverrule(overruleTemp).withOverruletime(overruletimeString);
564         sendMessage(nhcCmd);
565     }
566 }