joshuar-go-hass-agent/internal/agent/mqtt_controller_linux.go

191 lines
6.1 KiB
Go

// Copyright (c) 2024 Joshua Rich <joshua.rich@gmail.com>
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
package agent
import (
"context"
"log/slog"
mqtthass "github.com/joshuar/go-hass-anything/v11/pkg/hass"
mqttapi "github.com/joshuar/go-hass-anything/v11/pkg/mqtt"
"github.com/joshuar/go-hass-agent/internal/linux"
"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"
"github.com/joshuar/go-hass-agent/internal/logging"
)
type mqttWorker struct {
msgs chan *mqttapi.Msg
sensors []*mqtthass.SensorEntity
buttons []*mqtthass.ButtonEntity
numbers []*mqtthass.NumberEntity[int]
switches []*mqtthass.SwitchEntity
controls []*mqttapi.Subscription
binarySensors []*mqtthass.BinarySensorEntity
cameras []*mqtthass.ImageEntity
}
type linuxMQTTController struct {
*mqttWorker
logger *slog.Logger
}
// entity is a convienience interface to avoid duplicating a lot of loop content
// when configuring the controller.
type entity interface {
MarshalSubscription() (*mqttapi.Subscription, error)
MarshalConfig() (*mqttapi.Msg, error)
}
func (c *linuxMQTTController) Subscriptions() []*mqttapi.Subscription {
totalLength := len(c.buttons) + len(c.numbers) + len(c.switches) + len(c.cameras)
subs := make([]*mqttapi.Subscription, 0, totalLength)
// Create subscriptions for buttons.
for _, button := range c.buttons {
subs = append(subs, c.generateSubscription(button))
}
// Create subscriptions for numbers.
for _, number := range c.numbers {
subs = append(subs, c.generateSubscription(number))
}
// Create subscriptions for switches.
for _, sw := range c.switches {
subs = append(subs, c.generateSubscription(sw))
}
// Add subscriptions for any additional controls.
subs = append(subs, c.controls...)
return subs
}
func (c *linuxMQTTController) Configs() []*mqttapi.Msg {
totalLength := len(c.sensors) + len(c.binarySensors) + len(c.buttons) + len(c.switches) + len(c.numbers) + len(c.cameras)
configs := make([]*mqttapi.Msg, 0, totalLength)
// Create sensor configs.
for _, sensorEntity := range c.sensors {
configs = append(configs, c.generateConfig(sensorEntity))
}
// Create binary sensor configs.
for _, binarySensorEntity := range c.binarySensors {
configs = append(configs, c.generateConfig(binarySensorEntity))
}
// Create button configs.
for _, buttonEntity := range c.buttons {
configs = append(configs, c.generateConfig(buttonEntity))
}
// Create number configs.
for _, numberEntity := range c.numbers {
configs = append(configs, c.generateConfig(numberEntity))
}
// Create switch configs.
for _, switchEntity := range c.switches {
configs = append(configs, c.generateConfig(switchEntity))
}
// Create camera configs.
for _, cameraEntity := range c.cameras {
configs = append(configs, c.generateConfig(cameraEntity))
}
return configs
}
func (c *linuxMQTTController) Msgs() chan *mqttapi.Msg {
return c.msgs
}
// generateConfig is a helper function to avoid duplicate code around generating
// an entity subscription.
func (c *linuxMQTTController) generateSubscription(e entity) *mqttapi.Subscription {
sub, err := e.MarshalSubscription()
if err != nil {
c.logger.Warn("Could not create subscription.", slog.Any("error", err))
return nil
}
return sub
}
// generateConfig is a helper function to avoid duplicate code around generating
// an entity config.
func (c *linuxMQTTController) generateConfig(e entity) *mqttapi.Msg {
cfg, err := e.MarshalConfig()
if err != nil {
c.logger.Warn("Could not create config.", slog.Any("error", err.Error()))
return nil
}
return cfg
}
// newOSMQTTController initializes the list of MQTT workers for sensors and
// returns those that are supported on this device.
func newOSMQTTController(ctx context.Context, mqttDevice *mqtthass.Device) MQTTController {
ctx = linux.NewContext(ctx)
logger := logging.FromContext(ctx).With(slog.Group("linux", slog.String("controller", "mqtt")))
mqttController := &linuxMQTTController{
mqttWorker: &mqttWorker{
msgs: make(chan *mqttapi.Msg),
},
}
// Add the power controls (suspend, resume, poweroff, etc.).
powerEntities, err := power.NewPowerControl(ctx, mqttDevice)
if err != nil {
logger.Warn("Could not create power controls.", slog.Any("error", err))
} else {
mqttController.buttons = append(mqttController.buttons, powerEntities...)
}
// Add the screen lock controls.
screenControls, err := power.NewScreenLockControl(ctx, mqttDevice)
if err != nil {
logger.Warn("Could not create screen lock controls.", slog.Any("error", err))
} else {
mqttController.buttons = append(mqttController.buttons, screenControls...)
}
// Add the volume controls.
volEntity, muteEntity := media.VolumeControl(ctx, mqttController.Msgs(), mqttDevice)
if volEntity != nil && muteEntity != nil {
mqttController.numbers = append(mqttController.numbers, volEntity)
mqttController.switches = append(mqttController.switches, muteEntity)
}
// Add media control.
mprisEntity, err := media.MPRISControl(ctx, mqttDevice, mqttController.Msgs())
if err != nil {
logger.Warn("Could not activate MPRIS controller.", slog.Any("error", err))
} else {
mqttController.sensors = append(mqttController.sensors, mprisEntity)
}
// Add camera control.
cameraEntities := media.NewCameraControl(ctx, mqttController.Msgs(), mqttDevice)
if cameraEntities != nil {
mqttController.buttons = append(mqttController.buttons, cameraEntities.StartButton, cameraEntities.StopButton)
mqttController.cameras = append(mqttController.cameras, cameraEntities.Images)
mqttController.sensors = append(mqttController.sensors, cameraEntities.Status)
}
// Add the D-Bus command action.
dbusCmdController, err := system.NewDBusCommandSubscription(ctx, mqttDevice)
if err != nil {
logger.Warn("Could not activate D-Bus commands controller.", slog.Any("error", err))
} else {
mqttController.controls = append(mqttController.controls, dbusCmdController)
}
go func() {
defer close(mqttController.msgs)
<-ctx.Done()
}()
return mqttController
}