Add Telegram messenger (#16)
This commit is contained in:
parent
ef6140cce5
commit
037cb43380
15 changed files with 203 additions and 65 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
10
cmd/root.go
10
cmd/root.go
|
|
@ -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)
|
||||
|
|
|
|||
14
cmd/setup.go
14
cmd/setup.go
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
2
go.mod
|
|
@ -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
5
go.sum
|
|
@ -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
36
push/config.go
Normal 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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
76
push/telegram.go
Normal 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()
|
||||
}
|
||||
|
|
@ -28,8 +28,6 @@ type Audi struct {
|
|||
token string
|
||||
tokenValid time.Time
|
||||
cache time.Duration
|
||||
chargeStateVal float64
|
||||
chargeStateUpdated time.Time
|
||||
chargeStateG *provider.CacheGetter
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue