test(gateway): run the assembled gateway against a fake controller and Influx
Asserts the POMPA point keeps exactly the 8 legacy field names with their values, heat_pump is written, and /health and /snapshot answer. MockModbusServer becomes public to be used from another package. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
5b75c3b181
commit
e591c125cc
2 files changed
+160
-4
No files matched your search
@@ -0,0 +1,156 @@
|
||||
package home.gateway;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.sun.net.httpserver.HttpServer;
|
||||
import home.gateway.adapters.heatpump.HeatPumpSettings;
|
||||
import home.gateway.adapters.heatpump.RegisterBlock;
|
||||
import home.gateway.adapters.modbus.MockModbusServer;
|
||||
import home.gateway.config.GatewayConfig;
|
||||
import home.gateway.config.GatewayConfigLoader;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.ServerSocket;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Clock;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/** The assembled gateway against a fake controller and a fake InfluxDB, both on localhost. */
|
||||
class GatewayEndToEndTest {
|
||||
|
||||
private static final Set<String> LEGACY_FIELDS = Set.of(
|
||||
"External\\ temperature=-4.3", "Temp\\ wew=21.5", "Temp\\ co=35.0", "Temp\\ cwu=48.0",
|
||||
"Temp\\ in=8.5", "Temp\\ out=5.5", "Temp\\ pc\\ out=17.0", "Temp\\ zsr=5.2");
|
||||
|
||||
private final ObjectMapper json = new ObjectMapper();
|
||||
private final HttpClient http = HttpClient.newHttpClient();
|
||||
private final List<String> influxWrites = new CopyOnWriteArrayList<>();
|
||||
private MockModbusServer controller;
|
||||
private HttpServer influx;
|
||||
private Gateway gateway;
|
||||
private int gatewayPort;
|
||||
|
||||
@TempDir
|
||||
Path dir;
|
||||
|
||||
@BeforeEach
|
||||
void startFakes() throws IOException {
|
||||
controller = new MockModbusServer();
|
||||
for (RegisterBlock block : HeatPumpSettings.PRODUCTION_BLOCKS) {
|
||||
for (int address = block.start(); address < block.start() + block.count(); address++) {
|
||||
controller.holding(address, 0);
|
||||
}
|
||||
}
|
||||
controller.holding(4065, 0x8000 | 43);
|
||||
controller.holding(4066, 215);
|
||||
controller.holding(4067, 350);
|
||||
controller.holding(4069, 480);
|
||||
controller.holding(4070, 85);
|
||||
controller.holding(4071, 55);
|
||||
controller.holding(4072, 170);
|
||||
controller.holding(4199, 52);
|
||||
controller.holding(4143, 38);
|
||||
|
||||
influx = HttpServer.create(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0), 0);
|
||||
influx.createContext("/write", exchange -> {
|
||||
influxWrites.add(exchange.getRequestURI().getQuery() + "\n"
|
||||
+ new String(exchange.getRequestBody().readAllBytes(), StandardCharsets.UTF_8));
|
||||
exchange.sendResponseHeaders(204, -1);
|
||||
exchange.close();
|
||||
});
|
||||
influx.start();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void stopFakes() throws IOException {
|
||||
if (gateway != null) {
|
||||
gateway.close();
|
||||
}
|
||||
influx.stop(0);
|
||||
controller.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void polledHeatPumpReachesInfluxAndTheApi() throws Exception {
|
||||
gateway = Gateway.start(loadConfig(), Clock.systemUTC(), () -> { });
|
||||
|
||||
waitForFirstPoll();
|
||||
assertEquals(200, get("/health").statusCode());
|
||||
assertEquals("heat-pump", json.readTree(get("/snapshot").body()).get(0).get("name").asText());
|
||||
|
||||
gateway.close();
|
||||
List<String> lines = influxLines();
|
||||
assertTrue(influxWrites.get(0).startsWith("db=Dom_Pompa&"), influxWrites.get(0));
|
||||
assertEquals(LEGACY_FIELDS, fieldsOf(lineOf(lines, "POMPA")));
|
||||
Set<String> heatPump = fieldsOf(lineOf(lines, "heat_pump"));
|
||||
assertTrue(heatPump.contains("setpoint_cwu_economy=38.0"), heatPump.toString());
|
||||
assertTrue(heatPump.contains("outdoor_temp=-4.3"), heatPump.toString());
|
||||
}
|
||||
|
||||
private Path configFile() throws IOException {
|
||||
try (ServerSocket free = new ServerSocket(0)) {
|
||||
gatewayPort = free.getLocalPort();
|
||||
}
|
||||
return Files.writeString(dir.resolve("gateway.json"), """
|
||||
{"influx": {"baseUri": "http://localhost:%d", "spoolDirectory": "%s"},
|
||||
"http": {"port": %d},
|
||||
"devices": {"heatPump": {"enabled": true,
|
||||
"connection": {"host": "localhost", "port": %d, "minimumPauseMs": 1}}}}"""
|
||||
.formatted(influx.getAddress().getPort(), dir.resolve("spool"), gatewayPort,
|
||||
controller.port()));
|
||||
}
|
||||
|
||||
private GatewayConfig loadConfig() throws IOException {
|
||||
return GatewayConfigLoader.load(configFile());
|
||||
}
|
||||
|
||||
private void waitForFirstPoll() throws Exception {
|
||||
for (int attempt = 0; attempt < 100; attempt++) {
|
||||
JsonNode devices = json.readTree(get("/snapshot").body());
|
||||
if (devices.size() == 1 && !devices.get(0).get("lastSuccess").isNull()) {
|
||||
return;
|
||||
}
|
||||
Thread.sleep(100);
|
||||
}
|
||||
throw new AssertionError("the heat pump was not polled in 10 s");
|
||||
}
|
||||
|
||||
private HttpResponse<String> get(String path) throws Exception {
|
||||
return http.send(HttpRequest.newBuilder(URI.create("http://localhost:" + gatewayPort + path)).build(),
|
||||
HttpResponse.BodyHandlers.ofString());
|
||||
}
|
||||
|
||||
private List<String> influxLines() {
|
||||
return influxWrites.stream().flatMap(write -> Arrays.stream(write.split("\n")).skip(1)).toList();
|
||||
}
|
||||
|
||||
private static String lineOf(List<String> lines, String measurement) {
|
||||
return lines.stream().filter(line -> line.startsWith(measurement + " ")).findFirst()
|
||||
.orElseThrow(() -> new AssertionError("no " + measurement + " line in " + lines));
|
||||
}
|
||||
|
||||
/** The field part of {@code measurement fields timestamp}; no field key here contains a comma. */
|
||||
private static Set<String> fieldsOf(String line) {
|
||||
String fields = line.substring(line.indexOf(' ') + 1, line.lastIndexOf(' '));
|
||||
return Arrays.stream(fields.split(",")).collect(Collectors.toSet());
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@ import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
/** Local Modbus TCP fake answering FC3 and FC4; unknown addresses get exception code 2. */
|
||||
final class MockModbusServer implements Closeable {
|
||||
public final class MockModbusServer implements Closeable {
|
||||
|
||||
private final ServerSocket server;
|
||||
private final Map<Integer, Integer> holding = new ConcurrentHashMap<>();
|
||||
@@ -24,12 +24,12 @@ final class MockModbusServer implements Closeable {
|
||||
private volatile boolean wrongTransactionId;
|
||||
private volatile boolean overlapped;
|
||||
|
||||
MockModbusServer() throws IOException {
|
||||
public MockModbusServer() throws IOException {
|
||||
server = new ServerSocket(0, 50, InetAddress.getLoopbackAddress());
|
||||
Thread.ofVirtual().start(this::acceptLoop);
|
||||
}
|
||||
|
||||
int port() {
|
||||
public int port() {
|
||||
return server.getLocalPort();
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ final class MockModbusServer implements Closeable {
|
||||
return connections.get();
|
||||
}
|
||||
|
||||
void holding(int address, int value) {
|
||||
public void holding(int address, int value) {
|
||||
holding.put(address, value);
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user