Skip to content

Commit

Permalink
feat(agent): 🎨 MQTT agent adjustments
Browse files Browse the repository at this point in the history
add Power Off and Reboot controls; alter device info presented to Home Assistant to match registration info; simplify code and renaming; update documentation
  • Loading branch information
joshuar committed Jan 28, 2024
1 parent 0076019 commit b094c4a
Show file tree
Hide file tree
Showing 5 changed files with 81 additions and 49 deletions.
8 changes: 7 additions & 1 deletion docs/mqtt.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,10 @@ service call.

There is a significant discrepancy in permissions between the device running Go Hass Agent and Home Assistant.

Go Hass Agent runs under a user account on a device. So the above controls will only work where that user has permissions to run the underlying actions on that device. Home Assistant does not currently offer any fine-grained access control for controls like the above. So any Home Assistant user will be able to run any of the controls. This means that a Home Assistant user not associated with the device user running the agent can use the exposed controls to issue potentially disruptive actions on a device that another user is accessing.
Go Hass Agent runs under a user account on a device. So the above controls will only work where that user has permissions to run the underlying actions on that device. Home Assistant does not currently offer any fine-grained access control for controls like the above. So any Home Assistant user will be able to run any of the controls. This means that a Home Assistant user not associated with the device user running the agent can use the exposed controls to issue potentially disruptive actions on a device that another user is accessing.

## Implementation Details

### Linux

Controls rely on distribution/system support for `systemd-logind` and a working D-Bus connection.
3 changes: 1 addition & 2 deletions internal/agent/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ func (agent *Agent) Run(c string, cfg config.Config, trk SensorTracker) {
wg.Add(1)
go func() {
defer wg.Done()
agent.runMQTTWorker(runnerCtx)
runMQTTWorker(runnerCtx)
}()
}
// Listen for notifications from Home Assistant.
Expand All @@ -118,7 +118,6 @@ func (agent *Agent) Run(c string, cfg config.Config, trk SensorTracker) {
agent.ui.Run(agent.done)
}
wg.Wait()
log.Debug().Msg("here")
}

func (agent *Agent) Register(cfg config.Config, trk SensorTracker) {
Expand Down
18 changes: 8 additions & 10 deletions internal/agent/mqtt.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,31 +13,29 @@ import (
"github.com/joshuar/go-hass-agent/internal/agent/config"
)

type mqttDevice struct {
configs map[string]*mqtthass.EntityConfig
type mqttObj struct {
entities map[string]*mqtthass.EntityConfig
}

func (d *mqttDevice) Name() string {
func (o *mqttObj) Name() string {
return config.AppName
}

func (d *mqttDevice) Configuration() []*mqttapi.Msg {
func (o *mqttObj) Configuration() []*mqttapi.Msg {
var msgs []*mqttapi.Msg

for id, c := range d.configs {
for id, c := range o.entities {
if msg, err := mqtthass.MarshalConfig(c); err != nil {
log.Error().Err(err).Msgf("Failed to marshal payload for %s.", id)
} else {
msgs = append(msgs, msg)
}
}

return msgs
}

func (d *mqttDevice) Subscriptions() []*mqttapi.Subscription {
func (o *mqttObj) Subscriptions() []*mqttapi.Subscription {
var subs []*mqttapi.Subscription
for _, v := range d.configs {
for _, v := range o.entities {
if v.CommandCallback != nil {
if sub, err := mqtthass.MarshalSubscription(v); err != nil {
log.Error().Err(err).Str("entity", v.Entity.Name).
Expand All @@ -50,6 +48,6 @@ func (d *mqttDevice) Subscriptions() []*mqttapi.Subscription {
return subs
}

func (d *mqttDevice) States() []*mqttapi.Msg {
func (o *mqttObj) States() []*mqttapi.Msg {
return nil
}
89 changes: 59 additions & 30 deletions internal/agent/mqtt_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,59 +9,88 @@ import (
"context"

MQTT "github.com/eclipse/paho.mqtt.golang"
"github.com/godbus/dbus/v5"
"github.com/rs/zerolog/log"

mqtthass "github.com/joshuar/go-hass-anything/v3/pkg/hass"

"github.com/joshuar/go-hass-agent/internal/agent/config"
"github.com/joshuar/go-hass-agent/internal/linux"
"github.com/joshuar/go-hass-agent/pkg/linux/dbusx"
)

func (a *Agent) newMQTTDevice(ctx context.Context) *mqttDevice {
cfg := config.FetchFromContext(ctx)
const (
dbusDest = "org.freedesktop.login1"
dbusLockMethod = dbusDest + ".Session.Lock"
dbusUnlockMethod = dbusDest + ".Session.UnLock"
dbusRebootMethod = dbusDest + ".Manager.Reboot"
dbusPowerOffMethod = dbusDest + ".Manager.PowerOff"
)

func newMQTTObject(ctx context.Context) *mqttObj {
appName := "go_hass_agent"

baseEntity := func(entityID string) *mqtthass.EntityConfig {
return mqtthass.NewEntityByID(entityID, appName).
AsButton().
WithDefaultOriginInfo().
WithDeviceInfo(mqttDevice())
}

var deviceName string
cfg.Get(config.PrefDeviceName, &deviceName)
deviceInfo := &mqtthass.Device{
Name: deviceName,
URL: "https://github.com/joshuar/go-hass-agent",
SWVersion: config.AppVersion,
Manufacturer: "go-hass-agent",
Model: a.AppID(),
Identifiers: []string{"go-hass-agent01"},
dbusCall := func(ctx context.Context, path dbus.ObjectPath, dest, method string, args ...any) error {
return dbusx.NewBusRequest(ctx, dbusx.SystemBus).
Path(path).
Destination(dest).
Call(method, args...)
}

sessionPath := dbusx.GetSessionPath(ctx)
configs := make(map[string]*mqtthass.EntityConfig)
configs["lock_session"] = mqtthass.NewEntityByID("lock_session", "go_hass_agent").
AsButton().
WithDefaultOriginInfo().
WithDeviceInfo(deviceInfo).
entities := make(map[string]*mqtthass.EntityConfig)
entities["lock_session"] = baseEntity("lock_session").
WithIcon("mdi:eye-lock").
WithCommandCallback(func(_ MQTT.Client, _ MQTT.Message) {
err := dbusx.NewBusRequest(ctx, dbusx.SystemBus).
Path(sessionPath).
Destination("org.freedesktop.login1").
Call("org.freedesktop.login1.Session.Lock")
err := dbusCall(ctx, sessionPath, dbusDest, dbusLockMethod)
if err != nil {
log.Warn().Err(err).Msg("Could not lock session.")
}
})
configs["unlock_session"] = mqtthass.NewEntityByID("unlock_session", "go_hass_agent").
AsButton().
WithDefaultOriginInfo().
WithDeviceInfo(deviceInfo).
entities["unlock_session"] = baseEntity("unlock_session").
WithIcon("mdi:eye-lock-open").
WithCommandCallback(func(_ MQTT.Client, _ MQTT.Message) {
err := dbusx.NewBusRequest(ctx, dbusx.SystemBus).
Path(sessionPath).
Destination("org.freedesktop.login1").
Call("org.freedesktop.login1.Session.UnLock")
err := dbusCall(ctx, sessionPath, dbusDest, dbusUnlockMethod)
if err != nil {
log.Warn().Err(err).Msg("Could not unlock session.")
}
})
return &mqttDevice{
configs: configs,
entities["reboot"] = baseEntity("reboot").
WithIcon("mdi:restart").
WithCommandCallback(func(_ MQTT.Client, _ MQTT.Message) {
err := dbusCall(ctx, dbus.ObjectPath("/org/freedesktop/login1"), dbusDest, dbusRebootMethod, true)
if err != nil {
log.Warn().Err(err).Msg("Could not reboot session.")
}
})
entities["poweroff"] = baseEntity("poweroff").
WithIcon("mdi:power").
WithCommandCallback(func(_ MQTT.Client, _ MQTT.Message) {
err := dbusCall(ctx, dbus.ObjectPath("/org/freedesktop/login1"), dbusDest, dbusPowerOffMethod, true)
if err != nil {
log.Warn().Err(err).Msg("Could not power off session.")
}
})
return &mqttObj{
entities: entities,
}
}

func mqttDevice() *mqtthass.Device {
dev := linux.NewDevice(config.AppName, config.AppVersion)
return &mqtthass.Device{
Name: dev.DeviceName(),
URL: config.AppURL,
SWVersion: dev.OsVersion(),
Manufacturer: dev.Manufacturer(),
Model: dev.Model(),
Identifiers: []string{dev.DeviceID()},
}
}
12 changes: 6 additions & 6 deletions internal/agent/runners.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,28 +148,28 @@ func (agent *Agent) runNotificationsWorker(ctx context.Context) {

// runMQTTWorker will set up a connection to MQTT and listen on topics for
// controlling this device from Home Assistant.
func (agent *Agent) runMQTTWorker(ctx context.Context) {
func runMQTTWorker(ctx context.Context) {
cfg := config.FetchFromContext(ctx)

c, err := mqttapi.NewMQTTClient(cfg.Path())
if err != nil {
log.Error().Err(err).Msg("Could not start MQTT client.")
return
}
d := agent.newMQTTDevice(ctx)
if err := mqtthass.Register(d, c); err != nil {
o := newMQTTObject(ctx)
if err := mqtthass.Register(o, c); err != nil {
log.Error().Err(err).Msg("Failed to register app!")
return
}
if err := mqtthass.Subscribe(d, c); err != nil {
if err := mqtthass.Subscribe(o, c); err != nil {
log.Error().Err(err).Msg("Could not activate subscriptions.")
}
log.Debug().Msg("Listening for events on MQTT.")

<-ctx.Done()
}

func (agent *Agent) resetMQTTWorker(ctx context.Context) {
func resetMQTTWorker(ctx context.Context) {
cfg := config.FetchFromContext(ctx)

c, err := mqttapi.NewMQTTClient(cfg.Path())
Expand All @@ -179,7 +179,7 @@ func (agent *Agent) resetMQTTWorker(ctx context.Context) {
}

log.Info().Msgf("Clearing agent data from Home Assistant.")
d := agent.newMQTTDevice(ctx)
d := newMQTTObject(ctx)

if err := mqtthass.UnRegister(d, c); err != nil {
log.Error().Err(err).Msg("Failed to unregister app!")
Expand Down

0 comments on commit b094c4a

Please sign in to comment.