2025-10-26 19:04:51 +00:00
|
|
|
-- Calejo Control Adapter Database Schema
|
|
|
|
|
-- Version: 2.0
|
|
|
|
|
-- Date: October 26, 2025
|
|
|
|
|
|
|
|
|
|
-- Drop existing tables if they exist (for clean setup)
|
|
|
|
|
DROP TABLE IF EXISTS audit_log CASCADE;
|
|
|
|
|
DROP TABLE IF EXISTS emergency_stop_events CASCADE;
|
|
|
|
|
DROP TABLE IF EXISTS failsafe_events CASCADE;
|
|
|
|
|
DROP TABLE IF EXISTS safety_limit_violations CASCADE;
|
|
|
|
|
DROP TABLE IF EXISTS pump_safety_limits CASCADE;
|
|
|
|
|
DROP TABLE IF EXISTS pump_feedback CASCADE;
|
|
|
|
|
DROP TABLE IF EXISTS pump_plans CASCADE;
|
|
|
|
|
DROP TABLE IF EXISTS pumps CASCADE;
|
|
|
|
|
DROP TABLE IF EXISTS pump_stations CASCADE;
|
|
|
|
|
|
|
|
|
|
-- Create pump_stations table
|
|
|
|
|
CREATE TABLE pump_stations (
|
|
|
|
|
station_id VARCHAR(50) PRIMARY KEY,
|
|
|
|
|
station_name VARCHAR(200) NOT NULL,
|
|
|
|
|
location VARCHAR(200),
|
|
|
|
|
latitude DECIMAL(10, 8),
|
|
|
|
|
longitude DECIMAL(11, 8),
|
|
|
|
|
timezone VARCHAR(50) DEFAULT 'Europe/Rome',
|
|
|
|
|
active BOOLEAN DEFAULT TRUE,
|
|
|
|
|
created_at TIMESTAMP DEFAULT NOW(),
|
|
|
|
|
updated_at TIMESTAMP DEFAULT NOW()
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
COMMENT ON TABLE pump_stations IS 'Metadata about pump stations';
|
|
|
|
|
COMMENT ON COLUMN pump_stations.timezone IS 'Timezone for the pump station (default: Europe/Rome for Italian utilities)';
|
|
|
|
|
|
|
|
|
|
-- Create pumps table
|
|
|
|
|
CREATE TABLE pumps (
|
|
|
|
|
pump_id VARCHAR(50) NOT NULL,
|
|
|
|
|
station_id VARCHAR(50) NOT NULL,
|
|
|
|
|
pump_name VARCHAR(200),
|
|
|
|
|
pump_type VARCHAR(50), -- 'SUBMERSIBLE', 'CENTRIFUGAL', etc.
|
|
|
|
|
control_type VARCHAR(50) NOT NULL, -- 'LEVEL_CONTROLLED', 'POWER_CONTROLLED', 'DIRECT_SPEED'
|
|
|
|
|
manufacturer VARCHAR(100),
|
|
|
|
|
model VARCHAR(100),
|
|
|
|
|
rated_power_kw DECIMAL(10, 2),
|
|
|
|
|
min_speed_hz DECIMAL(5, 2) DEFAULT 20.0,
|
|
|
|
|
max_speed_hz DECIMAL(5, 2) DEFAULT 50.0,
|
|
|
|
|
|
|
|
|
|
-- Default setpoint (used in failsafe mode)
|
|
|
|
|
default_setpoint_hz DECIMAL(5, 2) NOT NULL DEFAULT 35.0,
|
|
|
|
|
|
|
|
|
|
-- Control-specific parameters (JSON)
|
|
|
|
|
control_parameters JSONB,
|
|
|
|
|
|
|
|
|
|
active BOOLEAN DEFAULT TRUE,
|
|
|
|
|
created_at TIMESTAMP DEFAULT NOW(),
|
|
|
|
|
updated_at TIMESTAMP DEFAULT NOW(),
|
|
|
|
|
|
|
|
|
|
PRIMARY KEY (station_id, pump_id),
|
|
|
|
|
FOREIGN KEY (station_id) REFERENCES pump_stations(station_id)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
COMMENT ON TABLE pumps IS 'Metadata about individual pumps';
|
|
|
|
|
COMMENT ON COLUMN pumps.default_setpoint_hz IS 'Default safe setpoint used in failsafe mode (existing pump configuration)';
|
|
|
|
|
COMMENT ON COLUMN pumps.control_parameters IS 'Control-specific parameters in JSON format (PID gains, thresholds, etc.)';
|
|
|
|
|
|
2025-10-27 07:00:57 +00:00
|
|
|
-- Create pump_plans table with version-based updates
|
2025-10-26 19:04:51 +00:00
|
|
|
CREATE TABLE pump_plans (
|
|
|
|
|
plan_id SERIAL PRIMARY KEY,
|
|
|
|
|
station_id VARCHAR(50) NOT NULL,
|
|
|
|
|
pump_id VARCHAR(50) NOT NULL,
|
|
|
|
|
interval_start TIMESTAMP NOT NULL,
|
|
|
|
|
interval_end TIMESTAMP NOT NULL,
|
|
|
|
|
|
|
|
|
|
-- Optimization outputs
|
|
|
|
|
target_flow_m3h DECIMAL(10, 2),
|
|
|
|
|
target_power_kw DECIMAL(10, 2),
|
|
|
|
|
target_level_m DECIMAL(5, 2),
|
|
|
|
|
suggested_speed_hz DECIMAL(5, 2),
|
|
|
|
|
|
2025-10-27 07:00:57 +00:00
|
|
|
-- Version-based update metadata
|
2025-10-26 19:04:51 +00:00
|
|
|
plan_created_at TIMESTAMP DEFAULT NOW(),
|
2025-10-27 07:00:57 +00:00
|
|
|
plan_updated_at TIMESTAMP DEFAULT NOW(),
|
|
|
|
|
plan_version INTEGER DEFAULT 1,
|
2025-10-26 19:04:51 +00:00
|
|
|
optimization_run_id INTEGER,
|
|
|
|
|
|
2025-10-27 07:00:57 +00:00
|
|
|
-- Status tracking
|
|
|
|
|
plan_status VARCHAR(20) DEFAULT 'ACTIVE', -- 'ACTIVE', 'SUPERSEDED', 'CANCELLED'
|
|
|
|
|
superseded_by INTEGER, -- Points to plan_id that superseded this plan
|
|
|
|
|
|
|
|
|
|
FOREIGN KEY (station_id, pump_id) REFERENCES pumps(station_id, pump_id),
|
|
|
|
|
FOREIGN KEY (superseded_by) REFERENCES pump_plans(plan_id)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
COMMENT ON TABLE pump_plans IS 'Optimization plans generated by Calejo Optimize with version-based updates';
|
|
|
|
|
COMMENT ON COLUMN pump_plans.plan_version IS 'Version number for tracking updates (increments on each update)';
|
|
|
|
|
COMMENT ON COLUMN pump_plans.plan_status IS 'Status of the plan: ACTIVE, SUPERSEDED, or CANCELLED';
|
|
|
|
|
COMMENT ON COLUMN pump_plans.superseded_by IS 'Reference to plan that superseded this one (for audit trail)';
|
|
|
|
|
|
|
|
|
|
-- Create indexes for efficient plan management
|
|
|
|
|
CREATE INDEX idx_pump_plans_active ON pump_plans(station_id, pump_id, interval_start, interval_end)
|
|
|
|
|
WHERE plan_status = 'ACTIVE';
|
|
|
|
|
CREATE INDEX idx_pump_plans_latest ON pump_plans(station_id, pump_id, plan_created_at DESC)
|
|
|
|
|
WHERE plan_status = 'ACTIVE';
|
|
|
|
|
CREATE INDEX idx_pump_plans_version ON pump_plans(station_id, pump_id, plan_version DESC);
|
|
|
|
|
|
|
|
|
|
-- Create generic optimization_plans table for future actuator support
|
|
|
|
|
CREATE TABLE optimization_plans (
|
|
|
|
|
plan_id SERIAL PRIMARY KEY,
|
|
|
|
|
station_id VARCHAR(50) NOT NULL,
|
|
|
|
|
resource_id VARCHAR(50) NOT NULL,
|
|
|
|
|
resource_type VARCHAR(20) NOT NULL DEFAULT 'PUMP', -- 'PUMP', 'VALVE', 'BLOWER', etc.
|
|
|
|
|
|
|
|
|
|
interval_start TIMESTAMP NOT NULL,
|
|
|
|
|
interval_end TIMESTAMP NOT NULL,
|
|
|
|
|
|
|
|
|
|
-- Generic optimization targets (JSON for flexibility)
|
|
|
|
|
optimization_targets JSONB NOT NULL,
|
|
|
|
|
|
|
|
|
|
-- Version-based update metadata
|
|
|
|
|
plan_created_at TIMESTAMP DEFAULT NOW(),
|
|
|
|
|
plan_updated_at TIMESTAMP DEFAULT NOW(),
|
|
|
|
|
plan_version INTEGER DEFAULT 1,
|
|
|
|
|
optimization_run_id INTEGER,
|
|
|
|
|
plan_priority INTEGER DEFAULT 1,
|
|
|
|
|
|
|
|
|
|
-- Status tracking
|
|
|
|
|
plan_status VARCHAR(20) DEFAULT 'ACTIVE',
|
|
|
|
|
superseded_by INTEGER,
|
|
|
|
|
|
|
|
|
|
FOREIGN KEY (station_id, resource_id) REFERENCES pumps(station_id, pump_id),
|
|
|
|
|
FOREIGN KEY (superseded_by) REFERENCES optimization_plans(plan_id),
|
|
|
|
|
|
|
|
|
|
-- Ensure resource_type is valid
|
|
|
|
|
CONSTRAINT valid_resource_type CHECK (resource_type IN ('PUMP', 'VALVE', 'BLOWER', 'COMPRESSOR', 'GATE'))
|
2025-10-26 19:04:51 +00:00
|
|
|
);
|
|
|
|
|
|
2025-10-27 07:00:57 +00:00
|
|
|
COMMENT ON TABLE optimization_plans IS 'Generic optimization plans for all actuator types with version-based updates';
|
|
|
|
|
COMMENT ON COLUMN optimization_plans.optimization_targets IS 'JSON structure containing actuator-specific optimization targets';
|
|
|
|
|
COMMENT ON COLUMN optimization_plans.resource_type IS 'Type of actuator: PUMP, VALVE, BLOWER, COMPRESSOR, GATE';
|
2025-10-26 19:04:51 +00:00
|
|
|
|
2025-10-27 07:00:57 +00:00
|
|
|
-- Create indexes for generic optimization plans
|
|
|
|
|
CREATE INDEX idx_optimization_plans_active ON optimization_plans(station_id, resource_id, interval_start, interval_end)
|
|
|
|
|
WHERE plan_status = 'ACTIVE';
|
|
|
|
|
CREATE INDEX idx_optimization_plans_latest ON optimization_plans(station_id, resource_id, plan_created_at DESC)
|
|
|
|
|
WHERE plan_status = 'ACTIVE';
|
|
|
|
|
CREATE INDEX idx_optimization_plans_type ON optimization_plans(resource_type, plan_status);
|
2025-10-26 19:04:51 +00:00
|
|
|
|
|
|
|
|
-- Create pump_feedback table
|
|
|
|
|
CREATE TABLE pump_feedback (
|
|
|
|
|
feedback_id SERIAL PRIMARY KEY,
|
|
|
|
|
station_id VARCHAR(50) NOT NULL,
|
|
|
|
|
pump_id VARCHAR(50) NOT NULL,
|
|
|
|
|
timestamp TIMESTAMP NOT NULL DEFAULT NOW(),
|
|
|
|
|
|
|
|
|
|
-- Actual measurements
|
|
|
|
|
actual_speed_hz DECIMAL(5, 2),
|
|
|
|
|
actual_power_kw DECIMAL(10, 2),
|
|
|
|
|
actual_flow_m3h DECIMAL(10, 2),
|
|
|
|
|
wet_well_level_m DECIMAL(5, 2),
|
|
|
|
|
pump_running BOOLEAN,
|
|
|
|
|
|
|
|
|
|
-- Status
|
|
|
|
|
alarm_active BOOLEAN DEFAULT FALSE,
|
|
|
|
|
alarm_code VARCHAR(50),
|
|
|
|
|
|
|
|
|
|
FOREIGN KEY (station_id, pump_id) REFERENCES pumps(station_id, pump_id)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
COMMENT ON TABLE pump_feedback IS 'Real-time feedback from pumps';
|
|
|
|
|
|
|
|
|
|
-- Create index for latest feedback
|
|
|
|
|
CREATE INDEX idx_pump_feedback_latest ON pump_feedback(station_id, pump_id, timestamp DESC);
|
|
|
|
|
|
|
|
|
|
-- Create pump_safety_limits table
|
|
|
|
|
CREATE TABLE pump_safety_limits (
|
|
|
|
|
station_id VARCHAR(50) NOT NULL,
|
|
|
|
|
pump_id VARCHAR(50) NOT NULL,
|
|
|
|
|
|
|
|
|
|
-- Speed limits (Layer 2: Station Safety Limits)
|
|
|
|
|
hard_min_speed_hz DECIMAL(5, 2) NOT NULL,
|
|
|
|
|
hard_max_speed_hz DECIMAL(5, 2) NOT NULL,
|
|
|
|
|
|
|
|
|
|
-- Level limits
|
|
|
|
|
hard_min_level_m DECIMAL(5, 2),
|
|
|
|
|
hard_max_level_m DECIMAL(5, 2),
|
|
|
|
|
emergency_stop_level_m DECIMAL(5, 2), -- Emergency stop if exceeded
|
|
|
|
|
dry_run_protection_level_m DECIMAL(5, 2), -- Stop pump if level too low
|
|
|
|
|
|
|
|
|
|
-- Power and flow limits
|
|
|
|
|
hard_max_power_kw DECIMAL(10, 2),
|
|
|
|
|
hard_max_flow_m3h DECIMAL(10, 2),
|
|
|
|
|
|
|
|
|
|
-- Operational limits
|
|
|
|
|
max_starts_per_hour INTEGER DEFAULT 6,
|
|
|
|
|
min_run_time_seconds INTEGER DEFAULT 300,
|
|
|
|
|
max_continuous_run_hours INTEGER DEFAULT 24,
|
|
|
|
|
|
|
|
|
|
-- Rate of change limits (prevent sudden changes that damage equipment)
|
|
|
|
|
max_speed_change_hz_per_min DECIMAL(5, 2) DEFAULT 5.0,
|
|
|
|
|
|
|
|
|
|
-- Metadata
|
|
|
|
|
set_by VARCHAR(100),
|
|
|
|
|
set_at TIMESTAMP DEFAULT NOW(),
|
|
|
|
|
approved_by VARCHAR(100), -- Dual approval
|
|
|
|
|
approved_at TIMESTAMP,
|
|
|
|
|
notes TEXT,
|
|
|
|
|
|
|
|
|
|
PRIMARY KEY (station_id, pump_id),
|
|
|
|
|
FOREIGN KEY (station_id, pump_id) REFERENCES pumps(station_id, pump_id),
|
|
|
|
|
|
|
|
|
|
-- Constraints
|
|
|
|
|
CONSTRAINT check_speed_limits CHECK (hard_min_speed_hz >= 15 AND hard_max_speed_hz <= 55),
|
|
|
|
|
CONSTRAINT check_min_max CHECK (hard_min_speed_hz < hard_max_speed_hz),
|
|
|
|
|
CONSTRAINT check_approved CHECK (approved_by IS NULL OR approved_by != set_by) -- Dual approval
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
COMMENT ON TABLE pump_safety_limits IS 'Hard operational limits enforced by Calejo Control adapter (Layer 2)';
|
|
|
|
|
COMMENT ON COLUMN pump_safety_limits.hard_min_speed_hz IS 'Minimum speed enforced by adapter (must be >= PLC physical limit)';
|
|
|
|
|
COMMENT ON COLUMN pump_safety_limits.hard_max_speed_hz IS 'Maximum speed enforced by adapter (must be <= PLC physical limit)';
|
|
|
|
|
|
|
|
|
|
-- Create safety_limit_violations table
|
|
|
|
|
CREATE TABLE safety_limit_violations (
|
|
|
|
|
violation_id SERIAL PRIMARY KEY,
|
|
|
|
|
station_id VARCHAR(50) NOT NULL,
|
|
|
|
|
pump_id VARCHAR(50) NOT NULL,
|
|
|
|
|
requested_setpoint DECIMAL(5, 2),
|
|
|
|
|
enforced_setpoint DECIMAL(5, 2),
|
|
|
|
|
violations TEXT[], -- Array of violation descriptions
|
|
|
|
|
timestamp TIMESTAMP DEFAULT NOW(),
|
|
|
|
|
|
|
|
|
|
FOREIGN KEY (station_id, pump_id) REFERENCES pumps(station_id, pump_id)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
COMMENT ON TABLE safety_limit_violations IS 'Audit trail of safety limit violations (immutable)';
|
|
|
|
|
|
|
|
|
|
-- Create index for violation timestamp
|
|
|
|
|
CREATE INDEX idx_violations_timestamp ON safety_limit_violations(timestamp DESC);
|
|
|
|
|
|
|
|
|
|
-- Create failsafe_events table
|
|
|
|
|
CREATE TABLE failsafe_events (
|
|
|
|
|
event_id SERIAL PRIMARY KEY,
|
|
|
|
|
station_id VARCHAR(50) NOT NULL,
|
|
|
|
|
pump_id VARCHAR(50) NOT NULL,
|
|
|
|
|
event_type VARCHAR(50) NOT NULL, -- 'DATABASE_TIMEOUT', 'COMMUNICATION_LOSS', 'INVALID_DATA'
|
|
|
|
|
default_setpoint DECIMAL(5, 2),
|
|
|
|
|
triggered_by VARCHAR(100),
|
|
|
|
|
timestamp TIMESTAMP DEFAULT NOW(),
|
|
|
|
|
cleared_at TIMESTAMP,
|
|
|
|
|
notes TEXT,
|
|
|
|
|
|
|
|
|
|
FOREIGN KEY (station_id, pump_id) REFERENCES pumps(station_id, pump_id)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
COMMENT ON TABLE failsafe_events IS 'Record of failsafe mode activations';
|
|
|
|
|
|
|
|
|
|
-- Create index for active failsafe events
|
|
|
|
|
CREATE INDEX idx_failsafe_events_active ON failsafe_events(station_id, pump_id, timestamp DESC)
|
|
|
|
|
WHERE cleared_at IS NULL;
|
|
|
|
|
|
|
|
|
|
-- Create emergency_stop_events table
|
|
|
|
|
CREATE TABLE emergency_stop_events (
|
|
|
|
|
event_id SERIAL PRIMARY KEY,
|
|
|
|
|
station_id VARCHAR(50), -- NULL = all stations
|
|
|
|
|
pump_id VARCHAR(50), -- NULL = all pumps at station
|
|
|
|
|
triggered_by VARCHAR(100) NOT NULL,
|
|
|
|
|
reason TEXT NOT NULL,
|
|
|
|
|
timestamp TIMESTAMP DEFAULT NOW(),
|
|
|
|
|
cleared_at TIMESTAMP,
|
|
|
|
|
cleared_by VARCHAR(100),
|
|
|
|
|
clear_notes TEXT,
|
|
|
|
|
|
|
|
|
|
FOREIGN KEY (station_id, pump_id) REFERENCES pumps(station_id, pump_id)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
COMMENT ON TABLE emergency_stop_events IS 'Emergency stop event log (big red button)';
|
|
|
|
|
|
|
|
|
|
-- Create index for active emergency stops
|
|
|
|
|
CREATE INDEX idx_emergency_stop_active ON emergency_stop_events(timestamp DESC)
|
|
|
|
|
WHERE cleared_at IS NULL;
|
|
|
|
|
|
|
|
|
|
-- Create audit_log table
|
|
|
|
|
CREATE TABLE audit_log (
|
|
|
|
|
log_id BIGSERIAL PRIMARY KEY,
|
|
|
|
|
timestamp TIMESTAMP DEFAULT NOW(),
|
|
|
|
|
event_type VARCHAR(50) NOT NULL,
|
|
|
|
|
severity VARCHAR(20) NOT NULL, -- 'INFO', 'WARNING', 'CRITICAL'
|
|
|
|
|
station_id VARCHAR(50),
|
|
|
|
|
pump_id VARCHAR(50),
|
|
|
|
|
user_id VARCHAR(100),
|
|
|
|
|
ip_address INET,
|
|
|
|
|
protocol VARCHAR(20), -- 'OPC_UA', 'MODBUS', 'REST_API'
|
|
|
|
|
action VARCHAR(100),
|
|
|
|
|
resource VARCHAR(200),
|
|
|
|
|
result VARCHAR(20), -- 'SUCCESS', 'FAILURE', 'DENIED'
|
|
|
|
|
event_data JSONB
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
COMMENT ON TABLE audit_log IS 'Immutable audit trail for compliance (IEC 62443, ISO 27001, NIS2 Directive)';
|
|
|
|
|
|
|
|
|
|
-- Create indexes for audit log
|
|
|
|
|
CREATE INDEX idx_audit_log_timestamp ON audit_log(timestamp DESC);
|
|
|
|
|
CREATE INDEX idx_audit_log_severity ON audit_log(severity, timestamp DESC);
|
|
|
|
|
CREATE INDEX idx_audit_log_user ON audit_log(user_id, timestamp DESC);
|
|
|
|
|
|
|
|
|
|
-- Make audit log immutable (append-only)
|
|
|
|
|
CREATE RULE audit_log_no_update AS ON UPDATE TO audit_log DO INSTEAD NOTHING;
|
|
|
|
|
CREATE RULE audit_log_no_delete AS ON DELETE TO audit_log DO INSTEAD NOTHING;
|
|
|
|
|
|
|
|
|
|
-- Create database user for adapter
|
|
|
|
|
-- Note: This should be executed by database administrator
|
|
|
|
|
-- CREATE USER control_reader WITH PASSWORD 'secure_password';
|
|
|
|
|
-- GRANT CONNECT ON DATABASE calejo TO control_reader;
|
|
|
|
|
-- GRANT USAGE ON SCHEMA public TO control_reader;
|
|
|
|
|
-- GRANT SELECT ON pump_stations, pumps, pump_plans, pump_feedback, pump_safety_limits TO control_reader;
|
|
|
|
|
-- GRANT INSERT ON safety_limit_violations, failsafe_events, emergency_stop_events, audit_log TO control_reader;
|
|
|
|
|
-- GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO control_reader;
|