Skip to content

Commit

Permalink
feat(linux): ✨ add sensors for IO ops in progress per disk (and total…
Browse files Browse the repository at this point in the history
… of all disks)
  • Loading branch information
joshuar committed Sep 30, 2024
1 parent 70dbac2 commit ea33a54
Show file tree
Hide file tree
Showing 3 changed files with 63 additions and 82 deletions.
122 changes: 51 additions & 71 deletions internal/linux/disk/ioSensors.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,27 +7,29 @@
package disk

import (
"strings"
"time"
"unicode"

"github.com/iancoleman/strcase"

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

const (
diskReads ioSensor = iota // Disk Reads
diskWrites // Disk Writes
diskReadRate // Disk Read Rate
diskWriteRate // Disk Write Rate
diskReads ioSensor = iota // Disk Reads
diskWrites // Disk Writes
diskReadRate // Disk Read Rate
diskWriteRate // Disk Write Rate
diskIOInProgress // Disk IOs In Progress

diskRateUnits = "kB/s"
diskCountUnits = "requests"
diskIOsUnits = "ops"

ioReadsIcon = "mdi:file-upload"
ioWritesIcon = "mdi:file-download"
ioOpsIcon = "mdi:content-save"
)

type ioSensor int
Expand All @@ -47,6 +49,8 @@ func (s *diskIOSensor) update(stats map[stat]uint64, delta time.Duration) {
s.Value = stats[TotalReads]
case diskWrites:
s.Value = stats[TotalWrites]
case diskIOInProgress:
s.Value = stats[ActiveIOs]
case diskReadRate:
curr = stats[TotalSectorsRead]
case diskWriteRate:
Expand Down Expand Up @@ -81,91 +85,67 @@ func (s *diskIOSensor) updateAttributes(stats map[stat]uint64) {
}
}

func newDiskIOSensor(boottime time.Time, device *device, sensorType ioSensor) *diskIOSensor {
newSensor := &diskIOSensor{
Entity: &sensor.Entity{
Name: generateName(device.id, sensorType.String()),
StateClass: types.StateClassTotalIncreasing,
Units: diskCountUnits,
State: &sensor.State{
ID: generateID(device.id, sensorType.String()),
Icon: generateIcon(sensorType),
Attributes: map[string]any{
"data_source": linux.DataSrcSysfs,
"native_unit_of_measurement": diskRateUnits,
"last_reset": boottime.Format(time.RFC3339),
},
},
},
sensorType: sensorType,
}

if device.model != "" {
newSensor.Attributes["device_model"] = device.model
}

if device.sysFSPath != "" {
newSensor.Attributes["sysfs_path"] = device.sysFSPath
}

if device.id != "total" {
newSensor.Category = types.CategoryDiagnostic
}

return newSensor
}
func newDiskIOSensor(device *device, sensorType ioSensor, boottime time.Time) *diskIOSensor {
r := []rune(device.id)
name := string(append([]rune{unicode.ToUpper(r[0])}, r[1:]...)) + " " + sensorType.String()
id := strings.ToLower(device.id + "_" + strings.ReplaceAll(sensorType.String(), " ", "_"))

func newDiskIORateSensor(device *device, sensorType ioSensor) *diskIOSensor {
newSensor := &diskIOSensor{
// Base diskIOSensor fields.
ioSensor := &diskIOSensor{
Entity: &sensor.Entity{
Name: generateName(device.id, sensorType.String()),
DeviceClass: types.SensorDeviceClassDataRate,
StateClass: types.StateClassMeasurement,
Units: diskRateUnits,
Name: name,
State: &sensor.State{
ID: generateID(device.id, sensorType.String()),
Icon: generateIcon(sensorType),
ID: id,
Attributes: map[string]any{
"data_source": linux.DataSrcSysfs,
"native_unit_of_measurement": diskRateUnits,
"data_source": linux.DataSrcSysfs,
},
},
},
sensorType: sensorType,
}

// Add attributes from device if available.
if device.model != "" {
newSensor.Attributes["device_model"] = device.model
ioSensor.Attributes["device_model"] = device.model
}

if device.sysFSPath != "" {
newSensor.Attributes["sysfs_path"] = device.sysFSPath
ioSensor.Attributes["sysfs_path"] = device.sysFSPath
}

if device.id != totalsID {
newSensor.Category = types.CategoryDiagnostic
if device.id != "total" {
ioSensor.Category = types.CategoryDiagnostic
}

return newSensor
}

func generateName(id string, sensorType string) string {
r := []rune(id)

return string(append([]rune{unicode.ToUpper(r[0])}, r[1:]...)) + " " + sensorType
}
// Fill out additional fields based on sensor type.
switch sensorType {
case diskIOInProgress:
ioSensor.Icon = ioOpsIcon
ioSensor.StateClass = types.StateClassMeasurement
ioSensor.Units = diskIOsUnits
case diskReads, diskWrites:
if sensorType == diskReads {
ioSensor.Icon = ioReadsIcon
} else {
ioSensor.Icon = ioWritesIcon
}

func generateID(id string, sensorType string) string {
return id + "_" + strcase.ToSnake(sensorType)
}
ioSensor.Units = diskCountUnits
ioSensor.Attributes["native_unit_of_measurement"] = diskCountUnits
ioSensor.StateClass = types.StateClassTotalIncreasing
ioSensor.Attributes["last_reset"] = boottime.Format(time.RFC3339)
case diskReadRate, diskWriteRate:
if sensorType == diskReadRate {
ioSensor.Icon = ioReadsIcon
} else {
ioSensor.Icon = ioWritesIcon
}

func generateIcon(sensorType ioSensor) string {
switch sensorType {
case diskReads, diskReadRate:
return ioReadsIcon
case diskWrites, diskWriteRate:
return ioWritesIcon
ioSensor.Units = diskRateUnits
ioSensor.Attributes["native_unit_of_measurement"] = diskRateUnits
ioSensor.DeviceClass = types.SensorDeviceClassDataRate
ioSensor.StateClass = types.StateClassMeasurement
}

return "mdi:file"
return ioSensor
}
5 changes: 3 additions & 2 deletions internal/linux/disk/ioSensors_generated.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 9 additions & 9 deletions internal/linux/disk/ioWorker.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,8 @@ import (
const (
ratesUpdateInterval = 5 * time.Second
ratesUpdateJitter = time.Second

ratesWorkerID = "disk_rates_sensors"

totalsID = "total"
ratesWorkerID = "disk_rates_sensors"
totalsID = "total"
)

// ioWorker creates sensors for disk IO counts and rates per device. It
Expand Down Expand Up @@ -79,7 +77,8 @@ func (w *ioWorker) Sensors(ctx context.Context) ([]sensor.Entity, error) {
return nil, fmt.Errorf("could not fetch disk devices: %w", err)
}

sensors := make([]sensor.Entity, 0, 4*len(deviceNames)+4) //nolint:mnd
var sensors []sensor.Entity

statsTotals := make(map[stat]uint64)

// Get the current device info and stats for all valid devices.
Expand Down Expand Up @@ -138,9 +137,10 @@ func NewIOWorker(ctx context.Context) (*linux.PollingSensorWorker, error) {

func newDeviceSensors(boottime time.Time, dev *device) []*diskIOSensor {
return []*diskIOSensor{
newDiskIOSensor(boottime, dev, diskReads),
newDiskIOSensor(boottime, dev, diskWrites),
newDiskIORateSensor(dev, diskReadRate),
newDiskIORateSensor(dev, diskWriteRate),
newDiskIOSensor(dev, diskReads, boottime),
newDiskIOSensor(dev, diskWrites, boottime),
newDiskIOSensor(dev, diskReadRate, boottime),
newDiskIOSensor(dev, diskWriteRate, boottime),
newDiskIOSensor(dev, diskIOInProgress, boottime),
}
}

0 comments on commit ea33a54

Please sign in to comment.