joshuar-go-hass-agent/internal/agent/agent.go

151 lines
3.5 KiB
Go

// Copyright (c) 2023 Joshua Rich <joshua.rich@gmail.com>
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
package agent
import (
"context"
_ "embed"
"os"
"os/signal"
"sync"
"syscall"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/storage"
"github.com/joshuar/go-hass-agent/internal/config"
"github.com/joshuar/go-hass-agent/internal/device"
"github.com/joshuar/go-hass-agent/internal/sensors"
"github.com/joshuar/go-hass-agent/internal/translations"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
)
//go:generate sh -c "printf %s $(git tag | tail -1) > VERSION"
//go:embed VERSION
var Version string
var translator *translations.Translator
var debugAppID = ""
const (
Name = "go-hass-agent"
fyneAppID = "com.github.joshuar.go-hass-agent"
)
type Agent struct {
app fyne.App
tray fyne.Window
Name, Version string
}
func NewAgent() *Agent {
return &Agent{
app: newUI(),
Name: Name,
Version: Version,
}
}
func Run(id string) {
if id != "" {
debugAppID = id
}
agentCtx, cancelfunc := context.WithCancel(context.Background())
agentCtx = device.SetupContext(agentCtx)
log.Info().Msg("Starting agent.")
agent := NewAgent()
translator = translations.NewTranslator()
// If possible, create and log to a file as well as the console.
logFile, err := agent.extraStoragePath("go-hass-app.log")
if err != nil {
log.Error().Err(err).
Msg("Unable to create a log file. Will only write logs to stdout.")
} else {
logWriter, err := storage.Writer(logFile)
if err != nil {
log.Error().Err(err).
Msg("Unable to open log file for writing.")
} else {
consoleWriter := zerolog.ConsoleWriter{Out: os.Stdout}
multiWriter := zerolog.MultiLevelWriter(consoleWriter, logWriter)
log.Logger = log.Output(multiWriter)
}
}
var wg sync.WaitGroup
// Try to load the app config. If it is not valid, start a new registration
// process. Keep trying until we successfully register with HA or the user
// quits.
wg.Add(1)
go func() {
defer wg.Done()
appConfig := agent.loadAppConfig()
for appConfig.Validate() != nil {
log.Warn().Msg("No suitable existing config found! Starting new registration process")
err := agent.runRegistrationWorker(agentCtx)
if err != nil {
log.Error().Err(err).
Msgf("Error trying to register: %v. Exiting.")
agent.stop()
}
appConfig = agent.loadAppConfig()
}
}()
// Wait for the config to load, then start the sensor tracker and
// notifications worker
trackerWg := &sync.WaitGroup{}
go func() {
wg.Wait()
appConfig := agent.loadAppConfig()
ctx := config.NewContext(agentCtx, appConfig)
registryPath, err := agent.extraStoragePath("sensorRegistry")
if err != nil {
log.Debug().Err(err).
Msg("Unable to store registry on disk, trying in-memory store.")
}
updateCh := make(chan interface{})
sensors.RunSensorTracker(ctx, registryPath, updateCh, trackerWg)
go agent.runNotificationsWorker(ctx)
}()
// Handle interrupt/termination signals
c := make(chan os.Signal)
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
go func() {
<-c
cancelfunc()
trackerWg.Wait()
agent.stop()
os.Exit(1)
}()
agent.setupSystemTray()
agent.app.Run()
cancelfunc()
trackerWg.Wait()
agent.stop()
}
func (agent *Agent) stop() {
log.Info().Msg("Shutting down agent.")
agent.tray.Close()
}
func (agent *Agent) extraStoragePath(id string) (fyne.URI, error) {
rootPath := agent.app.Storage().RootURI()
extraPath, err := storage.Child(rootPath, id)
if err != nil {
return nil, err
} else {
return extraPath, nil
}
}