2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.onewire.internal.owserver;
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;
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;
40 * The {@link OwserverConnection} defines the protocol for connections to owservers.
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.
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
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.
54 * @author Jan N. Klug - Initial contribution
58 public class OwserverConnection {
59 public static final int DEFAULT_PORT = 4304;
60 public static final int KEEPALIVE_INTERVAL = 1000;
62 private static final int CONNECTION_MAX_RETRY = 5;
64 private final Logger logger = LoggerFactory.getLogger(OwserverConnection.class);
66 private final OwserverBridgeHandler thingHandlerCallback;
67 private String owserverAddress = "";
68 private int owserverPort = DEFAULT_PORT;
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;
76 // reset to 0 after successful request
77 private int connectionErrorCounter = 0;
79 public OwserverConnection(OwserverBridgeHandler owBaseBridgeHandler) {
80 this.thingHandlerCallback = owBaseBridgeHandler;
84 * set the owserver host address
86 * @param address as String (IP or FQDN), defaults to localhost
88 public void setHost(String address) {
89 this.owserverAddress = address;
90 if (owserverConnectionState != OwserverConnectionState.STOPPED) {
96 * set the owserver port
98 * @param port defaults to 4304
100 public void setPort(int port) {
101 this.owserverPort = port;
102 if (owserverConnectionState != OwserverConnectionState.STOPPED) {
108 * start the owserver connection
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;
118 if (success && owserverConnectionState != OwserverConnectionState.FAILED) {
119 tryingConnectionRecovery = false;
121 } while (!success && (owserverConnectionState != OwserverConnectionState.FAILED || tryingConnectionRecovery));
125 * stop the owserver connection and report new {@link OwserverConnectionState} to {@link #thingHandlerCallback}.
129 owserverConnectionState = OwserverConnectionState.STOPPED;
130 thingHandlerCallback.reportConnectionState(owserverConnectionState);
134 * list all devices on this owserver
136 * @return a list of device ids
138 public @NonNullByDefault({}) List<SensorId> getDirectory(String basePath) throws OwException {
139 OwserverPacket requestPacket = new OwserverPacket(OwserverMessageType.DIRALL, basePath);
140 OwserverPacket returnPacket = request(requestPacket);
142 if ((returnPacket.getReturnCode() != -1) && returnPacket.hasPayload()) {
143 return Arrays.stream(returnPacket.getPayloadString().split(",")).map(this::stringToSensorId)
144 .filter(Objects::nonNull).collect(Collectors.toList());
146 throw new OwException("invalid of empty packet when requesting directory");
150 private @Nullable SensorId stringToSensorId(String s) {
152 return new SensorId(s);
153 } catch (IllegalArgumentException e) {
159 * check sensor presence
161 * Errors are caught and interpreted as sensor not present.
163 * @param path full owfs path to sensor
164 * @return OnOffType, ON=present, OFF=not present
166 public State checkPresence(String path) {
167 State returnValue = OnOffType.OFF;
169 OwserverPacket requestPacket;
170 requestPacket = new OwserverPacket(OwserverMessageType.PRESENT, path, OwserverControlFlag.UNCACHED);
172 OwserverPacket returnPacket = request(requestPacket);
173 if (returnPacket.getReturnCode() == 0) {
174 returnValue = OnOffType.ON;
177 } catch (OwException ignored) {
179 logger.trace("presence {} : {}", path, returnValue);
184 * read a decimal type
186 * @param path full owfs path to sensor
187 * @return DecimalType if successful
188 * @throws OwException in case an error occurs
190 public State readDecimalType(String path) throws OwException {
191 State returnState = UnDefType.UNDEF;
192 OwserverPacket requestPacket = new OwserverPacket(OwserverMessageType.READ, path);
194 OwserverPacket returnPacket = request(requestPacket);
195 if ((returnPacket.getReturnCode() != -1) && returnPacket.hasPayload()) {
197 returnState = DecimalType.valueOf(returnPacket.getPayloadString().trim());
198 } catch (NumberFormatException e) {
199 throw new OwException("could not parse '" + returnPacket.getPayloadString().trim() + "' to a number");
202 throw new OwException("invalid or empty packet when requesting decimal type");
209 * read a decimal type array
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
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())));
223 throw new OwException("invalid or empty packet when requesting decimal type array");
232 * @param path full owfs path to sensor
233 * @return requested String
234 * @throws OwException in case an error occurs
236 public String readString(String path) throws OwException {
237 OwserverPacket requestPacket = new OwserverPacket(OwserverMessageType.READ, path);
238 OwserverPacket returnPacket = request(requestPacket);
240 if ((returnPacket.getReturnCode() != -1) && returnPacket.hasPayload()) {
241 return returnPacket.getPayloadString().trim();
243 throw new OwException("invalid or empty packet when requesting string type");
248 * read all sensor pages
250 * @param path full owfs path to sensor
251 * @return page buffer
252 * @throws OwException in case an error occurs
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();
260 throw new OwException("invalid or empty packet when requesting pages");
265 * write a DecimalType
267 * @param path full owfs path to the sensor
268 * @param value the value to write
269 * @throws OwException in case an error occurs
271 public void writeDecimalType(String path, DecimalType value) throws OwException {
272 OwserverPacket requestPacket = new OwserverPacket(OwserverMessageType.WRITE, path);
273 requestPacket.appendPayload(String.valueOf(value));
275 // request method throws an OwException in case of issues...
276 OwserverPacket returnPacket = request(requestPacket);
278 logger.trace("wrote: {}, got: {} ", requestPacket, returnPacket);
282 * process a request to the owserver
284 * @param requestPacket the request to be send
285 * @return the raw owserver answer
286 * @throws OwException in case an error occurs
288 private OwserverPacket request(OwserverPacket requestPacket) throws OwException {
289 OwserverPacket returnPacket = new OwserverPacket(OwserverPacketType.RETURN);
291 // answer to value write is always empty
292 boolean payloadExpected = requestPacket.getMessageType() != OwserverMessageType.WRITE;
295 // write request - error may be thrown
296 write(requestPacket);
298 // try to read data as long as we don't get any feedback and no error is thrown...
300 if (requestPacket.getMessageType() == OwserverMessageType.PRESENT
301 || requestPacket.getMessageType() == OwserverMessageType.NOP) {
302 returnPacket = read(true);
304 returnPacket = read(false);
306 } while (returnPacket.isPingPacket() || !(returnPacket.hasPayload() == payloadExpected));
308 } catch (OwException e) {
309 logger.debug("failed requesting {}->{} [{}]", requestPacket, returnPacket, e.getMessage());
313 if (!returnPacket.hasControlFlag(OwserverControlFlag.PERSISTENCE)) {
314 logger.trace("closing connection because persistence was denied");
318 // Success! Reset error counter.
319 connectionErrorCounter = 0;
324 * open/reopen the connection to the owserver
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.
329 * @return true if open
331 private boolean open() {
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;
339 owserverInputStream = new DataInputStream(owserverSocket.getInputStream());
340 owserverOutputStream = new DataOutputStream(owserverSocket.getOutputStream());
342 owserverConnectionState = OwserverConnectionState.OPENED;
343 thingHandlerCallback.reportConnectionState(owserverConnectionState);
345 logger.debug("OW connection state: opened to {}:{}", owserverAddress, owserverPort);
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) {
356 logger.debug("input stream not available on skipping");
362 } catch (IOException e) {
363 logger.debug("could not open owServerConnection to {}:{}: {}", owserverAddress, owserverPort,
371 * close connection and report connection state to callback
373 private void close() {
378 * close the connection to the owserver instance.
380 * @param reportConnectionState true, if connection state shall be reported to callback
382 private void close(boolean reportConnectionState) {
383 final Socket owserverSocket = this.owserverSocket;
384 if (owserverSocket != null) {
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());
395 this.owserverSocket = null;
396 this.owserverInputStream = null;
397 this.owserverOutputStream = null;
399 if (reportConnectionState) {
400 thingHandlerCallback.reportConnectionState(owserverConnectionState);
405 * check if the connection is dead and close it
407 private void checkConnection() {
409 int pid = ((DecimalType) readDecimalType("/system/process/pid")).intValue();
410 logger.debug("read pid {} -> connection still alive", pid);
412 } catch (OwException e) {
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.
422 private void closeOnError() {
423 connectionErrorCounter++;
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);
437 * write to the owserver
439 * In case of issues, the connection is closed using {@link #closeOnError()} and an
440 * {@link OwException} is thrown.
442 * @param requestPacket data to write
443 * @throws OwException in case an error occurs
445 private void write(OwserverPacket requestPacket) throws OwException {
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);
454 logger.debug("output stream not available on write");
456 throw new OwException("I/O Error: output stream not available on write");
460 throw new OwException("I/O error: could not open connection to send request packet");
462 } catch (IOException e) {
464 logger.debug("couldn't send {}, {}", requestPacket, e.getMessage());
465 throw new OwException("I/O Error: exception while sending request packet - " + e.getMessage());
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()}.
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
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;
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());
490 throw new OwException("EOFException: exception while reading packet - " + e.getMessage());
491 } catch (OwException e) {
495 } catch (IOException e) {
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);
505 throw new OwException("I/O error: exception while reading packet - " + e.getMessage());
508 logger.trace("read: {}", returnPacket);
510 logger.debug("input stream not available on read");
512 throw new OwException("I/O Error: input stream not available on read");