diff --git a/internal/agent/agent.go b/internal/agent/agent.go index b04575477..6434cf64a 100644 --- a/internal/agent/agent.go +++ b/internal/agent/agent.go @@ -38,14 +38,14 @@ type Agent struct { } // newAgent creates the Agent struct. -func newAgent(ctx context.Context) *Agent { +func newAgent(ctx context.Context, tracker fyneui.Tracker) *Agent { agent := &Agent{ logger: logging.FromContext(ctx).WithGroup("agent"), } // If not running headless, set up the UI. if !preferences.Headless() { - agent.ui = fyneui.NewFyneUI(ctx) + agent.ui = fyneui.NewFyneUI(ctx, tracker) } return agent @@ -55,7 +55,7 @@ func newAgent(ctx context.Context) *Agent { // (i.e., `go-hass-agent run`). // //nolint:funlen -func Run(ctx context.Context, dataCh chan any) error { +func Run(ctx context.Context, dataCh chan any, tracker fyneui.Tracker) error { var ( wg sync.WaitGroup regWait sync.WaitGroup @@ -68,7 +68,7 @@ func Run(ctx context.Context, dataCh chan any) error { ctx, cancelFunc := context.WithCancel(ctx) defer cancelFunc() - agent := newAgent(ctx) + agent := newAgent(ctx, tracker) regWait.Add(1) @@ -165,7 +165,7 @@ func Run(ctx context.Context, dataCh chan any) error { func Register(ctx context.Context) error { var wg sync.WaitGroup - agent := newAgent(ctx) + agent := newAgent(ctx, nil) regCtx, cancelReg := signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM) go func() { @@ -197,7 +197,7 @@ func Register(ctx context.Context) error { // Reset is invoked when Go Hass Agent is run with the `reset` command-line // option (i.e., `go-hass-agent reset`). func Reset(ctx context.Context) error { - agent := newAgent(ctx) + agent := newAgent(ctx, nil) // If MQTT is enabled, reset any saved MQTT config. if preferences.MQTTEnabled() { if err := resetMQTTWorkers(ctx); err != nil { diff --git a/internal/agent/ui/fyneUI/fyneUI.go b/internal/agent/ui/fyneUI/fyneUI.go index ff5f3822f..f8fcc71ec 100644 --- a/internal/agent/ui/fyneUI/fyneUI.go +++ b/internal/agent/ui/fyneUI/fyneUI.go @@ -30,6 +30,7 @@ import ( agentvalidator "github.com/joshuar/go-hass-agent/internal/components/validation" "github.com/joshuar/go-hass-agent/internal/hass/discovery" + "github.com/joshuar/go-hass-agent/internal/hass/sensor" "github.com/joshuar/go-hass-agent/internal/agent/ui" "github.com/joshuar/go-hass-agent/internal/components/logging" @@ -49,21 +50,32 @@ var ( ErrInvalidHostPort = errors.New(ui.InvalidHostPortMsgString) ) +// Notification represents the methods for displaying a notification. type Notification interface { GetTitle() string GetMessage() string } +// Tracker represents the methods for the UI to show the current states of +// sensors. +type Tracker interface { + Get(id string) (*sensor.Entity, error) + SensorList() []string +} + +// FyneUI contains the data and methods to manage the UI state. type FyneUI struct { - app fyne.App - logger *slog.Logger + app fyne.App + logger *slog.Logger + tracker Tracker } // New FyneUI sets up the UI for the agent. -func NewFyneUI(ctx context.Context) *FyneUI { +func NewFyneUI(ctx context.Context, tracker Tracker) *FyneUI { appUI := &FyneUI{ - app: app.NewWithID(preferences.AppName), - logger: logging.FromContext(ctx).With(slog.String("subsystem", "fyne")), + app: app.NewWithID(preferences.AppName), + logger: logging.FromContext(ctx).With(slog.String("subsystem", "fyne")), + tracker: tracker, } appUI.app.SetIcon(&ui.TrayIcon{}) @@ -205,11 +217,16 @@ func (i *FyneUI) aboutWindow(ctx context.Context) fyne.Window { widget.NewLabelWithStyle("Home Assistant "+hass.Version(ctx), fyne.TextAlignCenter, fyne.TextStyle{Bold: true}), - widget.NewLabelWithStyle("Tracking "+strconv.Itoa(len(hass.SensorList()))+" Entities", - fyne.TextAlignCenter, - fyne.TextStyle{Italic: true}), ) + if i.tracker == nil { + widgets = append(widgets, + widget.NewLabelWithStyle("Tracking "+strconv.Itoa(len(i.tracker.SensorList()))+" Entities", + fyne.TextAlignCenter, + fyne.TextStyle{Italic: true}), + ) + } + linkWidgets := generateLinks() widgets = append(widgets, widget.NewLabel(""), @@ -284,19 +301,23 @@ func (i *FyneUI) agentSettingsWindow(ctx context.Context) fyne.Window { // //nolint:gocognit func (i *FyneUI) sensorsWindow() fyne.Window { - sensors := hass.SensorList() + if i.tracker == nil { + i.logger.Error("Cannot show sensors, no sensor tracker loaded.") + } + + sensors := i.tracker.SensorList() if sensors == nil { return nil } getValue := func(n string) string { - if sensor, err := hass.GetSensor(n); err == nil { + if details, err := i.tracker.Get(n); err == nil { var valueStr strings.Builder - fmt.Fprintf(&valueStr, "%v", sensor.Value) + fmt.Fprintf(&valueStr, "%v", details.Value) - if sensor.Units != "" { - fmt.Fprintf(&valueStr, " %s", sensor.Units) + if details.Units != "" { + fmt.Fprintf(&valueStr, " %s", details.Units) } return valueStr.String() diff --git a/internal/cli/runCmd.go b/internal/cli/runCmd.go index b560cc725..9f6194c18 100644 --- a/internal/cli/runCmd.go +++ b/internal/cli/runCmd.go @@ -14,6 +14,8 @@ import ( "github.com/joshuar/go-hass-agent/internal/agent" "github.com/joshuar/go-hass-agent/internal/components/logging" "github.com/joshuar/go-hass-agent/internal/components/preferences" + "github.com/joshuar/go-hass-agent/internal/components/registry" + "github.com/joshuar/go-hass-agent/internal/components/tracker" "github.com/joshuar/go-hass-agent/internal/hass" ) @@ -30,7 +32,9 @@ func (r *RunCmd) Run(opts *CmdOpts) error { ctx, cancelFunc := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer cancelFunc() + // Load up the contenxt. ctx = preferences.PathToCtx(ctx, opts.Path) + ctx = logging.ToContext(ctx, opts.Logger) // Load the preferences from file. Ignore the case where there are no // existing preferences. @@ -40,17 +44,23 @@ func (r *RunCmd) Run(opts *CmdOpts) error { return errors.Join(ErrRunCmdFailed, err) } - // Load up the context for the agent. - ctx = logging.ToContext(ctx, opts.Logger) + // Load the registry. + reg, err := registry.Load(ctx) + if err != nil { + return errors.Join(ErrRunCmdFailed, err) + } + + // Load the tracker. + trk := tracker.NewTracker() // Create a new hass data handler. - dataCh, err := hass.NewDataHandler(ctx) + dataCh, err := hass.NewDataHandler(ctx, reg, trk) if err != nil { return errors.Join(ErrRunCmdFailed, err) } // Run the agent. - if err := agent.Run(ctx, dataCh); err != nil { + if err := agent.Run(ctx, dataCh, trk); err != nil { return errors.Join(ErrRunCmdFailed, err) } diff --git a/internal/hass/client.go b/internal/hass/client.go index d129c5469..7969e7695 100644 --- a/internal/hass/client.go +++ b/internal/hass/client.go @@ -10,10 +10,10 @@ import ( "log/slog" "time" + "github.com/davecgh/go-spew/spew" + "github.com/joshuar/go-hass-agent/internal/components/logging" "github.com/joshuar/go-hass-agent/internal/components/preferences" - "github.com/joshuar/go-hass-agent/internal/components/registry" - "github.com/joshuar/go-hass-agent/internal/components/tracker" "github.com/joshuar/go-hass-agent/internal/hass/api" "github.com/joshuar/go-hass-agent/internal/hass/event" "github.com/joshuar/go-hass-agent/internal/hass/sensor" @@ -23,11 +23,6 @@ const ( DefaultTimeout = 30 * time.Second ) -var ( - sensorRegistry Registry - sensorTracker = tracker.NewTracker() -) - var ( ErrGetConfigFailed = errors.New("could not fetch Home Assistant config") ErrGenRequestFailed = errors.New("unable to generate request for sensor") @@ -48,33 +43,43 @@ var ( ErrInvalidSensor = errors.New("invalid sensor") ) -type Registry interface { +// sensorRegistry represents the required methods for hass to manage sensor +// registration state. +type sensorRegistry interface { SetDisabled(id string, state bool) error SetRegistered(id string, state bool) error IsDisabled(id string) bool IsRegistered(id string) bool } -type handler struct { - logger *slog.Logger +// sensorTracker represents the required methods for hass to track sensors and +// their current state. +type sensorTracker interface { + SensorList() []string + Get(id string) (*sensor.Entity, error) + Add(details *sensor.Entity) error } -func NewDataHandler(ctx context.Context) (chan any, error) { - var err error - - sensorRegistry, err = registry.Load(ctx) - if err != nil { - return nil, fmt.Errorf("could not start registry: %w", err) - } +type handler struct { + logger *slog.Logger + registry sensorRegistry + tracker sensorTracker +} +func NewDataHandler(ctx context.Context, reg sensorRegistry, trk sensorTracker) (chan any, error) { dataCh := make(chan any) client := &handler{ - logger: logging.FromContext(ctx).With(slog.String("subsystem", "hass")), + logger: logging.FromContext(ctx).With(slog.String("subsystem", "hass")), + registry: reg, + tracker: trk, } + spew.Dump(ctx) + go func() { for d := range dataCh { + var err error switch data := d.(type) { case sensor.Entity: err = client.processSensor(ctx, data) @@ -126,7 +131,7 @@ func (c *handler) processSensor(ctx context.Context, details sensor.Entity) erro } // Sensor update. - if sensorRegistry.IsRegistered(details.ID) { + if c.registry.IsRegistered(details.ID) { // For sensor updates, if the sensor is disabled, don't continue. if c.isDisabled(ctx, details) { c.logger. @@ -145,7 +150,7 @@ func (c *handler) processSensor(ctx context.Context, details sensor.Entity) erro return fmt.Errorf("failed to send sensor update for %s: %w", details.Name, err) } - go resp.Process(ctx, details) + go resp.Process(ctx, c.registry, c.tracker, details) return nil } @@ -160,7 +165,7 @@ func (c *handler) processSensor(ctx context.Context, details sensor.Entity) erro return fmt.Errorf("failed to send sensor registration: %w", err) } - go resp.Process(ctx, details) + go resp.Process(ctx, c.registry, c.tracker, details) return nil } @@ -183,7 +188,7 @@ func (c *handler) isDisabled(ctx context.Context, details sensor.Entity) bool { c.logger.Info("Sensor re-enabled in Home Assistant, Re-enabling in local registry and sending updates.", sensorLogAttrs(details)) - if err := sensorRegistry.SetDisabled(details.ID, false); err != nil { + if err := c.registry.SetDisabled(details.ID, false); err != nil { c.logger.Error("Could not re-enable sensor.", sensorLogAttrs(details), slog.Any("error", err)) @@ -204,7 +209,7 @@ func (c *handler) isDisabled(ctx context.Context, details sensor.Entity) bool { // //revive:disable:unused-receiver func (c *handler) isDisabledInReg(id string) bool { - return sensorRegistry.IsDisabled(id) + return c.registry.IsDisabled(id) } // isDisabledInHA returns the disabled state of the sensor from Home Assistant. @@ -232,19 +237,6 @@ func (c *handler) isDisabledInHA(ctx context.Context, details sensor.Entity) boo return status } -func GetSensor(id string) (*sensor.Entity, error) { - details, err := sensorTracker.Get(id) - if err != nil { - return nil, fmt.Errorf("could not get sensor details: %w", err) - } - - return details, nil -} - -func SensorList() []string { - return sensorTracker.SensorList() -} - // sensorLogAttrs is a convienience function that returns some slog attributes // for priting sensor details in the log. func sensorLogAttrs(details sensor.Entity) slog.Attr { diff --git a/internal/hass/response.go b/internal/hass/response.go index 9b732c961..12b0e667c 100644 --- a/internal/hass/response.go +++ b/internal/hass/response.go @@ -52,7 +52,7 @@ func (u *sensorUpdateReponse) Status() (responseStatus, error) { type bulkSensorUpdateResponse map[string]sensorUpdateReponse -func (u bulkSensorUpdateResponse) Process(ctx context.Context, details sensor.Entity) { +func (u bulkSensorUpdateResponse) Process(ctx context.Context, registry sensorRegistry, tracker sensorTracker, details sensor.Entity) { for id, sensorReponse := range u { status, err := sensorReponse.Status() @@ -65,7 +65,7 @@ func (u bulkSensorUpdateResponse) Process(ctx context.Context, details sensor.En return case Disabled: // Already disabled in registry, nothing to do. - if sensorRegistry.IsDisabled(id) { + if registry.IsDisabled(id) { return } // Disable in registry. @@ -73,7 +73,7 @@ func (u bulkSensorUpdateResponse) Process(ctx context.Context, details sensor.En Info("Sensor is disabled in Home Assistant. Setting disabled in local registry.", slog.String("id", id)) - if err := sensorRegistry.SetDisabled(id, true); err != nil { + if err := registry.SetDisabled(id, true); err != nil { logging.FromContext(ctx).Warn("Unable to disable sensor in registry.", slog.String("id", id), slog.Any("error", err)) @@ -85,7 +85,7 @@ func (u bulkSensorUpdateResponse) Process(ctx context.Context, details sensor.En } // Add the sensor update to the tracker. - if err := sensorTracker.Add(&details); err != nil { + if err := tracker.Add(&details); err != nil { logging.FromContext(ctx).Warn("Unable to update sensor state in tracker.", slog.String("id", id), slog.Any("error", err)) @@ -103,7 +103,7 @@ func (r *sensorRegistrationResponse) Status() (responseStatus, error) { return Failed, r.ErrorDetails } -func (r *sensorRegistrationResponse) Process(ctx context.Context, details sensor.Entity) { +func (r *sensorRegistrationResponse) Process(ctx context.Context, registry sensorRegistry, tracker sensorTracker, details sensor.Entity) { status, err := r.Status() switch status { @@ -115,14 +115,14 @@ func (r *sensorRegistrationResponse) Process(ctx context.Context, details sensor return case Registered: // Set registration status in registry. - err = sensorRegistry.SetRegistered(details.ID, true) + err = registry.SetRegistered(details.ID, true) if err != nil { logging.FromContext(ctx).Warn("Unable to set sensor registration in registry.", slog.String("id", details.ID), slog.Any("error", err)) } // Add the sensor update to the tracker. - if err := sensorTracker.Add(&details); err != nil { + if err := tracker.Add(&details); err != nil { logging.FromContext(ctx).Warn("Unable to update sensor state in tracker.", slog.String("id", details.ID), slog.Any("error", err))