Commit Graph
47 Commits
Author SHA1 Message Date
Marc Billow eecbc19dde fix(observe): clear stale notifications before re-attempting observe mode 2026-07-09 18:36:36 -05:00
Marc Billow 97260441f1 feat(observe): add subscribe-and-wait mode selection 2026-07-09 18:33:03 -05:00
Marc Billow eba73118e9 feat(observe): add ObserveManager with write-settle guard 2026-07-09 18:28:46 -05:00
Marc Billow 5d1a32aa9a Map remaining coverage-gap hrefs, fix discover() false-gap bug, improve device naming
Maps the fridge/dishwasher hrefs flagged by last session's coverage-gap
Repairs issue: defrost delay (switch) + block status (diagnostic binary
sensor), master ice-maker enable, self-check trigger/status, dishwasher
diagnosis trigger/status, and last-operation-source. Adds fallback
capabilities for /doors, /temperatures, and /icemaker/status that only
bind when the richer per-instance hrefs they duplicate are absent, so
simpler devices without those hrefs still get the data. Everything else
in the original gap list moves to capabilities.ignored (Bixby audio
feedback, inert DR/energy-planner resources, redundant metadata).

Fixes discover() reporting a known href as an unregistered gap whenever
its match_fn declined to bind for that device (e.g. a filter capability
on hardware without that filter) — it's now only a gap when no capability
is registered for the href at all.

Device naming now uses x.com.samsung.da.modelNum (not the OCF /oic/p,d
metadata, which produced inconsistent names like "[dishwasher] Samsung")
to build "Samsung <Type> (<model>)".
2026-07-08 15:24:34 -05:00
Marc Billow d2699f0f5b Add diagnostics platform, config-flow gap warning, issue template
Completes the device-capability-diagnostics spec:

- diagnostics.py: the standard HA diagnostics hook, returning device type,
  one_ui_version, unbound hrefs, and the redacted raw resource tree, plus
  integration and smartthings-local version numbers. This is what the
  Repairs issue (added in the previous commit) points users at, and what
  they attach to a device-support issue.
- config_flow.py: _probe_and_validate now also reports oneUiVersion and
  whether the device type is recognized. Recognized types are unaffected;
  an unrecognized type shows a new confirm_unknown_type step explaining
  that only common capabilities will be available before creating the
  entry, so expectations are set at setup time rather than only after the
  fact via Repairs.
- .github/ISSUE_TEMPLATE/device-support.yml: structured template for
  filing a capability gap, linked from both the Repairs issue and the
  config-flow confirmation step.
- README: short section pointing at this whole mechanism.
2026-07-08 00:10:30 -05:00
Marc Billow ca754cbb01 Re-add redacted device dump fixtures
Companion to the git-filter-repo pass that just stripped every prior
version of these two files (which carried a real Samsung account email,
Bixby access token/device-ID hash, and real WiFi/BLE MAC addresses) out of
history entirely. This commit reintroduces only the already-redacted
content, so the real values are no longer reachable from any commit.
2026-07-07 23:52:38 -05:00
Marc Billow 35b47d6b5a Add coverage-gap detection: ignored-hrefs registry, Repairs issue, redaction
Implements the first half of the device-capability-diagnostics spec:

- registry/capabilities/ignored.py: known-noise hrefs (Bixby/voice
  provisioning, WiFi/BLE info, OTA/region housekeeping, and a couple of
  redundant hrefs) declared as no-entity Capability objects, so
  discover()'s existing unknown-resource reporting treats them as covered
  instead of flagging every device as having gaps. Folded into each
  by_type registry and into the global fallback CAPABILITIES set.
- discovery.py: log() callback now passes the raw href instead of a
  formatted message, so callers can collect a clean unbound-hrefs list.
- coordinator.py: wires that callback into self._unbound_hrefs, tracks
  device_type_name/one_ui_version, and raises (or clears) a Repairs issue
  when a device's type is unrecognized or it has genuinely unmodeled
  hrefs left over.
- registry/redact.py: recursive, substring-keyed redaction for anything
  that looks like account/identity data, ahead of the diagnostics.py
  platform that will consume it.

Verified against the real dishwasher/refrigerator fixtures: known-noise
hrefs no longer show up as gaps, while genuine gaps (e.g. /bespoke/vs/0
on the fridge) still do.
2026-07-07 23:49:58 -05:00
Marc Billow b67e0e5cf5 Rename ocf/ to registry/, it's a capability registry now
The DTLS/CoAP transport code that made "ocf" an accurate name moved out to
the smartthings-local package. What's left here (capability.py, entities.py,
discovery.py, adapter.py, identity.py, capabilities/, by_type/, plus the
/device/0 batch parser) is entirely the device capability registry, so name
the package for what it does.

Flattened the redundant ocf/registry/ nesting into a single top-level
registry/ package and updated every import across the platform modules and
test suite accordingly. Verified: full test suite (80/80) passes, and the
Docker dev container reconnects to both live appliances and rediscovers
their entities cleanly after the rename.
2026-07-07 22:54:57 -05:00
Marc Billow db2148467b feat: dishwasher cycle/settings entities, stale-state fixes, LED/sound controls
Dishwasher:
- Revert remote_control to BinarySensorDesc ("Smart Control", read-only)
- Add cycle SelectDesc on /course/vs/0 via options-array RMW (AI Wash,
  Pre blast, Self clean, Normal, Express 60, Heavy, Pots and pans,
  Delicate, Plastic, Baby Care)
- Add Storm Wash+ and Auto release dry SwitchDescs on /course/vs/0
- Add Sanitize SwitchDesc and Smart Dry SelectDesc on /dishwasher/vs/0
- Add Sound volume NumberDesc (0–15 step 5) on /settings/sound/volume/vs/0
- Add Door LED night brightness, night start/end time to DOOR_LED capability
- Add SOUND_VOLUME capability to laundry module

Stale-state fixes (firmware leaves values set after cycle ends):
- finish_time: suppress when machine state is not active
- progress: return 'Idle' when machine state is not active
- water filter: gate WATER_FILTER capability on filterStatus != 'notused'

Translations:
- Add ICESTATUS_RUN → "Making ice" to strings.json and en.json

Tests:
- Update dishwasher and refrigerator golden fixtures to reflect current entity set
- Remove dead test_project_extrapolates_remaining_time (on_observation removed)
2026-07-01 00:36:24 -05:00
Marc Billow 022a192bca feat: CoAP inter-request pacing to prevent RT-OCF rate limit drops
Samsung appliances silently drop CoAP requests when hit faster than their
firmware ceiling (~8–14 req/s measured; dishwasher unknown). Multi-block
Block2 fetches on LAN hit ~100 req/s instantaneously, causing the observed
burst timeouts on block 4 and block 10 of /device/0.

Adds rate_limit_rps (default 5 req/s / 200 ms) to DtlsCoapSession with a
pace() method that enforces the inter-block delay via _stop.wait() so session
teardown interrupts the sleep cleanly. Coordinator calls pace() between hrefs
in _poll_hrefs_blocking. Also adds device IP to all GET log lines for easier
per-device filtering.
2026-06-30 22:40:14 -05:00
Marc Billow 57cf1cdd11 fix: address code review findings from post-onboarding audit
- Add button platform (button.py + PLATFORMS) so oven/washer Start/Pause/Stop
  controls actually appear in HA
- Load DTLS cert chain from memory via _load_pem_chain(); eliminate all temp
  file writes for key material in config_flow and coordinator
- Fix PEM boundary: ensure leaf and CA blocks are separated by a newline in
  the stored fullchain regardless of whether the user pasted a trailing newline
- Dryer mode select: add static options tuple and value_fn to decode Course_XX
  hex strings back to human names; was permanently empty before
- Remove callable branch from LocalThingsSelect (options_field covers dynamic
  case; callable contract was broken — called with no args vs Callable[[dict]])
- Guard native_min/max_value overrides with hasattr + super() fallback to
  prevent AttributeError when native_min=None and range_field returns nothing
- Add async_close() public method to coordinator; __init__ now calls it instead
  of reaching in to _close_session directly
- Update tests: rename CONF_CERT_PEM/KEY_PEM → CA/LEAF variants, add
  leaf_cert_pem/leaf_key_pem to mock_probe return, fix mock PEM constants
  (no trailing newlines, matching what .strip() stores)
2026-06-30 16:34:39 -05:00
Marc Billow cc2c1e5a1f refactor: move samsung_appliance/ into custom_components/localthings/ocf/
Makes the integration fully self-contained for HACS distribution.
Users installing via HACS get everything in one directory; no external
samsung_appliance package needed.

All component imports updated to relative (.ocf.*); all test imports
updated to custom_components.localthings.ocf.*. ocf_root_ca.pem moves
with the package and resolves correctly via Path(__file__).parent.
2026-06-30 16:34:20 -05:00
Marc Billow 0bb27a9204 test: scaffold pytest-homeassistant-custom-component test harness
Adds HA component tests covering the localthings integration layer
(previously only the samsung_appliance library was tested):

- pyproject.toml: asyncio_mode=auto for async HA test patterns
- tests/localthings/conftest.py: hass_config_dir override, mock fixtures
  for probe and coordinator session, fridge fixture loader
- test_config_flow.py: first-device form, second-device cred reuse,
  happy path, cannot_connect error, duplicate-device abort (5 tests)
- test_coordinator.py: discovery on first refresh, device_info populated,
  idle interval default, UpdateFailed on persistent poll error (4 tests)
- test_sensor.py: sensors registered, sensor state from coordinator (2 tests)

Total: 70 library + 11 component = 81 tests
2026-06-30 16:34:10 -05:00
Marc Billow f7b58f00ee feat: replace MQTT bridge with localthings HA custom component
Convert from MQTT bridge architecture to a proper Home Assistant
integration (custom_components/localthings/):

- Config flow: IP-only entry, probes ports [49154,49155], cert/key
  reused from first device on subsequent adds
- Coordinator: DataUpdateCoordinator wrapping blocking DTLS session
  via async_add_executor_job; discovery on first poll; dynamic 5s/30s
  interval based on active_when
- Entity platforms: sensor, binary_sensor, switch, number, select
- Library cleanup: delete bridge.py, main.py, config.py, logger.py,
  keepalive.py, poll_scheduler.py, sensors.py, state_cache.py
- adapter.py stripped to flatten(), is_active(), _key()
- _parse_device0_batch moved to samsung_appliance/batch.py
2026-06-30 16:34:06 -05:00
Marc Billow 785154eb83 refactor: replace exact-href door/temp/icemaker caps with generic pattern caps
Delete dead: DOOR_FRIDGE, DOOR_FREEZER, TEMP_FRIDGE/FREEZER_CURRENT/SETPOINT,
ICEMAKER_ONE, ICEMAKER_TWO, ICE2_MODES, temperature bounds constants.

Add: TEMP_CURRENT_GENERIC (href_prefix=/temperature/current/),
TEMP_SETPOINT_GENERIC (href_prefix=/temperature/desired/, write derives
CoAP path from href), ICEMAKER_GENERIC (href_prefix=/icemaker/,
match_fn gates on iceMaker.state field).

Key derivation strips 'vs' segments alongside digits so icemaker hrefs
like /icemaker/one/vs/0 produce icemaker_one_state not icemaker_one_vs_state.

write_fn signature extended: adapter now passes href as 3rd positional arg;
all existing write_fns updated to accept href=None.

Fridge golden: old ice1_*/ice2_*/fridge_*/freezer_* keys replaced by
icemaker_one_*/icemaker_two_*/temperature_current_*/temperature_desired_*.
2026-06-30 16:33:36 -05:00
Marc Billow 9e5a169c82 refactor: drop resources fallback; fixtures are device0-only 2026-06-30 16:33:36 -05:00
Marc Billow fd2aa9e64d fix: replace IP-named dumps with scrubbed named fixtures
Rename tests/fixtures/dumps/{IP}.json to tests/fixtures/{name}_device.json.
Scrub PII: serialNum, otnDUID, modelNum, diagEndPoint replaced with
TEST-* placeholders. conftest: _load_device(name) as primary API;
_load_resources(ip) kept as legacy shim for existing test call sites.
2026-06-30 16:33:36 -05:00
Marc Billow 27d6d09fd9 refactor: auto-derive pattern cap keys from href; move dumps to tests/fixtures
Remove key_fn from Capability — pattern cap keys are now always derived
automatically: strip digits from href segments, join with '_', append
'_' + desc.key. /door/cooler/0 + 'open' -> door_cooler_open.

DOOR_GENERIC entity key: 'open' (not 'door_open'). Remove _door_key helper.
Fridge registry: DOOR_FRIDGE/DOOR_FREEZER replaced by DOOR_GENERIC pattern cap.
Golden baseline: door_fridge_open -> door_cooler_open.

Move device dumps to tests/fixtures/dumps/ (tracked) so tests run in CI
without local-tools/ dependencies.
2026-06-30 16:33:36 -05:00
Marc Billow 1c4cb2e272 fix: address final review findings
- for_device(): suffix fallback for descriptive names
  ("french_door_refrigerator" → refrigerator registry)
- Capability.href_prefix: replaces rt_filter for pattern caps;
  DOOR_GENERIC now uses href_prefix='/door/' (batch reps have no rt)
- discover(): apply href_prefix gate before rt_filter in pattern loop
- Remove dead bound_hrefs variable from discover()
- Remove vestigial default_observe_port from RuntimeDescriptor
2026-06-30 16:33:36 -05:00
Marc Billow 6f681622c2 fix: remove unused request param, use context manager in e2e v2 test 2026-06-30 16:33:36 -05:00
Marc Billow ee7ae10481 test: add v2 e2e smoke test (batch + per-type registry full pipeline) 2026-06-30 16:33:36 -05:00
Marc Billow d4ff29d797 feat: switch conftest + golden tests to batch-first resource loading
conftest: _resources_from_dump() prefers /device/0 batch reps; falls
back to flat resources dict. Fixes _parse_device0_batch to include
stub reps (rep={href:...}) as {} so capabilities still bind on
resources with no current data (e.g. /alarms/vs/0 when no alarm active).

test_golden_regression: _new_state_and_uids now uses by_type.for_device
to select per-device-type capabilities + pattern caps.
2026-06-30 16:33:36 -05:00
Marc Billow 2b22673210 refactor: rewire bridge to batch-poll /device/0 + per-type registry
Replace PollScheduler + OBSERVE with:
- _parse_device0_batch() extracts {href: rep} from /device/0 list response
- _discover_and_publish() uses by_type.for_device(oneUiVersion) for
  device-specific capability registry; falls back to global CAPABILITIES
- _poll_loop thread: batch GET /device/0 at active_interval_s or
  idle_interval_s; applies each rep to the state cache
- All OBSERVE machinery removed (subscribe, notifications, fetchback)
2026-06-30 16:33:36 -05:00
Marc Billow 15f462563d refactor: simplify RuntimeDescriptor — drop observe/poll_tiers, add intervals
Remove observe_paths and poll_tiers from RuntimeDescriptor; add
active_interval_s (default 5s) and idle_interval_s (default 30s).
Delete _make_observe_paths() and _make_poll_tiers() helpers.
Bridge rewire in Task 6 will consume the new fields.
2026-06-30 16:32:42 -05:00
Marc Billow 1b36ec4a59 feat: fill per-device-type registries; add DOOR_GENERIC, OVEN_CONNECTED, dryer caps
- DOOR_GENERIC pattern cap (rt_filter=oic.r.door) in fridge.py with key_fn
- OVEN_CONNECTED (cloud connectivity) added to oven caps
- dryer.py: DRYER_SETTINGS, DRYER_COURSE, JOB_BEGINNING_STATUS, DRYER_DIAGNOSIS
- Dishwasher, refrigerator, oven, dryer DeviceRegistry objects fully populated
- Refrigerator registry includes DOOR_GENERIC as pattern cap
- Oven registry includes OVEN_SETPOINT + OVEN_MODE (safe: no fridge caps in oven registry)
2026-06-30 16:32:42 -05:00
Marc Billow e801af9ab7 feat: add by_type/ registry package with for_device() and DeviceRegistry 2026-06-30 16:32:42 -05:00
Marc Billow b10cfad194 feat: update discover() for pattern caps, match_fn, rt_filter gating
Registry changes from dict[str,Capability] to dict[str,list[Capability]].
discover() gains pattern_caps parameter (fallback for unmatched hrefs)
and gates each candidate on rt_filter and match_fn before binding.
Duplicate hrefs in registry allowed only if all caps set a discriminator.
2026-06-30 16:32:42 -05:00
Marc Billow 211fcd1324 fix: remove colliding oven caps from global ALL; update golden baselines
OVEN_SETPOINT (/temperatures/vs/0) and OVEN_MODE (/mode/vs/0) collide
with fridge hrefs. Remove from ALL; only OVEN_CAVITY (/oven/vs/0) is
oven-unique. Per-device-type registries (v2 plan) will restore them in
the oven-only registry.

Also updates golden baselines to include legitimate new entities added
since original capture (cycle_active, firmware_update, power_switch for
dishwasher; cabinet_light_switch, ice1 for refrigerator).
2026-06-30 16:32:42 -05:00
Marc Billow 81d97e2fb5 Task 1: Simplify Capability; extend BoundEntity
- Make href Optional[str] = None for pattern capabilities
- Remove observe field (v2 architecture observes all hrefs)
- Add rt_filter, match_fn, key_fn fields to Capability
- Add entities default value tuple[()] to Capability
- Add key_override field to BoundEntity
- Update _key() to use key_override when present
- Update _make_observe_paths() to observe all unique hrefs (OBSERVE as accelerator)
- Guard _build() to skip pattern capabilities (href is None)
- Add test_bound_entity_key_override_takes_precedence test
- Update test_capability_defaults to remove observe assertion

All 61 tests passing (60 existing + 1 new).
2026-06-30 16:32:42 -05:00
Marc Billow 1a76bf6d47 Fix C1 (switch discovery), C2 (power write_fn), I2 (golden oracle)
C1: SwitchDesc MQTT discovery now emits payload_on/off='On'/'Off',
state_on/off='On'/'Off', and a Jinja2 boolean-to-string value_template
so HA can correctly interpret and command switch entities.

C2: POWER write_fn corrected — path is ['power','vs','0'] segment list
(not a single slash-prefixed string), and payload comparison is
p == 'On' (not truthiness) so 'Off' can actually be sent.

I2: Golden regression oracle strengthened to exact equality for both
state_keys and discovery_unique_ids; prior subset-only check allowed
over-production to go undetected. Two golden tests now fail revealing
stale fixtures (dishwasher: +3 keys; refrigerator: +9 keys) — fixtures
intentionally NOT updated, human review required.
2026-06-30 16:32:42 -05:00
Marc Billow 5906aabd7c test: end-to-end discovery pipeline over real dumps 2026-06-30 16:32:42 -05:00
Marc Billow 35222e6be5 refactor: make StateCache descriptor-free 2026-06-30 16:32:37 -05:00
Marc Billow f2c1d8391e feat: read device identity from OCF /oic/p and /oic/d 2026-06-30 16:32:37 -05:00
Marc Billow 246379acbc feat: assemble CAPABILITIES registry with duplicate-href guard 2026-06-30 16:32:37 -05:00
Marc Billow 23d5090faa feat: add oven capabilities and cycle-active gating 2026-06-30 16:32:37 -05:00
Marc Billow 9cbeb6a2b2 feat: add refrigerator capabilities with multi-instance support 2026-06-30 16:32:37 -05:00
Marc Billow 23b4cdc315 feat: add laundry-family capabilities; dishwasher golden green 2026-06-30 16:32:37 -05:00
Marc Billow d720b29a67 test: capture golden baselines from existing descriptors
Generates regression oracle from DISHWASHER and REFRIGERATOR descriptors:
- tests/fixtures/golden/dishwasher.json: 21 state keys, 23 discovery entities
- tests/fixtures/golden/refrigerator.json: 26 state keys, 24 discovery entities
- tests/test_golden_regression.py: parametrized test that will turn green as
  Tasks 9-12 port capabilities into the new registry
2026-06-30 16:32:37 -05:00
Marc Billow 299719f727 feat: add adapter building RuntimeDescriptor from bound entities 2026-06-30 16:32:37 -05:00
Marc Billow 6778c60a09 feat: add operational-state capability with extrapolation hooks 2026-06-30 16:32:37 -05:00
Marc Billow 0499c702d7 refactor: key capabilities on href instead of rt
OCF rt is a schema category shared by many unrelated resources.
The stable unique resource address is the href. Update Capability,
discovery, common capabilities, tests, and plan accordingly.
2026-06-30 16:32:37 -05:00
Marc Billow bdae3cc82d feat: add shared common capabilities 2026-06-30 16:32:37 -05:00
Marc Billow 73fff9c2cf fix: log each unknown rt individually in discover() 2026-06-30 16:32:37 -05:00
Marc Billow 8d63610f82 feat: add runtime capability discovery
Implement discovery module for converting OCF resource metadata into bound
entities, with support for multi-instance device resources and logging of
unknown capabilities.
2026-06-30 16:32:37 -05:00
Marc Billow 9b2eef26f7 feat: add Capability dataclass 2026-06-30 16:32:37 -05:00
Marc Billow 9acc6bc8ce feat: add HA-shaped entity description dataclasses 2026-06-30 16:32:37 -05:00
Marc Billow c6f08ea340 test: add pytest infra and device-dump fixtures 2026-06-30 16:32:37 -05:00