Add Telegram messenger (#16)

This commit is contained in:
andig 2020-03-29 21:28:33 +02:00
parent ef6140cce5
commit 037cb43380
15 changed files with 203 additions and 65 deletions

View file

@ -14,6 +14,10 @@ EVCC is an extensible EV Charge Controller with PV integration implemented in [G
- Go-E (experimental)
- SimpleEVSE (experimental)
- any other charger using scripting
- support for different vehicles to show battery state:
- Audi
- Tesla
- any other vehicle using scripting
- notifications using [Telegram](https://telegram.org) and [PushOver](https://pushover.net)
- integration with home automation - supports shell scripts and MQTT
- logging using [InfluxDB](https://www.influxdata.com)

View file

@ -4,7 +4,7 @@ import (
"github.com/mitchellh/mapstructure"
)
// DecodeOther decodes string map into target configuration
// DecodeOther uses mapstructure to decode into target structure. Unused keys cause errors.
func DecodeOther(log *Logger, other map[string]interface{}, cc interface{}) {
decoderConfig := &mapstructure.DecoderConfig{
Result: cc,

View file

@ -16,7 +16,7 @@ type config struct {
Mqtt mqttConfig
Influx influxConfig
Menu []server.MenuConfig
Pushover messagingConfig
Messaging messagingConfig
Meters []meterConfig
Chargers []typedConfig
Vehicles []typedConfig
@ -29,9 +29,13 @@ type typedConfig struct {
}
type messagingConfig struct {
App string
Recipients []string
Events map[string]push.EventTemplate
Events map[string]push.EventTemplate
Services []messagingService
}
type messagingService struct {
Type string
Other map[string]interface{} `mapstructure:",remain"`
}
type mqttConfig struct {

View file

@ -7,7 +7,6 @@ import (
"github.com/andig/evcc/api"
"github.com/andig/evcc/core"
"github.com/andig/evcc/push"
"github.com/andig/evcc/server"
"github.com/spf13/cobra"
@ -178,14 +177,7 @@ func run(cmd *cobra.Command, args []string) {
log.INFO.Println("listening at", uri)
// setup messaging
var pushOver *push.PushOver
if conf.Pushover.App != "" {
pushOver = push.NewMessenger(conf.Pushover.App, conf.Pushover.Recipients)
}
notificationChan := make(chan push.Event, 1)
notificationHub := push.NewHub(conf.Pushover.Events, pushOver)
go notificationHub.Run(notificationChan)
notificationChan := configureMessengers(conf.Messaging)
// setup loadpoints
loadPoints := loadConfig(conf, notificationChan)

View file

@ -19,6 +19,20 @@ func init() {
rand.Seed(time.Now().UnixNano())
}
func configureMessengers(conf messagingConfig) chan push.Event {
notificationChan := make(chan push.Event, 1)
notificationHub := push.NewHub(conf.Events)
for _, service := range conf.Services {
impl := push.NewMessengerFromConfig(service.Type, service.Other)
notificationHub.Add(impl)
}
go notificationHub.Run(notificationChan)
return notificationChan
}
func clientID() string {
pid := rand.Int31()
return fmt.Sprintf("evcc-%d", pid)

View file

@ -19,7 +19,6 @@ var (
)
const (
evcc = "evcc" // notification sender
evStartCharge = "start" // update chargeTimer
evStopCharge = "stop" // update chargeTimer
evChargeCurrent = "current" // update fakeChargeMeter
@ -97,7 +96,6 @@ func (lp *LoadPoint) notify(event string, attributes map[string]interface{}) {
attributes["loadpoint"] = lp.Name
lp.notificationChan <- push.Event{
Event: event,
Sender: evcc,
Attributes: attributes,
}
}
@ -461,7 +459,7 @@ func (lp *LoadPoint) update() {
log.ERROR.Println(err)
}
lp.publish("chargedEnergy", 1e3*lp.chargedEnergy()) // return Wh for U)
lp.publish("chargedEnergy", 1e3*lp.chargedEnergy()) // return Wh for UI
lp.publish("chargeDuration", lp.chargeDuration())
lp.publishSoC()

View file

@ -23,18 +23,25 @@ menu:
# iframe: "http://..."
# push messages
pushover:
app: app id
recipients: # list of recipient ids
- recipient id
events: # message templates
start:
messaging:
events:
start: # charge start event
title: Charge started
msg: ${loadpoint} started charging in "${mode}" mode
stop:
stop: # charge stop event
title: Charge finished
msg: ${loadpoint} finished charging. Charged ${energy:%.1f}kWh in ${duration}.
services:
- type: pushover
app: # app id
recipients:
- # list of recipient ids
- type: telegram
token: # bot id
chats:
- # list of chat ids
# meters
meters:
- name: grid
power: # power reading

2
go.mod
View file

@ -7,6 +7,7 @@ require (
github.com/benbjohnson/clock v1.0.0
github.com/eclipse/paho.mqtt.golang v1.2.0
github.com/fsnotify/fsnotify v1.4.9 // indirect
github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible
github.com/golang/mock v1.4.3
github.com/google/go-github v17.0.0+incompatible // indirect
github.com/google/go-querystring v1.0.0 // indirect
@ -31,6 +32,7 @@ require (
github.com/spf13/pflag v1.0.5 // indirect
github.com/spf13/viper v1.6.2
github.com/tcnksm/go-latest v0.0.0-20170313132115-e3007ae9052e
github.com/technoweenie/multipartstreamer v1.0.1 // indirect
golang.org/x/net v0.0.0-20200301022130-244492dfa37a // indirect
golang.org/x/sys v0.0.0-20200317113312-5766fd39f98d // indirect
golang.org/x/tools v0.0.0-20200318150045-ba25ddc85566

5
go.sum
View file

@ -58,6 +58,9 @@ github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-telegram-bot-api/telegram-bot-api v1.0.0 h1:HXVtsZ+yINQeyyhPFAUU4yKmeN+iFhJ87jXZOC016gs=
github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible h1:2cauKuaELYAEARXRkq2LrJ0yDDv1rW7+wrTEdVL3uaU=
github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible/go.mod h1:qf9acutJ8cwBUhm1bqgz6Bei9/C/c93FPDljKWwsOgM=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
@ -237,6 +240,8 @@ github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/tcnksm/go-latest v0.0.0-20170313132115-e3007ae9052e h1:IWllFTiDjjLIf2oeKxpIUmtiDV5sn71VgeQgg6vcE7k=
github.com/tcnksm/go-latest v0.0.0-20170313132115-e3007ae9052e/go.mod h1:d7u6HkTYKSv5m6MCKkOQlHwaShTMl3HjqSGW3XtVhXM=
github.com/technoweenie/multipartstreamer v1.0.1 h1:XRztA5MXiR1TIRHxH2uNxXxaIkKQDeX7m2XsSOlQEnM=
github.com/technoweenie/multipartstreamer v1.0.1/go.mod h1:jNVxdtShOxzAsukZwTSw6MDx5eUJoiEBsSvzDU9uzog=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=

36
push/config.go Normal file
View file

@ -0,0 +1,36 @@
package push
import (
"strings"
"github.com/andig/evcc/api"
)
// Sender implements message sending
type Sender interface {
Send(event Event, title, msg string)
}
// EventTemplate is the push message template for an event
type EventTemplate struct {
Title, Msg string
}
var log = api.NewLogger("push")
// NewMessengerFromConfig creates a new messenger
func NewMessengerFromConfig(typ string, other map[string]interface{}) Sender {
switch strings.ToLower(typ) {
case "pushover":
var cc pushOverConfig
api.DecodeOther(log, other, &cc)
return NewPushOverMessenger(cc.App, cc.Recipients)
case "telegram":
var cc telegramConfig
api.DecodeOther(log, other, &cc)
return NewTelegramMessenger(cc.Token, cc.Chats)
}
log.FATAL.Fatalf("unknown messenger type: %s", typ)
return nil
}

View file

@ -5,19 +5,16 @@ import (
"fmt"
"regexp"
"strings"
"github.com/andig/evcc/api"
)
var log = api.NewLogger("push")
// Event is a notification event
type Event struct {
Event string
Sender string
Attributes map[string]interface{}
}
func (e Event) Apply(template string) (string, error) {
// apply applies the event template to the content to produce the actual message
func (e Event) apply(template string) (string, error) {
return replaceFormatted(template, e.Attributes)
}

View file

@ -1,29 +1,28 @@
package push
// 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
definitions map[string]EventTemplate
sender []Sender
}
// NewHub creates push hub with definitions and receiver
func NewHub(definitions map[string]EventTemplate, pushOver *PushOver) *Hub {
func NewHub(definitions map[string]EventTemplate) *Hub {
h := &Hub{
pushOver: pushOver,
definitions: definitions,
}
return h
}
// Add adds a sender to the list of senders
func (h *Hub) Add(sender Sender) {
h.sender = append(h.sender, sender)
}
// Run is the Hub's main publishing loop
func (h *Hub) Run(events <-chan Event) {
for ev := range events {
if h.pushOver == nil {
if len(h.sender) == 0 {
continue
}
@ -33,6 +32,13 @@ func (h *Hub) Run(events <-chan Event) {
break
}
go h.pushOver.Send(ev, definition.Title, definition.Msg)
msg, err := ev.apply(definition.Msg)
if err != nil {
log.ERROR.Printf("invalid message template: %v", err)
}
for _, sender := range h.sender {
go sender.Send(ev, definition.Title, msg)
}
}
}

View file

@ -7,42 +7,41 @@ import (
// PushOver implements the pushover messenger
type PushOver struct {
app *pushover.Pushover
recipients []*pushover.Recipient
recipients []string
}
// NewMessenger creates new pushover messenger
func NewMessenger(app string, recipients []string) *PushOver {
type pushOverConfig struct {
App string
Recipients []string
Events map[string]EventTemplate
}
// NewPushOverMessenger creates new pushover messenger
func NewPushOverMessenger(app string, recipients []string) *PushOver {
if app == "" {
app = "evcc"
log.FATAL.Fatal("pushover: missing app name")
}
po := &PushOver{
app: pushover.New(app),
m := &PushOver{
app: pushover.New(app),
recipients: recipients,
}
for _, r := range recipients {
po.recipients = append(po.recipients, pushover.NewRecipient(r))
}
return po
return m
}
// Send sends to all receivers
func (po *PushOver) Send(event Event, title, msg string) {
msg, err := event.Apply(msg)
if err != nil {
log.ERROR.Printf("invalid message template: %v", err)
}
func (m *PushOver) Send(event Event, title, msg string) {
message := pushover.NewMessageWithTitle(msg, title)
message.DeviceName = event.Sender
for _, recipient := range po.recipients {
go func(recipient *pushover.Recipient) {
_, err := po.app.SendMessage(message, recipient)
if err != nil {
for _, id := range m.recipients {
go func(id string) {
log.TRACE.Printf("pushover: sending to %s", id)
recipient := pushover.NewRecipient(id)
if _, err := m.app.SendMessage(message, recipient); err != nil {
log.ERROR.Print(err)
}
}(recipient)
}(id)
}
}

76
push/telegram.go Normal file
View file

@ -0,0 +1,76 @@
package push
import (
"sync"
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api"
)
// Telegram implements the Telegram messenger
type Telegram struct {
sync.Mutex
bot *tgbotapi.BotAPI
chats map[int64]struct{}
}
type telegramConfig struct {
Token string
Chats []int64
}
// NewTelegramMessenger creates new pushover messenger
func NewTelegramMessenger(token string, chats []int64) *Telegram {
bot, err := tgbotapi.NewBotAPI(token)
if err != nil {
log.FATAL.Fatal("telegram: missing token")
}
m := &Telegram{
bot: bot,
chats: make(map[int64]struct{}),
}
for _, chat := range chats {
m.chats[chat] = struct{}{}
}
go m.trackChats()
return m
}
// trackChats captures ids of all chats that bot participates in
func (m *Telegram) trackChats() {
conf := tgbotapi.NewUpdate(0)
conf.Timeout = 1000
updates, err := m.bot.GetUpdatesChan(conf)
if err != nil {
log.ERROR.Printf("telegram: %v", err)
}
for update := range updates {
m.Lock()
if _, ok := m.chats[update.Message.Chat.ID]; !ok {
log.INFO.Printf("telegram: new chat id: %d", update.Message.Chat.ID)
// m.chats[update.Message.Chat.ID] = struct{}{}
}
m.Unlock()
}
}
// Send sends to all receivers
func (m *Telegram) Send(event Event, title, msg string) {
m.Lock()
for chat := range m.chats {
go func(chat int64) {
log.TRACE.Printf("telegram: sending to %d", chat)
msg := tgbotapi.NewMessage(chat, msg)
if _, err := m.bot.Send(msg); err != nil {
log.ERROR.Print(err)
}
}(chat)
}
m.Unlock()
}

View file

@ -28,8 +28,6 @@ type Audi struct {
token string
tokenValid time.Time
cache time.Duration
chargeStateVal float64
chargeStateUpdated time.Time
chargeStateG *provider.CacheGetter
}