This commit is contained in:
root committed 2025-12-24 22:43:00 +01:00
1 parent 46030746eb
commit 4a84ae041f
8 files changed
+71 -137

No files matched your search

+31 -1
View File
@@ -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
+10 -22
View File
@@ -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">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>test.com</artifactId>
<version>1.0-SNAPSHOT</version>
<groupId>com.home</groupId>
<artifactId>home-pump</artifactId>
<version>1.0.0</version>
<dependencies>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
@@ -15,12 +15,12 @@
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>4.2.0</version>
<version>5.8.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<artifactId>slf4j-simple</artifactId>
<version>1.7.32</version>
</dependency>
<dependency>
@@ -41,13 +41,13 @@
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.8.2</version>
<version>5.10.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.20</version>
<version>1.18.30</version>
<scope>provided</scope>
</dependency>
</dependencies>
@@ -116,27 +116,15 @@
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<release>11</release>
<source>11</source>
<release>21</release>
<source>21</source>
<encoding>UTF-8</encoding>
<compilerArgs>
<arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED</arg>
<arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED</arg>
<arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED</arg>
<arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED</arg>
<arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED</arg>
<arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED</arg>
<arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED</arg>
<arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED</arg>
<arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED</arg>
<arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.jvm=ALL-UNNAMED</arg>
</compilerArgs>
<showWarnings>true</showWarnings>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.20</version>
<version>1.18.30</version>
</path>
</annotationProcessorPaths>
</configuration>
+8 -14
View File
@@ -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<RegistryMapping> 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<RegistryMapping> 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();
@@ -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<Registry>, Runnable {
private final ModbusService modbusService;
private final InfluxDB influxConnection;
@@ -26,7 +28,7 @@ public class InfluxMetricModbusSender implements MetricSender<Registry>, Runnabl
@Override
public boolean send(List<Registry> 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<Registry>, Runnabl
@Override
public void run() {
final List<Registry> registries = modbusService.readAll();
send(registries);
try {
final List<Registry> 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);
}
}
}
@@ -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<RegistryMapping> 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<Integer> 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<Integer> 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()) {
-4
View File
@@ -79,10 +79,6 @@
"registry": 4119,
"desc": "WyCHL"
},
{
"registry": 4116,
"desc": "Pompa CCW"
},
{
"registry": 4116,
"desc": "Pompa CCW"
@@ -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<Registry> firstTry = cachedRepository.getAll();
final List<Registry> secondTry = cachedRepository.getAll();
assertEquals(firstTry, secondTry);
}
@Test
public void givenRepositoryInMemoryAndCacheDurationOf3Seconds_WhenGetAllFromCachedRepository2timesWith4SecondDelay_ThenValuesAreTheDifferent() throws InterruptedException {
final List<Registry> firstTry = cachedRepository.getAll();
final long currentTimeMillis = System.currentTimeMillis();
final long desireTimeoutInMs = 1000;
while (desireTimeoutInMs > System.currentTimeMillis() - currentTimeMillis) {
sleep(510);
}
final List<Registry> secondTry = cachedRepository.getAll();
assertNotEquals(firstTry, secondTry);
}
@Test
public void givenRepositoryInMemoryAndCacheDurationOf3Seconds_WhenGetAllFromCachedRepositoryMultipleTimesWithIn3SecondDelay_ThenAllValuesAreTheSame() throws InterruptedException {
final List<Registry> 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<Registry> secondTry = cachedRepository.getAll();
assertEquals(firstTry, secondTry);
}
}
}
@@ -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()));