mirror of
https://github.com/openhab/openhab-addons.git
synced 2025-01-10 15:11:59 +01:00
[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>
This commit is contained in:
parent
66f8c82af8
commit
10048bc625
@ -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.
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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));
|
||||
}
|
||||
|
@ -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
|
||||
}
|
@ -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();
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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));
|
||||
}
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user