joshuar-go-hass-agent/internal/linux/power/inhibit.go

187 lines
4.5 KiB
Go

// Copyright 2025 Joshua Rich <joshua.rich@gmail.com>.
// SPDX-License-Identifier: MIT
package power
import (
"context"
"encoding/json"
"errors"
"fmt"
"log/slog"
"syscall"
"github.com/eclipse/paho.golang/paho"
mqtthass "github.com/joshuar/go-hass-anything/v12/pkg/hass"
mqttapi "github.com/joshuar/go-hass-anything/v12/pkg/mqtt"
"github.com/joshuar/go-hass-agent/internal/components/logging"
"github.com/joshuar/go-hass-agent/internal/components/preferences"
"github.com/joshuar/go-hass-agent/internal/linux"
"github.com/joshuar/go-hass-agent/pkg/linux/dbusx"
)
const (
inhibitWorkerID = "inhibit_control"
inhibitWorkerPrefID = controlsPrefPrefix + "inhibit_controls"
)
var ErrInitInhibitWorker = errors.New("could not init inhibit control worker")
type inhibitControlWorker struct {
prefs *preferences.CommonWorkerPrefs
entity *mqtthass.SwitchEntity
fd int
logger *slog.Logger
msgCh chan *mqttapi.Msg
bus *dbusx.Bus
}
func (w *inhibitControlWorker) PreferencesID() string {
return inhibitWorkerPrefID
}
func (w *inhibitControlWorker) DefaultPreferences() preferences.CommonWorkerPrefs {
return preferences.CommonWorkerPrefs{}
}
// inhibitStateCallback is executed when the inhibit state is read on MQTT.
func (w *inhibitControlWorker) inhibitStateCallback(_ ...any) (json.RawMessage, error) {
if w.fd > 0 {
return json.RawMessage(`ON`), nil
}
return json.RawMessage(`OFF`), nil
}
// inhibitCommandCallback is executed when the inhibit control is toggled.
func (w *inhibitControlWorker) inhibitCommandCallback(p *paho.Publish) {
var err error
state := string(p.Payload)
switch state {
case "ON":
err = w.createInhibitLock()
case "OFF":
err = w.releaseInhibitLock()
}
if err != nil {
w.logger.Error("Could not set inhibit state.",
slog.Any("error", err))
return
}
go func() {
if err := w.publishState(w.msgCh); err != nil {
w.logger.Error("Failed to publish mute state to MQTT.", slog.Any("error", err))
}
}()
}
// publishState will publish on MQTT the current state of the inhibit lock
// controlled by the worker.
func (w *inhibitControlWorker) publishState(msgCh chan *mqttapi.Msg) error {
msg, err := w.entity.MarshalState()
if err != nil {
return fmt.Errorf("could not marshal entity state: %w", err)
}
msgCh <- msg
return nil
}
// releaseInhibitLock will release any fd file lock the inhibit worker has been
// granted.
func (w *inhibitControlWorker) releaseInhibitLock() error {
if err := syscall.Close(w.fd); err != nil {
return fmt.Errorf("error closing inhibit file descriptor lock: %w", err)
}
return nil
}
// createInhibitLock will create an inhibit lock for the worker.
func (w *inhibitControlWorker) createInhibitLock() error {
fd, err := dbusx.GetData[int](w.bus,
"/org/freedesktop/login1",
"org.freedesktop.login1",
"org.freedesktop.login1.Manager.Inhibit",
"sleep:shutdown",
preferences.AppName,
"User requested",
"block",
)
if err != nil {
return fmt.Errorf("could not create inhibit lock: %w", err)
}
w.fd = fd
return nil
}
//nolint:nilnil
func NewInhibitControl(ctx context.Context, msgCh chan *mqttapi.Msg, device *mqtthass.Device) (*mqtthass.SwitchEntity, error) {
var err error
worker := &inhibitControlWorker{
logger: logging.FromContext(ctx).WithGroup(inhibitWorkerID),
msgCh: msgCh,
}
// Create an MQTT switch entity for toggling the inhibit lock.
worker.entity = mqtthass.NewSwitchEntity().
OptimisticMode().
WithDetails(
mqtthass.App(preferences.AppName),
mqtthass.Name("Inhibit Sleep/Shutdown"),
mqtthass.ID(device.Name+"_inhibit_lock"),
mqtthass.DeviceInfo(device),
mqtthass.Icon("mdi:lock"),
).
WithState(
mqtthass.StateCallback(worker.inhibitStateCallback),
mqtthass.ValueTemplate("{{ value }}"),
).
WithCommand(
mqtthass.CommandCallback(worker.inhibitCommandCallback),
)
worker.prefs, err = preferences.LoadWorker(worker)
if err != nil {
return nil, errors.Join(ErrInitInhibitWorker, err)
}
if worker.prefs.IsDisabled() {
return nil, nil
}
bus, ok := linux.CtxGetSystemBus(ctx)
if !ok {
return nil, errors.Join(linux.ErrNoSystemBus, err)
}
worker.bus = bus
// On agent shutdown, release any inhibit lock currently held.
go func() {
<-ctx.Done()
if err := worker.releaseInhibitLock(); err != nil {
worker.logger.Error("Could not release inhibit state.",
slog.Any("error", err))
}
}()
go func() {
if err := worker.publishState(worker.msgCh); err != nil {
worker.logger.Warn("Could not publish initial inhibit state.",
slog.Any("error", err))
}
}()
return worker.entity, nil
}