diff --git a/.claude/skills/adding-device-support/SKILL.md b/.claude/skills/adding-device-support/SKILL.md index 6002702..a627311 100644 --- a/.claude/skills/adding-device-support/SKILL.md +++ b/.claude/skills/adding-device-support/SKILL.md @@ -368,6 +368,19 @@ The new fixture is picked up automatically by the corpus-wide checks (the board token fails the build rather than silently mistyping someone's appliance. +**Don't put a reporter's name or GitHub username in code.** Fixture data +gets serials/MACs/other device PII scrubbed per point 1 above — the same +rule applies to the *prose* you write while fixing the issue: comments, +docstrings, `seeds_note`, and test/function names should say "the +reporter," "issue #NNN's reporter," or (when a module already distinguishes +multiple reporters, like `subdevices.py`'s Pattern A/Pattern B) "the +Pattern A reporter," never a real name or handle. That prose ships in the +package and lives in git history indefinitely — unlike an issue thread or a +release-notes thank-you (both fine places to credit someone by name), it's +not somewhere a person would expect to stay named forever. If you're fixing +an issue and about to write `'s board`/`'s dump` in a +comment, stop and swap in a generic reference instead. + ## 11. Triage: "one of my subdevices is missing" For an appliance that exposes several logical indoor subdevices over one IP — diff --git a/custom_components/localthings/coordinator.py b/custom_components/localthings/coordinator.py index 376a8a9..ab5a174 100644 --- a/custom_components/localthings/coordinator.py +++ b/custom_components/localthings/coordinator.py @@ -174,9 +174,10 @@ class LocalThingsCoordinator(DataUpdateCoordinator[dict[str, Any]]): # it's the *other* subdevices sharing this DTLS session, if any. self.subdevices: list[Subdevice] = [] # Candidates _run_discovery's gate rejected (an unused SmartThings - # slot that still answers its seed, e.g. HJcom's /device/2) -- - # surfaced in diagnostics alongside the materialized ones so a - # report shows what was found and why it didn't become an entity. + # slot that still answers its seed, e.g. the issue #177 reporter's + # /device/2) -- surfaced in diagnostics alongside the materialized + # ones so a report shows what was found and why it didn't become an + # entity. self._skipped_subdevices: list = [] # Those rejected candidates' raw reps, kept aside for diagnostics # only (see _live_subdevice_resources). They are deliberately not in the @@ -408,9 +409,10 @@ class LocalThingsCoordinator(DataUpdateCoordinator[dict[str, Any]]): """GET one subdevice's seed Collection and return its batch, normalized to real hrefs. A sibling failing to answer is a debug log, never a failed poll -- the master must not go unavailable - because a sibling timed out or dropped off (e.g. HJcom's - /device/2, a SmartThings-unused component that may not always - respond). Blocking -- called from _poll_once, already in executor.""" + because a sibling timed out or dropped off (e.g. the issue #177 + reporter's /device/2, a SmartThings-unused component that may not + always respond). Blocking -- called from _poll_once, already in + executor.""" sess = self._session if sess is None: return {} @@ -543,8 +545,9 @@ class LocalThingsCoordinator(DataUpdateCoordinator[dict[str, Any]]): _run_discovery sees every candidate's state without a second poll round trip. `_run_discovery` is what narrows self.subdevices down to the ones that are actually live (see discover_partitioned) -- this - method doesn't know how to tell an unused SmartThings slot (HJcom's - /device/2) from a real sibling, only that something answered. + method doesn't know how to tell an unused SmartThings slot (the + issue #177 reporter's /device/2) from a real sibling, only that + something answered. """ if self._session is None: self._connect_session() @@ -622,8 +625,8 @@ class LocalThingsCoordinator(DataUpdateCoordinator[dict[str, Any]]): # its own /information/vs/0 when it reports one and falling back to # the master's registry otherwise. See subdevices.discover_partitioned # -- it also gates each candidate down to whether it actually - # produced live primary state (HJcom's /device/2, an unused - # SmartThings slot, answers its seed but never does), so + # produced live primary state (the issue #177 reporter's /device/2, + # an unused SmartThings slot, answers its seed but never does), so # self.subdevices below is narrowed to the ones that passed, not # every candidate _enumerate_subdevices_blocking found. For a device # with no candidates (self.subdevices == []) this is exactly the @@ -641,11 +644,12 @@ class LocalThingsCoordinator(DataUpdateCoordinator[dict[str, Any]]): skip.subdevice.key, skip.subdevice.kind, list(skip.hrefs), ) # Corroborating signal, not a gate (DESIGN-177.md section 4): - # /multidevice/vs/0's numofsubdevice is a plain count HJcom's board - # reports independently of the liveness gate above. Log, don't - # raise, on a disagreement -- only this one board family is known to - # expose the resource at all, so a mismatch is a "look into this" - # signal for triage, not proof either side is wrong. + # /multidevice/vs/0's numofsubdevice is a plain count the issue + # #177 reporter's board reports independently of the liveness gate + # above. Log, don't raise, on a disagreement -- only this one board + # family is known to expose the resource at all, so a mismatch is a + # "look into this" signal for triage, not proof either side is + # wrong. numofsubdevice = self._multidevice.get( 'x.com.samsung.da.numofsubdevice') if numofsubdevice is not None: diff --git a/custom_components/localthings/diagnostics.py b/custom_components/localthings/diagnostics.py index 060a326..8190856 100644 --- a/custom_components/localthings/diagnostics.py +++ b/custom_components/localthings/diagnostics.py @@ -112,11 +112,11 @@ async def async_get_config_entry_diagnostics( "subdevices": [_subdevice_diag(su) for su in coordinator.subdevices], # Candidates that answered their seed but that discover_partitioned's # entity-level liveness gate rejected -- an unused SmartThings slot - # (HJcom's /device/2) that still answers a same-shaped batch, not a - # real second subdevice. Reported alongside subdevices above so a report - # shows what was found *and* why it didn't become an entity, not - # just silence where a third climate card might otherwise be - # expected. + # (the issue #177 reporter's /device/2) that still answers a + # same-shaped batch, not a real second subdevice. Reported alongside + # subdevices above so a report shows what was found *and* why it + # didn't become an entity, not just silence where a third climate + # card might otherwise be expected. "subdevices_skipped": [ { "kind": skip.subdevice.kind, diff --git a/custom_components/localthings/registry/capabilities/airconditioner.py b/custom_components/localthings/registry/capabilities/airconditioner.py index b817e3f..b753039 100644 --- a/custom_components/localthings/registry/capabilities/airconditioner.py +++ b/custom_components/localthings/registry/capabilities/airconditioner.py @@ -958,7 +958,7 @@ _AC_IGNORED = [ # Undocumented single int (runningMode: 0 on every dump seen), no # supported-values list to interpret it against -- 'don't guess'. '/runn/vs/0', - # 2-in-1/multi-indoor-subdevice systems (issue #177, HJcom's + # 2-in-1/multi-indoor-subdevice systems (issue #177, the reporter's # ARTIK051_DONGLE_FAC_18K): x.com.samsung.da.numofsubdevice, a plain # corroborating count of indoor subdevices on this connection. Confirmed # read-only (a write attempt returned CoAP 4.00). Absent from diff --git a/custom_components/localthings/registry/identity.py b/custom_components/localthings/registry/identity.py index 25907f6..9168456 100644 --- a/custom_components/localthings/registry/identity.py +++ b/custom_components/localthings/registry/identity.py @@ -73,7 +73,7 @@ def read_identity(sess, serial: Optional[str]) -> DeviceIdentity: # physical device -- one IP, one /oic/p -- exposing more than one logical # subdevice, each as its own Collection resource, same rt shape as our own # /device/0). This is what registry.subdevices.enumerate_subdevices reads - # to find a board's `/device/` siblings (Pattern A -- HJcom's + # to find a board's `/device/` siblings (Pattern A -- the reporter's # ARTIK051_DONGLE_FAC_18K) -- that probing, plus the /device/1 and # /device/2 speculative fallback it used to run right here on every # _connect_session (including every reconnect), moved to that module so diff --git a/custom_components/localthings/registry/subdevices.py b/custom_components/localthings/registry/subdevices.py index 242d401..a6f48b2 100644 --- a/custom_components/localthings/registry/subdevices.py +++ b/custom_components/localthings/registry/subdevices.py @@ -4,15 +4,16 @@ more than one logical indoor subdevice -- issue #177. Two reporters, two different board families, two genuinely different mechanisms for exposing a second indoor subdevice over one IP / one DTLS session (see DESIGN-177.md section 1 for the full evidence trail; the two -diagnostics dumps this was built against are HJcom's and issue #177's other -reporter's -- they each filed one of the two reports this module unifies): +diagnostics dumps this was built against come from the Pattern A and +Pattern B reporters, respectively -- they each filed one of the two +reports this module unifies): -Pattern A -- indexed siblings (`ARTIK051_DONGLE_FAC_18K`, HJcom's board). -`/oic/res` lists three complete parallel resource sets whose trailing path -segment is the index (`/mode/vs/0`, `/mode/vs/1`, `/mode/vs/2`, ... on both -OCF-standard and vendor hrefs), and `/device/0`'s batch carries only the -index-0 hrefs -- the sibling subdevices are reachable only via their own -`/device/` collection. +Pattern A -- indexed siblings (`ARTIK051_DONGLE_FAC_18K`, that reporter's +board). `/oic/res` lists three complete parallel resource sets whose +trailing path segment is the index (`/mode/vs/0`, `/mode/vs/1`, +`/mode/vs/2`, ... on both OCF-standard and vendor hrefs), and `/device/0`'s +batch carries only the index-0 hrefs -- the sibling subdevices are +reachable only via their own `/device/` collection. Pattern B -- UUID-prefixed tree (`TP2X_FAC_BORA_21K`, that reporter's board). `/oic/res` hides the whole appliance tree; `/device/0`'s batch instead @@ -38,27 +39,29 @@ surface. See `Subdevice.flat_hrefs`. Both are "the same thing wearing different clothes": a logical subdevice is a seed collection path to poll, plus an href transform between the canonical href the registry knows (`/mode/vs/0`) and the actual on-the-wire href. The -detection signals don't overlap on either captured board (HJcom's has no -`/subdevices/vs/0` at all; the other reporter's has no `/device/1`), so no -disambiguation logic is needed -- `enumerate_subdevices` checks both and -materializes any candidate whose seed answers with a non-empty batch. +detection signals don't overlap on either captured board (the Pattern A +reporter's has no `/subdevices/vs/0` at all; the Pattern B reporter's has +no `/device/1`), so no disambiguation logic is needed -- +`enumerate_subdevices` checks both and materializes any candidate whose +seed answers with a non-empty batch. A non-empty seed batch is necessary but not sufficient for the *candidate* -to actually be a live second subdevice, though: HJcom's own board also has a -`/device/2` -- a third, unused SmartThings slot -- that answers with the -exact same 14-href shape as the real `/device/1` sibling, populated with -three constant/echoed/shape-only reps (a region code identical to every -other subdevice's, an /information rep echoing the *same* model string as -subdevice 1, and a /temperatures items[] entry with an id/description but no -current/desired/minimum/maximum reading) and nothing resembling live -climate state. Gating on *resource* shape/hrefs turned out to be the wrong -layer -- it would need per-family domain knowledge (which hrefs mean "in -use" for a washer's second drum, a fridge's second compartment, ...) baked -into a registry field before any of those families could use this module -at all. `discover_partitioned` instead gates at the *entity* layer, after -discovery+flattening: a candidate is only kept if it produced at least one -*primary* (no `entity_category`) bound entity whose flattened value isn't -`None` -- e.g. HJcom's /device/2 does flatten to an `alarm_code` value, but +to actually be a live second subdevice, though: the Pattern A reporter's +own board also has a `/device/2` -- a third, unused SmartThings slot -- +that answers with the exact same 14-href shape as the real `/device/1` +sibling, populated with three constant/echoed/shape-only reps (a region +code identical to every other subdevice's, an /information rep echoing the +*same* model string as subdevice 1, and a /temperatures items[] entry with +an id/description but no current/desired/minimum/maximum reading) and +nothing resembling live climate state. Gating on *resource* shape/hrefs +turned out to be the wrong layer -- it would need per-family domain +knowledge (which hrefs mean "in use" for a washer's second drum, a +fridge's second compartment, ...) baked into a registry field before any +of those families could use this module at all. `discover_partitioned` +instead gates at the *entity* layer, after discovery+flattening: a +candidate is only kept if it produced at least one *primary* (no +`entity_category`) bound entity whose flattened value isn't `None` -- e.g. +the Pattern A reporter's /device/2 does flatten to an `alarm_code` value, but that entity is diagnostic-category and derived from an empty /alarms/vs/2, so it doesn't count. This reuses the same primary/config/diagnostic taxonomy every registry already declares (see the adding-device-support @@ -216,11 +219,11 @@ def normalize_seed_batch(subdevice: Subdevice, batch: dict[str, dict]) -> dict[s normalized so every href actually carries this subdevice's prefix/index. Indexed subdevices need no change -- the device echoes the real `/x/` - href in its own `/device/` batch (confirmed against HJcom's dump). - A prefixed subdevice's batch entries may or may not already carry the - `/` prefix (unconfirmed which -- the Pattern B reporter's board was - never probed live before the subdevice id was known), so it's added - when missing. + href in its own `/device/` batch (confirmed against the Pattern A + reporter's dump). A prefixed subdevice's batch entries may or may not + already carry the `/` prefix (unconfirmed which -- the Pattern B + reporter's board was never probed live before the subdevice id was + known), so it's added when missing. """ if subdevice.kind != 'prefixed': return batch @@ -283,9 +286,9 @@ def _get_batch(sess, path_segs: tuple[str, ...]) -> dict[str, dict]: def _get_property(sess, path_segs: tuple[str, ...]) -> dict: """GET a plain OCF Property-map resource (a bare dict, not a Collection batch). Used for `/multidevice/vs/0` (issue #177 follow-up): listed in - `/oic/res` on HJcom's board but absent from `/device/0`'s batch, so it - needs its own RETRIEVE, and it answers a single Property map, not a - [devcol-rep, ...] list.""" + `/oic/res` on the Pattern A reporter's board but absent from + `/device/0`'s batch, so it needs its own RETRIEVE, and it answers a + single Property map, not a [devcol-rep, ...] list.""" body = _get_raw(sess, path_segs) return body if isinstance(body, dict) else {} @@ -314,9 +317,10 @@ def enumerate_subdevices( Every candidate whose seed answers with a non-empty batch is returned here -- this function has no way to tell a real sibling from an unused - SmartThings slot that merely answers the same shape (HJcom's - `/device/2`); that requires discovering+flattening the candidate's own - entities first, which is `discover_partitioned`'s job, not this one's. + SmartThings slot that merely answers the same shape (the Pattern A + reporter's `/device/2`); that requires discovering+flattening the + candidate's own entities first, which is `discover_partitioned`'s job, + not this one's. See this module's docstring. """ subdevices: list[Subdevice] = [] @@ -410,9 +414,9 @@ def enumerate_subdevices( fetched.update(batch) # already real /x/ hrefs, no normalization needed subdevices.append(subdevice) - # /multidevice/vs/0 (issue #177 follow-up): HJcom's board lists it in - # /oic/res but it never appears in /device/0's batch, so it needs its - # own RETRIEVE. It's a plain corroborating count + # /multidevice/vs/0 (issue #177 follow-up): the Pattern A reporter's + # board lists it in /oic/res but it never appears in /device/0's batch, + # so it needs its own RETRIEVE. It's a plain corroborating count # (x.com.samsung.da.numofsubdevice), confirmed read-only (a write # attempt returned CoAP 4.00) -- captured for diagnostics only, folded # into the merged resources dict like any other href (see @@ -436,9 +440,9 @@ def enumerate_subdevices( class SkippedSubdevice: """A candidate `enumerate_subdevices` found whose seed answered, but that `discover_partitioned`'s entity-level liveness gate rejected -- an - unused SmartThings slot (HJcom's `/device/2`), not a real second subdevice. - Kept around (rather than silently dropped) so a caller can log/report - what was skipped and why.""" + unused SmartThings slot (the Pattern A reporter's `/device/2`), not a + real second subdevice. Kept around (rather than silently dropped) so a + caller can log/report what was skipped and why.""" subdevice: Subdevice hrefs: tuple[str, ...] @@ -450,7 +454,7 @@ def _has_live_primary_entity(bound, state: dict) -> bool: tier (see the adding-device-support skill's entity-taxonomy section). This is the materialization gate itself (see this module's docstring): - HJcom's `/device/2` does flatten to one non-`None` value + the Pattern A reporter's `/device/2` does flatten to one non-`None` value (`alarm_code`), but that entity is `diagnostic`-category and derived from an empty `/alarms/vs/2` -- a config/diagnostic entity reading "something" proves nothing about whether a physical subdevice is actually diff --git a/tests/fixtures/airconditioner_artik051_dongle_fac_18k_device.json b/tests/fixtures/airconditioner_artik051_dongle_fac_18k_device.json index c83e4fd..59f0238 100644 --- a/tests/fixtures/airconditioner_artik051_dongle_fac_18k_device.json +++ b/tests/fixtures/airconditioner_artik051_dongle_fac_18k_device.json @@ -2029,5 +2029,5 @@ "x.com.samsung.da.numofsubdevice": "2" } }, - "seeds_note": "ALL REAL CAPTURED DATA -- issue #177, HJcom's ARTIK051_DONGLE_FAC_18K (Samsung 2-in-1: floor-standing master + wall-mounted second indoor subdevice). device0, oic_res and both /device/ seeds come verbatim from their v0.16.0 diagnostics download, which is the first release that probes /device/1 and /device/2. Nothing here is reconstructed. The reporter masked a handful of values in their own dump before posting (serialNum, and the airflow `direction` / vendor `humidity` readings); those are normalized onto this repo's usual **REDACTED** marker. /device/1 is the real second subdevice -- populated power/mode/temperature and its own /information/vs/1 reporting ARTIK051_DONGLE_FAC_RAC_18K (RAC = the wall-mounted subdevice) against the master's ARTIK051_DONGLE_FAC_18K. /device/2 answers with the same 14-href shape but every state rep is empty {} -- it is the unused third slot SmartThings shows disabled, and it is why subdevice materialization gates on populated climate state rather than on a seed merely answering. `probes` is not part of any batch response: /multidevice/vs/0 is listed in oic_res but absent from /device/0's batch, and the reporter read it by hand through the debug panel (a write attempt returned CoAP 4.00, i.e. read-only). Its numofsubdevice=2 independently corroborates 2 real subdevices, not 3." + "seeds_note": "ALL REAL CAPTURED DATA -- issue #177, the reporter's ARTIK051_DONGLE_FAC_18K (Samsung 2-in-1: floor-standing master + wall-mounted second indoor subdevice). device0, oic_res and both /device/ seeds come verbatim from their v0.16.0 diagnostics download, which is the first release that probes /device/1 and /device/2. Nothing here is reconstructed. The reporter masked a handful of values in their own dump before posting (serialNum, and the airflow `direction` / vendor `humidity` readings); those are normalized onto this repo's usual **REDACTED** marker. /device/1 is the real second subdevice -- populated power/mode/temperature and its own /information/vs/1 reporting ARTIK051_DONGLE_FAC_RAC_18K (RAC = the wall-mounted subdevice) against the master's ARTIK051_DONGLE_FAC_18K. /device/2 answers with the same 14-href shape but every state rep is empty {} -- it is the unused third slot SmartThings shows disabled, and it is why subdevice materialization gates on populated climate state rather than on a seed merely answering. `probes` is not part of any batch response: /multidevice/vs/0 is listed in oic_res but absent from /device/0's batch, and the reporter read it by hand through the debug panel (a write attempt returned CoAP 4.00, i.e. read-only). Its numofsubdevice=2 independently corroborates 2 real subdevices, not 3." } diff --git a/tests/test_climate_subdevice.py b/tests/test_climate_subdevice.py index e5c45a1..9fedc1e 100644 --- a/tests/test_climate_subdevice.py +++ b/tests/test_climate_subdevice.py @@ -3,10 +3,11 @@ two -- and that the legacy-board test (is_legacy_board/_legacy_airflow) is evaluated per subdevice rather than once globally. -Uses HJcom's ARTIK051_DONGLE_FAC_18K fixture deliberately: it's a legacy -`/airflow/vs/` board (no `/wind/*` at all) on *both* the master and its -materialized sibling, which is exactly the shape climate.py's own comments -warn is easy to get wrong if the canonical view leaks between subdevices. +Uses the issue #177 reporter's ARTIK051_DONGLE_FAC_18K fixture deliberately: +it's a legacy `/airflow/vs/` board (no `/wind/*` at all) on *both* the +master and its materialized sibling, which is exactly the shape climate.py's +own comments warn is easy to get wrong if the canonical view leaks between +subdevices. """ from __future__ import annotations @@ -71,15 +72,16 @@ async def test_subdevice_climate_reads_its_own_power_state(climates): async def test_legacy_board_test_is_evaluated_per_subdevice(climates): - """HJcom's board has no /wind/* resources at all on *either* subdevice -- - is_legacy_board(self._resources) must independently evaluate True for - the master's own canonical view and for the subdevice's own canonical - view. If is_legacy_board were fed the raw, unpartitioned snapshot (or - if canonical_view leaked one subdevice's hrefs into the other's), this - wouldn't distinguish "this subdevice is legacy" from "some subdevice on - this connection is legacy" -- and a future board with one legacy + one - modern subdevice sharing a connection would silently read the wrong - fan/swing channel on one side.""" + """The reporter's board has no /wind/* resources at all on *either* + subdevice -- is_legacy_board(self._resources) must independently + evaluate True for the master's own canonical view and for the + subdevice's own canonical view. If is_legacy_board were fed the raw, + unpartitioned snapshot (or if canonical_view leaked one subdevice's + hrefs into the other's), this wouldn't distinguish "this subdevice is + legacy" from "some subdevice on this connection is legacy" -- and a + future board with one legacy + one modern subdevice sharing a + connection would silently read the wrong fan/swing channel on one + side.""" main, sub1 = climates[None], climates['1'] assert main._legacy_airflow() != {} assert sub1._legacy_airflow() != {} diff --git a/tests/test_golden_regression.py b/tests/test_golden_regression.py index b93d3d9..eeff63a 100644 --- a/tests/test_golden_regression.py +++ b/tests/test_golden_regression.py @@ -903,7 +903,7 @@ def _new_subdevice_aware_state_keys(name): def test_registry_reproduces_golden_state_keys_for_airconditioner_artik051_dongle_fac_18k(): - """HJcom's ARTIK051_DONGLE_FAC_18K (issue #177, Pattern A -- indexed + """The reporter's ARTIK051_DONGLE_FAC_18K (issue #177, Pattern A -- indexed siblings): a real v0.16.0 dump with a genuine second indoor subdevice at `/device/1` (subdevice1_-prefixed keys below) and an unused SmartThings slot at `/device/2` that answers its seed but never produces a diff --git a/tests/test_subdevice_discovery.py b/tests/test_subdevice_discovery.py index 49a3235..50065c5 100644 --- a/tests/test_subdevice_discovery.py +++ b/tests/test_subdevice_discovery.py @@ -87,10 +87,10 @@ def _climate_bound(coordinator, subdevice_key: str): # --------------------------------------------------------------------------- -# HJcom -- ARTIK051_DONGLE_FAC_18K, Pattern A (indexed siblings) +# Pattern A reporter -- ARTIK051_DONGLE_FAC_18K, indexed siblings # --------------------------------------------------------------------------- -async def test_hjcom_materializes_master_and_bedroom_subdevice(hass: HomeAssistant): +async def test_pattern_a_materializes_master_and_bedroom_subdevice(hass: HomeAssistant): coordinator = _coordinator(hass) await _discover(coordinator, 'airconditioner_artik051_dongle_fac_18k') @@ -104,11 +104,11 @@ async def test_hjcom_materializes_master_and_bedroom_subdevice(hass: HomeAssista assert sub1_climate.href == '/mode/vs/1' -async def test_hjcom_device_2_produces_no_entities_at_all(hass: HomeAssistant): - """HJcom's /device/2 is the unused SmartThings slot (DESIGN-177.md - section 4): it answers its seed with a full-shaped batch, but every - climate-state rep on it is empty. It must be recorded as skipped, not - materialized, and must contribute zero bound entities.""" +async def test_pattern_a_device_2_produces_no_entities_at_all(hass: HomeAssistant): + """The reporter's /device/2 is the unused SmartThings slot + (DESIGN-177.md section 4): it answers its seed with a full-shaped + batch, but every climate-state rep on it is empty. It must be recorded + as skipped, not materialized, and must contribute zero bound entities.""" coordinator = _coordinator(hass) await _discover(coordinator, 'airconditioner_artik051_dongle_fac_18k') diff --git a/tests/test_subdevices.py b/tests/test_subdevices.py index 86fcd48..38d2f72 100644 --- a/tests/test_subdevices.py +++ b/tests/test_subdevices.py @@ -383,10 +383,10 @@ def test_enumerate_no_subdevices_resource_at_all(): def test_enumerate_indexed_materializes_candidate_regardless_of_content(): """enumerate_subdevices itself has no way to tell a real sibling from an - unused slot that merely answers the same shape (HJcom's /device/2) -- - that's discover_partitioned's job (see its own tests below). A - same-shaped batch of otherwise-empty reps is still returned as a - candidate here.""" + unused slot that merely answers the same shape (the reporter's + /device/2) -- that's discover_partitioned's job (see its own tests + below). A same-shaped batch of otherwise-empty reps is still returned + as a candidate here.""" oic_res = [{'di': 'a', 'links': [{'href': '/device/2'}]}] sess = _FakeSession({ ('device', '2'): [_DEVCOL_REP, {'href': '/power/vs/2', 'rep': {}}, @@ -598,13 +598,14 @@ def test_discover_partitioned_no_subdevices_matches_plain_discover(): # --------------------------------------------------------------------------- # discover_partitioned's materialization gate: a candidate is only kept if # it produced at least one *primary* (no entity_category) bound entity whose -# flattened value isn't None. This is what tells HJcom's real /device/1 -# sibling apart from the unused /device/2 slot that answers the same shape. +# flattened value isn't None. This is what tells the reporter's real +# /device/1 sibling apart from the unused /device/2 slot that answers the +# same shape. # --------------------------------------------------------------------------- def test_discover_partitioned_skips_candidate_with_no_live_primary_entity(): """A candidate whose only populated entity is diagnostic-category - doesn't count -- exactly HJcom's /device/2 shape (an alarm_code + doesn't count -- exactly the reporter's /device/2 shape (an alarm_code sensor reading something even though the subdevice itself is empty).""" diag_cap = Capability( href='/alarms/vs/0', @@ -632,7 +633,7 @@ def test_discover_partitioned_skips_candidate_with_no_live_primary_entity(): def test_discover_partitioned_materializes_candidate_with_live_primary_entity(): """A candidate with a populated *primary* (no entity_category) entity is materialized, even alongside an all-empty diagnostic sibling href -- - exactly HJcom's /device/1 shape.""" + exactly the reporter's /device/1 shape.""" climate_cap = Capability( href='/mode/vs/0', entities=(BinarySensorDesc(key='mode', field='m'),), # no entity_category -> primary