diff --git a/README.md b/README.md index f2cd324..0cfe941 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,32 @@ -# home-pump +# Home Pump Monitor + +A Java application that reads data from a heat pump via Modbus TCP and sends metrics to InfluxDB. + +## Features + +- Reads Modbus holding registers from a heat pump +- Caches readings for 60 seconds to reduce load +- Sends temperature and pump status data to InfluxDB every minute +- Configurable via JSON file + +## Configuration + +Edit `src/main/resources/registryMapping.json` to configure: + +- Modbus TCP connection (address, port) +- InfluxDB connection (address, username, password) +- List of registers to read with descriptions + +## Build and Run + +```bash +mvn clean package +java -jar target/home-pump-1.0.0.jar +``` + +## Requirements + +- Java 21 +- Modbus TCP accessible heat pump +- InfluxDB instance diff --git a/pom.xml b/pom.xml index cdf7e9a..ad2a634 100644 --- a/pom.xml +++ b/pom.xml @@ -3,9 +3,9 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 - org.example - test.com - 1.0-SNAPSHOT + com.home + home-pump + 1.0.0 com.fasterxml.jackson.core @@ -15,12 +15,12 @@ org.mockito mockito-junit-jupiter - 4.2.0 + 5.8.0 test org.slf4j - slf4j-api + slf4j-simple 1.7.32 @@ -41,13 +41,13 @@ org.junit.jupiter junit-jupiter - 5.8.2 + 5.10.0 test org.projectlombok lombok - 1.18.20 + 1.18.30 provided @@ -116,27 +116,15 @@ maven-compiler-plugin 3.8.1 - 11 - 11 + 21 + 21 UTF-8 - - -J--add-opens=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED - -J--add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED - -J--add-opens=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED - -J--add-opens=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED - -J--add-opens=jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED - -J--add-opens=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED - -J--add-opens=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED - -J--add-opens=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED - -J--add-opens=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED - -J--add-opens=jdk.compiler/com.sun.tools.javac.jvm=ALL-UNNAMED - true org.projectlombok lombok - 1.18.20 + 1.18.30 diff --git a/src/main/java/home/automation/Main.java b/src/main/java/home/automation/Main.java index 62959dd..138495e 100644 --- a/src/main/java/home/automation/Main.java +++ b/src/main/java/home/automation/Main.java @@ -1,15 +1,14 @@ package home.automation; import home.automation.configuration.JsonFileBasedConfiguration; +import home.automation.configuration.RegistryConfiguration; import home.automation.configuration.RegistryMapping; import home.automation.influxdb.InfluxConnection; import home.automation.influxdb.InfluxMetricModbusSender; -import home.automation.modbus.ModbusCachedWrapper; import home.automation.modbus.ModbusConnection; import home.automation.modbus.ModbusRtuTcpRepository; import home.automation.modbus.ModbusService; -import java.time.Duration; import java.util.List; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; @@ -21,22 +20,17 @@ public class Main { final JsonFileBasedConfiguration jsonFileBasedConfiguration = new JsonFileBasedConfiguration(); - final List registryMappings = jsonFileBasedConfiguration + final RegistryConfiguration config = jsonFileBasedConfiguration .getConfig() - .orElseThrow(() -> new Exception("Missing configuration file")) - .getRegistry_mapping(); + .orElseThrow(() -> new Exception("Missing configuration file")); - final InfluxConnection connection = new InfluxConnection( - jsonFileBasedConfiguration.getConfig() - .get() - .getInflux_db() - ); + final List registryMappings = config.getRegistry_mapping(); + + final InfluxConnection connection = new InfluxConnection(config.getInflux_db()); ModbusService service = new ModbusService( - new ModbusCachedWrapper(new ModbusRtuTcpRepository( - new ModbusConnection( - new JsonFileBasedConfiguration() - ), registryMappings), Duration.ofSeconds(60)) + new ModbusRtuTcpRepository( + new ModbusConnection(jsonFileBasedConfiguration), registryMappings) ); ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); diff --git a/src/main/java/home/automation/influxdb/InfluxMetricModbusSender.java b/src/main/java/home/automation/influxdb/InfluxMetricModbusSender.java index f73cbd2..578b3cd 100644 --- a/src/main/java/home/automation/influxdb/InfluxMetricModbusSender.java +++ b/src/main/java/home/automation/influxdb/InfluxMetricModbusSender.java @@ -2,6 +2,7 @@ package home.automation.influxdb; import home.automation.modbus.ModbusService; import home.automation.modbus.Registry; +import lombok.extern.slf4j.Slf4j; import org.influxdb.InfluxDB; import org.influxdb.dto.Point; import org.influxdb.dto.Query; @@ -9,6 +10,7 @@ import org.influxdb.dto.Query; import java.util.List; import java.util.concurrent.TimeUnit; +@Slf4j public class InfluxMetricModbusSender implements MetricSender, Runnable { private final ModbusService modbusService; private final InfluxDB influxConnection; @@ -26,7 +28,7 @@ public class InfluxMetricModbusSender implements MetricSender, Runnabl @Override public boolean send(List metricsToSend) { - modbusService.readAll().forEach(registry -> + metricsToSend.forEach(registry -> influxConnection.write( Point.measurement("POMPA") .addField(registry.getDesc(), Double.valueOf(registry.getValue())) @@ -40,7 +42,15 @@ public class InfluxMetricModbusSender implements MetricSender, Runnabl @Override public void run() { - final List registries = modbusService.readAll(); - send(registries); + try { + final List registries = modbusService.readAll(); + if (!registries.isEmpty()) { + send(registries); + } else { + log.warn("No registries read from Modbus"); + } + } catch (Exception e) { + log.error("Error in scheduled Modbus reading", e); + } } } diff --git a/src/main/java/home/automation/modbus/ModbusRtuTcpRepository.java b/src/main/java/home/automation/modbus/ModbusRtuTcpRepository.java index 5c6df86..c0a0120 100644 --- a/src/main/java/home/automation/modbus/ModbusRtuTcpRepository.java +++ b/src/main/java/home/automation/modbus/ModbusRtuTcpRepository.java @@ -6,6 +6,7 @@ import com.serotonin.modbus4j.locator.BaseLocator; import com.serotonin.modbus4j.msg.ReadHoldingRegistersRequest; import com.serotonin.modbus4j.msg.ReadHoldingRegistersResponse; import home.automation.configuration.RegistryMapping; +import lombok.extern.slf4j.Slf4j; import org.jetbrains.annotations.NotNull; import java.util.Collections; @@ -14,10 +15,9 @@ import java.util.stream.Collectors; import static com.serotonin.modbus4j.code.DataType.TWO_BYTE_INT_SIGNED; +@Slf4j public class ModbusRtuTcpRepository implements Repository { - private static final int ZERO = 0; - private static final int HALF_SHORT_INT = 32768; private final Connection connection; private final List registryMappings; @@ -27,11 +27,7 @@ public class ModbusRtuTcpRepository implements Repository { } private String normalizeRegistryValue(Short value) { - if (ZERO > value) { - return String.valueOf(-(value + HALF_SHORT_INT) / 10.0); - } else { - return String.valueOf((value / 10.0)); - } + return String.valueOf(value / 10.0); } @Override @@ -39,14 +35,14 @@ public class ModbusRtuTcpRepository implements Repository { BatchRead requestBatchRead = new BatchRead<>(); registryMappings.forEach(elem -> requestBatchRead.addLocator( elem.getRegistry(), - BaseLocator.holdingRegister(1, elem.getRegistry(), TWO_BYTE_INT_SIGNED) + BaseLocator.holdingRegister(1, elem.getRegistry() - 1, TWO_BYTE_INT_SIGNED) )); try { final BatchResults send = connection.getConnection().send(requestBatchRead); return registryMappings.stream() .map(mapping -> { - final Double doubleValue = send.getDoubleValue(mapping.getRegistry()); + final Double doubleValue = send.getDoubleValue(mapping.getRegistry()) / 10.0; return Registry.builder() .registry(mapping.getRegistry()) .value(doubleValue.toString()) @@ -55,7 +51,7 @@ public class ModbusRtuTcpRepository implements Repository { } ).collect(Collectors.toList()); } catch (Exception e) { - e.printStackTrace(); + log.error("Cannot get all registries due to: ", e); return Collections.emptyList(); } } @@ -72,13 +68,14 @@ public class ModbusRtuTcpRepository implements Repository { .desc(mapping.getDesc()) .build(); } catch (Exception e) { + log.error("Cannot get registry {} due to: ", register, e); throw new RuntimeException(e.getMessage()); } } @NotNull private ReadHoldingRegistersResponse getRegistry(int register) throws Exception { - ReadHoldingRegistersRequest request = new ReadHoldingRegistersRequest(1, register, 1); + ReadHoldingRegistersRequest request = new ReadHoldingRegistersRequest(1, register - 1, 1); ReadHoldingRegistersResponse response = (ReadHoldingRegistersResponse) connection.getConnection().send(request); if (response.isException()) { diff --git a/src/main/resources/registryMapping.json b/src/main/resources/registryMapping.json index 5fc24ea..a5ef8ef 100644 --- a/src/main/resources/registryMapping.json +++ b/src/main/resources/registryMapping.json @@ -79,10 +79,6 @@ "registry": 4119, "desc": "WyCHL" }, - { - "registry": 4116, - "desc": "Pompa CCW" - }, { "registry": 4116, "desc": "Pompa CCW" diff --git a/src/test/java/home/automation/modbus/ModbusCachedWrapperTests.java b/src/test/java/home/automation/modbus/ModbusCachedWrapperTests.java deleted file mode 100644 index dde43d0..0000000 --- a/src/test/java/home/automation/modbus/ModbusCachedWrapperTests.java +++ /dev/null @@ -1,81 +0,0 @@ -package home.automation.modbus; - -import org.junit.jupiter.api.Test; - -import java.time.Duration; -import java.util.List; - -import static java.lang.Thread.sleep; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotEquals; - -class ModbusCachedWrapperTests { - - public static final int MAX_TESTS = 10; - private final RepositoryInMemory repository = new RepositoryInMemory(); - private final ModbusCachedWrapper cachedRepository = new ModbusCachedWrapper(repository, Duration.ofSeconds(1)); - - @Test - public void givenRepositoryInMemoryAndCacheDurationOf1Seconds_WhenGetRegistryFromCachedRepository2times_ThenValuesAreTheSame() { - final Registry firstTry = cachedRepository.get(4065); - final Registry secondTry = cachedRepository.get(4065); - assert (firstTry.getValue().equals(secondTry.getValue())); - } - - @Test - public void givenRepositoryInMemoryAndCacheDurationOf1Seconds_WhenGetRegistryFromCachedRepository2timesWithDelayMoreThan1Sec_ThenValuesAreTheDifferent() throws InterruptedException { - final Registry firstTry = cachedRepository.get(4065); - final long currentTimeMillis = System.currentTimeMillis(); - final long desireTimeoutInMs = 1000; - while (desireTimeoutInMs > System.currentTimeMillis() - currentTimeMillis) { - sleep(510); - } - final Registry secondTry = cachedRepository.get(4065); - assert (!firstTry.getValue().equals(secondTry.getValue())); - } - - @Test - public void givenRepositoryInMemoryAndCacheDurationOf3Seconds_WhenGetRegistryFromCachedRepositoryMultipleTimesWithIn3SecondDelay_ThenAllValuesAreTheSame() throws InterruptedException { - final Registry firstTry = cachedRepository.get(4065); - final long currentTimeMillis = System.currentTimeMillis(); - final long desireTimeoutInMs = 500; - int count = 0; - while (desireTimeoutInMs > System.currentTimeMillis() - currentTimeMillis || count == MAX_TESTS) { - count++; - final Registry secondTry = cachedRepository.get(4065); - assert (firstTry.getValue().equals(secondTry.getValue())); - } - } - - @Test - public void givenRepositoryInMemoryAndCacheDurationOf3Seconds_WhenGetAllFromCachedRepository2times_ThenValuesAreTheSame() { - final List firstTry = cachedRepository.getAll(); - final List secondTry = cachedRepository.getAll(); - assertEquals(firstTry, secondTry); - } - - @Test - public void givenRepositoryInMemoryAndCacheDurationOf3Seconds_WhenGetAllFromCachedRepository2timesWith4SecondDelay_ThenValuesAreTheDifferent() throws InterruptedException { - final List firstTry = cachedRepository.getAll(); - final long currentTimeMillis = System.currentTimeMillis(); - final long desireTimeoutInMs = 1000; - while (desireTimeoutInMs > System.currentTimeMillis() - currentTimeMillis) { - sleep(510); - } - final List secondTry = cachedRepository.getAll(); - assertNotEquals(firstTry, secondTry); - } - - @Test - public void givenRepositoryInMemoryAndCacheDurationOf3Seconds_WhenGetAllFromCachedRepositoryMultipleTimesWithIn3SecondDelay_ThenAllValuesAreTheSame() throws InterruptedException { - final List firstTry = cachedRepository.getAll(); - final long currentTimeMillis = System.currentTimeMillis(); - final long desireTimeoutInMs = 500; - int count = 0; - while (desireTimeoutInMs > System.currentTimeMillis() - currentTimeMillis || count == MAX_TESTS) { - count++; - final List secondTry = cachedRepository.getAll(); - assertEquals(firstTry, secondTry); - } - } -} \ No newline at end of file diff --git a/src/test/java/home/automation/modbus/ModbusRtuTcpRepositoryTests.java b/src/test/java/home/automation/modbus/ModbusRtuTcpRepositoryTests.java index 2639a7e..0bbe335 100644 --- a/src/test/java/home/automation/modbus/ModbusRtuTcpRepositoryTests.java +++ b/src/test/java/home/automation/modbus/ModbusRtuTcpRepositoryTests.java @@ -49,7 +49,7 @@ class ModbusRtuTcpRepositoryTests { @Test public void givenModbusRepositoryWhenGetRegistryIsNegativeValueIsNormalizedAndReturned() throws Exception { when(response.isException()).thenReturn(false); - short[] returnValue = {parseShort("-32668")}; + short[] returnValue = {parseShort("-100")}; when(response.getShortData()).thenReturn(returnValue); when(modbusMaster.send(any(ModbusRequest.class))).thenReturn(response); assert ("-10.0".equals(repository.get(4201).getValue()));