Refactor telemetry registry to use sensor definitions .json and historian identity
This commit is contained in:
@@ -76,7 +76,9 @@ public class BlockPollingService {
|
||||
|
||||
telemetryCache.put(new TelemetrySnapshot(
|
||||
sensor.getId(),
|
||||
sensor.getKey(),
|
||||
sensor.getName(),
|
||||
sensor.getCategory(),
|
||||
sensor.getModbusAddress(),
|
||||
sensor.getBitOffset(),
|
||||
rawValue,
|
||||
|
||||
+32
-5
@@ -5,6 +5,9 @@ import com.litoralregas.backend.acquisition.block.BlockPollingService;
|
||||
import com.litoralregas.backend.dashboard.DashboardOverviewResponse;
|
||||
import com.litoralregas.backend.dashboard.DashboardOverviewService;
|
||||
import com.litoralregas.backend.historian.HistorianService;
|
||||
import com.litoralregas.backend.modules.climate.ClimateModuleResponse;
|
||||
import com.litoralregas.backend.modules.climate.ClimateModuleService;
|
||||
import com.litoralregas.backend.modules.climate.websocket.ClimateModuleWebSocketPublisher;
|
||||
import com.litoralregas.backend.modules.meteo.MeteoModuleResponse;
|
||||
import com.litoralregas.backend.modules.meteo.websocket.MeteoModuleWebSocketPublisher;
|
||||
import com.litoralregas.backend.websocket.dashboard.DashboardOverviewWebSocketPublisher;
|
||||
@@ -35,6 +38,9 @@ public class AcquisitionSchedulerService {
|
||||
|
||||
private final AtomicBoolean polling = new AtomicBoolean(false);
|
||||
|
||||
private final ClimateModuleWebSocketPublisher climateModuleWebSocketPublisher;
|
||||
private final ClimateModuleService climateModuleService;
|
||||
|
||||
public AcquisitionSchedulerService(
|
||||
BlockPollingService blockPollingService,
|
||||
AcquisitionSchedulerProperties properties,
|
||||
@@ -44,7 +50,9 @@ public class AcquisitionSchedulerService {
|
||||
MeteoModuleWebSocketPublisher meteoModuleWebSocketPublisher,
|
||||
HistorianService historianService,
|
||||
DashboardOverviewService dashboardOverviewService,
|
||||
MeteoModuleService meteoModuleService
|
||||
MeteoModuleService meteoModuleService,
|
||||
ClimateModuleWebSocketPublisher climateModuleWebSocketPublisher,
|
||||
ClimateModuleService climateModuleService
|
||||
) {
|
||||
this.blockPollingService = blockPollingService;
|
||||
this.properties = properties;
|
||||
@@ -55,6 +63,8 @@ public class AcquisitionSchedulerService {
|
||||
this.historianService = historianService;
|
||||
this.dashboardOverviewService = dashboardOverviewService;
|
||||
this.meteoModuleService = meteoModuleService;
|
||||
this.climateModuleWebSocketPublisher = climateModuleWebSocketPublisher;
|
||||
this.climateModuleService = climateModuleService;
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
@@ -94,14 +104,31 @@ public class AcquisitionSchedulerService {
|
||||
|
||||
telemetryWebSocketPublisher.publishLatestTelemetry();
|
||||
|
||||
DashboardOverviewResponse overview = dashboardOverviewService.getOverview();
|
||||
historianService.recordDashboardOverview(overview);
|
||||
DashboardOverviewResponse overview =
|
||||
dashboardOverviewService.getOverview();
|
||||
|
||||
dashboardOverviewWebSocketPublisher.publishOverview(overview);
|
||||
|
||||
MeteoModuleResponse meteo = meteoModuleService.getLatest();
|
||||
historianService.recordModuleSensors("meteo", meteo.sensors(), meteo.timestamp());
|
||||
MeteoModuleResponse meteo =
|
||||
meteoModuleService.getLatest();
|
||||
|
||||
historianService.recordModuleSensors(
|
||||
meteo.sensors(),
|
||||
meteo.timestamp()
|
||||
);
|
||||
|
||||
meteoModuleWebSocketPublisher.publishLatest(meteo);
|
||||
|
||||
ClimateModuleResponse climate =
|
||||
climateModuleService.getLatest();
|
||||
|
||||
historianService.recordModuleSensors(
|
||||
climate.sensors(),
|
||||
climate.timestamp()
|
||||
);
|
||||
|
||||
climateModuleWebSocketPublisher.publishLatest(climate);
|
||||
|
||||
} catch (Exception exception) {
|
||||
runtimeStatus.setLastError(exception.getMessage());
|
||||
|
||||
|
||||
+32
-9
@@ -1,7 +1,5 @@
|
||||
package com.litoralregas.backend.acquisition.telemetry;
|
||||
|
||||
import com.litoralregas.backend.acquisition.telemetry.TelemetryCache;
|
||||
import com.litoralregas.backend.acquisition.telemetry.TelemetrySnapshot;
|
||||
import com.litoralregas.backend.modbus.LrModbusClient;
|
||||
import com.litoralregas.backend.modbus.ModbusReadResult;
|
||||
import com.litoralregas.backend.modbus.ModbusUnit;
|
||||
@@ -32,11 +30,19 @@ public class SensorTelemetryReader {
|
||||
}
|
||||
|
||||
public TelemetrySnapshot readSensor(Integer sensorId) {
|
||||
SensorDefinition sensorDefinition = sensorDefinitionRepository.findById(sensorId)
|
||||
.orElseThrow(() -> new EntityNotFoundException("Sensor definition not found: " + sensorId));
|
||||
|
||||
SensorDefinition sensorDefinition =
|
||||
sensorDefinitionRepository.findById(sensorId)
|
||||
.orElseThrow(() ->
|
||||
new EntityNotFoundException(
|
||||
"Sensor definition not found: " + sensorId
|
||||
)
|
||||
);
|
||||
|
||||
if (sensorDefinition.getSourceType() != SensorSourceType.MODBUS) {
|
||||
throw new IllegalArgumentException("Only MODBUS sensors can be read directly.");
|
||||
throw new IllegalArgumentException(
|
||||
"Only MODBUS sensors can be read directly."
|
||||
);
|
||||
}
|
||||
|
||||
ModbusReadResult result = modbusClient.readInputRegisters(
|
||||
@@ -46,11 +52,17 @@ public class SensorTelemetryReader {
|
||||
);
|
||||
|
||||
Integer rawValue = result.values().getFirst();
|
||||
Object value = convertValue(sensorDefinition, rawValue);
|
||||
|
||||
Object value = convertValue(
|
||||
sensorDefinition,
|
||||
rawValue
|
||||
);
|
||||
|
||||
TelemetrySnapshot snapshot = new TelemetrySnapshot(
|
||||
sensorDefinition.getId(),
|
||||
sensorDefinition.getKey(),
|
||||
sensorDefinition.getName(),
|
||||
sensorDefinition.getCategory(),
|
||||
sensorDefinition.getModbusAddress(),
|
||||
sensorDefinition.getBitOffset(),
|
||||
rawValue,
|
||||
@@ -64,19 +76,30 @@ public class SensorTelemetryReader {
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
private Object convertValue(SensorDefinition sensorDefinition, Integer rawValue) {
|
||||
private Object convertValue(
|
||||
SensorDefinition sensorDefinition,
|
||||
Integer rawValue
|
||||
) {
|
||||
|
||||
if (sensorDefinition.getValueType() == SensorValueType.BOOLEAN) {
|
||||
|
||||
Integer bitOffset = sensorDefinition.getBitOffset();
|
||||
|
||||
if (bitOffset == null) {
|
||||
throw new IllegalStateException("BOOLEAN sensor requires bitOffset.");
|
||||
throw new IllegalStateException(
|
||||
"BOOLEAN sensor requires bitOffset."
|
||||
);
|
||||
}
|
||||
|
||||
return ((rawValue >> bitOffset) & 1) == 1;
|
||||
}
|
||||
|
||||
if (sensorDefinition.getValueType() == SensorValueType.DECIMAL) {
|
||||
return rawValue / Math.pow(10, sensorDefinition.getDecimalPlaces());
|
||||
|
||||
return rawValue / Math.pow(
|
||||
10,
|
||||
sensorDefinition.getDecimalPlaces()
|
||||
);
|
||||
}
|
||||
|
||||
return rawValue;
|
||||
|
||||
@@ -4,7 +4,9 @@ import java.time.Instant;
|
||||
|
||||
public record TelemetrySnapshot(
|
||||
Integer sensorId,
|
||||
String key,
|
||||
String name,
|
||||
String category,
|
||||
Integer modbusAddress,
|
||||
Integer bitOffset,
|
||||
Integer rawValue,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.litoralregas.backend.historian;
|
||||
|
||||
import com.litoralregas.backend.dashboard.DashboardOverviewResponse;
|
||||
import com.litoralregas.backend.modules.shared.ModuleSensorResponse;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
@@ -13,78 +12,12 @@ import java.util.Map;
|
||||
@Service
|
||||
public class HistorianService {
|
||||
|
||||
private static final String SOURCE_DASHBOARD_OVERVIEW = "DASHBOARD_OVERVIEW";
|
||||
|
||||
private final HistorianSampleRepository historianSampleRepository;
|
||||
|
||||
private static final String SOURCE_MODULE = "MODULE";
|
||||
public HistorianService(HistorianSampleRepository historianSampleRepository) {
|
||||
this.historianSampleRepository = historianSampleRepository;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void recordDashboardOverview(DashboardOverviewResponse overview) {
|
||||
System.out.println("Historian recording overview at " + overview.timestamp());
|
||||
Instant sampledAt = overview.timestamp();
|
||||
|
||||
System.out.println("Meteo object = " + overview.meteo());
|
||||
|
||||
if (overview.climate() != null && !overview.climate().zones().isEmpty()) {
|
||||
System.out.println(
|
||||
"Zone 1 temp = " +
|
||||
overview.climate().zones().get(0).temperature()
|
||||
);
|
||||
}
|
||||
recordNumber(sampledAt, "meteo.exterior_temperature", overview.meteo().exteriorTemperature(), "°C");
|
||||
recordNumber(sampledAt, "meteo.exterior_humidity", overview.meteo().exteriorHumidity(), "%");
|
||||
recordNumber(sampledAt, "meteo.radiation", overview.meteo().radiation(), "W/m²");
|
||||
recordNumber(sampledAt, "meteo.wind_speed", overview.meteo().windSpeed(), "Km/h");
|
||||
recordNumber(sampledAt, "meteo.wind_direction", overview.meteo().windDirection(), "°");
|
||||
recordBoolean(sampledAt, "meteo.raining", overview.meteo().raining());
|
||||
|
||||
if (overview.climate() != null && overview.climate().zones() != null) {
|
||||
for (DashboardOverviewResponse.ClimateZoneOverview zone : overview.climate().zones()) {
|
||||
String prefix = "climate.zone_" + zone.zoneNumber();
|
||||
|
||||
recordNumber(sampledAt, prefix + ".temperature", zone.temperature(), "°C");
|
||||
recordNumber(sampledAt, prefix + ".humidity", zone.humidity(), "%");
|
||||
recordNumber(sampledAt, prefix + ".co2", zone.co2(), "ppm");
|
||||
|
||||
recordBoolean(sampledAt, prefix + ".fans_on", zone.fansOn());
|
||||
recordBoolean(sampledAt, prefix + ".extractors_on", zone.extractorsOn());
|
||||
|
||||
recordNumber(sampledAt, prefix + ".zenital_left_percent", zone.zenitalLeftPercent(), "%");
|
||||
recordNumber(sampledAt, prefix + ".zenital_right_percent", zone.zenitalRightPercent(), "%");
|
||||
recordNumber(sampledAt, prefix + ".lateral_left_percent", zone.lateralLeftPercent(), "%");
|
||||
recordNumber(sampledAt, prefix + ".lateral_right_percent", zone.lateralRightPercent(), "%");
|
||||
}
|
||||
}
|
||||
|
||||
if (overview.irrigation() != null) {
|
||||
recordNumber(
|
||||
sampledAt,
|
||||
"irrigation.active_valve_count",
|
||||
overview.irrigation().activeValveCount(),
|
||||
"valves"
|
||||
);
|
||||
|
||||
recordNumber(
|
||||
sampledAt,
|
||||
"irrigation.active_pump_count",
|
||||
overview.irrigation().activePumpCount(),
|
||||
"pumps"
|
||||
);
|
||||
}
|
||||
|
||||
if (overview.lighting() != null) {
|
||||
recordNumber(
|
||||
sampledAt,
|
||||
"lighting.active_sector_count",
|
||||
overview.lighting().activeSectorCount(),
|
||||
"sectors"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<HistorianSeriesPoint> getSeries(String keyName, Instant from, Instant to) {
|
||||
return historianSampleRepository
|
||||
@@ -126,7 +59,7 @@ public class HistorianService {
|
||||
sample.setKeyName(keyName);
|
||||
sample.setNumericValue(value.doubleValue());
|
||||
sample.setUnit(unit);
|
||||
sample.setSource(SOURCE_DASHBOARD_OVERVIEW);
|
||||
sample.setSource(SOURCE_MODULE);
|
||||
System.out.println("Saving historian sample: " + keyName + " = " + value);
|
||||
historianSampleRepository.save(sample);
|
||||
}
|
||||
@@ -138,7 +71,7 @@ public class HistorianService {
|
||||
sample.setSampledAt(sampledAt);
|
||||
sample.setKeyName(keyName);
|
||||
sample.setBooleanValue(value);
|
||||
sample.setSource(SOURCE_DASHBOARD_OVERVIEW);
|
||||
sample.setSource(SOURCE_MODULE);
|
||||
|
||||
historianSampleRepository.save(sample);
|
||||
}
|
||||
@@ -154,13 +87,12 @@ public class HistorianService {
|
||||
|
||||
@Transactional
|
||||
public void recordModuleSensors(
|
||||
String moduleName,
|
||||
List<ModuleSensorResponse> sensors,
|
||||
Instant sampledAt
|
||||
) {
|
||||
for (ModuleSensorResponse sensor : sensors) {
|
||||
|
||||
String keyName = moduleName + "." + sensor.key();
|
||||
String key = sensor.key();
|
||||
|
||||
Object value = sensor.value();
|
||||
|
||||
@@ -168,7 +100,7 @@ public class HistorianService {
|
||||
|
||||
recordNumber(
|
||||
sampledAt,
|
||||
keyName,
|
||||
key,
|
||||
numberValue,
|
||||
sensor.unit()
|
||||
);
|
||||
@@ -177,7 +109,7 @@ public class HistorianService {
|
||||
|
||||
recordBoolean(
|
||||
sampledAt,
|
||||
keyName,
|
||||
key,
|
||||
booleanValue
|
||||
);
|
||||
}
|
||||
|
||||
@@ -81,6 +81,7 @@ public class ClimateModuleService {
|
||||
private ModuleSensorResponse toResponse(TelemetrySnapshot snapshot) {
|
||||
return new ModuleSensorResponse(
|
||||
snapshot.sensorId(),
|
||||
snapshot.key(),
|
||||
snapshot.name(),
|
||||
buildKey(snapshot.name()),
|
||||
snapshot.value(),
|
||||
|
||||
+20
@@ -1,4 +1,24 @@
|
||||
package com.litoralregas.backend.modules.climate.websocket;
|
||||
|
||||
import com.litoralregas.backend.modules.climate.ClimateModuleResponse;
|
||||
import org.springframework.messaging.simp.SimpMessagingTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class ClimateModuleWebSocketPublisher {
|
||||
|
||||
private static final String DESTINATION =
|
||||
"/topic/modules/climate/latest";
|
||||
|
||||
private final SimpMessagingTemplate messagingTemplate;
|
||||
|
||||
public ClimateModuleWebSocketPublisher(
|
||||
SimpMessagingTemplate messagingTemplate
|
||||
) {
|
||||
this.messagingTemplate = messagingTemplate;
|
||||
}
|
||||
|
||||
public void publishLatest(ClimateModuleResponse response) {
|
||||
messagingTemplate.convertAndSend(DESTINATION, response);
|
||||
}
|
||||
}
|
||||
@@ -45,6 +45,7 @@ public class MeteoModuleService {
|
||||
private ModuleSensorResponse toResponse(TelemetrySnapshot snapshot) {
|
||||
return new ModuleSensorResponse(
|
||||
snapshot.sensorId(),
|
||||
snapshot.key(),
|
||||
snapshot.name(),
|
||||
buildKey(snapshot.name()),
|
||||
snapshot.value(),
|
||||
|
||||
@@ -4,8 +4,9 @@ import java.time.Instant;
|
||||
|
||||
public record ModuleSensorResponse(
|
||||
Integer sensorId,
|
||||
String name,
|
||||
String key,
|
||||
String name,
|
||||
String category,
|
||||
Object value,
|
||||
String unit,
|
||||
Integer modbusAddress,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.litoralregas.backend.sensor;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
@Entity
|
||||
@@ -11,10 +12,16 @@ public class SensorDefinition {
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Integer id;
|
||||
|
||||
@Column(unique = true, nullable = false)
|
||||
private String key;
|
||||
|
||||
@Column(nullable = false)
|
||||
private String name;
|
||||
|
||||
@Column(name = "modbus_address", nullable = false)
|
||||
@Column(nullable = false)
|
||||
private String category;
|
||||
|
||||
@Column(name = "modbus_address")
|
||||
private Integer modbusAddress;
|
||||
|
||||
@Column(name = "bit_offset")
|
||||
@@ -29,9 +36,6 @@ public class SensorDefinition {
|
||||
@Column(name = "decimal_places", nullable = false)
|
||||
private Integer decimalPlaces;
|
||||
|
||||
@Column(nullable = false)
|
||||
private String category;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "source_type", nullable = false)
|
||||
private SensorSourceType sourceType;
|
||||
@@ -49,24 +53,26 @@ public class SensorDefinition {
|
||||
}
|
||||
|
||||
public SensorDefinition(
|
||||
String key,
|
||||
String name,
|
||||
String category,
|
||||
Integer modbusAddress,
|
||||
Integer bitOffset,
|
||||
SensorValueType valueType,
|
||||
String unit,
|
||||
Integer decimalPlaces,
|
||||
String category,
|
||||
SensorSourceType sourceType,
|
||||
Integer pollingIntervalSeconds,
|
||||
Boolean enabled
|
||||
) {
|
||||
this.key = key;
|
||||
this.name = name;
|
||||
this.category = category;
|
||||
this.modbusAddress = modbusAddress;
|
||||
this.bitOffset = bitOffset;
|
||||
this.valueType = valueType;
|
||||
this.unit = unit;
|
||||
this.decimalPlaces = decimalPlaces;
|
||||
this.category = category;
|
||||
this.sourceType = sourceType;
|
||||
this.pollingIntervalSeconds = pollingIntervalSeconds;
|
||||
this.enabled = enabled;
|
||||
@@ -77,6 +83,14 @@ public class SensorDefinition {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getKey() {
|
||||
return key;
|
||||
}
|
||||
|
||||
public void setKey(String key) {
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
@@ -85,6 +99,14 @@ public class SensorDefinition {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getCategory() {
|
||||
return category;
|
||||
}
|
||||
|
||||
public void setCategory(String category) {
|
||||
this.category = category;
|
||||
}
|
||||
|
||||
public Integer getModbusAddress() {
|
||||
return modbusAddress;
|
||||
}
|
||||
@@ -125,14 +147,6 @@ public class SensorDefinition {
|
||||
this.decimalPlaces = decimalPlaces;
|
||||
}
|
||||
|
||||
public String getCategory() {
|
||||
return category;
|
||||
}
|
||||
|
||||
public void setCategory(String category) {
|
||||
this.category = category;
|
||||
}
|
||||
|
||||
public SensorSourceType getSourceType() {
|
||||
return sourceType;
|
||||
}
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
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,7 @@
|
||||
package com.litoralregas.backend.sensor.importer;
|
||||
|
||||
public record ModbusConfig(
|
||||
Integer address,
|
||||
Integer bitOffset
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.litoralregas.backend.sensor.importer;
|
||||
|
||||
public record SensorDefinitionConfig(
|
||||
String key,
|
||||
String name,
|
||||
String category,
|
||||
ModbusConfig modbus,
|
||||
String valueType,
|
||||
String unit,
|
||||
Integer decimalPlaces,
|
||||
Integer pollingIntervalSeconds,
|
||||
Boolean enabled
|
||||
) {
|
||||
}
|
||||
+1
-1
@@ -15,6 +15,6 @@ public class SensorDefinitionImportController {
|
||||
|
||||
@PostMapping("/api/sensor-definition-import/run")
|
||||
public SensorDefinitionImportResult runImport() {
|
||||
return importService.importSensorMap();
|
||||
return importService.importSensorDefinitions();
|
||||
}
|
||||
}
|
||||
+51
-60
@@ -1,113 +1,104 @@
|
||||
package com.litoralregas.backend.sensor.importer;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.litoralregas.backend.sensor.SensorDefinition;
|
||||
import com.litoralregas.backend.sensor.SensorDefinitionRepository;
|
||||
import com.litoralregas.backend.sensor.SensorSourceType;
|
||||
import com.litoralregas.backend.sensor.SensorValueType;
|
||||
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;
|
||||
import java.io.InputStream;
|
||||
|
||||
@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 static final String SENSOR_DEFINITIONS_PATH =
|
||||
"config/sensor-definitions.json";
|
||||
|
||||
private final SensorDefinitionMapParser parser;
|
||||
private final SensorDefinitionRepository repository;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public SensorDefinitionImportService(
|
||||
SensorDefinitionMapParser parser,
|
||||
SensorDefinitionRepository repository
|
||||
SensorDefinitionRepository repository,
|
||||
ObjectMapper objectMapper
|
||||
) {
|
||||
this.parser = parser;
|
||||
this.repository = repository;
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public SensorDefinitionImportResult importSensorMap() {
|
||||
List<String> lines = readSensorMapLines();
|
||||
public SensorDefinitionImportResult importSensorDefinitions() {
|
||||
|
||||
SensorDefinitionsFile file = readDefinitionsFile();
|
||||
|
||||
int imported = 0;
|
||||
int skippedExisting = 0;
|
||||
int skippedBlank = 0;
|
||||
|
||||
for (String line : lines) {
|
||||
if (line == null || line.isBlank()) {
|
||||
skippedBlank++;
|
||||
continue;
|
||||
}
|
||||
for (SensorDefinitionConfig config : file.sensors()) {
|
||||
|
||||
SensorDefinitionImportRow row = parser.parseLine(line)
|
||||
.orElseThrow(() -> new IllegalArgumentException(
|
||||
"Invalid sensor row: " + line
|
||||
));
|
||||
|
||||
List<SensorDefinition> existingSensors = repository.findAllByHardwareAddress(
|
||||
row.modbusAddress(),
|
||||
row.bitOffset()
|
||||
);
|
||||
|
||||
if (!existingSensors.isEmpty()) {
|
||||
for (SensorDefinition sensorDefinition : existingSensors) {
|
||||
sensorDefinition.setValueType(row.valueType());
|
||||
sensorDefinition.setUnit(row.unit());
|
||||
sensorDefinition.setDecimalPlaces(row.decimalPlaces());
|
||||
sensorDefinition.setCategory(row.category());
|
||||
sensorDefinition.setSourceType(row.sourceType());
|
||||
}
|
||||
|
||||
skippedExisting += existingSensors.size();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (repository.findByName(row.name()).isPresent()) {
|
||||
if (repository.findByName(config.name()).isPresent()) {
|
||||
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
|
||||
config.key(),
|
||||
config.name(),
|
||||
config.category(),
|
||||
config.modbus().address(),
|
||||
config.modbus().bitOffset(),
|
||||
SensorValueType.valueOf(config.valueType()),
|
||||
config.unit(),
|
||||
config.decimalPlaces(),
|
||||
SensorSourceType.MODBUS,
|
||||
config.pollingIntervalSeconds(),
|
||||
config.enabled()
|
||||
);
|
||||
|
||||
repository.save(sensorDefinition);
|
||||
|
||||
imported++;
|
||||
}
|
||||
|
||||
return new SensorDefinitionImportResult(
|
||||
lines.size(),
|
||||
file.sensorCount(),
|
||||
imported,
|
||||
skippedExisting,
|
||||
skippedBlank
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
private List<String> readSensorMapLines() {
|
||||
private SensorDefinitionsFile readDefinitionsFile() {
|
||||
|
||||
try {
|
||||
ClassPathResource resource = new ClassPathResource(SENSOR_MAP_PATH);
|
||||
|
||||
ClassPathResource resource =
|
||||
new ClassPathResource(SENSOR_DEFINITIONS_PATH);
|
||||
|
||||
if (!resource.exists()) {
|
||||
throw new IllegalStateException("Sensor map file not found: " + SENSOR_MAP_PATH);
|
||||
throw new IllegalStateException(
|
||||
"Sensor definitions file not found: "
|
||||
+ SENSOR_DEFINITIONS_PATH
|
||||
);
|
||||
}
|
||||
|
||||
try (InputStream inputStream = resource.getInputStream()) {
|
||||
|
||||
return objectMapper.readValue(
|
||||
inputStream,
|
||||
SensorDefinitionsFile.class
|
||||
);
|
||||
}
|
||||
|
||||
return resource.getContentAsString(StandardCharsets.UTF_8)
|
||||
.lines()
|
||||
.toList();
|
||||
} catch (Exception exception) {
|
||||
throw new IllegalStateException("Failed to read sensor map file.", exception);
|
||||
|
||||
throw new IllegalStateException(
|
||||
"Failed to load sensor definitions.",
|
||||
exception
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
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
|
||||
) {
|
||||
}
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
package com.litoralregas.backend.sensor.importer;
|
||||
|
||||
import com.litoralregas.backend.sensor.SensorDefinitionRepository;
|
||||
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class SensorDefinitionStartupImporter {
|
||||
|
||||
private final SensorDefinitionRepository repository;
|
||||
private final SensorDefinitionImportService importService;
|
||||
|
||||
public SensorDefinitionStartupImporter(
|
||||
SensorDefinitionRepository repository,
|
||||
SensorDefinitionImportService importService
|
||||
) {
|
||||
this.repository = repository;
|
||||
this.importService = importService;
|
||||
}
|
||||
|
||||
@EventListener(ApplicationReadyEvent.class)
|
||||
public void importSensorsIfMissing() {
|
||||
if (repository.count() > 0) {
|
||||
System.out.println("Sensor definitions already imported. Skipping startup import.");
|
||||
return;
|
||||
}
|
||||
|
||||
System.out.println("No sensor definitions found. Importing sensor-definitions.json...");
|
||||
|
||||
importService.importSensorDefinitions();
|
||||
|
||||
System.out.println("Sensor definitions imported successfully.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.litoralregas.backend.sensor.importer;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record SensorDefinitionsFile(
|
||||
Integer version,
|
||||
Integer sensorCount,
|
||||
List<SensorDefinitionConfig> sensors
|
||||
) {
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,14 +1,27 @@
|
||||
CREATE TABLE sensor_definition (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
|
||||
key VARCHAR(255) NOT NULL UNIQUE,
|
||||
|
||||
name VARCHAR(255) NOT NULL,
|
||||
modbus_address INTEGER NOT NULL,
|
||||
bit_offset INTEGER,
|
||||
value_type VARCHAR(50) NOT NULL,
|
||||
unit VARCHAR(50),
|
||||
decimal_places INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
category VARCHAR(100) NOT NULL,
|
||||
|
||||
modbus_address INTEGER,
|
||||
|
||||
bit_offset INTEGER,
|
||||
|
||||
value_type VARCHAR(50) NOT NULL,
|
||||
|
||||
unit VARCHAR(50),
|
||||
|
||||
decimal_places INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
source_type VARCHAR(50) NOT NULL,
|
||||
|
||||
polling_interval_seconds INTEGER NOT NULL DEFAULT 1,
|
||||
|
||||
enabled BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
|
||||
created_at TIMESTAMP NOT NULL
|
||||
);
|
||||
+2
@@ -2,6 +2,7 @@ CREATE TABLE historian_sample (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
|
||||
sampled_at TIMESTAMP NOT NULL,
|
||||
|
||||
key_name VARCHAR(160) NOT NULL,
|
||||
|
||||
numeric_value REAL,
|
||||
@@ -9,6 +10,7 @@ CREATE TABLE historian_sample (
|
||||
text_value VARCHAR(255),
|
||||
|
||||
unit VARCHAR(32),
|
||||
|
||||
source VARCHAR(50) NOT NULL,
|
||||
|
||||
created_at TIMESTAMP NOT NULL
|
||||
@@ -1,52 +0,0 @@
|
||||
INSERT INTO sensor_definition (
|
||||
name,
|
||||
modbus_address,
|
||||
bit_offset,
|
||||
value_type,
|
||||
unit,
|
||||
decimal_places,
|
||||
category,
|
||||
source_type,
|
||||
polling_interval_seconds,
|
||||
enabled,
|
||||
created_at
|
||||
) VALUES
|
||||
(
|
||||
'Greenhouse Temperature',
|
||||
100,
|
||||
NULL,
|
||||
'DECIMAL',
|
||||
'ºC',
|
||||
1,
|
||||
'CLIMATE',
|
||||
'MODBUS',
|
||||
2,
|
||||
TRUE,
|
||||
CURRENT_TIMESTAMP
|
||||
),
|
||||
(
|
||||
'Greenhouse Humidity',
|
||||
101,
|
||||
NULL,
|
||||
'DECIMAL',
|
||||
'%',
|
||||
1,
|
||||
'CLIMATE',
|
||||
'MODBUS',
|
||||
2,
|
||||
TRUE,
|
||||
CURRENT_TIMESTAMP
|
||||
),
|
||||
(
|
||||
'Irrigation Pump Running',
|
||||
200,
|
||||
0,
|
||||
'BOOLEAN',
|
||||
NULL,
|
||||
0,
|
||||
'IRRIGATION',
|
||||
'MODBUS',
|
||||
1,
|
||||
TRUE,
|
||||
CURRENT_TIMESTAMP
|
||||
);
|
||||
@@ -1,2 +0,0 @@
|
||||
CREATE UNIQUE INDEX ux_sensor_definition_name
|
||||
ON sensor_definition(name);
|
||||
Reference in New Issue
Block a user