Skip to content

Commit

Permalink
refactor: ♻️ additional code changes and adjust prefs tests for 7591c7a
Browse files Browse the repository at this point in the history
… changes

- remove fetching preferences from context, use package methods instead
- rework cli option parsing
- adjust preferences tests and fix some bugs
  • Loading branch information
joshuar committed Dec 26, 2024
1 parent db57b24 commit 768cc40
Show file tree
Hide file tree
Showing 44 changed files with 514 additions and 320 deletions.
94 changes: 53 additions & 41 deletions internal/agent/agent.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
// Copyright (c) 2024 Joshua Rich <[email protected]>
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
// Copyright 2024 Joshua Rich <[email protected]>.
// SPDX-License-Identifier: MIT

// revive:disable:unused-receiver
//
//go:generate go run github.com/matryer/moq -out agent_mocks_test.go . ui
package agent

Expand Down Expand Up @@ -42,18 +38,28 @@ type Agent struct {
ui ui
}

// CtxOption is a functional parameter that will add a value to the agent
// CtxOption is a functional parameter that will add a value to the agent's
// context.
type CtxOption func(context.Context) context.Context

// LoadCtx will "load" a context.Context with the given options (i.e. add values
// to it to be used by the agent).
func LoadCtx(ctx context.Context, options ...CtxOption) context.Context {
func newAgentCtx(options ...CtxOption) (context.Context, context.CancelFunc) {
ctx, cancelFunc := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)

for _, option := range options {
ctx = option(ctx) //nolint:fatcontext
}

return ctx
return ctx, cancelFunc
}

// SetLogger sets the given logger in the context.
func SetLogger(logger *slog.Logger) CtxOption {
return func(ctx context.Context) context.Context {
ctx = logging.ToContext(ctx, logger)
return ctx
}
}

// SetHeadless sets the headless flag in the context.
Expand Down Expand Up @@ -84,19 +90,21 @@ func SetForceRegister(value bool) CtxOption {
}
}

// Run is the "main loop" of the agent. It sets up the agent, loads the config
// then spawns a sensor tracker and the workers to gather sensor data and
// publish it to Home Assistant.
// Run is invoked when Go Hass Agent is run with the `run` command-line option
// (i.e., `go-hass-agent run`).
//
//nolint:funlen
//revive:disable:function-length
func Run(ctx context.Context) error {
func Run(options ...CtxOption) error {
var (
wg sync.WaitGroup
regWait sync.WaitGroup
err error
)

ctx, cancelFunc := newAgentCtx(options...)
defer cancelFunc()

agent := &Agent{}

// If running headless, do not set up the UI.
Expand All @@ -106,26 +114,18 @@ func Run(ctx context.Context) error {

// Load the preferences from file. Ignore the case where there are no
// existing preferences.
if err = preferences.Load(ctx); err != nil && !errors.Is(err, preferences.ErrLoadPreferences) {
if err = preferences.Load(); err != nil && !errors.Is(err, preferences.ErrLoadPreferences) {
return fmt.Errorf("%w: %w", ErrAgentStart, err)
}

// Set up a context for running the agent and tie its lifetime to the
// typical process termination signals.
runCtx, cancelRun := signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM)
go func() {
<-ctx.Done()
cancelRun()
}()

regWait.Add(1)

go func() {
defer regWait.Done()
// Check if the agent is registered. If not, start a registration flow.
if err = checkRegistration(runCtx, agent.ui); err != nil {
if err = checkRegistration(ctx, agent.ui); err != nil {
logging.FromContext(ctx).Error("Error checking registration status.", slog.Any("error", err))
cancelRun()
cancelFunc()
}
}()

Expand All @@ -148,18 +148,18 @@ func Run(ctx context.Context) error {
}

// Initialize and gather OS sensor and event workers.
sensorWorkers, eventWorkers := setupOSWorkers(runCtx)
sensorWorkers, eventWorkers := setupOSWorkers(ctx)
// Initialize and add connection latency sensor worker.
sensorWorkers = append(sensorWorkers, agentsensor.NewConnectionLatencySensorWorker())
// Initialize and add external IP address sensor worker.
sensorWorkers = append(sensorWorkers, agentsensor.NewExternalIPUpdaterWorker(runCtx))
sensorWorkers = append(sensorWorkers, agentsensor.NewExternalIPUpdaterWorker(ctx))
// Initialize and add external version sensor worker.
sensorWorkers = append(sensorWorkers, agentsensor.NewVersionWorker())

// Initialize and add the script worker.
scriptsWorkers, err := scripts.NewScriptsWorker(runCtx)
scriptsWorkers, err := scripts.NewScriptsWorker(ctx)
if err != nil {
logging.FromContext(runCtx).Warn("Could not init scripts workers.", slog.Any("error", err))
logging.FromContext(ctx).Warn("Could not init scripts workers.", slog.Any("error", err))
} else {
sensorWorkers = append(sensorWorkers, scriptsWorkers)
}
Expand All @@ -168,29 +168,29 @@ func Run(ctx context.Context) error {
// Process sensor workers.
go func() {
defer wg.Done()
processWorkers(runCtx, client, sensorWorkers...)
processWorkers(ctx, client, sensorWorkers...)
}()

wg.Add(1)
// Process event workers.
go func() {
defer wg.Done()
processWorkers(runCtx, client, eventWorkers...)
processWorkers(ctx, client, eventWorkers...)
}()

// If MQTT is enabled, init MQTT workers and process them.
if preferences.MQTTEnabled() {
if mqttPrefs, err := preferences.GetMQTTPreferences(); err != nil {
logging.FromContext(runCtx).Warn("Could not init mqtt workers.",
logging.FromContext(ctx).Warn("Could not init mqtt workers.",
slog.Any("error", err))
} else {
mqttWorkers := setupMQTT(runCtx)
mqttWorkers := setupMQTT(ctx)

wg.Add(1)

go func() {
defer wg.Done()
processMQTTWorkers(runCtx, mqttPrefs, mqttWorkers...)
processMQTTWorkers(ctx, mqttPrefs, mqttWorkers...)
}()
}
}
Expand All @@ -199,34 +199,40 @@ func Run(ctx context.Context) error {
// Listen for notifications from Home Assistant.
go func() {
defer wg.Done()
runNotificationsWorker(runCtx, agent.ui)
runNotificationsWorker(ctx, agent.ui)
}()
}()

// Do not run the UI loop if the agent is running in headless mode.
if !Headless(ctx) {
agent.ui.DisplayTrayIcon(runCtx, cancelRun)
agent.ui.Run(runCtx)
agent.ui.DisplayTrayIcon(ctx, cancelFunc)
agent.ui.Run(ctx)
}

wg.Wait()

return nil
}

func Register(ctx context.Context) error {
// Register is run when Go Hass Agent is invoked with the `register`
// command-line option (i.e., `go-hass-agent register`). It will attempt to
// register Go Hass Agent with Home Assistant.
func Register(options ...CtxOption) error {
var (
wg sync.WaitGroup
err error
)

ctx, cancelFunc := newAgentCtx(options...)
defer cancelFunc()

agent := &Agent{}
// If running headless, do not set up the UI.
if !Headless(ctx) {
agent.ui = fyneui.NewFyneUI(ctx)
}

if err = preferences.Load(ctx); err != nil && !errors.Is(err, preferences.ErrLoadPreferences) {
if err = preferences.Load(); err != nil && !errors.Is(err, preferences.ErrLoadPreferences) {
return fmt.Errorf("%w: %w", ErrAgentStart, err)
}

Expand Down Expand Up @@ -257,12 +263,18 @@ func Register(ctx context.Context) error {
return nil
}

// Reset will remove any agent related files and configuration.
func Reset(ctx context.Context) error {
if err := preferences.Load(ctx); err != nil && !errors.Is(err, preferences.ErrLoadPreferences) {
// Reset is invoked when Go Hass Agent is run with the `reset` command-line
// option (i.e., `go-hass-agent reset`).
func Reset(options ...CtxOption) error {
ctx, cancelFunc := newAgentCtx(options...)
defer cancelFunc()

// Load the preferences so we know what we need to reset.
if err := preferences.Load(); err != nil && !errors.Is(err, preferences.ErrLoadPreferences) {
return fmt.Errorf("%w: %w", ErrAgentStart, err)
}

// If MQTT is enabled, reset any saved MQTT config.
if preferences.MQTTEnabled() {
if err := resetMQTTWorkers(ctx); err != nil {
logging.FromContext(ctx).Error("Problems occurred resetting MQTT configuration.", slog.Any("error", err))
Expand Down
2 changes: 1 addition & 1 deletion internal/agent/agentsensor/external_ip.go
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ func NewExternalIPUpdaterWorker(ctx context.Context) *ExternalIPWorker {
With(slog.String("worker", externalIPWorkerID)),
}

prefs, err := preferences.LoadWorker(ctx, worker)
prefs, err := preferences.LoadWorker(worker)
if err != nil {
return nil
}
Expand Down
2 changes: 1 addition & 1 deletion internal/agent/agentsensor/version.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ func newVersionSensor() sensor.Entity {
sensor.AsDiagnostic(),
sensor.WithState(
sensor.WithIcon("mdi:face-agent"),
sensor.WithValue(preferences.AppVersion),
sensor.WithValue(preferences.AppVersion()),
),
)
}
Expand Down
10 changes: 4 additions & 6 deletions internal/agent/register.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
// Copyright (c) 2024 Joshua Rich <[email protected]>
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
// Copyright 2024 Joshua Rich <[email protected]>.
// SPDX-License-Identifier: MIT

//go:generate go run github.com/matryer/moq -out register_mocks_test.go . registrationPrefs
package agent
Expand Down Expand Up @@ -51,13 +49,13 @@ func checkRegistration(ctx context.Context, agentUI ui) error {
return fmt.Errorf("saving registration failed: %w", err)
}

if err := preferences.Save(ctx); err != nil {
if err := preferences.Save(); err != nil {
return fmt.Errorf("saving registration failed: %w", err)
}

// If the registration was forced, reset the sensor registry.
if ForceRegister(ctx) {
if err := registry.Reset(ctx); err != nil {
if err := registry.Reset(); err != nil {
logging.FromContext(ctx).Warn("Problem resetting registry.", slog.Any("error", err))
}
}
Expand Down
12 changes: 6 additions & 6 deletions internal/agent/ui/fyneUI/fyneUI.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ func (i *FyneUI) DisplayTrayIcon(ctx context.Context, cancelFunc context.CancelF
// Preferences/Settings items.
menuItemAppPrefs := fyne.NewMenuItem("App Settings",
func() {
i.agentSettingsWindow(ctx).Show()
i.agentSettingsWindow().Show()
})
menuItemFynePrefs := fyne.NewMenuItem("Fyne Settings",
func() {
Expand Down Expand Up @@ -187,7 +187,7 @@ func (i *FyneUI) DisplayRegistrationWindow(ctx context.Context, prefs *preferenc
func (i *FyneUI) aboutWindow(ctx context.Context) fyne.Window {
var widgets []fyne.CanvasObject

if err := preferences.Load(ctx); err != nil {
if err := preferences.Load(); err != nil {
logging.FromContext(ctx).Error("Could not start sensor controller.", slog.Any("error", err))
return nil
}
Expand All @@ -202,7 +202,7 @@ func (i *FyneUI) aboutWindow(ctx context.Context) fyne.Window {
icon.FillMode = canvas.ImageFillOriginal

widgets = append(widgets, icon,
widget.NewLabelWithStyle("Go Hass Agent "+preferences.AppVersion,
widget.NewLabelWithStyle("Go Hass Agent "+preferences.AppVersion(),
fyne.TextAlignCenter,
fyne.TextStyle{Bold: true}))

Expand Down Expand Up @@ -238,10 +238,10 @@ func (i *FyneUI) fyneSettingsWindow() fyne.Window {

// agentSettingsWindow creates a window for changing settings related to the
// agent functionality. Most of these settings will be optional.
func (i *FyneUI) agentSettingsWindow(ctx context.Context) fyne.Window {
func (i *FyneUI) agentSettingsWindow() fyne.Window {
var allFormItems []*widget.FormItem

if err := preferences.Load(ctx); err != nil {
if err := preferences.Load(); err != nil {
i.logger.Error("Could not start sensor controller.",
slog.Any("error", err))
return nil
Expand All @@ -265,7 +265,7 @@ func (i *FyneUI) agentSettingsWindow(ctx context.Context) fyne.Window {
i.logger.Error("Could note save preferences.", slog.Any("error", err))
}
// Save the new MQTT preferences to file.
if err := preferences.Save(ctx); err != nil {
if err := preferences.Save(); err != nil {
dialog.ShowError(err, window)
i.logger.Error("Could note save preferences.", slog.Any("error", err))
} else {
Expand Down
30 changes: 8 additions & 22 deletions internal/cli/cli.go
Original file line number Diff line number Diff line change
@@ -1,18 +1,14 @@
// Copyright (c) 2024 Joshua Rich <[email protected]>
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
// Copyright 2024 Joshua Rich <[email protected]>.
// SPDX-License-Identifier: MIT

package cli

import (
"context"
"embed"
"log/slog"
"os"
"path/filepath"

"github.com/joshuar/go-hass-agent/internal/logging"
"github.com/joshuar/go-hass-agent/internal/preferences"
)

Expand All @@ -26,19 +22,18 @@ var content embed.FS

type CmdOpts struct {
Logger *slog.Logger
AppID string
Headless bool
}

type Option func(*CmdOpts)

func CreateCtx(options ...Option) *CmdOpts {
ctx := &CmdOpts{}
func AddOptions(options ...Option) *CmdOpts {
commandOptions := &CmdOpts{}
for _, option := range options {
option(ctx)
option(commandOptions)
}

return ctx
return commandOptions
}

func RunHeadless(opt bool) Option {
Expand All @@ -48,8 +43,8 @@ func RunHeadless(opt bool) Option {
}

func WithAppID(id string) Option {
return func(ctx *CmdOpts) {
ctx.AppID = id
return func(_ *CmdOpts) {
preferences.SetAppID(id)
}
}

Expand All @@ -71,15 +66,6 @@ func (f *HeadlessFlag) AfterApply() error {
return nil
}

func newContext(opts *CmdOpts) (context.Context, context.CancelFunc) {
ctx, cancelFunc := context.WithCancel(context.Background())

ctx = logging.ToContext(ctx, opts.Logger)
ctx = preferences.AppIDToContext(ctx, opts.AppID)

return ctx, cancelFunc
}

func showHelpTxt(file string) string {
assetFile := filepath.Join(assetsPath, file+assetsExt)

Expand Down
Loading

0 comments on commit 768cc40

Please sign in to comment.