rename: unit/sub-unit -> subdevice, matching OCF terminology

"Unit"/"sub-unit" from #199's multi-indoor-device support wasn't OCF
idiomatic -- OCF calls each component of a composite device a
"subdevice" (see subdeviceIdList), so rename SubUnit -> Subdevice
throughout: the registry module, coordinator state, BoundEntity's
subdevice field, diagnostics keys (subdevices/subdevices_skipped/
subdevice_probes), entity unique_id prefixes (unit1_/sub_<uuid>_ ->
subdevice1_/subdevice_<uuid>_), device-name fallback labels, golden
fixtures, tests, README, and the adding-device-support skill.

Breaking change to entity unique_ids and diagnostics keys, acceptable
since this hasn't been released yet.
This commit is contained in:
Marc Billow
2026-07-29 22:57:11 +00:00
parent 79ac6f199d
commit 9e85395b44
32 changed files with 676 additions and 659 deletions
+21 -21
View File
@@ -28,7 +28,7 @@ from custom_components.localthings.registry.capabilities import laundry
from custom_components.localthings.registry.capabilities.airconditioner import _climate_write
from custom_components.localthings.registry.discovery import BoundEntity
from custom_components.localthings.registry.entities import ClimateDesc
from custom_components.localthings.registry.subunits import SubUnit
from custom_components.localthings.registry.subdevices import Subdevice
ENTRY_DATA = {
CONF_HOST: '10.0.0.199',
@@ -98,26 +98,26 @@ async def test_options_write_optimistic_cache_keeps_sibling_tokens(coordinator)
# ---------------------------------------------------------------------------
# Sub-unit write translation (issue #177): a composite-entity write_fn (here
# Subdevice write translation (issue #177): a composite-entity write_fn (here
# airconditioner._climate_write) returns *canonical* path_segs
# (['power', 'vs', '0']) -- async_send_command must translate that through
# bound_entity.sub_unit.to_actual before POSTing, applying the optimistic
# value, and starting the settle guard, or a sub-unit's climate card would
# bound_entity.subdevice.to_actual before POSTing, applying the optimistic
# value, and starting the settle guard, or a subdevice's climate card would
# write to (and read confirmation from) the master's resource instead of
# its own.
# ---------------------------------------------------------------------------
def _climate_bound(href: str, sub_unit: SubUnit) -> BoundEntity:
def _climate_bound(href: str, subdevice: Subdevice) -> BoundEntity:
desc = ClimateDesc(key='climate', translation_key='airconditioner',
write_fn=_climate_write)
return BoundEntity(href=href, capability=None, desc=desc, sub_unit=sub_unit)
return BoundEntity(href=href, capability=None, desc=desc, subdevice=subdevice)
async def test_indexed_sub_unit_write_posts_to_translated_path(coordinator) -> None:
"""A power write from the bedroom unit's (indexed '1') climate entity
async def test_indexed_subdevice_write_posts_to_translated_path(coordinator) -> None:
"""A power write from the bedroom subdevice's (indexed '1') climate entity
must POST to /power/vs/1, not the master's /power/vs/0."""
unit1 = SubUnit(kind='indexed', key='1', seed_path=('device', '1'))
bound = _climate_bound('/mode/vs/1', unit1)
sub1 = Subdevice(kind='indexed', key='1', seed_path=('device', '1'))
bound = _climate_bound('/mode/vs/1', sub1)
await coordinator.async_send_command(bound, ('power', True))
@@ -126,11 +126,11 @@ async def test_indexed_sub_unit_write_posts_to_translated_path(coordinator) -> N
assert cbor2.loads(posted_bytes) == {'x.com.samsung.da.power': 'On'}
async def test_indexed_sub_unit_write_applies_optimistic_value_to_translated_href(
async def test_indexed_subdevice_write_applies_optimistic_value_to_translated_href(
coordinator,
) -> None:
unit1 = SubUnit(kind='indexed', key='1', seed_path=('device', '1'))
bound = _climate_bound('/mode/vs/1', unit1)
sub1 = Subdevice(kind='indexed', key='1', seed_path=('device', '1'))
bound = _climate_bound('/mode/vs/1', sub1)
await coordinator.async_send_command(bound, ('power', True))
@@ -142,7 +142,7 @@ async def test_indexed_sub_unit_write_applies_optimistic_value_to_translated_hre
# The settle guard is armed on that same translated href: a stale poll
# reporting the pre-write value must be dropped, not allowed to revert
# the optimistic 'On' (issue #27's regression, translated to a
# sub-unit's own resource).
# subdevice's own resource).
applied = coordinator._observe.apply(
'/power/vs/1', {'x.com.samsung.da.power': 'Off'}, source='poll',
)
@@ -150,12 +150,12 @@ async def test_indexed_sub_unit_write_applies_optimistic_value_to_translated_hre
assert coordinator._cache.get('/power/vs/1') == {'x.com.samsung.da.power': 'On'}
async def test_prefixed_sub_unit_write_posts_to_translated_path(coordinator) -> None:
"""A power write from a UUID-prefixed unit's climate entity must POST
async def test_prefixed_subdevice_write_posts_to_translated_path(coordinator) -> None:
"""A power write from a UUID-prefixed subdevice's climate entity must POST
to /<uuid>/power/vs/0, not the bare canonical href."""
sub_id = '6c2dff6d-ee5c-dad1-6a5e-000000000001'
unit = SubUnit(kind='prefixed', key=sub_id, seed_path=(sub_id, 'device', '0'))
bound = _climate_bound(f'/{sub_id}/mode/vs/0', unit)
subdevice = Subdevice(kind='prefixed', key=sub_id, seed_path=(sub_id, 'device', '0'))
bound = _climate_bound(f'/{sub_id}/mode/vs/0', subdevice)
await coordinator.async_send_command(bound, ('power', True))
@@ -167,11 +167,11 @@ async def test_prefixed_sub_unit_write_posts_to_translated_path(coordinator) ->
}
async def test_main_climate_write_unaffected_by_sub_unit_translation(coordinator) -> None:
async def test_main_climate_write_unaffected_by_subdevice_translation(coordinator) -> None:
"""MAIN's to_actual is the identity transform -- a device with no
sub-units must keep posting to the exact same path as before this
subdevices must keep posting to the exact same path as before this
translation step existed."""
from custom_components.localthings.registry.subunits import MAIN
from custom_components.localthings.registry.subdevices import MAIN
bound = _climate_bound('/mode/vs/0', MAIN)
await coordinator.async_send_command(bound, ('power', True))