From 61f392e039d471d4a09f90d058362456e2996c7a Mon Sep 17 00:00:00 2001 From: andig Date: Sun, 22 Mar 2020 15:13:53 +0100 Subject: [PATCH] Make notifications configurable --- cmd/config.go | 2 + cmd/root.go | 12 ++- core/{loadpoint_api.go => api.go} | 15 +-- core/loadpoint.go | 168 +++++++++++++++++++++++++++--- core/loadpoint_handler.go | 108 ------------------- evcc.dist.yaml | 21 ++++ push/event.go | 9 +- push/push.go | 32 +++--- 8 files changed, 210 insertions(+), 157 deletions(-) rename core/{loadpoint_api.go => api.go} (91%) delete mode 100644 core/loadpoint_handler.go diff --git a/cmd/config.go b/cmd/config.go index 062c9d5ae..ba82a0807 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -4,6 +4,7 @@ import ( "time" "github.com/andig/evcc/api" + "github.com/andig/evcc/push" "github.com/andig/evcc/server" ) @@ -24,6 +25,7 @@ type config struct { type messagingConfig struct { App string Recipients []string + Events map[string]push.EventTemplate } type mqttConfig struct { diff --git a/cmd/root.go b/cmd/root.go index dae5e3d03..e2841a5ce 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -179,16 +179,18 @@ func run(cmd *cobra.Command, args []string) { log.INFO.Println("listening at", uri) // setup messaging - notificationChan := make(chan push.Event, 1) - notificationHub := &push.Hub{} - + var pushOver *push.PushOver if conf.Pushover.App != "" { - notificationHub.PushOver = push.NewMessenger(conf.Pushover.App, conf.Pushover.Recipients) + pushOver = push.NewMessenger(conf.Pushover.App, conf.Pushover.Recipients) } - loadPoints := loadConfig(conf, notificationChan) + notificationChan := make(chan push.Event, 1) + notificationHub := push.NewHub(conf.Pushover.Events, pushOver) go notificationHub.Run(notificationChan) + // setup loadpoints + loadPoints := loadConfig(conf, notificationChan) + // start broadcasting values valueChan := make(chan core.Param) triggerChan := make(chan struct{}) diff --git a/core/loadpoint_api.go b/core/api.go similarity index 91% rename from core/loadpoint_api.go rename to core/api.go index d6ab9b4d4..b6b169b65 100644 --- a/core/loadpoint_api.go +++ b/core/api.go @@ -9,8 +9,9 @@ import ( // Param is the broadcast channel data type type Param struct { - Key string - Val interface{} + LoadPoint string + Key string + Val interface{} } // Configuration is the loadpoint feature structure @@ -139,14 +140,14 @@ func (lp *LoadPoint) publishSoC() { if lp.connected() { f, err := lp.SoC.ChargeState() if err == nil { - log.TRACE.Printf("%s soc charge: %.1f%%", lp.Name, f) - lp.uiChan <- Param{Key: "socCharge", Val: f} - lp.uiChan <- Param{Key: "chargeEstimate", Val: lp.remainingChargeDuration(f)} + log.DEBUG.Printf("%s soc charge: %.1f%%", lp.Name, f) + lp.publish("socCharge", f) + lp.publish("chargeEstimate", lp.remainingChargeDuration(f)) return } log.ERROR.Printf("%s soc error: %v", lp.Name, err) } - lp.uiChan <- Param{Key: "socCharge", Val: "—"} - lp.uiChan <- Param{Key: "chargeEstimate", Val: -1} + lp.publish("socCharge", "—") + lp.publish("chargeEstimate", -1) } diff --git a/core/loadpoint.go b/core/loadpoint.go index c6c5ed89f..de03a59e7 100644 --- a/core/loadpoint.go +++ b/core/loadpoint.go @@ -7,6 +7,7 @@ import ( "time" "github.com/andig/evcc/api" + "github.com/andig/evcc/core/wrapper" "github.com/andig/evcc/push" evbus "github.com/asaskevich/EventBus" @@ -17,6 +18,14 @@ var ( presence = map[bool]string{false: "—", true: "✓"} ) +const ( + evcc = "evcc" // notification sender + evStartCharge = "start" // update chargeTimer + evStopCharge = "stop" // update chargeTimer + evChargeCurrent = "current" // update fakeChargeMeter + evChargePower = "power" // update chargeRater +) + // powerToCurrent is a helper function to convert power to per-phase current func powerToCurrent(power, voltage float64, phases int64) int64 { return int64(power / (float64(phases) * voltage)) @@ -71,6 +80,8 @@ func NewLoadPoint() *LoadPoint { bus: evbus.New(), triggerChan: make(chan struct{}, 1), Name: "Main", + Mode: api.ModeOff, + status: api.StatusNone, Phases: 1, Voltage: 230, // V MinCurrent: 6, // A @@ -81,6 +92,130 @@ func NewLoadPoint() *LoadPoint { } } +// notify sends push messages to clients +func (lp *LoadPoint) notify(event string, attributes map[string]interface{}) { + attributes["loadpoint"] = lp.Name + lp.notificationChan <- push.Event{ + Event: event, + Sender: evcc, + Attributes: attributes, + } +} + +// publish sends values to UI and databases +func (lp *LoadPoint) publish(key string, val interface{}) { + lp.uiChan <- Param{ + LoadPoint: lp.Name, + Key: key, + Val: val, + } +} + +// evChargeStartHandler sends external start event +func (lp *LoadPoint) evChargeStartHandler() { + lp.notify(evStartCharge, map[string]interface{}{ + "mode": lp.GetMode(), + }) +} + +// evChargeStartHandler sends external stop event +func (lp *LoadPoint) evChargeStopHandler() { + energy, err := lp.ChargeRater.ChargedEnergy() + if err != nil { + log.ERROR.Printf("%s charged energy: %v", lp.Name, err) + } + + duration, err := lp.ChargeTimer.ChargingTime() + if err != nil { + log.ERROR.Printf("%s charge duration: %v", lp.Name, err) + } + + lp.notify(evStopCharge, map[string]interface{}{ + "energy": energy, + "duration": duration.Truncate(time.Second), + }) +} + +// evChargeCurrentHandler updates proxy charge meter's charge current. +// If physical charge meter is present this handler is not used. +func (lp *LoadPoint) evChargeCurrentHandler(m *wrapper.ChargeMeter) func(para ...interface{}) { + return func(para ...interface{}) { + current := para[0].(int64) + if !lp.enabled { + current = 0 + } + if lp.status != api.StatusB && lp.status != api.StatusC { + current = 0 + } + m.SetChargeCurrent(current) + } +} + +// Prepare loadpoint configuration by adding missing helper elements +func (lp *LoadPoint) Prepare(uiChan chan<- Param, notificationChan chan<- push.Event) { + lp.notificationChan = notificationChan + lp.uiChan = uiChan + + if lp.PVMeter == nil && lp.GridMeter == nil { + log.FATAL.Fatal("missing either PV or Grid meter - aborting") + } + + // ensure charge meter exists + if lp.ChargeMeter == nil { + if mt, ok := lp.Charger.(api.Meter); ok { + lp.ChargeMeter = mt + } else { + mt := &wrapper.ChargeMeter{ + Phases: lp.Phases, + Voltage: lp.Voltage, + } + _ = lp.bus.Subscribe(evChargeCurrent, lp.evChargeCurrentHandler(mt)) + _ = lp.bus.Subscribe(evStopCharge, func() { + mt.SetChargeCurrent(0) + }) + lp.ChargeMeter = mt + } + } + + // ensure charge rater exists + if rt, ok := lp.Charger.(api.ChargeRater); ok { + lp.ChargeRater = rt + } else { + rt := wrapper.NewChargeRater(lp.Name, lp.ChargeMeter) + _ = lp.bus.Subscribe(evChargePower, rt.SetChargePower) + _ = lp.bus.Subscribe(evStartCharge, rt.StartCharge) + _ = lp.bus.Subscribe(evStopCharge, rt.StopCharge) + lp.ChargeRater = rt + } + + // ensure charge timer exists + if ct, ok := lp.Charger.(api.ChargeTimer); ok { + lp.ChargeTimer = ct + } else { + ct := wrapper.NewChargeTimer() + _ = lp.bus.Subscribe(evStartCharge, ct.StartCharge) + _ = lp.bus.Subscribe(evStopCharge, ct.StopCharge) + lp.ChargeTimer = ct + } + + // event handlers + _ = lp.bus.Subscribe(evStartCharge, lp.evChargeStartHandler) + _ = lp.bus.Subscribe(evStopCharge, lp.evChargeStopHandler) + + // read initial enabled state + var err error + if lp.enabled, err = lp.Charger.Enabled(); err != nil { + log.ERROR.Printf("%s charger error: %v", lp.Name, err) + } else { + log.INFO.Printf("%s charger %s", lp.Name, status[lp.enabled]) + } + + // set current to known value + if err = lp.setTargetCurrent(lp.MinCurrent); err != nil { + log.ERROR.Println(err) + } +} + // connected returns the EVs connection state func (lp *LoadPoint) connected() bool { return lp.status == api.StatusB || lp.status == api.StatusC @@ -92,8 +227,8 @@ func (lp *LoadPoint) chargerEnable(enable bool) error { log.FATAL.Fatal("charger enable/disable called without setting min current first") } - if time.Since(lp.guardUpdated) < lp.GuardDuration { - log.TRACE.Printf("%s min cycle time violated - ignoring charger %s", lp.Name, status[enable]) + if remaining := lp.GuardDuration - time.Since(lp.guardUpdated).Truncate(time.Second); remaining > 0 { + log.DEBUG.Printf("%s charger %s - contactor delay %v", lp.Name, status[enable], remaining) return nil } @@ -103,10 +238,15 @@ func (lp *LoadPoint) chargerEnable(enable bool) error { log.INFO.Printf("%s charger %s", lp.Name, status[enable]) lp.guardUpdated = time.Now() - if !enable { + if enable { + lp.bus.Publish(evChargeCurrent, lp.MinCurrent) + } else { lp.bus.Publish(evChargeCurrent, int64(0)) } + } else { + log.DEBUG.Printf("%s charger %s", lp.Name, status[enable]) } + return err } @@ -136,7 +276,7 @@ func (lp *LoadPoint) updateChargeStatus() api.ChargeStatus { log.ERROR.Printf("%s charger error: %v", lp.Name, err) return api.StatusNone } - log.TRACE.Printf("%s charger status: %s", lp.Name, status) + log.DEBUG.Printf("%s charger status: %s", lp.Name, status) if prevStatus := lp.status; status != prevStatus { lp.status = status @@ -270,7 +410,7 @@ func (lp *LoadPoint) updateModePV(mode api.ChargeMode) error { } } - log.TRACE.Printf("%s target charge current: %dA", lp.Name, targetChargeCurrent) + log.DEBUG.Printf("%s target charge current: %dA", lp.Name, targetChargeCurrent) if targetChargeCurrent == 0 { return lp.rampOff() @@ -288,17 +428,17 @@ func (lp *LoadPoint) updateMeter(name string, meter api.Meter, power *float64) { return } - log.TRACE.Printf("%s %s power: %.1fW", lp.Name, name, *power) - lp.uiChan <- Param{Key: name + "Power", Val: *power} + log.DEBUG.Printf("%s %s power: %.1fW", lp.Name, name, *power) + lp.publish(name+"Power", *power) } // update is the main control function. It reevaluates meters and charger state func (lp *LoadPoint) update() { lp.updateChargeStatus() - lp.uiChan <- Param{Key: "mode", Val: string(lp.GetMode())} - lp.uiChan <- Param{Key: "connected", Val: lp.connected()} - lp.uiChan <- Param{Key: "charging", Val: lp.charging} + lp.publish("mode", string(lp.GetMode())) + lp.publish("connected", lp.connected()) + lp.publish("charging", lp.charging) lp.updateMeter("grid", lp.GridMeter, &lp.gridPower) lp.updateMeter("pv", lp.PVMeter, &lp.pvPower) @@ -320,17 +460,15 @@ func (lp *LoadPoint) update() { case api.ModeNow: err = lp.rampOn(lp.MaxCurrent) case api.ModeMinPV, api.ModePV: - if lp.charging { - err = lp.updateModePV(mode) - } + err = lp.updateModePV(mode) } if err != nil { log.ERROR.Println(err) } - lp.uiChan <- Param{Key: "chargedEnergy", Val: 1e3 * lp.chargedEnergy()} - lp.uiChan <- Param{Key: "chargeDuration", Val: lp.chargeDuration()} + lp.publish("chargedEnergy", 1e3*lp.chargedEnergy()) // return Wh for U) + lp.publish("chargeDuration", lp.chargeDuration()) lp.publishSoC() } diff --git a/core/loadpoint_handler.go b/core/loadpoint_handler.go deleted file mode 100644 index ec141807a..000000000 --- a/core/loadpoint_handler.go +++ /dev/null @@ -1,108 +0,0 @@ -package core - -import ( - "github.com/andig/evcc/api" - "github.com/andig/evcc/core/wrapper" - "github.com/andig/evcc/push" -) - -const ( - evcc = "evcc" // notification sender - evChargeCurrent = "ChargeCurrent" // update fakeChargeMeter - evChargePower = "ChargePower" // update chargeRater - evStartCharge = "StartCharge" // update chargeTimer - evStopCharge = "StopCharge" // update chargeTimer -) - -// evChargeStartHandler sends external start event -func (lp *LoadPoint) evChargeStartHandler() { - lp.notificationChan <- push.Event{ - EventId: push.ChargeStart, - Sender: evcc, - Attributes: map[string]interface{}{ - "lp": lp.Name, - "mode": lp.GetMode(), - }, - } -} - -// evChargeStartHandler sends external stop event -func (lp *LoadPoint) evChargeStopHandler() { - energy, err := lp.ChargeRater.ChargedEnergy() - if err != nil { - log.ERROR.Printf("%s %v", lp.Name, err) - } - - lp.notificationChan <- push.Event{ - EventId: push.ChargeStop, - Sender: evcc, - Attributes: map[string]interface{}{ - "lp": lp.Name, - "energy": energy, - }, - } -} - -// evChargeCurrentHandler updates proxy charge meter's charge current. -// If physical charge meter is present this handler is not used. -func (lp *LoadPoint) evChargeCurrentHandler(m *wrapper.ChargeMeter) func(para ...interface{}) { - return func(para ...interface{}) { - current := para[0].(int64) - if current > 0 && lp.status != api.StatusC { - current = 0 - } - m.SetChargeCurrent(current) - } -} - -// Prepare loadpoint configuration by adding missing helper elements -func (lp *LoadPoint) Prepare(uiChan chan<- Param, notificationChan chan<- push.Event) { - lp.notificationChan = notificationChan - lp.uiChan = uiChan - - if lp.PVMeter == nil && lp.GridMeter == nil { - log.FATAL.Fatal("missing either PV or Grid meter - aborting") - } - - // ensure charge meter exists - if lp.ChargeMeter == nil { - if mt, ok := lp.Charger.(api.Meter); ok { - lp.ChargeMeter = mt - } else { - mt := &wrapper.ChargeMeter{ - Phases: lp.Phases, - Voltage: lp.Voltage, - } - _ = lp.bus.Subscribe(evChargeCurrent, lp.evChargeCurrentHandler(mt)) - _ = lp.bus.Subscribe(evStopCharge, func() { - mt.SetChargeCurrent(0) - }) - lp.ChargeMeter = mt - } - } - - // ensure charge rater exists - if rt, ok := lp.Charger.(api.ChargeRater); ok { - lp.ChargeRater = rt - } else { - rt := wrapper.NewChargeRater(lp.Name, lp.ChargeMeter) - _ = lp.bus.Subscribe(evChargePower, rt.SetChargePower) - _ = lp.bus.Subscribe(evStartCharge, rt.StartCharge) - _ = lp.bus.Subscribe(evStopCharge, rt.StopCharge) - lp.ChargeRater = rt - } - - // ensure charge timer exists - if ct, ok := lp.Charger.(api.ChargeTimer); ok { - lp.ChargeTimer = ct - } else { - ct := wrapper.NewChargeTimer() - _ = lp.bus.Subscribe(evStartCharge, ct.StartCharge) - _ = lp.bus.Subscribe(evStopCharge, ct.StopCharge) - lp.ChargeTimer = ct - } - - // event handlers - _ = lp.bus.Subscribe(evStartCharge, lp.evChargeStartHandler) - _ = lp.bus.Subscribe(evStopCharge, lp.evChargeStopHandler) -} diff --git a/evcc.dist.yaml b/evcc.dist.yaml index f05dfd1ed..5c6b01940 100644 --- a/evcc.dist.yaml +++ b/evcc.dist.yaml @@ -6,6 +6,27 @@ mqtt: # user: # password: +# additional menu entries +menu: +# - title: "Verlauf" +# subtitle: "Bezugs- und Einspeiseleistung" +# img: "http://..." +# link: "http://..." +# iframe: "http://..." + +# push messages +pushover: + app: app id + recipients: # list of recipient ids + - recipient id + events: # message templates + start: + title: Charge started + msg: ${loadpoint} started charging in "${mode}" mode + stop: + title: Charge finished + msg: ${loadpoint} finished charging. Charged ${energy:%.1f}kWh in ${duration}. + meters: - name: grid power: # power reading diff --git a/push/event.go b/push/event.go index a959103ec..019a6261c 100644 --- a/push/event.go +++ b/push/event.go @@ -11,15 +11,8 @@ import ( var log = api.NewLogger("push") -type EventId int - -const ( - ChargeStart EventId = iota - ChargeStop -) - type Event struct { - EventId EventId + Event string Sender string Attributes map[string]interface{} } diff --git a/push/push.go b/push/push.go index 90511040f..6ae165457 100644 --- a/push/push.go +++ b/push/push.go @@ -1,34 +1,38 @@ package push -var definitions = map[EventId]struct{ title, msg string }{ - ChargeStart: { - title: "Charge started", - msg: "Loadpoint ${lp} started charging in \"${mode}\" mode", - }, - ChargeStop: { - title: "Charge finished", - msg: "Loadpoint ${lp} finished charging. Charged ${energy:%.1f}kWh in ${duration}.", - }, +// EventTemplate is the push message template for an event +type EventTemplate struct { + title, msg string } // Hub subscribes to event notifications and sends them to client devices type Hub struct { - PushOver *PushOver + pushOver *PushOver + definitions map[string]EventTemplate +} + +// NewHub creates push hub with definitions and receiver +func NewHub(definitions map[string]EventTemplate, pushOver *PushOver) *Hub { + h := &Hub{ + pushOver: pushOver, + definitions: definitions, + } + return h } // Run is the Hub's main publishing loop func (h *Hub) Run(events <-chan Event) { for ev := range events { - if h.PushOver == nil { + if h.pushOver == nil { continue } - definition, ok := definitions[ev.EventId] + definition, ok := h.definitions[ev.Event] if !ok { - log.ERROR.Printf("invalid event %v", ev.EventId) + log.ERROR.Printf("invalid event %v", ev.Event) break } - go h.PushOver.Send(ev, definition.title, definition.msg) + go h.pushOver.Send(ev, definition.title, definition.msg) } }