Files
EMS/internal/metrics/metrics.go
Lutz Finsterle 99613c52ae 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>
2026-04-06 10:02:16 +02:00

121 lines
3.5 KiB
Go

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)
}
}