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>
121 lines
3.5 KiB
Go
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)
|
|
}
|
|
}
|