mirror of
https://github.com/openhab/openhab-addons.git
synced 2025-01-25 14:55:55 +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.
|
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
|
## Thing Configuration
|
||||||
|
|
||||||
@ -25,14 +25,15 @@ The IP address can be obtained from your router and the serial number can either
|
|||||||
|
|
||||||
### `logger` Thing Configuration
|
### `logger` Thing Configuration
|
||||||
|
|
||||||
| Name | Type | Description | Default | Required | Advanced |
|
| Name | Type | Description | Default | Required | Advanced |
|
||||||
|--------------------|---------|--------------------------------------------------------|---------|----------|----------|
|
|--------------------|---------|-------------------------------------------------------------------------------------------------------------------|-----------|----------|----------|
|
||||||
| hostname | text | Hostname or IP address of the Solarman logger | N/A | yes | no |
|
| 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 |
|
| 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 |
|
| inverterType | text | The type of inverter connected to the logger | N/A | yes | no |
|
||||||
| port | integer | Port of the Solarman logger | 8899 | no | yes |
|
| port | integer | Port of the Solarman logger | 8899 | no | yes |
|
||||||
| refreshInterval | integer | Interval the device is polled in sec. | 60 | 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 |
|
| 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.
|
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 serialNumber = "";
|
||||||
public String inverterType = "sg04lp3";
|
public String inverterType = "sg04lp3";
|
||||||
public int refreshInterval = 30;
|
public int refreshInterval = 30;
|
||||||
|
public String solarmanLoggerMode = SolarmanLoggerMode.V5MODBUS.toString();
|
||||||
@Nullable
|
@Nullable
|
||||||
public String additionalRequests;
|
public String additionalRequests;
|
||||||
|
|
||||||
@ -38,12 +39,13 @@ public class SolarmanLoggerConfiguration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public SolarmanLoggerConfiguration(String hostname, Integer port, String serialNumber, String inverterType,
|
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.hostname = hostname;
|
||||||
this.port = port;
|
this.port = port;
|
||||||
this.serialNumber = serialNumber;
|
this.serialNumber = serialNumber;
|
||||||
this.inverterType = inverterType;
|
this.inverterType = inverterType;
|
||||||
this.refreshInterval = refreshInterval;
|
this.refreshInterval = refreshInterval;
|
||||||
|
this.solarmanLoggerMode = solarmanLoggerMode;
|
||||||
this.additionalRequests = additionalRequests;
|
this.additionalRequests = additionalRequests;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -67,6 +69,10 @@ public class SolarmanLoggerConfiguration {
|
|||||||
return refreshInterval;
|
return refreshInterval;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public SolarmanLoggerMode getSolarmanLoggerMode() {
|
||||||
|
return SolarmanLoggerMode.valueOf(solarmanLoggerMode);
|
||||||
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
public String getAdditionalRequests() {
|
public String getAdditionalRequests() {
|
||||||
return additionalRequests;
|
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.Request;
|
||||||
import org.openhab.binding.solarman.internal.defmodel.Validation;
|
import org.openhab.binding.solarman.internal.defmodel.Validation;
|
||||||
import org.openhab.binding.solarman.internal.modbus.SolarmanLoggerConnector;
|
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.SolarmanChannelUpdater;
|
||||||
import org.openhab.binding.solarman.internal.updater.SolarmanProcessResult;
|
import org.openhab.binding.solarman.internal.updater.SolarmanProcessResult;
|
||||||
import org.openhab.core.thing.Channel;
|
import org.openhab.core.thing.Channel;
|
||||||
@ -94,7 +95,10 @@ public class SolarmanLoggerHandler extends BaseThingHandler {
|
|||||||
logger.debug("Found definition for {}", config.inverterType);
|
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(), "");
|
String additionalRequests = Objects.requireNonNullElse(config.getAdditionalRequests(), "");
|
||||||
|
|
||||||
@ -110,17 +114,17 @@ public class SolarmanLoggerHandler extends BaseThingHandler {
|
|||||||
|
|
||||||
scheduledFuture = scheduler
|
scheduledFuture = scheduler
|
||||||
.scheduleWithFixedDelay(
|
.scheduleWithFixedDelay(
|
||||||
() -> queryLoggerAndUpdateState(solarmanLoggerConnector, solarmanV5Protocol, mergedRequests,
|
() -> queryLoggerAndUpdateState(solarmanLoggerConnector, solarmanProtocol, mergedRequests,
|
||||||
paramToChannelMapping, solarmanChannelUpdater),
|
paramToChannelMapping, solarmanChannelUpdater),
|
||||||
0, config.refreshInterval, TimeUnit.SECONDS);
|
0, config.refreshInterval, TimeUnit.SECONDS);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void queryLoggerAndUpdateState(SolarmanLoggerConnector solarmanLoggerConnector,
|
private void queryLoggerAndUpdateState(SolarmanLoggerConnector solarmanLoggerConnector,
|
||||||
SolarmanV5Protocol solarmanV5Protocol, List<Request> mergedRequests,
|
SolarmanProtocol solarmanProtocol, List<Request> mergedRequests,
|
||||||
Map<ParameterItem, ChannelUID> paramToChannelMapping, SolarmanChannelUpdater solarmanChannelUpdater) {
|
Map<ParameterItem, ChannelUID> paramToChannelMapping, SolarmanChannelUpdater solarmanChannelUpdater) {
|
||||||
try {
|
try {
|
||||||
SolarmanProcessResult solarmanProcessResult = solarmanChannelUpdater.fetchDataFromLogger(mergedRequests,
|
SolarmanProcessResult solarmanProcessResult = solarmanChannelUpdater.fetchDataFromLogger(mergedRequests,
|
||||||
solarmanLoggerConnector, solarmanV5Protocol, paramToChannelMapping);
|
solarmanLoggerConnector, solarmanProtocol, paramToChannelMapping);
|
||||||
|
|
||||||
if (solarmanProcessResult.hasSuccessfulResponses()) {
|
if (solarmanProcessResult.hasSuccessfulResponses()) {
|
||||||
updateStatus(ThingStatus.ONLINE);
|
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,
|
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());
|
channel.getUID());
|
||||||
}).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
|
}).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.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.registers = convertRegisters(item.getRegisters());
|
||||||
baseChannelConfig.uom = item.getUom();
|
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;
|
private BigDecimal offset;
|
||||||
@Nullable
|
@Nullable
|
||||||
private Boolean isstr;
|
private Boolean isstr;
|
||||||
|
private List<Lookup> lookup = new ArrayList<>();
|
||||||
|
|
||||||
public ParameterItem() {
|
public ParameterItem() {
|
||||||
}
|
}
|
||||||
|
|
||||||
public ParameterItem(String name, @Nullable String itemClass, @Nullable String stateClass, @Nullable String uom,
|
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 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.name = name;
|
||||||
this.itemClass = itemClass;
|
this.itemClass = itemClass;
|
||||||
this.stateClass = stateClass;
|
this.stateClass = stateClass;
|
||||||
@ -64,6 +66,9 @@ public class ParameterItem {
|
|||||||
this.validation = validation;
|
this.validation = validation;
|
||||||
this.offset = offset;
|
this.offset = offset;
|
||||||
this.isstr = isstr;
|
this.isstr = isstr;
|
||||||
|
if (lookup != null) {
|
||||||
|
this.lookup = lookup;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getName() {
|
public String getName() {
|
||||||
@ -153,4 +158,16 @@ public class ParameterItem {
|
|||||||
public void setItemClass(String itemClass) {
|
public void setItemClass(String itemClass) {
|
||||||
this.itemClass = 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];
|
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]))
|
return IntStream.range(0, bytes.length).mapToObj(i -> String.format("%02X", bytes[i]))
|
||||||
.collect(Collectors.joining());
|
.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
|
* @author Catalin Sanda - Initial contribution
|
||||||
*/
|
*/
|
||||||
@NonNullByDefault
|
@NonNullByDefault
|
||||||
public class SolarmanV5Protocol {
|
public class SolarmanV5Protocol implements SolarmanProtocol {
|
||||||
private final SolarmanLoggerConfiguration solarmanLoggerConfiguration;
|
private final SolarmanLoggerConfiguration solarmanLoggerConfiguration;
|
||||||
|
|
||||||
public SolarmanV5Protocol(SolarmanLoggerConfiguration solarmanLoggerConfiguration) {
|
public SolarmanV5Protocol(SolarmanLoggerConfiguration solarmanLoggerConfiguration) {
|
||||||
this.solarmanLoggerConfiguration = solarmanLoggerConfiguration;
|
this.solarmanLoggerConfiguration = solarmanLoggerConfiguration;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public Map<Integer, byte[]> readRegisters(SolarmanLoggerConnection solarmanLoggerConnection, byte mbFunctionCode,
|
public Map<Integer, byte[]> readRegisters(SolarmanLoggerConnection solarmanLoggerConnection, byte mbFunctionCode,
|
||||||
int firstReg, int lastReg) throws SolarmanException {
|
int firstReg, int lastReg) throws SolarmanException {
|
||||||
byte[] solarmanV5Frame = buildSolarmanV5Frame(mbFunctionCode, firstReg, lastReg);
|
byte[] solarmanV5Frame = buildSolarmanV5Frame(mbFunctionCode, firstReg, lastReg);
|
||||||
|
@ -14,6 +14,7 @@ package org.openhab.binding.solarman.internal.typeprovider;
|
|||||||
|
|
||||||
import javax.measure.Unit;
|
import javax.measure.Unit;
|
||||||
import javax.measure.quantity.Dimensionless;
|
import javax.measure.quantity.Dimensionless;
|
||||||
|
import javax.measure.quantity.ElectricCharge;
|
||||||
import javax.measure.quantity.ElectricCurrent;
|
import javax.measure.quantity.ElectricCurrent;
|
||||||
import javax.measure.quantity.ElectricPotential;
|
import javax.measure.quantity.ElectricPotential;
|
||||||
import javax.measure.quantity.Energy;
|
import javax.measure.quantity.Energy;
|
||||||
@ -76,6 +77,7 @@ public class ChannelUtils {
|
|||||||
private static String computeNumberType(String uom) {
|
private static String computeNumberType(String uom) {
|
||||||
return switch (uom.toUpperCase()) {
|
return switch (uom.toUpperCase()) {
|
||||||
case "A" -> CoreItemFactory.NUMBER + ":" + ElectricCurrent.class.getSimpleName();
|
case "A" -> CoreItemFactory.NUMBER + ":" + ElectricCurrent.class.getSimpleName();
|
||||||
|
case "AH" -> CoreItemFactory.NUMBER + ":" + ElectricCharge.class.getSimpleName();
|
||||||
case "V" -> CoreItemFactory.NUMBER + ":" + ElectricPotential.class.getSimpleName();
|
case "V" -> CoreItemFactory.NUMBER + ":" + ElectricPotential.class.getSimpleName();
|
||||||
case "°C" -> CoreItemFactory.NUMBER + ":" + Temperature.class.getSimpleName();
|
case "°C" -> CoreItemFactory.NUMBER + ":" + Temperature.class.getSimpleName();
|
||||||
case "W", "KW", "VA", "KVA", "VAR", "KVAR" -> CoreItemFactory.NUMBER + ":" + Power.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) {
|
public static @Nullable Unit<?> getUnitFromDefinition(String uom) {
|
||||||
return switch (uom.toUpperCase()) {
|
return switch (uom.toUpperCase()) {
|
||||||
case "A" -> Units.AMPERE;
|
case "A" -> Units.AMPERE;
|
||||||
|
case "AH" -> Units.AMPERE_HOUR;
|
||||||
case "V" -> Units.VOLT;
|
case "V" -> Units.VOLT;
|
||||||
case "°C" -> SIUnits.CELSIUS;
|
case "°C" -> SIUnits.CELSIUS;
|
||||||
case "W" -> Units.WATT;
|
case "W" -> Units.WATT;
|
||||||
|
@ -33,11 +33,12 @@ import javax.measure.format.MeasurementParseException;
|
|||||||
|
|
||||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
import org.eclipse.jdt.annotation.Nullable;
|
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.ParameterItem;
|
||||||
import org.openhab.binding.solarman.internal.defmodel.Request;
|
import org.openhab.binding.solarman.internal.defmodel.Request;
|
||||||
import org.openhab.binding.solarman.internal.modbus.SolarmanLoggerConnection;
|
import org.openhab.binding.solarman.internal.modbus.SolarmanLoggerConnection;
|
||||||
import org.openhab.binding.solarman.internal.modbus.SolarmanLoggerConnector;
|
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.SolarmanConnectionException;
|
||||||
import org.openhab.binding.solarman.internal.modbus.exception.SolarmanException;
|
import org.openhab.binding.solarman.internal.modbus.exception.SolarmanException;
|
||||||
import org.openhab.binding.solarman.internal.typeprovider.ChannelUtils;
|
import org.openhab.binding.solarman.internal.typeprovider.ChannelUtils;
|
||||||
@ -64,7 +65,7 @@ public class SolarmanChannelUpdater {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public SolarmanProcessResult fetchDataFromLogger(List<Request> requests,
|
public SolarmanProcessResult fetchDataFromLogger(List<Request> requests,
|
||||||
SolarmanLoggerConnector solarmanLoggerConnector, SolarmanV5Protocol solarmanV5Protocol,
|
SolarmanLoggerConnector solarmanLoggerConnector, SolarmanProtocol solarmanProtocol,
|
||||||
Map<ParameterItem, ChannelUID> paramToChannelMapping) {
|
Map<ParameterItem, ChannelUID> paramToChannelMapping) {
|
||||||
try (SolarmanLoggerConnection solarmanLoggerConnection = solarmanLoggerConnector.createConnection()) {
|
try (SolarmanLoggerConnection solarmanLoggerConnection = solarmanLoggerConnector.createConnection()) {
|
||||||
logger.debug("Fetching data from logger");
|
logger.debug("Fetching data from logger");
|
||||||
@ -77,7 +78,7 @@ public class SolarmanChannelUpdater {
|
|||||||
SolarmanProcessResult solarmanProcessResult = requests.stream().map(request -> {
|
SolarmanProcessResult solarmanProcessResult = requests.stream().map(request -> {
|
||||||
try {
|
try {
|
||||||
return SolarmanProcessResult.ofValue(request,
|
return SolarmanProcessResult.ofValue(request,
|
||||||
solarmanV5Protocol.readRegisters(solarmanLoggerConnection,
|
solarmanProtocol.readRegisters(solarmanLoggerConnection,
|
||||||
(byte) request.getMbFunctioncode().intValue(), request.getStart(),
|
(byte) request.getMbFunctioncode().intValue(), request.getStart(),
|
||||||
request.getEnd()));
|
request.getEnd()));
|
||||||
} catch (SolarmanException e) {
|
} catch (SolarmanException e) {
|
||||||
@ -120,6 +121,7 @@ public class SolarmanChannelUpdater {
|
|||||||
.map(rawVal -> String.format("%02d", rawVal / 100) + ":" + String.format("%02d", rawVal % 100))
|
.map(rawVal -> String.format("%02d", rawVal / 100) + ":" + String.format("%02d", rawVal % 100))
|
||||||
.collect(Collectors.joining());
|
.collect(Collectors.joining());
|
||||||
|
|
||||||
|
logger.debug("Update state: channelUID: {}, state: {}", channelUID.getAsString(), stringValue);
|
||||||
stateUpdater.updateState(channelUID, new StringType(stringValue));
|
stateUpdater.updateState(channelUID, new StringType(stringValue));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -143,6 +145,7 @@ public class SolarmanChannelUpdater {
|
|||||||
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yy/M/d H:m:s");
|
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yy/M/d H:m:s");
|
||||||
LocalDateTime dateTime = LocalDateTime.parse(stringValue, formatter);
|
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())));
|
stateUpdater.updateState(channelUID, new DateTimeType(dateTime.atZone(ZoneId.systemDefault())));
|
||||||
} catch (DateTimeParseException e) {
|
} catch (DateTimeParseException e) {
|
||||||
logger.debug("Unable to parse string date {} to a DateTime object", stringValue);
|
logger.debug("Unable to parse string date {} to a DateTime object", stringValue);
|
||||||
@ -156,6 +159,7 @@ public class SolarmanChannelUpdater {
|
|||||||
+ (rawVal & 0x0F))
|
+ (rawVal & 0x0F))
|
||||||
.collect(Collectors.joining());
|
.collect(Collectors.joining());
|
||||||
|
|
||||||
|
logger.debug("Update Version state: channelUID: {}, state: {}", channelUID.getAsString(), stringValue);
|
||||||
stateUpdater.updateState(channelUID, new StringType(stringValue));
|
stateUpdater.updateState(channelUID, new StringType(stringValue));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -166,6 +170,7 @@ public class SolarmanChannelUpdater {
|
|||||||
return acc.append((char) (shortValue >> 8)).append((char) (shortValue & 0xFF));
|
return acc.append((char) (shortValue >> 8)).append((char) (shortValue & 0xFF));
|
||||||
}, StringBuilder::append).toString();
|
}, StringBuilder::append).toString();
|
||||||
|
|
||||||
|
logger.debug("Update String state: channelUID: {}, state: {}", channelUID.getAsString(), stringValue);
|
||||||
stateUpdater.updateState(channelUID, new StringType(stringValue));
|
stateUpdater.updateState(channelUID, new StringType(stringValue));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -175,23 +180,37 @@ public class SolarmanChannelUpdater {
|
|||||||
BigDecimal convertedValue = convertNumericValue(value, parameterItem.getOffset(), parameterItem.getScale());
|
BigDecimal convertedValue = convertNumericValue(value, parameterItem.getOffset(), parameterItem.getScale());
|
||||||
String uom = Objects.requireNonNullElse(parameterItem.getUom(), "");
|
String uom = Objects.requireNonNullElse(parameterItem.getUom(), "");
|
||||||
|
|
||||||
State state;
|
if (parameterItem.hasLookup()) {
|
||||||
if (!uom.isBlank()) {
|
String stringValue = getStringFromLookupList(value.intValue(), parameterItem.getLookup());
|
||||||
try {
|
logger.debug("Update Lookup state: channelUID: {}, key: {}, state: {}", channelUID.getAsString(),
|
||||||
Unit<?> unitFromDefinition = ChannelUtils.getUnitFromDefinition(uom);
|
value.intValue(), stringValue);
|
||||||
if (unitFromDefinition != null) {
|
stateUpdater.updateState(channelUID, new StringType(stringValue));
|
||||||
state = new QuantityType<>(convertedValue, unitFromDefinition);
|
} else {
|
||||||
} else {
|
State state;
|
||||||
logger.debug("Unable to parse unit: {}", uom);
|
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);
|
state = new DecimalType(convertedValue);
|
||||||
}
|
}
|
||||||
} catch (MeasurementParseException e) {
|
} else {
|
||||||
state = new DecimalType(convertedValue);
|
state = new DecimalType(convertedValue);
|
||||||
}
|
}
|
||||||
} else {
|
logger.debug("Update Numeric state: channelUID: {}, state: {}", channelUID.getAsString(),
|
||||||
state = new DecimalType(convertedValue);
|
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,
|
private void updateChannelWithRawValue(ParameterItem parameterItem, ChannelUID channelUID, List<Integer> registers,
|
||||||
@ -200,7 +219,7 @@ public class SolarmanChannelUpdater {
|
|||||||
reversed(registers).stream().map(readRegistersMap::get).map(
|
reversed(registers).stream().map(readRegistersMap::get).map(
|
||||||
val -> String.format("0x%02X", ByteBuffer.wrap(val).order(ByteOrder.BIG_ENDIAN).getShort()))
|
val -> String.format("0x%02X", ByteBuffer.wrap(val).order(ByteOrder.BIG_ENDIAN).getShort()))
|
||||||
.collect(Collectors.joining(",")));
|
.collect(Collectors.joining(",")));
|
||||||
|
logger.debug("Update RawValue state: channelUID: {}, state: {}", channelUID.getAsString(), hexString);
|
||||||
stateUpdater.updateState(channelUID, new StringType(hexString));
|
stateUpdater.updateState(channelUID, new StringType(hexString));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -57,6 +57,17 @@
|
|||||||
<default>60</default>
|
<default>60</default>
|
||||||
<advanced>true</advanced>
|
<advanced>true</advanced>
|
||||||
</parameter>
|
</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">
|
<parameter name="additionalRequests" type="text" required="false">
|
||||||
<label>Additional Requests</label>
|
<label>Additional Requests</label>
|
||||||
<description>Additional requests besides the ones defined in the inverter definition.
|
<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.junit.jupiter.api.extension.ExtendWith;
|
||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
import org.openhab.binding.solarman.internal.SolarmanLoggerConfiguration;
|
import org.openhab.binding.solarman.internal.SolarmanLoggerConfiguration;
|
||||||
|
import org.openhab.binding.solarman.internal.SolarmanLoggerMode;
|
||||||
import org.openhab.binding.solarman.internal.modbus.exception.SolarmanException;
|
import org.openhab.binding.solarman.internal.modbus.exception.SolarmanException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -39,7 +40,7 @@ class SolarmanV5ProtocolTest {
|
|||||||
SolarmanLoggerConnection.class);
|
SolarmanLoggerConnection.class);
|
||||||
|
|
||||||
private SolarmanLoggerConfiguration loggerConfiguration = new SolarmanLoggerConfiguration("192.168.1.1", 8899,
|
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);
|
private SolarmanV5Protocol solarmanV5Protocol = new SolarmanV5Protocol(loggerConfiguration);
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user