LocalCommsStatusfields were mislabeled (previously guessed from limited captures). Corrected to the verified field semantics: field 2health→status, field 3link_state→visible_devices_count, field 4version→expected_devices_count, field 5health_changed_ts→last_session_change_ts, field 6connection_state(int32) →reason(LocalCommsHealthReasonenum). Decoding still worked by field number, but the model exposed wrong names/semantics and discarded the reason diagnostics.QuiltSmartModule/Controllernow exposelocal_comms_visible_devices,local_comms_expected_devices,local_comms_reason, andlocal_comms_last_session_change(replacinglocal_comms_link_state,local_comms_connection_state, andlocal_comms_version).
IndoorUnitnow exposesmodel_sku,serial_number, andfirmware_version, resolved fromHomeDatastoreSystem.indoor_unit_hardware(previously discarded). This mirrorsOutdoorUnitandControllerand lets downstream consumers (e.g. the Home Assistant integration) populate IDU device serial/firmware.- CLI
setcommand gained a--fanoption (AUTO,QUIET,LOW,MEDIUM,HIGH,BLAST) that applies the chosen fan speed to every indoor unit in the target space. SystemSnapshot.indoor_units_for_space()returns all indoor units linked to a space (accepts aSpaceor space-ID string).LocalCommsHealthReasonenum (12 values).ZENOH_SESSION_DOWNidentifies the local mesh transport as Eclipse Zenoh, not NATS (port 7447 is Zenoh's default).IndoorUnitConditions.compressor_minimum_run_time(proto field 13).- Marked
SpaceSettings.safety_heating=9confirmed (was "unconfirmed").
- Docs deploy workflow upgraded to the current Node 24 GitHub Pages actions
(
configure-pages@v6,upload-pages-artifact@v4,deploy-pages@v5), removing the deprecated Node 20 runtime path. - Docs deploy now retries
deploy-pagesonce on transient GitHub Pages backend failures, failing the job only if both attempts fail.
Fixes all findings from a full architecture/code/performance/bug-hunt evaluation.
- Proto3 absence detection never fired on real protos. Truthiness and
getattr-with-default checks always pass on generated messages (unset
sub-messages return truthy default instances), so sparse stream diffs zeroed
room state, IDU controls, sensor readings, and controller temperatures on
merge. All model parsing now uses
HasField-based helpers, andSystemSnapshot.apply_*preserves identity/relationship fields, controller temperatures, ODU telemetry, and QSM LED state. New test suite (tests/test_models_real_proto_merge.py) reproduces the notifier stream's serialize/parse path with real generated protos. - Transport UNAUTHENTICATED retry was dead code. In
grpc.aio, awaiting the interceptor continuation returns the call object;AioRpcErroronly surfaces when the call is awaited — so token refresh/retry never executed and clients broke permanently ~1h after token expiry. The interceptor now awaits the call and retries once. Tests updated to model realgrpc.aiosemantics.
- Stream reconnect backoff/budget reset after a healthy connection (routine LB
stream recycling no longer escalates to permanent 60s delays or kills the
stream after N lifetime disconnects); non-gRPC failures reconnect and surface
via
on_errorinstead of dying silently; malformed events are skipped; jitter added; prompt reconnect after successful token refresh. authenticate()no longer returns a locally-unexpired token the server just rejected; rotated Cognito refresh tokens are persisted; expiry measured from token receipt.grpc_callpassesCancelledErrorthrough untranslated (asyncio.timeout/ cancellation semantics restored).- TUI: fatal stream death now shows a disconnect indicator and auto-recovers; first-time users get an OTP modal; boot failures show an error screen instead of a stuck spinner.
- Unified error translation: all hds/user RPCs route through
grpc_call;UNAVAILABLE/DEADLINE_EXCEEDED→QuiltConnectionErrorandNOT_FOUND→QuiltNotFoundErroreverywhere. - Single-flight guards on token refresh and cold snapshot fetch;
get_system_id(home=...)no longer poisons the default-system cache;close()stops tracked streams; duplicate authorization metadata eliminated. - Stream API:
on_connectedcallback, unsubscribe handles from allon_*registrations,apply_software_update_info, descriptor-derived wire-scan field numbers. - CLI/TUI: app-owned snapshot (stacked screens no longer go stale), cursor preservation, setpoint bounds, energy period validation, app-level °C/°F, LED brightness restoration, 0.0 readings rendered correctly, token store corruption recovery + fsync + flock, config dir 0700, dead code removal.
- Docs: README no longer demonstrates raw-stream usage; stream/token types re-exported at package top level; stale OutdoorUnit/Controller reference listings corrected; contradictory proto comments fixed.
Intentionally unchanged: Python >= 3.14 floor and boto3 dependency (HA 2026.3+ runs Python 3.14).
_make_cognito_client()now creates the boto3 client with EC2 instance metadata (IMDS) credential discovery disabled and explicit connect/read timeouts. On non-EC2 hosts (e.g., Home Assistant Yellow) the IMDS endpoint at 169.254.169.254 may accept TCP connections but never respond, causing the calling thread to block for the full OS-level TCP timeout — well beyond the 20-second setup window — and triggering a spuriousConfigEntryNotReady.
SpaceControls.display_setpoint_str()— removed dead unreachable code in the fallback branch;temperature_setpoint_cis typedfloatso theNone-guard lines were never executed (confirmed by coverage)SystemSnapshot.apply_outdoor_unit()— refactored to collect field patches into anupdatesdict and calldataclasses.replace()once, consistent with all otherapply_*methods; previously calledreplace()twice in sequence, creating an unnecessary intermediate objectQuiltClient.close()now setsself._token = None; previously left a stale token accessible viaget_current_token()after the channel was closed
invoke_refresh_callback(formerly_invoke_refresh_callback) extracted fromtransport.pyandservices/streaming.pyinto a single shared implementation intokens.py; the streaming copy lacked theWeakKeyDictionarysignature cache, causinginspect.signature()to be called on every token-refresh eventFanSpeed.to_wire()andLouverAngle.to_wire()now reference module-level constant dicts (_FAN_SPEED_WIRE_MAP,_LOUVER_ANGLE_WIRE_MAP) instead of re-allocating the mapping on every call_id_variants()moved frommodels/system.pyintomodels/_helpers.pyand reused bylookup_hardware(); eliminates duplicated ID-normalisation logicQuiltClient.invalidate_snapshot()log level changed fromWARNINGtoDEBUG
HVACMode.DRY = 8— dehumidification mode; gate:mobile_dry_mode_selection_enabled. Fan non-interactive (QSM forces ~600 RPM). No user-configurable temperature setpoint; built-in temperature floor is server-side.HVACState.DRY = 11,DRY_DEFERRED = 12,DRY_PREPARING = 13LocalCommsHealthStatusenum (UNSPECIFIED=0,HEALTHY=1,DEGRADED=2,OFFLINE=3,STARTING_UP=4) — gate:mobile_local_control_health_enabledQuiltSmartModule.local_comms_health— extracted from newLocalCommsStatusnested message (proto field 8, subfield 2)Controller.local_comms_health— extracted from newLocalCommsStatusnested message (proto field 9, subfield 2)LocalCommsStatusproto message withupdated_ts,health,link_state,version,health_changed_ts,connection_statesubfields (fields 1–6; wire-confirmed)
APP_VERSIONbumped to1.0.26LocalCommsStatusis a nested message (not a simple enum) on both QSM and Controller — the earlier inferred field type was incorrect; wire-confirmed via 2026-06-04 mitmproxy capture
LouverAngle.labelproperty and__str__with human-readable position names:ANGLE1→"Horizontal",ANGLE2→"Slightly Down",ANGLE3→"Down",ANGLE4→"Mostly Down",ANGLE5→"Straight Down"
RST_STREAM with error code 0(HTTP/2NO_ERROR, a normal server-side graceful reset) is now logged atDEBUGinstead ofWARNINGto reduce log noiseCANCELLED(server closed the stream normally, e.g. keepalive timeout or server rotation) is now logged atINFOinstead ofWARNINGUNAUTHENTICATEDreconnects handled by the automatic token refresh are now logged atINFOinstead ofWARNING
NotifierStreamhealth properties:is_connected,last_event_at,stream_stateNotifierStreamdebounce_sparameter to coalesce rapid update burstsMetricBucketStatusenum exposed onEnergyBucket.status(was untypedint)grpc_call()context manager inservicesfor consistent gRPC error translation and optional retry- Structured
logging.getLogger(__name__)across all modules (auth, client, transport, services, CLI) QuiltStreamErrorre-exported from the top-levelquilt_hppackage- Shared model helpers (
_helpers.py) for WiFi signal parsing and hardware lookup
ScheduleEvent.hvac_modeis now typed asHVACMode(wasint)- Token temp file created with
os.open(..., 0o600)so permissions are secure from creation (no transient world-readable window) - Signature cache in transport layer uses
weakref.WeakKeyDictionaryinstead ofdict[int, bool]— prevents unbounded growth and id-reuse bugs after GC login()clears the token cache so a re-login always fetches fresh credentials_GrpcCallContextavoids self-chaining when re-raising aQuiltErrorunchanged
EnergyBucket.has_missing_energy_valuenow treatsNone(absent proto field) as missing, not justNaN; preventsTypeErrorinSpaceEnergyMetrics.total_kwhMetricBucketStatus()conversion inget_energy_metricscatchesValueErrorfor unknown server values and falls back toUNSPECIFIEDinstead of raisingWeakKeyDictionary.get()for the refresh-callback signature cache is now guarded againstTypeErrorfor non-weakrefable callables- AUTO mode setpoint deadband clamp now runs before setpoint selection, ensuring the correct (clamped) value is sent to the device
- CLI settings
boolcoercion usesisinstance(v, bool)to avoidbool("false") == True - Zero-value proto3 fields (0 °C temperature, 0 dBm WiFi signal, 0% humidity) are now preserved instead of being dropped as falsy
NotifierStreamreconnect subscription is now protected by anasyncio.Lockto prevent concurrent subscribe/reconnect races- CLI enum lookups raise a clear error showing valid options on invalid input
auth.pynarrows broadexcept Exceptiontoexcept (QuiltAuthError, ClientError)to avoid swallowing unexpected errors
- Corrected mapping of outdoor units to indoor units in SystemSnapshot
- Fixed TUI interaction issues with button handling and bindings
- Restored CI/release quality-gate stability by applying required
ruff formatupdates in model files.
boto3.client()was called synchronously inside async functions, causing a blocking HTTP request to the EC2 instance metadata service (IMDS) at169.254.169.254during credential resolution. This manifested as anHTTPClientErrorin Home Assistant's async event loop. The client is now created vialoop.run_in_executor()like the subsequent API calls.
- Regenerated gRPC stubs with
grpcio-tools==1.78.0so the library works inside Home Assistant, which hard-pinsgrpcio==1.78.0in its package constraints. Previously the stubs were generated with 1.80.0 and raisedRuntimeErrorat import time on older grpcio versions.
- GitHub Actions release automation for SemVer tags (
vX.Y.Z) that enforces quality gates, creates a GitHub Release, and publishes distribution artifacts to PyPI via trusted publishing - Initial async client for Quilt cloud gRPC API
- Cognito OTP authentication with token caching
- HomeDatastoreService: spaces, indoor units, comfort settings, schedules
- SystemInformationService: system listing, energy metrics
- NotifierService: real-time streaming subscriptions
- CLI for interactive use (
quiltcommand)