Skip to content
This repository has been archived by the owner on Jan 25, 2025. It is now read-only.

Commit

Permalink
Detect & log a mismatch between the request and the response logger IDs
Browse files Browse the repository at this point in the history
  • Loading branch information
catalinsanda committed Aug 26, 2023
1 parent 1714fad commit 2517c2e
Show file tree
Hide file tree
Showing 5 changed files with 66 additions and 73 deletions.
4 changes: 2 additions & 2 deletions src/main/history/dependencies.xml
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<features xmlns="http://karaf.apache.org/xmlns/features/v1.6.0" name="org.openhab.binding.solarman-0.2.5-SNAPSHOT">
<features xmlns="http://karaf.apache.org/xmlns/features/v1.6.0" name="org.openhab.binding.solarman-0.3.2-SNAPSHOT">
<feature version="0.0.0">
<feature>openhab-runtime-base</feature>
<feature>wrap</feature>
<bundle>mvn:org.openhab.addons.bundles/org.openhab.binding.solarman/0.2.5-SNAPSHOT</bundle>
<bundle>mvn:org.openhab.addons.bundles/org.openhab.binding.solarman/0.3.2-SNAPSHOT</bundle>
<bundle>wrap:mvn:org.lastnpe.eea/eea-all/2.2.1</bundle>
</feature>
</features>
Original file line number Diff line number Diff line change
Expand Up @@ -12,28 +12,15 @@
*/
package org.openhab.binding.solarman.internal;

import java.math.BigDecimal;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.*;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;

import javax.measure.Unit;
import javax.measure.format.MeasurementParseException;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
Expand All @@ -43,34 +30,17 @@
import org.openhab.binding.solarman.internal.defmodel.ParameterItem;
import org.openhab.binding.solarman.internal.defmodel.Request;
import org.openhab.binding.solarman.internal.defmodel.Validation;
import org.openhab.binding.solarman.internal.modbus.SolarmanLoggerConnection;
import org.openhab.binding.solarman.internal.modbus.SolarmanLoggerConnector;
import org.openhab.binding.solarman.internal.modbus.SolarmanV5Protocol;
import org.openhab.binding.solarman.internal.typeprovider.ChannelUtils;
import org.openhab.binding.solarman.internal.updater.SolarmanChannelUpdater;
import org.openhab.binding.solarman.internal.util.StreamUtils;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.thing.*;
import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.thing.binding.builder.ChannelBuilder;
import org.openhab.core.thing.binding.builder.ThingBuilder;
import org.openhab.core.thing.type.ChannelGroupDefinition;
import org.openhab.core.thing.type.ChannelGroupTypeUID;
import org.openhab.core.thing.type.ChannelKind;
import org.openhab.core.types.Command;
import org.openhab.core.types.State;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import tech.units.indriya.format.SimpleUnitFormat;

import static org.openhab.binding.solarman.internal.SolarmanBindingConstants.DYNAMIC_CHANNEL;
import static org.openhab.binding.solarman.internal.typeprovider.ChannelUtils.escapeName;
import static org.openhab.binding.solarman.internal.util.StreamUtils.reverse;

/**
* The {@link SolarmanLoggerHandler} is responsible for handling commands, which are
Expand Down Expand Up @@ -129,7 +99,7 @@ public void initialize() {
}
}
SolarmanLoggerConnector solarmanV5Connector = new SolarmanLoggerConnector(config);
SolarmanV5Protocol solarmanV5Protocol = new SolarmanV5Protocol(config, solarmanV5Connector);
SolarmanV5Protocol solarmanV5Protocol = new SolarmanV5Protocol(config);

List<Request> mergedRequests = (StringUtils.isNotEmpty(config.getAdditionalRequests())) ?
mergeRequests(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package org.openhab.binding.solarman.internal.modbus;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Arrays;
Expand All @@ -19,19 +18,16 @@
public class SolarmanV5Protocol {
private final static Logger LOGGER = LoggerFactory.getLogger(SolarmanLoggerHandler.class);
private final SolarmanLoggerConfiguration solarmanLoggerConfiguration;
private final SolarmanLoggerConnector solarmanV5Connector;

public SolarmanV5Protocol(SolarmanLoggerConfiguration solarmanLoggerConfiguration,
SolarmanLoggerConnector solarmanV5Connector) {
public SolarmanV5Protocol(SolarmanLoggerConfiguration solarmanLoggerConfiguration) {
this.solarmanLoggerConfiguration = solarmanLoggerConfiguration;
this.solarmanV5Connector = solarmanV5Connector;
}

public Map<Integer, byte[]> readRegisters(SolarmanLoggerConnection solarmanLoggerConnection, byte mbFunctionCode, int firstReg, int lastReg) {
byte[] solarmanV5Frame = buildSolarmanV5Frame(mbFunctionCode, firstReg, lastReg);
byte[] respFrame = solarmanLoggerConnection.sendRequest(solarmanV5Frame);
if (respFrame.length > 0) {
byte[] modbusRespFrame = extractModbusResponseFrame(respFrame);
byte[] modbusRespFrame = extractModbusResponseFrame(respFrame, solarmanV5Frame);
return parseModbusReadHoldingRegistersResponse(modbusRespFrame, firstReg, lastReg);
} else {
return Collections.emptyMap();
Expand All @@ -44,8 +40,8 @@ public Map<Integer, byte[]> readRegisters(SolarmanLoggerConnection solarmanLogge
* <a href="https://pysolarmanv5.readthedocs.io/en/latest/solarmanv5_protocol.html">Solarman V5 Protocol</a>
*
* @param mbFunctionCode
* @param firstReg - the start register
* @param lastReg - the end register
* @param firstReg - the start register
* @param lastReg - the end register
* @return byte array containing the Solarman V5 frame
*/
protected byte[] buildSolarmanV5Frame(byte mbFunctionCode, int firstReg, int lastReg) {
Expand All @@ -64,11 +60,11 @@ private byte[] buildSolarmanV5FrameTrailer(byte[] header, byte[] requestPayload)
// Checksum (obviously!) and End.
// Note, that this field is completely separate to the Modbus RTU checksum, which coincidentally, is the two
// bytes immediately preceding this field.
byte[] checksum = new byte[] {
computeChecksum(Arrays.copyOfRange(headerAndPayload, 1, headerAndPayload.length)) };
byte[] checksum = new byte[]{
computeChecksum(Arrays.copyOfRange(headerAndPayload, 1, headerAndPayload.length))};

// (one byte) – Denotes the end of the V5 frame. Always 0x15.
byte[] end = new byte[] { (byte) 0x15 };
byte[] end = new byte[]{(byte) 0x15};

return ByteBuffer.allocate(checksum.length + end.length).put(checksum).put(end).array();
}
Expand All @@ -85,23 +81,23 @@ private byte computeChecksum(byte[] frame) {

private byte[] buildSolarmanV5FrameHeader(int payloadSize) {
// (one byte) Denotes the start of the V5 frame. Always 0xA5.
byte[] start = new byte[] { (byte) 0xA5 };
byte[] start = new byte[]{(byte) 0xA5};

// (two bytes) Payload length
byte[] length = ByteBuffer.allocate(Short.BYTES).order(ByteOrder.LITTLE_ENDIAN).putShort((short) payloadSize)
.array();

// (two bytes) – Describes the type of V5 frame. For Modbus RTU requests, the control code is 0x4510. For Modbus
// RTU responses, the control code is 0x1510.
byte[] controlCode = new byte[] { (byte) 0x10, (byte) 0x45 };
byte[] controlCode = new byte[]{(byte) 0x10, (byte) 0x45};

// (two bytes) – This field acts as a two-way sequence number. On outgoing requests, the first byte of this
// field is echoed back in the same position on incoming responses.
// This is done by initialising this byte to a random value, and incrementing for each subsequent request.
// The second byte is incremented by the data logging stick for every response sent (either to Solarman Cloud or
// local requests).
// @TODO the increment part
byte[] serial = new byte[] { (byte) 0x00, (byte) 0x00 };
byte[] serial = new byte[]{(byte) 0x00, (byte) 0x00};

// (four bytes) – Serial number of Solarman data logging stick
byte[] loggerSerial = ByteBuffer.allocate(Integer.BYTES).order(ByteOrder.LITTLE_ENDIAN)
Expand All @@ -115,16 +111,16 @@ private byte[] buildSolarmanV5FrameHeader(int payloadSize) {

protected byte[] buildSolarmanV5FrameRequestPayload(byte mbFunctionCode, int firstReg, int lastReg) {
// (one byte) – Denotes the frame type.
byte[] frameType = new byte[] { 0x02 };
byte[] frameType = new byte[]{0x02};
// (two bytes) – Denotes the sensor type.
byte[] sensorType = new byte[] { 0x00, 0x00 };
byte[] sensorType = new byte[]{0x00, 0x00};
// (four bytes) – Denotes the frame total working time. See corresponding response field of same name for
// further details.
byte[] totalWorkingTime = new byte[] { 0x00, 0x00, 0x00, 0x00 };
byte[] totalWorkingTime = new byte[]{0x00, 0x00, 0x00, 0x00};
// (four bytes) – Denotes the frame power on time.
byte[] powerOnTime = new byte[] { 0x00, 0x00, 0x00, 0x00 };
byte[] powerOnTime = new byte[]{0x00, 0x00, 0x00, 0x00};
// Denotes the frame offset time.
byte[] offsetTime = new byte[] { 0x00, 0x00, 0x00, 0x00 };
byte[] offsetTime = new byte[]{0x00, 0x00, 0x00, 0x00};
// (variable length) – Modbus RTU request frame.
byte[] requestFrame = buildModbusReadHoldingRegistersRequestFrame((byte) 0x01, mbFunctionCode, firstReg,
lastReg);
Expand All @@ -140,14 +136,14 @@ protected byte[] buildSolarmanV5FrameRequestPayload(byte mbFunctionCode, int fir
* Based on <a href="https://www.modbustools.com/modbus.html#function03">Function 03 (03hex) Read Holding
* Registers</a>
*
* @param slaveId - Slave Address
* @param slaveId - Slave Address
* @param mbFunctionCode -
* @param firstReg - Starting Address
* @param lastReg - Ending Address
* @param firstReg - Starting Address
* @param lastReg - Ending Address
* @return byte array containing the Modbus request frame
*/
protected byte[] buildModbusReadHoldingRegistersRequestFrame(byte slaveId, byte mbFunctionCode, int firstReg,
int lastReg) {
int lastReg) {
int regCount = lastReg - firstReg + 1;
byte[] req = ByteBuffer.allocate(6).put(slaveId).put(mbFunctionCode).putShort((short) firstReg)
.putShort((short) regCount).array();
Expand All @@ -171,49 +167,62 @@ protected Map<Integer, byte[]> parseModbusReadHoldingRegistersResponse(byte[] fr
int expectedCrc = CRC16Modbus.calculate(Arrays.copyOfRange(frame, 0, expectedFrameDataLen));

if (actualCrc != expectedCrc) {
LOGGER.error(
String.format("Modbus frame crc is not valid. Expected %04x, got %04x", expectedCrc, actualCrc));
LOGGER.error(String.format("Modbus frame crc is not valid. Expected %04x, got %04x", expectedCrc, actualCrc));
return registers;
}

for (int i = 0; i < regCount; i++) {
int p1 = 3 + (i * 2);
ByteBuffer order = ByteBuffer.wrap(frame, p1, 2).order(ByteOrder.BIG_ENDIAN);
byte[] array = new byte[] { order.get(), order.get() };
byte[] array = new byte[]{order.get(), order.get()};
registers.put(i + firstReg, array);
}

return registers;
}

protected byte[] extractModbusResponseFrame(byte[] frame) {
if (frame == null) {
protected byte[] extractModbusResponseFrame(byte[] responseFrame, byte[] requestFrame) {
if (responseFrame == null) {
LOGGER.error("No response frame");
return null;
} else if (frame.length == 29) {
parseResponseErrorCode(frame);
} else if (responseFrame.length == 29) {
parseResponseErrorCode(responseFrame, requestFrame);
return null;
} else if (frame.length < (29 + 4)) {
} else if (responseFrame.length < (29 + 4)) {
LOGGER.error("Response frame is too short");
return null;
} else if (frame[0] != (byte) 0xA5) {
} else if (responseFrame[0] != (byte) 0xA5) {
LOGGER.error("Response frame has invalid starting byte");
return null;
} else if (frame[frame.length - 1] != (byte) 0x15) {
} else if (responseFrame[responseFrame.length - 1] != (byte) 0x15) {
LOGGER.error("Response frame has invalid ending byte");
return null;
}

return Arrays.copyOfRange(frame, 25, frame.length - 2);
return Arrays.copyOfRange(responseFrame, 25, responseFrame.length - 2);
}

protected void parseResponseErrorCode(byte[] frame) {
if (frame[1] != (byte) 0x10 || frame[2] != (byte) 0x45) {
protected void parseResponseErrorCode(byte[] responseFrame, byte[] requestFrame) {
if (responseFrame[0] == (byte) 0xA5 && responseFrame[1] == (byte) 0x10 &&
!Arrays.equals(Arrays.copyOfRange(responseFrame, 7, 11),
Arrays.copyOfRange(requestFrame, 7, 11))) {

String requestInverterId = parseInverterId(requestFrame);
String responseInverterId = parseInverterId(responseFrame);

LOGGER.error(String.format("There was a missmatch between the request logger ID: %s and the response logger ID: %s . " +
"Make sure you are using the logger ID and not the inverter ID. If in doubt, try the one in the response",
requestInverterId,
responseInverterId));
return;
}

if (responseFrame[1] != (byte) 0x10 || responseFrame[2] != (byte) 0x45) {
LOGGER.error("Unexpected control code in error response frame");
return;
}

int errorCode = frame[25];
int errorCode = responseFrame[25];
switch (errorCode) {
case 0x01 -> LOGGER.error("Error response frame: Illegal Function");
case 0x02 -> LOGGER.error("Error response frame: Illegal Data Address");
Expand All @@ -222,4 +231,10 @@ protected void parseResponseErrorCode(byte[] frame) {
default -> LOGGER.error(String.format("Error response frame: Unknown error code %02x", errorCode));
}
}

private static String parseInverterId(byte[] requestFrame) {
byte[] inverterIdBytes = Arrays.copyOfRange(requestFrame, 7, 11);
int inverterIdInt = ByteBuffer.wrap(inverterIdBytes).order(ByteOrder.LITTLE_ENDIAN).getInt();
return String.valueOf(inverterIdInt & 0x00000000ffffffffL);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,17 @@ public static void main(String[] args) {

System.out.print("Enter request frame: ");
String requestFrameString = scanner.nextLine();

System.out.print("Enter response frame: ");
String responseFrameString = scanner.nextLine();

byte[] requestFrame = convertHexToByteArray(requestFrameString);
byte[] responseFrame = convertHexToByteArray(responseFrameString);

Integer[] startEnd = parseStartEnd(requestFrame);
System.out.printf("Request was from: 0x%04X to 0x%04X%n", startEnd[0], startEnd[1]);
String requestInverterId = parseInverterId(requestFrame);
System.out.printf("Request was from: 0x%04X to 0x%04X for logger with ID: %s%n", startEnd[0], startEnd[1], requestInverterId);
String responseInverterId = parseInverterId(responseFrame);
System.out.printf("Response was from logger with ID: %s%n", responseInverterId);

byte[] responseFrameRegister = Arrays.copyOfRange(responseFrame, 25, responseFrame.length - 2);

Expand All @@ -32,6 +34,12 @@ public static void main(String[] args) {
}
}

private static String parseInverterId(byte[] requestFrame) {
byte[] inverterIdBytes = Arrays.copyOfRange(requestFrame, 7, 11);
int inverterIdInt = ByteBuffer.wrap(inverterIdBytes).order(ByteOrder.LITTLE_ENDIAN).getInt();
return String.valueOf(inverterIdInt & 0x00000000ffffffffL);
}

private static Integer[] parseStartEnd(byte[] requestFrame) {
int start = (requestFrame[28] << 8) + requestFrame[29];
int length = (requestFrame[30] << 8) + requestFrame[31];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ void setUp() {
SolarmanLoggerConfiguration loggerConfiguration = new SolarmanLoggerConfiguration("192.168.1.1", 8899,
"1234567890", "sg04lp3", 60, null);

solarmanV5Protocol = new SolarmanV5Protocol(loggerConfiguration, solarmanLoggerConnector);
solarmanV5Protocol = new SolarmanV5Protocol(loggerConfiguration);
}

@Test
Expand Down

0 comments on commit 2517c2e

Please sign in to comment.