smarthome: keep the devices under the cap, and stop asserting what the house did not do
States sorted every entity by id and cut at MaxEntities. Entity ids sort by domain prefix, so binary_sensor came first and forty slots went to connectivity and update-available rows: propose found nothing controllable, and homeSummary, reading the same list, said everything was off with the lights on. The cap stays, because a tool name the 1.7B half-remembers is a wrong act. What changes is which forty. Controllable domains are taken first and round-robin, so every switch and light is in before any sensor. CallService reported done for a call that changed nothing. Home Assistant answers a service call with the states it changed, and a removed entity or an offline integration gets 200 and an empty array. That is the one place Maven asserts something about the physical world, so an empty array is now ErrUnknownEntity. The confirm turn on a house row was a column, not an invariant. The proposal is destructive, but /tools writes the checkbox through on enable, so unticking it once made an unlock row that ran on first hearing. Exec now demands the second turn for any smarthome row whatever the column says, and lock is out of the default domain set so a bare block does not propose an unlock for every door. wireSmartHome enumerated the house synchronously, inside wireVoice, before the socket was serving and inside the unlock handler. A box that black-holes the connection held the daemon's start for the per-call timeout. The first propose moved onto the ticker goroutine. Smaller: an unreachable lamp is counted apart from an off one, a truncated on-list says how many it left out, refresh has a floor of a minute, and the http url is documented as a deliberate wg-only choice. Found in review of #80. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TrVSBKe3RFDF4fGYKWYQnX
This commit is contained in:
+42
-9
@@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -44,9 +45,12 @@ func wireSmartHome(cfg *config.Config, st *store.Store) *homeWiring {
|
||||
st: st,
|
||||
refresh: time.Duration(cfg.SmartHome.Refresh),
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
w.propose(ctx)
|
||||
// No first propose here. This runs inside wireVoice, inside run, before the
|
||||
// IPC socket is serving, and on the locked path inside the passkey unlock
|
||||
// handler. A Home Assistant box that is powered off but still on a routed
|
||||
// subnet black-holes the connection rather than refusing it, so a
|
||||
// synchronous enumeration held the daemon's start for the per-call timeout.
|
||||
// run does the first propose off the ticker instead.
|
||||
return w
|
||||
}
|
||||
|
||||
@@ -113,6 +117,9 @@ func (w *homeWiring) run(ctx context.Context) {
|
||||
}
|
||||
t := time.NewTicker(iv)
|
||||
defer t.Stop()
|
||||
// The first enumeration, off the daemon's start path. wireSmartHome used to
|
||||
// do it synchronously and a dead house delayed the socket coming up.
|
||||
w.propose(ctx)
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
@@ -140,28 +147,54 @@ func (w *homeWiring) homeSummary(ctx context.Context) (string, bool) {
|
||||
}
|
||||
var on []string
|
||||
var sensors []string
|
||||
dark := 0
|
||||
for _, e := range ents {
|
||||
switch {
|
||||
case e.Domain == "sensor" || e.Domain == "binary_sensor":
|
||||
if len(sensors) < 3 && e.State != "" && e.State != "unavailable" {
|
||||
if e.State == "" || e.State == "unavailable" {
|
||||
dark++
|
||||
continue
|
||||
}
|
||||
if len(sensors) < 3 {
|
||||
sensors = append(sensors, e.Name+" "+e.State+e.Unit)
|
||||
}
|
||||
case e.State == "unavailable" || e.State == "unknown" || e.State == "":
|
||||
// A lamp that is not reachable is not a lamp that is off. Counting
|
||||
// it as neither used to make "всё выключено" and "one device is
|
||||
// unreachable" read identically.
|
||||
dark++
|
||||
case e.State == "on" || e.State == "open" || e.State == "unlocked":
|
||||
on = append(on, e.Name)
|
||||
}
|
||||
}
|
||||
var parts []string
|
||||
if len(on) > 0 {
|
||||
if len(on) > 5 {
|
||||
on = on[:5]
|
||||
switch {
|
||||
case len(on) > 0:
|
||||
shown, rest := on, 0
|
||||
if len(shown) > 5 {
|
||||
rest = len(shown) - 5
|
||||
shown = shown[:5]
|
||||
}
|
||||
parts = append(parts, "включено: "+strings.Join(on, ", "))
|
||||
} else {
|
||||
// Silent truncation on a status read is the same failure as the cap
|
||||
// one layer up: she has to say the list is not the whole list.
|
||||
line := "включено: " + strings.Join(shown, ", ")
|
||||
if rest > 0 {
|
||||
line += fmt.Sprintf(" и ещё %d", rest)
|
||||
}
|
||||
parts = append(parts, line)
|
||||
case dark > 0 && len(sensors) == 0:
|
||||
// Nothing is on and everything she can see is unreachable. "всё
|
||||
// выключено" would be a claim about the house she cannot make.
|
||||
return fmt.Sprintf("дом молчит: %d %s не отвечают.", dark, hostWord(dark)), true
|
||||
default:
|
||||
parts = append(parts, "всё выключено")
|
||||
}
|
||||
if len(sensors) > 0 {
|
||||
parts = append(parts, strings.Join(sensors, ", "))
|
||||
}
|
||||
if dark > 0 {
|
||||
parts = append(parts, fmt.Sprintf("%d %s не отвечают", dark, hostWord(dark)))
|
||||
}
|
||||
return strings.Join(parts, "; ") + ".", true
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user