Skip to content

Commit

Permalink
[solarman] Added LSE-3 (LAN Stick Logger) Support (#17559)
Browse files Browse the repository at this point in the history
Raw LAN Modbus for LSE-3
Use raw Modbus for LAN Stick an V5 for Wifi Stick. Unchecked for Wifi V5 Protocol, Checked for LAN Raw Protocol.

Signed-off-by: Peter Kretz peter.kretz@kretz-net.de
  • Loading branch information
kretzp committed Oct 13, 2024
1 parent 39a2754 commit 362b029
Show file tree
Hide file tree
Showing 10 changed files with 398 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -31,19 +31,21 @@ public class SolarmanLoggerConfiguration {
public String serialNumber = "";
public String inverterType = "sg04lp3";
public int refreshInterval = 30;
public boolean rawLanMode = false;
@Nullable
public String additionalRequests;

public SolarmanLoggerConfiguration() {
}

public SolarmanLoggerConfiguration(String hostname, Integer port, String serialNumber, String inverterType,
int refreshInterval, @Nullable String additionalRequests) {
int refreshInterval, boolean rawLanMode, @Nullable String additionalRequests) {
this.hostname = hostname;
this.port = port;
this.serialNumber = serialNumber;
this.inverterType = inverterType;
this.refreshInterval = refreshInterval;
this.rawLanMode = rawLanMode;
this.additionalRequests = additionalRequests;
}

Expand All @@ -67,6 +69,10 @@ public int getRefreshInterval() {
return refreshInterval;
}

public boolean getRawLanMode() {
return rawLanMode;
}

@Nullable
public String getAdditionalRequests() {
return additionalRequests;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,9 @@
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.ISolarmanProtocol;
import org.openhab.binding.solarman.internal.modbus.SolarmanLoggerConnector;
import org.openhab.binding.solarman.internal.modbus.SolarmanV5Protocol;
import org.openhab.binding.solarman.internal.modbus.SolarmanProtocolFactory;
import org.openhab.binding.solarman.internal.updater.SolarmanChannelUpdater;
import org.openhab.binding.solarman.internal.updater.SolarmanProcessResult;
import org.openhab.core.thing.Channel;
Expand Down Expand Up @@ -94,7 +95,12 @@ public void initialize() {
logger.debug("Found definition for {}", config.inverterType);
}
}
SolarmanV5Protocol solarmanV5Protocol = new SolarmanV5Protocol(config);

if (logger.isDebugEnabled()) {
logger.debug("Raw Type {}", config.rawLanMode);
}

ISolarmanProtocol solarmanProtocol = SolarmanProtocolFactory.CreateSolarmanProtocol(config);

String additionalRequests = Objects.requireNonNullElse(config.getAdditionalRequests(), "");

Expand All @@ -110,17 +116,17 @@ public void initialize() {

scheduledFuture = scheduler
.scheduleWithFixedDelay(
() -> queryLoggerAndUpdateState(solarmanLoggerConnector, solarmanV5Protocol, mergedRequests,
() -> queryLoggerAndUpdateState(solarmanLoggerConnector, solarmanProtocol, mergedRequests,
paramToChannelMapping, solarmanChannelUpdater),
0, config.refreshInterval, TimeUnit.SECONDS);
}

private void queryLoggerAndUpdateState(SolarmanLoggerConnector solarmanLoggerConnector,
SolarmanV5Protocol solarmanV5Protocol, List<Request> mergedRequests,
ISolarmanProtocol solarmanProtocol, List<Request> mergedRequests,
Map<ParameterItem, ChannelUID> paramToChannelMapping, SolarmanChannelUpdater solarmanChannelUpdater) {
try {
SolarmanProcessResult solarmanProcessResult = solarmanChannelUpdater.fetchDataFromLogger(mergedRequests,
solarmanLoggerConnector, solarmanV5Protocol, paramToChannelMapping);
solarmanLoggerConnector, solarmanProtocol, paramToChannelMapping);

if (solarmanProcessResult.hasSuccessfulResponses()) {
updateStatus(ThingStatus.ONLINE);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.solarman.internal.modbus;

import java.util.Map;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.solarman.internal.modbus.exception.SolarmanException;

/**
* @author Peter Kretz - Initial contribution
*/
@NonNullByDefault
public interface ISolarmanProtocol {

Map<Integer, byte[]> readRegisters(SolarmanLoggerConnection solarmanLoggerConnection, byte mbFunctionCode,
int firstReg, int lastReg) throws SolarmanException;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.solarman.internal.modbus;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.solarman.internal.SolarmanLoggerConfiguration;

/**
* @author Peter Kretz - Added RAW Modbus for LAN Stick
*/
@NonNullByDefault
public class SolarmanProtocolFactory {

public static ISolarmanProtocol CreateSolarmanProtocol(SolarmanLoggerConfiguration solarmanLoggerConfiguration) {
if (solarmanLoggerConfiguration.getRawLanMode()) {
return new SolarmanRawProtocol(solarmanLoggerConfiguration);
} else {
return new SolarmanV5Protocol(solarmanLoggerConfiguration);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.solarman.internal.modbus;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.solarman.internal.SolarmanLoggerConfiguration;
import org.openhab.binding.solarman.internal.modbus.exception.SolarmanConnectionException;
import org.openhab.binding.solarman.internal.modbus.exception.SolarmanException;
import org.openhab.binding.solarman.internal.modbus.exception.SolarmanProtocolException;

/**
* @author Catalin Sanda - Initial contribution
* @author Peter Kretz - Added RAW Modbus for LAN Stick
*/
@NonNullByDefault
public class SolarmanRawProtocol implements ISolarmanProtocol {
private final SolarmanLoggerConfiguration solarmanLoggerConfiguration;

public SolarmanRawProtocol(SolarmanLoggerConfiguration solarmanLoggerConfiguration) {
this.solarmanLoggerConfiguration = solarmanLoggerConfiguration;
}

public Map<Integer, byte[]> readRegisters(SolarmanLoggerConnection solarmanLoggerConnection, byte mbFunctionCode,
int firstReg, int lastReg) throws SolarmanException {
byte[] solarmanRawFrame = buildSolarmanRawFrame(mbFunctionCode, firstReg, lastReg);
byte[] respFrame = solarmanLoggerConnection.sendRequest(solarmanRawFrame);
if (respFrame.length > 0) {
byte[] modbusRespFrame = extractModbusRawResponseFrame(respFrame, solarmanRawFrame);
return parseRawModbusReadHoldingRegistersResponse(modbusRespFrame, firstReg, lastReg);
} else {
throw new SolarmanConnectionException("Response frame was empty");
}
}

protected byte[] extractModbusRawResponseFrame(byte @Nullable [] responseFrame, byte[] requestFrame)
throws SolarmanException {
if (responseFrame == null || responseFrame.length == 0) {
throw new SolarmanProtocolException("No response frame");
} else if (responseFrame.length < 13) {
throw new SolarmanProtocolException("Response frame is too short");
} else if (responseFrame[0] != (byte) 0x03) {
throw new SolarmanProtocolException("Response frame has invalid starting byte");
}

return Arrays.copyOfRange(responseFrame, 6, responseFrame.length);
}

protected Map<Integer, byte[]> parseRawModbusReadHoldingRegistersResponse(byte @Nullable [] frame, int firstReg,
int lastReg) throws SolarmanProtocolException {
int regCount = lastReg - firstReg + 1;
Map<Integer, byte[]> registers = new HashMap<>();
int expectedFrameDataLen = 2 + 1 + regCount * 2;
if (frame == null || frame.length < expectedFrameDataLen) {
throw new SolarmanProtocolException("Modbus frame is too short or empty");
}

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() };
registers.put(i + firstReg, array);
}

return registers;
}

/**
* Builds a SolarMAN Raw frame to request data from firstReg to lastReg.
* Frame format is based on
* <a href="https://github.com/StephanJoubert/home_assistant_solarman/issues/247">Solarman RAW Protocol</a>
* Request send:
* Header 03e8: Transaction identifier
* Header 0000: Protocol identifier
* Header 0006: Message length (w/o CRC)
* Payload 01: Slave ID
* Payload 03: Read function
* Payload 0003: 1st register address
* Payload 006e: Nb of registers to read
* Trailer 3426: CRC-16 ModBus
*
* @param mbFunctionCode
* @param firstReg - the start register
* @param lastReg - the end register
* @return byte array containing the Solarman Raw frame
*/
protected byte[] buildSolarmanRawFrame(byte mbFunctionCode, int firstReg, int lastReg) {
byte[] requestPayload = buildSolarmanRawFrameRequestPayload(mbFunctionCode, firstReg, lastReg);
byte[] header = buildSolarmanRawFrameHeader(requestPayload.length);

return ByteBuffer.allocate(header.length + requestPayload.length).put(header).put(requestPayload).array();
}

/**
* Builds a SolarMAN Raw frame Header
* Frame format is based on
* <a href="https://github.com/StephanJoubert/home_assistant_solarman/issues/247">Solarman RAW Protocol</a>
* Request send:
* Header 03e8: Transaction identifier
* Header 0000: Protocol identifier
* Header 0006: Message length (w/o CRC)
*
* @param payloadSize th
* @return byte array containing the Solarman Raw frame header
*/
private byte[] buildSolarmanRawFrameHeader(int payloadSize) {
// (two byte) Denotes the start of the Raw frame. Always 0x03 0xE8.
byte[] transactionId = new byte[] { (byte) 0x03, (byte) 0xE8 };

// (two bytes) – Always 0x00 0x00
byte[] protocolId = new byte[] { (byte) 0x00, (byte) 0x00 };

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

// Append all fields into the header
return ByteBuffer.allocate(transactionId.length + protocolId.length + messageLength.length).put(transactionId)
.put(protocolId).put(messageLength).array();
}

/**
* Builds a SolarMAN Raw frame payload
* Frame format is based on
* <a href="https://github.com/StephanJoubert/home_assistant_solarman/issues/247">Solarman RAW Protocol</a>
* Request send:
* Payload 01: Slave ID
* Payload 03: Read function
* Payload 0003: 1st register address
* Payload 006e: Nb of registers to read
* Trailer 3426: CRC-16 ModBus
*
* @param mbFunctionCode
* @param firstReg - the start register
* @param lastReg - the end register
* @return byte array containing the Solarman Raw frame payload
*/
protected byte[] buildSolarmanRawFrameRequestPayload(byte mbFunctionCode, int firstReg, int lastReg) {
int regCount = lastReg - firstReg + 1;
byte[] req = ByteBuffer.allocate(6).put((byte) 0x01).put(mbFunctionCode).putShort((short) firstReg)
.putShort((short) regCount).array();
byte[] crc = ByteBuffer.allocate(Short.BYTES).order(ByteOrder.LITTLE_ENDIAN)
.putShort((short) CRC16Modbus.calculate(req)).array();

return ByteBuffer.allocate(req.length + crc.length).put(req).put(crc).array();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,14 @@
* @author Catalin Sanda - Initial contribution
*/
@NonNullByDefault
public class SolarmanV5Protocol {
public class SolarmanV5Protocol implements ISolarmanProtocol {
private final SolarmanLoggerConfiguration solarmanLoggerConfiguration;

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

@Override
public Map<Integer, byte[]> readRegisters(SolarmanLoggerConnection solarmanLoggerConnection, byte mbFunctionCode,
int firstReg, int lastReg) throws SolarmanException {
byte[] solarmanV5Frame = buildSolarmanV5Frame(mbFunctionCode, firstReg, lastReg);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,9 @@
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.solarman.internal.defmodel.ParameterItem;
import org.openhab.binding.solarman.internal.defmodel.Request;
import org.openhab.binding.solarman.internal.modbus.ISolarmanProtocol;
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.modbus.exception.SolarmanConnectionException;
import org.openhab.binding.solarman.internal.modbus.exception.SolarmanException;
import org.openhab.binding.solarman.internal.typeprovider.ChannelUtils;
Expand All @@ -64,7 +64,7 @@ public SolarmanChannelUpdater(StateUpdater stateUpdater) {
}

public SolarmanProcessResult fetchDataFromLogger(List<Request> requests,
SolarmanLoggerConnector solarmanLoggerConnector, SolarmanV5Protocol solarmanV5Protocol,
SolarmanLoggerConnector solarmanLoggerConnector, ISolarmanProtocol solarmanProtocol,
Map<ParameterItem, ChannelUID> paramToChannelMapping) {
try (SolarmanLoggerConnection solarmanLoggerConnection = solarmanLoggerConnector.createConnection()) {
logger.debug("Fetching data from logger");
Expand All @@ -77,7 +77,7 @@ public SolarmanProcessResult fetchDataFromLogger(List<Request> requests,
SolarmanProcessResult solarmanProcessResult = requests.stream().map(request -> {
try {
return SolarmanProcessResult.ofValue(request,
solarmanV5Protocol.readRegisters(solarmanLoggerConnection,
solarmanProtocol.readRegisters(solarmanLoggerConnection,
(byte) request.getMbFunctioncode().intValue(), request.getStart(),
request.getEnd()));
} catch (SolarmanException e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,13 @@
<default>60</default>
<advanced>true</advanced>
</parameter>
<parameter name="rawLanMode" type="boolean" required="false">
<label>Raw LAN Modbus</label>
<description>Use raw Modbus for LAN Stick LSE-3 and V5 for Wifi Stick. Unchecked for Wifi V5 Protocol, Checked for LAN Raw
Protocol.</description>
<default>false</default>
<advanced>true</advanced>
</parameter>
<parameter name="additionalRequests" type="text" required="false">
<label>Additional Requests</label>
<description>Additional requests besides the ones defined in the inverter definition.
Expand Down
Loading

0 comments on commit 362b029

Please sign in to comment.