Files
mysubaru-mq/workers/mysubaru.go

450 lines
20 KiB
Go

// 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"
msc "git.savin.nyc/alex/mysubaru/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 {
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(&msc.Config{MySubaru: s.config.MySubaru, TimeZone: s.config.Timezone, Logger: s.log})
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://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:gas-station","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:gas-station","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","ic":"mdi:map-marker-distance","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","ic":"mdi:map-marker-distance","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"}`
for n, d := range v.Doors {
// homeassistant/sensor/mysubaru/VIN-NUMBER-HERE/door_frontleft_status/config
position := d.Position + ` ` + d.SubPosition
hassioConfig[s.config.Hassio.Topics.Discovery+`/sensor/`+v.Vin+`/`+n+`_status/config`] = `{` + device + origin + topic + `"name":"Door ` + position + ` Status","uniq_id":"` + v.Vin + n + `_status","obj_id":"` + obj_id_prefix + n + `_status","ic":"mdi:car-door","json_attr_t":"mysubarumq/` + v.Vin + `/doors/attr","json_attr_tpl":"{{value_json.` + n + `}}","stat_t":"mysubarumq/` + v.Vin + `/doors/state","val_tpl":"{{value_json.` + n + `}}"}`
}
for n, w := range v.Windows {
// homeassistant/sensor/mysubaru/VIN-NUMBER-HERE/window_frontleft/config
position := w.Position + ` ` + w.SubPosition
hassioConfig[s.config.Hassio.Topics.Discovery+`/sensor/`+v.Vin+`/`+n+`/config`] = `{` + device + origin + topic + `"name":"Window ` + position + `","uniq_id":"` + v.Vin + n + `_status","obj_id":"` + obj_id_prefix + n + `_status","ic":"mdi:car-door","stat_t":"mysubarumq/` + v.Vin + `/windows/state","val_tpl":"{{value_json.` + n + `}}"}`
}
for n, t := range v.Tires {
// homeassistant/sensor/mysubaru/VIN-NUMBER-HERE/tire_frontleft_psi/config
position := t.Position + ` ` + t.SubPosition
hassioConfig[s.config.Hassio.Topics.Discovery+`/sensor/`+v.Vin+`/`+n+`_psi/config`] = `{` + device + origin + topic + `"name":"Tire ` + position + ` (Psi)","uniq_id":"` + v.Vin + n + `_psi","obj_id":"` + obj_id_prefix + n + `_psi","ic":"mdi:tire","stat_t":"mysubarumq/` + v.Vin + `/tires/state","val_tpl":"{{value_json.` + n + `}}","unit_of_meas":"psi"}`
}
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{}
tank := ""
if v.DistanceToEmpty.Percentage > 0 && 101 >= v.DistanceToEmpty.Percentage {
tank = `"dist_to_empty_pc":` + strconv.Itoa(v.DistanceToEmpty.Percentage) + `,`
}
state[`mysubarumq/`+v.Vin+`/state`] = `{` + tank + `"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) + `,"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 + `"}`
doors := `{`
locks := `{}`
dq := len(v.Doors)
dc := 1
for n, d := range v.Doors {
doors = doors + `"` + n + `":"` + d.Status + `"`
locks = locks + `"` + n + `":"` + d.Lock + `"`
if dc != dq {
doors = doors + `,`
locks = locks + `,`
}
dc++
}
doors = doors + `}`
locks = locks + `}`
state[`mysubarumq/`+v.Vin+`/doors/state`] = doors
state[`mysubarumq/`+v.Vin+`/doors/attr`] = locks
windows := `{`
wq := len(v.Windows)
wc := 1
for n, w := range v.Windows {
windows = windows + `"` + n + `":"` + w.Status + `"`
if wc != wq {
windows = windows + `,`
}
wc++
}
windows = windows + `}`
state[`mysubarumq/`+v.Vin+`/windows/state`] = windows
tires := `{`
tq := len(v.Tires)
tc := 1
for n, t := range v.Tires {
tires = tires + `"` + n + `":` + strconv.Itoa(t.PressurePsi)
if tc != tq {
tires = tires + `,`
}
tc++
}
tires = tires + `}`
state[`mysubarumq/`+v.Vin+`/tires/state`] = tires
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
}
}