Import legacy sensor map into sensor definitions
This commit is contained in:
@@ -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
|
||||||
|
) {
|
||||||
|
}
|
||||||
+20
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
+93
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+22
@@ -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
Reference in New Issue
Block a user