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.teleinfo.internal.reader.io;
15 import java.io.BufferedReader;
16 import java.io.IOException;
17 import java.io.InputStream;
18 import java.io.InputStreamReader;
19 import java.nio.charset.StandardCharsets;
21 import org.eclipse.jdt.annotation.NonNullByDefault;
22 import org.eclipse.jdt.annotation.Nullable;
23 import org.openhab.binding.teleinfo.internal.data.Frame;
24 import org.openhab.binding.teleinfo.internal.reader.io.serialport.FrameUtil;
25 import org.openhab.binding.teleinfo.internal.reader.io.serialport.InvalidFrameException;
26 import org.openhab.binding.teleinfo.internal.reader.io.serialport.Label;
27 import org.openhab.binding.teleinfo.internal.serial.TeleinfoTicMode;
28 import org.slf4j.Logger;
29 import org.slf4j.LoggerFactory;
32 * InputStream for Teleinfo {@link Frame} in serial port format.
35 * The {@link TeleinfoInputStream} class is an {@link InputStream} to decode/read Teleinfo frames.
37 * @author Nicolas SIBERIL - Initial contribution
40 public class TeleinfoInputStream extends InputStream {
42 private final Logger logger = LoggerFactory.getLogger(TeleinfoInputStream.class);
44 private BufferedReader bufferedReader;
45 private @Nullable String groupLine;
46 private boolean autoRepairInvalidADPSgroupLine;
47 private final TeleinfoTicMode ticMode;
48 private final boolean verifyChecksum;
49 private final Frame frame = new Frame();
51 public TeleinfoInputStream(final InputStream teleinfoInputStream, TeleinfoTicMode ticMode) {
52 this(teleinfoInputStream, false, ticMode, true);
55 public TeleinfoInputStream(final InputStream teleinfoInputStream, boolean autoRepairInvalidADPSgroupLine,
56 TeleinfoTicMode ticMode) {
57 this(teleinfoInputStream, autoRepairInvalidADPSgroupLine, ticMode, true);
60 public TeleinfoInputStream(final InputStream teleinfoInputStream, TeleinfoTicMode ticMode, boolean verifyChecksum) {
61 this(teleinfoInputStream, false, ticMode, verifyChecksum);
64 public TeleinfoInputStream(final @Nullable InputStream teleinfoInputStream, boolean autoRepairInvalidADPSgroupLine,
65 TeleinfoTicMode ticMode, boolean verifyChecksum) {
66 if (teleinfoInputStream == null) {
67 throw new IllegalArgumentException("Teleinfo inputStream is null");
70 this.autoRepairInvalidADPSgroupLine = autoRepairInvalidADPSgroupLine;
71 this.ticMode = ticMode;
72 this.verifyChecksum = verifyChecksum;
73 this.bufferedReader = new BufferedReader(new InputStreamReader(teleinfoInputStream, StandardCharsets.US_ASCII));
79 public void close() throws IOException {
80 logger.debug("close() [start]");
81 bufferedReader.close();
83 logger.debug("close() [end]");
87 * Returns the next frame.
89 * @return the next frame or null if end of stream
90 * @throws TimeoutException
92 * @throws InvalidFrameException
95 public synchronized @Nullable Frame readNextFrame() throws InvalidFrameException, IOException {
96 // seek the next header frame
97 while (!isHeaderFrame(groupLine)) {
98 groupLine = bufferedReader.readLine();
99 if (logger.isTraceEnabled()) {
100 logger.trace("groupLine = {}", groupLine);
102 if (groupLine == null) { // end of stream
103 logger.trace("end of stream reached !");
109 while ((groupLine = bufferedReader.readLine()) != null && !isHeaderFrame(groupLine)) {
110 logger.trace("groupLine = {}", groupLine);
111 String groupLineRef = groupLine;
112 if (groupLineRef != null) {
113 String[] groupLineTokens = groupLineRef.split(ticMode.getSeparator());
114 if (ticMode == TeleinfoTicMode.HISTORICAL && groupLineTokens.length != 2 && groupLineTokens.length != 3
115 || ticMode == TeleinfoTicMode.STANDARD && groupLineTokens.length != 3
116 && groupLineTokens.length != 4) {
117 final String error = String.format("The groupLine '%1$s' is incomplete", groupLineRef);
118 throw new InvalidFrameException(error);
120 String labelStr = groupLineTokens[0];
122 String timestampString = null;
125 valueString = groupLineTokens[1];
128 if (groupLineTokens.length == 3) {
129 valueString = groupLineTokens[1];
131 timestampString = groupLineTokens[1];
132 valueString = groupLineTokens[2];
137 // verify integrity (through checksum)
138 if (verifyChecksum) {
139 char checksum = groupLineRef.charAt(groupLineRef.length() - 1);
140 char computedChecksum = FrameUtil
141 .computeGroupLineChecksum(groupLineRef.substring(0, groupLineRef.length() - 2), ticMode);
142 if (computedChecksum != checksum) {
143 logger.trace("computedChecksum = {}", computedChecksum);
144 logger.trace("checksum = {}", checksum);
145 final String error = String.format(
146 "The groupLine '%s' is corrupted (integrity not checked). Actual checksum: '%s' / Expected checksum: '%s'",
147 groupLineRef, checksum, computedChecksum);
148 throw new InvalidFrameException(error);
154 label = Label.getEnum(labelStr);
155 } catch (IllegalArgumentException e) {
156 if (autoRepairInvalidADPSgroupLine && labelStr.startsWith(Label.ADPS.name())) {
157 // in this hardware issue, label variable is composed by label name and value. E.g:
159 logger.warn("Try to auto repair malformed ADPS groupLine '{}'", labelStr);
161 valueString = labelStr.substring(Label.ADPS.name().length());
163 final String error = String.format("The label '%s' is unknown", labelStr);
164 throw new InvalidFrameException(error);
168 frame.put(label, valueString);
169 if (timestampString != null) {
170 frame.putTimestamp(label, timestampString);
178 public boolean isAutoRepairInvalidADPSgroupLine() {
179 return autoRepairInvalidADPSgroupLine;
182 public void setAutoRepairInvalidADPSgroupLine(boolean autoRepairInvalidADPSgroupLine) {
183 this.autoRepairInvalidADPSgroupLine = autoRepairInvalidADPSgroupLine;
187 public int read() throws IOException {
188 throw new UnsupportedOperationException("The 'read()' is not supported");
191 public static boolean isHeaderFrame(final @Nullable String line) {
192 // A new teleinfo trame begin with '3' and '2' bytes (END OF TEXT et START OF TEXT)
193 return (line != null && line.length() > 1 && line.codePointAt(0) == 3 && line.codePointAt(1) == 2);