[solarman] Add support for LSE-3 (LAN Stick Logger) (#17563)

* [solarman] Added LSE-3 (LAN Stick Logger) Support (#17559)

Signed-off-by: Peter Kretz <peter.kretz@kretz-net.de>
Signed-off-by: Ciprian Pascu <contact@ciprianpascu.ro>
This commit is contained in:
Peter Kretz 2024-11-11 22:49:57 +01:00 committed by Ciprian Pascu
parent aa13775a79
commit 92daa5d319
17 changed files with 541 additions and 37 deletions

View File

@ -15,7 +15,7 @@ These data loggers are used by inverters from a lot of manufacturers, just to na
The `solarman:logger` thing supports reading data from a Solarman LSW-3 Stick Logger (it might also work with LSE-3 and maybe others) when connected to a supported inverter.
It was tested on a SUN-12K-SG04LP3-EU only, but because the implementation uses the inverter definitions created as part of Stephan Joubert's Home Assistant plugin it **might** work with the other inverters supported by the plugin.
It was tested on a SUN-12K-SG04LP3-EU only, with LAN Stick LSE-3 in RAW MODBUS solarmanLoggerMode and Wifi Stick in V5 MODBUS solarmanLoggerMode but because the implementation uses the inverter definitions created as part of Stephan Joubert's Home Assistant plugin it **might** work with the other inverters supported by the plugin.
## Thing Configuration
@ -25,14 +25,15 @@ The IP address can be obtained from your router and the serial number can either
### `logger` Thing Configuration
| Name | Type | Description | Default | Required | Advanced |
|--------------------|---------|--------------------------------------------------------|---------|----------|----------|
| hostname | text | Hostname or IP address of the Solarman logger | N/A | yes | no |
| serialNumber | text | Serial number of the Solarman logger | N/A | yes | no |
| inverterType | text | The type of inverter connected to the logger | N/A | yes | no |
| port | integer | Port of the Solarman logger | 8899 | no | yes |
| refreshInterval | integer | Interval the device is polled in sec. | 60 | no | yes |
| additionalRequests | text | Additional requests besides the ones in the definition | N/A | no | yes |
| Name | Type | Description | Default | Required | Advanced |
|--------------------|---------|-------------------------------------------------------------------------------------------------------------------|-----------|----------|----------|
| hostname | text | Hostname or IP address of the Solarman logger | N/A | yes | no |
| serialNumber | text | Serial number of the Solarman logger | N/A | yes | no |
| inverterType | text | The type of inverter connected to the logger | N/A | yes | no |
| port | integer | Port of the Solarman logger | 8899 | no | yes |
| refreshInterval | integer | Interval the device is polled in sec. | 60 | no | yes |
| solarmanLoggerMode | option | RAW Modbus for LAN Stick LSE-3 and V5 MODBUS for most Wifi Sticks. If your Wifi stick uses Raw Modbus choose RAW. | V5 MODBUS | no | yes |
| additionalRequests | text | Additional requests besides the ones in the definition | N/A | no | yes |
The `inverterType` parameter governs what registers the binding will read from the logger and what channels it will expose.

View File

@ -31,6 +31,7 @@ public class SolarmanLoggerConfiguration {
public String serialNumber = "";
public String inverterType = "sg04lp3";
public int refreshInterval = 30;
public String solarmanLoggerMode = SolarmanLoggerMode.V5MODBUS.toString();
@Nullable
public String additionalRequests;
@ -38,12 +39,13 @@ public class SolarmanLoggerConfiguration {
}
public SolarmanLoggerConfiguration(String hostname, Integer port, String serialNumber, String inverterType,
int refreshInterval, @Nullable String additionalRequests) {
int refreshInterval, String solarmanLoggerMode, @Nullable String additionalRequests) {
this.hostname = hostname;
this.port = port;
this.serialNumber = serialNumber;
this.inverterType = inverterType;
this.refreshInterval = refreshInterval;
this.solarmanLoggerMode = solarmanLoggerMode;
this.additionalRequests = additionalRequests;
}
@ -67,6 +69,10 @@ public class SolarmanLoggerConfiguration {
return refreshInterval;
}
public SolarmanLoggerMode getSolarmanLoggerMode() {
return SolarmanLoggerMode.valueOf(solarmanLoggerMode);
}
@Nullable
public String getAdditionalRequests() {
return additionalRequests;

View File

@ -34,7 +34,8 @@ 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.SolarmanLoggerConnector;
import org.openhab.binding.solarman.internal.modbus.SolarmanV5Protocol;
import org.openhab.binding.solarman.internal.modbus.SolarmanProtocol;
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;
@ -94,7 +95,10 @@ public class SolarmanLoggerHandler extends BaseThingHandler {
logger.debug("Found definition for {}", config.inverterType);
}
}
SolarmanV5Protocol solarmanV5Protocol = new SolarmanV5Protocol(config);
logger.debug("Raw Type {}", config.solarmanLoggerMode);
SolarmanProtocol solarmanProtocol = SolarmanProtocolFactory.createSolarmanProtocol(config);
String additionalRequests = Objects.requireNonNullElse(config.getAdditionalRequests(), "");
@ -110,17 +114,17 @@ public class SolarmanLoggerHandler extends BaseThingHandler {
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,
SolarmanProtocol 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);
@ -149,7 +153,7 @@ public class SolarmanLoggerHandler extends BaseThingHandler {
}
return new AbstractMap.SimpleEntry<>(new ParameterItem(label, "N/A", "N/A", bcc.uom, bcc.scale, bcc.rule,
parseRegisters(bcc.registers), "N/A", new Validation(), bcc.offset, Boolean.FALSE),
parseRegisters(bcc.registers), "N/A", new Validation(), bcc.offset, Boolean.FALSE, null),
channel.getUID());
}).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
}

View File

@ -0,0 +1,21 @@
/**
* 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;
/**
* @author Peter Kretz - Initial contribution
*/
public enum SolarmanLoggerMode {
V5MODBUS,
RAWMODBUS
}

View File

@ -80,7 +80,13 @@ public class SolarmanChannelManager {
baseChannelConfig.scale = scale;
}
baseChannelConfig.rule = item.getRule();
if (item.hasLookup() || Boolean.TRUE.equals(item.getIsstr())) {
// Set 5 for Text (String), when isstr is true or Lookup is present
baseChannelConfig.rule = 5;
} else {
baseChannelConfig.rule = item.getRule();
}
baseChannelConfig.registers = convertRegisters(item.getRegisters());
baseChannelConfig.uom = item.getUom();

View File

@ -0,0 +1,43 @@
/**
* 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.defmodel;
import org.eclipse.jdt.annotation.NonNullByDefault;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
/**
* @author Peter Kretz - Initial contribution
*/
@JsonIgnoreProperties(ignoreUnknown = true)
@NonNullByDefault
public class Lookup {
private int key;
private String value = "";
public int getKey() {
return key;
}
public void setKey(int key) {
this.key = key;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
}

View File

@ -46,13 +46,15 @@ public class ParameterItem {
private BigDecimal offset;
@Nullable
private Boolean isstr;
private List<Lookup> lookup = new ArrayList<>();
public ParameterItem() {
}
public ParameterItem(String name, @Nullable String itemClass, @Nullable String stateClass, @Nullable String uom,
@Nullable BigDecimal scale, Integer rule, List<Integer> registers, @Nullable String icon,
@Nullable Validation validation, @Nullable BigDecimal offset, @Nullable Boolean isstr) {
@Nullable Validation validation, @Nullable BigDecimal offset, @Nullable Boolean isstr,
@Nullable List<Lookup> lookup) {
this.name = name;
this.itemClass = itemClass;
this.stateClass = stateClass;
@ -64,6 +66,9 @@ public class ParameterItem {
this.validation = validation;
this.offset = offset;
this.isstr = isstr;
if (lookup != null) {
this.lookup = lookup;
}
}
public String getName() {
@ -153,4 +158,16 @@ public class ParameterItem {
public void setItemClass(String itemClass) {
this.itemClass = itemClass;
}
public List<Lookup> getLookup() {
return lookup;
}
public void setLookup(List<Lookup> lookup) {
this.lookup = lookup;
}
public Boolean hasLookup() {
return !lookup.isEmpty();
}
}

View File

@ -92,7 +92,7 @@ public class SolarmanLoggerConnection implements AutoCloseable {
return new byte[0];
}
private static String bytesToHex(byte[] bytes) {
protected static String bytesToHex(byte[] bytes) {
return IntStream.range(0, bytes.length).mapToObj(i -> String.format("%02X", bytes[i]))
.collect(Collectors.joining());
}

View File

@ -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 SolarmanProtocol {
Map<Integer, byte[]> readRegisters(SolarmanLoggerConnection solarmanLoggerConnection, byte mbFunctionCode,
int firstReg, int lastReg) throws SolarmanException;
}

View File

@ -0,0 +1,34 @@
/**
* 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 SolarmanProtocol createSolarmanProtocol(SolarmanLoggerConfiguration solarmanLoggerConfiguration) {
switch (solarmanLoggerConfiguration.getSolarmanLoggerMode()) {
case RAWMODBUS: {
return new SolarmanRawProtocol(solarmanLoggerConfiguration);
}
case V5MODBUS:
default:
return new SolarmanV5Protocol(solarmanLoggerConfiguration);
}
}
}

View File

@ -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 SolarmanProtocol {
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 < 11) {
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();
}
}

View File

@ -30,13 +30,14 @@ import org.openhab.binding.solarman.internal.modbus.exception.SolarmanProtocolEx
* @author Catalin Sanda - Initial contribution
*/
@NonNullByDefault
public class SolarmanV5Protocol {
public class SolarmanV5Protocol implements SolarmanProtocol {
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);

View File

@ -14,6 +14,7 @@ package org.openhab.binding.solarman.internal.typeprovider;
import javax.measure.Unit;
import javax.measure.quantity.Dimensionless;
import javax.measure.quantity.ElectricCharge;
import javax.measure.quantity.ElectricCurrent;
import javax.measure.quantity.ElectricPotential;
import javax.measure.quantity.Energy;
@ -76,6 +77,7 @@ public class ChannelUtils {
private static String computeNumberType(String uom) {
return switch (uom.toUpperCase()) {
case "A" -> CoreItemFactory.NUMBER + ":" + ElectricCurrent.class.getSimpleName();
case "AH" -> CoreItemFactory.NUMBER + ":" + ElectricCharge.class.getSimpleName();
case "V" -> CoreItemFactory.NUMBER + ":" + ElectricPotential.class.getSimpleName();
case "°C" -> CoreItemFactory.NUMBER + ":" + Temperature.class.getSimpleName();
case "W", "KW", "VA", "KVA", "VAR", "KVAR" -> CoreItemFactory.NUMBER + ":" + Power.class.getSimpleName();
@ -96,6 +98,7 @@ public class ChannelUtils {
public static @Nullable Unit<?> getUnitFromDefinition(String uom) {
return switch (uom.toUpperCase()) {
case "A" -> Units.AMPERE;
case "AH" -> Units.AMPERE_HOUR;
case "V" -> Units.VOLT;
case "°C" -> SIUnits.CELSIUS;
case "W" -> Units.WATT;

View File

@ -33,11 +33,12 @@ import javax.measure.format.MeasurementParseException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.solarman.internal.defmodel.Lookup;
import org.openhab.binding.solarman.internal.defmodel.ParameterItem;
import org.openhab.binding.solarman.internal.defmodel.Request;
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.SolarmanProtocol;
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;
@ -64,7 +65,7 @@ public class SolarmanChannelUpdater {
}
public SolarmanProcessResult fetchDataFromLogger(List<Request> requests,
SolarmanLoggerConnector solarmanLoggerConnector, SolarmanV5Protocol solarmanV5Protocol,
SolarmanLoggerConnector solarmanLoggerConnector, SolarmanProtocol solarmanProtocol,
Map<ParameterItem, ChannelUID> paramToChannelMapping) {
try (SolarmanLoggerConnection solarmanLoggerConnection = solarmanLoggerConnector.createConnection()) {
logger.debug("Fetching data from logger");
@ -77,7 +78,7 @@ public class SolarmanChannelUpdater {
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) {
@ -120,6 +121,7 @@ public class SolarmanChannelUpdater {
.map(rawVal -> String.format("%02d", rawVal / 100) + ":" + String.format("%02d", rawVal % 100))
.collect(Collectors.joining());
logger.debug("Update state: channelUID: {}, state: {}", channelUID.getAsString(), stringValue);
stateUpdater.updateState(channelUID, new StringType(stringValue));
}
@ -143,6 +145,7 @@ public class SolarmanChannelUpdater {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yy/M/d H:m:s");
LocalDateTime dateTime = LocalDateTime.parse(stringValue, formatter);
logger.debug("Update state: channelUID: {}, state: {}", channelUID.getAsString(), dateTime.toString());
stateUpdater.updateState(channelUID, new DateTimeType(dateTime.atZone(ZoneId.systemDefault())));
} catch (DateTimeParseException e) {
logger.debug("Unable to parse string date {} to a DateTime object", stringValue);
@ -156,6 +159,7 @@ public class SolarmanChannelUpdater {
+ (rawVal & 0x0F))
.collect(Collectors.joining());
logger.debug("Update Version state: channelUID: {}, state: {}", channelUID.getAsString(), stringValue);
stateUpdater.updateState(channelUID, new StringType(stringValue));
}
@ -166,6 +170,7 @@ public class SolarmanChannelUpdater {
return acc.append((char) (shortValue >> 8)).append((char) (shortValue & 0xFF));
}, StringBuilder::append).toString();
logger.debug("Update String state: channelUID: {}, state: {}", channelUID.getAsString(), stringValue);
stateUpdater.updateState(channelUID, new StringType(stringValue));
}
@ -175,23 +180,37 @@ public class SolarmanChannelUpdater {
BigDecimal convertedValue = convertNumericValue(value, parameterItem.getOffset(), parameterItem.getScale());
String uom = Objects.requireNonNullElse(parameterItem.getUom(), "");
State state;
if (!uom.isBlank()) {
try {
Unit<?> unitFromDefinition = ChannelUtils.getUnitFromDefinition(uom);
if (unitFromDefinition != null) {
state = new QuantityType<>(convertedValue, unitFromDefinition);
} else {
logger.debug("Unable to parse unit: {}", uom);
if (parameterItem.hasLookup()) {
String stringValue = getStringFromLookupList(value.intValue(), parameterItem.getLookup());
logger.debug("Update Lookup state: channelUID: {}, key: {}, state: {}", channelUID.getAsString(),
value.intValue(), stringValue);
stateUpdater.updateState(channelUID, new StringType(stringValue));
} else {
State state;
if (!uom.isBlank()) {
try {
Unit<?> unitFromDefinition = ChannelUtils.getUnitFromDefinition(uom);
if (unitFromDefinition != null) {
state = new QuantityType<>(convertedValue, unitFromDefinition);
} else {
logger.debug("Unable to parse unit: {}", uom);
state = new DecimalType(convertedValue);
}
} catch (MeasurementParseException e) {
state = new DecimalType(convertedValue);
}
} catch (MeasurementParseException e) {
} else {
state = new DecimalType(convertedValue);
}
} else {
state = new DecimalType(convertedValue);
logger.debug("Update Numeric state: channelUID: {}, state: {}", channelUID.getAsString(),
state.toFullString());
stateUpdater.updateState(channelUID, state);
}
stateUpdater.updateState(channelUID, state);
}
private @Nullable String getStringFromLookupList(int key, List<Lookup> lookupList) {
return lookupList.stream().filter(lookup -> key == lookup.getKey()).map(Lookup::getValue).findFirst()
.orElse("");
}
private void updateChannelWithRawValue(ParameterItem parameterItem, ChannelUID channelUID, List<Integer> registers,
@ -200,7 +219,7 @@ public class SolarmanChannelUpdater {
reversed(registers).stream().map(readRegistersMap::get).map(
val -> String.format("0x%02X", ByteBuffer.wrap(val).order(ByteOrder.BIG_ENDIAN).getShort()))
.collect(Collectors.joining(",")));
logger.debug("Update RawValue state: channelUID: {}, state: {}", channelUID.getAsString(), hexString);
stateUpdater.updateState(channelUID, new StringType(hexString));
}

View File

@ -57,6 +57,17 @@
<default>60</default>
<advanced>true</advanced>
</parameter>
<parameter name="solarmanLoggerMode" type="text" required="false">
<label>Logger Mode</label>
<description>Use RAW Modbus for LAN Stick LSE-3 and V5 NODBUS for most Wifi Sticks. If your Wifi stick uses Raw
Modbus choose RAW. If you do not use this advanced option, V5 MODBUS will be the default.</description>
<options>
<option value="V5MODBUS">V5 Modbus</option>
<option value="RAWMODBUS">RAW Modbus</option>
</options>
<default>V5MODBUS</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.

View File

@ -0,0 +1,146 @@
/**
* 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 static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.util.Map;
import javax.validation.constraints.NotNull;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;
import org.openhab.binding.solarman.internal.SolarmanLoggerConfiguration;
import org.openhab.binding.solarman.internal.SolarmanLoggerMode;
import org.openhab.binding.solarman.internal.modbus.exception.SolarmanException;
/**
* @author Catalin Sanda - Initial contribution
*/
@ExtendWith(MockitoExtension.class)
@NonNullByDefault
class SolarmanRawProtocolTest {
SolarmanLoggerConnection solarmanLoggerConnection = (@NotNull SolarmanLoggerConnection) mock(
SolarmanLoggerConnection.class);
private SolarmanLoggerConfiguration loggerConfiguration = new SolarmanLoggerConfiguration("192.168.1.1", 8899,
"1234567890", "sg04lp3", 60, SolarmanLoggerMode.RAWMODBUS.toString(), null);
private SolarmanRawProtocol solarmanRawProtocol = new SolarmanRawProtocol(loggerConfiguration);
@Test
void testbuildSolarmanRawFrame() {
byte[] requestFrame = solarmanRawProtocol.buildSolarmanRawFrame((byte) 0x03, 0x0063, 0x006D);
byte[] expectedFrame = { (byte) 0x03, (byte) 0xE8, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x08,
(byte) 0x01, (byte) 0x03, (byte) 0x00, (byte) 0x63, (byte) 0x00, (byte) 0x0B, (byte) 0xF4,
(byte) 0x13 };
assertArrayEquals(requestFrame, expectedFrame);
}
@Test
void testReadRegister0x01() throws SolarmanException {
// given
when(solarmanLoggerConnection.sendRequest(any()))
.thenReturn(hexStringToByteArray("03E800000019010316168016801590012C11940014005A000000050096007D"));
// when
Map<Integer, byte[]> regValues = solarmanRawProtocol.readRegisters(solarmanLoggerConnection, (byte) 0x03, 1, 1);
// then
assertEquals(1, regValues.size());
assertTrue(regValues.containsKey(1));
assertEquals("1680", bytesToHex(regValues.get(1)));
}
@Test
void testReadRegisters0x02to0x03() throws SolarmanException {
// given
when(solarmanLoggerConnection.sendRequest(any()))
.thenReturn(hexStringToByteArray("03E800000019010316168016801590012C11940014005A000000050096007D"));
// when
Map<Integer, byte[]> regValues = solarmanRawProtocol.readRegisters(solarmanLoggerConnection, (byte) 0x03, 2, 3);
// then
assertEquals(2, regValues.size());
assertTrue(regValues.containsKey(2));
assertTrue(regValues.containsKey(3));
assertEquals("1680", bytesToHex(regValues.get(2)));
assertEquals("1680", bytesToHex(regValues.get(3)));
}
@Test
void testReadRegisterSUN10KSG04LP3EUPart1() throws SolarmanException {
// given
when(solarmanLoggerConnection.sendRequest(any())).thenReturn(hexStringToByteArray(
"03E80000005101034E091A08FD092700000000000000020003000000050000138800800037002800A5004A003D000600010003000A00000000000600010003000A0000091B08F6091C006E00500014010E00C9003E0215"));
// when
Map<Integer, byte[]> regValues = solarmanRawProtocol.readRegisters(solarmanLoggerConnection, (byte) 0x03, 0x3c,
0x4f);
// then
assertEquals(20, regValues.size());
assertTrue(regValues.containsKey(0x3c));
assertTrue(regValues.containsKey(0x4f));
assertEquals("091A", bytesToHex(regValues.get(0x3c)));
assertEquals("0001", bytesToHex(regValues.get(0x4f)));
}
@Test
void testReadRegisterSUN10KSG04LP3EUPart2() throws SolarmanException {
// given
when(solarmanLoggerConnection.sendRequest(any())).thenReturn(hexStringToByteArray(
"03E80000005101034E091A08FD092700000000000000020003000000050000138800800037002800A5004A003D000600010003000A00000000000600010003000A0000091B08F6091C006E00500014010E00C9003E0215"));
// when
Map<Integer, byte[]> regValues = solarmanRawProtocol.readRegisters(solarmanLoggerConnection, (byte) 0x03, 0x50,
0x5f);
// then
assertEquals(16, regValues.size());
assertTrue(regValues.containsKey(0x50));
assertTrue(regValues.containsKey(0x5f));
assertEquals("091A", bytesToHex(regValues.get(0x50)));
assertEquals("00A5", bytesToHex(regValues.get(0x5f)));
}
private static byte[] hexStringToByteArray(String s) {
int len = s.length();
byte[] data = new byte[len / 2];
for (int i = 0; i < len; i += 2) {
data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) + Character.digit(s.charAt(i + 1), 16));
}
return data;
}
@Nullable
private static String bytesToHex(byte @Nullable [] bytes) {
if (bytes == null) {
return null;
}
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
sb.append(String.format("%02X", b));
}
return sb.toString();
}
}

View File

@ -27,6 +27,7 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;
import org.openhab.binding.solarman.internal.SolarmanLoggerConfiguration;
import org.openhab.binding.solarman.internal.SolarmanLoggerMode;
import org.openhab.binding.solarman.internal.modbus.exception.SolarmanException;
/**
@ -39,7 +40,7 @@ class SolarmanV5ProtocolTest {
SolarmanLoggerConnection.class);
private SolarmanLoggerConfiguration loggerConfiguration = new SolarmanLoggerConfiguration("192.168.1.1", 8899,
"1234567890", "sg04lp3", 60, null);
"1234567890", "sg04lp3", 60, SolarmanLoggerMode.V5MODBUS.toString(), null);
private SolarmanV5Protocol solarmanV5Protocol = new SolarmanV5Protocol(loggerConfiguration);