From 4f484779d8799592a7a34073831720f30383fd1d Mon Sep 17 00:00:00 2001 From: Esteban Date: Mon, 2 Feb 2026 01:43:11 -0600 Subject: [PATCH] Meters columns --- water-api/sql/add_meter_extended_fields.sql | 124 +++++++++++++++++++ water-api/src/services/meter.service.ts | 126 ++++++++++++++++++++ water-api/src/validators/meter.validator.ts | 108 +++++++++++++++++ 3 files changed, 358 insertions(+) create mode 100644 water-api/sql/add_meter_extended_fields.sql diff --git a/water-api/sql/add_meter_extended_fields.sql b/water-api/sql/add_meter_extended_fields.sql new file mode 100644 index 0000000..e853485 --- /dev/null +++ b/water-api/sql/add_meter_extended_fields.sql @@ -0,0 +1,124 @@ +-- ============================================================================ +-- Add extended fields to meters table +-- These fields store additional technical and operational data for meters +-- All fields are nullable (not required) +-- ============================================================================ + +-- Communication & Network Fields +ALTER TABLE meters ADD COLUMN IF NOT EXISTS protocol VARCHAR(50); +ALTER TABLE meters ADD COLUMN IF NOT EXISTS mac VARCHAR(50); +ALTER TABLE meters ADD COLUMN IF NOT EXISTS gateway VARCHAR(100); +ALTER TABLE meters ADD COLUMN IF NOT EXISTS network_mode VARCHAR(50); +ALTER TABLE meters ADD COLUMN IF NOT EXISTS phone_id VARCHAR(50); +ALTER TABLE meters ADD COLUMN IF NOT EXISTS phone_model VARCHAR(100); +ALTER TABLE meters ADD COLUMN IF NOT EXISTS phone_name VARCHAR(100); + +-- Voltage & Power Fields +ALTER TABLE meters ADD COLUMN IF NOT EXISTS voltage DECIMAL(10, 2); +ALTER TABLE meters ADD COLUMN IF NOT EXISTS voltage_rtu DECIMAL(10, 2); +ALTER TABLE meters ADD COLUMN IF NOT EXISTS voltage_status VARCHAR(50); + +-- Signal & Communication Quality +ALTER TABLE meters ADD COLUMN IF NOT EXISTS signal INTEGER; + +-- Status Fields +ALTER TABLE meters ADD COLUMN IF NOT EXISTS storage_status VARCHAR(50); +ALTER TABLE meters ADD COLUMN IF NOT EXISTS flow_status VARCHAR(50); +ALTER TABLE meters ADD COLUMN IF NOT EXISTS open_status VARCHAR(50); +ALTER TABLE meters ADD COLUMN IF NOT EXISTS actuator_status VARCHAR(50); +ALTER TABLE meters ADD COLUMN IF NOT EXISTS counter_status VARCHAR(50); +ALTER TABLE meters ADD COLUMN IF NOT EXISTS leakage_status VARCHAR(50); +ALTER TABLE meters ADD COLUMN IF NOT EXISTS burst_status VARCHAR(50); +ALTER TABLE meters ADD COLUMN IF NOT EXISTS valid_status VARCHAR(50); + +-- Security & Alerts +ALTER TABLE meters ADD COLUMN IF NOT EXISTS magnetic_attack BOOLEAN; +ALTER TABLE meters ADD COLUMN IF NOT EXISTS realtime_information_flag BOOLEAN; + +-- Flow Measurements +ALTER TABLE meters ADD COLUMN IF NOT EXISTS current_flow DECIMAL(15, 4); +ALTER TABLE meters ADD COLUMN IF NOT EXISTS total_flow_reverse DECIMAL(15, 4); +ALTER TABLE meters ADD COLUMN IF NOT EXISTS current_flow_reverse DECIMAL(15, 4); + +-- Protocol Fields (M-Bus/LoRaWAN specific) +ALTER TABLE meters ADD COLUMN IF NOT EXISTS l_field VARCHAR(10); +ALTER TABLE meters ADD COLUMN IF NOT EXISTS c_field VARCHAR(10); +ALTER TABLE meters ADD COLUMN IF NOT EXISTS ver VARCHAR(10); +ALTER TABLE meters ADD COLUMN IF NOT EXISTS dev VARCHAR(20); +ALTER TABLE meters ADD COLUMN IF NOT EXISTS ci_field VARCHAR(10); + +-- Company & Manufacturer Info +ALTER TABLE meters ADD COLUMN IF NOT EXISTS company_abbreviation VARCHAR(50); +ALTER TABLE meters ADD COLUMN IF NOT EXISTS manufacturer VARCHAR(100); + +-- Geolocation +ALTER TABLE meters ADD COLUMN IF NOT EXISTS latitude DECIMAL(10, 8); +ALTER TABLE meters ADD COLUMN IF NOT EXISTS longitude DECIMAL(11, 8); + +-- Additional Data (JSON for flexible data storage) +ALTER TABLE meters ADD COLUMN IF NOT EXISTS data JSONB; + +-- ============================================================================ +-- Add indexes for commonly queried fields +-- ============================================================================ + +CREATE INDEX IF NOT EXISTS idx_meters_protocol ON meters(protocol); +CREATE INDEX IF NOT EXISTS idx_meters_mac ON meters(mac); +CREATE INDEX IF NOT EXISTS idx_meters_gateway ON meters(gateway); +CREATE INDEX IF NOT EXISTS idx_meters_manufacturer ON meters(manufacturer); +CREATE INDEX IF NOT EXISTS idx_meters_flow_status ON meters(flow_status); +CREATE INDEX IF NOT EXISTS idx_meters_leakage_status ON meters(leakage_status); +CREATE INDEX IF NOT EXISTS idx_meters_geolocation ON meters(latitude, longitude) WHERE latitude IS NOT NULL AND longitude IS NOT NULL; + +-- ============================================================================ +-- Add comments for documentation +-- ============================================================================ + +COMMENT ON COLUMN meters.protocol IS 'Communication protocol (LoRa, LoRaWAN, NB-IoT, etc.)'; +COMMENT ON COLUMN meters.mac IS 'MAC address of the device'; +COMMENT ON COLUMN meters.gateway IS 'Gateway identifier or name'; +COMMENT ON COLUMN meters.network_mode IS 'Network operation mode'; +COMMENT ON COLUMN meters.voltage IS 'Battery voltage (V)'; +COMMENT ON COLUMN meters.voltage_rtu IS 'RTU voltage (V)'; +COMMENT ON COLUMN meters.voltage_status IS 'Battery status (OK, LOW, CRITICAL)'; +COMMENT ON COLUMN meters.signal IS 'Signal strength (RSSI or similar)'; +COMMENT ON COLUMN meters.storage_status IS 'Internal storage status'; +COMMENT ON COLUMN meters.flow_status IS 'Flow measurement status'; +COMMENT ON COLUMN meters.leakage_status IS 'Leak detection status'; +COMMENT ON COLUMN meters.burst_status IS 'Burst pipe detection status'; +COMMENT ON COLUMN meters.magnetic_attack IS 'Magnetic tampering detected'; +COMMENT ON COLUMN meters.realtime_information_flag IS 'Real-time data available'; +COMMENT ON COLUMN meters.current_flow IS 'Current flow rate (m³/h or L/h)'; +COMMENT ON COLUMN meters.total_flow_reverse IS 'Total reverse flow accumulated'; +COMMENT ON COLUMN meters.current_flow_reverse IS 'Current reverse flow rate'; +COMMENT ON COLUMN meters.l_field IS 'M-Bus L-Field (length)'; +COMMENT ON COLUMN meters.c_field IS 'M-Bus C-Field (control)'; +COMMENT ON COLUMN meters.ver IS 'Protocol version'; +COMMENT ON COLUMN meters.dev IS 'Device type identifier'; +COMMENT ON COLUMN meters.ci_field IS 'M-Bus CI-Field (control information)'; +COMMENT ON COLUMN meters.latitude IS 'Latitude coordinate (WGS84)'; +COMMENT ON COLUMN meters.longitude IS 'Longitude coordinate (WGS84)'; +COMMENT ON COLUMN meters.data IS 'Additional flexible data storage (JSON)'; + +-- ============================================================================ +-- Verify the changes +-- ============================================================================ + +SELECT + column_name, + data_type, + is_nullable, + column_default +FROM information_schema.columns +WHERE table_name = 'meters' + AND column_name IN ( + 'protocol', 'mac', 'gateway', 'network_mode', 'phone_id', 'phone_model', + 'phone_name', 'voltage', 'voltage_rtu', 'voltage_status', 'signal', + 'storage_status', 'flow_status', 'open_status', 'actuator_status', + 'counter_status', 'leakage_status', 'burst_status', 'valid_status', + 'magnetic_attack', 'realtime_information_flag', 'current_flow', + 'total_flow_reverse', 'current_flow_reverse', 'l_field', 'c_field', + 'ver', 'dev', 'ci_field', 'company_abbreviation', 'manufacturer', + 'latitude', 'longitude', 'data' + ) +ORDER BY ordinal_position; diff --git a/water-api/src/services/meter.service.ts b/water-api/src/services/meter.service.ts index 23ed9e8..68a28dd 100644 --- a/water-api/src/services/meter.service.ts +++ b/water-api/src/services/meter.service.ts @@ -18,6 +18,60 @@ export interface Meter { installation_date: Date | null; created_at: Date; updated_at: Date; + + // Communication & Network Fields + protocol?: string | null; + mac?: string | null; + gateway?: string | null; + network_mode?: string | null; + phone_id?: string | null; + phone_model?: string | null; + phone_name?: string | null; + + // Voltage & Power Fields + voltage?: number | null; + voltage_rtu?: number | null; + voltage_status?: string | null; + + // Signal & Communication Quality + signal?: number | null; + + // Status Fields + storage_status?: string | null; + flow_status?: string | null; + open_status?: string | null; + actuator_status?: string | null; + counter_status?: string | null; + leakage_status?: string | null; + burst_status?: string | null; + valid_status?: string | null; + + // Security & Alerts + magnetic_attack?: boolean | null; + realtime_information_flag?: boolean | null; + + // Flow Measurements + current_flow?: number | null; + total_flow_reverse?: number | null; + current_flow_reverse?: number | null; + + // Protocol Fields (M-Bus/LoRaWAN specific) + l_field?: string | null; + c_field?: string | null; + ver?: string | null; + dev?: string | null; + ci_field?: string | null; + + // Company & Manufacturer Info + company_abbreviation?: string | null; + manufacturer?: string | null; + + // Geolocation + latitude?: number | null; + longitude?: number | null; + + // Additional Data + data?: Record | null; } /** @@ -74,6 +128,42 @@ export interface CreateMeterInput { type?: string; status?: string; installation_date?: string; + + // Extended fields (all optional) + protocol?: string; + mac?: string; + gateway?: string; + network_mode?: string; + phone_id?: string; + phone_model?: string; + phone_name?: string; + voltage?: number; + voltage_rtu?: number; + voltage_status?: string; + signal?: number; + storage_status?: string; + flow_status?: string; + open_status?: string; + actuator_status?: string; + counter_status?: string; + leakage_status?: string; + burst_status?: string; + valid_status?: string; + magnetic_attack?: boolean; + realtime_information_flag?: boolean; + current_flow?: number; + total_flow_reverse?: number; + current_flow_reverse?: number; + l_field?: string; + c_field?: string; + ver?: string; + dev?: string; + ci_field?: string; + company_abbreviation?: string; + manufacturer?: string; + latitude?: number; + longitude?: number; + data?: Record; } /** @@ -88,6 +178,42 @@ export interface UpdateMeterInput { type?: string; status?: string; installation_date?: string; + + // Extended fields (all optional) + protocol?: string; + mac?: string; + gateway?: string; + network_mode?: string; + phone_id?: string; + phone_model?: string; + phone_name?: string; + voltage?: number; + voltage_rtu?: number; + voltage_status?: string; + signal?: number; + storage_status?: string; + flow_status?: string; + open_status?: string; + actuator_status?: string; + counter_status?: string; + leakage_status?: string; + burst_status?: string; + valid_status?: string; + magnetic_attack?: boolean; + realtime_information_flag?: boolean; + current_flow?: number; + total_flow_reverse?: number; + current_flow_reverse?: number; + l_field?: string; + c_field?: string; + ver?: string; + dev?: string; + ci_field?: string; + company_abbreviation?: string; + manufacturer?: string; + latitude?: number; + longitude?: number; + data?: Record; } /** diff --git a/water-api/src/validators/meter.validator.ts b/water-api/src/validators/meter.validator.ts index 2c99662..a908181 100644 --- a/water-api/src/validators/meter.validator.ts +++ b/water-api/src/validators/meter.validator.ts @@ -73,6 +73,60 @@ export const createMeterSchema = z.object({ .datetime({ message: 'Installation date must be a valid ISO date string' }) .optional() .nullable(), + + // Communication & Network Fields + protocol: z.string().max(50).optional().nullable(), + mac: z.string().max(50).optional().nullable(), + gateway: z.string().max(100).optional().nullable(), + network_mode: z.string().max(50).optional().nullable(), + phone_id: z.string().max(50).optional().nullable(), + phone_model: z.string().max(100).optional().nullable(), + phone_name: z.string().max(100).optional().nullable(), + + // Voltage & Power Fields + voltage: z.number().optional().nullable(), + voltage_rtu: z.number().optional().nullable(), + voltage_status: z.string().max(50).optional().nullable(), + + // Signal & Communication Quality + signal: z.number().int().optional().nullable(), + + // Status Fields + storage_status: z.string().max(50).optional().nullable(), + flow_status: z.string().max(50).optional().nullable(), + open_status: z.string().max(50).optional().nullable(), + actuator_status: z.string().max(50).optional().nullable(), + counter_status: z.string().max(50).optional().nullable(), + leakage_status: z.string().max(50).optional().nullable(), + burst_status: z.string().max(50).optional().nullable(), + valid_status: z.string().max(50).optional().nullable(), + + // Security & Alerts + magnetic_attack: z.boolean().optional().nullable(), + realtime_information_flag: z.boolean().optional().nullable(), + + // Flow Measurements + current_flow: z.number().optional().nullable(), + total_flow_reverse: z.number().optional().nullable(), + current_flow_reverse: z.number().optional().nullable(), + + // Protocol Fields + l_field: z.string().max(10).optional().nullable(), + c_field: z.string().max(10).optional().nullable(), + ver: z.string().max(10).optional().nullable(), + dev: z.string().max(20).optional().nullable(), + ci_field: z.string().max(10).optional().nullable(), + + // Company & Manufacturer Info + company_abbreviation: z.string().max(50).optional().nullable(), + manufacturer: z.string().max(100).optional().nullable(), + + // Geolocation + latitude: z.number().min(-90).max(90).optional().nullable(), + longitude: z.number().min(-180).max(180).optional().nullable(), + + // Additional Data + data: z.record(z.any()).optional().nullable(), }); /** @@ -117,6 +171,60 @@ export const updateMeterSchema = z.object({ .datetime({ message: 'Installation date must be a valid ISO date string' }) .optional() .nullable(), + + // Communication & Network Fields + protocol: z.string().max(50).optional().nullable(), + mac: z.string().max(50).optional().nullable(), + gateway: z.string().max(100).optional().nullable(), + network_mode: z.string().max(50).optional().nullable(), + phone_id: z.string().max(50).optional().nullable(), + phone_model: z.string().max(100).optional().nullable(), + phone_name: z.string().max(100).optional().nullable(), + + // Voltage & Power Fields + voltage: z.number().optional().nullable(), + voltage_rtu: z.number().optional().nullable(), + voltage_status: z.string().max(50).optional().nullable(), + + // Signal & Communication Quality + signal: z.number().int().optional().nullable(), + + // Status Fields + storage_status: z.string().max(50).optional().nullable(), + flow_status: z.string().max(50).optional().nullable(), + open_status: z.string().max(50).optional().nullable(), + actuator_status: z.string().max(50).optional().nullable(), + counter_status: z.string().max(50).optional().nullable(), + leakage_status: z.string().max(50).optional().nullable(), + burst_status: z.string().max(50).optional().nullable(), + valid_status: z.string().max(50).optional().nullable(), + + // Security & Alerts + magnetic_attack: z.boolean().optional().nullable(), + realtime_information_flag: z.boolean().optional().nullable(), + + // Flow Measurements + current_flow: z.number().optional().nullable(), + total_flow_reverse: z.number().optional().nullable(), + current_flow_reverse: z.number().optional().nullable(), + + // Protocol Fields + l_field: z.string().max(10).optional().nullable(), + c_field: z.string().max(10).optional().nullable(), + ver: z.string().max(10).optional().nullable(), + dev: z.string().max(20).optional().nullable(), + ci_field: z.string().max(10).optional().nullable(), + + // Company & Manufacturer Info + company_abbreviation: z.string().max(50).optional().nullable(), + manufacturer: z.string().max(100).optional().nullable(), + + // Geolocation + latitude: z.number().min(-90).max(90).optional().nullable(), + longitude: z.number().min(-180).max(180).optional().nullable(), + + // Additional Data + data: z.record(z.any()).optional().nullable(), }); /**