]> git.basschouten.com Git - openhab-addons.git/blob
86f4617ec3303e06ab2da13209dd052d4d4132d9
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 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.vitotronic.internal.handler;
14
15 import java.io.IOException;
16 import java.io.InputStream;
17 import java.io.PrintStream;
18 import java.net.Socket;
19 import java.net.UnknownHostException;
20 import java.util.HashMap;
21 import java.util.HashSet;
22 import java.util.Map;
23 import java.util.Set;
24 import java.util.concurrent.ScheduledFuture;
25 import java.util.concurrent.TimeUnit;
26
27 import org.openhab.binding.vitotronic.internal.VitotronicBindingConfiguration;
28 import org.openhab.binding.vitotronic.internal.discovery.VitotronicDiscoveryService;
29 import org.openhab.core.thing.Bridge;
30 import org.openhab.core.thing.ChannelUID;
31 import org.openhab.core.thing.ThingStatus;
32 import org.openhab.core.thing.binding.BaseBridgeHandler;
33 import org.openhab.core.types.Command;
34 import org.slf4j.Logger;
35 import org.slf4j.LoggerFactory;
36 import org.xml.sax.Attributes;
37 import org.xml.sax.ContentHandler;
38 import org.xml.sax.InputSource;
39 import org.xml.sax.Locator;
40 import org.xml.sax.SAXException;
41 import org.xml.sax.XMLReader;
42 import org.xml.sax.helpers.XMLReaderFactory;
43
44 /**
45  * The {@link VitotronicBridgeHandler} class handles the connection to the
46  * optolink adapter.
47  *
48  * @author Stefan Andres - Initial contribution
49  */
50 public class VitotronicBridgeHandler extends BaseBridgeHandler {
51
52     private final Logger logger = LoggerFactory.getLogger(VitotronicBridgeHandler.class);
53
54     private String ipAddress;
55     private int port;
56     private int refreshInterval = 300;
57     private Socket socket;
58     private PrintStream out;
59     private InputStream inStream;
60     private boolean isConnect = false;
61     private boolean isDiscover = false;
62
63     public VitotronicBridgeHandler(Bridge bridge) {
64         super(bridge);
65     }
66
67     @Override
68     public void updateStatus(ThingStatus status) {
69         super.updateStatus(status);
70         updateThingHandlersStatus(status);
71     }
72
73     public void updateStatus() {
74         if (isConnect) {
75             updateStatus(ThingStatus.ONLINE);
76         } else {
77             updateStatus(ThingStatus.OFFLINE);
78         }
79     }
80
81     // Managing Thing Discovery Service
82
83     private VitotronicDiscoveryService discoveryService = null;
84
85     public void registerDiscoveryService(VitotronicDiscoveryService discoveryService) {
86         if (discoveryService == null) {
87             throw new IllegalArgumentException("It's not allowed to pass a null ThingDiscoveryListener.");
88         } else {
89             this.discoveryService = discoveryService;
90             logger.trace("register Discovery Service");
91         }
92     }
93
94     public void unregisterDiscoveryService() {
95         discoveryService = null;
96         logger.trace("unregister Discovery Service");
97     }
98
99     // Handles Thing discovery
100
101     private void createThing(String thingType, String thingID) {
102         logger.trace("Create thing Type='{}' id='{}'", thingType, thingID);
103         if (discoveryService != null) {
104             discoveryService.addVitotronicThing(thingType, thingID);
105         }
106     }
107
108     // Managing ThingHandler
109
110     private Map<String, VitotronicThingHandler> thingHandlerMap = new HashMap<>();
111
112     public void registerVitotronicThingListener(VitotronicThingHandler thingHandler) {
113         if (thingHandler == null) {
114             throw new IllegalArgumentException("It's not allowed to pass a null ThingHandler.");
115         } else {
116             String thingID = thingHandler.getThing().getUID().getId();
117             if (thingHandlerMap.get(thingID) == null) {
118                 thingHandlerMap.put(thingID, thingHandler);
119                 logger.trace("register thingHandler for thing: {}", thingID);
120                 updateThingHandlerStatus(thingHandler, this.getStatus());
121                 sendSocketData("get " + thingID);
122             } else {
123                 logger.trace("thingHandler for thing: '{}' already registered", thingID);
124             }
125         }
126     }
127
128     public void unregisterThingListener(VitotronicThingHandler thingHandler) {
129         if (thingHandler != null) {
130             String thingID = thingHandler.getThing().getUID().getId();
131             if (thingHandlerMap.remove(thingID) == null) {
132                 logger.trace("thingHandler for thing: {} not registered", thingID);
133             }
134         }
135     }
136
137     private void updateThingHandlerStatus(VitotronicThingHandler thingHandler, ThingStatus status) {
138         thingHandler.updateStatus(status);
139     }
140
141     private void updateThingHandlersStatus(ThingStatus status) {
142         for (Map.Entry<String, VitotronicThingHandler> entry : thingHandlerMap.entrySet()) {
143             updateThingHandlerStatus(entry.getValue(), status);
144         }
145     }
146
147     // Background Runables
148
149     private ScheduledFuture<?> pollingJob;
150
151     private Runnable pollingRunnable = () -> {
152         logger.trace("Polling job called");
153         if (!isConnect) {
154             startSocketReceiver();
155             try {
156                 Thread.sleep(5000); // Wait for connection .
157             } catch (InterruptedException e) {
158             }
159         }
160         if (isConnect) {
161             scanThings();
162             refreshData();
163         }
164     };
165
166     private synchronized void startAutomaticRefresh() {
167         if (pollingJob == null || pollingJob.isCancelled()) {
168             pollingJob = scheduler.scheduleWithFixedDelay(pollingRunnable, 0, refreshInterval, TimeUnit.SECONDS);
169         }
170     }
171
172     private void refreshData() {
173         logger.trace("Job: refresh Data...");
174         for (Map.Entry<String, VitotronicThingHandler> entry : thingHandlerMap.entrySet()) {
175             String channelList = entry.getValue().getActiveChannelListAsString();
176             String thingId = entry.getValue().getThing().getUID().getId();
177             if (isConnect && (channelList.length() > 0)) {
178                 logger.trace("Get Data for '{}'", thingId);
179                 sendSocketData("get " + thingId + " " + channelList);
180             }
181         }
182     }
183
184     // Methods for ThingHandler
185
186     public void scanThings() {
187         logger.trace("Job: Discover Things...");
188         if (!isDiscover) {
189             sendSocketData("list");
190             isDiscover = true;
191         }
192     }
193
194     public ThingStatus getStatus() {
195         return getThing().getStatus();
196     }
197
198     public void updateChannel(String thingId, String channelId, String value) {
199         sendSocketData("set " + thingId + ":" + channelId + " " + value);
200     }
201
202     // internal Methods
203
204     @Override
205     public void handleCommand(ChannelUID channelUID, Command command) {
206         // No channels - nothing to do
207     }
208
209     @Override
210     public void initialize() {
211         logger.debug("Initializing Vitotronic bridge handler {}", getThing().getUID());
212         updateStatus();
213         VitotronicBindingConfiguration configuration = getConfigAs(VitotronicBindingConfiguration.class);
214         ipAddress = configuration.ipAddress;
215         port = configuration.port;
216         refreshInterval = configuration.refreshInterval;
217
218         isDiscover = false;
219         startAutomaticRefresh();
220     }
221
222     @Override
223     public void dispose() {
224         logger.debug("Dispose Vitotronic bridge handler {}", getThing().getUID());
225
226         if (pollingJob != null && !pollingJob.isCancelled()) {
227             pollingJob.cancel(true);
228             pollingJob = null;
229         }
230     }
231
232     // Connection to adapter
233
234     private void openSocket() {
235         logger.trace("Try to open connection to Optolink Adapter {}:{}", ipAddress, port);
236
237         try {
238             socket = new Socket(ipAddress, port);
239             out = new PrintStream(socket.getOutputStream());
240             inStream = socket.getInputStream();
241         } catch (UnknownHostException e) {
242             logger.error("Can't find Host: {}:{}", ipAddress, port);
243         } catch (IOException e) {
244             logger.debug("Error in communication to Host: {}:{}", ipAddress, port);
245             logger.trace("Diagnostic: ", e);
246         }
247     }
248
249     Runnable socketReceiverRunnable = () -> {
250         logger.trace("Start Background Thread for recieving data from adapter");
251         try {
252             XMLReader xmlReader = XMLReaderFactory.createXMLReader();
253             xmlReader.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
254             xmlReader.setContentHandler(new XmlHandler());
255             logger.trace("Start Parser for optolink adapter");
256             xmlReader.parse(new InputSource(inStream));
257
258         } catch (IOException e) {
259             logger.trace("Connection error from optolink adapter");
260         } catch (SAXException e) {
261             logger.trace("XML Parser Error");
262
263         }
264         updateStatus(ThingStatus.OFFLINE);
265         isConnect = false;
266         try {
267             if (!socket.isClosed()) {
268                 socket.close();
269             }
270         } catch (Exception e) {
271         }
272         logger.trace("Connection to optolink adapter is died ... wait for restart");
273     };
274
275     private void startSocketReceiver() {
276         if (!isConnect) {
277             openSocket();
278
279             Thread thread = new Thread(socketReceiverRunnable);
280             thread.setName("VitotronicSocketThread");
281             thread.start();
282         }
283     }
284
285     private void sendSocketData(String message) {
286         try {
287             logger.trace("Send Message {}", message);
288             if (isConnect) {
289                 if (message.matches("^set.*REFRESH$")) {
290                     String[] msgParts = message.split(" ");
291                     String[] thingChannel = msgParts[1].split(":");
292                     message = "get " + thingChannel[0] + " " + thingChannel[1];
293                 }
294                 out.write((message + "\n").getBytes());
295             }
296         } catch (IOException e) {
297             logger.error("Error in sending data to optolink adapter");
298             logger.trace("Diagnostic: ", e);
299         }
300     }
301
302     // Handles all data what received from optolink adapter
303
304     public class XmlHandler implements ContentHandler {
305         boolean isData;
306         boolean isDefine;
307         boolean isThing;
308         boolean isChannel;
309         boolean isDescription;
310         String thingID;
311         String thingType;
312         String channelID;
313         String description;
314         VitotronicThingHandler thingHandler;
315         Set<String> channels = new HashSet<>();
316
317         @Override
318         public void startElement(String uri, String localName, String pName, Attributes attr) throws SAXException {
319             try {
320                 switch (localName) {
321                     case "optolink":
322                         isConnect = true;
323                         updateStatus(ThingStatus.ONLINE);
324                         break;
325                     case "data":
326                         isDefine = false;
327                         break;
328                     case "define":
329                         isDefine = true;
330                         break;
331                     case "description":
332                         isDescription = true;
333                         break;
334                     case "thing":
335                         isThing = true;
336                         if (isDefine) {
337                             thingType = attr.getValue("type");
338                         }
339                         thingID = attr.getValue("id");
340                         channels.clear();
341                         thingHandler = thingHandlerMap.get(thingID);
342                         break;
343                     case "channel":
344                         isChannel = true;
345                         channelID = attr.getValue("id");
346                         if (isDefine) {
347                             channels.add(channelID);
348                         } else { // is data
349                             if (thingHandler != null) {
350                                 logger.trace("Set Data for channel '{}' value '{}'", channelID, attr.getValue("value"));
351                                 thingHandler.setChannelValue(channelID, attr.getValue("value"));
352                             }
353                         }
354                         break;
355                 }
356             } catch (Exception e) {
357                 logger.error("Error in parsing data");
358                 logger.trace("Diagnostic: ", e);
359             }
360         }
361
362         @Override
363         public void characters(char[] ch, int start, int length) throws SAXException {
364             if (isDescription) {
365                 description = new String(ch, start, length);
366             }
367         }
368
369         @Override
370         public void endElement(String uri, String localName, String qName) throws SAXException {
371             switch (localName) {
372                 case "description":
373                     isDescription = false;
374                     break;
375                 case "thing":
376                     if (isDefine) {
377                         createThing(thingType, thingID);
378                     }
379                     isThing = false;
380                     thingHandler = null;
381                     break;
382                 case "channel":
383                     isChannel = false;
384                     break;
385             }
386         }
387
388         // Unused function of xmlReader
389         @Override
390         public void endDocument() throws SAXException {
391         }
392
393         @Override
394         public void ignorableWhitespace(char[] arg0, int arg1, int arg2) throws SAXException {
395         }
396
397         @Override
398         public void processingInstruction(String arg0, String arg1) throws SAXException {
399         }
400
401         @Override
402         public void setDocumentLocator(Locator arg0) {
403         }
404
405         @Override
406         public void skippedEntity(String arg0) throws SAXException {
407         }
408
409         @Override
410         public void startDocument() throws SAXException {
411         }
412
413         @Override
414         public void startPrefixMapping(String arg0, String arg1) throws SAXException {
415         }
416
417         @Override
418         public void endPrefixMapping(String prefix) throws SAXException {
419         }
420     }
421 }