141 lines
5.6 KiB
SQL
141 lines
5.6 KiB
SQL
-- Run once as the TimescaleDB superuser (fitdata):
|
|
-- psql -h localhost -p 5433 -U fitdata -f schema.sql
|
|
|
|
CREATE DATABASE energy;
|
|
|
|
\c energy
|
|
|
|
CREATE EXTENSION IF NOT EXISTS timescaledb;
|
|
|
|
CREATE USER energy WITH PASSWORD 'changeme';
|
|
GRANT ALL ON DATABASE energy TO energy;
|
|
GRANT ALL ON SCHEMA public TO energy;
|
|
|
|
-- ── Raw hypertables ───────────────────────────────────────────────────────────
|
|
|
|
-- Written by energy-collector (pvcollect replacement) every 10 s
|
|
CREATE TABLE inverter (
|
|
time TIMESTAMPTZ NOT NULL,
|
|
pv1_power REAL, -- W
|
|
pv2_power REAL, -- W
|
|
pv_l1_power REAL, -- W (inverter AC output phase 1)
|
|
pv_l2_power REAL, -- W
|
|
pv_l3_power REAL, -- W
|
|
battery_soc REAL, -- %
|
|
grid_import_kwh REAL, -- kWh cumulative
|
|
grid_export_kwh REAL, -- kWh cumulative
|
|
pv_energy_kwh REAL -- kWh cumulative
|
|
);
|
|
SELECT create_hypertable('inverter', 'time', chunk_time_interval => INTERVAL '7 days');
|
|
|
|
-- Written by energy-collector (meter replacement) every 10 s, one row per device
|
|
CREATE TABLE power_meter (
|
|
time TIMESTAMPTZ NOT NULL,
|
|
device TEXT NOT NULL, -- 'house' | 'barn'
|
|
l1_power REAL, -- W
|
|
l2_power REAL, -- W
|
|
l3_power REAL, -- W
|
|
import_kwh REAL, -- kWh cumulative
|
|
export_kwh REAL -- kWh cumulative
|
|
);
|
|
SELECT create_hypertable('power_meter', 'time', chunk_time_interval => INTERVAL '7 days');
|
|
CREATE INDEX ON power_meter (device, time DESC);
|
|
|
|
GRANT ALL ON ALL TABLES IN SCHEMA public TO energy;
|
|
|
|
-- ── Continuous aggregates ─────────────────────────────────────────────────────
|
|
|
|
-- 10-minute buckets — live chart (1 h, 6 h, 24 h ranges)
|
|
CREATE MATERIALIZED VIEW inverter_10m
|
|
WITH (timescaledb.continuous, timescaledb.materialized_only = false) AS
|
|
SELECT time_bucket('10 minutes', time) AS bucket,
|
|
AVG(pv1_power + pv2_power) AS pv_power,
|
|
AVG(battery_soc) AS battery_soc
|
|
FROM inverter
|
|
GROUP BY bucket;
|
|
|
|
CREATE MATERIALIZED VIEW power_meter_10m
|
|
WITH (timescaledb.continuous, timescaledb.materialized_only = false) AS
|
|
SELECT time_bucket('10 minutes', time) AS bucket,
|
|
device,
|
|
AVG(l1_power + l2_power + l3_power) AS total_power
|
|
FROM power_meter
|
|
GROUP BY bucket, device;
|
|
|
|
-- 1-hour buckets — live chart (7 d range), built on 10 m (hierarchical)
|
|
CREATE MATERIALIZED VIEW inverter_1h
|
|
WITH (timescaledb.continuous, timescaledb.materialized_only = false) AS
|
|
SELECT time_bucket('1 hour', bucket) AS bucket,
|
|
AVG(pv_power) AS pv_power,
|
|
AVG(battery_soc) AS battery_soc
|
|
FROM inverter_10m
|
|
GROUP BY 1;
|
|
|
|
CREATE MATERIALIZED VIEW power_meter_1h
|
|
WITH (timescaledb.continuous, timescaledb.materialized_only = false) AS
|
|
SELECT time_bucket('1 hour', bucket) AS bucket,
|
|
device,
|
|
AVG(total_power) AS total_power
|
|
FROM power_meter_10m
|
|
GROUP BY 1, device;
|
|
|
|
-- Daily last-value snapshots — basis for bar-chart energy deltas
|
|
-- Query with LAG() to get per-period consumption.
|
|
CREATE MATERIALIZED VIEW inverter_daily
|
|
WITH (timescaledb.continuous, timescaledb.materialized_only = false) AS
|
|
SELECT time_bucket('1 day', time) AS bucket,
|
|
last(grid_import_kwh, time) AS grid_import_kwh,
|
|
last(grid_export_kwh, time) AS grid_export_kwh,
|
|
last(pv_energy_kwh, time) AS pv_energy_kwh
|
|
FROM inverter
|
|
GROUP BY bucket;
|
|
|
|
CREATE MATERIALIZED VIEW power_meter_daily
|
|
WITH (timescaledb.continuous, timescaledb.materialized_only = false) AS
|
|
SELECT time_bucket('1 day', time) AS bucket,
|
|
device,
|
|
last(import_kwh, time) AS import_kwh
|
|
FROM power_meter
|
|
GROUP BY bucket, device;
|
|
|
|
-- ── Retention — keep 30 days of raw data; aggregates stay forever ─────────────
|
|
|
|
SELECT add_retention_policy('inverter', INTERVAL '30 days');
|
|
SELECT add_retention_policy('power_meter', INTERVAL '30 days');
|
|
|
|
-- ── Refresh policies ──────────────────────────────────────────────────────────
|
|
|
|
SELECT add_continuous_aggregate_policy('inverter_10m',
|
|
start_offset => INTERVAL '1 hour',
|
|
end_offset => INTERVAL '10 minutes',
|
|
schedule_interval => INTERVAL '10 minutes');
|
|
|
|
SELECT add_continuous_aggregate_policy('power_meter_10m',
|
|
start_offset => INTERVAL '1 hour',
|
|
end_offset => INTERVAL '10 minutes',
|
|
schedule_interval => INTERVAL '10 minutes');
|
|
|
|
SELECT add_continuous_aggregate_policy('inverter_1h',
|
|
start_offset => INTERVAL '3 hours',
|
|
end_offset => INTERVAL '1 hour',
|
|
schedule_interval => INTERVAL '1 hour');
|
|
|
|
SELECT add_continuous_aggregate_policy('power_meter_1h',
|
|
start_offset => INTERVAL '3 hours',
|
|
end_offset => INTERVAL '1 hour',
|
|
schedule_interval => INTERVAL '1 hour');
|
|
|
|
SELECT add_continuous_aggregate_policy('inverter_daily',
|
|
start_offset => INTERVAL '3 days',
|
|
end_offset => INTERVAL '1 day',
|
|
schedule_interval => INTERVAL '1 day');
|
|
|
|
SELECT add_continuous_aggregate_policy('power_meter_daily',
|
|
start_offset => INTERVAL '3 days',
|
|
end_offset => INTERVAL '1 day',
|
|
schedule_interval => INTERVAL '1 day');
|
|
|
|
-- Grant SELECT on all tables and views (including continuous aggregates) to energy.
|
|
-- Run after all views are created so the grant covers them.
|
|
GRANT SELECT ON ALL TABLES IN SCHEMA public TO energy;
|