246 lines
6.7 KiB
Go
246 lines
6.7 KiB
Go
// Copyright 2024 Joshua Rich <joshua.rich@gmail.com>.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package preferences
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"log/slog"
|
|
"os"
|
|
"path/filepath"
|
|
"sync"
|
|
|
|
"github.com/adrg/xdg"
|
|
"github.com/knadh/koanf/parsers/toml"
|
|
"github.com/knadh/koanf/providers/file"
|
|
"github.com/knadh/koanf/providers/structs"
|
|
"github.com/knadh/koanf/v2"
|
|
|
|
"github.com/joshuar/go-hass-agent/internal/validation"
|
|
)
|
|
|
|
const (
|
|
prefsEnvPrefix = "GOHASSAGENT_"
|
|
AppName = "Go Hass Agent"
|
|
AppURL = "https://github.com/joshuar/go-hass-agent"
|
|
FeatureRequestURL = AppURL + "/issues/new?assignees=joshuar&labels=&template=feature_request.md&title="
|
|
IssueURL = AppURL + "/issues/new?assignees=joshuar&labels=&template=bug_report.md&title=%5BBUG%5D"
|
|
AppDescription = "A Home Assistant, native app for desktop/laptop devices."
|
|
MQTTTopicPrefix = "homeassistant"
|
|
LogFile = "go-hass-agent.log"
|
|
defaultFilePerms = 0o600
|
|
preferencesFilename = "preferences.toml"
|
|
|
|
DefaultServer = "http://localhost:8123"
|
|
DefaultSecret = "ALongSecretString"
|
|
|
|
prefRegistered = "registered"
|
|
)
|
|
|
|
var (
|
|
//lint:ignore U1000 some of these will be used in the future
|
|
gitVersion, gitCommit, gitTreeState, buildDate string
|
|
appVersion = gitVersion
|
|
appID = "go-hass-agent"
|
|
)
|
|
|
|
// Consistent error messages.
|
|
var (
|
|
ErrLoadPreferences = errors.New("error loading preferences")
|
|
ErrSavePreferences = errors.New("error saving preferences")
|
|
ErrValidatePreferences = errors.New("error validating preferences")
|
|
ErrSetPreference = errors.New("error setting preference")
|
|
)
|
|
|
|
// Default agent preferences.
|
|
var defaultAgentPreferences = &preferences{
|
|
Version: AppVersion(),
|
|
Registered: false,
|
|
MQTT: &MQTT{
|
|
MQTTEnabled: false,
|
|
MQTTTopicPrefix: MQTTTopicPrefix,
|
|
},
|
|
Registration: &Registration{
|
|
Server: DefaultServer,
|
|
Token: DefaultSecret,
|
|
},
|
|
Hass: &Hass{
|
|
RestAPIURL: DefaultServer,
|
|
WebsocketURL: DefaultServer,
|
|
WebhookID: DefaultSecret,
|
|
},
|
|
}
|
|
|
|
var (
|
|
prefsSrc = koanf.New(".")
|
|
preferencesFile = filepath.Join(xdg.ConfigHome, appID, preferencesFilename)
|
|
mu = sync.Mutex{}
|
|
)
|
|
|
|
// preferences defines all preferences for Go Hass Agent.
|
|
type preferences struct {
|
|
MQTT *MQTT `toml:"mqtt,omitempty"`
|
|
Registration *Registration `toml:"registration"`
|
|
Hass *Hass `toml:"hass"`
|
|
Device *Device `toml:"device"`
|
|
Version string `toml:"version"`
|
|
Registered bool `toml:"registered" validate:"boolean"`
|
|
}
|
|
|
|
// Load will retrieve the current preferences from the preference file on disk.
|
|
// If there is a problem during retrieval, an error will be returned.
|
|
var Load = func() error {
|
|
return sync.OnceValue(func() error {
|
|
preferencesFile = filepath.Join(Path(), preferencesFilename)
|
|
|
|
slog.Debug("Loading preferences.", slog.String("file", preferencesFile))
|
|
|
|
// Load config file
|
|
if err := prefsSrc.Load(file.Provider(preferencesFile), toml.Parser()); err != nil {
|
|
slog.Warn("No preferences found, using defaults.", slog.Any("error", err))
|
|
if err := prefsSrc.Load(structs.Provider(defaultAgentPreferences, "toml"), nil); err != nil {
|
|
return fmt.Errorf("%w: %w", ErrLoadPreferences, err)
|
|
}
|
|
}
|
|
// Unmarshal config, overwriting defaults.
|
|
if err := prefsSrc.UnmarshalWithConf("", defaultAgentPreferences, koanf.UnmarshalConf{Tag: "toml"}); err != nil {
|
|
return fmt.Errorf("%w: %w", ErrLoadPreferences, err)
|
|
}
|
|
|
|
// Validate preferences.
|
|
return validate()
|
|
})()
|
|
}
|
|
|
|
// Reset will remove the preferences file.
|
|
func Reset() error {
|
|
preferencesFile = filepath.Join(Path(), preferencesFilename)
|
|
|
|
slog.Debug("Removing preferences.", slog.String("file", preferencesFile))
|
|
|
|
_, err := os.Stat(preferencesFile)
|
|
if os.IsNotExist(err) {
|
|
return fmt.Errorf("%w: %w", ErrLoadPreferences, err)
|
|
}
|
|
|
|
err = os.Remove(preferencesFile)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to reset preferences: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// validate ensures the configuration is valid.
|
|
func validate() error {
|
|
currentPreferences := &preferences{}
|
|
|
|
// Unmarshal current preferences.
|
|
if err := prefsSrc.UnmarshalWithConf("", currentPreferences, koanf.UnmarshalConf{Tag: "toml"}); err != nil {
|
|
return fmt.Errorf("%w: %w", ErrLoadPreferences, err)
|
|
}
|
|
|
|
// Ensure hass preferences are valid.
|
|
if err := validation.Validate.Struct(currentPreferences.Hass); err != nil {
|
|
return fmt.Errorf("%w: %s", ErrValidatePreferences, validation.ParseValidationErrors(err))
|
|
}
|
|
|
|
if currentPreferences.IsMQTTEnabled() {
|
|
// Validate MQTT preferences are valid.
|
|
err := validation.Validate.Struct(currentPreferences.MQTT)
|
|
if err != nil {
|
|
return fmt.Errorf("%w: %s", ErrValidatePreferences, validation.ParseValidationErrors(err))
|
|
}
|
|
}
|
|
|
|
slog.Debug("Preferences are valid.")
|
|
|
|
return nil
|
|
}
|
|
|
|
// Save will save the new values of the specified preferences to the existing
|
|
// preferences file. NOTE: if the preferences file does not exist, Save will
|
|
// return an error. Use New if saving preferences for the first time.
|
|
func Save() error {
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
|
|
preferencesFile = filepath.Join(Path(), preferencesFilename)
|
|
|
|
slog.Debug("Saving preferences.", slog.String("file", preferencesFile))
|
|
|
|
if err := validate(); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := checkPath(filepath.Dir(preferencesFile)); err != nil {
|
|
return err
|
|
}
|
|
|
|
b, err := prefsSrc.Marshal(toml.Parser())
|
|
if err != nil {
|
|
return fmt.Errorf("%w: %w", ErrSavePreferences, err)
|
|
}
|
|
|
|
err = os.WriteFile(preferencesFile, b, defaultFilePerms)
|
|
if err != nil {
|
|
return fmt.Errorf("%w: %w", ErrSavePreferences, err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// SetRegistered sets whether Go Hass Agent has been registered with Home
|
|
// Assistant.
|
|
func SetRegistered(value bool) error {
|
|
if err := prefsSrc.Set(prefRegistered, value); err != nil {
|
|
return fmt.Errorf("%w: %w", ErrSetPreference, err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Registered returns the registration status of Go Hass Agent.
|
|
func Registered() bool {
|
|
return prefsSrc.Bool(prefRegistered)
|
|
}
|
|
|
|
// SetAppID sets an ID that is used as part of the path to the preferences file.
|
|
func SetAppID(id string) {
|
|
appID = id
|
|
}
|
|
|
|
// AppID retrieves the ID.
|
|
func AppID() string {
|
|
return appID
|
|
}
|
|
|
|
// AppVersion returns the version of Go Hass Agent.
|
|
func AppVersion() string {
|
|
if appVersion != "" {
|
|
return appVersion
|
|
}
|
|
|
|
return "Unknown"
|
|
}
|
|
|
|
// Path returns a path where preferences are stored.
|
|
func Path() string {
|
|
return filepath.Join(xdg.ConfigHome, appID)
|
|
}
|
|
|
|
// checkPath checks that the given directory exists. If it doesn't it will be
|
|
// created.
|
|
func checkPath(path string) error {
|
|
_, err := os.Stat(path)
|
|
if os.IsNotExist(err) {
|
|
err := os.MkdirAll(path, os.ModePerm)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to create new directory: %w", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|