Initial commit: EMS — Energie Management System
Complete self-consumption optimisation system for 7 kWp PV installation: - Prometheus collector (grid power, SOC, PV, per-phase, compressor) - Pure decision engine with SOC gates, hysteresis, priority ordering - Shelly Gen1/Gen2 actuator (SHA-256 Digest auth, PM power readback) - Viessmann OAuth2 client for DHW temperature control - PV forecast integration (forecast.solar) - Wallbox mutual exclusion (VX3 4.6 kW AC output constraint) - Car-not-charging detection via Shelly PM - Compressor idle → early SG-Ready release - Per-phase grid power for single-phase wallbox decisions - Manual override detection and web UI with override buttons - Full unit test coverage for decision engine - systemd service, Makefile, complete documentation Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
120
internal/metrics/metrics.go
Normal file
120
internal/metrics/metrics.go
Normal file
@@ -0,0 +1,120 @@
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/tb/ems/internal/engine"
|
||||
)
|
||||
|
||||
// Metrics holds all EMS Prometheus metrics.
|
||||
type Metrics struct {
|
||||
// Current system state
|
||||
GridPowerW prometheus.Gauge
|
||||
BatterySOC prometheus.Gauge
|
||||
PVProductionW prometheus.Gauge
|
||||
|
||||
// Consumer states
|
||||
ConsumerActive *prometheus.GaugeVec
|
||||
|
||||
// Decision tracking
|
||||
SwitchCyclesTotal *prometheus.CounterVec
|
||||
DecisionDuration prometheus.Histogram
|
||||
|
||||
// Actuator health
|
||||
ActuatorErrors *prometheus.CounterVec
|
||||
APILatency *prometheus.HistogramVec
|
||||
|
||||
// Cumulative
|
||||
SelfConsumedKWh prometheus.Counter
|
||||
GridExportKWh prometheus.Counter
|
||||
GridImportKWh prometheus.Counter
|
||||
}
|
||||
|
||||
// NewMetrics creates and registers all EMS metrics.
|
||||
func NewMetrics(reg prometheus.Registerer) *Metrics {
|
||||
m := &Metrics{
|
||||
GridPowerW: prometheus.NewGauge(prometheus.GaugeOpts{
|
||||
Name: "ems_grid_power_watts",
|
||||
Help: "Current grid power exchange in watts (positive=import, negative=export)",
|
||||
}),
|
||||
BatterySOC: prometheus.NewGauge(prometheus.GaugeOpts{
|
||||
Name: "ems_battery_soc_percent",
|
||||
Help: "Current battery state of charge in percent",
|
||||
}),
|
||||
PVProductionW: prometheus.NewGauge(prometheus.GaugeOpts{
|
||||
Name: "ems_pv_production_watts",
|
||||
Help: "Current PV production in watts",
|
||||
}),
|
||||
ConsumerActive: prometheus.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Name: "ems_consumer_active",
|
||||
Help: "Whether a consumer is currently active (1=on, 0=off)",
|
||||
}, []string{"consumer"}),
|
||||
SwitchCyclesTotal: prometheus.NewCounterVec(prometheus.CounterOpts{
|
||||
Name: "ems_switch_cycles_total",
|
||||
Help: "Total number of switch cycles per consumer",
|
||||
}, []string{"consumer", "action"}),
|
||||
DecisionDuration: prometheus.NewHistogram(prometheus.HistogramOpts{
|
||||
Name: "ems_decision_duration_seconds",
|
||||
Help: "Time taken for the decision engine to evaluate",
|
||||
Buckets: prometheus.DefBuckets,
|
||||
}),
|
||||
ActuatorErrors: prometheus.NewCounterVec(prometheus.CounterOpts{
|
||||
Name: "ems_actuator_errors_total",
|
||||
Help: "Total actuator errors per consumer",
|
||||
}, []string{"consumer"}),
|
||||
APILatency: prometheus.NewHistogramVec(prometheus.HistogramOpts{
|
||||
Name: "ems_api_latency_seconds",
|
||||
Help: "API call latency per source",
|
||||
Buckets: []float64{0.01, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10},
|
||||
}, []string{"source"}),
|
||||
SelfConsumedKWh: prometheus.NewCounter(prometheus.CounterOpts{
|
||||
Name: "ems_self_consumed_kwh_total",
|
||||
Help: "Total energy self-consumed in kWh (estimated)",
|
||||
}),
|
||||
GridExportKWh: prometheus.NewCounter(prometheus.CounterOpts{
|
||||
Name: "ems_grid_export_kwh_total",
|
||||
Help: "Total energy exported to grid in kWh (estimated)",
|
||||
}),
|
||||
GridImportKWh: prometheus.NewCounter(prometheus.CounterOpts{
|
||||
Name: "ems_grid_import_kwh_total",
|
||||
Help: "Total energy imported from grid in kWh (estimated)",
|
||||
}),
|
||||
}
|
||||
|
||||
reg.MustRegister(
|
||||
m.GridPowerW,
|
||||
m.BatterySOC,
|
||||
m.PVProductionW,
|
||||
m.ConsumerActive,
|
||||
m.SwitchCyclesTotal,
|
||||
m.DecisionDuration,
|
||||
m.ActuatorErrors,
|
||||
m.APILatency,
|
||||
m.SelfConsumedKWh,
|
||||
m.GridExportKWh,
|
||||
m.GridImportKWh,
|
||||
)
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
// RecordActions records switching actions in the metrics.
|
||||
func (m *Metrics) RecordActions(actions []engine.Action) {
|
||||
for _, a := range actions {
|
||||
action := "on"
|
||||
if !a.TurnOn {
|
||||
action = "off"
|
||||
}
|
||||
m.SwitchCyclesTotal.WithLabelValues(a.Consumer.String(), action).Inc()
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateConsumerStates updates the consumer active gauges.
|
||||
func (m *Metrics) UpdateConsumerStates(states map[engine.Consumer]bool) {
|
||||
for c, active := range states {
|
||||
val := 0.0
|
||||
if active {
|
||||
val = 1.0
|
||||
}
|
||||
m.ConsumerActive.WithLabelValues(c.String()).Set(val)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user