Refactor telemetry registry to use sensor definitions .json and historian identity

This commit is contained in:
litoral05
2026-05-27 08:39:51 +01:00
parent 0a33f42502
commit 727278d644
26 changed files with 25802 additions and 2145 deletions
@@ -76,7 +76,9 @@ public class BlockPollingService {
telemetryCache.put(new TelemetrySnapshot(
sensor.getId(),
sensor.getKey(),
sensor.getName(),
sensor.getCategory(),
sensor.getModbusAddress(),
sensor.getBitOffset(),
rawValue,
@@ -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());
@@ -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(),
@@ -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
) {
}
@@ -15,6 +15,6 @@ public class SensorDefinitionImportController {
@PostMapping("/api/sensor-definition-import/run")
public SensorDefinitionImportResult runImport() {
return importService.importSensorMap();
return importService.importSensorDefinitions();
}
}
@@ -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
) {
}
}
@@ -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,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);