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 e) {
178 returnValue = OnOffType.OFF;
180 logger.trace("presence {} : {}", path, returnValue);
185 * read a decimal type
187 * @param path full owfs path to sensor
188 * @return DecimalType if successful
189 * @throws OwException
191 public State readDecimalType(String path) throws OwException {
192 State returnState = UnDefType.UNDEF;
193 OwserverPacket requestPacket = new OwserverPacket(OwserverMessageType.READ, path);
195 OwserverPacket returnPacket = request(requestPacket);
196 if ((returnPacket.getReturnCode() != -1) && returnPacket.hasPayload()) {
198 returnState = DecimalType.valueOf(returnPacket.getPayloadString().trim());
199 } catch (NumberFormatException e) {
200 throw new OwException("could not parse '" + returnPacket.getPayloadString().trim() + "' to a number");
203 throw new OwException("invalid or empty packet when requesting decimal type");
210 * read a decimal type array
212 * @param path full owfs path to sensor
213 * @return a List of DecimalType values if successful
214 * @throws OwException
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())));
224 throw new OwException("invalid or empty packet when requesting decimal type array");
233 * @param path full owfs path to sensor
234 * @return requested String
235 * @throws OwException
237 public String readString(String path) throws OwException {
238 OwserverPacket requestPacket = new OwserverPacket(OwserverMessageType.READ, path);
239 OwserverPacket returnPacket = request(requestPacket);
241 if ((returnPacket.getReturnCode() != -1) && returnPacket.hasPayload()) {
242 return returnPacket.getPayloadString().trim();
244 throw new OwException("invalid or empty packet when requesting string type");
249 * read all sensor pages
251 * @param path full owfs path to sensor
252 * @return page buffer
253 * @throws OwException
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();
261 throw new OwException("invalid or empty packet when requesting pages");
266 * write a DecimalType
268 * @param path full owfs path to the sensor
269 * @param value the value to write
270 * @throws OwException
272 public void writeDecimalType(String path, DecimalType value) throws OwException {
273 OwserverPacket requestPacket = new OwserverPacket(OwserverMessageType.WRITE, path);
274 requestPacket.appendPayload(String.valueOf(value));
276 // request method throws an OwException in case of issues...
277 OwserverPacket returnPacket = request(requestPacket);
279 logger.trace("wrote: {}, got: {} ", requestPacket, returnPacket);
283 * process a request to the owserver
285 * @param requestPacket the request to be send
286 * @return the raw owserver answer
287 * @throws OwException
289 private OwserverPacket request(OwserverPacket requestPacket) throws OwException {
290 OwserverPacket returnPacket = new OwserverPacket(OwserverPacketType.RETURN);
292 // answer to value write is always empty
293 boolean payloadExpected = requestPacket.getMessageType() != OwserverMessageType.WRITE;
296 // write request - error may be thrown
297 write(requestPacket);
299 // try to read data as long as we don't get any feedback and no error is thrown...
301 if (requestPacket.getMessageType() == OwserverMessageType.PRESENT
302 || requestPacket.getMessageType() == OwserverMessageType.NOP) {
303 returnPacket = read(true);
305 returnPacket = read(false);
307 } while (returnPacket.isPingPacket() || !(returnPacket.hasPayload() == payloadExpected));
309 } catch (OwException e) {
310 logger.debug("failed requesting {}->{} [{}]", requestPacket, returnPacket, e.getMessage());
314 if (!returnPacket.hasControlFlag(OwserverControlFlag.PERSISTENCE)) {
315 logger.trace("closing connection because persistence was denied");
319 // Success! Reset error counter.
320 connectionErrorCounter = 0;
325 * open/reopen the connection to the owserver
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.
330 * @return true if open
332 private boolean open() {
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;
340 owserverInputStream = new DataInputStream(owserverSocket.getInputStream());
341 owserverOutputStream = new DataOutputStream(owserverSocket.getOutputStream());
343 owserverConnectionState = OwserverConnectionState.OPENED;
344 thingHandlerCallback.reportConnectionState(owserverConnectionState);
346 logger.debug("OW connection state: opened to {}:{}", owserverAddress, owserverPort);
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) {
357 logger.debug("input stream not available on skipping");
363 } catch (IOException e) {
364 logger.debug("could not open owServerConnection to {}:{}: {}", owserverAddress, owserverPort,
372 * close connection and report connection state to callback
374 private void close() {
379 * close the connection to the owserver instance.
381 * @param reportConnectionState true, if connection state shall be reported to callback
383 private void close(boolean reportConnectionState) {
384 final Socket owserverSocket = this.owserverSocket;
385 if (owserverSocket != null) {
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());
396 this.owserverSocket = null;
397 this.owserverInputStream = null;
398 this.owserverOutputStream = null;
400 if (reportConnectionState) {
401 thingHandlerCallback.reportConnectionState(owserverConnectionState);
406 * check if the connection is dead and close it
408 private void checkConnection() {
410 int pid = ((DecimalType) readDecimalType("/system/process/pid")).intValue();
411 logger.debug("read pid {} -> connection still alive", pid);
413 } catch (OwException e) {
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.
423 private void closeOnError() {
424 connectionErrorCounter++;
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);
438 * write to the owserver
440 * In case of issues, the connection is closed using {@link #closeOnError()} and an
441 * {@link OwException} is thrown.
443 * @param requestPacket data to write
444 * @throws OwException
446 private void write(OwserverPacket requestPacket) throws OwException {
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);
455 logger.debug("output stream not available on write");
457 throw new OwException("I/O Error: output stream not available on write");
461 throw new OwException("I/O error: could not open connection to send request packet");
463 } catch (IOException e) {
465 logger.debug("couldn't send {}, {}", requestPacket, e.getMessage());
466 throw new OwException("I/O Error: exception while sending request packet - " + e.getMessage());
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()}.
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
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;
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());
491 throw new OwException("EOFException: exception while reading packet - " + e.getMessage());
492 } catch (OwException e) {
496 } catch (IOException e) {
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);
506 throw new OwException("I/O error: exception while reading packet - " + e.getMessage());
509 logger.trace("read: {}", returnPacket);
511 logger.debug("input stream not available on read");
513 throw new OwException("I/O Error: input stream not available on read");