changes
This commit is contained in:
1 parent
46030746eb
commit
4a84ae041f
8 files changed
+71
-137
No files matched your search
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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()));
|
||||
|
||||
Reference in new issue
Block a user