- charger.go: polls go-e /api/status?filter=nrg,eto every 10 s - db.go: WriteCharger() inserts into charger hypertable - config.go: ChargerConf with host field - main.go: polls charger in parallel with inverter and meters - schema.sql: charger table + charger_10m/1h/daily aggregates + policies Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
187 lines
7.2 KiB
SQL
187 lines
7.2 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;
|
|
|
|
-- 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;
|