]> git.basschouten.com Git - openhab-addons.git/blob
f19fdbcc2fe1156e229a022e403e47e4b8672291
[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.onewire.internal.owserver;
14
15 import java.io.DataInputStream;
16 import java.io.DataOutputStream;
17 import java.io.EOFException;
18 import java.io.IOException;
19 import java.net.Socket;
20 import java.util.ArrayList;
21 import java.util.Arrays;
22 import java.util.List;
23 import java.util.Objects;
24 import java.util.stream.Collectors;
25
26 import org.eclipse.jdt.annotation.NonNullByDefault;
27 import org.eclipse.jdt.annotation.Nullable;
28 import org.openhab.binding.onewire.internal.OwException;
29 import org.openhab.binding.onewire.internal.OwPageBuffer;
30 import org.openhab.binding.onewire.internal.SensorId;
31 import org.openhab.binding.onewire.internal.handler.OwserverBridgeHandler;
32 import org.openhab.core.library.types.DecimalType;
33 import org.openhab.core.library.types.OnOffType;
34 import org.openhab.core.types.State;
35 import org.openhab.core.types.UnDefType;
36 import org.slf4j.Logger;
37 import org.slf4j.LoggerFactory;
38
39 /**
40  * The {@link OwserverConnection} defines the protocol for connections to owservers.
41  *
42  * Data is requested by using one of the read / write methods. In case of errors, an {@link OwException}
43  * is thrown. All other exceptions are caught and handled.
44  *
45  * The data request methods follow a general pattern:
46  * * build the appropriate {@link OwserverPacket} for the request
47  * * call {@link #request(OwserverPacket)} to ask for the data, which then
48  * * uses {@link #write(OwserverPacket)} to get the request to the server and
49  * * uses {@link #read(boolean)} to get the result
50  *
51  * Hereby, the resulting packet is examined on an appropriate return code (!= -1) and whether the
52  * expected payload is attached. If not, an {@link OwException} is thrown.
53  *
54  * @author Jan N. Klug - Initial contribution
55  */
56
57 @NonNullByDefault
58 public class OwserverConnection {
59     public static final int DEFAULT_PORT = 4304;
60     public static final int KEEPALIVE_INTERVAL = 1000;
61
62     private static final int CONNECTION_MAX_RETRY = 5;
63
64     private final Logger logger = LoggerFactory.getLogger(OwserverConnection.class);
65
66     private final OwserverBridgeHandler thingHandlerCallback;
67     private String owserverAddress = "";
68     private int owserverPort = DEFAULT_PORT;
69
70     private @Nullable Socket owserverSocket = null;
71     private @Nullable DataInputStream owserverInputStream = null;
72     private @Nullable DataOutputStream owserverOutputStream = null;
73     private OwserverConnectionState owserverConnectionState = OwserverConnectionState.STOPPED;
74     private boolean tryingConnectionRecovery = false;
75
76     // reset to 0 after successful request
77     private int connectionErrorCounter = 0;
78
79     public OwserverConnection(OwserverBridgeHandler owBaseBridgeHandler) {
80         this.thingHandlerCallback = owBaseBridgeHandler;
81     }
82
83     /**
84      * set the owserver host address
85      *
86      * @param address as String (IP or FQDN), defaults to localhost
87      */
88     public void setHost(String address) {
89         this.owserverAddress = address;
90         if (owserverConnectionState != OwserverConnectionState.STOPPED) {
91             close();
92         }
93     }
94
95     /**
96      * set the owserver port
97      *
98      * @param port defaults to 4304
99      */
100     public void setPort(int port) {
101         this.owserverPort = port;
102         if (owserverConnectionState != OwserverConnectionState.STOPPED) {
103             close();
104         }
105     }
106
107     /**
108      * start the owserver connection
109      */
110     public void start() {
111         logger.debug("Trying to (re)start OW server connection - previous state: {}",
112                 owserverConnectionState.toString());
113         connectionErrorCounter = 0;
114         tryingConnectionRecovery = true;
115         boolean success = false;
116         do {
117             success = open();
118             if (success && owserverConnectionState != OwserverConnectionState.FAILED) {
119                 tryingConnectionRecovery = false;
120             }
121         } while (!success && (owserverConnectionState != OwserverConnectionState.FAILED || tryingConnectionRecovery));
122     }
123
124     /**
125      * stop the owserver connection and report new {@link OwserverConnectionState} to {@link #thingHandlerCallback}.
126      */
127     public void stop() {
128         close();
129         owserverConnectionState = OwserverConnectionState.STOPPED;
130         thingHandlerCallback.reportConnectionState(owserverConnectionState);
131     }
132
133     /**
134      * list all devices on this owserver
135      *
136      * @return a list of device ids
137      */
138     public @NonNullByDefault({}) List<SensorId> getDirectory(String basePath) throws OwException {
139         OwserverPacket requestPacket = new OwserverPacket(OwserverMessageType.DIRALL, basePath);
140         OwserverPacket returnPacket = request(requestPacket);
141
142         if ((returnPacket.getReturnCode() != -1) && returnPacket.hasPayload()) {
143             return Arrays.stream(returnPacket.getPayloadString().split(",")).map(this::stringToSensorId)
144                     .filter(Objects::nonNull).collect(Collectors.toList());
145         } else {
146             throw new OwException("invalid of empty packet when requesting directory");
147         }
148     }
149
150     private @Nullable SensorId stringToSensorId(String s) {
151         try {
152             return new SensorId(s);
153         } catch (IllegalArgumentException e) {
154             return null;
155         }
156     }
157
158     /**
159      * check sensor presence
160      *
161      * Errors are caught and interpreted as sensor not present.
162      *
163      * @param path full owfs path to sensor
164      * @return OnOffType, ON=present, OFF=not present
165      */
166     public State checkPresence(String path) {
167         State returnValue = OnOffType.OFF;
168         try {
169             OwserverPacket requestPacket;
170             requestPacket = new OwserverPacket(OwserverMessageType.PRESENT, path, OwserverControlFlag.UNCACHED);
171
172             OwserverPacket returnPacket = request(requestPacket);
173             if (returnPacket.getReturnCode() == 0) {
174                 returnValue = OnOffType.ON;
175             }
176
177         } catch (OwException ignored) {
178         }
179         logger.trace("presence {} : {}", path, returnValue);
180         return returnValue;
181     }
182
183     /**
184      * read a decimal type
185      *
186      * @param path full owfs path to sensor
187      * @return DecimalType if successful
188      * @throws OwException in case an error occurs
189      */
190     public State readDecimalType(String path) throws OwException {
191         State returnState = UnDefType.UNDEF;
192         OwserverPacket requestPacket = new OwserverPacket(OwserverMessageType.READ, path);
193
194         OwserverPacket returnPacket = request(requestPacket);
195         if ((returnPacket.getReturnCode() != -1) && returnPacket.hasPayload()) {
196             try {
197                 returnState = DecimalType.valueOf(returnPacket.getPayloadString().trim());
198             } catch (NumberFormatException e) {
199                 throw new OwException("could not parse '" + returnPacket.getPayloadString().trim() + "' to a number");
200             }
201         } else {
202             throw new OwException("invalid or empty packet when requesting decimal type");
203         }
204
205         return returnState;
206     }
207
208     /**
209      * read a decimal type array
210      *
211      * @param path full owfs path to sensor
212      * @return a List of DecimalType values if successful
213      * @throws OwException in case an error occurs
214      */
215     public List<State> readDecimalTypeArray(String path) throws OwException {
216         List<State> returnList = new ArrayList<>();
217         OwserverPacket requestPacket = new OwserverPacket(OwserverMessageType.READ, path);
218         OwserverPacket returnPacket = request(requestPacket);
219         if ((returnPacket.getReturnCode() != -1) && returnPacket.hasPayload()) {
220             Arrays.stream(returnPacket.getPayloadString().split(","))
221                     .forEach(v -> returnList.add(DecimalType.valueOf(v.trim())));
222         } else {
223             throw new OwException("invalid or empty packet when requesting decimal type array");
224         }
225
226         return returnList;
227     }
228
229     /**
230      * read a string
231      *
232      * @param path full owfs path to sensor
233      * @return requested String
234      * @throws OwException in case an error occurs
235      */
236     public String readString(String path) throws OwException {
237         OwserverPacket requestPacket = new OwserverPacket(OwserverMessageType.READ, path);
238         OwserverPacket returnPacket = request(requestPacket);
239
240         if ((returnPacket.getReturnCode() != -1) && returnPacket.hasPayload()) {
241             return returnPacket.getPayloadString().trim();
242         } else {
243             throw new OwException("invalid or empty packet when requesting string type");
244         }
245     }
246
247     /**
248      * read all sensor pages
249      *
250      * @param path full owfs path to sensor
251      * @return page buffer
252      * @throws OwException in case an error occurs
253      */
254     public OwPageBuffer readPages(String path) throws OwException {
255         OwserverPacket requestPacket = new OwserverPacket(OwserverMessageType.READ, path + "/pages/page.ALL");
256         OwserverPacket returnPacket = request(requestPacket);
257         if ((returnPacket.getReturnCode() != -1) && returnPacket.hasPayload()) {
258             return returnPacket.getPayload();
259         } else {
260             throw new OwException("invalid or empty packet when requesting pages");
261         }
262     }
263
264     /**
265      * write a DecimalType
266      *
267      * @param path full owfs path to the sensor
268      * @param value the value to write
269      * @throws OwException in case an error occurs
270      */
271     public void writeDecimalType(String path, DecimalType value) throws OwException {
272         OwserverPacket requestPacket = new OwserverPacket(OwserverMessageType.WRITE, path);
273         requestPacket.appendPayload(String.valueOf(value));
274
275         // request method throws an OwException in case of issues...
276         OwserverPacket returnPacket = request(requestPacket);
277
278         logger.trace("wrote: {}, got: {} ", requestPacket, returnPacket);
279     }
280
281     /**
282      * process a request to the owserver
283      *
284      * @param requestPacket the request to be send
285      * @return the raw owserver answer
286      * @throws OwException in case an error occurs
287      */
288     private OwserverPacket request(OwserverPacket requestPacket) throws OwException {
289         OwserverPacket returnPacket = new OwserverPacket(OwserverPacketType.RETURN);
290
291         // answer to value write is always empty
292         boolean payloadExpected = requestPacket.getMessageType() != OwserverMessageType.WRITE;
293
294         try {
295             // write request - error may be thrown
296             write(requestPacket);
297
298             // try to read data as long as we don't get any feedback and no error is thrown...
299             do {
300                 if (requestPacket.getMessageType() == OwserverMessageType.PRESENT
301                         || requestPacket.getMessageType() == OwserverMessageType.NOP) {
302                     returnPacket = read(true);
303                 } else {
304                     returnPacket = read(false);
305                 }
306             } while (returnPacket.isPingPacket() || returnPacket.hasPayload() != payloadExpected);
307
308         } catch (OwException e) {
309             logger.debug("failed requesting {}->{} [{}]", requestPacket, returnPacket, e.getMessage());
310             throw e;
311         }
312
313         if (!returnPacket.hasControlFlag(OwserverControlFlag.PERSISTENCE)) {
314             logger.trace("closing connection because persistence was denied");
315             close();
316         }
317
318         // Success! Reset error counter.
319         connectionErrorCounter = 0;
320         return returnPacket;
321     }
322
323     /**
324      * open/reopen the connection to the owserver
325      *
326      * In case of issues, the connection is closed using {@link #closeOnError()} and false is returned.
327      * If the {@link #owserverConnectionState} is in STOPPED or FAILED, the method directly returns false.
328      *
329      * @return true if open
330      */
331     private boolean open() {
332         try {
333             if (owserverConnectionState == OwserverConnectionState.CLOSED || tryingConnectionRecovery) {
334                 // open socket & set timeout to 3000ms
335                 final Socket owserverSocket = new Socket(owserverAddress, owserverPort);
336                 owserverSocket.setSoTimeout(3000);
337                 this.owserverSocket = owserverSocket;
338
339                 owserverInputStream = new DataInputStream(owserverSocket.getInputStream());
340                 owserverOutputStream = new DataOutputStream(owserverSocket.getOutputStream());
341
342                 owserverConnectionState = OwserverConnectionState.OPENED;
343                 thingHandlerCallback.reportConnectionState(owserverConnectionState);
344
345                 logger.debug("OW connection state: opened to {}:{}", owserverAddress, owserverPort);
346                 return true;
347             } else if (owserverConnectionState == OwserverConnectionState.OPENED) {
348                 // socket already open, clear input buffer
349                 logger.trace("owServerConnection already open, skipping input buffer");
350                 final DataInputStream owserverInputStream = this.owserverInputStream;
351                 while (owserverInputStream != null) {
352                     if (owserverInputStream.skip(owserverInputStream.available()) == 0) {
353                         return true;
354                     }
355                 }
356                 logger.debug("input stream not available on skipping");
357                 closeOnError();
358                 return false;
359             } else {
360                 return false;
361             }
362         } catch (IOException e) {
363             logger.debug("could not open owServerConnection to {}:{}: {}", owserverAddress, owserverPort,
364                     e.getMessage());
365             closeOnError();
366             return false;
367         }
368     }
369
370     /**
371      * close connection and report connection state to callback
372      */
373     private void close() {
374         this.close(true);
375     }
376
377     /**
378      * close the connection to the owserver instance.
379      *
380      * @param reportConnectionState true, if connection state shall be reported to callback
381      */
382     private void close(boolean reportConnectionState) {
383         final Socket owserverSocket = this.owserverSocket;
384         if (owserverSocket != null) {
385             try {
386                 owserverSocket.close();
387                 owserverConnectionState = OwserverConnectionState.CLOSED;
388                 logger.debug("closed connection");
389             } catch (IOException e) {
390                 owserverConnectionState = OwserverConnectionState.FAILED;
391                 logger.warn("could not close connection: {}", e.getMessage());
392             }
393         }
394
395         this.owserverSocket = null;
396         this.owserverInputStream = null;
397         this.owserverOutputStream = null;
398
399         if (reportConnectionState) {
400             thingHandlerCallback.reportConnectionState(owserverConnectionState);
401         }
402     }
403
404     /**
405      * check if the connection is dead and close it
406      */
407     private void checkConnection() {
408         try {
409             int pid = ((DecimalType) readDecimalType("/system/process/pid")).intValue();
410             logger.debug("read pid {} -> connection still alive", pid);
411             return;
412         } catch (OwException e) {
413             closeOnError();
414         }
415     }
416
417     /**
418      * close the connection to the owserver instance after an error occured.
419      * if {@link #CONNECTION_MAX_RETRY} is exceeded, {@link #owserverConnectionState} is set to FAILED
420      * and state is reported to callback.
421      */
422     private void closeOnError() {
423         connectionErrorCounter++;
424         close(false);
425         if (connectionErrorCounter > CONNECTION_MAX_RETRY) {
426             logger.debug("OW connection state: set to failed as max retries exceeded.");
427             owserverConnectionState = OwserverConnectionState.FAILED;
428             tryingConnectionRecovery = false;
429             thingHandlerCallback.reportConnectionState(owserverConnectionState);
430         } else if (!tryingConnectionRecovery) {
431             // as close did not report connections state and we are not trying to recover ...
432             thingHandlerCallback.reportConnectionState(owserverConnectionState);
433         }
434     }
435
436     /**
437      * write to the owserver
438      *
439      * In case of issues, the connection is closed using {@link #closeOnError()} and an
440      * {@link OwException} is thrown.
441      *
442      * @param requestPacket data to write
443      * @throws OwException in case an error occurs
444      */
445     private void write(OwserverPacket requestPacket) throws OwException {
446         try {
447             if (open()) {
448                 requestPacket.setControlFlags(OwserverControlFlag.PERSISTENCE);
449                 final DataOutputStream owserverOutputStream = this.owserverOutputStream;
450                 if (owserverOutputStream != null) {
451                     owserverOutputStream.write(requestPacket.toBytes());
452                     logger.trace("wrote: {}", requestPacket);
453                 } else {
454                     logger.debug("output stream not available on write");
455                     closeOnError();
456                     throw new OwException("I/O Error: output stream not available on write");
457                 }
458             } else {
459                 // was not opened
460                 throw new OwException("I/O error: could not open connection to send request packet");
461             }
462         } catch (IOException e) {
463             closeOnError();
464             logger.debug("couldn't send {}, {}", requestPacket, e.getMessage());
465             throw new OwException("I/O Error: exception while sending request packet - " + e.getMessage());
466         }
467     }
468
469     /**
470      * read from owserver
471      *
472      * In case of errors (which may also be due to an erroneous path), the connection is checked and potentially closed
473      * using {@link #checkConnection()}.
474      *
475      * @param noTimeoutException retry in case of read time outs instead of exiting with an {@link OwException}.
476      * @return the read packet
477      * @throws OwException in case an error occurs
478      */
479     private OwserverPacket read(boolean noTimeoutException) throws OwException {
480         OwserverPacket returnPacket = new OwserverPacket(OwserverPacketType.RETURN);
481         final DataInputStream owserverInputStream = this.owserverInputStream;
482         if (owserverInputStream != null) {
483             DataInputStream inputStream = owserverInputStream;
484             try {
485                 returnPacket = new OwserverPacket(inputStream, OwserverPacketType.RETURN);
486             } catch (EOFException e) {
487                 // Read suddenly ended ....
488                 logger.warn("EOFException: exception while reading packet - {}", e.getMessage());
489                 checkConnection();
490                 throw new OwException("EOFException: exception while reading packet - " + e.getMessage());
491             } catch (OwException e) {
492                 // Some other issue
493                 checkConnection();
494                 throw e;
495             } catch (IOException e) {
496                 // Read time out
497                 if ("Read timed out".equals(e.getMessage()) && noTimeoutException) {
498                     logger.trace("timeout - setting error code to -1");
499                     // will lead to re-try reading in request method!!!
500                     returnPacket.setPayload("timeout");
501                     returnPacket.setReturnCode(-1);
502                 } else {
503                     // Other I/O issue
504                     checkConnection();
505                     throw new OwException("I/O error: exception while reading packet - " + e.getMessage());
506                 }
507             }
508             logger.trace("read: {}", returnPacket);
509         } else {
510             logger.debug("input stream not available on read");
511             closeOnError();
512             throw new OwException("I/O Error: input stream not available on read");
513         }
514
515         return returnPacket;
516     }
517 }