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
91 * @throws InvalidFrameException
93 public synchronized @Nullable Frame readNextFrame() throws InvalidFrameException, IOException {
94 // seek the next header frame
95 while (!isHeaderFrame(groupLine)) {
96 groupLine = bufferedReader.readLine();
97 if (logger.isTraceEnabled()) {
98 logger.trace("groupLine = {}", groupLine);
100 if (groupLine == null) { // end of stream
101 logger.trace("end of stream reached !");
107 while ((groupLine = bufferedReader.readLine()) != null && !isHeaderFrame(groupLine)) {
108 logger.trace("groupLine = {}", groupLine);
109 String groupLineRef = groupLine;
110 if (groupLineRef != null) {
111 String[] groupLineTokens = groupLineRef.split(ticMode.getSeparator());
112 if (ticMode == TeleinfoTicMode.HISTORICAL && groupLineTokens.length != 2 && groupLineTokens.length != 3
113 || ticMode == TeleinfoTicMode.STANDARD && groupLineTokens.length != 3
114 && groupLineTokens.length != 4) {
115 final String error = String.format("The groupLine '%1$s' is incomplete", groupLineRef);
116 throw new InvalidFrameException(error);
118 String labelStr = groupLineTokens[0];
120 String timestampString = null;
123 valueString = groupLineTokens[1];
126 if (groupLineTokens.length == 3) {
127 valueString = groupLineTokens[1];
129 timestampString = groupLineTokens[1];
130 valueString = groupLineTokens[2];
135 // verify integrity (through checksum)
136 if (verifyChecksum) {
137 char checksum = groupLineRef.charAt(groupLineRef.length() - 1);
138 char computedChecksum = FrameUtil
139 .computeGroupLineChecksum(groupLineRef.substring(0, groupLineRef.length() - 2), ticMode);
140 if (computedChecksum != checksum) {
141 logger.trace("computedChecksum = {}", computedChecksum);
142 logger.trace("checksum = {}", checksum);
143 final String error = String.format(
144 "The groupLine '%s' is corrupted (integrity not checked). Actual checksum: '%s' / Expected checksum: '%s'",
145 groupLineRef, checksum, computedChecksum);
146 throw new InvalidFrameException(error);
152 label = Label.getEnum(labelStr);
153 } catch (IllegalArgumentException e) {
154 if (autoRepairInvalidADPSgroupLine && labelStr.startsWith(Label.ADPS.name())) {
155 // in this hardware issue, label variable is composed by label name and value. E.g:
157 logger.warn("Try to auto repair malformed ADPS groupLine '{}'", labelStr);
159 valueString = labelStr.substring(Label.ADPS.name().length());
161 final String error = String.format("The label '%s' is unknown", labelStr);
162 throw new InvalidFrameException(error);
166 frame.put(label, valueString);
167 if (timestampString != null) {
168 frame.putTimestamp(label, timestampString);
176 public boolean isAutoRepairInvalidADPSgroupLine() {
177 return autoRepairInvalidADPSgroupLine;
180 public void setAutoRepairInvalidADPSgroupLine(boolean autoRepairInvalidADPSgroupLine) {
181 this.autoRepairInvalidADPSgroupLine = autoRepairInvalidADPSgroupLine;
185 public int read() throws IOException {
186 throw new UnsupportedOperationException("The 'read()' is not supported");
189 public static boolean isHeaderFrame(final @Nullable String line) {
190 // A new teleinfo trame begin with '3' and '2' bytes (END OF TEXT et START OF TEXT)
191 return (line != null && line.length() > 1 && line.codePointAt(0) == 3 && line.codePointAt(1) == 2);