Files
HATray/internal/app/app.go
2025-08-01 12:16:59 -05:00

218 lines
4.9 KiB
Go

package app
import (
"fmt"
"ha-tray/internal"
"log/slog"
"sync"
"time"
ga "github.com/Xevion/gome-assistant"
)
// App represents the main application layer that is generic and cross-platform
type App struct {
logger *slog.Logger
mu sync.RWMutex
state AppState
config *Config
lastStarted *time.Time // time of last start, nil if never started
tray *Tray // simple interface to systray
ha *ga.App
}
// AppState represents the current state of the application
type AppState int
const (
StatePaused AppState = iota
StateRunning
)
// String returns the string representation of the AppState
func (s AppState) String() string {
switch s {
case StatePaused:
return "paused"
case StateRunning:
return "running"
default:
return "unknown"
}
}
// NewApp creates a new application instance
func NewApp(logger *slog.Logger) *App {
return &App{
logger: logger.With("type", "app"),
state: StatePaused,
config: nil,
lastStarted: nil,
tray: NewTray(logger.With("type", "tray")),
ha: nil,
}
}
// Pause disconnects from the server and ceases any background tasks
func (app *App) Pause() error {
app.mu.Lock()
defer app.mu.Unlock()
switch app.state {
case StatePaused:
return fmt.Errorf("application is already paused")
case StateRunning:
// valid state to pause from, do nothing
default:
return fmt.Errorf("unexpected state encountered while pausing application: %s", app.state)
}
app.logger.Info("pausing application",
"action", "pause",
"previous_state", app.state,
"new_state", StatePaused)
// - Disconnect from Home Assistant WebSocket
err := app.ha.Close()
if err != nil {
app.logger.Error("failed to close home assistant connection", "error", err)
return err
}
// - Stop tray icon event loop
err = app.tray.Stop()
if err != nil {
app.logger.Error("failed to stop tray", "error", err)
return err
}
app.state = StatePaused
app.logger.Info("paused successfully",
"action", "pause",
"state", app.state)
return nil
}
// Resume connects to the server and initiates background tasks
func (app *App) Resume() error {
app.mu.Lock()
defer app.mu.Unlock()
switch app.state {
case StateRunning:
return fmt.Errorf("application is already running")
case StatePaused:
// valid state to resume from, do nothing
default:
return fmt.Errorf("unexpected state encountered while resuming application: %s", app.state)
}
app.logger.Info("resuming application",
"action", "resume",
"previous_state", app.state,
"new_state", StateRunning,
"has_started", app.lastStarted,
)
// TODO: Implement actual resume logic
// - Connect to Home Assistant WebSocket
// - Start background tasks
// - Resume sensor monitoring
err := app.tray.Start(fmt.Sprintf("HATray v%s", "0.0.1"))
if err != nil {
app.logger.Error("failed to start tray", "error", err)
return err
}
app.config = DefaultConfig()
app.ha, err = ga.NewApp(ga.NewAppRequest{
URL: app.config.Server,
HAAuthToken: app.config.APIKey,
})
if err != nil {
app.logger.Error("failed to create Home Assistant app", "error", err)
return err
}
app.ha.Cleanup()
app.tray.SetIcon(IconUnknown)
app.state = StateRunning
app.lastStarted = internal.Ptr(time.Now())
app.logger.Info("resumed successfully",
"action", "resume",
"state", app.state)
return nil
}
// Reload pauses the application, re-reads configuration files, then resumes
func (a *App) Reload() error {
a.mu.Lock()
defer a.mu.Unlock()
switch a.state {
case StatePaused:
return fmt.Errorf("cannot reload application when paused")
case StateRunning:
// valid state to reload from, do nothing
default:
return fmt.Errorf("unexpected state encountered while reloading application: %s", a.state)
}
a.logger.Info("starting application reload",
"action", "reload",
"current_state", a.state)
// Pause if not already paused
switch a.state {
case StatePaused:
// already paused, do nothing
a.logger.Info("application is already paused during reload")
case StateRunning:
if err := a.Pause(); err != nil {
a.logger.Error("failed to pause during reload",
"action", "reload",
"error", err)
return err
}
default:
return fmt.Errorf("unexpected state encountered while pausing for reload: %s", a.state)
}
// TODO: Implement configuration reload logic
// - Re-read TOML configuration files
// - Validate configuration
// - Update internal state with new configuration
a.logger.Info("configuration reloaded successfully",
"action", "reload")
// Resume the application
if err := a.Resume(); err != nil {
a.logger.Error("failed to resume during reload",
"action", "reload",
"error", err)
return err
}
a.logger.Info("application reload completed successfully",
"action", "reload",
"final_state", a.state)
return nil
}
// GetState returns the current state of the application
func (a *App) GetState() AppState {
a.mu.RLock()
defer a.mu.RUnlock()
return a.state
}