Skip to content

Commit

Permalink
fix(linux/hwmon)!: refactor sensor parsing (again)
Browse files Browse the repository at this point in the history
BREAKING CHANGE: This is another refactoring of
the hardware sensor (hwmon) parsing code. This
hsould handle duplicate devices and generate
unique sensors for all of them. As a result
entities in Home Assistant will be renamed (again)
which may break any automations and other
functionality using the current names.
  • Loading branch information
joshuar committed May 5, 2024
1 parent 5e6aeb6 commit de865f1
Show file tree
Hide file tree
Showing 2 changed files with 136 additions and 90 deletions.
22 changes: 14 additions & 8 deletions internal/linux/system/hwmon.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ package system

import (
"context"
"fmt"
"strconv"
"time"

"github.com/rs/zerolog/log"
Expand All @@ -30,14 +28,12 @@ type hwSensor struct {
}

func (s *hwSensor) asBool(h *hwmon.Sensor) {
// we don't care if the value cannot be parsed, treat it as false
value, _ := strconv.ParseBool(fmt.Sprint(int(h.Value())))
if value {
s.Value = h.Value()
if v, ok := s.Value.(bool); ok && v {
s.IconString = "mdi:alarm-light"
} else {
s.IconString = "mdi:alarm-light-off"
}
s.Value = value
s.IsBinary = true
}

Expand Down Expand Up @@ -97,6 +93,8 @@ func newHWSensor(s *hwmon.Sensor) *hwSensor {

func HWSensorUpdater(ctx context.Context) chan sensor.Details {
sensorCh := make(chan sensor.Details)

// update will fetch all hardware sensors and send them to Home Assistant.
update := func(_ time.Duration) {
allSensors, err := hwmon.GetAllSensors()
if err != nil && len(allSensors) > 0 {
Expand All @@ -107,11 +105,19 @@ func HWSensorUpdater(ctx context.Context) chan sensor.Details {
return
}
for _, s := range allSensors {
hwSensor := newHWSensor(s)
sensorCh <- hwSensor
go func(s *hwmon.Sensor) {
hwSensor := newHWSensor(s)
sensorCh <- hwSensor
}(s)
}
}

// send all sensors as an initial update
go func() {
update(0)
}()

// continue sending sensors on an interval
go helpers.PollSensors(ctx, update, time.Minute, time.Second*5)
go func() {
defer close(sensorCh)
Expand Down
204 changes: 122 additions & 82 deletions pkg/linux/hwmon/hwmon.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"sync"

"github.com/iancoleman/strcase"
"github.com/rs/zerolog/log"
"github.com/sourcegraph/conc/pool"
"golang.org/x/text/cases"
"golang.org/x/text/language"
Expand Down Expand Up @@ -44,22 +45,9 @@ type SensorType int
// API. These are retrieved from the directories in the sysfs /sys/devices tree
// under /sys/class/hwmon/hwmon*.
type Chip struct {
// Name is the descriptive label for the chip, if any.
Name string
// Sensors is a slice of all sensors exposed by this chip.
Name string
id string
Sensors []*Sensor
chipID int
}

// update ensures the chip name is unique. This is needed for some drivers that
// duplicate chip names (for example drivetemp, which exposes any temperature
// sensors for disk drives with each drive having the chip name "drivetemp").
func (c *Chip) update(newID int) {
c.chipID = newID
c.Name += " " + strconv.Itoa(c.chipID)
for i := range c.Sensors {
c.Sensors[i].chip = c.Name
}
}

func processChip(path string) (*Chip, error) {
Expand All @@ -70,6 +58,7 @@ func processChip(path string) (*Chip, error) {

c := &Chip{
Name: n,
id: filepath.Base(path),
}

sensors, err := getSensors(path)
Expand All @@ -90,17 +79,9 @@ func GetAllChips() ([]*Chip, error) {
}

p := pool.New().WithErrors()
lastID := make(map[string]int)
var mu sync.Mutex
for _, f := range files {
p.Go(func() error {
chip, err := processChip(filepath.Join(hwmonPath, f.Name()))
mu.Lock()
defer mu.Unlock()
if _, ok := lastID[chip.Name]; ok {
lastID[chip.Name]++
}
chip.update(lastID[chip.Name])
chips = append(chips, chip)
return err
})
Expand All @@ -114,7 +95,8 @@ func GetAllChips() ([]*Chip, error) {
// a name. The Sensor will also have a value. It may also have zero or more
// Attributes, which are additional measurements like max/min/avg of the value.
type Sensor struct {
chip string
chipLabel string
chipID string
deviceModel string
label string
id string
Expand All @@ -125,44 +107,100 @@ type Sensor struct {
// values for the sensor.
Attributes []Attribute
scaleFactor float64
value float64
SensorType SensorType
}

// Value returns the sensor value.
func (s *Sensor) Value() float64 {
return s.value / s.scaleFactor
// Value returns the sensor value. This will be either a bool for alarm and
// intrusion sensors, or a float64 for all other types of sensors.
func (s *Sensor) Value() any {
var path string
switch s.SensorType {
case Alarm:
path = filepath.Join(s.SysFSPath, s.id+"_alarm")
value, err := getValueAsBool(path)
if err != nil {
log.Debug().Err(err).Str("sensor", s.Name()).Msg("Problem fetching sensor value.")
return nil
}
return value
case Intrusion:
path = filepath.Join(s.SysFSPath, s.id+"_intrusion")
value, err := getValueAsBool(path)
if err != nil {
log.Debug().Err(err).Str("sensor", s.Name()).Msg("Problem fetching sensor value.")
return nil
}
return value
default:
path = filepath.Join(s.SysFSPath, s.id+"_input")
value, err := getValueAsFloat(path)
if err != nil {
log.Debug().Err(err).Str("sensor", s.Name()).Msg("Problem fetching sensor value.")
return 0.0
}
return value / s.scaleFactor
}
}

// Name returns a name for the sensor. It will be derived from the chip name
// plus either any label, else name of the sensor itself.
// Name returns a formatted string as the name for the sensor. It will be
// derived from the chip name plus either any label, else name of the sensor
// itself.
func (s *Sensor) Name() string {
c := cases.Title(language.AmericanEnglish)
var chipFormatted string
capitaliser := cases.Title(language.English)
var name strings.Builder
if s.deviceModel != "" {
chipFormatted = s.deviceModel
name.WriteString(s.deviceModel)
} else {
chipFormatted = c.String(strings.ReplaceAll(s.chip, "_", " "))
name.WriteString("Hardware Sensor")
if s.chipLabel != "" {
name.WriteString(" ")
name.WriteString(capitaliser.String(strings.ReplaceAll(s.chipLabel, "_", " ")))
}
}
idFormatted := c.String(s.id)
labelFormatted := c.String(s.label)
switch {
case s.SensorType == Alarm || s.SensorType == Intrusion:
return chipFormatted + " " + idFormatted + " " + labelFormatted
case s.label != "":
return chipFormatted + " " + labelFormatted
default:
return chipFormatted + " " + idFormatted
name.WriteString(" ")
if s.SensorType == Alarm || s.SensorType == Intrusion {
if !strings.Contains(s.id, "_") {
name.WriteString(capitaliser.String(s.id))
name.WriteString(" ")
}
name.WriteString(capitaliser.String(s.label))
} else {
if s.label != "" {
name.WriteString(capitaliser.String(s.label))
} else {
name.WriteString(capitaliser.String(s.id))
}
}
return name.String()
}

// ID returns a string that can be used as a unique identifier for this sensor.
// It combines the chip name and sensor id from hwmon to create a unique string.
// Chip returns a formatted string for identifying the chip to which this sensor
// belongs.
func (s *Sensor) Chip() string {
if s.deviceModel != "" {
return s.deviceModel
}
if s.chipLabel != "" {
return s.chipLabel
}
return s.chipID
}

// ID returns a formatted string that can be used as a unique identifier for
// this sensor. This will be some combination of the chip and sensor details, as
// appropriate.
func (s *Sensor) ID() string {
var id strings.Builder
id.WriteString(s.chipID)
id.WriteString("_")
id.WriteString(s.chipLabel)
id.WriteString("_")
id.WriteString(s.id)
if s.SensorType == Alarm || s.SensorType == Intrusion {
return strcase.ToSnake(s.chip + "_" + s.id + "_" + s.SensorType.String())
id.WriteString("_")
id.WriteString(s.SensorType.String())
}
return strcase.ToSnake(s.chip + "_" + s.id)
return strcase.ToSnake(id.String())
}

// Units returns the units for the value of this sensor.
Expand All @@ -173,7 +211,7 @@ func (s *Sensor) Units() string {
// String will format the sensor name and value as a pretty string.
func (s *Sensor) String() string {
var b strings.Builder
fmt.Fprintf(&b, "%s: %.3f %s [%s] (id: %s, path: %s)", s.Name(), s.Value(), s.Units(), s.SensorType, s.ID(), s.SysFSPath)
fmt.Fprintf(&b, "%s: %v %s [%s] (id: %s, path: %s, chip: %s)", s.Name(), s.Value(), s.Units(), s.SensorType, s.ID(), s.SysFSPath, s.Chip())
for i, a := range s.Attributes {
if i == 0 {
fmt.Fprintf(&b, " (")
Expand All @@ -190,35 +228,26 @@ func (s *Sensor) String() string {
}

func (s *Sensor) updateFromFile(file *sensorFile) error {
path := filepath.Join(file.path, file.filename)
switch {
case file.sensorAttr == "input":
case file.sensorAttr == "label":
l, err := file.getValueAsString()
l, err := getValueAsString(path)
if err != nil {
return err
}
s.label = l
case file.sensorAttr == "input":
v, err := file.getValueAsFloat()
if err != nil {
return err
}
s.value = v
case strings.Contains(file.sensorAttr, "alarm"):
v, err := file.getValueAsFloat()
if err != nil {
return err
if b, _, ok := strings.Cut(file.sensorAttr, "_"); ok {
s.label = file.sensorType + " " + b + " Alarm"
s.id += "_" + b
} else {
s.label = "Alarm"
}
s.value = v
s.label = "alarm"
case strings.Contains(file.sensorAttr, "intrusion"):
v, err := file.getValueAsFloat()
if err != nil {
return err
}
s.value = v
s.label = "intrusion"
default:
v, err := file.getValueAsFloat()
v, err := getValueAsFloat(path)
if err != nil {
return err
}
Expand Down Expand Up @@ -246,18 +275,6 @@ type sensorFile struct {
sensorAttr string
}

func (f *sensorFile) getValueAsString() (string, error) {
return getFileContents(filepath.Join(f.path, f.filename))
}

func (f *sensorFile) getValueAsFloat() (float64, error) {
strValue, err := getFileContents(filepath.Join(f.path, f.filename))
if err != nil {
return 0, err
}
return strconv.ParseFloat(strValue, 64)
}

func (f *sensorFile) getSensorType() (sensorType SensorType, scaleFactor float64, units string) {
switch {
case strings.Contains(f.sensorAttr, "intrusion"):
Expand Down Expand Up @@ -294,10 +311,12 @@ func getSensors(path string) ([]*Sensor, error) {
}

// retrieve the chip name
chip, err := getFileContents(filepath.Join(path, "name"))
if err != nil {
return nil, err
var chipLabel, chipID string
l, err := getFileContents(filepath.Join(path, "name"))
if err == nil {
chipLabel = l
}
chipID = filepath.Base(path)

var deviceModel string
fh, err := os.Stat(filepath.Join(path, "device", "model"))
Expand Down Expand Up @@ -344,7 +363,8 @@ func getSensors(path string) ([]*Sensor, error) {
}
// otherwise, its a new sensor, start tracking it
allSensors[trackerID] = &Sensor{
chip: chip,
chipLabel: chipLabel,
chipID: chipID,
deviceModel: deviceModel,
id: sensorFile.sensorType,
SensorType: t,
Expand Down Expand Up @@ -387,3 +407,23 @@ func getFileContents(p string) (string, error) {
}
return strings.TrimSpace(string(b)), nil
}

func getValueAsString(p string) (string, error) {
return getFileContents(p)
}

func getValueAsFloat(p string) (float64, error) {
strValue, err := getFileContents(p)
if err != nil {
return 0, err
}
return strconv.ParseFloat(strValue, 64)
}

func getValueAsBool(p string) (bool, error) {
strValue, err := getFileContents(p)
if err != nil {
return false, err
}
return strconv.ParseBool(strValue)
}

0 comments on commit de865f1

Please sign in to comment.