package config import ( "fmt" "time" "github.com/kami/maven/internal/smarthome" ) // SmartHomeConfig — the Home Assistant block (Vikunja #256). Dark until // `"enabled": true`, and even then a discovered device is only ever PROPOSED // into the act allowlist: Kami enables it on /tools, behind step-up, exactly as // he would a shell tool. Finding a switch on the network is not the same as // being allowed to flip it. type SmartHomeConfig struct { // Provider — only "homeassistant" is implemented. MQTT / Zigbee2MQTT are // not: Home Assistant already fronts them, and a broker client is a // dependency this vendored module tree cannot take on tonight. Provider string `json:"provider,omitempty"` // URL — the instance base, "http://192.168.1.50:8123". // // Plain http is accepted and is what the deploy block uses. That is a // deliberate choice, not an oversight: the instance is on the LAN behind // wireguard, and a self-signed cert on a home box buys a warning rather // than a guarantee. It does mean the long-lived token crosses the LAN in // cleartext on every refresh, so the LAN is part of the trust boundary. URL string `json:"url,omitempty"` // Token — a long-lived access token. Use ${HA_TOKEN} and keep the value in // the gitignored env file, like the telegram credentials. Token string `json:"token,omitempty"` // Domains — entity domains to take. Empty ⇒ the controllable domains // EXCEPT lock (light, switch, fan, cover) plus sensor and binary_sensor // for reads. A lock is only enumerated when it is named here, because a // front door is not a lamp. Narrow it when the instance is large: a tool name the 1.7B // half-remembers is a wrong act. Domains []string `json:"domains,omitempty"` // MaxEntities — cap on the proposal catalogue. 0 ⇒ 40. MaxEntities int `json:"max_entities,omitempty"` // Timeout — per-call budget. 0 ⇒ 10s. Timeout Duration `json:"timeout,omitempty"` // Refresh — how often the entity list is re-read and new devices proposed. // 0 ⇒ 15m, and anything under MinSmartHomeRefresh is raised to it: // "refresh": "1s" used to pass validation and enumerate the whole instance // every second. Discovery is idempotent, so this only ever adds rows. Refresh Duration `json:"refresh,omitempty"` // Enabled — false (the default) keeps a written block dark, so it can be // reviewed before the house is wired to a voice. Enabled bool `json:"enabled,omitempty"` } // DefaultSmartHomeRefresh — how often the house is re-enumerated for new // devices. Slow on purpose: discovery only adds proposals, and a flat does not // grow a new lamp every minute. const DefaultSmartHomeRefresh = 15 * time.Minute // MinSmartHomeRefresh — the floor under SmartHomeConfig.Refresh. Enumerating // every entity in the house is a full /api/states read; a misconfigured second // would hammer the instance for proposals that are idempotent anyway. const MinSmartHomeRefresh = time.Minute // SmartHomeClient maps the config block onto the smarthome package's own type. // Returns ok=false when nothing is configured or it is disabled, so validation // and daemon wiring cannot drift on the mapping. func (c *Config) SmartHomeClient() (smarthome.Config, bool) { if c.SmartHome == nil || !c.SmartHome.Enabled { return smarthome.Config{}, false } return smarthome.Config{ URL: c.SmartHome.URL, Token: c.SmartHome.Token, Domains: c.SmartHome.Domains, MaxEntities: c.SmartHome.MaxEntities, Timeout: time.Duration(c.SmartHome.Timeout), }, true } // normaliseSmartHome applies the block's defaults. A block that is not enabled // is the same as no block at all, so "off" stays in one place. func (c *Config) normaliseSmartHome() { if c.SmartHome != nil && !c.SmartHome.Enabled { c.SmartHome = nil } if c.SmartHome != nil && c.SmartHome.Refresh <= 0 { c.SmartHome.Refresh = Duration(DefaultSmartHomeRefresh) } if c.SmartHome != nil && c.SmartHome.Refresh < Duration(MinSmartHomeRefresh) { c.SmartHome.Refresh = Duration(MinSmartHomeRefresh) } } // validateSmartHome fails a missing token or a bare hostname at startup, not at // the first "выключи свет". func (c *Config) validateSmartHome() error { hc, ok := c.SmartHomeClient() if !ok { return nil } if p := c.SmartHome.Provider; p != "" && p != "homeassistant" { return fmt.Errorf("smarthome: provider %q: only \"homeassistant\" is implemented", p) } return smarthome.Validate(hc) }