Read and control the house through Home Assistant (#256) #80
Closed
claude
wants to merge 1 commits from
overnight/smarthome into overnight/replay-simulator
pull from: overnight/smarthome
merge into: kami:overnight/replay-simulator
kami:master
kami:task/725-capability-ledger-and-empirical-baseline
kami:task/692-heads-path-may-equal-model-path-and-noth
kami:task/694-staticcheck-and-deadcode-are-still-not-i
kami:task/682-go-1-25-5-and-x-text-0-14-0-carry-20-rea
kami:task/674-caveats
kami:task/673-mavgpud-serves-the-model-to-the-whole-la
kami:task/487-capture-device-doc
kami:task/487-capture-device
kami:task/487-wake-word-deploy
kami:task/487-wake-word-threshold
kami:task/487-wake-word-stage-two
kami:task/671-mavwaked-registers-as-a-voice-consumer-i
kami:task/670-cut-claude-md-to-200-lines
kami:task/515-deploy-mavwaked-workpc
kami:task/669-prune-claude-md
kami:task/668-e4b-phrasing
kami:task/668-title-capital
kami:task/668-kiwix-answers-a-question-it-cannot-answe
kami:task/666-only-a-stage-0-grammar-may-take-the-pers
kami:task/487-mavwaked-has-no-wake-word-only-an-energy
kami:task/486-deploy-the-workstation-transcriber
kami:task/486-move-stt-and-tts-to-the-workstation-wher
kami:task/665-crisperwhisper-2-russian
kami:task/664-routing-heads-in-go
kami:task/662-usage-harness-source-badge
kami:task/661-post-merge-usage-rerun
kami:task/661-routing-heads-step-3-train-the-multi-hea
kami:task/660-router-prompt-destination
kami:task/659-destination-fixture
kami:task/655-query-source-is-a-routing-decision-made
kami:task/654-a-pending-clarify-has-no-way-out-neither
kami:task/654-week-of-usage-eval-docs
kami:task/649-needs-kami-telegram-is-the-only-reach-an
kami:task/643-memorystore-search-decodes-and-unmarshal
kami:task/641-two-maps-grow-for-the-process-lifetime-w
kami:task/644-mavcaldav-is-built-documented-as-running
kami:task/642-the-store-caps-sqlite-at-one-connection
kami:task/647-factenrichmentworker-walks-the-pending-q
kami:task/646-v-637-follow-up-telegram-intake-has-no-d
kami:task/638-no-deadline-survives-the-turn-path-from
kami:task/637-inbound-telegram-turns-and-corrections-f
kami:task/636-correcting-a-turn-from-telegram-and-from
kami:task/634-an-act-alias-resolves-the-verb-but-not-t
kami:task/630-one-gesture-correction-on-chat-v-628
kami:task/629-persist-the-routing-trace-and-record-it
kami:task/631-mode-inventory-written-from-the-handlers
kami:task/586-defaultfactparser-uses-hand-written-russ
kami:task/633-reconcile-the-seed-labels-with-the-handl
kami:task/627-reminder-verbs-has-no-alarm-verb-so-an-a
kami:task/626-the-classifier-seeds-teach-an-older-inte
kami:task/546-route-with-a-fine-tuned-e5-small-instead
kami:task/586-measure-the-fact-parser
kami:fix/gofmt-ecosystem-acts
kami:task/584-media-store-a-failed-write-leaks-its-bud
kami:task/518-no-write-path-for-a-backdated-event-so-t
kami:task/287-qa-voice-session-quality-polish
kami:task/492-qa-plan-reconcile
kami:task/530-sweep-tail-four-files-the-russian-sweep
kami:task/405-score-how-often-a-real-utterance-reaches
kami:task/529-money-and-list-pick-a-mechanism
kami:task/528-sweep-tail-the-three-files-on-467
kami:task/527-embedder-open-set-phrasings-stop-being-r
kami:task/526-morphology-a-dictionary-answers-the-gram
kami:task/525-lexicons-the-finite-russian-sets-move-to
kami:task/524-entity-reference-ask-nexus-about-every-l
kami:task/523-risk-tiers-take-hexis-s-tier-for-a-hexis
kami:task/521-review-pr-111-query-strings-declension-h
kami:task/491-llama-server-core-dumps-on-every-sigterm
kami:task/479-bug-an-unconfigured-capability-does-not
kami:task/467-bug-spoken-task-capture-is-dead-the-rout
kami:task/463-deploy-mavwaked-and-mavenclient-run-nowh
kami:task/480-hearing-no-shipped-client-can-start-a-re
kami:task/432-ambient-calendar-intake-is-fragile-and-p
kami:task/431-board-surface-maven-holds-the-work-board
kami:task/433-reactivehandler-has-30-fields-and-is-pas
kami:task/371-swap-the-embedder-for-an-asymmetric-retr
kami:task/408-review-31-07-split-the-30-method-coreapi
kami:task/410-review-31-07-hand-rolled-string-enums-st
kami:task/423-review-pr50-split-internal-ipc-server-go
kami:task/422-review-pr50-split-cmd-mavend-tick-go-860
kami:task/409-review-31-07-finish-moving-mavweb-markup
kami:task/482-ambient-ingest-reads-a-notification-s-ti
kami:task/444-kuma-a-fact-per-monitor-so-she-can-name
kami:task/452-capability-model-homelab-docker-restart
kami:task/449-destructive-confirm-policy-risk-tiers-no
kami:task/453-grocery-list-items-table-fourth-append-o
kami:task/399-run-the-persona-checks-inside-the-daemon
kami:task/448-bounded-follow-up-state-pending-candidat
kami:task/455-conversation-repair-name-the-misroute-co
kami:task/454-go-mod-tidy
kami:task/458-pronunciation-dictionary-for-piper
kami:task/456-command-history-read-only-query-over-exi
kami:task/457-clarification-templates-for-the-router-s
kami:task/474-query-source-ordering-feeds-and-calendar
kami:task/469-reminders-spelled-out-times-fail-the-bod
kami:task/475-bug-the-praxis-attention-capability-is-u
kami:task/481-bug-a-transient-complaint-is-stored-as-a
kami:task/476-bug-the-router-transliterates-latin-enti
kami:task/385-decide-whether-a-parked-clarify-question
kami:task/377-backfill-routines
kami:task/421-weather-geocoder
kami:task/390-no-read-path-for-delivery-attempts
kami:task/386-recall-fixture-filler-note-ids
kami:task/473-bug-morning-item-has-no-required-flag
kami:task/465-bug-make-simulate-routes-with-an-empty
kami:task/467-bug-spoken-task-capture-is-dead
kami:task/466-bug-a-pending-clarify-is-global-so-one-u
kami:task/468-bug-pattern-detect-has-no-minimum-interv
kami:task/462-bug-checkfeminine-flags-second-person-ma
kami:task/443-safekey-drops-cyrillic-so-russian-calend
kami:task/471-bug-agendaquerygrammars-covers-today-but
kami:task/383-slottext-in-clarify-answer-would-clobber
kami:task/323-qa-phraser-coverage-is-65-3-but-the-llam
kami:task/498-bug-and-x-reach-the-model-with-no-determ
kami:task/506-strings-family-6-summaries-and-reports-i
kami:task/504-strings-family-4-act-and-smart-home-repl
kami:task/503-strings-family-3-query-answers-and-gaps
kami:task/502-strings-family-2-capture-acknowledgement
kami:task/501-strings-family-1-phrasing-fallbacks-into
kami:task/397-phrasechat-and-phrasequery-hide-model-fa
kami:task/396-the-reply-path-can-t-be-tested-llmreplie
kami:task/496-recall-a-cross-language-question-loses-i
kami:task/495-bug-x-escapes-the-personal-boundary-and
kami:task/499-llama-server-holds-7-9gb-rss-for-a-1-1gb
kami:task/470-bug-a-question-writes-invented-knowledge
kami:task/493-bug-the-memory-index-stores-the-raw-utte
kami:task/490-name-the-gap-world-questions-through-the
kami:task/485-run-the-big-model-on-the-workstation-wit
kami:task/489-workstation-deploy-mavgpud-on-workpc-and
kami:task/488-workstation-a-supervisor-that-keeps-llam
kami:task/483-docs-offload-design
kami:task/483-design-offload-ml-to-the-workstation-kee
kami:task/459-docs-refresh-the-qa-plan-against-the-liv
kami:task/446-doc-reorg-tier-the-tree-retire-the-three
kami:fix/367-voice-parks-routine-accept
kami:task/365-dialogue-slots-and-router-slots-are-hand
kami:task/364-snooze-does-nothing-at-runtime-the-gate
kami:task/447-retire-progress-md-the-backlog-and-the-f
kami:task/445-session-workflow
kami:overnight/eco-versioned-traces
kami:overnight/eco-entity-refs
kami:overnight/eco-degraded-suite
kami:overnight/netscan
kami:overnight/replay-simulator
kami:overnight/event-envelope
kami:overnight/coldstart-unlock
kami:overnight/voice-barge-in
kami:overnight/stt-golden-audio
kami:overnight/senses-speaker
kami:overnight/senses-hearing
kami:overnight/senses-media-vision
kami:overnight/mcp-tools
kami:overnight/mcp-client
kami:overnight/self-update
kami:overnight/model-swap
kami:overnight/web-crawler
kami:overnight/rss-feeds
kami:overnight/email-poller
kami:overnight/email-extract
kami:overnight/email-imap
kami:overnight/money-zenmoney
kami:overnight/task-priority
kami:overnight/task-capture
kami:overnight/behavior-profile
kami:overnight/day-plan
kami:overnight/ambient-calendar
kami:overnight/local-calendar
kami:overnight/memory-eval
kami:overnight/proactive-proposals
kami:overnight/split-voice-quiet
kami:overnight/nginx-maven-block
kami:overnight/stepup-chat-surface
kami:integration/small-batch
kami:docs/fix-drift
kami:fix/ru-wording
kami:integration/jul31
kami:overnight/resident-1.7b
kami:overnight/nudge-templates
kami:overnight/kiwix-rewrite
kami:overnight/eval-writeup
kami:overnight/fix-truncation
kami:overnight/kiwix-client
kami:overnight/ru-prompts
kami:overnight/external-data
kami:overnight/phrasing-grammar
kami:overnight/talk-eval
kami:overnight/prompt-context
kami:overnight/prompt-address
kami:overnight/eval-label-kill
kami:overnight/delivery-boundary
kami:overnight/address-check
kami:overnight/system-replies-pr
kami:overnight/clock-intent-pr
kami:overnight/embedder-backfill-pr
kami:overnight/embedder-marker-pr
kami:overnight/note-recall-pr
kami:overnight/thinking-off-pr
kami:overnight/dialogue-persist-pr
kami:overnight/persona-2p-pr
kami:overnight/clarify-expiry-pr
kami:overnight/clarify-rework
kami:overnight/phrasing
kami:overnight/bakeoff
kami:overnight/recall-margin
kami:overnight/router-on
kami:overnight/slot-extract
kami:overnight/embedder-e5
kami:overnight/router-refusal
kami:overnight/eval-rerun
kami:overnight/eval-harnesses
kami:overnight/eval-rerun-base
kami:overnight/fmt-gate
kami:overnight/routines-fire
kami:overnight/router-prompt
kami:overnight/away-leak
kami:overnight/recall-eval
kami:overnight/snooze-works
kami:overnight/clarify-wiring
kami:overnight/delivery-tests
kami:overnight/routine-accept
kami:overnight/llm-router-flag
kami:overnight/clarify-data-layer
kami:overnight/loop-rule-tests
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Delete Branch "overnight/smarthome"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Read and control the house through Home Assistant (#256)
A
smarthomeblock points Maven at a Home Assistant instance. She reads itsentity states to answer "что включено дома?", and every controllable device
becomes a PROPOSED row in the existing act allowlist — cmd
["smarthome",<entity_id>,], scope smarthome: — so nothing new
had to be invented for the mutating half. ProposeTool/EnableTool/DisableTool,
tool.Matcher and the confirm turn are untouched; one branch in Executor.Exec
routes such a row to the client instead of exec, and "smarthome" is never run as
a binary. This is the same trick overnight/mcp-tools used for #251, on purpose.
Discovery only ever PROPOSES, and every control row is destructive=true: there
is no read-only way to turn the heating off, so flipping something in his flat
always costs a confirm turn and always had to be enabled by hand on /tools,
behind step-up.
The entity and the service come from the row he enabled, never from the
utterance — Exec drops the spoken tail for a house row. A router that misheard
can pick the wrong lamp; it cannot compose a target of its own. The service is
checked against the domain's table on the way out too, so a hand-edited cmd
column cannot reach an arbitrary Home Assistant service. set_brightness and
set_temperature are deliberately absent: a spoken number the router got wrong is
a wrong act on real hardware, and on/off is the whole of what a voice turn can
defend.
The read side is a query source ("home", before calendar and the recall passes)
so "что нового дома?" is not answered from an old note. Its matcher needs a
house marker plus an ask plus a device word and bails out on weather wording,
because "какая температура на улице?" belongs to the weather source.
Off unless configured: the block is dark without "enabled": true, and
applyDefaults normalises a disabled block to nil so "off" stays in one place.
deploy/mavend.json carries it disabled, with the token as ${HA_TOKEN}.
NOT shipped, and not faked: MQTT / Zigbee2MQTT (plan steps 2 and 5) and the
sensor-to-fact and presence-probe pipelines. There is no broker and no Home
Assistant anywhere on this network — 8123 and 1883 are closed on every host in
192.168.1.0/24 — the module tree is vendored so a paho dependency cannot be
added offline, and Home Assistant already fronts Zigbee2MQTT where it exists.
Writing a sensor pipeline with no sensor to test it against would be a guess.
Vikunja #256
Encoding a device as an allowlist row with
cmd = ["smarthome", "<entity_id>", "<service>"]is the right move. It reuses the enable gate, the confirm turn andtool.Matcherwithout touching any of them, exactly as MCP did. Dropping the spoken args in theExecbranch is the load-bearing detail. The router picks a row and never composes a target. A misheard number cannot become a different lamp. Leavingset_temperatureandset_brightnessout of thecontrollabletable for the same reason is consistent. The block ships"enabled": false.1.
MaxEntitiestruncates a globally id-sorted list, so sensors crowd out every switch.Statessorts byentity_idand then cuts toMaxEntities. Entity ids sort by domain prefix. The order isbinary_sensor<cover<fan<light<lock<sensor<switch.Take the default, an absent
domainslist.wantedthen admits the five controllable domains plussensorandbinary_sensor. A stock Home Assistant carries dozens ofbinary_sensor.*before it carries anything else: connectivity, motion, update-available, one per integration. WithMaxEntitiesat 40 and 40 binary sensors on the instance,Statesreturns 40 binary sensors and nothing else.proposethen finds zero controllable entities and proposes nothing at all, and the log line reads "40 entities, 0 controllable".The deploy block narrows to
["light", "switch", "sensor"], which does not fix it.light<sensor<switch, and a real instance has far moresensor.*rows than lamps. Everyswitch.*in the flat falls off the end.homeSummaryreads the same capped list, and that is where it stops being a discovery bug. With the controllable entities truncated away, theonslice is empty and she answers "всё выключено" while the lights are on. That is a spoken false statement about the house, produced by a cap that was meant to bound a catalogue.Cap per domain, or partition and take controllable entities first and fill the remainder with sensors.
2. "Every control row is destructive" is a comment, not an invariant.
internal/smarthome's package doc calls it non-negotiable, andProposeSmartHomeToolhardcodestrue. But enable is not propose.handleToolsreadsdestructive := r.FormValue("destructive") != ""from a checkbox, andEnableToolwritesdestructive=excluded.destructiveon the conflict path. The proposal row's own value is overwritten.So: the form pre-checks the box from the proposed row, Kami unchecks it, presses enable, and
home_lock_front_door_unlockis now an enabled non-destructive row.Execreachest.Destructive && !confirmedand passes straight through toCallService. The confirm turn is gone. That is the one row class whose whole argument is that the confirm turn is always there.Make it structural. In
Exec, after the destructive check, ifsmarthome.ParseCmd(t.Cmd)succeeds and!confirmed, returnErrNeedsConfirmregardless of the column. Then the guarantee holds no matter what any surface writes.3.
CallServicereports "готово" for a service call that changed nothing.doaccepts any 2xx andCallServicereturns the literal "готово". Home Assistant answersPOST /api/services/light/turn_offwith 200 and a JSON array of the states it changed. Anentity_idthat does not exist, or one whose integration is offline, gets 200 and[].Both cases are reachable. A row survives in the allowlist after the device is removed from HA. That is the case
ErrUnknownEntitywas written for, and it never fires:CallServicevalidates only the domain and the service name, both locally. And a Zigbee lamp with a flat battery is unavailable while its entity still exists.He says "выключи свет", she says "готово", the light stays on. Decode the response array and say so when it is empty. This is the one place in the PR where Maven asserts something about the physical world.
4.
queryHomeclaims the turn on a box that has nosmarthomeblock.isHomeQueryruns beforeh.homeis consulted, and the nil branch returns"дом не подключён — я его не вижу."withhandled=true. That fires on every Maven, configured house or not, since the query source is registered unconditionally.Walk it on the default box. He once said "дома температура 22", stored as a fact. He asks "какая температура в доме?".
isHomeQuerysees "в доме", an ask, and "температур", returns true, and the turn is claimed by a source backed by nothing. Before this PR that reachedqueryEmbedand the recall path. A capability that is off is not supposed to change what an unconfigured box answers.Return
"", falsewhenh.home == niland let the cascade continue. The "дом не подключён" line is right when the block exists but the instance is unreachable.homeSummaryalready covers that with "не смогла достучаться до дома".5.
wireSmartHomeblocks daemon startup on the house for up to 30 seconds.The comment says the daemon never waits on the house. Its words: "an instance that is down at boot is logged and retried, because Maven starting is not contingent on someone else's process". The code calls
w.propose(ctx)synchronously with a 30-second context. That happens insidewireVoice, insiderun, before the IPC socket is serving.A Home Assistant box that is powered off but still on a routed subnet does not refuse the connection. It black-holes it, and the client waits out the per-call
DefaultTimeoutof 10s. Maven's start is contingent on it, for as long as that takes. On the locked path this happens inside the passkey unlock handler, so the unlock request hangs too. Do the firstproposefromrun's goroutine and let the ticker pick it up.Smaller notes:
lockis incontrollableand in the default domain set, so a bare{"url":…, "token":…, "enabled":true}auto-proposesunlockrows for every deadbolt in the flat. Proposals are inert until enabled, and the confirm turn is finding 2's problem. But a front door is a different class of object from a lamp. Consider requiringlockto be named explicitly indomains.Validateacceptshttp, and the deploy block uses it. The long-lived token goes over the LAN in cleartext on every refresh. Worth one line inSmartHomeConfigsaying that is a deliberate wg-only choice.Refreshhas no floor."refresh": "1s"passes validation and enumerates the whole instance every second.applyDefaultsonly fills<= 0.unavailablematches neither the sensor branch nor the on branch ofhomeSummary, so it vanishes from the answer. "всё выключено" and "one lamp is unreachable" read the same to him.homeSummarytruncatesonto 5 and sensors to 3 with no "и ещё N". Silent truncation on a status read has the same shape as finding 1, one layer up.Landed on master. The stack was one linear chain, so #84 carried every commit from #50 up, and master now contains this branch in full. Merging this PR on its own is an empty diff, so it is closed rather than merged. The review findings for it were fixed in the 2026-08-01 pass and are on master as commits on the stack tip, not on this branch.
Pull request closed