Import legacy sensor map into sensor definitions

This commit is contained in:
litoral05
2026-05-19 15:46:38 +01:00
parent ac62b554f5
commit 4c4025f37f
7 changed files with 2065 additions and 0 deletions
@@ -0,0 +1,9 @@
package com.litoralregas.backend.sensor.dto;
public record SensorDefinitionImportResult(
int totalLines,
int imported,
int skippedExisting,
int skippedBlank
) {
}
@@ -0,0 +1,16 @@
package com.litoralregas.backend.sensor.dto;
import com.litoralregas.backend.sensor.SensorSourceType;
import com.litoralregas.backend.sensor.SensorValueType;
public record SensorDefinitionImportRow(
String name,
Integer modbusAddress,
Integer bitOffset,
SensorValueType valueType,
String unit,
Integer decimalPlaces,
String category,
SensorSourceType sourceType
) {
}
@@ -0,0 +1,20 @@
package com.litoralregas.backend.sensor.importer;
import com.litoralregas.backend.sensor.dto.SensorDefinitionImportResult;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class SensorDefinitionImportController {
private final SensorDefinitionImportService importService;
public SensorDefinitionImportController(SensorDefinitionImportService importService) {
this.importService = importService;
}
@PostMapping("/api/sensor-definition-import/run")
public SensorDefinitionImportResult runImport() {
return importService.importSensorMap();
}
}
@@ -0,0 +1,93 @@
package com.litoralregas.backend.sensor.importer;
import com.litoralregas.backend.sensor.SensorDefinition;
import com.litoralregas.backend.sensor.SensorDefinitionRepository;
import com.litoralregas.backend.sensor.dto.SensorDefinitionImportResult;
import com.litoralregas.backend.sensor.dto.SensorDefinitionImportRow;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.nio.charset.StandardCharsets;
import java.util.List;
@Service
public class SensorDefinitionImportService {
private static final String SENSOR_MAP_PATH = "config/sensor-map.txt";
private static final int DEFAULT_POLLING_INTERVAL_SECONDS = 2;
private final SensorDefinitionMapParser parser;
private final SensorDefinitionRepository repository;
public SensorDefinitionImportService(
SensorDefinitionMapParser parser,
SensorDefinitionRepository repository
) {
this.parser = parser;
this.repository = repository;
}
@Transactional
public SensorDefinitionImportResult importSensorMap() {
List<String> lines = readSensorMapLines();
int imported = 0;
int skippedExisting = 0;
int skippedBlank = 0;
for (String line : lines) {
if (line == null || line.isBlank()) {
skippedBlank++;
continue;
}
SensorDefinitionImportRow row = parser.parseLine(line)
.orElseThrow(() -> new IllegalArgumentException("Invalid empty sensor row."));
if (repository.existsByName(row.name())) {
skippedExisting++;
continue;
}
SensorDefinition sensorDefinition = new SensorDefinition(
row.name(),
row.modbusAddress(),
row.bitOffset(),
row.valueType(),
row.unit(),
row.decimalPlaces(),
row.category(),
row.sourceType(),
DEFAULT_POLLING_INTERVAL_SECONDS,
true
);
repository.save(sensorDefinition);
imported++;
}
return new SensorDefinitionImportResult(
lines.size(),
imported,
skippedExisting,
skippedBlank
);
}
private List<String> readSensorMapLines() {
try {
ClassPathResource resource = new ClassPathResource(SENSOR_MAP_PATH);
if (!resource.exists()) {
throw new IllegalStateException("Sensor map file not found: " + SENSOR_MAP_PATH);
}
return resource.getContentAsString(StandardCharsets.UTF_8)
.lines()
.toList();
} catch (Exception exception) {
throw new IllegalStateException("Failed to read sensor map file.", exception);
}
}
}
@@ -0,0 +1,22 @@
package com.litoralregas.backend.sensor.importer;
import com.litoralregas.backend.sensor.dto.SensorDefinitionImportRow;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class SensorDefinitionImportTestController {
private final SensorDefinitionMapParser parser;
public SensorDefinitionImportTestController(SensorDefinitionMapParser parser) {
this.parser = parser;
}
@GetMapping("/api/sensor-definition-import/parse-line")
public SensorDefinitionImportRow parseLine(@RequestParam String line) {
return parser.parseLine(line)
.orElseThrow(() -> new IllegalArgumentException("Line is empty."));
}
}
@@ -0,0 +1,96 @@
package com.litoralregas.backend.sensor.importer;
import com.litoralregas.backend.sensor.SensorSourceType;
import com.litoralregas.backend.sensor.SensorValueType;
import com.litoralregas.backend.sensor.dto.SensorDefinitionImportRow;
import org.springframework.stereotype.Component;
import java.util.Optional;
@Component
public class SensorDefinitionMapParser {
public Optional<SensorDefinitionImportRow> parseLine(String line) {
if (line == null || line.isBlank()) {
return Optional.empty();
}
String[] parts = line.split("\\*");
if (parts.length != 5) {
throw new IllegalArgumentException("Invalid sensor map line: " + line);
}
String name = parts[0].trim();
String addressPart = parts[1].trim();
Integer decimalPlaces = Integer.parseInt(parts[2].trim());
String unit = normalizeUnit(parts[3].trim());
String category = mapCategory(parts[4].trim());
ParsedAddress parsedAddress = parseAddress(addressPart);
SensorSourceType sourceType = parsedAddress.modbusAddress() < 0
? SensorSourceType.CALCULATED
: SensorSourceType.MODBUS;
SensorValueType valueType = parsedAddress.bitOffset() != null
? SensorValueType.BOOLEAN
: decimalPlaces > 0 ? SensorValueType.DECIMAL : SensorValueType.INTEGER;
return Optional.of(new SensorDefinitionImportRow(
name,
parsedAddress.modbusAddress(),
parsedAddress.bitOffset(),
valueType,
unit,
decimalPlaces,
category,
sourceType
));
}
private ParsedAddress parseAddress(String addressPart) {
if (addressPart.contains(",")) {
String[] addressParts = addressPart.split(",");
if (addressParts.length != 2) {
throw new IllegalArgumentException("Invalid bit address: " + addressPart);
}
return new ParsedAddress(
Integer.parseInt(addressParts[0].trim()),
Integer.parseInt(addressParts[1].trim())
);
}
return new ParsedAddress(
Integer.parseInt(addressPart),
null
);
}
private String normalizeUnit(String unit) {
if (unit == null || unit.isBlank() || unit.equalsIgnoreCase("SU")) {
return null;
}
return unit;
}
private String mapCategory(String categoryCode) {
return switch (categoryCode.toLowerCase()) {
case "c" -> "CLIMATE";
case "r" -> "IRRIGATION";
case "i" -> "LIGHTING";
case "h" -> "HYDRO";
case "a" -> "AEROPONICS";
default -> "UNKNOWN";
};
}
private record ParsedAddress(
Integer modbusAddress,
Integer bitOffset
) {
}
}
File diff suppressed because it is too large Load Diff