joshuar-go-hass-agent/internal/linux/battery/sensor.go

177 lines
4.6 KiB
Go

// Copyright 2025 Joshua Rich <joshua.rich@gmail.com>.
// SPDX-License-Identifier: MIT
package battery
import (
"fmt"
"math"
"strings"
"github.com/godbus/dbus/v5"
"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"
"github.com/joshuar/go-hass-agent/pkg/linux/dbusx"
)
// newBatterySensor creates a new sensor for Home Assistant from a battery
// property.
func newBatterySensor(battery *upowerBattery, sensorType sensorType, value dbus.Variant) sensor.Entity {
var (
name, id, icon, units string
deviceClass types.DeviceClass
stateClass types.StateClass
)
if battery.model == "" {
name = battery.id + " " + sensorType.String()
} else {
name = battery.model + " " + sensorType.String()
}
id = battery.id + "_" + strings.ToLower(strcase.ToSnake(sensorType.String()))
switch sensorType {
case typePercentage:
icon = batteryPercentIcon(value.Value())
deviceClass = types.SensorDeviceClassBattery
stateClass = types.StateClassMeasurement
units = "%"
case typeTemp:
deviceClass = types.SensorDeviceClassTemperature
stateClass = types.StateClassMeasurement
units = "°C"
case typeEnergyRate:
icon = batteryChargeIcon(value.Value())
deviceClass = types.SensorDeviceClassPower
stateClass = types.StateClassMeasurement
units = "W"
case typeEnergy:
deviceClass = types.SensorDeviceClassEnergyStorage
stateClass = types.StateClassMeasurement
units = "Wh"
case typeVoltage:
deviceClass = types.SensorDeviceClassVoltage
stateClass = types.StateClassMeasurement
units = "V"
default:
icon = batteryIcon
}
return sensor.NewSensor(
sensor.WithName(name),
sensor.WithID(id),
sensor.WithDeviceClass(deviceClass),
sensor.WithStateClass(stateClass),
sensor.WithUnits(units),
sensor.AsDiagnostic(),
sensor.WithState(
sensor.WithIcon(icon),
sensor.WithValue(generateSensorState(sensorType, value.Value())),
sensor.WithAttributes(generateSensorAttributes(sensorType, battery)),
),
)
}
// generateSensorState will take the raw value (from D-Bus) and format it as
// appropriate for the battery sensor type.
func generateSensorState(sensorType sensorType, value any) any {
if value == nil {
return sensor.StateUnknown
}
switch sensorType {
case typeVoltage, typeTemp, typeEnergy, typeEnergyRate, typePercentage:
if value, ok := value.(float64); !ok {
return sensor.StateUnknown
} else {
return value
}
case typeState:
if value, ok := value.(uint32); !ok {
return sensor.StateUnknown
} else {
return chargingState(value).String()
}
case typeLevel:
if value, ok := value.(uint32); !ok {
return sensor.StateUnknown
} else {
return level(value).String()
}
default:
if value, ok := value.(string); !ok {
return sensor.StateUnknown
} else {
return value
}
}
}
// generateSensorAttributes will add some appropriate attributes to certain
// battery sensor types.
func generateSensorAttributes(sensorType sensorType, battery *upowerBattery) map[string]any {
attributes := make(map[string]any)
attributes["data_source"] = linux.DataSrcDbus
switch sensorType {
case typeEnergyRate:
var (
variant dbus.Variant
err error
voltage, energy float64
)
if variant, err = battery.getProp(typeVoltage); err == nil {
voltage, _ = dbusx.VariantToValue[float64](variant) //nolint:lll,errcheck // its not important if this attribute value is not correct due to errors
}
if variant, err = battery.getProp(typeEnergy); err == nil {
energy, _ = dbusx.VariantToValue[float64](variant) //nolint:lll,errcheck // its not important if this attribute value is not correct due to errors
}
attributes["voltage"] = voltage
attributes["energy"] = energy
case typePercentage, typeLevel:
attributes["battery_type"] = battery.battType.String()
}
return attributes
}
// batteryPercentIcon takes the percent value of level and returns an
// appropriate icon to represent it.
//
//nolint:mnd
func batteryPercentIcon(v any) string {
percentage, ok := v.(float64)
if !ok {
return batteryIcon + "-unknown"
}
if percentage >= 95 {
return batteryIcon
}
return fmt.Sprintf("%s-%d", batteryIcon, int(math.Round(percentage/10)*10))
}
// batteryChargeIcon takes the value of the battery charge and returns an
// appropriate icon to represent it.
func batteryChargeIcon(v any) string {
energyRate, ok := v.(float64)
if !ok {
return batteryIcon
}
if math.Signbit(energyRate) {
return batteryIcon + "-minus"
}
return batteryIcon + "-plus"
}