first commit
This commit is contained in:
207
workers/mqtt.go
Normal file
207
workers/mqtt.go
Normal file
@ -0,0 +1,207 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// SPDX-FileCopyrightText: 2023 mysubarumq
|
||||
// SPDX-FileContributor: alex-savin
|
||||
|
||||
package workers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"git.savin.nyc/alex/mysubaru-mq/bus"
|
||||
"git.savin.nyc/alex/mysubaru-mq/config"
|
||||
mqtt "github.com/eclipse/paho.mqtt.golang"
|
||||
)
|
||||
|
||||
const (
|
||||
ServiceAvailable = `{"value":"online"}`
|
||||
ServiceUnavailable = `{"value":"offline"}`
|
||||
)
|
||||
|
||||
// UPSClient is a client that connects to the APCUPSd server by establishing tcp connection
|
||||
type MQTTClient struct {
|
||||
sync.RWMutex
|
||||
id string // the internal id of the listener
|
||||
bus *bus.Bus // the internal bus for the interapp communication
|
||||
config *config.Config // configuration values for the listener
|
||||
mqtt mqtt.Client //
|
||||
cancel context.CancelFunc //
|
||||
end uint32 // ensure the close methods are only called once
|
||||
log *slog.Logger // server logger
|
||||
mqttHandlers mqttHandlers //
|
||||
}
|
||||
|
||||
type mqttHandlers struct {
|
||||
publish mqtt.MessageHandler //
|
||||
connected mqtt.OnConnectHandler //
|
||||
disconnected mqtt.ConnectionLostHandler //
|
||||
}
|
||||
|
||||
// NewUPSClient initialises and returns a UPS client
|
||||
func NewMQTTClient(id string, bus *bus.Bus, config *config.Config) *MQTTClient {
|
||||
if config == nil {
|
||||
slog.Error("")
|
||||
}
|
||||
|
||||
return &MQTTClient{
|
||||
id: id,
|
||||
bus: bus,
|
||||
config: config,
|
||||
}
|
||||
}
|
||||
|
||||
// ID returns the id of the listener.
|
||||
func (w *MQTTClient) ID() string {
|
||||
return w.id
|
||||
}
|
||||
|
||||
// ID returns the id of the listener.
|
||||
func (w *MQTTClient) Type() string {
|
||||
return "mqtt-client"
|
||||
}
|
||||
|
||||
// Init .
|
||||
func (w *MQTTClient) Init(log *slog.Logger) error {
|
||||
w.log = log
|
||||
|
||||
opts := mqtt.NewClientOptions()
|
||||
opts.AddBroker(fmt.Sprintf("tcp://%s:%d", w.config.MQTT.Host, w.config.MQTT.Port))
|
||||
opts.SetUsername(w.config.MQTT.Username)
|
||||
opts.SetPassword(w.config.MQTT.Password)
|
||||
opts.SetClientID(w.config.MQTT.ClientId)
|
||||
opts.SetWill(w.config.MQTT.Topic+"/status", ServiceUnavailable, 0, true)
|
||||
opts.OnConnect = func(m mqtt.Client) {
|
||||
w.log.Debug("mqtt client is connected to a mqtt server")
|
||||
m.Publish(w.config.MQTT.Topic+"/status", 0, w.config.MQTT.Retained, ServiceAvailable)
|
||||
}
|
||||
|
||||
w.mqtt = mqtt.NewClient(opts)
|
||||
token := w.mqtt.Connect()
|
||||
for !token.WaitTimeout(1 * time.Second) {
|
||||
}
|
||||
if err := token.Error(); err != nil {
|
||||
w.log.Error("couldn't connect to a mqtt server", "error", err.Error())
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
w.mqttHandlers.publish = func(client mqtt.Client, msg mqtt.Message) {
|
||||
var messages []*bus.Message
|
||||
|
||||
w.log.Debug("received mqtt message", "topic", msg.Topic(), "payload", msg.Payload())
|
||||
switch msg.Topic() {
|
||||
case w.config.Hassio.Topics.Status:
|
||||
// Subscribing for the topic to resend auto discovery topics after Home Assistant restarted and got a status "online"
|
||||
messages = append(messages, &bus.Message{
|
||||
Topic: msg.Topic(),
|
||||
Payload: string(msg.Payload()),
|
||||
})
|
||||
w.bus.Publish("hassio:status", messages)
|
||||
case "mysubarumq/4S4BTGPD0P3199198/lock/set":
|
||||
messages = append(messages, &bus.Message{
|
||||
Topic: msg.Topic(),
|
||||
Payload: string(msg.Payload()),
|
||||
})
|
||||
w.bus.Publish("mysubarumq:4S4BTGPD0P3199198:lock", messages)
|
||||
case "mysubarumq/4S4BTGPD0P3199198/ignition/set":
|
||||
messages = append(messages, &bus.Message{
|
||||
Topic: msg.Topic(),
|
||||
Payload: string(msg.Payload()),
|
||||
})
|
||||
w.bus.Publish("mysubarumq:4S4BTGPD0P3199198:ignition", messages)
|
||||
}
|
||||
}
|
||||
|
||||
w.mqttHandlers.connected = func(client mqtt.Client) {
|
||||
w.log.Debug("mqtt client is connected")
|
||||
}
|
||||
|
||||
w.mqttHandlers.disconnected = func(client mqtt.Client, err error) {
|
||||
w.log.Debug("mqtt lost connection", "error", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// OneTime .
|
||||
func (w *MQTTClient) OneTime() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Serve starts waiting for new TCP connections, and calls the establish
|
||||
// connection callback for any received.
|
||||
func (w *MQTTClient) Serve() {
|
||||
if atomic.LoadUint32(&w.end) == 1 {
|
||||
return
|
||||
}
|
||||
|
||||
chMQTTSubscribeCommand, err := w.bus.Subscribe("mqtt:subscribe", "command", w.config.SubscriptionSize["mqtt:subscribe"])
|
||||
if err != nil {
|
||||
w.log.Error("couldn't subscribe to a channel", "channel", "mqtt:subscribe", "error", err.Error())
|
||||
}
|
||||
|
||||
chMQTTPublishStatus, err := w.bus.Subscribe("mqtt:publish", "status", w.config.SubscriptionSize["mqtt:publish"])
|
||||
if err != nil {
|
||||
w.log.Error("couldn't subscribe to a channel", "channel", "mqtt:subscribe", "error", err.Error())
|
||||
}
|
||||
|
||||
// ctx is used only by tests.
|
||||
// ctx, ctxCancel := context.WithCancel(context.Background())
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
w.cancel = cancel
|
||||
|
||||
go w.eventLoop(ctx, chMQTTPublishStatus, chMQTTSubscribeCommand)
|
||||
|
||||
if atomic.LoadUint32(&w.end) == 0 {
|
||||
go func() {
|
||||
if !w.mqtt.IsConnected() {
|
||||
w.bus.Publish("app:connected", &bus.ConnectionStatus{WorkerID: w.ID(), WorkerType: w.Type(), IsConnected: false})
|
||||
w.log.Warn("mqtt client is disconnected")
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
// Close closes the listener and any client connections.
|
||||
func (w *MQTTClient) Close() {
|
||||
w.Lock()
|
||||
defer w.Unlock()
|
||||
|
||||
if atomic.CompareAndSwapUint32(&w.end, 0, 1) {
|
||||
w.cancel()
|
||||
w.bus.Unsubscribe("mqtt:publish", "status")
|
||||
w.mqtt.Publish(w.config.MQTT.Topic+"/status", 0, true, ServiceUnavailable)
|
||||
w.mqtt.Disconnect(250)
|
||||
w.log.Info("disconnected from mqtt server")
|
||||
}
|
||||
}
|
||||
|
||||
// eventLoop loops forever
|
||||
func (w *MQTTClient) eventLoop(ctx context.Context, chMQTTPublishStatus, chMQTTSubscribeCommand chan bus.Event) {
|
||||
w.log.Debug("mqtt communication event loop started")
|
||||
defer w.log.Debug("mqtt communication event loop halted")
|
||||
|
||||
for {
|
||||
select {
|
||||
case event := <-chMQTTPublishStatus:
|
||||
for _, message := range event.Data.([]*bus.Message) {
|
||||
w.log.Debug("publishing mqtt message", "topic", message.Topic, "qos", message.QOS, "retained", message.Retained, "payload", message.Payload)
|
||||
w.mqtt.Publish(message.Topic, message.QOS, message.Retained, message.Payload)
|
||||
}
|
||||
case event := <-chMQTTSubscribeCommand:
|
||||
for _, message := range event.Data.([]*bus.Message) {
|
||||
w.log.Debug("subscribing to a topic", "topic", message.Topic, "qos", message.QOS)
|
||||
// w.mqtt.SubscribeMultiple()
|
||||
w.mqtt.Subscribe(message.Topic, message.QOS, w.mqttHandlers.publish)
|
||||
}
|
||||
case <-ctx.Done():
|
||||
w.log.Info("stopping mqtt communication event loop")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
389
workers/mysubaru.go
Normal file
389
workers/mysubaru.go
Normal file
@ -0,0 +1,389 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// SPDX-FileCopyrightText: 2023 mysubarumq
|
||||
// SPDX-FileContributor: alex-savin
|
||||
|
||||
package workers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"git.savin.nyc/alex/mysubaru"
|
||||
"git.savin.nyc/alex/mysubaru-mq/bus"
|
||||
"git.savin.nyc/alex/mysubaru-mq/config"
|
||||
)
|
||||
|
||||
// MySubaruClient is a client that connects to the MySubaru server by establishing tcp connection
|
||||
type MySubaruClient struct {
|
||||
sync.RWMutex
|
||||
id string // the internal id of the listener
|
||||
bus *bus.Bus // the internal bus for the interapp communication
|
||||
config *config.Config // configuration values for the listener
|
||||
mysubaru *mysubaru.Client //
|
||||
cancel context.CancelFunc //
|
||||
end uint32 // ensure the close methods are only called once
|
||||
log *slog.Logger // server logger
|
||||
}
|
||||
|
||||
// NewMySubaruClient initialises and returns a MySubaru client
|
||||
func NewMySubaruClient(id string, bus *bus.Bus, config *config.Config) *MySubaruClient {
|
||||
if config == nil {
|
||||
slog.Error("")
|
||||
}
|
||||
|
||||
s := &MySubaruClient{
|
||||
id: id,
|
||||
bus: bus,
|
||||
config: config,
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
// ID returns the id of the listener.
|
||||
func (s *MySubaruClient) ID() string {
|
||||
return s.id
|
||||
}
|
||||
|
||||
// ID returns the id of the listener.
|
||||
func (s *MySubaruClient) Type() string {
|
||||
return "mysubaru-client"
|
||||
}
|
||||
|
||||
// Init .
|
||||
func (s *MySubaruClient) Init(log *slog.Logger) error {
|
||||
s.log = log
|
||||
|
||||
mys, err := mysubaru.New(log, &s.config.MySubaru)
|
||||
if err != nil {
|
||||
s.log.Error("couldn't connect to MySubaru server", "error", err.Error())
|
||||
}
|
||||
|
||||
s.mysubaru = mys
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// OneTime .
|
||||
func (s *MySubaruClient) OneTime() error {
|
||||
if s.config.Hassio.AutoDiscovery {
|
||||
time.Sleep(3 * time.Second)
|
||||
vehicles := s.mysubaru.GetVehicles()
|
||||
|
||||
for _, vehicle := range vehicles {
|
||||
err := s.bus.Publish("mqtt:publish", s.mySubaruConfigToMQTTHassioConfig(vehicle))
|
||||
if err != nil {
|
||||
s.log.Error("got an error from bus", "error", err.Error())
|
||||
|
||||
return err
|
||||
}
|
||||
err = s.bus.Publish("mqtt:publish", s.mySubaruStatusToMQTTMessage(vehicle))
|
||||
if err != nil {
|
||||
s.log.Error("got an error from bus", "error", err.Error())
|
||||
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Serve starts waiting for new TCP connections, and calls the establish
|
||||
// connection callback for any received.
|
||||
func (s *MySubaruClient) Serve() {
|
||||
if atomic.LoadUint32(&s.end) == 1 {
|
||||
return
|
||||
}
|
||||
|
||||
var subs []*bus.Message
|
||||
// Subscribing for the topic to resend auto discovery topics after Home Assistant restarted and got a status "online"
|
||||
if s.config.Hassio.AutoDiscovery {
|
||||
subs = append(subs, &bus.Message{
|
||||
Topic: s.config.Hassio.Topics.Status,
|
||||
QOS: 0,
|
||||
})
|
||||
}
|
||||
// TODO: Go over MySubaru devices with switch and lock options
|
||||
subs = append(subs, &bus.Message{
|
||||
Topic: "mysubarumq/4S4BTGPD0P3199198/lock/set",
|
||||
QOS: 0,
|
||||
})
|
||||
subs = append(subs, &bus.Message{
|
||||
Topic: "mysubarumq/4S4BTGPD0P3199198/ignition/set",
|
||||
QOS: 0,
|
||||
})
|
||||
s.bus.Publish("mqtt:subscribe", subs)
|
||||
|
||||
tickerS := time.NewTicker(time.Duration(60) * time.Second)
|
||||
tickerM := time.NewTicker(time.Duration(60) * time.Minute)
|
||||
|
||||
chMQTTLockStatus, err := s.bus.Subscribe("mysubarumq:4S4BTGPD0P3199198:lock", "mysubarumq:4S4BTGPD0P3199198:lock", s.config.SubscriptionSize["device:95452:status"])
|
||||
if err != nil {
|
||||
s.log.Error("couldn't subscribe to a channel", "channel", "mysubarumq:4S4BTGPD0P3199198:lock", "error", err.Error())
|
||||
}
|
||||
|
||||
chMQTTIgnitionStatus, err := s.bus.Subscribe("mysubarumq:4S4BTGPD0P3199198:ignition", "mysubarumq:4S4BTGPD0P3199198:ignition", s.config.SubscriptionSize["device:95452:status"])
|
||||
if err != nil {
|
||||
s.log.Error("couldn't subscribe to a channel", "channel", "mysubarumq:4S4BTGPD0P3199198:ignition", "error", err.Error())
|
||||
}
|
||||
|
||||
chMQTTHassioStatus, err := s.bus.Subscribe("hassio:status", s.ID(), s.config.SubscriptionSize["hassio:status"])
|
||||
if err != nil {
|
||||
s.log.Error("couldn't subscribe to a channel", "channel", "hassio:status", "error", err.Error())
|
||||
}
|
||||
|
||||
// ctx is used only by tests.
|
||||
// ctx, ctxCancel := context.WithCancel(context.Background())
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
s.cancel = cancel
|
||||
|
||||
go s.eventLoop(ctx, chMQTTLockStatus, chMQTTIgnitionStatus, chMQTTHassioStatus)
|
||||
|
||||
if atomic.LoadUint32(&s.end) == 0 {
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-tickerS.C:
|
||||
vehicles := s.mysubaru.GetVehicles()
|
||||
for _, vehicle := range vehicles {
|
||||
err := s.bus.Publish("mqtt:publish", s.mySubaruStatusToMQTTMessage(vehicle))
|
||||
if err != nil {
|
||||
s.log.Error("got an error from bus", "error", err.Error())
|
||||
}
|
||||
}
|
||||
case <-tickerM.C:
|
||||
vehicles := s.mysubaru.GetVehicles()
|
||||
for _, vehicle := range vehicles {
|
||||
vehicle.GetLocation(true)
|
||||
err := s.bus.Publish("mqtt:publish", s.mySubaruStatusToMQTTMessage(vehicle))
|
||||
if err != nil {
|
||||
s.log.Error("got an error from bus", "error", err.Error())
|
||||
}
|
||||
}
|
||||
case <-ctx.Done():
|
||||
s.log.Info("stopping communication eventloop", "type", s.Type())
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
// Close closes the listener and any client connections.
|
||||
func (s *MySubaruClient) Close() {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
|
||||
if atomic.CompareAndSwapUint32(&s.end, 0, 1) {
|
||||
s.cancel()
|
||||
s.log.Info("disconnected from mysubaru server", "type", s.Type())
|
||||
}
|
||||
}
|
||||
|
||||
// eventLoop loops forever
|
||||
func (s *MySubaruClient) eventLoop(ctx context.Context, chMQTTLockStatus, chMQTTIgnitionStatus, chMQTTHassioStatus chan bus.Event) {
|
||||
s.log.Debug("mysubaru communication event loop started")
|
||||
defer s.log.Debug("mysubaru communication event loop halted")
|
||||
|
||||
for {
|
||||
select {
|
||||
case event := <-chMQTTLockStatus:
|
||||
for _, message := range event.Data.([]*bus.Message) {
|
||||
s.log.Debug("received a message with mysubary lock status", "topic", message.Topic, "payload", message.Payload)
|
||||
if message.Payload == "LOCK" {
|
||||
v := s.mysubaru.GetVehicleByVIN("4S4BTGPD0P3199198")
|
||||
v.Lock()
|
||||
var msgs []*bus.Message
|
||||
msgs = s.messages("mysubarumq/4S4BTGPD0P3199198/lock", 0, true, "LOCK", msgs)
|
||||
s.bus.Publish("mqtt:publish", msgs)
|
||||
}
|
||||
if message.Payload == "UNLOCK" {
|
||||
v := s.mysubaru.GetVehicleByVIN("4S4BTGPD0P3199198")
|
||||
v.Unlock()
|
||||
var msgs []*bus.Message
|
||||
msgs = s.messages("mysubarumq/4S4BTGPD0P3199198/lock", 0, true, "UNLOCK", msgs)
|
||||
s.bus.Publish("mqtt:publish", msgs)
|
||||
}
|
||||
}
|
||||
case event := <-chMQTTIgnitionStatus:
|
||||
for _, message := range event.Data.([]*bus.Message) {
|
||||
s.log.Debug("received a message with mysubary ignition status", "topic", message.Topic, "payload", message.Payload)
|
||||
if message.Payload == "ON" {
|
||||
v := s.mysubaru.GetVehicleByVIN("4S4BTGPD0P3199198")
|
||||
v.EngineStart()
|
||||
var msgs []*bus.Message
|
||||
msgs = s.messages("mysubarumq/4S4BTGPD0P3199198/ignition", 0, true, "ON", msgs)
|
||||
s.bus.Publish("mqtt:publish", msgs)
|
||||
}
|
||||
if message.Payload == "OFF" {
|
||||
v := s.mysubaru.GetVehicleByVIN("4S4BTGPD0P3199198")
|
||||
v.EngineStop()
|
||||
var msgs []*bus.Message
|
||||
msgs = s.messages("mysubarumq/4S4BTGPD0P3199198/ignition", 0, true, "OFF", msgs)
|
||||
s.bus.Publish("mqtt:publish", msgs)
|
||||
}
|
||||
}
|
||||
case event := <-chMQTTHassioStatus:
|
||||
for _, message := range event.Data.([]*bus.Message) {
|
||||
s.log.Info("received a message with hassio instance status", "topic", message.Topic, "payload", message.Payload)
|
||||
if message.Payload == "online" {
|
||||
s.OneTime()
|
||||
}
|
||||
}
|
||||
case <-ctx.Done():
|
||||
s.log.Info("stopping mqtt communication event loop")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// mySubaruConfigToMQTTHassioConfig .
|
||||
func (s *MySubaruClient) mySubaruConfigToMQTTHassioConfig(v *mysubaru.Vehicle) []*bus.Message {
|
||||
// {
|
||||
// "~": "homeassistant/sensor/VIN_SENSOR_NAME",
|
||||
// "name": null,
|
||||
// "uniq_id": "4S4BTGPD0P3199198_SENSOR_NAME",
|
||||
// "obj_id": "",
|
||||
// "ic": "",
|
||||
// "stat_t": "~/state",
|
||||
// "json_attr_t": "~/state",
|
||||
// "val_tpl": "{{value_json.value}}",
|
||||
// "dev_cla": "",
|
||||
// "stat_cla": "",
|
||||
// "unit_of_meas": "",
|
||||
// "en": true,
|
||||
// "ent_cat": "",
|
||||
// "ent_pic": "",
|
||||
// "dev": {
|
||||
// "ids": [
|
||||
// "4S4BTGPD0P3199198"
|
||||
// ],
|
||||
// "name": "Subaru Outback Touring TX (2023)",
|
||||
// "mf": "Subaru",
|
||||
// "mdl": "Outback Touring TX",
|
||||
// "sw": "1.0",
|
||||
// "hw": "PDL"
|
||||
// },
|
||||
// "o": {
|
||||
// "name": "MySubaruMQ",
|
||||
// "sw": "1.0.0",
|
||||
// "url": "https://www.github.com/alex-savin/"
|
||||
// },
|
||||
// "avty": [
|
||||
// {
|
||||
// "topic": ""
|
||||
// }
|
||||
// ]
|
||||
// }
|
||||
var hassioConfig = map[string]string{}
|
||||
|
||||
// availability := `{"avty":["{"topic":"mysubaru/` + v.Vin + `"}"]}`
|
||||
origin := `"o":{"name":"MySubaruMQ","sw":"1.0.1","url":"https://www.git.savin.nyc/alex/mysubaru-mq"},`
|
||||
// availability := `"avty":[{"t":"musubarymq/status"}],"avty_t":"{{value_json.value}}",`
|
||||
device := `"dev":{"ids":["` + v.Vin + `"],"name":"` + v.CarNickname + `","mf":"Subaru Corp.","mdl":"` + v.CarNickname + `","hw":"` + v.ModelCode + `"},` // TODO chnage model to the proper one
|
||||
obj_id_prefix := strings.Replace(strings.ToLower(v.CarNickname), " ", "_", -1)
|
||||
// topic := `"~":"mysubaru/` + v.Vin + `",`
|
||||
topic := ""
|
||||
// homeassistant/sensor/mysubaru/VIN-NUMBER-HERE/odometer_km/config
|
||||
hassioConfig[s.config.Hassio.Topics.Discovery+`/sensor/`+v.Vin+`/odometer_km/config`] = `{` + device + origin + topic + `"name":"Odometer (km)","uniq_id":"` + v.Vin + `_odometer_km","obj_id":"` + obj_id_prefix + `_odometer_km","ic":"mdi:counter","stat_t":"mysubarumq/` + v.Vin + `/state","val_tpl":"{{value_json.odometer_km}}","unit_of_meas":"km"}`
|
||||
// homeassistant/sensor/mysubaru/VIN-NUMBER-HERE/odometer_mi/config
|
||||
hassioConfig[s.config.Hassio.Topics.Discovery+`/sensor/`+v.Vin+`/odometer_mi/config`] = `{` + device + origin + topic + `"name":"Odometer (mi)","uniq_id":"` + v.Vin + `_odometer_mi","obj_id":"` + obj_id_prefix + `_odometer_mi","ic":"mdi:counter","stat_t":"mysubarumq/` + v.Vin + `/state","val_tpl":"{{value_json.odometer_mi}}","unit_of_meas":"mi"}`
|
||||
// homeassistant/sensor/mysubaru/VIN-NUMBER-HERE/dist_to_empty_km/config
|
||||
hassioConfig[s.config.Hassio.Topics.Discovery+`/sensor/`+v.Vin+`/dist_to_empty_km/config`] = `{` + device + origin + topic + `"name":"Distance to Empty (km)","uniq_id":"` + v.Vin + `_dist_to_empty_km","obj_id":"` + obj_id_prefix + `_dist_to_empty_km","ic":"mdi:map-marker-distance","stat_t":"mysubarumq/` + v.Vin + `/state","val_tpl":"{{value_json.dist_to_empty_km}}","unit_of_meas":"km"}`
|
||||
// homeassistant/sensor/mysubaru/VIN-NUMBER-HERE/dist_to_empty_mi/config
|
||||
hassioConfig[s.config.Hassio.Topics.Discovery+`/sensor/`+v.Vin+`/dist_to_empty_mi/config`] = `{` + device + origin + topic + `"name":"Distance to Empty (mi)","uniq_id":"` + v.Vin + `_dist_to_empty_mi","obj_id":"` + obj_id_prefix + `_dist_to_empty_mi","ic":"mdi:map-marker-distance","stat_t":"mysubarumq/` + v.Vin + `/state","val_tpl":"{{value_json.dist_to_empty_mi}}","unit_of_meas":"mi"}`
|
||||
// homeassistant/sensor/mysubaru/VIN-NUMBER-HERE/dist_to_empty_pc/config
|
||||
hassioConfig[s.config.Hassio.Topics.Discovery+`/sensor/`+v.Vin+`/dist_to_empty_pc/config`] = `{` + device + origin + topic + `"name":"Gas Tank (%)","uniq_id":"` + v.Vin + `_dist_to_empty_pc","obj_id":"` + obj_id_prefix + `_dist_to_empty_pc","ic":"mdi:gauge","stat_t":"mysubarumq/` + v.Vin + `/state","val_tpl":"{{value_json.dist_to_empty_pc}}","unit_of_meas":"%"}`
|
||||
// homeassistant/sensor/mysubaru/VIN-NUMBER-HERE/consumption_us/config
|
||||
hassioConfig[s.config.Hassio.Topics.Discovery+`/sensor/`+v.Vin+`/consumption_us/config`] = `{` + device + origin + topic + `"name":"Consumption (MPG)","uniq_id":"` + v.Vin + `_consumption_us","obj_id":"` + obj_id_prefix + `_consumption_us","stat_t":"mysubarumq/` + v.Vin + `/state","val_tpl":"{{value_json.consumption_us}}","unit_of_meas":"MPG"}`
|
||||
// homeassistant/sensor/mysubaru/VIN-NUMBER-HERE/consumption_eu/config
|
||||
hassioConfig[s.config.Hassio.Topics.Discovery+`/sensor/`+v.Vin+`/consumption_eu/config`] = `{` + device + origin + topic + `"name":"Consumption (L/100km)","uniq_id":"` + v.Vin + `_consumption_eu","obj_id":"` + obj_id_prefix + `_consumption_eu","stat_t":"mysubarumq/` + v.Vin + `/state","val_tpl":"{{value_json.consumption_eu}}","unit_of_meas":"L100km"}`
|
||||
// homeassistant/sensor/mysubaru/VIN-NUMBER-HERE/engine_state/consumption_eu/config
|
||||
hassioConfig[s.config.Hassio.Topics.Discovery+`/sensor/`+v.Vin+`/engine_state/config`] = `{` + device + origin + topic + `"name":"Engine State","uniq_id":"` + v.Vin + `_engine_state","obj_id":"` + obj_id_prefix + `_engine_state","ic":"mdi:engine","stat_t":"mysubarumq/` + v.Vin + `/state","val_tpl":"{{value_json.engine_state}}"}`
|
||||
hassioConfig[s.config.Hassio.Topics.Discovery+`/device_tracker/`+v.Vin+`/config`] = `{` + device + origin + `"name":"` + v.CarNickname + `","uniq_id":"` + v.Vin + `_device_tracker","obj_id":"` + obj_id_prefix + `","ic":"mdi:car-connected","json_attr_t":"mysubarumq/` + v.Vin + `/attr"}`
|
||||
|
||||
topicState := `mysubarumq/` + v.Vin + `/ignition`
|
||||
topicSet := `mysubarumq/` + v.Vin + `/ignition/set`
|
||||
hassioConfig[s.config.Hassio.Topics.Discovery+`/switch/`+v.Vin+`/ignition/config`] = `{` + origin + device + `"name":"Ignition","cmd_t":"` + topicSet + `","stat_t":"` + topicState + `","name":null,"obj_id":"` + obj_id_prefix + `_ignition","ic":"mdi:engine","pl_off":"OFF","pl_on":"ON","uniq_id":"` + v.Vin + `_ignition"}`
|
||||
|
||||
topicState = `mysubarumq/` + v.Vin + `/lock`
|
||||
topicSet = `mysubarumq/` + v.Vin + `/lock/set`
|
||||
hassioConfig[s.config.Hassio.Topics.Discovery+`/lock/`+v.Vin+`/config`] = `{` + origin + device + `"name":"Lock","cmd_t":"` + topicSet + `","stat_t":"` + topicState + `","name":null,"obj_id":"` + obj_id_prefix + `_lock","ic":"mdi:car-key","pl_unlk":"UNLOCK","pl_lock":"LOCK","stat_locked":"LOCK","stat_unlocked":"UNLOCK","uniq_id":"` + v.Vin + `_lock"}`
|
||||
|
||||
hassioConfig[s.config.Hassio.Topics.Discovery+`/event/`+v.Vin+`/engine/config`] = `{` + device + origin + `"name":"Ignition Events","dev_cla":"button","evt_typ":["start","stop"],"uniq_id":"` + v.Vin + `_event_ignition","obj_id":"` + obj_id_prefix + `_event_ignition","ic":"mdi:engine","stat_t":"mysubarumq/` + v.Vin + `/event/ignition"}`
|
||||
hassioConfig[s.config.Hassio.Topics.Discovery+`/event/`+v.Vin+`/lock/config`] = `{` + device + origin + `"name":"Lock Events","dev_cla":"button","evt_typ":["lock","unlock"],"uniq_id":"` + v.Vin + `_event_lock","obj_id":"` + obj_id_prefix + `_event_lock","ic":"mdi:car-key","stat_t":"mysubarumq/` + v.Vin + `/event/lock"}`
|
||||
|
||||
// {"availability":[{"topic":"zigbee02/bridge/state"}],"availability_mode":"all","command_topic":"zigbee02/bridge/request/restart","device":{"hw_version":"zStack3x0 20230507","identifiers":["zigbee2mqtt_bridge_0x00124b00237e0682"],"manufacturer":"Zigbee2MQTT","model":"Bridge","name":"Zigbee2MQTT Bridge","sw_version":"1.35.1"},"device_class":"restart","name":"Restart","object_id":"zigbee2mqtt_bridge_restart","origin":{"name":"Zigbee2MQTT","sw":"1.35.1","url":"https://www.zigbee2mqtt.io"},"payload_press":"","unique_id":"bridge_0x00124b00237e0682_restart_zigbee02"}
|
||||
// LOCK
|
||||
// state_topic: "home-assistant/frontdoor/state"
|
||||
// code_format: "^\\d{4}$"
|
||||
// command_topic: "home-assistant/frontdoor/set"
|
||||
// command_template: '{ "action": "{{ value }}", "code":"{{ code }}" }'
|
||||
// payload_lock: "LOCK"
|
||||
// payload_unlock: "UNLOCK"
|
||||
// state_locked: "LOCK"
|
||||
// state_unlocked: "UNLOCK"
|
||||
// state_locking: "LOCKING"
|
||||
// state_unlocking: "UNLOCKING"
|
||||
// state_jammed: "MOTOR_JAMMED"
|
||||
// state_ok: "MOTOR_OK"
|
||||
// optimistic: false
|
||||
// qos: 1
|
||||
// retain: true
|
||||
// value_template: "{{ value.x }}"
|
||||
|
||||
// mdi:tire
|
||||
// mdi:car-door
|
||||
// mdi:car-door-lock | mdi:car-door-lock-open | mdi:car-key
|
||||
|
||||
var msgs []*bus.Message
|
||||
for topic, payload := range hassioConfig {
|
||||
msgs = s.messages(topic, 1, true, payload, msgs)
|
||||
s.log.Debug("hassio mqtt configuration", "topic", string(topic), "payload", string(payload))
|
||||
}
|
||||
|
||||
return msgs
|
||||
}
|
||||
|
||||
// mySubaruStatusToMQTTMessage .
|
||||
func (s *MySubaruClient) mySubaruStatusToMQTTMessage(v *mysubaru.Vehicle) []*bus.Message {
|
||||
var state = map[string]string{}
|
||||
|
||||
state[`mysubarumq/`+v.Vin+`/state`] = `{"odometer_km":` + strconv.Itoa(v.Odometer.Kilometers) + `,"odometer_mi":` + strconv.Itoa(v.Odometer.Miles) + `,"dist_to_empty_km":` + strconv.Itoa(v.DistanceToEmpty.Kilometers) + `,"dist_to_empty_mi":` + strconv.Itoa(v.DistanceToEmpty.Miles) + `,"dist_to_empty_pc":` + strconv.Itoa(v.DistanceToEmpty.Percentage) + `,"consumption_us":` + fmt.Sprintf("%.2f", v.FuelConsumptionAvg.MPG) + `,"consumption_eu":` + fmt.Sprintf("%.2f", v.FuelConsumptionAvg.LP100Km) + `,"engine_state":"` + v.EngineState + `"}`
|
||||
state[`mysubarumq/`+v.Vin+`/attr`] = `{"source_type":"gps","latitude":` + fmt.Sprintf("%.6f", v.GeoLocation.Latitude) + `,"longitude":` + fmt.Sprintf("%.6f", v.GeoLocation.Longitude) + `,"course":` + strconv.Itoa(v.GeoLocation.Heading) + `,"speed":` + fmt.Sprintf("%.2f", v.GeoLocation.Speed) + `,"friendly_name":"` + v.CarNickname + `"}`
|
||||
|
||||
var msgs []*bus.Message
|
||||
for topic, payload := range state {
|
||||
msgs = s.messages(topic, 0, false, payload, msgs)
|
||||
s.log.Debug("hassio mqtt configuration", "topic", string(topic), "payload", string(payload))
|
||||
}
|
||||
|
||||
return msgs
|
||||
}
|
||||
|
||||
// messages .
|
||||
func (s *MySubaruClient) messages(t string, q byte, r bool, p string, l []*bus.Message) []*bus.Message {
|
||||
s.log.Debug("hassio mqtt configuration", "topic", string(t), "qos", q, "retained", r, "payload", string(p))
|
||||
m := bus.Message{
|
||||
Topic: t,
|
||||
QOS: q,
|
||||
Retained: r,
|
||||
Payload: p,
|
||||
}
|
||||
|
||||
if l != nil {
|
||||
l = append(l, &m)
|
||||
return l
|
||||
} else {
|
||||
var l []*bus.Message
|
||||
l = append(l, &m)
|
||||
return l
|
||||
}
|
||||
}
|
129
workers/workers.go
Normal file
129
workers/workers.go
Normal file
@ -0,0 +1,129 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// SPDX-FileCopyrightText: 2023 mysubarumq
|
||||
// SPDX-FileContributor: alex-savin
|
||||
|
||||
package workers
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"log/slog"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
// TLSConfig is a tls.Config configuration to be used with the listener.
|
||||
// See examples folder for basic and mutual-tls use.
|
||||
Test string
|
||||
}
|
||||
|
||||
// EstablishFn is a callback function for establishing new clients.
|
||||
type EstablishFn func(id string) error
|
||||
|
||||
// CloseFn is a callback function for closing all listener clients.
|
||||
type CloseFn func(id string)
|
||||
|
||||
// Worker .
|
||||
type Worker interface {
|
||||
ID() string // returns ID in string format
|
||||
Type() string // returns the type of the worker
|
||||
Init(*slog.Logger) error //
|
||||
OneTime() error //
|
||||
Serve() // starting actively listening for new connections
|
||||
Close() // stop and close the worker
|
||||
}
|
||||
|
||||
// Workers contains the network workers for the app.
|
||||
type Workers struct {
|
||||
ClientsWg sync.WaitGroup // a waitgroup that waits for all clients in all workers to finish.
|
||||
internal map[string]Worker // a map of active workers.
|
||||
sync.RWMutex
|
||||
}
|
||||
|
||||
// New returns a new instance of Workers.
|
||||
func New() *Workers {
|
||||
return &Workers{
|
||||
internal: map[string]Worker{},
|
||||
}
|
||||
}
|
||||
|
||||
// Add adds a new worker to the workers map, keyed on id.
|
||||
func (w *Workers) Add(val Worker) {
|
||||
w.Lock()
|
||||
defer w.Unlock()
|
||||
w.internal[val.ID()] = val
|
||||
}
|
||||
|
||||
// Get returns the value of a worker if it exists.
|
||||
func (w *Workers) Get(id string) (Worker, bool) {
|
||||
w.RLock()
|
||||
defer w.RUnlock()
|
||||
val, ok := w.internal[id]
|
||||
return val, ok
|
||||
}
|
||||
|
||||
// Len returns the length of the workers map.
|
||||
func (w *Workers) Len() int {
|
||||
w.RLock()
|
||||
defer w.RUnlock()
|
||||
return len(w.internal)
|
||||
}
|
||||
|
||||
// Delete removes a worker from the internal map.
|
||||
func (w *Workers) Delete(id string) {
|
||||
w.Lock()
|
||||
defer w.Unlock()
|
||||
delete(w.internal, id)
|
||||
}
|
||||
|
||||
// Serve starts a worker serving from the internal map.
|
||||
func (w *Workers) Serve(id string) {
|
||||
w.RLock()
|
||||
defer w.RUnlock()
|
||||
worker := w.internal[id]
|
||||
|
||||
go func() {
|
||||
worker.Serve()
|
||||
worker.OneTime()
|
||||
}()
|
||||
}
|
||||
|
||||
// ServeAll starts all workers serving from the internal map.
|
||||
func (w *Workers) ServeAll() {
|
||||
w.RLock()
|
||||
i := 0
|
||||
ids := make([]string, len(w.internal))
|
||||
for id := range w.internal {
|
||||
ids[i] = id
|
||||
i++
|
||||
}
|
||||
w.RUnlock()
|
||||
|
||||
for _, id := range ids {
|
||||
w.Serve(id)
|
||||
}
|
||||
}
|
||||
|
||||
// Close stops a worker from the internal map.
|
||||
func (w *Workers) Close(id string) {
|
||||
w.RLock()
|
||||
defer w.RUnlock()
|
||||
if worker, ok := w.internal[id]; ok {
|
||||
worker.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// CloseAll iterates and closes all registered workere.
|
||||
func (w *Workers) CloseAll() {
|
||||
w.RLock()
|
||||
i := 0
|
||||
ids := make([]string, len(w.internal))
|
||||
for id := range w.internal {
|
||||
ids[i] = id
|
||||
i++
|
||||
}
|
||||
w.RUnlock()
|
||||
|
||||
for _, id := range ids {
|
||||
w.Close(id)
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user