mirror of
https://github.com/Xevion/HATray.git
synced 2025-12-06 01:15:11 -06:00
feat: tray icon, connect to home assistant, misc functions
This commit is contained in:
@@ -2,8 +2,12 @@ package app
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"ha-tray/internal"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"sync"
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
ga "github.com/Xevion/gome-assistant"
|
||||||
)
|
)
|
||||||
|
|
||||||
// App represents the main application layer that is generic and cross-platform
|
// App represents the main application layer that is generic and cross-platform
|
||||||
@@ -11,7 +15,10 @@ type App struct {
|
|||||||
logger *slog.Logger
|
logger *slog.Logger
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
state AppState
|
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
|
// AppState represents the current state of the application
|
||||||
@@ -37,9 +44,12 @@ func (s AppState) String() string {
|
|||||||
// NewApp creates a new application instance
|
// NewApp creates a new application instance
|
||||||
func NewApp(logger *slog.Logger) *App {
|
func NewApp(logger *slog.Logger) *App {
|
||||||
return &App{
|
return &App{
|
||||||
logger: logger,
|
logger: logger.With("type", "app"),
|
||||||
state: StatePaused,
|
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
|
// - Disconnect from Home Assistant WebSocket
|
||||||
// - Stop background tasks
|
// - Stop background tasks
|
||||||
// - Pause sensor monitoring
|
// - 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
|
app.state = StatePaused
|
||||||
|
|
||||||
@@ -94,16 +116,37 @@ func (app *App) Resume() error {
|
|||||||
"action", "resume",
|
"action", "resume",
|
||||||
"previous_state", app.state,
|
"previous_state", app.state,
|
||||||
"new_state", StateRunning,
|
"new_state", StateRunning,
|
||||||
"has_started", app.hasStarted,
|
"has_started", app.lastStarted,
|
||||||
)
|
)
|
||||||
|
|
||||||
// TODO: Implement actual resume logic
|
// TODO: Implement actual resume logic
|
||||||
// - Connect to Home Assistant WebSocket
|
// - Connect to Home Assistant WebSocket
|
||||||
// - Start background tasks
|
// - Start background tasks
|
||||||
// - Resume sensor monitoring
|
// - 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.state = StateRunning
|
||||||
app.hasStarted = true
|
app.lastStarted = internal.Ptr(time.Now())
|
||||||
|
|
||||||
app.logger.Info("resumed successfully",
|
app.logger.Info("resumed successfully",
|
||||||
"action", "resume",
|
"action", "resume",
|
||||||
@@ -134,6 +177,7 @@ func (a *App) Reload() error {
|
|||||||
switch a.state {
|
switch a.state {
|
||||||
case StatePaused:
|
case StatePaused:
|
||||||
// already paused, do nothing
|
// already paused, do nothing
|
||||||
|
a.logger.Info("application is already paused during reload")
|
||||||
case StateRunning:
|
case StateRunning:
|
||||||
if err := a.Pause(); err != nil {
|
if err := a.Pause(); err != nil {
|
||||||
a.logger.Error("failed to pause during reload",
|
a.logger.Error("failed to pause during reload",
|
||||||
|
|||||||
87
internal/app/tray.go
Normal file
87
internal/app/tray.go
Normal 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
5
internal/misc.go
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
package internal
|
||||||
|
|
||||||
|
func Ptr[T any](value T) *T {
|
||||||
|
return &value
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user