feat: tray icon, connect to home assistant, misc functions

This commit is contained in:
2025-07-31 16:24:25 -05:00
parent 74abe9d2e4
commit 7a2b778803
3 changed files with 145 additions and 9 deletions

View File

@@ -2,8 +2,12 @@ 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
@@ -11,7 +15,10 @@ type App struct {
logger *slog.Logger
mu sync.RWMutex
state AppState
hasStarted bool // true only if the application has ever been started (i.e. has been resumed from initial paused state)
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
@@ -37,9 +44,12 @@ func (s AppState) String() string {
// NewApp creates a new application instance
func NewApp(logger *slog.Logger) *App {
return &App{
logger: logger,
logger: logger.With("type", "app"),
state: StatePaused,
hasStarted: false,
config: nil,
lastStarted: nil,
tray: &Tray{},
ha: nil,
}
}
@@ -66,6 +76,18 @@ func (app *App) Pause() error {
// - Disconnect from Home Assistant WebSocket
// - Stop background tasks
// - Pause sensor monitoring
// - Stop tray icon event loop
err := app.ha.Close()
if err != nil {
app.logger.Error("failed to close home assistant connection", "error", err)
return err
}
err = app.tray.Stop()
if err != nil {
app.logger.Error("failed to stop tray", "error", err)
return err
}
app.state = StatePaused
@@ -94,16 +116,37 @@ func (app *App) Resume() error {
"action", "resume",
"previous_state", app.state,
"new_state", StateRunning,
"has_started", app.hasStarted,
"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.hasStarted = true
app.lastStarted = internal.Ptr(time.Now())
app.logger.Info("resumed successfully",
"action", "resume",
@@ -134,6 +177,7 @@ func (a *App) Reload() error {
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",

87
internal/app/tray.go Normal file
View File

@@ -0,0 +1,87 @@
package app
import (
"fmt"
"ha-tray/internal"
"time"
"github.com/getlantern/systray"
)
type IconReference string
const (
IconOpen IconReference = "open"
IconClosed IconReference = "closed"
IconUnknown IconReference = "unknown"
)
// Path returns the path to the icon file
func (i IconReference) Path() string {
switch i {
case IconOpen:
return "resources/open.ico"
case IconClosed:
return "resources/closed.ico"
default:
return "resources/unknown.ico"
}
}
type Tray struct {
active bool
currentIcon IconReference
}
func (t *Tray) SetIcon(icon IconReference) error {
if !t.active {
return fmt.Errorf("tray is not active")
}
iconBytes, err := internal.Icons.ReadFile(icon.Path())
if err != nil {
return fmt.Errorf("failed to read icon: %w", err)
}
systray.SetIcon(iconBytes)
t.currentIcon = icon
return nil
}
func (t *Tray) Start(title string) error {
if t.active {
return fmt.Errorf("tray is already active")
}
readyTimeout := make(chan struct{}, 1)
go systray.Run(func() {
systray.SetTitle(title)
systray.SetTooltip(title)
readyTimeout <- struct{}{}
close(readyTimeout)
}, func() {
t.active = false
})
select {
case <-readyTimeout:
fmt.Println("tray started")
t.active = true
return nil
case <-time.After(5 * time.Second):
close(readyTimeout)
return fmt.Errorf("tray did not start in time")
}
}
func (t *Tray) Stop() error {
if !t.active {
return fmt.Errorf("tray is not active")
}
systray.Quit()
t.active = false
return nil
}

5
internal/misc.go Normal file
View File

@@ -0,0 +1,5 @@
package internal
func Ptr[T any](value T) *T {
return &value
}