]> git.basschouten.com Git - openhab-addons.git/blob
f5ee5e02306c8fb1c2777b376e35ddea864fb768
[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 e) {
178             returnValue = OnOffType.OFF;
179         }
180         logger.trace("presence {} : {}", path, returnValue);
181         return returnValue;
182     }
183
184     /**
185      * read a decimal type
186      *
187      * @param path full owfs path to sensor
188      * @return DecimalType if successful
189      * @throws OwException
190      */
191     public State readDecimalType(String path) throws OwException {
192         State returnState = UnDefType.UNDEF;
193         OwserverPacket requestPacket = new OwserverPacket(OwserverMessageType.READ, path);
194
195         OwserverPacket returnPacket = request(requestPacket);
196         if ((returnPacket.getReturnCode() != -1) && returnPacket.hasPayload()) {
197             try {
198                 returnState = DecimalType.valueOf(returnPacket.getPayloadString().trim());
199             } catch (NumberFormatException e) {
200                 throw new OwException("could not parse '" + returnPacket.getPayloadString().trim() + "' to a number");
201             }
202         } else {
203             throw new OwException("invalid or empty packet when requesting decimal type");
204         }
205
206         return returnState;
207     }
208
209     /**
210      * read a decimal type array
211      *
212      * @param path full owfs path to sensor
213      * @return a List of DecimalType values if successful
214      * @throws OwException
215      */
216     public List<State> readDecimalTypeArray(String path) throws OwException {
217         List<State> returnList = new ArrayList<>();
218         OwserverPacket requestPacket = new OwserverPacket(OwserverMessageType.READ, path);
219         OwserverPacket returnPacket = request(requestPacket);
220         if ((returnPacket.getReturnCode() != -1) && returnPacket.hasPayload()) {
221             Arrays.stream(returnPacket.getPayloadString().split(","))
222                     .forEach(v -> returnList.add(DecimalType.valueOf(v.trim())));
223         } else {
224             throw new OwException("invalid or empty packet when requesting decimal type array");
225         }
226
227         return returnList;
228     }
229
230     /**
231      * read a string
232      *
233      * @param path full owfs path to sensor
234      * @return requested String
235      * @throws OwException
236      */
237     public String readString(String path) throws OwException {
238         OwserverPacket requestPacket = new OwserverPacket(OwserverMessageType.READ, path);
239         OwserverPacket returnPacket = request(requestPacket);
240
241         if ((returnPacket.getReturnCode() != -1) && returnPacket.hasPayload()) {
242             return returnPacket.getPayloadString().trim();
243         } else {
244             throw new OwException("invalid or empty packet when requesting string type");
245         }
246     }
247
248     /**
249      * read all sensor pages
250      *
251      * @param path full owfs path to sensor
252      * @return page buffer
253      * @throws OwException
254      */
255     public OwPageBuffer readPages(String path) throws OwException {
256         OwserverPacket requestPacket = new OwserverPacket(OwserverMessageType.READ, path + "/pages/page.ALL");
257         OwserverPacket returnPacket = request(requestPacket);
258         if ((returnPacket.getReturnCode() != -1) && returnPacket.hasPayload()) {
259             return returnPacket.getPayload();
260         } else {
261             throw new OwException("invalid or empty packet when requesting pages");
262         }
263     }
264
265     /**
266      * write a DecimalType
267      *
268      * @param path full owfs path to the sensor
269      * @param value the value to write
270      * @throws OwException
271      */
272     public void writeDecimalType(String path, DecimalType value) throws OwException {
273         OwserverPacket requestPacket = new OwserverPacket(OwserverMessageType.WRITE, path);
274         requestPacket.appendPayload(String.valueOf(value));
275
276         // request method throws an OwException in case of issues...
277         OwserverPacket returnPacket = request(requestPacket);
278
279         logger.trace("wrote: {}, got: {} ", requestPacket, returnPacket);
280     }
281
282     /**
283      * process a request to the owserver
284      *
285      * @param requestPacket the request to be send
286      * @return the raw owserver answer
287      * @throws OwException
288      */
289     private OwserverPacket request(OwserverPacket requestPacket) throws OwException {
290         OwserverPacket returnPacket = new OwserverPacket(OwserverPacketType.RETURN);
291
292         // answer to value write is always empty
293         boolean payloadExpected = requestPacket.getMessageType() != OwserverMessageType.WRITE;
294
295         try {
296             // write request - error may be thrown
297             write(requestPacket);
298
299             // try to read data as long as we don't get any feedback and no error is thrown...
300             do {
301                 if (requestPacket.getMessageType() == OwserverMessageType.PRESENT
302                         || requestPacket.getMessageType() == OwserverMessageType.NOP) {
303                     returnPacket = read(true);
304                 } else {
305                     returnPacket = read(false);
306                 }
307             } while (returnPacket.isPingPacket() || !(returnPacket.hasPayload() == payloadExpected));
308
309         } catch (OwException e) {
310             logger.debug("failed requesting {}->{} [{}]", requestPacket, returnPacket, e.getMessage());
311             throw e;
312         }
313
314         if (!returnPacket.hasControlFlag(OwserverControlFlag.PERSISTENCE)) {
315             logger.trace("closing connection because persistence was denied");
316             close();
317         }
318
319         // Success! Reset error counter.
320         connectionErrorCounter = 0;
321         return returnPacket;
322     }
323
324     /**
325      * open/reopen the connection to the owserver
326      *
327      * In case of issues, the connection is closed using {@link #closeOnError()} and false is returned.
328      * If the {@link #owserverConnectionState} is in STOPPED or FAILED, the method directly returns false.
329      *
330      * @return true if open
331      */
332     private boolean open() {
333         try {
334             if (owserverConnectionState == OwserverConnectionState.CLOSED || tryingConnectionRecovery) {
335                 // open socket & set timeout to 3000ms
336                 final Socket owserverSocket = new Socket(owserverAddress, owserverPort);
337                 owserverSocket.setSoTimeout(3000);
338                 this.owserverSocket = owserverSocket;
339
340                 owserverInputStream = new DataInputStream(owserverSocket.getInputStream());
341                 owserverOutputStream = new DataOutputStream(owserverSocket.getOutputStream());
342
343                 owserverConnectionState = OwserverConnectionState.OPENED;
344                 thingHandlerCallback.reportConnectionState(owserverConnectionState);
345
346                 logger.debug("OW connection state: opened to {}:{}", owserverAddress, owserverPort);
347                 return true;
348             } else if (owserverConnectionState == OwserverConnectionState.OPENED) {
349                 // socket already open, clear input buffer
350                 logger.trace("owServerConnection already open, skipping input buffer");
351                 final DataInputStream owserverInputStream = this.owserverInputStream;
352                 while (owserverInputStream != null) {
353                     if (owserverInputStream.skip(owserverInputStream.available()) == 0) {
354                         return true;
355                     }
356                 }
357                 logger.debug("input stream not available on skipping");
358                 closeOnError();
359                 return false;
360             } else {
361                 return false;
362             }
363         } catch (IOException e) {
364             logger.debug("could not open owServerConnection to {}:{}: {}", owserverAddress, owserverPort,
365                     e.getMessage());
366             closeOnError();
367             return false;
368         }
369     }
370
371     /**
372      * close connection and report connection state to callback
373      */
374     private void close() {
375         this.close(true);
376     }
377
378     /**
379      * close the connection to the owserver instance.
380      *
381      * @param reportConnectionState true, if connection state shall be reported to callback
382      */
383     private void close(boolean reportConnectionState) {
384         final Socket owserverSocket = this.owserverSocket;
385         if (owserverSocket != null) {
386             try {
387                 owserverSocket.close();
388                 owserverConnectionState = OwserverConnectionState.CLOSED;
389                 logger.debug("closed connection");
390             } catch (IOException e) {
391                 owserverConnectionState = OwserverConnectionState.FAILED;
392                 logger.warn("could not close connection: {}", e.getMessage());
393             }
394         }
395
396         this.owserverSocket = null;
397         this.owserverInputStream = null;
398         this.owserverOutputStream = null;
399
400         if (reportConnectionState) {
401             thingHandlerCallback.reportConnectionState(owserverConnectionState);
402         }
403     }
404
405     /**
406      * check if the connection is dead and close it
407      */
408     private void checkConnection() {
409         try {
410             int pid = ((DecimalType) readDecimalType("/system/process/pid")).intValue();
411             logger.debug("read pid {} -> connection still alive", pid);
412             return;
413         } catch (OwException e) {
414             closeOnError();
415         }
416     }
417
418     /**
419      * close the connection to the owserver instance after an error occured.
420      * if {@link #CONNECTION_MAX_RETRY} is exceeded, {@link #owserverConnectionState} is set to FAILED
421      * and state is reported to callback.
422      */
423     private void closeOnError() {
424         connectionErrorCounter++;
425         close(false);
426         if (connectionErrorCounter > CONNECTION_MAX_RETRY) {
427             logger.debug("OW connection state: set to failed as max retries exceeded.");
428             owserverConnectionState = OwserverConnectionState.FAILED;
429             tryingConnectionRecovery = false;
430             thingHandlerCallback.reportConnectionState(owserverConnectionState);
431         } else if (!tryingConnectionRecovery) {
432             // as close did not report connections state and we are not trying to recover ...
433             thingHandlerCallback.reportConnectionState(owserverConnectionState);
434         }
435     }
436
437     /**
438      * write to the owserver
439      *
440      * In case of issues, the connection is closed using {@link #closeOnError()} and an
441      * {@link OwException} is thrown.
442      *
443      * @param requestPacket data to write
444      * @throws OwException
445      */
446     private void write(OwserverPacket requestPacket) throws OwException {
447         try {
448             if (open()) {
449                 requestPacket.setControlFlags(OwserverControlFlag.PERSISTENCE);
450                 final DataOutputStream owserverOutputStream = this.owserverOutputStream;
451                 if (owserverOutputStream != null) {
452                     owserverOutputStream.write(requestPacket.toBytes());
453                     logger.trace("wrote: {}", requestPacket);
454                 } else {
455                     logger.debug("output stream not available on write");
456                     closeOnError();
457                     throw new OwException("I/O Error: output stream not available on write");
458                 }
459             } else {
460                 // was not opened
461                 throw new OwException("I/O error: could not open connection to send request packet");
462             }
463         } catch (IOException e) {
464             closeOnError();
465             logger.debug("couldn't send {}, {}", requestPacket, e.getMessage());
466             throw new OwException("I/O Error: exception while sending request packet - " + e.getMessage());
467         }
468     }
469
470     /**
471      * read from owserver
472      *
473      * In case of errors (which may also be due to an erroneous path), the connection is checked and potentially closed
474      * using {@link #checkConnection()}.
475      *
476      * @param noTimeoutException retry in case of read time outs instead of exiting with an {@link OwException}.
477      * @return the read packet
478      * @throws OwException
479      */
480     private OwserverPacket read(boolean noTimeoutException) throws OwException {
481         OwserverPacket returnPacket = new OwserverPacket(OwserverPacketType.RETURN);
482         final DataInputStream owserverInputStream = this.owserverInputStream;
483         if (owserverInputStream != null) {
484             DataInputStream inputStream = owserverInputStream;
485             try {
486                 returnPacket = new OwserverPacket(inputStream, OwserverPacketType.RETURN);
487             } catch (EOFException e) {
488                 // Read suddenly ended ....
489                 logger.warn("EOFException: exception while reading packet - {}", e.getMessage());
490                 checkConnection();
491                 throw new OwException("EOFException: exception while reading packet - " + e.getMessage());
492             } catch (OwException e) {
493                 // Some other issue
494                 checkConnection();
495                 throw e;
496             } catch (IOException e) {
497                 // Read time out
498                 if (e.getMessage().equals("Read timed out") && noTimeoutException) {
499                     logger.trace("timeout - setting error code to -1");
500                     // will lead to re-try reading in request method!!!
501                     returnPacket.setPayload("timeout");
502                     returnPacket.setReturnCode(-1);
503                 } else {
504                     // Other I/O issue
505                     checkConnection();
506                     throw new OwException("I/O error: exception while reading packet - " + e.getMessage());
507                 }
508             }
509             logger.trace("read: {}", returnPacket);
510         } else {
511             logger.debug("input stream not available on read");
512             closeOnError();
513             throw new OwException("I/O Error: input stream not available on read");
514         }
515
516         return returnPacket;
517     }
518 }