223 lines
5.0 KiB
Go
223 lines
5.0 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 battery
|
|
|
|
import (
|
|
"context"
|
|
"log/slog"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/godbus/dbus/v5"
|
|
|
|
"github.com/joshuar/go-hass-agent/internal/hass/sensor"
|
|
"github.com/joshuar/go-hass-agent/internal/linux"
|
|
"github.com/joshuar/go-hass-agent/internal/logging"
|
|
"github.com/joshuar/go-hass-agent/pkg/linux/dbusx"
|
|
)
|
|
|
|
type sensorWorker struct {
|
|
bus *dbusx.Bus
|
|
batteryList map[dbus.ObjectPath]context.CancelFunc
|
|
logger *slog.Logger
|
|
mu sync.Mutex
|
|
}
|
|
|
|
// ?: implement initial battery sensor retrieval.
|
|
//
|
|
//revive:disable:unused-receiver
|
|
func (w *sensorWorker) Sensors(_ context.Context) ([]sensor.Entity, error) {
|
|
return nil, linux.ErrUnimplemented
|
|
}
|
|
|
|
func (w *sensorWorker) Events(ctx context.Context) (<-chan sensor.Entity, error) {
|
|
sensorCh := make(chan sensor.Entity)
|
|
|
|
var wg sync.WaitGroup
|
|
|
|
// Get a list of all current connected batteries and monitor them.
|
|
batteries, err := w.getBatteries()
|
|
if err != nil {
|
|
w.logger.Warn("Could not retrieve any battery details from D-Bus.", slog.Any("error", err))
|
|
}
|
|
|
|
for _, path := range batteries {
|
|
wg.Add(1)
|
|
|
|
go func(path dbus.ObjectPath) {
|
|
defer wg.Done()
|
|
|
|
for batterySensor := range w.track(ctx, path) {
|
|
sensorCh <- batterySensor
|
|
}
|
|
}(path)
|
|
}
|
|
|
|
wg.Add(1)
|
|
|
|
go func() {
|
|
defer wg.Done()
|
|
|
|
for batterySensor := range w.monitorBatteryChanges(ctx) {
|
|
sensorCh <- batterySensor
|
|
}
|
|
}()
|
|
|
|
go func() {
|
|
defer close(sensorCh)
|
|
wg.Wait()
|
|
}()
|
|
|
|
return sensorCh, nil
|
|
}
|
|
|
|
// getBatteries is a helper function to retrieve all of the known batteries
|
|
// connected to the system.
|
|
func (w *sensorWorker) getBatteries() ([]dbus.ObjectPath, error) {
|
|
batteryList, err := dbusx.GetData[[]dbus.ObjectPath](w.bus, upowerDBusPath, upowerDBusDest, upowerGetDevicesMethod)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return batteryList, nil
|
|
}
|
|
|
|
func (w *sensorWorker) track(ctx context.Context, batteryPath dbus.ObjectPath) <-chan sensor.Entity {
|
|
sensorCh := make(chan sensor.Entity)
|
|
|
|
var wg sync.WaitGroup
|
|
|
|
battery, err := newBattery(w.bus, w.logger, batteryPath)
|
|
if err != nil {
|
|
w.logger.Warn("Cannot monitor battery.",
|
|
slog.Any("path", batteryPath),
|
|
slog.Any("error", err))
|
|
|
|
return sensorCh
|
|
}
|
|
|
|
battCtx, cancelFunc := context.WithCancel(ctx)
|
|
|
|
w.mu.Lock()
|
|
w.batteryList[batteryPath] = cancelFunc
|
|
w.mu.Unlock()
|
|
|
|
wg.Add(1)
|
|
|
|
go func() {
|
|
defer wg.Done()
|
|
|
|
for prop := range battery.getSensors(battery.sensors...) {
|
|
sensorCh <- prop
|
|
}
|
|
}()
|
|
|
|
wg.Add(1)
|
|
|
|
go func() {
|
|
defer wg.Done()
|
|
|
|
for battery := range monitorBattery(battCtx, battery) {
|
|
sensorCh <- battery
|
|
}
|
|
}()
|
|
|
|
go func() {
|
|
defer close(sensorCh)
|
|
wg.Wait()
|
|
}()
|
|
|
|
return sensorCh
|
|
}
|
|
|
|
func (w *sensorWorker) remove(batteryPath dbus.ObjectPath) {
|
|
if cancelFunc, ok := w.batteryList[batteryPath]; ok {
|
|
cancelFunc()
|
|
w.mu.Lock()
|
|
delete(w.batteryList, batteryPath)
|
|
w.mu.Unlock()
|
|
}
|
|
}
|
|
|
|
// monitorBatteryChanges monitors for battery devices being added/removed from
|
|
// the system and will start/stop monitory each battery as appropriate.
|
|
func (w *sensorWorker) monitorBatteryChanges(ctx context.Context) <-chan sensor.Entity {
|
|
triggerCh, err := dbusx.NewWatch(
|
|
dbusx.MatchPath(upowerDBusPath),
|
|
dbusx.MatchInterface(upowerDBusDest),
|
|
dbusx.MatchMembers(deviceAddedSignal, deviceRemovedSignal),
|
|
).Start(ctx, w.bus)
|
|
if err != nil {
|
|
w.logger.Debug("Unable to set-up D-Bus watch for battery changes.", slog.Any("error", err))
|
|
|
|
return nil
|
|
}
|
|
|
|
sensorCh := make(chan sensor.Entity)
|
|
|
|
// events, err := dbusx.NewWatch(
|
|
// dbusx.MatchPath(upowerDBusPath),
|
|
// dbusx.MatchInterface(upowerDBusDest),
|
|
// dbusx.MatchMember(deviceAddedSignal, deviceRemovedSignal),
|
|
// ).Start(ctx, w.bus)
|
|
// if err != nil {
|
|
// w.logger.Debug("Failed to create D-Bus watch for battery additions/removals.", "error", err.Error())
|
|
// close(sensorCh)
|
|
|
|
// return sensorCh
|
|
// }
|
|
|
|
go func() {
|
|
w.logger.Debug("Monitoring for battery additions/removals.")
|
|
|
|
defer close(sensorCh)
|
|
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
w.logger.Debug("Stopped monitoring for batteries.")
|
|
|
|
return
|
|
case event := <-triggerCh:
|
|
batteryPath, validBatteryPath := event.Content[0].(dbus.ObjectPath)
|
|
if !validBatteryPath {
|
|
continue
|
|
}
|
|
|
|
switch {
|
|
case strings.Contains(event.Signal, deviceAddedSignal):
|
|
go func() {
|
|
for s := range w.track(ctx, batteryPath) {
|
|
sensorCh <- s
|
|
}
|
|
}()
|
|
case strings.Contains(event.Signal, deviceRemovedSignal):
|
|
w.remove(batteryPath)
|
|
}
|
|
}
|
|
}
|
|
}()
|
|
|
|
return sensorCh
|
|
}
|
|
|
|
func NewBatteryWorker(ctx context.Context) (*linux.EventSensorWorker, error) {
|
|
worker := linux.NewEventSensorWorker(workerID)
|
|
|
|
bus, ok := linux.CtxGetSystemBus(ctx)
|
|
if !ok {
|
|
return worker, linux.ErrNoSystemBus
|
|
}
|
|
|
|
worker.EventSensorType = &sensorWorker{
|
|
batteryList: make(map[dbus.ObjectPath]context.CancelFunc),
|
|
bus: bus,
|
|
logger: logging.FromContext(ctx).With(slog.String("worker", workerID)),
|
|
}
|
|
|
|
return worker, nil
|
|
}
|