From decd825a6b07897513a12bebbaa188ad1746620b Mon Sep 17 00:00:00 2001 From: Joshua Rich Date: Sun, 5 May 2024 11:01:52 +1000 Subject: [PATCH] refactor!: major MQTT functionality refactor BREAKING CHANGE: This commit is a major refactoring of the MQTT functionality coinciding with changes to the underlying library that powers it. **MQTT entities have been renamed, which will result in some breakage of automations and features in Home Assistant.** No functionality has been lost however, and this change will make it easier to add additional controls and features powered by MQTT to Go Hass Agent. --- go.mod | 7 +- go.sum | 15 +-- internal/agent/mqtt.go | 67 ++----------- internal/agent/mqtt_linux.go | 115 ++++++++++++++++++---- internal/agent/runners.go | 43 +++++--- internal/linux/device.go | 15 +++ internal/linux/media/volume.go | 66 ++++++++----- internal/linux/mqtt.go | 38 ------- internal/linux/power/powerControl.go | 40 ++++---- internal/linux/power/screenLockControl.go | 43 ++++---- internal/linux/system/dbusCommand.go | 8 +- internal/preferences/prefs.go | 2 +- 12 files changed, 250 insertions(+), 209 deletions(-) delete mode 100644 internal/linux/mqtt.go diff --git a/go.mod b/go.mod index b0d4ee819..9574e57c5 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.22.0 require ( fyne.io/fyne/v2 v2.4.5 github.com/cenkalti/backoff/v4 v4.3.0 - github.com/eclipse/paho.mqtt.golang v1.4.3 + github.com/eclipse/paho.golang v0.21.0 github.com/go-playground/validator/v10 v10.19.0 github.com/godbus/dbus/v5 v5.1.0 github.com/grandcat/zeroconf v1.0.0 @@ -27,11 +27,10 @@ require ( require ( github.com/dolthub/maphash v0.1.0 // indirect github.com/gorilla/websocket v1.5.1 // indirect - github.com/kr/text v0.2.0 // indirect github.com/mandykoh/go-parallel v0.1.0 // indirect go.uber.org/atomic v1.7.0 // indirect go.uber.org/multierr v1.9.0 // indirect - golang.org/x/sync v0.7.0 // indirect + golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f // indirect ) require ( @@ -55,7 +54,7 @@ require ( github.com/go-text/typesetting v0.1.0 // indirect github.com/gopherjs/gopherjs v1.17.2 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/joshuar/go-hass-anything/v7 v7.2.0 + github.com/joshuar/go-hass-anything/v9 v9.1.0 github.com/jsummers/gobmp v0.0.0-20151104160322-e2ba15ffa76e // indirect github.com/klauspost/compress v1.17.5 // indirect github.com/leodido/go-urn v1.4.0 // indirect diff --git a/go.sum b/go.sum index 11975921a..840265f7d 100644 --- a/go.sum +++ b/go.sum @@ -68,15 +68,14 @@ github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSV github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dolthub/maphash v0.1.0 h1:bsQ7JsF4FkkWyrP3oCnFJgrCUAFbFf3kOl4L/QxPDyQ= github.com/dolthub/maphash v0.1.0/go.mod h1:gkg4Ch4CdCDu5h6PMriVLawB7koZ+5ijb9puGMV50a4= -github.com/eclipse/paho.mqtt.golang v1.4.3 h1:2kwcUGn8seMUfWndX0hGbvH8r7crgcJguQNCyp70xik= -github.com/eclipse/paho.mqtt.golang v1.4.3/go.mod h1:CSYvoAlsMkhYOXh/oKyxa8EcBci6dVkLCbo5tTC1RIE= +github.com/eclipse/paho.golang v0.21.0 h1:cxxEReu+iFbA5RrHfRGxJOh8tXZKDywuehneoeBeyn8= +github.com/eclipse/paho.golang v0.21.0/go.mod h1:GHF6vy7SvDbDHBguaUpfuBkEB5G6j0zKxMG4gbh6QRQ= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= @@ -235,10 +234,8 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jeandeaual/go-locale v0.0.0-20240223122105-ce5225dcaa49 h1:Po+wkNdMmN+Zj1tDsJQy7mJlPlwGNQd9JZoPjObagf8= github.com/jeandeaual/go-locale v0.0.0-20240223122105-ce5225dcaa49/go.mod h1:YiutDnxPRLk5DLUFj6Rw4pRBBURZY07GFr54NdV9mQg= -github.com/joshuar/go-hass-anything/v7 v7.1.0 h1:wEOvu/68rzsxUjbzigb0FxLay8XwCMfrHTuXe7QPK2k= -github.com/joshuar/go-hass-anything/v7 v7.1.0/go.mod h1:ozEpyBHdsl74qveKTh7eFwTPO0bOGzZkn6RZuMFooag= -github.com/joshuar/go-hass-anything/v7 v7.2.0 h1:6FbPnFzDauKF4Y/YYWMhx/QxxWL+L2mU6QL58jwXeiI= -github.com/joshuar/go-hass-anything/v7 v7.2.0/go.mod h1:Sc2im9Z6ZvWJGYBhnP8UaAAPJ81fU3jxGgm8VZmprXY= +github.com/joshuar/go-hass-anything/v9 v9.1.0 h1:dPNfCrMVu1k6+0vnNqm8ncAynAuckrhL6RHpaULgpiY= +github.com/joshuar/go-hass-anything/v9 v9.1.0/go.mod h1:kaNCsLaXjI+7yCnBwO2ZbUViQkoiriu52i8lnPx/NAQ= github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= @@ -395,6 +392,8 @@ go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A= +go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= @@ -422,6 +421,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f h1:99ci1mjWVBWwJiEKYY6jWa4d2nTQVIEhZIptnrVb1XY= +golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.10.0/go.mod h1:jtrku+n79PfroUbvDdeUWMAI+heR786BofxrbiSF+J0= diff --git a/internal/agent/mqtt.go b/internal/agent/mqtt.go index caf99d8dc..74057142d 100644 --- a/internal/agent/mqtt.go +++ b/internal/agent/mqtt.go @@ -8,67 +8,12 @@ package agent import ( "context" - mqtthass "github.com/joshuar/go-hass-anything/v7/pkg/hass" - mqttapi "github.com/joshuar/go-hass-anything/v7/pkg/mqtt" - "github.com/rs/zerolog/log" - - "github.com/joshuar/go-hass-agent/internal/preferences" + mqttapi "github.com/joshuar/go-hass-anything/v9/pkg/mqtt" ) -type mqttObj struct { - msgCh chan mqttapi.Msg - entities []*mqtthass.EntityConfig - subscriptions []*mqttapi.Subscription -} - -func (o *mqttObj) Name() string { - return preferences.AppName -} - -func (o *mqttObj) Configuration() []*mqttapi.Msg { - var msgs []*mqttapi.Msg - for _, c := range o.entities { - if msg, err := mqtthass.MarshalConfig(c); err != nil { - log.Error().Err(err).Msg("Failed to marshal payload for entity.") - } else { - msgs = append(msgs, msg) - } - } - return msgs -} - -func (o *mqttObj) Subscriptions() []*mqttapi.Subscription { - var subs []*mqttapi.Subscription - 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). - Msg("Error adding subscription.") - } else { - subs = append(subs, sub) - } - } - } - if len(o.subscriptions) > 0 { - subs = append(subs, o.subscriptions...) - } - return subs -} - -func (o *mqttObj) States() []*mqttapi.Msg { - return nil -} - -func (o *mqttObj) Run(ctx context.Context, client *mqttapi.Client) { - for { - select { - case msg := <-o.msgCh: - if err := client.Publish(&msg); err != nil { - log.Warn().Err(err).Msg("Unable to publish message to MQTT.") - } - case <-ctx.Done(): - close(o.msgCh) - return - } - } +type mqttDevice interface { + Subscriptions() []*mqttapi.Subscription + Configs() []*mqttapi.Msg + Msgs() chan *mqttapi.Msg + Setup(ctx context.Context) error } diff --git a/internal/agent/mqtt_linux.go b/internal/agent/mqtt_linux.go index 04c0426e3..ef08d9647 100644 --- a/internal/agent/mqtt_linux.go +++ b/internal/agent/mqtt_linux.go @@ -8,35 +8,110 @@ package agent import ( "context" - mqtthass "github.com/joshuar/go-hass-anything/v7/pkg/hass" - mqttapi "github.com/joshuar/go-hass-anything/v7/pkg/mqtt" + mqtthass "github.com/joshuar/go-hass-anything/v9/pkg/hass" + mqttapi "github.com/joshuar/go-hass-anything/v9/pkg/mqtt" + "github.com/rs/zerolog/log" "github.com/joshuar/go-hass-agent/internal/linux/media" "github.com/joshuar/go-hass-agent/internal/linux/power" "github.com/joshuar/go-hass-agent/internal/linux/system" ) -// newMQTTObject creates an MQTT object for the agent to use for this operating -// system (Linux). -func newMQTTObject(ctx context.Context) *mqttObj { - var entities []*mqtthass.EntityConfig - var subscriptions []*mqttapi.Subscription +type linuxMQTTDevice struct { + msgs chan *mqttapi.Msg + sensors []*mqtthass.SensorEntity + buttons []*mqtthass.ButtonEntity + numbers []*mqtthass.NumberEntity[int] + switches []*mqtthass.SwitchEntity + controls []*mqttapi.Subscription +} + +func (d *linuxMQTTDevice) Subscriptions() []*mqttapi.Subscription { + var subs []*mqttapi.Subscription + + for _, button := range d.buttons { + if sub, err := button.MarshalSubscription(); err != nil { + log.Warn().Err(err).Str("entity", button.Name).Msg("Could not create subscription.") + } else { + subs = append(subs, sub) + } + } + for _, number := range d.numbers { + if sub, err := number.MarshalSubscription(); err != nil { + log.Warn().Err(err).Str("entity", number.Name).Msg("Could not create subscription.") + } else { + subs = append(subs, sub) + } + } + for _, sw := range d.switches { + if sub, err := sw.MarshalSubscription(); err != nil { + log.Warn().Err(err).Str("entity", sw.Name).Msg("Could not create subscription.") + } else { + subs = append(subs, sub) + } + } + + subs = append(subs, d.controls...) + + return subs +} + +func (d *linuxMQTTDevice) Configs() []*mqttapi.Msg { + var configs []*mqttapi.Msg - msgCh := make(chan mqttapi.Msg) + for _, sensor := range d.sensors { + if sub, err := sensor.MarshalConfig(); err != nil { + log.Warn().Err(err).Str("entity", sensor.Name).Msg("Could not create subscription.") + } else { + configs = append(configs, sub) + } + } + for _, button := range d.buttons { + if sub, err := button.MarshalConfig(); err != nil { + log.Warn().Err(err).Str("entity", button.Name).Msg("Could not create subscription.") + } else { + configs = append(configs, sub) + } + } + for _, number := range d.numbers { + if sub, err := number.MarshalConfig(); err != nil { + log.Warn().Err(err).Str("entity", number.Name).Msg("Could not create subscription.") + } else { + configs = append(configs, sub) + } + } + for _, sw := range d.switches { + if sub, err := sw.MarshalConfig(); err != nil { + log.Warn().Err(err).Str("entity", sw.Name).Msg("Could not create subscription.") + } else { + configs = append(configs, sub) + } + } + + return configs +} - // Add screensaver/screenlock control. - entities = append(entities, power.NewScreenLockControl(ctx)) - // Add power controls (poweroff, reboot, suspend, etc.). - entities = append(entities, power.NewPowerControl(ctx)...) - // Add volume control - entities = append(entities, media.VolumeControl(ctx, msgCh)...) +func (d *linuxMQTTDevice) Msgs() chan *mqttapi.Msg { + return d.msgs +} - // Add subscription for issuing D-Bus commands to the Linux device. - subscriptions = append(subscriptions, system.NewDBusCommandSubscription(ctx)) +func (d *linuxMQTTDevice) Setup(_ context.Context) error { + return nil +} - return &mqttObj{ - entities: entities, - subscriptions: subscriptions, - msgCh: msgCh, +func newMQTTDevice(ctx context.Context) *linuxMQTTDevice { + dev := &linuxMQTTDevice{ + msgs: make(chan *mqttapi.Msg), } + + // Add the power controls (suspend, resume, poweroff, etc.). + dev.buttons = append(dev.buttons, power.NewPowerControl(ctx)...) + // Add the screen lock controls. + dev.buttons = append(dev.buttons, power.NewScreenLockControl(ctx)) + // Add the volume controls. + dev.numbers = append(dev.numbers, media.VolumeControl(ctx, dev.Msgs())) + // Add the D-Bus command action. + dev.controls = append(dev.controls, system.NewDBusCommandSubscription(ctx)) + + return dev } diff --git a/internal/agent/runners.go b/internal/agent/runners.go index b5bdb361d..f290cff33 100644 --- a/internal/agent/runners.go +++ b/internal/agent/runners.go @@ -12,7 +12,7 @@ import ( "github.com/robfig/cron/v3" "github.com/rs/zerolog/log" - mqttapi "github.com/joshuar/go-hass-anything/v7/pkg/mqtt" + mqttapi "github.com/joshuar/go-hass-anything/v9/pkg/mqtt" "github.com/joshuar/go-hass-agent/internal/device" "github.com/joshuar/go-hass-agent/internal/hass" @@ -163,20 +163,41 @@ func runMQTTWorker(ctx context.Context) { return } - o := newMQTTObject(ctx) + // Create an MQTT device for this operating system and run its Setup. + mqttDevice := newMQTTDevice(ctx) + if err := mqttDevice.Setup(ctx); err != nil { + log.Error().Err(err).Msg("Could not set up device MQTT functionality.") + return + } - client, err := mqttapi.NewClient(ctx, prefs, o) + // Create a new connection to the MQTT broker. This will also publish the + // device subscriptions. + client, err := mqttapi.NewClient(ctx, prefs, mqttDevice.Subscriptions(), mqttDevice.Configs()) if err != nil { - log.Error().Err(err).Msg("Could not start MQTT client.") + log.Error().Err(err).Msg("Could not connect to MQTT broker.") return } + // Publish the device configs. + log.Debug().Msg("Publishing configs.") + if err := client.Publish(mqttDevice.Configs()...); err != nil { + log.Error().Err(err).Msg("Failed to publish configuration messages.") + } + go func() { - o.Run(ctx, client) + log.Debug().Msg("Listening for messages to publish to MQTT.") + for { + select { + case msg := <-mqttDevice.Msgs(): + if err := client.Publish(msg); err != nil { + log.Warn().Err(err).Msg("Unable to publish message to MQTT.") + } + case <-ctx.Done(): + return + } + } }() - log.Debug().Msg("Listening for events on MQTT.") - <-ctx.Done() } @@ -190,15 +211,15 @@ func resetMQTTWorker(ctx context.Context) { return } - o := newMQTTObject(ctx) + mqttDevice := newMQTTDevice(ctx) - c, err := mqttapi.NewClient(ctx, prefs, o) + c, err := mqttapi.NewClient(ctx, prefs, nil, nil) if err != nil { - log.Error().Err(err).Msg("Could not start MQTT client.") + log.Error().Err(err).Msg("Could not connect to MQTT broker.") return } - if err := c.Unpublish(o.Configuration()...); err != nil { + if err := c.Unpublish(mqttDevice.Configs()...); err != nil { log.Error().Err(err).Msg("Failed to reset MQTT.") } } diff --git a/internal/linux/device.go b/internal/linux/device.go index 7ec4024fd..3f14571e4 100644 --- a/internal/linux/device.go +++ b/internal/linux/device.go @@ -10,8 +10,11 @@ import ( "os/user" "strings" + mqtthass "github.com/joshuar/go-hass-anything/v9/pkg/hass" "github.com/rs/zerolog/log" "github.com/shirou/gopsutil/v3/host" + + "github.com/joshuar/go-hass-agent/internal/preferences" ) type Device struct { @@ -102,6 +105,18 @@ func NewDevice(name, version string) *Device { } } +func MQTTDevice() *mqtthass.Device { + dev := NewDevice(preferences.AppName, preferences.AppVersion) + return &mqtthass.Device{ + Name: dev.DeviceName(), + URL: preferences.AppURL, + SWVersion: dev.OsVersion(), + Manufacturer: dev.Manufacturer(), + Model: dev.Model(), + Identifiers: []string{dev.DeviceID()}, + } +} + // getDeviceID retrieves the unique host ID of the device running the agent, or // unknown if that doesn't work. func getDeviceID() string { diff --git a/internal/linux/media/volume.go b/internal/linux/media/volume.go index d50c035d9..cc3891d8c 100644 --- a/internal/linux/media/volume.go +++ b/internal/linux/media/volume.go @@ -9,26 +9,28 @@ import ( "context" "strconv" - MQTT "github.com/eclipse/paho.mqtt.golang" "mrogalski.eu/go/pulseaudio" - mqtthass "github.com/joshuar/go-hass-anything/v7/pkg/hass" - mqttapi "github.com/joshuar/go-hass-anything/v7/pkg/mqtt" + "github.com/eclipse/paho.golang/paho" + mqtthass "github.com/joshuar/go-hass-anything/v9/pkg/hass" + mqttapi "github.com/joshuar/go-hass-anything/v9/pkg/mqtt" "github.com/rs/zerolog/log" "github.com/joshuar/go-hass-agent/internal/linux" + "github.com/joshuar/go-hass-agent/internal/preferences" ) type audioDevice struct { pulseAudio *pulseaudio.Client - topics *mqtthass.Topics - msgCh chan mqttapi.Msg - volume float64 + stateTopic string + msgCh chan *mqttapi.Msg + volume int } -func VolumeControl(ctx context.Context, msgCh chan mqttapi.Msg) []*mqtthass.EntityConfig { - var entities []*mqtthass.EntityConfig +func VolumeControl(ctx context.Context, msgCh chan *mqttapi.Msg) *mqtthass.NumberEntity[int] { + device := linux.MQTTDevice() + client, err := pulseaudio.NewClient() if err != nil { log.Warn().Err(err).Msg("Unable to connect to Pulseaudio. Volume control will be unavailable.") @@ -40,12 +42,21 @@ func VolumeControl(ctx context.Context, msgCh chan mqttapi.Msg) []*mqtthass.Enti msgCh: msgCh, } - volCtrl := linux.NewSlider("volume", 1, 0, 100). - WithIcon("mdi:knob"). - WithCommandCallback(audioDev.parseVolume). - WithValueTemplate("{{ value_json.value }}") - audioDev.topics = volCtrl.GetTopics() - entities = append(entities, volCtrl) + volCtrl := mqtthass.AsNumber( + mqtthass.NewEntity(preferences.AppName, "Volume", device.Name+"_volume"). + WithOriginInfo(preferences.MQTTOrigin()). + WithDeviceInfo(device). + WithIcon("mdi:knob"). + WithCommandCallback(audioDev.parseVolume). + WithValueTemplate("{{ value_json.value }}"), + 1, 0, 100, mqtthass.NumberSlider) + audioDev.stateTopic = volCtrl.StateTopic + // muteCtl := linux.NewToggle("volume_mute"). + // WithIcon("mdi:volume-mute"). + // WithCommandCallback(audioDev.parseVolume). + // WithValueTemplate("{{ value_json.value }}") + // audioDev.topics = volCtrl.GetTopics() + // entities = append(entities, volCtrl) if _, err := audioDev.getVolume(); err != nil { log.Warn().Err(err).Msg("Could not get volume.") @@ -58,7 +69,6 @@ func VolumeControl(ctx context.Context, msgCh chan mqttapi.Msg) []*mqtthass.Enti events, err := client.Updates() if err != nil { log.Warn().Err(err).Msg("Cannot monitor Pulseaudio.") - entities = nil return } log.Debug().Msg("Monitoring pulseaudio for events.") @@ -77,23 +87,25 @@ func VolumeControl(ctx context.Context, msgCh chan mqttapi.Msg) []*mqtthass.Enti } } }() - return entities + return volCtrl } func (d *audioDevice) getVolume() (bool, error) { - newVol, err := d.pulseAudio.Volume() + v, err := d.pulseAudio.Volume() + newVol := int(v * 100) if err != nil { return false, err } - if newVol != float32(d.volume) { - d.volume = float64(newVol * 100) + if newVol != d.volume { + d.volume = newVol return true, nil } return false, nil } -func (d *audioDevice) setVolume(v float64) error { - if err := d.pulseAudio.SetVolume(float32(v)); err != nil { +func (d *audioDevice) setVolume(v int) error { + newVol := float32(v) / 100 + if err := d.pulseAudio.SetVolume(newVol); err != nil { return err } d.volume = v @@ -101,16 +113,16 @@ func (d *audioDevice) setVolume(v float64) error { } func (d *audioDevice) publishVolume() { - msg := mqttapi.NewMsg(d.topics.State, []byte(`{ "value": `+strconv.FormatFloat(d.volume, 'f', -1, 64)+` }`)) - d.msgCh <- *msg + msg := mqttapi.NewMsg(d.stateTopic, []byte(`{ "value": `+strconv.Itoa(d.volume)+` }`)) + d.msgCh <- msg } -func (d *audioDevice) parseVolume(_ MQTT.Client, msg MQTT.Message) { - if newValue, err := strconv.ParseFloat(string(msg.Payload()), 64); err != nil { +func (d *audioDevice) parseVolume(p *paho.Publish) { + if newValue, err := strconv.Atoi(string(p.Payload)); err != nil { log.Warn().Err(err).Msg("Could not parse new volume level.") } else { - log.Trace().Float64("volume", newValue).Msg("Received volume change from Home Assistant.") - if err := d.setVolume(newValue / 100); err != nil { + log.Trace().Int("volume", newValue).Msg("Received volume change from Home Assistant.") + if err := d.setVolume(newValue); err != nil { log.Warn().Err(err).Msg("Could not set volume level.") return } diff --git a/internal/linux/mqtt.go b/internal/linux/mqtt.go deleted file mode 100644 index 3b18741b6..000000000 --- a/internal/linux/mqtt.go +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright (c) 2024 Joshua Rich -// -// This software is released under the MIT License. -// https://opensource.org/licenses/MIT - -package linux - -import ( - mqtthass "github.com/joshuar/go-hass-anything/v7/pkg/hass" - - "github.com/joshuar/go-hass-agent/internal/preferences" -) - -func NewButton(entityID string) *mqtthass.EntityConfig { - return mqtthass.NewEntityByID(entityID, preferences.AppName, preferences.MQTTTopicPrefix). - AsButton(). - WithOriginInfo(preferences.MQTTOrigin()). - WithDeviceInfo(mqttDevice()) -} - -func NewSlider(entityID string, step, min, max float64) *mqtthass.EntityConfig { - return mqtthass.NewEntityByID(entityID, preferences.AppName, preferences.MQTTTopicPrefix). - AsNumber(step, min, max, mqtthass.NumberSlider). - WithOriginInfo(preferences.MQTTOrigin()). - WithDeviceInfo(mqttDevice()) -} - -func mqttDevice() *mqtthass.Device { - dev := NewDevice(preferences.AppName, preferences.AppVersion) - return &mqtthass.Device{ - Name: dev.DeviceName(), - URL: preferences.AppURL, - SWVersion: dev.OsVersion(), - Manufacturer: dev.Manufacturer(), - Model: dev.Model(), - Identifiers: []string{dev.DeviceID()}, - } -} diff --git a/internal/linux/power/powerControl.go b/internal/linux/power/powerControl.go index 2a6000a86..761fb8180 100644 --- a/internal/linux/power/powerControl.go +++ b/internal/linux/power/powerControl.go @@ -8,12 +8,13 @@ package power import ( "context" - MQTT "github.com/eclipse/paho.mqtt.golang" + "github.com/eclipse/paho.golang/paho" "github.com/godbus/dbus/v5" - mqtthass "github.com/joshuar/go-hass-anything/v7/pkg/hass" + mqtthass "github.com/joshuar/go-hass-anything/v9/pkg/hass" "github.com/rs/zerolog/log" "github.com/joshuar/go-hass-agent/internal/linux" + "github.com/joshuar/go-hass-agent/internal/preferences" "github.com/joshuar/go-hass-agent/pkg/linux/dbusx" ) @@ -36,65 +37,70 @@ type commandConfig struct { var commands = map[string]commandConfig{ "lock_session": { - name: "lock", + name: "Lock Session", icon: "mdi:eye-lock", method: dbusSessionLockMethod, }, "unlock_session": { - name: "unlock", + name: "Unlock Session", icon: "mdi:eye-lock-open", method: dbusSessionUnlockMethod, }, "reboot": { - name: "reboot", + name: "Reboot", icon: "mdi:restart", path: dbus.ObjectPath("/org/freedesktop/login1"), method: dbusSessionRebootMethod, }, "suspend": { - name: "suspend", + name: "Suspend", icon: "mdi:power-sleep", path: dbus.ObjectPath("/org/freedesktop/login1"), method: dbusSessionSuspendMethod, }, "hibernate": { - name: "hibernate", + name: "Hibernate", icon: "mdi:power-sleep", path: dbus.ObjectPath("/org/freedesktop/login1"), method: dbusSessionHibernateMethod, }, - "poweroff": { - name: "power off", + "power_off": { + name: "Power Off", icon: "mdi:power", path: dbus.ObjectPath("/org/freedesktop/login1"), method: dbusSessionPowerOffMethod, }, } -func NewPowerControl(ctx context.Context) []*mqtthass.EntityConfig { - var entities []*mqtthass.EntityConfig +func NewPowerControl(ctx context.Context) []*mqtthass.ButtonEntity { + var entities []*mqtthass.ButtonEntity + device := linux.MQTTDevice() sessionPath := dbusx.GetSessionPath(ctx) for k, v := range commands { - var callback func(MQTT.Client, MQTT.Message) + var callback func(p *paho.Publish) if v.path == "" { - callback = func(_ MQTT.Client, _ MQTT.Message) { + callback = func(_ *paho.Publish) { err := systemDBusCall(ctx, sessionPath, dbusSessionDest, v.method) if err != nil { log.Warn().Err(err).Msgf("Could not %s session.", v.name) } } } else { - callback = func(_ MQTT.Client, _ MQTT.Message) { + callback = func(_ *paho.Publish) { err := systemDBusCall(ctx, v.path, dbusSessionDest, v.method, true) if err != nil { log.Warn().Err(err).Msg("Could not power off session.") } } } - entities = append(entities, linux.NewButton(k). - WithIcon(v.icon). - WithCommandCallback(callback)) + entities = append(entities, + mqtthass.AsButton( + mqtthass.NewEntity(preferences.AppName, v.name, device.Name+"_"+k). + WithOriginInfo(preferences.MQTTOrigin()). + WithDeviceInfo(device). + WithIcon(v.icon). + WithCommandCallback(callback))) } return entities } diff --git a/internal/linux/power/screenLockControl.go b/internal/linux/power/screenLockControl.go index 830a85164..b5aa6ef93 100644 --- a/internal/linux/power/screenLockControl.go +++ b/internal/linux/power/screenLockControl.go @@ -10,36 +10,41 @@ import ( "os" "strings" - MQTT "github.com/eclipse/paho.mqtt.golang" + "github.com/eclipse/paho.golang/paho" "github.com/godbus/dbus/v5" - mqtthass "github.com/joshuar/go-hass-anything/v7/pkg/hass" + mqtthass "github.com/joshuar/go-hass-anything/v9/pkg/hass" "github.com/joshuar/go-hass-agent/internal/linux" + "github.com/joshuar/go-hass-agent/internal/preferences" "github.com/joshuar/go-hass-agent/pkg/linux/dbusx" "github.com/rs/zerolog/log" ) -func NewScreenLockControl(ctx context.Context) *mqtthass.EntityConfig { +func NewScreenLockControl(ctx context.Context) *mqtthass.ButtonEntity { dbusScreensaverDest, dbusScreensaverPath, dbusScreensaverMsg := getDesktopEnvScreensaverConfig() dbusScreensaverLockMethod := dbusScreensaverDest + ".Lock" + device := linux.MQTTDevice() - return linux.NewButton("lock_screensaver"). - WithIcon("mdi:eye-lock"). - WithCommandCallback(func(_ MQTT.Client, _ MQTT.Message) { - if dbusScreensaverPath == "" { - log.Warn().Msg("Could not determine screensaver method.") - } - var err error - if dbusScreensaverMsg != nil { - err = sessionDBusCall(ctx, dbus.ObjectPath(dbusScreensaverPath), dbusScreensaverDest, dbusScreensaverLockMethod, dbusScreensaverMsg) - } else { - err = sessionDBusCall(ctx, dbus.ObjectPath(dbusScreensaverPath), dbusScreensaverDest, dbusScreensaverLockMethod) - } - if err != nil { - log.Warn().Err(err).Msg("Could not lock screensaver.") - } - }) + return mqtthass.AsButton( + mqtthass.NewEntity(preferences.AppName, "Lock Screensaver", device.Name+"_lock_screensaver"). + WithOriginInfo(preferences.MQTTOrigin()). + WithDeviceInfo(device). + WithIcon("mdi:eye-lock"). + WithCommandCallback(func(_ *paho.Publish) { + if dbusScreensaverPath == "" { + log.Warn().Msg("Could not determine screensaver method.") + } + var err error + if dbusScreensaverMsg != nil { + err = sessionDBusCall(ctx, dbus.ObjectPath(dbusScreensaverPath), dbusScreensaverDest, dbusScreensaverLockMethod, dbusScreensaverMsg) + } else { + err = sessionDBusCall(ctx, dbus.ObjectPath(dbusScreensaverPath), dbusScreensaverDest, dbusScreensaverLockMethod) + } + if err != nil { + log.Warn().Err(err).Msg("Could not lock screensaver.") + } + })) } func getDesktopEnvScreensaverConfig() (dest, path string, msg *string) { diff --git a/internal/linux/system/dbusCommand.go b/internal/linux/system/dbusCommand.go index a751df810..e2b78dec5 100644 --- a/internal/linux/system/dbusCommand.go +++ b/internal/linux/system/dbusCommand.go @@ -9,9 +9,9 @@ import ( "context" "encoding/json" - MQTT "github.com/eclipse/paho.mqtt.golang" + "github.com/eclipse/paho.golang/paho" "github.com/godbus/dbus/v5" - mqttapi "github.com/joshuar/go-hass-anything/v7/pkg/mqtt" + mqttapi "github.com/joshuar/go-hass-anything/v9/pkg/mqtt" "github.com/rs/zerolog/log" "github.com/joshuar/go-hass-agent/pkg/linux/dbusx" @@ -32,10 +32,10 @@ type dbusCommandMsg struct { func NewDBusCommandSubscription(ctx context.Context) *mqttapi.Subscription { return &mqttapi.Subscription{ - Callback: func(_ MQTT.Client, msg MQTT.Message) { + Callback: func(p *paho.Publish) { var dbusMsg dbusCommandMsg - if err := json.Unmarshal(msg.Payload(), &dbusMsg); err != nil { + if err := json.Unmarshal(p.Payload, &dbusMsg); err != nil { log.Warn().Err(err).Msg("could not unmarshal dbus MQTT message") return } diff --git a/internal/preferences/prefs.go b/internal/preferences/prefs.go index 43e54b5af..d0c1d702f 100644 --- a/internal/preferences/prefs.go +++ b/internal/preferences/prefs.go @@ -12,7 +12,7 @@ import ( "sync" "github.com/adrg/xdg" - mqtthass "github.com/joshuar/go-hass-anything/v7/pkg/hass" + mqtthass "github.com/joshuar/go-hass-anything/v9/pkg/hass" "github.com/pelletier/go-toml/v2" "github.com/sourcegraph/conc/pool" )