Skip to content

Commit

Permalink
feat(linux): ✨ add volume level control
Browse files Browse the repository at this point in the history
  • Loading branch information
joshuar committed May 1, 2024
1 parent 2ab73bb commit cac7077
Show file tree
Hide file tree
Showing 8 changed files with 167 additions and 6 deletions.
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,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.1.0
github.com/joshuar/go-hass-anything/v7 v7.2.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
Expand All @@ -82,4 +82,5 @@ require (
golang.org/x/net v0.23.0 // indirect
golang.org/x/sys v0.18.0 // indirect
honnef.co/go/js/dom v0.0.0-20210725211120-f030747120f2 // indirect
mrogalski.eu/go/pulseaudio v0.0.0-20240327130323-384e01075e6e
)
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,8 @@ github.com/jeandeaual/go-locale v0.0.0-20240223122105-ce5225dcaa49 h1:Po+wkNdMmN
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/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=
Expand Down Expand Up @@ -797,6 +799,8 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
mrogalski.eu/go/pulseaudio v0.0.0-20240327130323-384e01075e6e h1:SQSUCiMUx1MukBgCf+pYWTOCoV0Y2YbQ0y2vqBvCY50=
mrogalski.eu/go/pulseaudio v0.0.0-20240327130323-384e01075e6e/go.mod h1:C3V0v+gsiHHbMtFJvwozjNVdPJZw9oUxlRTVc619wSU=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
17 changes: 17 additions & 0 deletions internal/agent/mqtt.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
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"
Expand All @@ -14,6 +16,7 @@ import (
)

type mqttObj struct {
msgCh chan mqttapi.Msg
entities []*mqtthass.EntityConfig
subscriptions []*mqttapi.Subscription
}
Expand Down Expand Up @@ -55,3 +58,17 @@ func (o *mqttObj) Subscriptions() []*mqttapi.Subscription {
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
}
}
}
6 changes: 6 additions & 0 deletions internal/agent/mqtt_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
mqtthass "github.com/joshuar/go-hass-anything/v7/pkg/hass"
mqttapi "github.com/joshuar/go-hass-anything/v7/pkg/mqtt"

"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"
)
Expand All @@ -21,16 +22,21 @@ func newMQTTObject(ctx context.Context) *mqttObj {
var entities []*mqtthass.EntityConfig
var subscriptions []*mqttapi.Subscription

msgCh := make(chan mqttapi.Msg)

// 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)...)

// Add subscription for issuing D-Bus commands to the Linux device.
subscriptions = append(subscriptions, system.NewDBusCommandSubscription(ctx))

return &mqttObj{
entities: entities,
subscriptions: subscriptions,
msgCh: msgCh,
}
}
6 changes: 5 additions & 1 deletion internal/agent/runners.go
Original file line number Diff line number Diff line change
Expand Up @@ -165,12 +165,16 @@ func runMQTTWorker(ctx context.Context) {

o := newMQTTObject(ctx)

_, err = mqttapi.NewClient(ctx, prefs, o)
client, err := mqttapi.NewClient(ctx, prefs, o)
if err != nil {
log.Error().Err(err).Msg("Could not start MQTT client.")
return
}

go func() {
o.Run(ctx, client)
}()

log.Debug().Msg("Listening for events on MQTT.")

<-ctx.Done()
Expand Down
121 changes: 121 additions & 0 deletions internal/linux/media/volume.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
// Copyright (c) 2024 Joshua Rich <[email protected]>
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT

package media

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/rs/zerolog/log"

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

type audioDevice struct {
pulseAudio *pulseaudio.Client
topics *mqtthass.Topics
msgCh chan mqttapi.Msg
volume float64
}

func VolumeControl(ctx context.Context, msgCh chan mqttapi.Msg) []*mqtthass.EntityConfig {
var entities []*mqtthass.EntityConfig
client, err := pulseaudio.NewClient()
if err != nil {
log.Warn().Err(err).Msg("Unable to connect to Pulseaudio. Volume control will be unavailable.")
return nil
}

audioDev := &audioDevice{
pulseAudio: client,
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)

if _, err := audioDev.getVolume(); err != nil {
log.Warn().Err(err).Msg("Could not get volume.")
}
go func() {
audioDev.publishVolume()
}()

go func() {
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.")
for {
select {
case <-events:
changed, err := audioDev.getVolume()
if err != nil {
log.Warn().Err(err).Msg("Could not get volume.")
}
if changed {
audioDev.publishVolume()
}
case <-ctx.Done():
return
}
}
}()
return entities
}

func (d *audioDevice) getVolume() (bool, error) {
newVol, err := d.pulseAudio.Volume()
if err != nil {
return false, err
}
if newVol != float32(d.volume) {
d.volume = float64(newVol * 100)
return true, nil
}
return false, nil
}

func (d *audioDevice) setVolume(v float64) error {
if err := d.pulseAudio.SetVolume(float32(v)); err != nil {
return err
}
d.volume = v
return nil
}

func (d *audioDevice) publishVolume() {
msg := mqttapi.NewMsg(d.topics.State, []byte(`{ "value": `+strconv.FormatFloat(d.volume, 'f', -1, 64)+` }`))
d.msgCh <- *msg
}

func (d *audioDevice) parseVolume(_ MQTT.Client, msg MQTT.Message) {
if newValue, err := strconv.ParseFloat(string(msg.Payload()), 64); 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.Warn().Err(err).Msg("Could not set volume level.")
return
}
go func() {
d.publishVolume()
}()
}
}
11 changes: 9 additions & 2 deletions internal/linux/mqtt.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,16 @@ import (
)

func NewButton(entityID string) *mqtthass.EntityConfig {
return mqtthass.NewEntityByID(entityID, preferences.AppName, "homeassistant").
return mqtthass.NewEntityByID(entityID, preferences.AppName, preferences.MQTTTopicPrefix).
AsButton().
WithDefaultOriginInfo().
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())
}

Expand Down
5 changes: 3 additions & 2 deletions internal/preferences/prefs.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@ import (
)

var (
AppName = "go-hass-agent"
AppURL = "https://github.com/joshuar/go-hass-agent"
AppName = "go-hass-agent"
AppURL = "https://github.com/joshuar/go-hass-agent"
MQTTTopicPrefix = "homeassistant"
)

//go:generate sh -c "printf %s $(git tag | tail -1) > VERSION"
Expand Down

0 comments on commit cac7077

Please sign in to comment.