Files
home-sensors/internal/sensors/sensors.go
T
Charles Hathaway ac4e32697c add: sensors
2023-10-01 22:02:30 -07:00

145 lines
3.1 KiB
Go

package sensors
import (
"bufio"
"context"
"os/exec"
"strconv"
"strings"
"sync"
"time"
pb "github.com/chathaway-codes/home-sensors/v2/gen"
"github.com/chathaway-codes/home-sensors/v2/internal/pipespy"
"github.com/chathaway-codes/home-sensors/v2/internal/watcher/config"
"github.com/google/uuid"
"github.com/rs/zerolog/log"
)
var Default = &Mod{}
type Sensors struct {
mu sync.Mutex
ctx context.Context
readCmd *exec.Cmd
cancelFunc func()
ticker time.Ticker
listeners map[string]chan<- *pb.Sample
}
func New(cfg *config.Config) (*Sensors, error) {
ctx, cancelFunc := context.WithCancel(context.Background())
readCmd := exec.CommandContext(ctx, cfg.SensorCmd.Binary, cfg.SensorCmd.Arguments...)
ticker := time.NewTicker(time.Duration(cfg.SensorRateMS) * time.Millisecond)
return &Sensors{
ctx: ctx,
cancelFunc: cancelFunc,
readCmd: readCmd,
ticker: *ticker,
// it would be better if sensors.Run handled sending to the server; to do that,
// it needs a connection and the auth token. I don't have a clean way of getting that
// right now.
listeners: make(map[string]chan<- *pb.Sample),
}, nil
}
func (v *Sensors) Run() {
pipe := pipespy.New()
snoop := pipe.Add(pipespy.NewCmd(v.readCmd)).Snoop()
defer snoop.Close()
cleanUp := pipe.Start()
defer func() {
errs := cleanUp()
for _, err := range errs {
log.Err(err).Send()
}
}()
scanner := bufio.NewScanner(snoop)
for scanner.Scan() {
line := scanner.Text()
parts := strings.Split(line, " ")
if len(parts) != 3 {
log.Error().Str("line", line).Msg("malformed line; expected 3 floats: humidity pressure temperature_c")
}
humidity, err := strconv.ParseFloat(parts[0], 64)
if err != nil {
log.Error().Err(err).Str("val", parts[0]).Msg("failed to parse humidity")
}
pressure, err := strconv.ParseFloat(parts[1], 64)
if err != nil {
log.Error().Err(err).Str("val", parts[1]).Msg("failed to parse pressure")
}
temperatureC, err := strconv.ParseFloat(parts[2], 64)
if err != nil {
log.Error().Err(err).Str("val", parts[2]).Msg("failed to parse temperature_c")
}
select {
case <-v.ticker.C:
func() {
v.mu.Lock()
defer v.mu.Unlock()
samples := []*pb.Sample{
{
Type: pb.Sample_HUMIDITY,
Reading: humidity,
},
{
Type: pb.Sample_PRESSURE,
Reading: pressure,
},
{
Type: pb.Sample_TEMPERATURE_C,
Reading: temperatureC,
},
}
for _, listener := range v.listeners {
for _, sample := range samples {
listener <- sample
}
}
}()
default:
// do nothing
}
}
}
// Join will connect to a running stream.
func (v *Sensors) Join() (<-chan *pb.Sample, func()) {
v.mu.Lock()
defer v.mu.Unlock()
myID := uuid.New().String()
ch := make(chan *pb.Sample)
v.listeners[myID] = ch
return ch, func() {
v.mu.Lock()
defer v.mu.Unlock()
delete(v.listeners, myID)
}
}
// Done stops the processing.
func (v *Sensors) Done() {
v.cancelFunc()
}
type Mod struct{}
func (m *Mod) Get() (*Sensors, error) {
cfg, err := config.Default.Get()
if err != nil {
return nil, err
}
return New(cfg)
}