126 lines
3.2 KiB
Go
126 lines
3.2 KiB
Go
// Copyright 2024 Joshua Rich <joshua.rich@gmail.com>.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package system
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"os/exec"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"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 (
|
|
chronyPollInterval = 5 * time.Minute
|
|
chronyPollJitter = 10 * time.Second
|
|
|
|
chronyWorkerID = "chrony_sensors"
|
|
|
|
sensorStat = "System time"
|
|
)
|
|
|
|
type chronyWorker struct {
|
|
chronycPath string
|
|
}
|
|
|
|
//revive:disable:unused-receiver
|
|
func (w *chronyWorker) UpdateDelta(_ time.Duration) {}
|
|
|
|
func (w *chronyWorker) Sensors(_ context.Context) ([]sensor.Entity, error) {
|
|
// Get chrony tracking stats via chronyc.
|
|
stats, err := w.getChronyTrackingStats()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not retrieve chrony stats: %w", err)
|
|
}
|
|
// Generate a sensor.
|
|
return []sensor.Entity{newChronyOffsetSensor(stats)}, nil
|
|
}
|
|
|
|
// getChronyTrackingStats executes chronyc, parsing its output and returning the
|
|
// individual stats in a map.
|
|
func (w *chronyWorker) getChronyTrackingStats() (map[string]string, error) {
|
|
chronycOutput, err := exec.Command(w.chronycPath, "-n", "tracking").Output()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to run chronyc: %w", err)
|
|
}
|
|
|
|
stats := make(map[string]string)
|
|
|
|
lines := bufio.NewScanner(bytes.NewBuffer(chronycOutput))
|
|
for lines.Scan() {
|
|
value := strings.Split(lines.Text(), ":")
|
|
statName := strings.TrimSpace(value[0])
|
|
statValue := strings.TrimSpace(value[1])
|
|
stats[statName] = statValue
|
|
}
|
|
|
|
return stats, nil
|
|
}
|
|
|
|
// newChronyOffsetSensor creates a new sensor representing the system clock
|
|
// offset from the NTP server time. Attributes contain other stats acquired from
|
|
// chrony.
|
|
func newChronyOffsetSensor(stats map[string]string) sensor.Entity {
|
|
var value any
|
|
|
|
// Try to parse the value into a float. If failed, use the string value.
|
|
valueArr := strings.Split(stats[sensorStat], " ")
|
|
valueParsed, err := strconv.ParseFloat(valueArr[0], 64)
|
|
|
|
if err != nil {
|
|
value = valueArr[0]
|
|
} else {
|
|
value = valueParsed
|
|
}
|
|
|
|
// Base sensor attributes.
|
|
attrs := map[string]any{
|
|
"native_unit_of_measurement": "s",
|
|
"data_source": "chrony",
|
|
}
|
|
// Add other chrony stats as sensor attributes.
|
|
for attr, value := range stats {
|
|
if attr == sensorStat {
|
|
continue
|
|
}
|
|
|
|
attrs[attr] = value
|
|
}
|
|
|
|
return sensor.Entity{
|
|
Name: "Chrony System Time Offset",
|
|
Category: types.CategoryDiagnostic,
|
|
Units: "s",
|
|
StateClass: types.StateClassMeasurement,
|
|
State: &sensor.State{
|
|
ID: "chrony_system_time_offset",
|
|
Value: value,
|
|
Icon: "mdi:clock",
|
|
Attributes: attrs,
|
|
EntityType: types.Sensor,
|
|
},
|
|
}
|
|
}
|
|
|
|
// NewChronyWorker creates a worker to track sensors from chronyd.
|
|
func NewChronyWorker(_ context.Context) (*linux.PollingSensorWorker, error) {
|
|
worker := linux.NewPollingSensorWorker(chronyWorkerID, chronyPollInterval, chronyPollJitter)
|
|
|
|
path, err := exec.LookPath("chronyc")
|
|
if err != nil {
|
|
return worker, fmt.Errorf("chronyc is not available: %w", err)
|
|
}
|
|
|
|
worker.PollingSensorType = &chronyWorker{chronycPath: path}
|
|
|
|
return worker, nil
|
|
}
|