From 79763b9ececd8ae79db23700fef8626c9cb4a13c Mon Sep 17 00:00:00 2001 From: Joshua Rich Date: Sat, 8 Jun 2024 17:17:23 +1000 Subject: [PATCH] refactor(linux): :recycle: improve readability and reduce lint issues --- internal/linux/apps/activeApps.go | 15 +- internal/linux/apps/apps.go | 16 ++- internal/linux/apps/runningApps.go | 36 +++-- internal/linux/battery/battery.go | 166 +++++++++++++++------- internal/linux/cpu/loadAvgs.go | 39 ++--- internal/linux/cpu/usage.go | 28 ++-- internal/linux/desktop/desktop.go | 85 +++++++---- internal/linux/device.go | 11 +- internal/linux/device_test.go | 17 ++- internal/linux/disk/diskRates.go | 64 ++++++--- internal/linux/disk/diskUsage.go | 43 +++--- internal/linux/media/volume.go | 29 +++- internal/linux/mem/memUsage.go | 98 +++++++------ internal/linux/net/networkConnection.go | 133 ++++++++++++----- internal/linux/net/networkRates.go | 53 ++++--- internal/linux/net/wifiProps.go | 32 ++++- internal/linux/power/idle.go | 51 +++++-- internal/linux/power/laptop.go | 27 +++- internal/linux/power/powerControl.go | 19 +-- internal/linux/power/powerProfile.go | 27 ++-- internal/linux/power/powerState.go | 25 +++- internal/linux/power/screenLock.go | 15 +- internal/linux/power/screenLockControl.go | 4 + internal/linux/problems/problems.go | 37 +++-- internal/linux/system/dbusCommand.go | 4 +- internal/linux/system/hwmon.go | 64 +++++---- internal/linux/system/info.go | 1 + internal/linux/system/time.go | 28 ++-- internal/linux/user/users.go | 59 +++++--- 29 files changed, 822 insertions(+), 404 deletions(-) diff --git a/internal/linux/apps/activeApps.go b/internal/linux/apps/activeApps.go index c5f6e47ac..4c758473d 100644 --- a/internal/linux/apps/activeApps.go +++ b/internal/linux/apps/activeApps.go @@ -20,6 +20,7 @@ func (a *activeAppSensor) app() string { if app, ok := a.State().(string); ok { return app } + return "" } @@ -28,17 +29,21 @@ func (a *activeAppSensor) update(l map[string]dbus.Variant) sensor.Details { if appState, ok := v.Value().(uint32); ok { if appState == 2 && a.app() != app { a.Value = app + return a } } } + return nil } +//nolint:exhaustruct func newActiveAppSensor() *activeAppSensor { - s := &activeAppSensor{} - s.SensorSrc = linux.DataSrcDbus - s.SensorTypeValue = linux.SensorAppActive - s.IconString = "mdi:application" - return s + newSensor := &activeAppSensor{} + newSensor.SensorSrc = linux.DataSrcDbus + newSensor.SensorTypeValue = linux.SensorAppActive + newSensor.IconString = "mdi:application" + + return newSensor } diff --git a/internal/linux/apps/apps.go b/internal/linux/apps/apps.go index dc9b3a58a..f05f267ce 100644 --- a/internal/linux/apps/apps.go +++ b/internal/linux/apps/apps.go @@ -7,7 +7,6 @@ package apps import ( "context" - "errors" "fmt" "github.com/godbus/dbus/v5" @@ -32,6 +31,7 @@ type worker struct { portalDest string } +//nolint:exhaustruct func (w *worker) Setup(_ context.Context) *dbusx.Watch { return &dbusx.Watch{ Bus: dbusx.SessionBus, @@ -48,8 +48,10 @@ func (w *worker) Watch(ctx context.Context, triggerCh chan dbusx.Trigger) chan s appSensors, err := w.Sensors(ctx) if err != nil { log.Warn().Err(err).Msg("Failed to update app sensors.") + return } + for _, s := range appSensors { sensorCh <- s } @@ -58,10 +60,12 @@ func (w *worker) Watch(ctx context.Context, triggerCh chan dbusx.Trigger) chan s // Watch for active app changes. go func() { defer close(sensorCh) + for { select { case <-ctx.Done(): log.Debug().Msg("Stopped app sensor.") + return case <-triggerCh: sendSensors(ctx, sensorCh) @@ -79,26 +83,30 @@ func (w *worker) Watch(ctx context.Context, triggerCh chan dbusx.Trigger) chan s func (w *worker) Sensors(ctx context.Context) ([]sensor.Details, error) { var sensors []sensor.Details + appList, err := dbusx.GetData[map[string]dbus.Variant](ctx, dbusx.SessionBus, appStateDBusPath, w.portalDest, appStateDBusMethod) if err != nil { return nil, fmt.Errorf("could not retrieve app list from D-Bus: %w", err) } + if appList != nil { if s := w.activeApp.update(appList); s != nil { sensors = append(sensors, s) } + if s := w.runningApps.update(appList); s != nil { sensors = append(sensors, s) } } + return sensors, nil } func NewAppWorker() (*linux.SensorWorker, error) { // If we cannot find a portal interface, we cannot monitor the active app. - portalDest := linux.FindPortal() - if portalDest == "" { - return nil, errors.New("unable to monitor for active applications: no portal present") + portalDest, err := linux.FindPortal() + if err != nil { + return nil, fmt.Errorf("unable to monitor for active applications: %w", err) } return &linux.SensorWorker{ diff --git a/internal/linux/apps/runningApps.go b/internal/linux/apps/runningApps.go index f9115b5ce..ebfe6d74a 100644 --- a/internal/linux/apps/runningApps.go +++ b/internal/linux/apps/runningApps.go @@ -23,12 +23,14 @@ type runningAppsSensor struct { } type runningAppsSensorAttributes struct { - DataSource string `json:"Data Source"` - RunningApps []string `json:"Running Apps"` + DataSource string `json:"data_source"` + RunningApps []string `json:"running_apps"` } +//nolint:exhaustruct func (r *runningAppsSensor) Attributes() any { attrs := &runningAppsSensorAttributes{} + r.mu.Lock() for appName, state := range r.appList { if dbusx.VariantToValue[uint32](state) > 0 { @@ -36,7 +38,9 @@ func (r *runningAppsSensor) Attributes() any { } } r.mu.Unlock() + attrs.DataSource = linux.DataSrcDbus + return attrs } @@ -44,33 +48,41 @@ func (r *runningAppsSensor) count() int { if count, ok := r.State().(int); ok { return count } + return -1 } -func (r *runningAppsSensor) update(l map[string]dbus.Variant) sensor.Details { +func (r *runningAppsSensor) update(apps map[string]dbus.Variant) sensor.Details { var count int + r.mu.Lock() defer r.mu.Unlock() - r.appList = l - for _, raw := range l { - if appState, ok := raw.Value().(uint32); ok { + r.appList = apps + + for _, appState := range apps { + if appState, ok := appState.Value().(uint32); ok { if appState > 0 { count++ } } } + if r.count() != count { r.Value = count + return r } + return nil } +//nolint:exhaustruct func newRunningAppsSensor() *runningAppsSensor { - s := &runningAppsSensor{} - s.SensorTypeValue = linux.SensorAppRunning - s.IconString = "mdi:apps" - s.UnitsString = "apps" - s.StateClassValue = types.StateClassMeasurement - return s + newSensor := &runningAppsSensor{} + newSensor.SensorTypeValue = linux.SensorAppRunning + newSensor.IconString = "mdi:apps" + newSensor.UnitsString = "apps" + newSensor.StateClassValue = types.StateClassMeasurement + + return newSensor } diff --git a/internal/linux/battery/battery.go b/internal/linux/battery/battery.go index ecb78805d..e3a018c70 100644 --- a/internal/linux/battery/battery.go +++ b/internal/linux/battery/battery.go @@ -36,6 +36,8 @@ const ( batteryIcon = "mdi:battery" ) +var ErrInvalidBattery = errors.New("invalid battery") + // dBusSensorToProps is a map of battery sensors to their D-Bus properties. var dBusSensorToProps = map[linux.SensorTypeValue]string{ linux.SensorBattType: upowerDBusDeviceDest + ".Type", @@ -72,64 +74,74 @@ type upowerBattery struct { // getProp retrieves the property from D-Bus that matches the given battery sensor type. func (b *upowerBattery) getProp(ctx context.Context, t linux.SensorTypeValue) (dbus.Variant, error) { if !b.dBusPath.IsValid() { - return dbus.MakeVariant(""), errors.New("invalid battery path") + return dbus.MakeVariant(""), ErrInvalidBattery } + return dbusx.GetProp[dbus.Variant](ctx, dbusx.SystemBus, string(b.dBusPath), upowerDBusDest, dBusSensorToProps[t]) } // getSensors retrieves the sensors passed in for a given battery. func (b *upowerBattery) getSensors(ctx context.Context, sensors ...linux.SensorTypeValue) chan sensor.Details { sensorCh := make(chan sensor.Details, len(sensors)) - for _, s := range sensors { - value, err := b.getProp(ctx, s) + defer close(sensorCh) + + for _, batterySensor := range sensors { + value, err := b.getProp(ctx, batterySensor) if err != nil { - log.Warn().Err(err).Str("battery", string(b.dBusPath)).Str("sensor", s.String()).Msg("Could not retrieve battery sensor.") + log.Warn().Err(err).Str("battery", string(b.dBusPath)).Str("sensor", batterySensor.String()).Msg("Could not retrieve battery sensor.") + continue } - sensorCh <- newBatterySensor(ctx, b, s, value) + sensorCh <- newBatterySensor(ctx, b, batterySensor, value) } - close(sensorCh) + return sensorCh } // newBattery creates a battery object that will have a number of properties to // be treated as sensors in Home Assistant. +// +//nolint:exhaustruct,mnd func newBattery(ctx context.Context, path dbus.ObjectPath) (*upowerBattery, error) { - b := &upowerBattery{ + battery := &upowerBattery{ dBusPath: path, } // Get the battery type. Depending on the value, additional sensors will be added. - battType, err := b.getProp(ctx, linux.SensorBattType) + battType, err := battery.getProp(ctx, linux.SensorBattType) if err != nil { - return nil, fmt.Errorf("could not determine battery type: %v", err) + return nil, fmt.Errorf("could not determine battery type: %w", err) } - b.battType = dbusx.VariantToValue[batteryType](battType) + + battery.battType = dbusx.VariantToValue[batteryType](battType) // use the native path D-Bus property for the battery id. - id, err := b.getProp(ctx, linux.SensorBattNativePath) + id, err := battery.getProp(ctx, linux.SensorBattNativePath) if err != nil { - return nil, fmt.Errorf("could not retrieve battery path in D-Bus: %v", err) + return nil, fmt.Errorf("could not retrieve battery path in D-Bus: %w", err) } - b.id = dbusx.VariantToValue[string](id) - model, err := b.getProp(ctx, linux.SensorBattModel) + battery.id = dbusx.VariantToValue[string](id) + + model, err := battery.getProp(ctx, linux.SensorBattModel) if err != nil { - log.Warn().Err(err).Str("battery", string(b.dBusPath)).Msg("Could not determine battery model.") + log.Warn().Err(err).Str("battery", string(battery.dBusPath)).Msg("Could not determine battery model.") } - b.model = dbusx.VariantToValue[string](model) + + battery.model = dbusx.VariantToValue[string](model) // At a minimum, monitor the battery type and the charging state. - b.sensors = append(b.sensors, linux.SensorBattState) + battery.sensors = append(battery.sensors, linux.SensorBattState) if dbusx.VariantToValue[uint32](battType) == 2 { // Battery has charge percentage, temp and charging rate sensors - b.sensors = append(b.sensors, linux.SensorBattPercentage, linux.SensorBattTemp, linux.SensorBattEnergyRate) + battery.sensors = append(battery.sensors, linux.SensorBattPercentage, linux.SensorBattTemp, linux.SensorBattEnergyRate) } else { // Battery has a textual level sensor - b.sensors = append(b.sensors, linux.SensorBattLevel) + battery.sensors = append(battery.sensors, linux.SensorBattLevel) } - return b, nil + + return battery, nil } type upowerBatterySensor struct { @@ -145,6 +157,7 @@ func (s *upowerBatterySensor) Name() string { if s.model == "" { return s.batteryID + " " + s.SensorTypeValue.String() } + return s.model + " " + s.SensorTypeValue.String() } @@ -152,6 +165,7 @@ func (s *upowerBatterySensor) ID() string { return s.batteryID + "_" + strings.ToLower(strcase.ToSnake(s.SensorTypeValue.String())) } +//nolint:exhaustive func (s *upowerBatterySensor) Icon() string { switch s.SensorTypeValue { case linux.SensorBattPercentage: @@ -163,6 +177,7 @@ func (s *upowerBatterySensor) Icon() string { } } +//nolint:exhaustive func (s *upowerBatterySensor) DeviceClass() types.DeviceClass { switch s.SensorTypeValue { case linux.SensorBattPercentage: @@ -176,6 +191,7 @@ func (s *upowerBatterySensor) DeviceClass() types.DeviceClass { } } +//nolint:exhaustive func (s *upowerBatterySensor) StateClass() types.StateClass { switch s.SensorTypeValue { case linux.SensorBattPercentage, linux.SensorBattTemp, linux.SensorBattEnergyRate: @@ -185,10 +201,12 @@ func (s *upowerBatterySensor) StateClass() types.StateClass { } } +//nolint:exhaustive func (s *upowerBatterySensor) State() any { if s.Value == nil { return sensor.StateUnknown } + switch s.SensorTypeValue { case linux.SensorBattVoltage, linux.SensorBattTemp, linux.SensorBattEnergy, linux.SensorBattEnergyRate, linux.SensorBattPercentage: if value, ok := s.Value.(float64); !ok { @@ -217,6 +235,7 @@ func (s *upowerBatterySensor) State() any { } } +//nolint:exhaustive func (s *upowerBatterySensor) Units() string { switch s.SensorTypeValue { case linux.SensorBattPercentage: @@ -234,21 +253,24 @@ func (s *upowerBatterySensor) Attributes() any { return s.attributes } -func (s *upowerBatterySensor) generateAttributes(ctx context.Context, b *upowerBattery) { +//nolint:exhaustive +func (s *upowerBatterySensor) generateAttributes(ctx context.Context, battery *upowerBattery) { switch s.SensorTypeValue { case linux.SensorBattEnergyRate: - voltage, err := b.getProp(ctx, linux.SensorBattVoltage) + voltage, err := battery.getProp(ctx, linux.SensorBattVoltage) if err != nil { - log.Warn().Err(err).Str("battery", string(b.dBusPath)).Msg("Could not retrieve battery voltage.") + log.Warn().Err(err).Str("battery", string(battery.dBusPath)).Msg("Could not retrieve battery voltage.") } - energy, err := b.getProp(ctx, linux.SensorBattEnergy) + + energy, err := battery.getProp(ctx, linux.SensorBattEnergy) if err != nil { - log.Warn().Err(err).Str("battery", string(b.dBusPath)).Msg("Could not retrieve battery energy.") + log.Warn().Err(err).Str("battery", string(battery.dBusPath)).Msg("Could not retrieve battery energy.") } + s.attributes = &struct { - DataSource string `json:"Data Source"` - Voltage float64 `json:"Voltage"` - Energy float64 `json:"Energy"` + DataSource string `json:"data_source"` + Voltage float64 `json:"voltage"` + Energy float64 `json:"energy"` }{ Voltage: dbusx.VariantToValue[float64](voltage), Energy: dbusx.VariantToValue[float64](energy), @@ -256,10 +278,10 @@ func (s *upowerBatterySensor) generateAttributes(ctx context.Context, b *upowerB } case linux.SensorBattPercentage, linux.SensorBattLevel: s.attributes = &struct { - Type string `json:"Battery Type"` - DataSource string `json:"Data Source"` + Type string `json:"battery_type"` + DataSource string `json:"data_source"` }{ - Type: b.battType.String(), + Type: battery.battType.String(), DataSource: linux.DataSrcDbus, } } @@ -267,16 +289,19 @@ func (s *upowerBatterySensor) generateAttributes(ctx context.Context, b *upowerB // newBatterySensor creates a new sensor for Home Assistant from a battery // property. -func newBatterySensor(ctx context.Context, b *upowerBattery, t linux.SensorTypeValue, v dbus.Variant) *upowerBatterySensor { - s := &upowerBatterySensor{ - batteryID: b.id, - model: b.model, +// +//nolint:exhaustruct,lll +func newBatterySensor(ctx context.Context, battery *upowerBattery, sensorType linux.SensorTypeValue, value dbus.Variant) *upowerBatterySensor { + batterySensor := &upowerBatterySensor{ + batteryID: battery.id, + model: battery.model, } - s.SensorTypeValue = t - s.Value = v.Value() - s.IsDiagnostic = true - s.generateAttributes(ctx, b) - return s + batterySensor.SensorTypeValue = sensorType + batterySensor.Value = value.Value() + batterySensor.IsDiagnostic = true + batterySensor.generateAttributes(ctx, battery) + + return batterySensor } type batteryTracker struct { @@ -284,28 +309,33 @@ type batteryTracker struct { mu sync.Mutex } -func (t *batteryTracker) track(ctx context.Context, p dbus.ObjectPath) <-chan sensor.Details { - battery, err := newBattery(ctx, p) +func (t *batteryTracker) track(ctx context.Context, batteryPath dbus.ObjectPath) <-chan sensor.Details { + battery, err := newBattery(ctx, batteryPath) if err != nil { log.Warn().Err(err).Msg("Cannot monitory battery.") + return nil } + battCtx, cancelFunc := context.WithCancel(ctx) + t.mu.Lock() - t.batteryList[p] = cancelFunc + t.batteryList[batteryPath] = cancelFunc t.mu.Unlock() + return sensor.MergeSensorCh(battCtx, battery.getSensors(battCtx, battery.sensors...), monitorBattery(battCtx, battery)) } -func (t *batteryTracker) remove(p dbus.ObjectPath) { - if cancelFunc, ok := t.batteryList[p]; ok { +func (t *batteryTracker) remove(batteryPath dbus.ObjectPath) { + if cancelFunc, ok := t.batteryList[batteryPath]; ok { cancelFunc() t.mu.Lock() - delete(t.batteryList, p) + delete(t.batteryList, batteryPath) t.mu.Unlock() } } +//nolint:exhaustruct func newBatteryTracker() *batteryTracker { return &batteryTracker{ batteryList: make(map[dbus.ObjectPath]context.CancelFunc), @@ -319,13 +349,17 @@ func getBatteries(ctx context.Context) ([]dbus.ObjectPath, error) { if err != nil { return nil, err } + return batteryList, nil } // monitorBattery will monitor a battery device for any property changes and // send these as sensors. +// +//nolint:exhaustruct func monitorBattery(ctx context.Context, battery *upowerBattery) <-chan sensor.Details { log.Debug().Str("battery", battery.id).Msg("Monitoring battery.") + sensorCh := make(chan sensor.Details) // Create a DBus signal match to watch for property changes for this // battery. @@ -339,21 +373,27 @@ func monitorBattery(ctx context.Context, battery *upowerBattery) <-chan sensor.D log.Debug().Err(err). Msg("Failed to create battery props D-Bus watch.") close(sensorCh) + return sensorCh } + go func() { defer close(sensorCh) + for { select { case <-ctx.Done(): log.Debug().Str("battery", battery.id).Msg("Stopped monitoring battery.") + return case event := <-events: props, err := dbusx.ParsePropertiesChanged(event.Content) if err != nil { log.Warn().Err(err).Msg("Did not understand received trigger.") + continue } + for prop, value := range props.Changed { if s, ok := dBusPropToSensor[prop]; ok { sensorCh <- newBatterySensor(ctx, battery, s, value) @@ -362,12 +402,15 @@ func monitorBattery(ctx context.Context, battery *upowerBattery) <-chan sensor.D } } }() + return sensorCh } // monitorBatteryChanges monitors for battery devices being added/removed from // the system and will start/stop monitory each battery as appropriate. -func monitorBatteryChanges(ctx context.Context, t *batteryTracker) <-chan sensor.Details { +// +//nolint:exhaustruct +func monitorBatteryChanges(ctx context.Context, tracker *batteryTracker) <-chan sensor.Details { sensorCh := make(chan sensor.Details) events, err := dbusx.WatchBus(ctx, &dbusx.Watch{ @@ -380,56 +423,66 @@ func monitorBatteryChanges(ctx context.Context, t *batteryTracker) <-chan sensor log.Debug().Err(err). Msg("Failed to create battery state D-Bus watch.") close(sensorCh) + return sensorCh } go func() { defer close(sensorCh) + for { select { case <-ctx.Done(): log.Debug().Msg("Stopped monitoring for batteries.") + return case event := <-events: batteryPath, validBatteryPath := event.Content[0].(dbus.ObjectPath) if !validBatteryPath { continue } + switch { case strings.Contains(event.Signal, deviceAddedSignal): go func() { - for s := range t.track(ctx, batteryPath) { + for s := range tracker.track(ctx, batteryPath) { sensorCh <- s } }() case strings.Contains(event.Signal, deviceRemovedSignal): - t.remove(batteryPath) + tracker.remove(batteryPath) } } } }() + return sensorCh } +//nolint:mnd func batteryPercentIcon(v any) string { - pc, ok := v.(float64) + percentage, ok := v.(float64) if !ok { return batteryIcon + "-unknown" } - if pc >= 95 { + + if percentage >= 95 { return batteryIcon } - return fmt.Sprintf("%s-%d", batteryIcon, int(math.Round(pc/10)*10)) + + return fmt.Sprintf("%s-%d", batteryIcon, int(math.Round(percentage/10)*10)) } func batteryChargeIcon(v any) string { - er, ok := v.(float64) + energyRate, ok := v.(float64) if !ok { return batteryIcon } - if math.Signbit(er) { + + if math.Signbit(energyRate) { return batteryIcon + "-minus" } + return batteryIcon + "-plus" } @@ -437,11 +490,13 @@ type worker struct{} // TODO: implement initial battery sensor retrieval. func (w *worker) Sensors(_ context.Context) ([]sensor.Details, error) { - return nil, errors.New("unimplemented") + return nil, linux.ErrUnimplemented } +//nolint:prealloc func (w *worker) Events(ctx context.Context) chan sensor.Details { batteryTracker := newBatteryTracker() + var sensorCh []<-chan sensor.Details // Get a list of all current connected batteries and monitor them. @@ -449,6 +504,7 @@ func (w *worker) Events(ctx context.Context) chan sensor.Details { if err != nil { log.Warn().Err(err).Msg("Could not retrieve battery list. Cannot find any existing batteries.") } + for _, path := range batteries { sensorCh = append(sensorCh, batteryTracker.track(ctx, path)) } diff --git a/internal/linux/cpu/loadAvgs.go b/internal/linux/cpu/loadAvgs.go index e9bb20181..fe461a609 100644 --- a/internal/linux/cpu/loadAvgs.go +++ b/internal/linux/cpu/loadAvgs.go @@ -20,6 +20,9 @@ import ( const ( loadAvgIcon = "mdi:chip" loadAvgUnit = "load" + + loadAvgUpdateInterval = time.Minute + loadAvgUpdateJitter = 5 * time.Second ) type loadavgSensor struct { @@ -28,37 +31,41 @@ type loadavgSensor struct { type loadAvgsSensorWorker struct{} -func (w *loadAvgsSensorWorker) Interval() time.Duration { return time.Minute } +func (w *loadAvgsSensorWorker) Interval() time.Duration { return loadAvgUpdateInterval } -func (w *loadAvgsSensorWorker) Jitter() time.Duration { return 5 * time.Second } +func (w *loadAvgsSensorWorker) Jitter() time.Duration { return loadAvgUpdateJitter } +//nolint:exhaustive,exhaustruct,mnd func (w *loadAvgsSensorWorker) Sensors(ctx context.Context, _ time.Duration) ([]sensor.Details, error) { - var sensors []sensor.Details + sensors := make([]sensor.Details, 0, 3) - latest, err := load.AvgWithContext(ctx) + loadAvgs, err := load.AvgWithContext(ctx) if err != nil { return nil, fmt.Errorf("problem fetching load averages: %w", err) } for _, loadType := range []linux.SensorTypeValue{linux.SensorLoad1, linux.SensorLoad5, linux.SensorLoad15} { - l := &loadavgSensor{} - l.IconString = loadAvgIcon - l.UnitsString = loadAvgUnit - l.SensorSrc = linux.DataSrcProcfs - l.StateClassValue = types.StateClassMeasurement + newSensor := &loadavgSensor{} + newSensor.IconString = loadAvgIcon + newSensor.UnitsString = loadAvgUnit + newSensor.SensorSrc = linux.DataSrcProcfs + newSensor.StateClassValue = types.StateClassMeasurement + switch loadType { case linux.SensorLoad1: - l.Value = latest.Load1 - l.SensorTypeValue = linux.SensorLoad1 + newSensor.Value = loadAvgs.Load1 + newSensor.SensorTypeValue = linux.SensorLoad1 case linux.SensorLoad5: - l.Value = latest.Load5 - l.SensorTypeValue = linux.SensorLoad5 + newSensor.Value = loadAvgs.Load5 + newSensor.SensorTypeValue = linux.SensorLoad5 case linux.SensorLoad15: - l.Value = latest.Load15 - l.SensorTypeValue = linux.SensorLoad15 + newSensor.Value = loadAvgs.Load15 + newSensor.SensorTypeValue = linux.SensorLoad15 } - sensors = append(sensors, l) + + sensors = append(sensors, newSensor) } + return sensors, nil } diff --git a/internal/linux/cpu/usage.go b/internal/linux/cpu/usage.go index 95a478d42..57b5f1bec 100644 --- a/internal/linux/cpu/usage.go +++ b/internal/linux/cpu/usage.go @@ -17,29 +17,37 @@ import ( "github.com/joshuar/go-hass-agent/internal/linux" ) +const ( + usageUpdateInterval = 10 * time.Second + usageUpdateJitter = time.Second +) + type cpuUsageSensor struct { linux.Sensor } type usageWorker struct{} -func (w *usageWorker) Interval() time.Duration { return 10 * time.Second } +func (w *usageWorker) Interval() time.Duration { return usageUpdateInterval } -func (w *usageWorker) Jitter() time.Duration { return time.Second } +func (w *usageWorker) Jitter() time.Duration { return usageUpdateJitter } +//nolint:exhaustruct func (w *usageWorker) Sensors(ctx context.Context, d time.Duration) ([]sensor.Details, error) { usage, err := cpu.PercentWithContext(ctx, d, false) if err != nil { return nil, fmt.Errorf("could not retrieve CPU usage: %w", err) } - s := &cpuUsageSensor{} - s.IconString = "mdi:chip" - s.UnitsString = "%" - s.SensorSrc = linux.DataSrcProcfs - s.StateClassValue = types.StateClassMeasurement - s.Value = usage[0] - s.SensorTypeValue = linux.SensorCPUPc - return []sensor.Details{s}, nil + + newSensor := &cpuUsageSensor{} + newSensor.IconString = "mdi:chip" + newSensor.UnitsString = "%" + newSensor.SensorSrc = linux.DataSrcProcfs + newSensor.StateClassValue = types.StateClassMeasurement + newSensor.Value = usage[0] + newSensor.SensorTypeValue = linux.SensorCPUPc + + return []sensor.Details{newSensor}, nil } func NewUsageWorker() (*linux.SensorWorker, error) { diff --git a/internal/linux/desktop/desktop.go b/internal/linux/desktop/desktop.go index 0c4ee91c1..8260c3f3e 100644 --- a/internal/linux/desktop/desktop.go +++ b/internal/linux/desktop/desktop.go @@ -9,7 +9,6 @@ package desktop import ( "context" - "errors" "fmt" "strings" "time" @@ -32,6 +31,8 @@ const ( settingsChangedSignal = "SettingChanged" colorSchemeProp = "color-scheme" accentColorProp = "accent-color" + + reqTimeout = 15 * time.Second ) type desktopSettingSensor struct { @@ -40,6 +41,7 @@ type desktopSettingSensor struct { type worker struct{} +//nolint:exhaustruct func (w *worker) Setup(_ context.Context) *dbusx.Watch { return &dbusx.Watch{ Bus: dbusx.SessionBus, @@ -54,25 +56,32 @@ func (w *worker) Watch(ctx context.Context, triggerCh chan dbusx.Trigger) chan s go func() { defer close(sensorCh) + for { select { case <-ctx.Done(): log.Debug().Msg("Stopped desktop settings sensors.") + return case event := <-triggerCh: if !strings.Contains(event.Signal, settingsChangedSignal) { continue } + prop, ok := event.Content[1].(string) if !ok { log.Warn().Msg("Didn't understand changed property.") + continue } + value, ok := event.Content[2].(dbus.Variant) if !ok { log.Warn().Msg("Didn't understand changed property value.") + continue } + switch prop { case colorSchemeProp: s := parseColorScheme(value) @@ -90,28 +99,34 @@ func (w *worker) Watch(ctx context.Context, triggerCh chan dbusx.Trigger) chan s if err != nil { log.Warn().Err(err).Msg("Could not get initial sensor updates.") } + for _, s := range sensors { sensorCh <- s } }() + return sensorCh } +//nolint:mnd func (w *worker) Sensors(ctx context.Context) ([]sensor.Details, error) { - var sensors []sensor.Details - reqCtx, cancelReq := context.WithTimeout(ctx, 15*time.Second) + reqCtx, cancelReq := context.WithTimeout(ctx, reqTimeout) defer cancelReq() + + sensors := make([]sensor.Details, 0, 2) + sensors = append(sensors, newAccentColorSensor(reqCtx, ""), newColorSchemeSensor(reqCtx, "")) + return sensors, nil } func NewDesktopWorker() (*linux.SensorWorker, error) { // If we cannot find a portal interface, we cannot monitor desktop settings. - portalDest := linux.FindPortal() - if portalDest == "" { - return nil, errors.New("unable to monitor for desktop settings: no portal present") + _, err := linux.FindPortal() + if err != nil { + return nil, fmt.Errorf("unable to monitor for desktop settings: %w", err) } return &linux.SensorWorker{ @@ -122,6 +137,7 @@ func NewDesktopWorker() (*linux.SensorWorker, error) { nil } +//nolint:mnd func parseColorScheme(value dbus.Variant) string { scheme := dbusx.VariantToValue[uint32](value) switch scheme { @@ -134,70 +150,83 @@ func parseColorScheme(value dbus.Variant) string { } } +//nolint:mnd func parseAccentColor(value dbus.Variant) string { values := dbusx.VariantToValue[[]any](value) rgb := make([]uint8, 3) - for i, v := range values { + + for colour, v := range values { val, ok := v.(float64) if !ok { continue } - rgb[i] = srgb.To8Bit(float32(val)) + + rgb[colour] = srgb.To8Bit(float32(val)) } + return fmt.Sprintf("#%02x%02x%02x", rgb[0], rgb[1], rgb[2]) } +//nolint:exhaustruct func newAccentColorSensor(ctx context.Context, accent string) *desktopSettingSensor { if accent == "" { accent = getProp(ctx, accentColorProp) } - s := &desktopSettingSensor{} - s.IsDiagnostic = true - s.IconString = "mdi:palette" - s.SensorSrc = linux.DataSrcDbus - s.SensorTypeValue = linux.SensorAccentColor - s.Value = accent - return s + + newSensor := &desktopSettingSensor{} + newSensor.IsDiagnostic = true + newSensor.IconString = "mdi:palette" + newSensor.SensorSrc = linux.DataSrcDbus + newSensor.SensorTypeValue = linux.SensorAccentColor + newSensor.Value = accent + + return newSensor } +//nolint:exhaustruct func newColorSchemeSensor(ctx context.Context, scheme string) *desktopSettingSensor { if scheme == "" { scheme = getProp(ctx, colorSchemeProp) } - s := &desktopSettingSensor{} - s.IsDiagnostic = true - s.SensorSrc = linux.DataSrcDbus - s.SensorTypeValue = linux.SensorColorScheme - s.Value = scheme + + newSensor := &desktopSettingSensor{} + newSensor.IsDiagnostic = true + newSensor.SensorSrc = linux.DataSrcDbus + newSensor.SensorTypeValue = linux.SensorColorScheme + newSensor.Value = scheme + switch scheme { case "dark": - s.IconString = "mdi:weather-night" + newSensor.IconString = "mdi:weather-night" case "light": - s.IconString = "mdi:weather-sunny" + newSensor.IconString = "mdi:weather-sunny" default: - s.IconString = "mdi:theme-light-dark" + newSensor.IconString = "mdi:theme-light-dark" } - return s + + return newSensor } func getProp(ctx context.Context, prop string) string { - var value dbus.Variant - var err error - if value, err = dbusx.GetData[dbus.Variant](ctx, + value, err := dbusx.GetData[dbus.Variant](ctx, dbusx.SessionBus, desktopPortalPath, desktopPortalInterface, settingsPortalInterface+".Read", "org.freedesktop.appearance", - prop); err != nil { + prop) + if err != nil { log.Warn().Err(err).Msg("Could not retrieve accent color from D-Bus.") + return sensor.StateUnknown } + switch prop { case accentColorProp: return parseAccentColor(value) case colorSchemeProp: return parseColorScheme(value) } + return sensor.StateUnknown } diff --git a/internal/linux/device.go b/internal/linux/device.go index fee35adf5..8d69f43d9 100644 --- a/internal/linux/device.go +++ b/internal/linux/device.go @@ -8,6 +8,7 @@ package linux import ( + "errors" "os" "strings" "syscall" @@ -29,6 +30,8 @@ const ( unknownDistroVersion = "Unknown Version" ) +var ErrDesktopPortalMissing = errors.New("no portal present") + type Device struct { appName string appVersion string @@ -164,16 +167,16 @@ func Chassis() string { // FindPortal is a helper function to work out which portal interface should be // used for getting information on running apps. -func FindPortal() string { +func FindPortal() (string, error) { desktop := os.Getenv("XDG_CURRENT_DESKTOP") switch { case strings.Contains(desktop, "KDE"): - return "org.freedesktop.impl.portal.desktop.kde" + return "org.freedesktop.impl.portal.desktop.kde", nil case strings.Contains(desktop, "GNOME"): - return "org.freedesktop.impl.portal.desktop.gtk" + return "org.freedesktop.impl.portal.desktop.gtk", nil default: - return "" + return "", ErrDesktopPortalMissing } } diff --git a/internal/linux/device_test.go b/internal/linux/device_test.go index 52b21633c..3010d530a 100644 --- a/internal/linux/device_test.go +++ b/internal/linux/device_test.go @@ -167,9 +167,10 @@ func TestFindPortal(t *testing.T) { setup func() } tests := []struct { - name string - args args - want string + name string + args args + want string + wantErr bool }{ { name: "KDE", @@ -190,15 +191,21 @@ func TestFindPortal(t *testing.T) { args: args{ setup: func() { os.Setenv("XDG_CURRENT_DESKTOP", "UNKNOWN") }, }, - want: "", + want: "", + wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { tt.args.setup() - if got := FindPortal(); got != tt.want { + got, err := FindPortal() + if got != tt.want { t.Errorf("FindPortal() = %v, want %v", got, tt.want) } + if (err != nil) != tt.wantErr { + t.Errorf("Load() error = %v, wantErr %v", err, tt.wantErr) + return + } }) } } diff --git a/internal/linux/disk/diskRates.go b/internal/linux/disk/diskRates.go index 66317c2ea..478d9b154 100644 --- a/internal/linux/disk/diskRates.go +++ b/internal/linux/disk/diskRates.go @@ -25,6 +25,9 @@ import ( const ( diskRateUnits = "kB/s" diskCountUnits = "requests" + + ratesUpdateInterval = 5 * time.Second + ratesUpdateJitter = time.Second ) type diskIOSensor struct { @@ -35,12 +38,12 @@ type diskIOSensor struct { } type diskIOSensorAttributes struct { - DataSource string `json:"Data Source"` + DataSource string `json:"data_source"` NativeUnit string `json:"native_unit_of_measurement,omitempty"` - Model string `json:"Device Model,omitempty"` - SysFSPath string `json:"SysFS Path,omitempty"` - Sectors uint64 `json:"Total Sectors,omitempty"` - Time uint64 `json:"Total Milliseconds,omitempty"` + Model string `json:"device_model,omitempty"` + SysFSPath string `json:"sysfs_path,omitempty"` + Sectors uint64 `json:"total_sectors,omitempty"` + Time uint64 `json:"total_milliseconds,omitempty"` } type sensors struct { @@ -52,6 +55,7 @@ type sensors struct { func (s *diskIOSensor) Name() string { r := []rune(s.device.ID) + return string(append([]rune{unicode.ToUpper(r[0])}, r[1:]...)) + " " + s.SensorTypeValue.String() } @@ -59,6 +63,7 @@ func (s *diskIOSensor) ID() string { return s.device.ID + "_" + strcase.ToSnake(s.SensorTypeValue.String()) } +//nolint:exhaustive,exhaustruct func (s *diskIOSensor) Attributes() any { // Common attributes for all disk IO sensors attrs := &diskIOSensorAttributes{ @@ -66,24 +71,30 @@ func (s *diskIOSensor) Attributes() any { Model: s.device.Model, SysFSPath: s.device.SysFSPath, } + switch s.SensorTypeValue { case linux.SensorDiskReads: attrs.Sectors = s.stats[diskstats.TotalSectorsRead] attrs.Time = s.stats[diskstats.TotalTimeReading] attrs.NativeUnit = diskCountUnits + return attrs case linux.SensorDiskWrites: attrs.Sectors = s.stats[diskstats.TotalSectorsWritten] attrs.Time = s.stats[diskstats.TotalTimeWriting] attrs.NativeUnit = diskCountUnits + return attrs case linux.SensorDiskReadRate, linux.SensorDiskWriteRate: attrs.NativeUnit = diskRateUnits + return attrs } + return nil } +//nolint:exhaustive func (s *diskIOSensor) Icon() string { switch s.SensorTypeValue { case linux.SensorDiskReads, linux.SensorDiskReadRate: @@ -91,12 +102,16 @@ func (s *diskIOSensor) Icon() string { case linux.SensorDiskWrites, linux.SensorDiskWriteRate: return "mdi:file-download" } + return "mdi:file" } +//nolint:exhaustive,mnd func (s *diskIOSensor) update(stats map[diskstats.Stat]uint64, delta time.Duration) { s.stats = stats + var curr uint64 + switch s.SensorTypeValue { case linux.SensorDiskReads: s.Value = s.stats[diskstats.TotalReads] @@ -107,17 +122,20 @@ func (s *diskIOSensor) update(stats map[diskstats.Stat]uint64, delta time.Durati case linux.SensorDiskWriteRate: curr = s.stats[diskstats.TotalSectorsWritten] } + if s.SensorTypeValue == linux.SensorDiskReadRate || s.SensorTypeValue == linux.SensorDiskWriteRate { if uint64(delta.Seconds()) > 0 { log.Trace().Msgf("%s IO rate calc: (%d - %d) / uint64(%d) / 2", s.device, curr, s.prev, uint64(delta.Seconds())) s.Value = (curr - s.prev) / uint64(delta.Seconds()) / 2 } + s.prev = curr } } +//nolint:exhaustruct func newDiskIOSensor(device diskstats.Device, sensorType linux.SensorTypeValue) *diskIOSensor { - s := &diskIOSensor{ + newSensor := &diskIOSensor{ device: device, Sensor: linux.Sensor{ StateClassValue: types.StateClassTotalIncreasing, @@ -126,13 +144,15 @@ func newDiskIOSensor(device diskstats.Device, sensorType linux.SensorTypeValue) }, } if device.ID != "total" { - s.IsDiagnostic = true + newSensor.IsDiagnostic = true } - return s + + return newSensor } +//nolint:exhaustruct func newDiskIORateSensor(device diskstats.Device, sensorType linux.SensorTypeValue) *diskIOSensor { - s := &diskIOSensor{ + newSensor := &diskIOSensor{ device: device, Sensor: linux.Sensor{ DeviceClassValue: types.DeviceClassDataRate, @@ -142,9 +162,10 @@ func newDiskIORateSensor(device diskstats.Device, sensorType linux.SensorTypeVal }, } if device.ID != "total" { - s.IsDiagnostic = true + newSensor.IsDiagnostic = true } - return s + + return newSensor } func newDevice(dev diskstats.Device) *sensors { @@ -169,6 +190,7 @@ type ioWorker struct { func (w *ioWorker) addDevice(dev diskstats.Device) { w.mu.Lock() defer w.mu.Unlock() + if _, ok := w.devices[dev]; !ok { w.devices[dev] = newDevice(dev) } @@ -189,6 +211,7 @@ func (w *ioWorker) updateDevice(dev diskstats.Device, stats map[diskstats.Stat]u func (w *ioWorker) deviceSensors(dev diskstats.Device) []sensor.Details { w.mu.Lock() defer w.mu.Unlock() + return []sensor.Details{ w.devices[dev].totalReads, w.devices[dev].totalWrites, @@ -197,27 +220,31 @@ func (w *ioWorker) deviceSensors(dev diskstats.Device) []sensor.Details { } } -func (w *ioWorker) Interval() time.Duration { return 5 * time.Second } +func (w *ioWorker) Interval() time.Duration { return ratesUpdateInterval } -func (w *ioWorker) Jitter() time.Duration { return time.Second } +func (w *ioWorker) Jitter() time.Duration { return ratesUpdateJitter } -func (w *ioWorker) Sensors(_ context.Context, d time.Duration) ([]sensor.Details, error) { - var sensors []sensor.Details - newStats, err := diskstats.ReadDiskStatsFromSysFS() +func (w *ioWorker) Sensors(_ context.Context, duration time.Duration) ([]sensor.Details, error) { + stats, err := diskstats.ReadDiskStatsFromSysFS() if err != nil { return nil, fmt.Errorf("error reading disk stats from %s: %w", linux.DataSrcSysfs, err) } - for dev, stats := range newStats { + + sensors := make([]sensor.Details, 0, len(stats)) + + for dev, stats := range stats { // Add device (if it isn't already tracked). w.addDevice(dev) // Update the stats. - w.updateDevice(dev, stats, d) + w.updateDevice(dev, stats, duration) // Append its sensors. sensors = append(sensors, w.deviceSensors(dev)...) } + return sensors, nil } +//nolint:exhaustruct func NewIOWorker() (*linux.SensorWorker, error) { worker := &ioWorker{ devices: make(map[diskstats.Device]*sensors), @@ -227,6 +254,7 @@ func NewIOWorker() (*linux.SensorWorker, error) { if err != nil { log.Warn().Err(err).Msg("Error reading disk stats from procfs. Will not send disk rate sensors.") } + for dev := range newStats { worker.addDevice(dev) } diff --git a/internal/linux/disk/diskUsage.go b/internal/linux/disk/diskUsage.go index 40134823a..f79de2c2f 100644 --- a/internal/linux/disk/diskUsage.go +++ b/internal/linux/disk/diskUsage.go @@ -21,22 +21,27 @@ import ( "github.com/joshuar/go-hass-agent/internal/linux" ) +const ( + usageUpdateInterval = time.Minute + usageUpdateJitter = 10 * time.Second +) + type diskUsageSensor struct { stats *disk.UsageStat linux.Sensor } -func newDiskUsageSensor(d *disk.UsageStat) *diskUsageSensor { - s := &diskUsageSensor{} - s.IconString = "mdi:harddisk" - s.StateClassValue = types.StateClassTotal - s.UnitsString = "%" - s.stats = d - s.Value = math.Round(d.UsedPercent/0.05) * 0.05 - return s -} +//nolint:exhaustruct,mnd +func newDiskUsageSensor(stat *disk.UsageStat) *diskUsageSensor { + newSensor := &diskUsageSensor{} + newSensor.IconString = "mdi:harddisk" + newSensor.StateClassValue = types.StateClassTotal + newSensor.UnitsString = "%" + newSensor.stats = stat + newSensor.Value = math.Round(stat.UsedPercent/0.05) * 0.05 -// diskUsageState implements hass.SensorUpdate + return newSensor +} func (d *diskUsageSensor) Name() string { return "Mountpoint " + d.stats.Path + " Usage" @@ -46,12 +51,13 @@ func (d *diskUsageSensor) ID() string { if d.stats.Path == "/" { return "mountpoint_root" } + return "mountpoint" + strings.ReplaceAll(d.stats.Path, "/", "_") } func (d *diskUsageSensor) Attributes() any { return struct { - DataSource string `json:"Data Source"` + DataSource string `json:"data_source"` Stats disk.UsageStat }{ DataSource: linux.DataSrcProcfs, @@ -61,24 +67,29 @@ func (d *diskUsageSensor) Attributes() any { type usageWorker struct{} -func (w *usageWorker) Interval() time.Duration { return time.Minute } +func (w *usageWorker) Interval() time.Duration { return usageUpdateInterval } -func (w *usageWorker) Jitter() time.Duration { return 10 * time.Second } +func (w *usageWorker) Jitter() time.Duration { return usageUpdateJitter } func (w *usageWorker) Sensors(ctx context.Context, _ time.Duration) ([]sensor.Details, error) { - var sensors []sensor.Details - p, err := disk.PartitionsWithContext(ctx, false) + partitions, err := disk.PartitionsWithContext(ctx, false) if err != nil { return nil, fmt.Errorf("could not retrieve list of physical partitions: %w", err) } - for _, partition := range p { + + sensors := make([]sensor.Details, 0, len(partitions)) + + for _, partition := range partitions { usage, err := disk.UsageWithContext(ctx, partition.Mountpoint) if err != nil { log.Warn().Err(err).Msgf("Failed to get usage info for mountpount %s.", partition.Mountpoint) + continue } + sensors = append(sensors, newDiskUsageSensor(usage)) } + return sensors, nil } diff --git a/internal/linux/media/volume.go b/internal/linux/media/volume.go index ad9d2da09..f7a25e769 100644 --- a/internal/linux/media/volume.go +++ b/internal/linux/media/volume.go @@ -28,14 +28,17 @@ type audioDevice struct { volEntity *mqtthass.NumberEntity[int] } +//nolint:exhaustruct,mnd func VolumeControl(ctx context.Context, msgCh chan *mqttapi.Msg) (*mqtthass.NumberEntity[int], *mqtthass.SwitchEntity) { device := linux.MQTTDevice() client, err := pulseaudiox.NewPulseClient(ctx) if err != nil { log.Warn().Err(err).Msg("Unable to connect to Pulseaudio. Volume control will be unavailable.") + return nil, nil } + log.Debug().Msg("Connected to pulseaudio.") audioDev := &audioDevice{ @@ -67,18 +70,23 @@ func VolumeControl(ctx context.Context, msgCh chan *mqttapi.Msg) (*mqtthass.Numb log.Debug().Msg("Monitoring pulseaudio for events.") audioDev.publishVolume() audioDev.publishMute() + for { select { case <-ctx.Done(): log.Debug().Msg("Closing pulseaudio connection.") + return case <-client.EventCh: repl, err := client.GetState() if err != nil { log.Debug().Err(err).Msg("Failed to parse pulseaudio state.") + continue } + volPct := pulseaudiox.ParseVolume(repl) + switch { case repl.Mute != client.Mute: audioDev.publishMute() @@ -90,6 +98,7 @@ func VolumeControl(ctx context.Context, msgCh chan *mqttapi.Msg) (*mqtthass.Numb } } }() + return audioDev.volEntity, audioDev.muteEntity } @@ -97,6 +106,7 @@ func (d *audioDevice) publishVolume() { msg, err := d.volEntity.MarshalState() if err != nil { log.Debug().Err(err).Msg("Could not retrieve current volume.") + return } d.msgCh <- msg @@ -104,10 +114,12 @@ func (d *audioDevice) publishVolume() { func (d *audioDevice) volStateCallback(_ ...any) (json.RawMessage, error) { vol, err := d.pulseAudio.GetVolume() - log.Trace().Int("volume", int(vol)).Msg("Publishing volume change.") if err != nil { return json.RawMessage(`{ "value": 0 }`), err } + + log.Trace().Int("volume", int(vol)).Msg("Publishing volume change.") + return json.RawMessage(`{ "value": ` + strconv.FormatFloat(vol, 'f', 0, 64) + ` }`), nil } @@ -116,24 +128,29 @@ func (d *audioDevice) volCommandCallback(p *paho.Publish) { log.Debug().Err(err).Msg("Could not parse new volume level.") } else { log.Trace().Int("volume", newValue).Msg("Received volume change from Home Assistant.") + if err := d.pulseAudio.SetVolume(float64(newValue)); err != nil { log.Debug().Err(err).Msg("Could not set volume level.") + return } + go func() { d.publishVolume() }() } } -func (d *audioDevice) setMute(v bool) { +func (d *audioDevice) setMute(muteVal bool) { var err error - switch v { + + switch muteVal { case true: err = d.pulseAudio.SetMute(true) case false: err = d.pulseAudio.SetMute(false) } + if err != nil { log.Debug().Err(err).Msg("Could not set mute state.") } @@ -149,11 +166,12 @@ func (d *audioDevice) publishMute() { } func (d *audioDevice) muteStateCallback(_ ...any) (json.RawMessage, error) { - muteState, err := d.pulseAudio.GetMute() + muteVal, err := d.pulseAudio.GetMute() if err != nil { return json.RawMessage(`OFF`), err } - switch muteState { + + switch muteVal { case true: return json.RawMessage(`ON`), nil default: @@ -169,6 +187,7 @@ func (d *audioDevice) muteCommandCallback(p *paho.Publish) { case "OFF": d.setMute(false) } + go func() { d.publishMute() }() diff --git a/internal/linux/mem/memUsage.go b/internal/linux/mem/memUsage.go index d8b23c212..e0f66190a 100644 --- a/internal/linux/mem/memUsage.go +++ b/internal/linux/mem/memUsage.go @@ -19,6 +19,15 @@ import ( "github.com/joshuar/go-hass-agent/internal/linux" ) +const ( + scaleFactor = 100 + + updateInterval = time.Minute + updateJitter = 5 * time.Second +) + +var ErrUnknownSensor = errors.New("unknown sensor") + type memorySensor struct { linux.Sensor } @@ -26,71 +35,71 @@ type memorySensor struct { func (s *memorySensor) Attributes() any { return struct { NativeUnit string `json:"native_unit_of_measurement"` - DataSource string `json:"Data Source"` + DataSource string `json:"data_source"` }{ NativeUnit: s.UnitsString, DataSource: s.SensorSrc, } } +//nolint:exhaustive,exhaustruct func newMemoryUsageSensor(sensorType linux.SensorTypeValue, stats *mem.VirtualMemoryStat) (*memorySensor, error) { - s := &memorySensor{} + newSensor := &memorySensor{} - s.SensorTypeValue = sensorType - s.IconString = "mdi:memory" - s.SensorSrc = linux.DataSrcSysfs + newSensor.SensorTypeValue = sensorType + newSensor.IconString = "mdi:memory" + newSensor.SensorSrc = linux.DataSrcSysfs - switch s.SensorTypeValue { + switch newSensor.SensorTypeValue { case linux.SensorMemTotal: - s.Value = stats.Total - s.UnitsString = "B" - s.DeviceClassValue = types.DeviceClassDataSize - s.StateClassValue = types.StateClassTotal + newSensor.Value = stats.Total + newSensor.UnitsString = "B" + newSensor.DeviceClassValue = types.DeviceClassDataSize + newSensor.StateClassValue = types.StateClassTotal case linux.SensorMemAvail: - s.Value = stats.Available - s.UnitsString = "B" - s.DeviceClassValue = types.DeviceClassDataSize - s.StateClassValue = types.StateClassTotal + newSensor.Value = stats.Available + newSensor.UnitsString = "B" + newSensor.DeviceClassValue = types.DeviceClassDataSize + newSensor.StateClassValue = types.StateClassTotal case linux.SensorMemUsed: - s.Value = stats.Used - s.UnitsString = "B" - s.DeviceClassValue = types.DeviceClassDataSize - s.StateClassValue = types.StateClassTotal + newSensor.Value = stats.Used + newSensor.UnitsString = "B" + newSensor.DeviceClassValue = types.DeviceClassDataSize + newSensor.StateClassValue = types.StateClassTotal case linux.SensorMemPc: - s.Value = float64(stats.Used) / float64(stats.Total) * 100 - s.UnitsString = "%" - s.StateClassValue = types.StateClassMeasurement + newSensor.Value = float64(stats.Used) / float64(stats.Total) * scaleFactor + newSensor.UnitsString = "%" + newSensor.StateClassValue = types.StateClassMeasurement case linux.SensorSwapTotal: - s.Value = stats.SwapTotal - s.UnitsString = "B" - s.DeviceClassValue = types.DeviceClassDataSize - s.StateClassValue = types.StateClassTotal + newSensor.Value = stats.SwapTotal + newSensor.UnitsString = "B" + newSensor.DeviceClassValue = types.DeviceClassDataSize + newSensor.StateClassValue = types.StateClassTotal case linux.SensorSwapFree: - s.Value = stats.SwapFree - s.UnitsString = "B" - s.DeviceClassValue = types.DeviceClassDataSize - s.StateClassValue = types.StateClassTotal + newSensor.Value = stats.SwapFree + newSensor.UnitsString = "B" + newSensor.DeviceClassValue = types.DeviceClassDataSize + newSensor.StateClassValue = types.StateClassTotal case linux.SensorSwapPc: - s.Value = float64(stats.SwapCached) / float64(stats.SwapTotal) * 100 - s.UnitsString = "%" - s.StateClassValue = types.StateClassMeasurement + newSensor.Value = float64(stats.SwapCached) / float64(stats.SwapTotal) * scaleFactor + newSensor.UnitsString = "%" + newSensor.StateClassValue = types.StateClassMeasurement default: - return nil, errors.New("unknown memory stat") + return nil, ErrUnknownSensor } - return s, nil + + return newSensor, nil } type usageWorker struct{} -func (w *usageWorker) Interval() time.Duration { return time.Minute } +func (w *usageWorker) Interval() time.Duration { return updateInterval } -func (w *usageWorker) Jitter() time.Duration { return 5 * time.Second } +func (w *usageWorker) Jitter() time.Duration { return updateJitter } func (w *usageWorker) Sensors(ctx context.Context, _ time.Duration) ([]sensor.Details, error) { - var sensors []sensor.Details - var memDetails *mem.VirtualMemoryStat - var err error - if memDetails, err = mem.VirtualMemoryWithContext(ctx); err != nil { + memDetails, err := mem.VirtualMemoryWithContext(ctx) + if err != nil { return nil, fmt.Errorf("problem fetching memory stats: %w", err) } @@ -110,14 +119,19 @@ func (w *usageWorker) Sensors(ctx context.Context, _ time.Duration) ([]sensor.De ) } + sensors := make([]sensor.Details, 0, len(stats)) + for _, stat := range stats { - s, err := newMemoryUsageSensor(stat, memDetails) + memSensor, err := newMemoryUsageSensor(stat, memDetails) if err != nil { log.Warn().Err(err).Msg("Could not retrieve memory usage stat.") + continue } - sensors = append(sensors, s) + + sensors = append(sensors, memSensor) } + return sensors, nil } diff --git a/internal/linux/net/networkConnection.go b/internal/linux/net/networkConnection.go index 75b6c0ad3..4de90dd1b 100644 --- a/internal/linux/net/networkConnection.go +++ b/internal/linux/net/networkConnection.go @@ -7,7 +7,6 @@ package net import ( "context" - "errors" "slices" "sync" @@ -57,12 +56,12 @@ type connection struct { } type connectionAttributes struct { - ConnectionType string `json:"Connection Type,omitempty"` - Ipv4 string `json:"IPv4 Address,omitempty"` - Ipv6 string `json:"IPv6 Address,omitempty"` - DataSource string `json:"Data Source"` - IPv4Mask int `json:"IPv4 Mask,omitempty"` - IPv6Mask int `json:"IPv6 Mask,omitempty"` + ConnectionType string `json:"connection_type,omitempty"` + Ipv4 string `json:"ipv4_address,omitempty"` + Ipv6 string `json:"ipv6_address,omitempty"` + DataSource string `json:"data_source"` + IPv4Mask int `json:"ipv4_mask,omitempty"` + IPv6Mask int `json:"ipv6_mask,omitempty"` } func (c *connection) Name() string { @@ -73,10 +72,12 @@ func (c *connection) ID() string { return strcase.ToSnake(c.name) + "_connection_state" } +//nolint:mnd func (c *connection) Icon() string { c.mu.Lock() i := c.state + 5 c.mu.Unlock() + return i.String() } @@ -87,11 +88,13 @@ func (c *connection) Attributes() any { func (c *connection) State() any { c.mu.Lock() defer c.mu.Unlock() + return c.state.String() } +//nolint:exhaustruct,mnd func newConnection(ctx context.Context, path dbus.ObjectPath) *connection { - c := &connection{ + newConnection := &connection{ path: path, Sensor: linux.Sensor{ SensorTypeValue: linux.SensorConnectionState, @@ -104,114 +107,132 @@ func newConnection(ctx context.Context, path dbus.ObjectPath) *connection { // fetch properties for the connection var err error - c.name, err = dbusx.GetProp[string](ctx, dbusx.SystemBus, string(path), dBusNMObj, dbusNMActiveConnIntr+".Id") + + newConnection.name, err = dbusx.GetProp[string](ctx, dbusx.SystemBus, string(path), dBusNMObj, dbusNMActiveConnIntr+".Id") if err != nil { log.Warn().Err(err).Msg("Could not retrieve connection ID.") } - c.state, err = dbusx.GetProp[connState](ctx, dbusx.SystemBus, string(path), dBusNMObj, dbusNMActiveConnIntr+".State") + + newConnection.state, err = dbusx.GetProp[connState](ctx, dbusx.SystemBus, string(path), dBusNMObj, dbusNMActiveConnIntr+".State") if err != nil { log.Warn().Err(err).Msg("Could not retrieve connection state.") } - c.attrs.ConnectionType, err = dbusx.GetProp[string](ctx, dbusx.SystemBus, string(path), dBusNMObj, dbusNMActiveConnIntr+".Type") + + newConnection.attrs.ConnectionType, err = dbusx.GetProp[string](ctx, + dbusx.SystemBus, string(path), dBusNMObj, dbusNMActiveConnIntr+".Type") if err != nil { log.Warn().Err(err).Msg("Could not retrieve connection type.") } + ip4ConfigPath, err := dbusx.GetProp[dbus.ObjectPath](ctx, dbusx.SystemBus, string(path), dBusNMObj, dbusNMActiveConnIntr+".Ip4Config") if err != nil { - log.Warn().Err(err).Str("connection", c.name).Msg("Could not fetch IPv4 address.") + log.Warn().Err(err).Str("connection", newConnection.name).Msg("Could not fetch IPv4 address.") } - c.attrs.Ipv4, c.attrs.IPv4Mask = getAddr(ctx, 4, ip4ConfigPath) + + newConnection.attrs.Ipv4, newConnection.attrs.IPv4Mask = getAddr(ctx, 4, ip4ConfigPath) + ip6ConfigPath, err := dbusx.GetProp[dbus.ObjectPath](ctx, dbusx.SystemBus, string(path), dBusNMObj, dbusNMActiveConnIntr+".Ip6Config") if err != nil { - log.Warn().Err(err).Str("connection", c.name).Msg("Could not fetch IPv4 address.") + log.Warn().Err(err).Str("connection", newConnection.name).Msg("Could not fetch IPv4 address.") } - c.attrs.Ipv6, c.attrs.IPv6Mask = getAddr(ctx, 6, ip6ConfigPath) - return c + + newConnection.attrs.Ipv6, newConnection.attrs.IPv6Mask = getAddr(ctx, 6, ip6ConfigPath) + + return newConnection } +//nolint:mnd func monitorConnection(ctx context.Context, p dbus.ObjectPath) <-chan sensor.Details { sensorCh := make(chan sensor.Details) updateCh := make(chan any) // create a new connection sensor - c := newConnection(ctx, p) + conn := newConnection(ctx, p) // process updates and handle cancellation connCtx, connCancel := context.WithCancel(ctx) + go func() { defer close(sensorCh) defer close(updateCh) + for { select { case <-connCtx.Done(): - log.Debug().Str("connection", c.Name()).Str("path", string(c.path)). + log.Debug().Str("connection", conn.Name()).Str("path", string(conn.path)). Msg("Connection deactivated.") + return case <-ctx.Done(): - log.Debug().Str("connection", c.Name()).Str("path", string(c.path)). + log.Debug().Str("connection", conn.Name()).Str("path", string(conn.path)). Msg("Stopped monitoring connection.") + return case u := <-updateCh: - c.mu.Lock() + conn.mu.Lock() switch update := u.(type) { case connState: - if c.state != update { - c.state = update + if conn.state != update { + conn.state = update } case address: if update.class == 4 { - if c.attrs.Ipv4 != update.address { - c.attrs.Ipv4 = update.address - c.attrs.IPv4Mask = update.mask + if conn.attrs.Ipv4 != update.address { + conn.attrs.Ipv4 = update.address + conn.attrs.IPv4Mask = update.mask } } + if update.class == 6 { - if c.attrs.Ipv6 != update.address { - c.attrs.Ipv6 = update.address - c.attrs.IPv6Mask = update.mask + if conn.attrs.Ipv6 != update.address { + conn.attrs.Ipv6 = update.address + conn.attrs.IPv6Mask = update.mask } } } - sensorCh <- c - c.mu.Unlock() + sensorCh <- conn + conn.mu.Unlock() } } }() // send the initial connection state as a sensor go func() { - sensorCh <- c + sensorCh <- conn }() // monitor state changes go func() { defer connCancel() - for state := range monitorConnectionState(connCtx, string(c.path)) { + + for state := range monitorConnectionState(connCtx, string(conn.path)) { updateCh <- state } }() // monitor address changes go func() { - for addr := range monitorAddresses(connCtx, string(c.path)) { + for addr := range monitorAddresses(connCtx, string(conn.path)) { updateCh <- addr } }() // monitor for additional states depending on the type of connection - switch c.attrs.ConnectionType { + switch conn.attrs.ConnectionType { case "802-11-wireless": go func() { - for s := range monitorWifi(connCtx, c.path) { + for s := range monitorWifi(connCtx, conn.path) { sensorCh <- s } }() } - log.Debug().Str("connection", c.Name()).Msg("Monitoring connection.") + log.Debug().Str("connection", conn.Name()).Msg("Monitoring connection.") + return sensorCh } +//nolint:exhaustruct,mnd func monitorConnectionState(ctx context.Context, path string) chan connState { stateCh := make(chan connState) @@ -225,38 +246,48 @@ func monitorConnectionState(ctx context.Context, path string) chan connState { log.Debug().Err(err).Str("path", path). Msg("Failed to create connection state D-Bus watch.") close(stateCh) + return stateCh } log.Debug().Str("path", path).Msg("Monitoring connection state.") + go func() { defer close(stateCh) + for { select { case <-ctx.Done(): log.Debug().Str("path", path).Msg("Unmonitoring connection state.") + return case event := <-events: props, err := dbusx.ParsePropertiesChanged(event.Content) if err != nil { continue } + stateProp, stateChanged := props.Changed["State"] if !stateChanged { continue } + currentState := dbusx.VariantToValue[connState](stateProp) stateCh <- currentState + if currentState == 4 { log.Debug().Str("path", path).Msg("Unmonitoring connection state.") + return } } } }() + return stateCh } +//nolint:exhaustruct,mnd func monitorAddresses(ctx context.Context, path string) chan address { sensorCh := make(chan address) @@ -270,26 +301,32 @@ func monitorAddresses(ctx context.Context, path string) chan address { log.Debug().Err(err). Msg("Failed to create address changes D-Bus watch.") close(sensorCh) + return sensorCh } log.Debug().Str("path", path).Msg("Monitoring address changes.") + go func() { defer close(sensorCh) + for { select { case <-ctx.Done(): log.Debug().Str("path", path).Msg("Unmonitoring address changes.") + return case event := <-events: props, err := dbusx.ParsePropertiesChanged(event.Content) if err != nil { continue } + if ipv4Change, ipv4Changed := props.Changed[ipv4ConfigProp]; ipv4Changed { addr, mask := getAddr(ctx, 4, dbusx.VariantToValue[dbus.ObjectPath](ipv4Change)) sensorCh <- address{address: addr, mask: mask, class: 4} } + if ipv6Change, ipv6Changed := props.Changed[ipv6ConfigProp]; ipv6Changed { addr, mask := getAddr(ctx, 4, dbusx.VariantToValue[dbus.ObjectPath](ipv6Change)) sensorCh <- address{address: addr, mask: mask, class: 6} @@ -301,40 +338,50 @@ func monitorAddresses(ctx context.Context, path string) chan address { return sensorCh } +//nolint:mnd func getAddr(ctx context.Context, ver int, path dbus.ObjectPath) (addr string, mask int) { if path == "/" { return "", 0 } + var connProp string + switch ver { case 4: connProp = dBusNMObj + ".IP4Config" case 6: connProp = dBusNMObj + ".IP6Config" } + addrDetails, err := dbusx.GetProp[[]map[string]dbus.Variant](ctx, dbusx.SystemBus, string(path), dBusNMObj, connProp+".AddressData") if err != nil { return "", 0 } + var address string + var prefix int + if len(addrDetails) > 0 { address = dbusx.VariantToValue[string](addrDetails[0]["address"]) prefix = dbusx.VariantToValue[int](addrDetails[0]["prefix"]) log.Debug().Str("path", string(path)).Str("address", address).Int("prefix", prefix). Msg("Retrieved address.") } + return address, prefix } func getActiveConnections(ctx context.Context) []dbus.ObjectPath { - v, err := dbusx.GetProp[[]dbus.ObjectPath](ctx, dbusx.SystemBus, dBusNMPath, dBusNMObj, dBusNMObj+".ActiveConnections") + connectionPaths, err := dbusx.GetProp[[]dbus.ObjectPath](ctx, dbusx.SystemBus, dBusNMPath, dBusNMObj, dBusNMObj+".ActiveConnections") if err != nil { log.Debug().Err(err). Msg("Could not retrieve active connection list.") + return nil } - return v + + return connectionPaths } type address struct { @@ -365,13 +412,15 @@ func (w *connectionsWorker) untrack(path dbus.ObjectPath) { func (w *connectionsWorker) isTracked(path dbus.ObjectPath) bool { w.mu.Lock() defer w.mu.Unlock() + return slices.Contains(w.list, path) } func (w *connectionsWorker) Sensors(_ context.Context) ([]sensor.Details, error) { - return nil, errors.New("unimplemented") + return nil, linux.ErrUnimplemented } +//nolint:exhaustruct,mnd func (w *connectionsWorker) Events(ctx context.Context) chan sensor.Details { sensorCh := make(chan sensor.Details) w.list = getActiveConnections(ctx) @@ -380,6 +429,7 @@ func (w *connectionsWorker) Events(ctx context.Context) chan sensor.Details { for s := range monitorConnection(ctx, path) { sensorCh <- s } + w.untrack(path) }() } @@ -395,16 +445,20 @@ func (w *connectionsWorker) Events(ctx context.Context) chan sensor.Details { log.Debug().Err(err). Msg("Failed to create network connections D-Bus watch.") close(sensorCh) + return sensorCh } go func() { log.Debug().Msg("Monitoring for network connection changes.") + defer close(sensorCh) + for { select { case <-ctx.Done(): log.Debug().Msg("Stopped network connection monitoring.") + return case event := <-events: connectionPath := dbus.ObjectPath(event.Path) @@ -432,6 +486,7 @@ func (w *connectionsWorker) Events(ctx context.Context) chan sensor.Details { return sensorCh } +//nolint:exhaustruct func NewConnectionWorker() (*linux.SensorWorker, error) { return &linux.SensorWorker{ WorkerName: "Network Connection Sensors", diff --git a/internal/linux/net/networkRates.go b/internal/linux/net/networkRates.go index 6e64f4128..55047174c 100644 --- a/internal/linux/net/networkRates.go +++ b/internal/linux/net/networkRates.go @@ -20,13 +20,16 @@ import ( const ( countUnit = "B" rateUnit = "B/s" + + rateInterval = 5 * time.Second + rateJitter = time.Second ) type netIOSensorAttributes struct { - Packets uint64 `json:"Packets"` // number of packets - Errors uint64 `json:"Errors"` // total number of errors - Drops uint64 `json:"Drops"` // total number of packets which were dropped - FifoErrors uint64 `json:"Fifo Errors"` // total number of FIFO buffers errors + Packets uint64 `json:"packets"` // number of packets + Errors uint64 `json:"errors"` // total number of errors + Drops uint64 `json:"drops"` // total number of packets which were dropped + FifoErrors uint64 `json:"fifo_errors"` // total number of FIFO buffers errors } type netIOSensor struct { @@ -37,7 +40,7 @@ type netIOSensor struct { func (s *netIOSensor) Attributes() any { return struct { NativeUnit string `json:"native_unit_of_measurement"` - DataSource string `json:"Data Source"` + DataSource string `json:"data_source"` netIOSensorAttributes }{ NativeUnit: s.UnitsString, @@ -46,6 +49,7 @@ func (s *netIOSensor) Attributes() any { } } +//nolint:exhaustive func (s *netIOSensor) Icon() string { switch s.SensorTypeValue { case linux.SensorBytesRecv: @@ -53,26 +57,29 @@ func (s *netIOSensor) Icon() string { case linux.SensorBytesSent: return "mdi:upload-network" } + return "mdi:help-network" } -func (s *netIOSensor) update(c *net.IOCountersStat) { +//nolint:exhaustive +func (s *netIOSensor) update(counters *net.IOCountersStat) { switch s.SensorTypeValue { case linux.SensorBytesRecv: - s.Value = c.BytesRecv - s.Packets = c.PacketsRecv - s.Errors = c.Errin - s.Drops = c.Dropin - s.FifoErrors = c.Fifoin + s.Value = counters.BytesRecv + s.Packets = counters.PacketsRecv + s.Errors = counters.Errin + s.Drops = counters.Dropin + s.FifoErrors = counters.Fifoin case linux.SensorBytesSent: - s.Value = c.BytesSent - s.Packets = c.PacketsSent - s.Errors = c.Errout - s.Drops = c.Dropout - s.FifoErrors = c.Fifoout + s.Value = counters.BytesSent + s.Packets = counters.PacketsSent + s.Errors = counters.Errout + s.Drops = counters.Dropout + s.FifoErrors = counters.Fifoout } } +//nolint:exhaustruct func newNetIOSensor(t linux.SensorTypeValue) *netIOSensor { return &netIOSensor{ Sensor: linux.Sensor{ @@ -89,6 +96,7 @@ type netIORateSensor struct { lastValue uint64 } +//nolint:exhaustive func (s *netIORateSensor) Icon() string { switch s.SensorTypeValue { case linux.SensorBytesRecvRate: @@ -96,6 +104,7 @@ func (s *netIORateSensor) Icon() string { case linux.SensorBytesSentRate: return "mdi:transfer-up" } + return "mdi:help-network" } @@ -103,9 +112,11 @@ func (s *netIORateSensor) update(d time.Duration, b uint64) { if uint64(d.Seconds()) > 0 && s.lastValue != 0 { s.Value = (b - s.lastValue) / uint64(d.Seconds()) } + s.lastValue = b } +//nolint:exhaustruct func newNetIORateSensor(t linux.SensorTypeValue) *netIORateSensor { return &netIORateSensor{ Sensor: linux.Sensor{ @@ -123,11 +134,11 @@ type ratesWorker struct { bytesRxRate, bytesTxRate *netIORateSensor } -func (w *ratesWorker) Interval() time.Duration { return 5 * time.Second } +func (w *ratesWorker) Interval() time.Duration { return rateInterval } -func (w *ratesWorker) Jitter() time.Duration { return time.Second } +func (w *ratesWorker) Jitter() time.Duration { return rateJitter } -func (w *ratesWorker) Sensors(ctx context.Context, d time.Duration) ([]sensor.Details, error) { +func (w *ratesWorker) Sensors(ctx context.Context, duration time.Duration) ([]sensor.Details, error) { // Retrieve new stats. netIO, err := net.IOCountersWithContext(ctx, false) if err != nil { @@ -136,8 +147,8 @@ func (w *ratesWorker) Sensors(ctx context.Context, d time.Duration) ([]sensor.De // Update all sensors. w.bytesRx.update(&netIO[0]) w.bytesTx.update(&netIO[0]) - w.bytesRxRate.update(d, netIO[0].BytesRecv) - w.bytesTxRate.update(d, netIO[0].BytesSent) + w.bytesRxRate.update(duration, netIO[0].BytesRecv) + w.bytesTxRate.update(duration, netIO[0].BytesSent) // Return sensors with new values. return []sensor.Details{w.bytesRx, w.bytesTx, w.bytesRxRate, w.bytesTxRate}, nil } diff --git a/internal/linux/net/wifiProps.go b/internal/linux/net/wifiProps.go index d40931015..025e1b3a7 100644 --- a/internal/linux/net/wifiProps.go +++ b/internal/linux/net/wifiProps.go @@ -35,6 +35,7 @@ type wifiSensor struct { linux.Sensor } +//nolint:exhaustive func (w *wifiSensor) State() any { switch w.SensorTypeValue { case linux.SensorWifiSSID: @@ -66,6 +67,7 @@ func (w *wifiSensor) State() any { } } +//nolint:exhaustive,mnd func (w *wifiSensor) Icon() string { switch w.SensorTypeValue { case linux.SensorWifiSSID, linux.SensorWifiHWAddress, linux.SensorWifiFrequency, linux.SensorWifiSpeed: @@ -75,6 +77,7 @@ func (w *wifiSensor) Icon() string { if !ok { return "mdi:wifi-strength-alert-outline" } + switch { case value <= 25: return "mdi:wifi-strength-1" @@ -86,9 +89,11 @@ func (w *wifiSensor) Icon() string { return "mdi:wifi-strength-4" } } + return "mdi:network" } +//nolint:exhaustruct func newWifiSensor(sensorType string) *wifiSensor { switch sensorType { case ssidProp: @@ -135,6 +140,7 @@ func newWifiSensor(sensorType string) *wifiSensor { }, } } + return nil } @@ -146,28 +152,37 @@ func monitorWifi(ctx context.Context, p dbus.ObjectPath) <-chan sensor.Details { wifiDevices, err := dbusx.GetProp[[]dbus.ObjectPath](ctx, dbusx.SystemBus, string(p), dBusNMObj, dbusNMActiveConnIntr+".Devices") if err != nil { log.Warn().Err(err).Msg("Could not retrieve active wireless devices.") + return nil } + for _, d := range wifiDevices { // for each device, get the access point it is currently associated with - ap, err := dbusx.GetProp[dbus.ObjectPath](ctx, dbusx.SystemBus, string(d), dBusNMObj, accessPointProp) + accessPointPath, err := dbusx.GetProp[dbus.ObjectPath](ctx, dbusx.SystemBus, string(d), dBusNMObj, accessPointProp) if err != nil { log.Warn().Err(err).Msg("Could not ascertain access point.") + continue } + for _, prop := range wifiPropList { // for the associated access point, get the wifi properties as sensors - value, err := dbusx.GetProp[any](ctx, dbusx.SystemBus, string(ap), dBusNMObj, accessPointInterface+"."+prop) + value, err := dbusx.GetProp[any](ctx, dbusx.SystemBus, string(accessPointPath), dBusNMObj, accessPointInterface+"."+prop) if err != nil { log.Warn().Err(err).Str("prop", prop).Msg("Could not get Wi-Fi property %s.") + continue } + wifiSensor := newWifiSensor(prop) if wifiSensor == nil { log.Warn().Str("prop", prop).Msg("Unknown wifi property.") + continue } + wifiSensor.Value = value + // send the wifi property as a sensor go func() { outCh <- wifiSensor @@ -175,42 +190,49 @@ func monitorWifi(ctx context.Context, p dbus.ObjectPath) <-chan sensor.Details { } // monitor for changes in the wifi properties for this device go func() { - for s := range monitorWifiProps(ctx, ap) { + for s := range monitorWifiProps(ctx, accessPointPath) { outCh <- s } }() } + return outCh } -func monitorWifiProps(ctx context.Context, p dbus.ObjectPath) chan sensor.Details { +//nolint:exhaustruct +func monitorWifiProps(ctx context.Context, propPath dbus.ObjectPath) chan sensor.Details { sensorCh := make(chan sensor.Details) events, err := dbusx.WatchBus(ctx, &dbusx.Watch{ Bus: dbusx.SystemBus, Names: wifiPropList, - Path: string(p), + Path: string(propPath), }) if err != nil { log.Debug().Err(err). Msg("Failed to create wifi properties D-Bus watch.") close(sensorCh) + return sensorCh } go func() { defer close(sensorCh) + for { select { case <-ctx.Done(): log.Debug().Msg("Unmonitoring wifi properties.") + return case event := <-events: props, err := dbusx.ParsePropertiesChanged(event.Content) if err != nil { log.Warn().Err(err).Msg("Did not understand received trigger.") + continue } + for prop, value := range props.Changed { if slices.Contains(wifiPropList, prop) { wifiSensor := newWifiSensor(prop) diff --git a/internal/linux/power/idle.go b/internal/linux/power/idle.go index fd4a73c7f..d749b8430 100644 --- a/internal/linux/power/idle.go +++ b/internal/linux/power/idle.go @@ -9,7 +9,7 @@ package power import ( "context" - "path/filepath" + "fmt" "time" "github.com/rs/zerolog/log" @@ -25,6 +25,8 @@ const ( idleProp = managerInterface + "." + sessionIdleProp idleTimeProp = managerInterface + "." + sessionIdleTimeProp + + idleSF = 1000 ) type idleSensor struct { @@ -37,6 +39,7 @@ func (s *idleSensor) Icon() string { if !ok { return notIdleIcon } + switch state { case true: return idleIcon @@ -47,37 +50,51 @@ func (s *idleSensor) Icon() string { func (s *idleSensor) Attributes() any { return struct { - DataSource string `json:"Data Source"` - Seconds float64 `json:"Duration"` + DataSource string `json:"data_source"` + Seconds float64 `json:"duration"` }{ DataSource: linux.DataSrcDbus, Seconds: idleTime(s.idleTime), } } -func newIdleSensor(ctx context.Context) *idleSensor { - s := &idleSensor{ +//nolint:exhaustruct +func newIdleSensor(ctx context.Context) (*idleSensor, error) { + newSensor := &idleSensor{ Sensor: linux.Sensor{ SensorTypeValue: linux.SensorIdleState, IsBinary: true, }, } + var idleState bool + var idleTime int64 + var err error + if idleState, err = dbusx.GetProp[bool](ctx, dbusx.SystemBus, loginBasePath, loginBaseInterface, idleProp); err != nil { - log.Debug().Err(err).Str("prop", filepath.Ext(idleProp)).Msg("Could not retrieve property from D-Bus.") - return nil + return nil, fmt.Errorf("could not retrieve idle state: %w", err) + } + + newSensor.Value = idleState + idleTime, err = dbusx.GetProp[int64](ctx, dbusx.SystemBus, loginBasePath, loginBaseInterface, idleTimeProp) + if err != nil { + return nil, fmt.Errorf("could not retrieve idle time: %w", err) } - s.Value = idleState - idleTime, _ = dbusx.GetProp[int64](ctx, dbusx.SystemBus, loginBasePath, loginBaseInterface, idleTimeProp) - s.idleTime = idleTime - return s + newSensor.idleTime = idleTime + + return newSensor, nil } func IdleUpdater(ctx context.Context) chan sensor.Details { sensorCh := make(chan sensor.Details) - idleSensor := newIdleSensor(ctx) + idleSensor, err := newIdleSensor(ctx) + if err != nil { + log.Debug().Err(err).Msg("Cannot create idle sensor.") + close(sensorCh) + return sensorCh + } sessionPath := dbusx.GetSessionPath(ctx) @@ -123,7 +140,13 @@ func IdleUpdater(ctx context.Context) chan sensor.Details { // Send an initial sensor update. go func() { - sensorCh <- newIdleSensor(ctx) + idleSensor, err := newIdleSensor(ctx) + if err != nil { + log.Debug().Err(err).Msg("Cannot start idle sensor.") + + return + } + sensorCh <- idleSensor }() return sensorCh @@ -131,6 +154,6 @@ func IdleUpdater(ctx context.Context) chan sensor.Details { func idleTime(current int64) float64 { epoch := time.Unix(0, 0) - uptime := time.Unix(current/1000, 0) + uptime := time.Unix(current/idleSF, 0) return uptime.Sub(epoch).Seconds() } diff --git a/internal/linux/power/laptop.go b/internal/linux/power/laptop.go index 59f12b511..631e187d5 100644 --- a/internal/linux/power/laptop.go +++ b/internal/linux/power/laptop.go @@ -24,7 +24,11 @@ const ( externalPowerProp = managerInterface + ".OnExternalPower" ) -var laptopPropList = []string{dockedProp, lidClosedProp, externalPowerProp} +var ( + laptopPropList = []string{dockedProp, lidClosedProp, externalPowerProp} + + ErrUnsupportedHardware = errors.New("unsupported hardware for laptop sensor monitoring") +) type laptopSensor struct { prop string @@ -36,6 +40,7 @@ func (s *laptopSensor) Icon() string { if !ok { return "mdi:alert" } + switch s.prop { case dockedProp: if state { @@ -59,6 +64,7 @@ func (s *laptopSensor) Icon() string { return "mdi:help" } +//nolint:exhaustruct func newLaptopEvent(prop string, state bool) *laptopSensor { sensorEvent := &laptopSensor{ prop: prop, @@ -69,6 +75,7 @@ func newLaptopEvent(prop string, state bool) *laptopSensor { Value: state, }, } + switch prop { case dockedProp: sensorEvent.SensorTypeValue = linux.SensorDocked @@ -82,6 +89,7 @@ func newLaptopEvent(prop string, state bool) *laptopSensor { type laptopWorker struct{} +//nolint:exhaustruct func (w *laptopWorker) Setup(ctx context.Context) *dbusx.Watch { sessionPath := dbusx.GetSessionPath(ctx) @@ -98,6 +106,7 @@ func (w *laptopWorker) Watch(ctx context.Context, triggerCh chan dbusx.Trigger) go func() { defer close(sensorCh) + for { select { case <-ctx.Done(): @@ -107,8 +116,10 @@ func (w *laptopWorker) Watch(ctx context.Context, triggerCh chan dbusx.Trigger) props, err := dbusx.ParsePropertiesChanged(event.Content) if err != nil { log.Warn().Err(err).Msg("Did not understand received trigger.") + continue } + for prop, value := range props.Changed { if slices.Contains(laptopPropList, prop) { sensorCh <- newLaptopEvent(prop, dbusx.VariantToValue[bool](value)) @@ -124,6 +135,7 @@ func (w *laptopWorker) Watch(ctx context.Context, triggerCh chan dbusx.Trigger) if err != nil { log.Warn().Err(err).Msg("Could not get initial sensor updates.") } + for _, s := range sensors { sensorCh <- s } @@ -132,22 +144,25 @@ func (w *laptopWorker) Watch(ctx context.Context, triggerCh chan dbusx.Trigger) } func (w *laptopWorker) Sensors(ctx context.Context) ([]sensor.Details, error) { - var sensors []sensor.Details + sensors := make([]sensor.Details, 0, len(laptopPropList)) + for _, prop := range laptopPropList { - var state bool - var err error - if state, err = dbusx.GetProp[bool](ctx, dbusx.SystemBus, loginBasePath, loginBaseInterface, prop); err != nil { + state, err := dbusx.GetProp[bool](ctx, dbusx.SystemBus, loginBasePath, loginBaseInterface, prop) + if err != nil { log.Debug().Err(err).Str("prop", filepath.Ext(prop)).Msg("Could not retrieve laptop property from D-Bus.") + continue } + sensors = append(sensors, newLaptopEvent(prop, state)) } + return sensors, nil } func NewLaptopWorker() (*linux.SensorWorker, error) { if linux.Chassis() != "laptop" { - return nil, errors.New("unsupported hardware for laptop sensor monitoring") + return nil, ErrUnsupportedHardware } return &linux.SensorWorker{ diff --git a/internal/linux/power/powerControl.go b/internal/linux/power/powerControl.go index 88e6e88ab..f0a50a040 100644 --- a/internal/linux/power/powerControl.go +++ b/internal/linux/power/powerControl.go @@ -33,6 +33,7 @@ type commandConfig struct { method string } +//nolint:exhaustruct var commands = map[string]commandConfig{ "lock_session": { name: "Lock Session", @@ -71,34 +72,36 @@ var commands = map[string]commandConfig{ } func NewPowerControl(ctx context.Context) []*mqtthass.ButtonEntity { - var entities []*mqtthass.ButtonEntity + entities := make([]*mqtthass.ButtonEntity, 0, len(commands)) device := linux.MQTTDevice() sessionPath := dbusx.GetSessionPath(ctx) - for k, v := range commands { + for cmdName, cmdInfo := range commands { var callback func(p *paho.Publish) - if v.path == "" { + if cmdInfo.path == "" { callback = func(_ *paho.Publish) { - err := dbusx.Call(ctx, dbusx.SystemBus, string(sessionPath), loginBaseInterface, v.method) + err := dbusx.Call(ctx, dbusx.SystemBus, string(sessionPath), loginBaseInterface, cmdInfo.method) if err != nil { - log.Warn().Err(err).Msgf("Could not %s session.", v.name) + log.Warn().Err(err).Msgf("Could not %s session.", cmdInfo.name) } } } else { callback = func(_ *paho.Publish) { - err := dbusx.Call(ctx, dbusx.SystemBus, v.path, loginBaseInterface, v.method, true) + err := dbusx.Call(ctx, dbusx.SystemBus, cmdInfo.path, loginBaseInterface, cmdInfo.method, true) if err != nil { log.Warn().Err(err).Msg("Could not power off session.") } } } + entities = append(entities, mqtthass.AsButton( - mqtthass.NewEntity(preferences.AppName, v.name, device.Name+"_"+k). + mqtthass.NewEntity(preferences.AppName, cmdInfo.name, device.Name+"_"+cmdName). WithOriginInfo(preferences.MQTTOrigin()). WithDeviceInfo(device). - WithIcon(v.icon). + WithIcon(cmdInfo.icon). WithCommandCallback(callback))) } + return entities } diff --git a/internal/linux/power/powerProfile.go b/internal/linux/power/powerProfile.go index 490931a02..71d976f26 100644 --- a/internal/linux/power/powerProfile.go +++ b/internal/linux/power/powerProfile.go @@ -29,18 +29,21 @@ type powerSensor struct { linux.Sensor } -func newPowerSensor(t linux.SensorTypeValue, v dbus.Variant) *powerSensor { - s := &powerSensor{} - s.Value = dbusx.VariantToValue[string](v) - s.SensorTypeValue = t - s.IconString = "mdi:flash" - s.SensorSrc = linux.DataSrcDbus - s.IsDiagnostic = true - return s +//nolint:exhaustruct +func newPowerSensor(sensorType linux.SensorTypeValue, sensorValue dbus.Variant) *powerSensor { + newSensor := &powerSensor{} + newSensor.Value = dbusx.VariantToValue[string](sensorValue) + newSensor.SensorTypeValue = sensorType + newSensor.IconString = "mdi:flash" + newSensor.SensorSrc = linux.DataSrcDbus + newSensor.IsDiagnostic = true + + return newSensor } type profileWorker struct{} +//nolint:exhaustruct func (w *profileWorker) Setup(_ context.Context) *dbusx.Watch { return &dbusx.Watch{ Bus: dbusx.SystemBus, @@ -55,19 +58,21 @@ func (w *profileWorker) Watch(ctx context.Context, triggerCh chan dbusx.Trigger) // Check for power profile support, exit if not available. Otherwise, send // an initial update. - s, err := w.Sensors(ctx) + sensors, err := w.Sensors(ctx) if err != nil { log.Warn().Err(err).Msg("Cannot monitor power profile.") close(sensorCh) return sensorCh } + go func() { - sensorCh <- s[0] + sensorCh <- sensors[0] }() // Watch for power profile changes. go func() { defer close(sensorCh) + for { select { case <-ctx.Done(): @@ -77,8 +82,10 @@ func (w *profileWorker) Watch(ctx context.Context, triggerCh chan dbusx.Trigger) props, err := dbusx.ParsePropertiesChanged(event.Content) if err != nil { log.Warn().Err(err).Msg("Did not understand received trigger.") + continue } + if profile, profileChanged := props.Changed[activeProfileProp]; profileChanged { sensorCh <- newPowerSensor(linux.SensorPowerProfile, profile) } diff --git a/internal/linux/power/powerState.go b/internal/linux/power/powerState.go index 2f60f7734..28e66721b 100644 --- a/internal/linux/power/powerState.go +++ b/internal/linux/power/powerState.go @@ -31,11 +31,12 @@ type powerStateSensor struct { } func (s *powerStateSensor) State() any { - b, ok := s.Value.(bool) + boolVal, ok := s.Value.(bool) if !ok { return sensor.StateUnknown } - if b { + + if boolVal { switch s.signal { case suspend: return "Suspended" @@ -51,6 +52,7 @@ func (s *powerStateSensor) Icon() string { if !ok { str = "" } + switch str { case "Suspended": return "mdi:power-sleep" @@ -61,12 +63,13 @@ func (s *powerStateSensor) Icon() string { } } -func newPowerState(s powerSignal, v any) *powerStateSensor { +//nolint:exhaustruct +func newPowerState(signalName powerSignal, signalValue any) *powerStateSensor { return &powerStateSensor{ - signal: s, + signal: signalName, Sensor: linux.Sensor{ SensorTypeValue: linux.SensorPowerState, - Value: v, + Value: signalValue, SensorSrc: linux.DataSrcDbus, IsDiagnostic: true, }, @@ -75,7 +78,8 @@ func newPowerState(s powerSignal, v any) *powerStateSensor { type stateWorker struct{} -func (w *stateWorker) Setup(ctx context.Context) *dbusx.Watch { +//nolint:exhaustruct +func (w *stateWorker) Setup(_ context.Context) *dbusx.Watch { return &dbusx.Watch{ Bus: dbusx.SystemBus, Names: []string{sleepSignal, shutdownSignal}, @@ -90,6 +94,7 @@ func (w *stateWorker) Watch(ctx context.Context, triggerCh chan dbusx.Trigger) c // Watch for state changes. go func() { defer close(sensorCh) + for { select { case <-ctx.Done(): @@ -112,7 +117,13 @@ func (w *stateWorker) Watch(ctx context.Context, triggerCh chan dbusx.Trigger) c // Send an initial state update (on, not suspended). go func() { - sensors, _ := w.Sensors(ctx) + sensors, err := w.Sensors(ctx) + if err != nil { + log.Debug().Err(err).Msg("Failed to retrieve sensors.") + + return + } + for _, s := range sensors { sensorCh <- s } diff --git a/internal/linux/power/screenLock.go b/internal/linux/power/screenLock.go index 28684182d..2da6b1e7e 100644 --- a/internal/linux/power/screenLock.go +++ b/internal/linux/power/screenLock.go @@ -7,7 +7,6 @@ package power import ( "context" - "errors" "github.com/rs/zerolog/log" @@ -25,25 +24,28 @@ func (s *screenlockSensor) Icon() string { if !ok { return "mdi:lock-alert" } + if state { return "mdi:eye-lock" } return "mdi:eye-lock-open" } -func newScreenlockEvent(v bool) *screenlockSensor { +//nolint:exhaustruct +func newScreenlockEvent(value bool) *screenlockSensor { return &screenlockSensor{ Sensor: linux.Sensor{ SensorTypeValue: linux.SensorScreenLock, IsBinary: true, SensorSrc: linux.DataSrcDbus, - Value: v, + Value: value, }, } } type screenLockWorker struct{} +//nolint:exhaustruct func (w *screenLockWorker) Setup(ctx context.Context) *dbusx.Watch { sessionPath := dbusx.GetSessionPath(ctx) return &dbusx.Watch{ @@ -58,6 +60,7 @@ func (w *screenLockWorker) Watch(ctx context.Context, triggerCh chan dbusx.Trigg sensorCh := make(chan sensor.Details) go func() { defer close(sensorCh) + for { select { case <-ctx.Done(): @@ -69,8 +72,10 @@ func (w *screenLockWorker) Watch(ctx context.Context, triggerCh chan dbusx.Trigg props, err := dbusx.ParsePropertiesChanged(event.Content) if err != nil { log.Warn().Err(err).Msg("Did not understand received trigger.") + continue } + if state, lockChanged := props.Changed[sessionLockedProp]; lockChanged { sensorCh <- newScreenlockEvent(dbusx.VariantToValue[bool](state)) } @@ -86,8 +91,8 @@ func (w *screenLockWorker) Watch(ctx context.Context, triggerCh chan dbusx.Trigg } // TODO: retrieve the current screen lock state when called. -func (w *screenLockWorker) Sensors(ctx context.Context) ([]sensor.Details, error) { - return nil, errors.New("unimplemented") +func (w *screenLockWorker) Sensors(_ context.Context) ([]sensor.Details, error) { + return nil, linux.ErrUnimplemented } func NewScreenLockWorker() (*linux.SensorWorker, error) { diff --git a/internal/linux/power/screenLockControl.go b/internal/linux/power/screenLockControl.go index 0c5c556ce..2ecefa1ab 100644 --- a/internal/linux/power/screenLockControl.go +++ b/internal/linux/power/screenLockControl.go @@ -34,12 +34,15 @@ func NewScreenLockControl(ctx context.Context) *mqtthass.ButtonEntity { if dbusScreensaverPath == "" { log.Warn().Msg("Could not determine screensaver method.") } + var err error + if dbusScreensaverMsg != nil { err = dbusx.Call(ctx, dbusx.SessionBus, dbusScreensaverPath, dbusScreensaverDest, dbusScreensaverLockMethod, dbusScreensaverMsg) } else { err = dbusx.Call(ctx, dbusx.SessionBus, dbusScreensaverPath, dbusScreensaverDest, dbusScreensaverLockMethod) } + if err != nil { log.Warn().Err(err).Msg("Could not lock screensaver.") } @@ -48,6 +51,7 @@ func NewScreenLockControl(ctx context.Context) *mqtthass.ButtonEntity { func getDesktopEnvScreensaverConfig() (dest, path string, msg *string) { desktop := os.Getenv("XDG_CURRENT_DESKTOP") + switch { case strings.Contains(desktop, "KDE"): return "org.freedesktop.ScreenSaver", "/ScreenSaver", nil diff --git a/internal/linux/problems/problems.go b/internal/linux/problems/problems.go index 078805ea7..9cb7e4a25 100644 --- a/internal/linux/problems/problems.go +++ b/internal/linux/problems/problems.go @@ -19,6 +19,11 @@ import ( "github.com/joshuar/go-hass-agent/pkg/linux/dbusx" ) +const ( + problemInterval = 15 * time.Minute + problemJitter = time.Minute +) + const ( dBusProblemsDest = "/org/freedesktop/problems" dBusProblemIntr = "org.freedesktop.problems" @@ -31,8 +36,8 @@ type problemsSensor struct { func (s *problemsSensor) Attributes() any { return struct { - ProblemList map[string]map[string]any `json:"Problem List"` - DataSource string `json:"Data Source"` + ProblemList map[string]map[string]any `json:"problem_list"` + DataSource string `json:"data_source"` }{ ProblemList: s.list, DataSource: linux.DataSrcDbus, @@ -41,37 +46,38 @@ func (s *problemsSensor) Attributes() any { func parseProblem(details map[string]string) map[string]any { parsed := make(map[string]any) - for k, v := range details { - switch k { + + for key, value := range details { + switch key { case "time": - timeValue, err := strconv.ParseInt(v, 10, 64) + timeValue, err := strconv.ParseInt(value, 10, 64) if err != nil { - log.Debug().Err(err).Msg("Could not parse problem time.") parsed["time"] = 0 } else { parsed["time"] = time.Unix(timeValue, 0).Format(time.RFC3339) } case "count": - countValue, err := strconv.Atoi(v) + countValue, err := strconv.Atoi(value) if err != nil { - log.Debug().Err(err).Msg("Could not parse problem count.") parsed["count"] = 0 } else { parsed["count"] = countValue } case "package", "reason": - parsed[k] = v + parsed[key] = value } } + return parsed } type worker struct{} -func (w *worker) Interval() time.Duration { return 15 * time.Minute } +func (w *worker) Interval() time.Duration { return problemInterval } -func (w *worker) Jitter() time.Duration { return time.Minute } +func (w *worker) Jitter() time.Duration { return problemJitter } +//nolint:exhaustruct func (w *worker) Sensors(ctx context.Context, _ time.Duration) ([]sensor.Details, error) { problems := &problemsSensor{ list: make(map[string]map[string]any), @@ -86,22 +92,25 @@ func (w *worker) Sensors(ctx context.Context, _ time.Duration) ([]sensor.Details return nil, fmt.Errorf("unable to retrieve the list of ABRT problems: %w", err) } - for _, p := range problemList { + for _, problem := range problemList { problemDetails, err := dbusx.GetData[map[string]string](ctx, dbusx.SystemBus, dBusProblemsDest, dBusProblemIntr, - dBusProblemIntr+".GetInfo", p, []string{"time", "count", "package", "reason"}) + dBusProblemIntr+".GetInfo", problem, []string{"time", "count", "package", "reason"}) if problemDetails == nil || err != nil { log.Debug().Msg("No problems retrieved.") } else { - problems.list[p] = parseProblem(problemDetails) + problems.list[problem] = parseProblem(problemDetails) } } + if len(problems.list) > 0 { problems.Value = len(problems.list) + return []sensor.Details{problems}, nil } + return nil, nil } diff --git a/internal/linux/system/dbusCommand.go b/internal/linux/system/dbusCommand.go index c221b76f0..cefc6ec0e 100644 --- a/internal/linux/system/dbusCommand.go +++ b/internal/linux/system/dbusCommand.go @@ -27,7 +27,7 @@ type dbusCommandMsg struct { Path dbus.ObjectPath `json:"path"` Method string `json:"method"` Args []any `json:"args"` - UseSessionPath bool `json:"useSessionPath"` + UseSessionPath bool `json:"use_session_path"` } func NewDBusCommandSubscription(ctx context.Context) *mqttapi.Subscription { @@ -37,6 +37,7 @@ func NewDBusCommandSubscription(ctx context.Context) *mqttapi.Subscription { if err := json.Unmarshal(p.Payload, &dbusMsg); err != nil { log.Warn().Err(err).Msg("could not unmarshal dbus MQTT message") + return } @@ -47,6 +48,7 @@ func NewDBusCommandSubscription(ctx context.Context) *mqttapi.Subscription { dbusType, ok := dbusx.DbusTypeMap[dbusMsg.Bus] if !ok { log.Warn().Msg("unsupported dbus type") + return } diff --git a/internal/linux/system/hwmon.go b/internal/linux/system/hwmon.go index 3c9dd0415..a2345f173 100644 --- a/internal/linux/system/hwmon.go +++ b/internal/linux/system/hwmon.go @@ -18,6 +18,11 @@ import ( "github.com/joshuar/go-hass-agent/pkg/linux/hwmon" ) +const ( + hwMonInterval = time.Minute + hwMonJitter = 5 * time.Second +) + type hwSensor struct { ExtraAttrs map[string]float64 hwType string @@ -27,24 +32,26 @@ type hwSensor struct { linux.Sensor } -func (s *hwSensor) asBool(h *hwmon.Sensor) { - s.Value = h.Value() +func (s *hwSensor) asBool(details *hwmon.Sensor) { + s.Value = details.Value() if v, ok := s.Value.(bool); ok && v { s.IconString = "mdi:alarm-light" } else { s.IconString = "mdi:alarm-light-off" } + s.IsBinary = true } -func (s *hwSensor) asFloat(h *hwmon.Sensor) { - s.Value = h.Value() - s.UnitsString = h.Units() - i, d := parseSensorType(h.SensorType.String()) +func (s *hwSensor) asFloat(details *hwmon.Sensor) { + s.Value = details.Value() + s.UnitsString = details.Units() + i, d := parseSensorType(details.SensorType.String()) s.IconString = i s.DeviceClassValue = d s.StateClassValue = types.StateClassMeasurement - for _, a := range h.Attributes { + + for _, a := range details.Attributes { s.ExtraAttrs[a.Name] = a.Value } } @@ -59,11 +66,11 @@ func (s *hwSensor) ID() string { func (s *hwSensor) Attributes() any { return struct { - Attributes map[string]float64 `json:"Extra Attributes,omitempty"` + Attributes map[string]float64 `json:"extra_attributes,omitempty"` NativeUnit string `json:"native_unit_of_measurement,omitempty"` - DataSource string `json:"Data Source"` - SensorType string `json:"Sensor Type"` - HWMonPath string `json:"SysFS Path"` + DataSource string `json:"data_source"` + SensorType string `json:"sensor_type"` + HWMonPath string `json:"sysfs_path"` }{ NativeUnit: s.UnitsString, DataSource: linux.DataSrcSysfs, @@ -73,42 +80,49 @@ func (s *hwSensor) Attributes() any { } } -func newHWSensor(s *hwmon.Sensor) *hwSensor { - hw := &hwSensor{ - name: s.Name(), - id: s.ID(), - hwType: s.SensorType.String(), - path: s.SysFSPath, +//nolint:exhaustruct +func newHWSensor(details *hwmon.Sensor) *hwSensor { + newSensor := &hwSensor{ + name: details.Name(), + id: details.ID(), + hwType: details.SensorType.String(), + path: details.SysFSPath, ExtraAttrs: make(map[string]float64), } - hw.IsDiagnostic = true - switch hw.hwType { + newSensor.IsDiagnostic = true + + switch newSensor.hwType { case hwmon.Alarm.String(), hwmon.Intrusion.String(): - hw.asBool(s) + newSensor.asBool(details) default: - hw.asFloat(s) + newSensor.asFloat(details) } - return hw + + return newSensor } type hwMonWorker struct{} -func (w *hwMonWorker) Interval() time.Duration { return time.Minute } +func (w *hwMonWorker) Interval() time.Duration { return hwMonInterval } -func (w *hwMonWorker) Jitter() time.Duration { return 5 * time.Second } +func (w *hwMonWorker) Jitter() time.Duration { return hwMonJitter } func (w *hwMonWorker) Sensors(_ context.Context, _ time.Duration) ([]sensor.Details, error) { - var sensors []sensor.Details hwmonSensors, err := hwmon.GetAllSensors() + sensors := make([]sensor.Details, 0, len(hwmonSensors)) + if err != nil && len(hwmonSensors) > 0 { log.Warn().Err(err).Msg("Errors fetching some chip/sensor values from hwmon API.") } + if err != nil && len(hwmonSensors) == 0 { return nil, fmt.Errorf("could not retrieve hwmon sensor details: %w", err) } + for _, s := range hwmonSensors { sensors = append(sensors, newHWSensor(s)) } + return sensors, nil } diff --git a/internal/linux/system/info.go b/internal/linux/system/info.go index 35e299ff4..f0f27d4a4 100644 --- a/internal/linux/system/info.go +++ b/internal/linux/system/info.go @@ -14,6 +14,7 @@ import ( type infoWorker struct{} +//nolint:exhaustruct func (w *infoWorker) Sensors(_ context.Context) ([]sensor.Details, error) { // Get distribution name and version. distro, distroVersion := linux.GetDistroDetails() diff --git a/internal/linux/system/time.go b/internal/linux/system/time.go index 555e6ed2d..38b5b32da 100644 --- a/internal/linux/system/time.go +++ b/internal/linux/system/time.go @@ -17,23 +17,29 @@ import ( "github.com/joshuar/go-hass-agent/internal/linux" ) +const ( + uptimeInterval = 15 * time.Minute + uptimeJitter = time.Minute +) + type timeSensor struct { linux.Sensor } +//nolint:exhaustive func (s *timeSensor) Attributes() any { switch s.SensorTypeValue { case linux.SensorUptime: return struct { NativeUnit string `json:"native_unit_of_measurement"` - DataSource string `json:"Data Source"` + DataSource string `json:"data_source"` }{ NativeUnit: s.UnitsString, DataSource: linux.DataSrcProcfs, } default: return struct { - DataSource string `json:"Data Source"` + DataSource string `json:"data_source"` }{ DataSource: linux.DataSrcProcfs, } @@ -42,10 +48,11 @@ func (s *timeSensor) Attributes() any { type timeWorker struct{} -func (w *timeWorker) Interval() time.Duration { return 15 * time.Minute } +func (w *timeWorker) Interval() time.Duration { return uptimeInterval } -func (w *timeWorker) Jitter() time.Duration { return time.Minute } +func (w *timeWorker) Jitter() time.Duration { return uptimeJitter } +//nolint:exhaustruct func (w *timeWorker) Sensors(ctx context.Context, _ time.Duration) ([]sensor.Details, error) { return []sensor.Details{ &timeSensor{ @@ -82,23 +89,28 @@ func NewTimeWorker() (*linux.SensorWorker, error) { } func getUptime(ctx context.Context) any { - u, err := host.UptimeWithContext(ctx) + value, err := host.UptimeWithContext(ctx) if err != nil { log.Debug().Caller().Err(err). Msg("Failed to retrieve uptime.") + return sensor.StateUnknown } + epoch := time.Unix(0, 0) - uptime := time.Unix(int64(u), 0) + uptime := time.Unix(int64(value), 0) + return uptime.Sub(epoch).Hours() } func getBoottime(ctx context.Context) string { - u, err := host.BootTimeWithContext(ctx) + value, err := host.BootTimeWithContext(ctx) if err != nil { log.Debug().Caller().Err(err). Msg("Failed to retrieve boottime.") + return sensor.StateUnknown } - return time.Unix(int64(u), 0).Format(time.RFC3339) + + return time.Unix(int64(value), 0).Format(time.RFC3339) } diff --git a/internal/linux/user/users.go b/internal/linux/user/users.go index 50d3d60d1..6dec81805 100644 --- a/internal/linux/user/users.go +++ b/internal/linux/user/users.go @@ -7,6 +7,7 @@ package user import ( "context" + "fmt" "strings" "github.com/rs/zerolog/log" @@ -33,43 +34,51 @@ type usersSensor struct { func (s *usersSensor) Attributes() any { return struct { - DataSource string `json:"Data Source"` - Usernames []string `json:"Usernames"` + DataSource string `json:"data_source"` + Usernames []string `json:"usernames"` }{ DataSource: linux.DataSrcDbus, Usernames: s.userNames, } } -func (s *usersSensor) updateUsers(ctx context.Context) { +func (s *usersSensor) updateUsers(ctx context.Context) error { userData, err := dbusx.GetData[[][]any](ctx, dbusx.SystemBus, loginBasePath, loginBaseInterface, listSessionsMethod) if err != nil { - log.Warn().Err(err).Msg("Could not retrieve users from D-Bus.") - return + return fmt.Errorf("could not retrieve users from D-Bus: %w", err) } + s.Value = len(userData) + var users []string + for _, u := range userData { if user, ok := u[2].(string); ok { users = append(users, user) } } + s.userNames = users + + return nil } +//nolint:exhaustruct func newUsersSensor() *usersSensor { - s := &usersSensor{} - s.SensorTypeValue = linux.SensorUsers - s.UnitsString = "users" - s.IconString = "mdi:account" - s.StateClassValue = types.StateClassMeasurement - return s + userSensor := &usersSensor{} + userSensor.SensorTypeValue = linux.SensorUsers + userSensor.UnitsString = "users" + userSensor.IconString = "mdi:account" + userSensor.StateClassValue = types.StateClassMeasurement + + return userSensor } type worker struct { sensor *usersSensor } +//nolint:exhaustruct func (w *worker) Setup(_ context.Context) *dbusx.Watch { return &dbusx.Watch{ Bus: dbusx.SystemBus, @@ -82,37 +91,45 @@ func (w *worker) Setup(_ context.Context) *dbusx.Watch { func (w *worker) Watch(ctx context.Context, triggerCh chan dbusx.Trigger) chan sensor.Details { sensorCh := make(chan sensor.Details) + sendUpdate := func() { + err := w.sensor.updateUsers(ctx) + if err != nil { + log.Debug().Err(err).Msg("Update failed.") + + return + } + sensorCh <- w.sensor + } + go func() { defer close(sensorCh) + for { select { case <-ctx.Done(): log.Debug().Msg("Stopped users sensors.") + return case event := <-triggerCh: if !strings.Contains(event.Signal, sessionAddedSignal) && !strings.Contains(event.Signal, sessionRemovedSignal) { continue } - go func() { - w.sensor.updateUsers(ctx) - sensorCh <- w.sensor - }() + + go sendUpdate() } } }() // Send an initial sensor update. - go func() { - w.sensor.updateUsers(ctx) - sensorCh <- w.sensor - }() + go sendUpdate() return sensorCh } func (w *worker) Sensors(ctx context.Context) ([]sensor.Details, error) { - w.sensor.updateUsers(ctx) - return []sensor.Details{w.sensor}, nil + err := w.sensor.updateUsers(ctx) + + return []sensor.Details{w.sensor}, err } func NewUserWorker() (*linux.SensorWorker, error) {