Refactor to door-lock-driven guest lifecycle + housekeeping state#2
Merged
Conversation
Replaces the original "current/next guest with phone/email" model with a single-property, lock-aware lifecycle: - Guest status: reserved → due_in → in_house → departed → vacant, derived from PMS data + a locally-latched entered_at flag set by lock events. - House state: ready / occupied / dirty / cleaning, persisted across restarts and advanced via dedicated buttons. - Departed is held for a predictable 10s window so automations (auto-lock, away mode) fire reliably before the next guest rotates in. - Lock trigger source is configurable: Keymaster slot events or a generic HA lock/sensor entity with user-defined unlock states. - State persisted via homeassistant.helpers.storage.Store. Removed: phone/email fields, multi-property selection, next-guest sensors, Mark Checked Out button. Added: House State sensor, Mark Cleaning Started / Mark Ready / Mark Departed buttons, three lifecycle events (guest_changed, guest_status_changed, house_state_changed). https://claude.ai/code/session_01V68vQAmMQwUctanNhh1tN9
- Switch every entity to translation_key so names come from the translations bundle in the user's language. - Add entity name + enum state translations for guest_status (reserved/due_in/in_house/departed/vacant) and house_state (ready/occupied/dirty/cleaning) across all four locales. - Drop the "Current" prefix on the guest name sensor / unique_id — single-guest scope makes it redundant. https://claude.ai/code/session_01V68vQAmMQwUctanNhh1tN9
Match the project branding throughout: - HA domain, module folder, manifest, hacs.json - Event names (str_concierge_guest_changed, …) - Service domain (str_concierge.sync_keymaster) - Device identifier tuple - Test imports - README, Makefile, devcontainer https://claude.ai/code/session_01V68vQAmMQwUctanNhh1tN9
… notes - Rewrite README around what users get and how to wire it into automations. Expand the Keymaster section into a step-by-step end-to-end walkthrough (create slot → point STR Concierge at it → auto-push PIN on guest change). Pull out development, testing, and PMS internals so the README stays approachable for non-technical hosts. - Add CONTRIBUTING.md as the home for community contributions. Open with why we want help (every host uses a different PMS), document the STRProvider interface and 5-step recipe for adding one, and call out that partial support is fine — list the patterns for missing get_properties, missing door codes, no mark_arrived/checked_out endpoints, rate-limit constraints, and webhook-only PMSes. Move the full dev lifecycle here: symlink workflow, remote SSH, tests, lint, triggering state transitions during testing. - Add docs/providers/ with an index, per-provider implementation notes for Host Tools (auth, endpoints, field mapping, quirks) and Custom Endpoint (API contract that was previously embedded in the README). https://claude.ai/code/session_01V68vQAmMQwUctanNhh1tN9
Add a "Getting Python set up on macOS" subsection covering the three viable paths — VS Code dev container, pyenv, Homebrew — with honest trade-offs and a "what we don't recommend" list (system Python, Anaconda, --user pip installs). https://claude.ai/code/session_01V68vQAmMQwUctanNhh1tN9
Both were written from the public API docs and pass the unit tests, but neither has been confirmed against a live account. Mark them with a warning in the README and per-provider docs index, and invite users who try them to report back. https://claude.ai/code/session_01V68vQAmMQwUctanNhh1tN9
Expand "Remote HA over SSH" in CONTRIBUTING into three focused sections: configuring HASS_CONFIG / HASS_SSH (with a table mapping HA install types to config paths), step-by-step SSH setup for HA OS / Container / Core, and a troubleshooting table covering the common make deploy / deploy-ssh failure modes (Connection refused, mDNS, publickey, rsync missing on the official SSH add-on). Also drop the stale HASS_TOKEN reference from the Makefile header — nothing in the Makefile actually uses it. https://claude.ai/code/session_01V68vQAmMQwUctanNhh1tN9
Expand the HA OS SSH setup with the three traps the first contributor hit: ssh.port: 0 disables the SSH server entirely (matching the add-on log line "SSH port is disabled"), rsync isn't installed by default and needs packages: [rsync] in the add-on config, and the VS Code dev container has no keys of its own. Add a "When you're running inside the VS Code dev container" subsection covering both the quick fix (generate a key in the container) and the permanent fix (bind-mount ~/.ssh from the Mac host into the container via devcontainer.json). Extend the troubleshooting table to distinguish the failure modes: "Connection refused with SSH-port-disabled log", "Connection refused without log", "Permission denied from Mac" vs "Permission denied from inside the dev container", "rsync not found after SSH succeeds" vs "can't shell out at all (wrong add-on)". https://claude.ai/code/session_01V68vQAmMQwUctanNhh1tN9
Based on a working reference implementation from a sister project,
the previous Host Tools client was wrong on every axis:
- Base URL was /api/v1, should be /api (no version segment). The
wrong path returns the marketing SPA's HTML instead of a 404,
which silently swallowed the bug.
- Auth header was `Authorization: Bearer <token>`. Host Tools
requires a custom `authToken: <token>` header — Bearer is also
silently routed to the SPA.
- get_properties hit /listings, which doesn't exist. Host Tools
has no listings-list endpoint, so we now collect the listing ID
during setup and surface it via a synthetic Property.
- get_property_data hit /reservations?listingId=…, also wrong.
Real path is GET /getReservations/{listingId}/{startDate}/{endDate}
with dates as YYYY-MM-DD in the URL path.
- Reservation field shape was wrong: real fields are
guestFirstName + guestLastName (not guestName), checkIn/checkOut
(not startDate/endDate as the primary form), and status values
are accepted/confirmed/pending/inquiry — with cancelled/canceled/
declined/blocked needing to be filtered out (they share the
response stream with real bookings).
Wire it in:
- Add CONF_HOST_TOOLS_LISTING_ID, ask for it in the credentials
step when provider is host_tools, persist in entry data, pass
through create_provider(**extra) into HostToolsProvider.
- Translate the new credentials field for en/de/es/nl.
- mark_arrived / mark_checked_out: drop (inherit base class
NotImplementedError). Host Tools has /setreservation for
field writes but no status-transition endpoint; local state is
truth for guest lifecycle anyway.
- Rewrite tests against the new endpoint shape + add coverage for
cancelled/blocked filtering and the authToken header.
- Rewrite docs/providers/host_tools.md with the corrected info,
including the two "silently returns HTML" gotchas so the next
person debugging doesn't waste an afternoon.
https://claude.ai/code/session_01V68vQAmMQwUctanNhh1tN9
The /api/getListings endpoint does exist after all — drop the
manual-listing-ID config field and go back to the standard flow
of fetching the listings, populating a dropdown, and letting the
user pick.
Reverts the listing_id plumbing added in the previous Host Tools
fix (CONF_HOST_TOOLS_LISTING_ID, factory **extra kwarg, config
flow extra field, translations, init pass-through) since it's no
longer needed. Keeps the rest of that commit: correct base URL,
authToken header, correct reservation field shape, cancelled/blocked
filtering.
Implementation:
- HostToolsProvider.get_properties now calls GET /getListings
and parses with _parse_listing (field aliases _id/id/listingId
→ property_id, nickname/name/title → property_name)
- Add unit tests for the listings endpoint including alternate
field names and the wrapped-envelope shape
- Update docs/providers/host_tools.md to describe both endpoints
https://claude.ai/code/session_01V68vQAmMQwUctanNhh1tN9
The lock trigger option now has three values: "Lock entity",
"Keymaster slot", and "Disabled — manual buttons only". Picking
Disabled installs no listener (no warning either) so users who
only want the manual Mark Guest Arrived/Departed buttons have a
clean, intentional choice.
Add INFO-level logging the user can actually see without flipping
to DEBUG:
- coordinator logs every guest_changed / guest_status_changed /
house_state_changed transition with before → after values
- Host Tools provider logs the result of each /getReservations
poll (how many returned, how many kept, how many filtered),
so "nothing is syncing" can be diagnosed at a glance
- Skipped reservations log their booking ID and status at DEBUG
for follow-up
Keep per-poll state snapshots at DEBUG (status, house state,
current/next bookings, lock window, entered_at) so bumping the
logger to debug gives a complete picture without parsing events.
https://claude.ai/code/session_01V68vQAmMQwUctanNhh1tN9
Three changes:
1. Default the "Lock trigger source" to "Disabled — manual buttons
only" via DEFAULT_LOCK_TRIGGER_SOURCE. New installs don't get a
"no entity configured" warning until the user explicitly opts in
to lock-based detection.
2. Add CONF_CLEANER_KEYMASTER_SLOT — a second, independent Keymaster
slot for the cleaner. When the cleaner enters that slot's PIN and
the house state is `dirty`, it auto-flips to `cleaning`. Other
states are no-ops with a DEBUG log (we don't want a cleaner
entering while a guest is in-house to silently reset the workflow).
The cleaner listener is installed regardless of the main lock
trigger source — guest-arrival detection and cleaner-arrival
detection are independent concerns. A user can leave the main
trigger Disabled and still get auto cleaner detection.
3. Document "what's automatic vs. what's manual" in the README:
add a transition table making it explicit that "Mark Guest
Departed" (in_house → departed) and "Mark Ready" (cleaning →
ready) are the two transitions that always require a button
press or an external automation. Add Step 5 to the Keymaster
walkthrough covering the cleaner slot.
Plumbing:
- const.py: new CONF, new DEFAULT_LOCK_TRIGGER_SOURCE constant
- __init__.py: install cleaner listener up front, then dispatch
on trigger source; default switched
- coordinator.py: new async_handle_cleaner_arrived; import
HOUSE_CLEANING
- config_flow.py: new optional field, default switched
- translations: keymaster_slot label clarified as "(guest arrival)"
and a parallel "(cleaner arrival)" added for all four locales
https://claude.ai/code/session_01V68vQAmMQwUctanNhh1tN9
Create branding/ with four PNG placeholders at the dimensions the home-assistant/brands repo expects (icon 256² + @2x 512², logo 256×128 + @2x 512×256). They're intentionally crude — cyan background, "STR" lettering, a big PLACEHOLDER banner — so it's obvious they need to be replaced before submission. branding/README.md documents: - What each file is for and what dimensions are required - That HA loads brand assets from the home-assistant/brands CDN at custom_integrations/str_concierge/*, not from this repo at runtime - How to swap in real artwork and PR it to the brands repo - Design guidance (transparent background preferred, must work at 32² favicon scale) CONTRIBUTING.md gets a "Designers welcome too" section pointing at branding/README.md so it's discoverable. https://claude.ai/code/session_01V68vQAmMQwUctanNhh1tN9
The provider's _pick_current_and_next only marks a booking as `current` when [checkin <= now <= checkout]. An upcoming guest with a checkin in a few hours stays in `next_guest` until they actually start, which meant the integration sat at `vacant` and no entities populated — even though the user's README said "when current is vacant and the next booking's arrival window opens, that booking takes the Current Guest slot automatically". Add _promote_upcoming() in the coordinator: when current is None but next exists and `now >= next.checkin - arrival_window`, promote next to current. The rest of derive_state then naturally flips status to `due_in` and populates name, door code, check-in/out, lock window sensors. Regression seen in the wild: a booking for "Kaite Sambrook" with checkin in ~10 hours and arrival_window=4 left the integration showing vacant and no upcoming-guest data anywhere. Two new tests: - test_next_guest_promoted_to_current_inside_arrival_window - test_next_guest_NOT_promoted_outside_arrival_window https://claude.ai/code/session_01V68vQAmMQwUctanNhh1tN9
Drop the arrival-window gate from _promote_upcoming. Hosts want to
see who's coming next the moment the current guest departs, even
if the next booking is months away. Status derivation already
handles the time-based naming naturally:
- far in the future → reserved
- within arrival window → due_in
- actively in-house → in_house
- within courtesy window → departed
- nothing at all on calendar → vacant
So `vacant` is now reserved (heh) for the genuine empty-calendar
case rather than "the next booking is more than N hours away".
Test renamed and inverted:
test_next_guest_NOT_promoted_outside_arrival_window
→
test_next_guest_promoted_even_when_far_in_future
(now asserts the promotion happens and the status is `reserved`,
not `vacant`).
README updated:
- Guest Status bullet calls out the always-show-next behaviour
explicitly
- "departed → next guest / vacant" row clarifies that rotation
happens regardless of how far the next booking is
https://claude.ai/code/session_01V68vQAmMQwUctanNhh1tN9
…nputs
User-driven changes:
1. Default provider look-ahead to 30 days (was 90 in host_tools, 90
each in hostfully + guesty). Make it configurable via a new
"Vacancy threshold (days)" option, default 30. Provider fetches
only that far ahead, and the coordinator's _promote_upcoming
refuses to surface a next_guest whose checkin is beyond the
threshold — so `vacant` now means "no booking inside the next N
days" with N user-controlled.
2. Rename arrival_window_hours → arrival_window_minutes so the time
units match the lock-window options. Default 240 (= 4 hours).
__init__.py keeps a one-shot backward-compat read of the old key
for users who already have it persisted.
3. Replace vol.Range integer fields in the options form with
selector.NumberSelector(mode=BOX) so they render as a text-box
number input with a unit-of-measurement label, not a slider.
Coerce back to int on submit since NumberSelector returns floats.
Plumbing:
- providers/__init__.py: factory takes lookahead_days, passes to
each provider's constructor
- host_tools / hostfully / guesty: accept lookahead_days, store
as a timedelta, use in the fetch window
- coordinator: takes both arrival_window_minutes AND
vacancy_threshold_days; _promote_upcoming gates on the threshold
- translations: rename arrival_window field, add vacancy_threshold
field across en/de/es/nl
- README: reflect new defaults + new field
New regression test:
test_vacant_when_next_guest_beyond_vacancy_threshold — a booking
6+ months out stays vacant with the default 30-day threshold.
https://claude.ai/code/session_01V68vQAmMQwUctanNhh1tN9
- Drop the optional base_url field from the Guesty credentials step. Guesty has a single public API endpoint; the constructor still accepts a base_url override so tests and edge cases keep working, but the UI no longer surfaces an extra field that confuses users. - Reorder PROVIDER_OPTIONS so Custom Endpoint comes last. It's the "I run my own backend" fallback and shouldn't sit between the named PMS providers in the dropdown. https://claude.ai/code/session_01V68vQAmMQwUctanNhh1tN9
Rework the Custom Endpoint provider against the new path shape:
GET /properties/{propertyId}/reservations
POST /properties/{propertyId}/reservations/{reservationId}/arrive
POST /properties/{propertyId}/reservations/{reservationId}/checkout
(Was: GET /reservations?propertyId=…, POST /reservations/{id}/…)
The reservations endpoints now embed propertyId in the path, so the
provider stashes it from the first get_property_data call and reuses
it for mark_arrived / mark_checked_out. A defensive RuntimeError fires
if those are called before any get_property_data — useful in tests
and surfaces wiring bugs early.
Also publish docs/providers/backend-api-spec.md: a complete,
standalone API specification written so it can be handed to another
agent or developer and implemented end-to-end. Includes auth shape,
all four endpoints with worked examples, the field-alias table,
status-value semantics (including "drop cancelled bookings"), error
conventions, and a conformance checklist.
The existing docs/providers/custom_endpoint.md is rewritten as a
short integration-side summary that points at the spec for the
wire-level details.
Tests:
- test_get_property_data now mocks /properties/p1/reservations
- test_mark_arrive / test_mark_checkout fetch property data first
so the provider knows the property_id
- new test_mark_actions_fail_before_property_known guards against
silently posting to a malformed URL
All 43 tests pass; ruff clean.
https://claude.ai/code/session_01V68vQAmMQwUctanNhh1tN9
Two parsing fixes for Host Tools, both confirmed against live data:
1. Check-in / check-out times were always midnight UTC because the
parser only read the date fields (`checkIn`, `checkOut`). Host
Tools sends the time of day as separate fields — `checkInTime`,
`checkOutTime` (plus `checkin_time` / `checkout_time` /
`startTime` / `endTime` as variant aliases). The time field
comes in any of these shapes:
16 → 16:00
15.5 → 15:30
"16" → 16:00
"15:30" → 15:30
"4:00 PM" → 16:00
"4 PM" → 16:00
ISO ts → pull the HH:MM portion
Add _parse_time_of_day() that handles all of those and
_combine_date_and_time() that overlays the parsed time onto the
date. Falls back to the date's embedded time if the explicit
time field is absent.
2. Door code field is actually `lockCode` in the Host Tools API,
not `doorCode`. Add `lockCode` first in the alias list; keep
`doorCode` / `door_code` / `accessCode` / `access_code` as
defensive fallbacks for variant payloads. When parsing succeeds
but no door code is found, log the full payload key list at
DEBUG so the next "where's my code?" debugging session is
one log line, not a curl session.
Tests: 8 new — 6 for time parsing (numeric, HH:MM, 12-hour, fractional,
ISO-fallback, midnight-default) + 2 for the lockCode field and the
alias chain. 51 total pass.
Docs/providers/host_tools.md gets a new "Date + time-of-day handling"
subsection with the input → interpretation table, and the door-code
section calls out lockCode as canonical.
https://claude.ai/code/session_01V68vQAmMQwUctanNhh1tN9
Confirmed bug: when Host Tools sends checkIn="2026-06-01" (date only)
and checkInTime=16, the `16` means 4 PM in the property's local
timezone — but the previous code stamped it as 16:00 UTC, which is
wrong for every HA install outside UTC.
Fix _combine_date_and_time():
- If date_value is date-only (YYYY-MM-DD), build a naive datetime,
attach dt_util.DEFAULT_TIME_ZONE, then convert to UTC. So a
Vienna HA install reading checkInTime=16 in summer stores
2026-06-01 14:00 UTC; the HA UI then renders it back as 16:00
local for the user.
- If date_value is already a full ISO timestamp with embedded
offset, we trust the offset and don't reinterpret as local.
Overrides from time_value still apply but in the date's zone.
- Date-only with no time field defaults to local midnight,
converted to UTC (was: UTC midnight directly).
3 new tests pin the behaviour:
- Vienna summer (DST, UTC+2): checkInTime=16 → 14:00 UTC
- Vienna winter (UTC+1): checkInTime=16 → 15:00 UTC
- ISO timestamp with `Z`: kept as UTC regardless of HA tz
Other time tests updated to use a utc_tz fixture so they're
explicit about which timezone they assume — no more accidental
UTC-only correctness.
docs/providers/host_tools.md gains a "Timezone semantics" subsection
explaining the assumption (HA's tz == property's tz), the conversion
flow, and the remote-property workaround.
54 tests pass.
https://claude.ai/code/session_01V68vQAmMQwUctanNhh1tN9
Document the guest-status and house-state state machines visually via two Mermaid stateDiagrams, with the relevant configuration options labelled directly on each transition. GitHub renders both inline so contributors see them in the README without leaving the tab. Mermaid was chosen over SVG/PNG so the diagrams diff cleanly in PRs and stay maintainable. The guest-status diagram covers the full vacant → reserved → due_in → in_house → departed → (next/vacant) loop, with vacancy_threshold_days, arrival_window_minutes, and the lock_minutes_* options annotated on the arrows. A side note spells out how the lock-access window bounds in_house latching. The house-state diagram is a separate, smaller chart since house state is independent of guest state except for the two automatic crossings (in_house → occupied, departed → dirty). Add a "Config knobs at a glance" table beneath the diagrams that maps each option key to which transition it affects and the default value — so users can read the chart, find the knob they want, and jump straight to Configure. https://claude.ai/code/session_01V68vQAmMQwUctanNhh1tN9
Remove _attr_entity_registry_visible_default = False from the GuestDoorCodeSensor. That flag only affects first-registration visibility, but HA still surfaces a "(Hidden)" tag in the entity list even after the user un-hides it — confusing because the value IS visible. Trade-off: the door PIN is no longer auto-hidden in dashboards. Update the README to call out "keep this off public dashboards" as a guideline rather than promise something the code doesn't reliably deliver. Users who want it hidden can do so per-entity in HA's UI. https://claude.ai/code/session_01V68vQAmMQwUctanNhh1tN9
Per user request, re-add _attr_entity_registry_visible_default = False on GuestDoorCodeSensor. Default behaviour for fresh installs: door PIN is registered as hidden in the entity registry. Users who want to surface it on a dashboard can un-hide it per-entity from the UI. Update the README to spell out how to un-hide it if desired, since "(Hidden)" can be confusing when first encountered. https://claude.ai/code/session_01V68vQAmMQwUctanNhh1tN9
Two improvements:
1. Device name now shows the rental name ("Beach House"), not the
opaque listing ID. The config flow already presents friendly
names in the property dropdown — we just weren't keeping the
picked name around afterwards. Store it as
CONF_PROPERTY_NAME in entry.data, plumb through the coordinator
as `property_name`, and use it in entity.device_info as the
fallback when the PMS provider doesn't echo the listing name
per poll (Host Tools doesn't, since /getReservations only
returns reservation rows). The live PMS name still wins when
available; the captured name is the second choice; the raw
property ID is now only used for entries created before this
change exists.
2. Add a DEBUG log line inside _combine_date_and_time showing the
raw input fields, the resolved local timezone, the constructed
local datetime, and the resulting UTC. When a user reports
"PST 11am shows as 4am" we can confirm in one log line whether
dt_util.DEFAULT_TIME_ZONE was actually set to America/Los_Angeles
or still defaulting to UTC.
Tests updated to pass property_name to the coordinator fixture.
https://claude.ai/code/session_01V68vQAmMQwUctanNhh1tN9
The previous fix only treated the date-only branch as local — full ISO timestamps (e.g. "2026-05-31T11:00:00Z") were trusted as UTC. Empirically, Host Tools sends property-local times with a misleading `Z` suffix: a user in PST reported their 11 AM check-in showing as 4 AM in the HA UI, which is exactly the symptom of taking the `Z` at face value (11:00 UTC rendered in PDT = 04:00). Unify the helper: always extract date + time-of-day from whatever the field contains, ignore any offset, stamp HA's configured tz, convert to UTC. The reference TypeScript implementation behaves the same way (extracts only the HH:MM portion, drops the offset). Regression test added: test_pst_11am_iso_z_regression — under America/Los_Angeles, a Host Tools payload of "T11:00:00Z" becomes 18:00 UTC, so HA renders 11:00 AM PDT to the user. The previous "ISO timestamp keeps its own offset" test had to flip sign — now it asserts the offset is ignored. Renamed to test_iso_timestamp_with_z_is_treated_as_local and updated test_iso_timestamp_no_explicit_time_field to be explicit about the UTC default. Docs: rewrite the "Timezone semantics" subsection with a side-by-side table showing the three input shapes and what they resolve to under America/Los_Angeles, with a note that the `Z` suffix is a Host Tools mis-encoding to be ignored. https://claude.ai/code/session_01V68vQAmMQwUctanNhh1tN9
Just a label change — translation_key stays "mark_ready" so the entity ID and unique_id are unchanged. Updates all four locales (en/de/es/nl) plus the README references. https://claude.ai/code/session_01V68vQAmMQwUctanNhh1tN9
Stand up .github/workflows/ci.yml as the PR gate. Four parallel
jobs, all required:
- **Ruff** `ruff check custom_components/ tests/`
- **Pytest** Full suite on Python 3.13 with coverage
- **Hassfest** HA's official manifest + structure validator
- **HACS** hacs/action@main, category: integration
Triggered on pull_request, push to main, and workflow_dispatch
(useful when a transient external-action failure can be re-run
without an empty commit). Adds concurrency group keyed to the
ref so a follow-up push cancels the previous in-flight run.
Pip cache is keyed to requirements_test.txt for fast warm starts.
Pinned Python 3.13 to match the pytest-homeassistant-custom-component
0.13.185 minimum.
CONTRIBUTING.md gets a new "What CI runs on your PR" subsection
mapping each job to the local make command (where one exists), so
contributors can reproduce gate failures locally. Also drops the
"or 4 pre-existing aioresponses failures" caveat — those were the
custom-endpoint URL mismatches, fixed when the provider moved to
the nested /properties/{id}/reservations path.
https://claude.ai/code/session_01V68vQAmMQwUctanNhh1tN9
Investigated the run on commit 70282f3 — all four checks failed for distinct reasons: **Ruff (10 errors)** - 6× E402 in tests/conftest.py: imports after the required `pytest_plugins = "..."` declaration. This pattern is mandatory for pytest-homeassistant-custom-component. Add pyproject.toml with a per-file ignore for that file. - 4× F401 unused imports across test files. `ruff --fix` cleaned these plus 37 other modernizations (mostly `from datetime import timezone` → `UTC`). - 1× B017 blind `pytest.raises(Exception)` → tightened to aiohttp.ClientResponseError. - 1× E501 long inline comment → reflow to two lines. **Pytest (install fails)** - requirements_test.txt was over-pinned. freezegun==1.5.0 conflicts with the rest of the pinned toolchain on the GitHub Actions runner; also freezegun is unused (no `from freezegun` anywhere). Rewrite the file: pin only pytest-homeassistant-custom-component (since it drives the whole HA transitive tree) and ruff/mypy (for reproducible lint). Let pip pick compatible pytest, pytest- asyncio, pytest-cov, aioresponses versions. **Hassfest** - manifest.json keys weren't in HA's required order (domain, name, then alphabetical). Reorder: domain → name → after_dependencies → codeowners → config_flow → dependencies → documentation → iot_class → issue_tracker → requirements → version. - Translation `base_url` labels embedded the example URL inline (e.g. "Base URL (e.g. https://your-server.com/api)"), which hassfest flags. Strip to just "Base URL" / "Basis-URL" / etc. in all four locales — the example was redundant with the field name anyway. **HACS** - hacs.json had a `description` key, which is not a permitted key in the HACS manifest schema. Removed. - HACS also requires repo-level **GitHub description** and **topics** on the GitHub repo settings — those have to be set in the repo UI (Repo → About ⚙) since there's no API access from this PR. I'll flag this to the user. Add pyproject.toml with a basic ruff config (target py312, line 100, select E/F/W/I/UP/B) so behaviour is consistent in CI and local. https://claude.ai/code/session_01V68vQAmMQwUctanNhh1tN9
Local was on ruff 0.15.15 where UP038 (no-tuple-isinstance) was deprecated for being a perf regression. CI uses the pinned 0.4.4 where it's still active and fires on host_tools.py:109. Switch to `int | float` syntax — supported since Python 3.10 and we target 3.12+, so this is uniformly safe. https://claude.ai/code/session_01V68vQAmMQwUctanNhh1tN9
Locally I'd been on an unpinned pytest-homeassistant-custom-component
that picked the latest 0.13.x. CI installed the explicit 0.13.185 pin
from requirements_test.txt and hit a stricter lingering-thread check:
AssertionError: assert (False or False)
+ where False = isinstance(<Thread(Thread-1 (_run_safe_shutdown_loop)…
<class 'threading._DummyThread'>)
+ and False = 'Thread-1 (_run_safe_shutdown_loop)'.startswith(
'waitpid-')
(pytest_homeassistant_custom_component/plugins.py:405)
The thread is left behind by aiohttp's ClientSession shutdown.
0.13.300+ tolerates it; reproduced 56 passed in a fresh CI-replica
venv with the new floor.
Pin as `>=0.13.300` rather than ==latest so a transient pypi yank
or a brand-new release doesn't break us. The hard upper boundary is
implicitly Python 3.13 (which the workflow already uses).
https://claude.ai/code/session_01V68vQAmMQwUctanNhh1tN9
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Streamlines the integration around two state machines: a door-lock-driven guest lifecycle and a separate housekeeping state.
reserved → due_in → in_house → departed → vacant, derived each poll from PMS data + a locally-latchedentered_atflag set by lock eventsready/occupied/dirty/cleaning, persisted across restarts, advanced via dedicated buttonsdepartedwindow: held for 10 seconds before next-guest rotation so automations (auto-lock, away mode) fire reliablyhomeassistant.helpers.storage.StoreRemoved
next_guest_*sensors (next booking is fetched internally and rotated intocurrentonce the active guest goesvacant)Mark Checked Outbutton (automatic now)Added
House Statesensor +Guest Statusenum sensorMark Guest Departed,Mark Cleaning Started,Mark Readybuttonsstr_ha_guest_changed,str_ha_guest_status_changed,str_ha_house_state_changedTest plan
occupied → dirty, manualdirty → cleaning → ready)STRStatelock.*entity unlock eventhttps://claude.ai/code/session_01V68vQAmMQwUctanNhh1tN9
Generated by Claude Code