This commit is contained in:
Dusan Vojacek
2026-03-20 14:30:03 +01:00
parent 2cc5ccfda7
commit 897b95f728
48 changed files with 4034 additions and 842 deletions

View File

@@ -73,8 +73,34 @@ SELECT create_hypertable(
-- ============================================================
-- Kompresní politiky pro staré chunky
-- Telemetrie starší 30 dní komprimovat (čtení stačí)
-- Nutné nejdřív zapnout kompresi na hypertable (TimescaleDB 2.x+ / Tiger Data),
-- jinak add_compression_policy hlásí chybu o columnstore / compression.
-- ============================================================
ALTER TABLE ems.telemetry_inverter SET (
timescaledb.compress,
timescaledb.compress_orderby = 'measured_at DESC',
timescaledb.compress_segmentby = 'site_id, inverter_id'
);
ALTER TABLE ems.telemetry_ev_charger SET (
timescaledb.compress,
timescaledb.compress_orderby = 'measured_at DESC',
timescaledb.compress_segmentby = 'site_id, charger_id, connector_id'
);
ALTER TABLE ems.telemetry_heat_pump SET (
timescaledb.compress,
timescaledb.compress_orderby = 'measured_at DESC',
timescaledb.compress_segmentby = 'site_id, heat_pump_id'
);
ALTER TABLE ems.market_interval_price SET (
timescaledb.compress,
timescaledb.compress_orderby = 'interval_start DESC',
timescaledb.compress_segmentby = 'market_source'
);
SELECT add_compression_policy('ems.telemetry_inverter', INTERVAL '30 days', if_not_exists => TRUE);
SELECT add_compression_policy('ems.telemetry_ev_charger', INTERVAL '30 days', if_not_exists => TRUE);
SELECT add_compression_policy('ems.telemetry_heat_pump', INTERVAL '30 days', if_not_exists => TRUE);

View File

@@ -1,7 +1,12 @@
-- =============================================================
-- V003__seed_site_home01.sql
-- EMS Platform seed data první lokality home-01
-- Doplnit: latitude, longitude, IP adresy, azimuty FVE polí
--
-- Deye Modbus (holding, SUN-20K) viz docs/04-modules/telemetry.md:
-- 0x0215 PV W, 0x0103 SoC %, 0x0105 bat W, 0x0101 bat V×0.1,
-- 0x0169 grid W, 0x016F grid L1 V×0.1, 0x0213 load W,
-- 0x0220 inv temp ×0.1, 0x0168 mode, 0x0180 fault
-- Teltonika / Samsung registry: TODO doplnit z dokumentace / Loxone šablony
-- =============================================================
-- ============================================================
@@ -13,8 +18,8 @@ VALUES (
'home-01',
'Hlavní objekt',
'Europe/Prague',
NULL, -- TODO: doplnit GPS
NULL, -- TODO: doplnit GPS
49.24466967511591,
17.40658656876068,
true,
'První instalace. Deye 20kW + 64kWh baterie + 2x Teltonika EV + Samsung TČ.'
);
@@ -27,13 +32,11 @@ VALUES (
INSERT INTO ems.site_endpoint (site_id, endpoint_type, host, port, protocol, unit_id, enabled, notes)
SELECT id, 'modbus_tcp', '192.168.1.100', 502, 'modbus_tcp', 1, true, 'Waveshare WS-ETH pro Deye SUN-20K. Unit ID dle DIP přepínače.'
FROM ems.site WHERE code = 'home-01';
-- TODO: doplnit skutečnou IP adresy Waveshare
-- Teltonika EV nabíječka 1 přes Waveshare
INSERT INTO ems.site_endpoint (site_id, endpoint_type, host, port, protocol, unit_id, enabled, notes)
SELECT id, 'modbus_tcp', '192.168.1.101', 502, 'modbus_tcp', 1, true, 'Waveshare pro Teltonika TeltoCharge #1.'
FROM ems.site WHERE code = 'home-01';
-- TODO: doplnit IP a unit_id
-- Teltonika EV nabíječka 2 přes Waveshare
INSERT INTO ems.site_endpoint (site_id, endpoint_type, host, port, protocol, unit_id, enabled, notes)
@@ -49,7 +52,6 @@ FROM ems.site WHERE code = 'home-01';
INSERT INTO ems.site_endpoint (site_id, endpoint_type, host, port, protocol, unit_id, enabled, notes)
SELECT id, 'loxone_http', '192.168.1.10', 80, 'http', NULL, true, 'Loxone Miniserver příjem setpointů přes Virtual HTTP Inputs.'
FROM ems.site WHERE code = 'home-01';
-- TODO: doplnit IP Loxone
-- ============================================================
-- SÍŤOVÉ PŘIPOJENÍ
@@ -126,12 +128,12 @@ INSERT INTO ems.asset_pv_array (site_id, inverter_id, code, name, nominal_power_
SELECT
s.id, inv.id, 'pv-a', 'FVE pole A',
10000, -- 10 kWp
NULL, -- TODO: doplnit azimut (0=jih)
NULL, -- TODO: doplnit sklon (stupně)
184,
35, -- sklon odhad; upřesnit dle střechy
NULL,
1.0,
true,
'Hlavní FVE pole řízené Deye střídačem. Doplnit azimut a sklon.'
'Hlavní FVE pole řízené Deye střídačem.'
FROM ems.site s
JOIN ems.asset_inverter inv ON inv.site_id = s.id AND inv.code = 'deye-main'
WHERE s.code = 'home-01';
@@ -141,8 +143,8 @@ INSERT INTO ems.asset_pv_array (site_id, inverter_id, code, name, nominal_power_
SELECT
s.id, inv.id, 'pv-b', 'FVE pole B (ongrid)',
10000,
NULL, -- TODO: doplnit azimut
NULL, -- TODO: doplnit sklon
184,
35,
NULL,
1.0,
false,
@@ -187,15 +189,15 @@ INSERT INTO ems.asset_heat_pump (
tuv_temp_sensor_ref, schedulable, notes
)
SELECT
s.id, 'hp-samsung', 'Samsung', NULL, -- TODO: doplnit model
s.id, 'hp-samsung', 'Samsung', 'EHS Mono (placeholder)',
ep.id,
NULL, -- TODO: doplnit jmenovitý výkon W
NULL, -- TODO: doplnit COP rated
12000, -- jmenovitý topný výkon W upřesnit z datasheetu
3.20, -- COP @ 7 °C upřesnit
7.0, -- referenční teplota A7/W35
30, 15,
NULL, -- TODO: doplnit objem zásobníku
200, -- objem TUV zásobníku (l) upřesnit
45, 60, 55,
NULL, -- TODO: doplnit odkaz na teplotní čidlo
'TODO: Loxone / Modbus čidlo TUV',
true,
'Samsung tepelné čerpadlo s Modbus modulem. Řídit dle COP a venkovní teploty (výhodné kolem poledne v chladných měsících).'
FROM ems.site s

View File

@@ -0,0 +1,5 @@
-- Zapnutí/vypnutí střídače pro sběr telemetrie a plánování (JOIN s endpointem zůstává nutný).
ALTER TABLE ems.asset_inverter
ADD COLUMN IF NOT EXISTS active BOOLEAN NOT NULL DEFAULT true;
COMMENT ON COLUMN ems.asset_inverter.active IS 'Pokud false, střídač se přeskočí při sběru telemetrie a plánování.';

View File

@@ -0,0 +1,26 @@
-- Role pro PostgREST anonymní přístup (read-only).
-- GRANT na views je v db/views/R__z_postgrest_ems_anon_grants.sql (Flyway je aplikuje až po R__vw_*).
DO $$ BEGIN
IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'ems_anon') THEN
CREATE ROLE ems_anon NOLOGIN;
END IF;
END $$;
GRANT USAGE ON SCHEMA ems TO ems_anon;
-- Read-only na tabulky (existují po V001V008)
GRANT SELECT ON ems.market_interval_price TO ems_anon;
GRANT SELECT ON ems.planning_run TO ems_anon;
GRANT SELECT ON ems.planning_interval TO ems_anon;
GRANT SELECT ON ems.forecast_pv_interval TO ems_anon;
GRANT SELECT ON ems.forecast_pv_run TO ems_anon;
GRANT SELECT ON ems.operating_mode_def TO ems_anon;
GRANT SELECT ON ems.site_operating_mode TO ems_anon;
GRANT SELECT ON ems.site_operating_mode_log TO ems_anon;
GRANT SELECT ON ems.ev_session TO ems_anon;
GRANT SELECT ON ems.asset_vehicle TO ems_anon;
COMMENT ON ROLE ems_anon IS
'Anonymní role pro PostgREST. Read-only přístup na views a vybrané tabulky.
Zápisy jdou výhradně přes FastAPI backend který má vlastní DB connection.';

View File

@@ -0,0 +1,40 @@
-- =============================================================
-- V010__indexes.sql
-- B-tree indexy pro časté dotazy (plán, telemetrie, ceny, audit, EV, režimy).
-- Pozn.: idx_ev_session_active na (charger_id, session_end) je ve V006;
-- zde idx_ev_session_site_active doplňuje vyhledávání aktivní session podle site.
-- =============================================================
-- Planning (control exporter hledá aktivní plán pro aktuální slot)
CREATE INDEX IF NOT EXISTS idx_planning_run_site_status
ON ems.planning_run (site_id, status, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_planning_interval_run_start
ON ems.planning_interval (run_id, interval_start);
-- Telemetrie (dashboard čte poslední hodnoty)
CREATE INDEX IF NOT EXISTS idx_telemetry_inverter_site_time
ON ems.telemetry_inverter (site_id, measured_at DESC);
CREATE INDEX IF NOT EXISTS idx_telemetry_ev_site_time
ON ems.telemetry_ev_charger (site_id, measured_at DESC);
CREATE INDEX IF NOT EXISTS idx_telemetry_hp_site_time
ON ems.telemetry_heat_pump (site_id, measured_at DESC);
-- Market prices (forecast + planning čte ceny pro horizont)
CREATE INDEX IF NOT EXISTS idx_market_price_source_start
ON ems.market_interval_price (market_source, interval_start);
-- Audit (dashboard čte dnešní data)
CREATE INDEX IF NOT EXISTS idx_audit_interval_site_start
ON ems.audit_interval (site_id, interval_start DESC);
-- EV session (control exporter + UI hledá aktivní session podle lokality)
CREATE INDEX IF NOT EXISTS idx_ev_session_site_active
ON ems.ev_session (site_id, session_end)
WHERE session_end IS NULL;
-- Operating mode log
CREATE INDEX IF NOT EXISTS idx_mode_log_site_time
ON ems.site_operating_mode_log (site_id, activated_at DESC);

View File

@@ -0,0 +1,58 @@
-- ============================================================
-- V011__indexes_and_aggregates.sql
-- Doplňuje V010__indexes.sql: indexy na forecast + hourly CA telemetrie.
-- (Indexy planning_run, planning_interval, market_price, audit, mode_log,
-- ev_session jsou již ve V010 zde se neopakují, aby nevznikly duplicitní B-stromy.)
-- ============================================================
-- ============================================================
-- Indexy pro výkon (forecast nové oproti V010)
-- ============================================================
CREATE INDEX IF NOT EXISTS idx_forecast_run_site_array
ON ems.forecast_pv_run (site_id, pv_array_id, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_forecast_interval_run_start
ON ems.forecast_pv_interval (run_id, interval_start);
-- ============================================================
-- TimescaleDB Continuous Aggregates pro dashboard výkon
-- ============================================================
-- Hodinové agregáty telemetrie střídače (pro graf posledních 7 dní)
CREATE MATERIALIZED VIEW IF NOT EXISTS ems.telemetry_inverter_hourly
WITH (timescaledb.continuous) AS
SELECT
time_bucket('1 hour', measured_at) AS hour,
site_id,
AVG(pv_power_w)::INT AS avg_pv_w,
AVG(battery_power_w)::INT AS avg_battery_w,
AVG(grid_power_w)::INT AS avg_grid_w,
AVG(load_power_w)::INT AS avg_load_w,
LAST(battery_soc_percent, measured_at) AS last_soc_pct,
COUNT(*) AS sample_count
FROM ems.telemetry_inverter
GROUP BY hour, site_id
WITH NO DATA;
-- Refresh policy: každých 15 minut. Okno musí pokrývat ≥2× time_bucket (1h) → min. šířka >2h.
SELECT add_continuous_aggregate_policy(
'ems.telemetry_inverter_hourly',
start_offset => INTERVAL '2 hours 15 minutes',
end_offset => INTERVAL '1 minute',
schedule_interval => INTERVAL '15 minutes'
);
-- ============================================================
-- View pro použití v dashboardu (7 dní zpět)
-- ============================================================
CREATE OR REPLACE VIEW ems.vw_telemetry_hourly_7d AS
SELECT *
FROM ems.telemetry_inverter_hourly
WHERE hour >= now() - INTERVAL '7 days'
ORDER BY hour DESC;
COMMENT ON VIEW ems.telemetry_inverter_hourly IS
'Hodinové agregáty telemetrie střídače. TimescaleDB continuous aggregate.
Refresh každých 15 minut. Používat pro grafy delší než 1 den.';