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