Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
227 changes: 145 additions & 82 deletions bundles/org.openhab.binding.solarman/README.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@
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.binding.solarman.internal.updater.SolarmanRegisterUpdater;
import org.openhab.binding.solarman.internal.util.ParserUtils;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
Expand All @@ -63,6 +65,8 @@ public class SolarmanLoggerHandler extends BaseThingHandler {
private final SolarmanChannelManager solarmanChannelManager;
@Nullable
private volatile ScheduledFuture<?> scheduledFuture;
@Nullable
private SolarmanRegisterUpdater solarmanRegisterUpdater;

public SolarmanLoggerHandler(Thing thing) {
super(thing);
Expand All @@ -72,6 +76,13 @@ public SolarmanLoggerHandler(Thing thing) {

@Override
public void handleCommand(ChannelUID channelUID, Command command) {
logger.trace("Received command {} in channel {}", command, channelUID);

if (solarmanRegisterUpdater != null) {
solarmanRegisterUpdater.updateLoggerRegisters(channelUID, command);
} else {
logger.error("SolarmanRegisterUpdater is not initialized yet");
}
}

@Override
Expand Down Expand Up @@ -110,6 +121,8 @@ public void initialize() {
extractChannelMappingFromChannels(staticChannels),
setupChannelsForInverterDefinition(inverterDefinition));

solarmanRegisterUpdater = new SolarmanRegisterUpdater(paramToChannelMapping, solarmanLoggerConnector,
solarmanProtocol);
SolarmanChannelUpdater solarmanChannelUpdater = new SolarmanChannelUpdater(this::updateState);

scheduledFuture = scheduler
Expand Down Expand Up @@ -152,19 +165,13 @@ private Map<ParameterItem, ChannelUID> extractChannelMappingFromChannels(List<Ch
throw new IllegalStateException("Channel label should not be null");
}

// TODO: Add lookup for static channels
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, null),
channel.getUID());
ParserUtils.parseRegisters(bcc.registers), "N/A", new Validation(), bcc.offset, Boolean.FALSE,
bcc.readOnly, null), channel.getUID());
}).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
}

private List<Integer> parseRegisters(String registers) {
String[] tokens = registers.split(",");
Pattern pattern = Pattern.compile("\\s*(0x[\\da-fA-F]+|[\\d]+)\\s*");
return Stream.of(tokens).map(pattern::matcher).filter(Matcher::matches).map(matcher -> matcher.group(1))
.map(SolarmanLoggerHandler::parseNumber).toList();
}

// For now just concatenate the list, in the future, merge overlapping requests
private List<Request> mergeRequests(List<Request> requestList1, List<Request> requestList2) {
return Stream.concat(requestList1.stream(), requestList2.stream()).collect(Collectors.toList());
Expand All @@ -177,19 +184,15 @@ private List<Request> extractAdditionalRequests(String channels) {

return Stream.of(tokens).map(pattern::matcher).filter(Matcher::matches).map(matcher -> {
try {
int functionCode = parseNumber(matcher.group(1));
int start = parseNumber(matcher.group(2));
int end = parseNumber(matcher.group(3));
int functionCode = ParserUtils.parseNumber(matcher.group(1));
int start = ParserUtils.parseNumber(matcher.group(2));
int end = ParserUtils.parseNumber(matcher.group(3));
return new Request(functionCode, start, end);
} catch (NumberFormatException e) {
logger.debug("Invalid number format in token: {} , ignoring additional requests", matcher.group(), e);
return new Request(-1, 0, 0);
return Request.NONE;
}
}).filter(request -> request.getMbFunctioncode() > 0).collect(Collectors.toList());
}

private static int parseNumber(String number) {
return number.startsWith("0x") ? Integer.parseInt(number.substring(2), 16) : Integer.parseInt(number);
}).filter(request -> !Request.NONE.equals(request)).collect(Collectors.toList());
}

private Map<ParameterItem, ChannelUID> setupChannelsForInverterDefinition(InverterDefinition inverterDefinition) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,5 @@ public class BaseChannelConfig {
public Integer rule = 1;
public BigDecimal offset = BigDecimal.ZERO;
public String registers = "";
public boolean readOnly = true;
}
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ private Configuration buildConfigurationFromItem(ParameterItem item) {
baseChannelConfig.rule = item.getRule();
}

baseChannelConfig.readOnly = !Boolean.FALSE.equals(item.getIsReadOnly());

baseChannelConfig.registers = convertRegisters(item.getRegisters());
baseChannelConfig.uom = item.getUom();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ public class ParameterItem {
private BigDecimal offset;
@Nullable
private Boolean isstr;
@Nullable
private Boolean isReadOnly;
private List<Lookup> lookup = new ArrayList<>();

public ParameterItem() {
Expand All @@ -54,18 +56,20 @@ 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 List<Lookup> lookup) {
@Nullable Boolean isReadOnly, @Nullable List<Lookup> lookup) {
this.name = name;
this.itemClass = itemClass;
this.stateClass = stateClass;
this.uom = uom;
this.scale = scale;
this.rule = rule;
this.registers = registers;
this.registers = new ArrayList<>(registers);
this.registers.sort(Integer::compareTo);
this.icon = icon;
this.validation = validation;
this.offset = offset;
this.isstr = isstr;
this.isReadOnly = isReadOnly;
if (lookup != null) {
this.lookup = lookup;
}
Expand Down Expand Up @@ -151,6 +155,14 @@ public void setIsstr(Boolean isstr) {
this.isstr = isstr;
}

public @Nullable Boolean getIsReadOnly() {
return isReadOnly;
}

public void setIsReadOnly(Boolean isReadOnly) {
this.isReadOnly = isReadOnly;
}

public @Nullable String getItemClass() {
return itemClass;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* Copyright (c) 2010-2025 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.enums;

import org.eclipse.jdt.annotation.NonNullByDefault;

/**
* @author Oleksandr Mishchuk - Initial contribution
*/
@NonNullByDefault
public enum IntegerValueType {
UNSIGNED,
SIGNED
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,12 @@
*/
@NonNullByDefault
public interface SolarmanProtocol {
byte DEFAULT_SLAVE_ID = 0x01;
byte WRITE_REGISTERS_FUNCTION_CODE = 0x10;

Map<Integer, byte[]> readRegisters(SolarmanLoggerConnection solarmanLoggerConnection, byte mbFunctionCode,
int firstReg, int lastReg) throws SolarmanException;

boolean writeRegisters(SolarmanLoggerConnection solarmanLoggerConnection, int register, byte[] data)
throws SolarmanException;
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,11 @@ public SolarmanRawProtocol(SolarmanLoggerConfiguration solarmanLoggerConfigurati
this.solarmanLoggerConfiguration = solarmanLoggerConfiguration;
}

@Override
public Map<Integer, byte[]> readRegisters(SolarmanLoggerConnection solarmanLoggerConnection, byte mbFunctionCode,
int firstReg, int lastReg) throws SolarmanException {
byte[] solarmanRawFrame = buildSolarmanRawFrame(mbFunctionCode, firstReg, lastReg);
int regCount = lastReg - firstReg + 1;
byte[] solarmanRawFrame = buildSolarmanRawReadFrame(mbFunctionCode, firstReg, regCount);
byte[] respFrame = solarmanLoggerConnection.sendRequest(solarmanRawFrame);
if (respFrame.length > 0) {
byte[] modbusRespFrame = extractModbusRawResponseFrame(respFrame, solarmanRawFrame);
Expand All @@ -49,6 +51,73 @@ public Map<Integer, byte[]> readRegisters(SolarmanLoggerConnection solarmanLogge
}
}

@Override
public boolean writeRegisters(SolarmanLoggerConnection solarmanLoggerConnection, int firstReg, byte[] data)
throws SolarmanException {
if (data.length % 2 != 0) {
throw new SolarmanException("Data to be written should be packed as two bytes per register!");
}

byte[] solarmanRawFrame = buildSolarmanRawWriteFrame(firstReg, data);
byte[] respFrame = solarmanLoggerConnection.sendRequest(solarmanRawFrame);
if (respFrame.length > 0) {
byte[] modbusRespFrame = extractModbusRawResponseFrame(respFrame, solarmanRawFrame);
parseRawModbusWriteHoldingRegistersResponse(modbusRespFrame, data);
return true;
} else {
throw new SolarmanConnectionException("Response frame was empty");
}
}

/**
* Builds a SolarMAN Raw frame to request data write from firstReg.
* Frame format is based on
* <a href="https://github.com/StephanJoubert/home_assistant_solarman/issues/247">Solarman RAW Protocol</a>
*
* @param firstReg - the start register
* @param data - the data to be written
* @return byte array containing the Solarman Raw frame
*/
private byte[] buildSolarmanRawWriteFrame(int firstReg, byte[] data) throws SolarmanException {
byte[] requestPayload = buildSolarmanRawWriteFrameRequestPayload(DEFAULT_SLAVE_ID, firstReg, data);
byte[] header = buildSolarmanRawFrameHeader(requestPayload.length);

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

/**
* Builds a SolarMAN Raw write frame payload
* Frame format is based on
* <a href="https://www.modbustools.com/modbus.html#function16">Modbus RTU Write Multiple Registers</a>
*
* @param slaveId - Modbus slave ID
* @param firstReg - the start register
* @param data - the data to be written
* @return byte array containing the Modbus RTU Raw frame payload
*/
private byte[] buildSolarmanRawWriteFrameRequestPayload(byte slaveId, int firstReg, byte[] data)
throws SolarmanException {
if (data.length % 2 != 0) {
throw new SolarmanException("Data to be written should be packed as two bytes per register!");
}

// slaveId (1 byte)
// mbFunction (1 byte)
// firstRegister (2 bytes)
// registerCount (2 bytes)
// data length (1 byte)
// data
int bufferSize = 1 + 1 + 2 + 2 + 1 + data.length;
int registerCount = data.length / 2;

byte[] req = ByteBuffer.allocate(bufferSize).put(slaveId).put(WRITE_REGISTERS_FUNCTION_CODE)
.putShort((short) firstReg).putShort((short) registerCount).put((byte) data.length).put(data).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();
}

protected byte[] extractModbusRawResponseFrame(byte @Nullable [] responseFrame, byte[] requestFrame)
throws SolarmanException {
if (responseFrame == null || responseFrame.length == 0) {
Expand Down Expand Up @@ -81,6 +150,36 @@ protected Map<Integer, byte[]> parseRawModbusReadHoldingRegistersResponse(byte @
return registers;
}

private void parseRawModbusWriteHoldingRegistersResponse(byte[] frame, byte[] data)
throws SolarmanProtocolException {
int expectedRegistersCount = data.length / 2;

// slaveId (1 byte)
// modbusFunction (1 byte)
// firstRegister (2 bytes)
// registerCount (2 bytes)
int expectedFrameDataLen = 1 + 1 + 2 + 2;
if (frame == null || frame.length < expectedFrameDataLen + 2) {
throw new SolarmanProtocolException("Modbus frame is too short or empty");
}

int actualCrc = ByteBuffer.wrap(frame, expectedFrameDataLen, 2).order(ByteOrder.LITTLE_ENDIAN).getShort()
& 0xFFFF;
int expectedCrc = CRC16Modbus.calculate(Arrays.copyOfRange(frame, 0, expectedFrameDataLen));

if (actualCrc != expectedCrc) {
throw new SolarmanProtocolException(
String.format("Modbus frame crc is not valid. Expected %04x, got %04x", expectedCrc, actualCrc));
}

short registersWrittenCount = ByteBuffer.wrap(frame, 4, 2).getShort();
if (registersWrittenCount != expectedRegistersCount) {
throw new SolarmanProtocolException(
String.format("Modbus written registers count is not valid. Expected %04x, got %04x",
expectedRegistersCount, registersWrittenCount));
}
}

/**
* Builds a SolarMAN Raw frame to request data from firstReg to lastReg.
* Frame format is based on
Expand All @@ -94,14 +193,14 @@ protected Map<Integer, byte[]> parseRawModbusReadHoldingRegistersResponse(byte @
* Payload 0003: 1st register address
* Payload 006e: Nb of registers to read
* Trailer 3426: CRC-16 ModBus
*
* @param mbFunctionCode
*
* @param mbFunctionCode - Modbus function code
* @param firstReg - the start register
* @param lastReg - the end register
* @param regCount - the registers count
* @return byte array containing the Solarman Raw frame
*/
protected byte[] buildSolarmanRawFrame(byte mbFunctionCode, int firstReg, int lastReg) {
byte[] requestPayload = buildSolarmanRawFrameRequestPayload(mbFunctionCode, firstReg, lastReg);
protected byte[] buildSolarmanRawReadFrame(byte mbFunctionCode, int firstReg, int regCount) {
byte[] requestPayload = buildSolarmanRawReadFrameRequestPayload(mbFunctionCode, firstReg, regCount);
byte[] header = buildSolarmanRawFrameHeader(requestPayload.length);

return ByteBuffer.allocate(header.length + requestPayload.length).put(header).put(requestPayload).array();
Expand All @@ -115,7 +214,7 @@ protected byte[] buildSolarmanRawFrame(byte mbFunctionCode, int firstReg, int la
* 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
*/
Expand All @@ -136,7 +235,7 @@ private byte[] buildSolarmanRawFrameHeader(int payloadSize) {
}

/**
* Builds a SolarMAN Raw frame payload
* Builds a SolarMAN Raw read frame payload
* Frame format is based on
* <a href="https://github.com/StephanJoubert/home_assistant_solarman/issues/247">Solarman RAW Protocol</a>
* Request send:
Expand All @@ -145,14 +244,13 @@ private byte[] buildSolarmanRawFrameHeader(int payloadSize) {
* Payload 0003: 1st register address
* Payload 006e: Nb of registers to read
* Trailer 3426: CRC-16 ModBus
*
* @param mbFunctionCode
*
* @param mbFunctionCode - Modbus function code
* @param firstReg - the start register
* @param lastReg - the end register
* @param regCount - the registers count
* @return byte array containing the Solarman Raw frame payload
*/
protected byte[] buildSolarmanRawFrameRequestPayload(byte mbFunctionCode, int firstReg, int lastReg) {
int regCount = lastReg - firstReg + 1;
protected byte[] buildSolarmanRawReadFrameRequestPayload(byte mbFunctionCode, int firstReg, int regCount) {
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)
Expand Down
Loading