diff --git a/src/main/java/home/gateway/adapters/heatpump/ModbusSetpointRegisters.java b/src/main/java/home/gateway/adapters/heatpump/ModbusSetpointRegisters.java new file mode 100644 index 0000000..8950f93 --- /dev/null +++ b/src/main/java/home/gateway/adapters/heatpump/ModbusSetpointRegisters.java @@ -0,0 +1,42 @@ +package home.gateway.adapters.heatpump; + +import home.gateway.adapters.modbus.ModbusClient; +import home.gateway.application.SetpointRegisters; +import java.io.IOException; + +/** Setpoint registers of the controller: whole values in sign-magnitude words, read with FC3 and written with FC6. */ +public class ModbusSetpointRegisters implements SetpointRegisters { + + private static final int SIGN_BIT = 0x8000; + private static final int MAX_MAGNITUDE = 0x7FFF; + + private final ModbusClient modbus; + private final int unitId; + + public ModbusSetpointRegisters(ModbusClient modbus, int unitId) { + this.modbus = modbus; + this.unitId = unitId; + } + + @Override + public int read(int register) throws IOException { + int[] words = modbus.readHoldingRegisters(unitId, register, 1); + if (words.length != 1) { + throw new IOException("Expected 1 word from register " + register + ", got " + words.length); + } + return (int) Scale.UNITS.decode(words[0]); + } + + @Override + public void write(int register, int value) throws IOException { + modbus.writeSingleRegister(unitId, register, encode(value)); + } + + private static int encode(int value) { + int magnitude = Math.abs(value); + if (magnitude > MAX_MAGNITUDE) { + throw new IllegalArgumentException("Setpoint does not fit 15 bits: " + value); + } + return value < 0 ? magnitude | SIGN_BIT : magnitude; + } +} diff --git a/src/test/java/home/gateway/adapters/heatpump/ModbusSetpointRegistersTest.java b/src/test/java/home/gateway/adapters/heatpump/ModbusSetpointRegistersTest.java new file mode 100644 index 0000000..677a5ad --- /dev/null +++ b/src/test/java/home/gateway/adapters/heatpump/ModbusSetpointRegistersTest.java @@ -0,0 +1,98 @@ +package home.gateway.adapters.heatpump; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import home.gateway.adapters.modbus.ModbusClient; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.Test; + +class ModbusSetpointRegistersTest { + + private final FakeClient client = new FakeClient(); + private final ModbusSetpointRegisters registers = new ModbusSetpointRegisters(client, 7); + + @Test + void readsOneHoldingRegisterOfItsUnit() throws IOException { + client.word = 8; + + assertEquals(8, registers.read(4143)); + assertEquals(List.of("read 7:4143+1"), client.calls); + } + + @Test + void decodesSignMagnitudeOnRead() throws IOException { + client.word = 0x8005; + + assertEquals(-5, registers.read(4143)); + } + + @Test + void writesOneRegisterOfItsUnit() throws IOException { + registers.write(4143, 9); + + assertEquals(List.of("write 7:4143=9"), client.calls); + } + + @Test + void encodesNegativeValuesAsSignMagnitude() throws IOException { + registers.write(4143, -5); + + assertEquals(List.of("write 7:4143=" + 0x8005), client.calls); + } + + @Test + void rejectsValuesThatDoNotFitFifteenBits() { + assertThrows(IllegalArgumentException.class, () -> registers.write(4143, 0x8000)); + assertThrows(IllegalArgumentException.class, () -> registers.write(4143, -0x8000)); + assertEquals(List.of(), client.calls); + } + + @Test + void propagatesWriteFailure() { + client.failure = new IOException("timeout"); + + assertThrows(IOException.class, () -> registers.write(4143, 9)); + } + + @Test + void propagatesReadFailureAndShortAnswer() { + client.failure = new IOException("timeout"); + assertThrows(IOException.class, () -> registers.read(4143)); + + client.failure = null; + client.empty = true; + assertThrows(IOException.class, () -> registers.read(4143)); + } + + private static final class FakeClient implements ModbusClient { + final List calls = new ArrayList<>(); + int word; + boolean empty; + IOException failure; + + @Override + public int[] readHoldingRegisters(int unitId, int address, int count) throws IOException { + if (failure != null) { + throw failure; + } + calls.add("read " + unitId + ":" + address + "+" + count); + return empty ? new int[0] : new int[] {word}; + } + + @Override + public int[] readInputRegisters(int unitId, int address, int count) { + throw new UnsupportedOperationException(); + } + + @Override + public void writeSingleRegister(int unitId, int address, int value) throws IOException { + if (failure != null) { + throw failure; + } + calls.add("write " + unitId + ":" + address + "=" + value); + } + } +}