-- 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; -- Written by energy-collector every 10 s CREATE TABLE charger ( time TIMESTAMPTZ NOT NULL, power REAL, -- W total charging power eto_wh BIGINT -- Wh cumulative total energy (from go-e eto field) ); SELECT create_hypertable('charger', 'time', chunk_time_interval => INTERVAL '7 days'); CREATE MATERIALIZED VIEW charger_10m WITH (timescaledb.continuous, timescaledb.materialized_only = false) AS SELECT time_bucket('10 minutes', time) AS bucket, AVG(power) AS power FROM charger GROUP BY bucket; CREATE MATERIALIZED VIEW charger_1h WITH (timescaledb.continuous, timescaledb.materialized_only = false) AS SELECT time_bucket('1 hour', bucket) AS bucket, AVG(power) AS power FROM charger_10m GROUP BY 1; CREATE MATERIALIZED VIEW charger_daily WITH (timescaledb.continuous, timescaledb.materialized_only = false) AS SELECT time_bucket('1 day', time) AS bucket, last(eto_wh, time) AS eto_wh FROM charger GROUP BY bucket; -- ── 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'); SELECT add_retention_policy('charger', 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'); SELECT add_continuous_aggregate_policy('charger_10m', start_offset => INTERVAL '1 hour', end_offset => INTERVAL '10 minutes', schedule_interval => INTERVAL '10 minutes'); SELECT add_continuous_aggregate_policy('charger_1h', start_offset => INTERVAL '3 hours', end_offset => INTERVAL '1 hour', schedule_interval => INTERVAL '1 hour'); SELECT add_continuous_aggregate_policy('charger_daily', start_offset => INTERVAL '3 days', end_offset => INTERVAL '1 day', schedule_interval => INTERVAL '1 day'); -- Grant privileges after all objects are created. -- INSERT on raw hypertables (collector writes), SELECT on everything else (frontend reads). GRANT INSERT ON inverter, power_meter, charger TO energy; GRANT SELECT ON ALL TABLES IN SCHEMA public TO energy;