joshuar-go-hass-agent/docs/development/sensors.md

6.9 KiB

Device Sensors

Code Location

Platform/device sensor code belongs under GOARCH/. Ideally, create a new .go file for each sensor or sensor group you are adding.

Representing data as a sensor

For data to be sent to Home Assistant, it needs to implement a few interfaces.

Firstly, the data should satisfy the sensor.SensorState interface, which represents the minimum data required to be sent to Home Assistant when the data changes:

type SensorState interface {
	ID() string
	Icon() string
	State() any
	SensorType() types.SensorClass
	Units() string
	Attributes() any
}

Initially however, the sensor will need to be registered with Home Assistant and needs to satisfy the sensor.SensorRegistration interface:

type SensorRegistration interface {
	SensorState
	Name() string
	DeviceClass() types.DeviceClass
	StateClass() types.StateClass
	Category() string
}

The recommended approach is to define your own custom type (usually a struct) that satisfies both of these interfaces.

An explanation of the methods follows.

Name() string

A friendly name for this sensor. It will be what is shown in the Home Assistant UI for the sensor.

An example would be “My Network Sensor”.

ID() string

A unique ID for the sensor. This is used by Home Assistant to identify this particular sensor and store its state data in its database. This should be unique and never change. It should be formatted as snake case and all lowercase.

An example would be “my_network_sensor_1”.

Icon() string

The Material Design Icon representing the current state. It can be changed dynamically based on the current state or remain constant. Format is “mdi:icon_name”.

SensorType() types.SensorClass

The type of sensor. This is either types.Sensor (mapping to a Home Assistant Sensor Entity) or types.Binary (mapping to a Home Assistant Binary Sensor Entity).

DeviceClass() types.DeviceClass

The device class for this sensor, which reflects the type of measurement it is recording. This maps to the Home Assistant Device Classes, such as “temperature”, “data_size”, “speed” etc. See also source code.

StateClass() types.StateClass

For the values, what kind of state they represent, such as an instataneous measurement, an increasing/decreasing value, etc. This maps to the Home Assistant State Classes. See also source code.

State() interface

The current state (value) of the sensor. For Home Assistant, a valid return type would be bool (for TypeBinary), float, int, or string.

Units() string

What units the state should be represented as. Generally, if this sensor has a device class, that will define the units.

Category() string

This affects how the sensor is displayed in the interface. Generally, return “diagnostic” for an entity exposing some configuration parameter or diagnostics of a device or an empty string for anything else.

Attributes() interface

Any additional attributes or state/values you would like to associate with the sensor. This should be formatted as a struct{} that can be marshalled into valid JSON.

Managing sensor updates

To track and send sensor updates to Home Assistant, create a function with the following signature:

func SensorUpdater(context.Context) chan sensor.Details
  • The context.Context parameter is a context which you should respect a potential cancellation of (see below). It can also contain any device specific context values, such as common API configuration data.
  • The sensor.Details is a catch-all interface that could be either a sensor.SensorState or sensor.SensorRegistration. If you custom type satisfies those interfaces, it can be passed out through this channel.

Within this function, you should create the sensors you want to report to Home Assistant and set up a way to send updates. How this is achieved will vary and can be done in any way. For example, the battery sensor data for Linux is manifested by listening for D-Bus signals that indicate a battery property has changed.

You can create as many sensors as you require. Whenever you have a sensor update, send it via the sensor.Details channel.

As mentioned you should respect/expect cancellation of the context received as a parameter. You can do this by including code similar to the following in your function:

go func() {
  // Likely you'll want to clean up the sensor channel...
  // defer close(chan sensor.Sensor)
  <-ctx.Done
  // any additional clean up code can go here...
}

Pseudo Go code of what a complete function would look like:

func SensorUpdater(ctx context.Context) chan sensor.Details {
  sensorCh := make(chan sensor.Details, 1)
  ...code to set up your sensors...
  for ...some timer, event channel, other loop... {
    ...code to create a sensor object...
    // send your sensor updates
    sensorCh <- sensor
  }
  go func() {
    defer close(sensorCh)
    <-ctx.Done()
  }
}

Helper Functions

There are some helper functions that might be useful to you in internal/device/helpers. For example, the PollSensors function can be used to update sensors on an interval. It adds a bit of jitter to your interval as well to avoid any “thundering herd” problems.

Sensor tracking

To have your sensors tracked by the agent, the updater function needs to be added to the list of these functions returned by the platform-specific sensorWorkers() function. It has the following signature:

sensorWorkers() []func(context.Context) chan sensor.Details

An example for Linux can be found in the source code. This is called by the agent code to retrieve the list of updater functions, that the agent then executes in its own goroutine. Each updater function then runs for the lifetime of the agent, sending updates via the sensor.Details channel back to the agent, which handles tracking internally and forwarding to Home Assistant.

Examples

Linux

  • A reusable, common struct is implemented that satisfies the sensor.SensorState and Sensor.SensorRegistration interfaces in internal/linux/sensor.go.
  • Each sensor then encapsulates this common struct and uses it as appropriate for its needs. As an example see the load averages sensors in internal/linux/cpu/loadavgs.go.