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