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.
This commit is contained in:
Marc Billow
2026-07-07 22:54:57 -05:00
parent 3502a385cc
commit b67e0e5cf5
51 changed files with 72 additions and 70 deletions
+11 -12
View File
@@ -110,17 +110,16 @@ custom_components/localthings/
sensor.py / binary_sensor.py / switch.py / number.py / select.py / button.py / time.py
One module per HA platform
strings.json / translations/ Config-flow copy + entity state translations
ocf/
registry/
batch.py /device/0 batch response parsing
registry/
capability.py Capability dataclass (href, entities, transforms)
entities.py Per-platform entity descriptor dataclasses
discovery.py Binds a device's live resources to registered capabilities
adapter.py Flattens bound entities into HA-ready state
identity.py Reads device identity (serial, oneUiVersion) for type detection
capabilities/ Shared + per-family Capability definitions (common, dryer, oven,
capability.py Capability dataclass (href, entities, transforms)
entities.py Per-platform entity descriptor dataclasses
discovery.py Binds a device's live resources to registered capabilities
adapter.py Flattens bound entities into HA-ready state
identity.py Reads device identity (serial, oneUiVersion) for type detection
capabilities/ Shared + per-family Capability definitions (common, dryer, oven,
dishwasher, fridge, laundry, operational)
by_type/ One DeviceRegistry per appliance type, composed from capabilities/
by_type/ One DeviceRegistry per appliance type, composed from capabilities/
tests/ 80+ tests: registry composition, discovery, entity descriptors,
golden-file regression against captured device dumps
requirements-dev.txt Test deps, including the smartthings-local package
@@ -132,9 +131,9 @@ docker-compose.yml / ha_config/ Local HA dev environment
## Adding a new appliance type
1. Capture the appliance's `/device/0` response to see what resources/fields it exposes. An authenticated `DtlsCoapSession` from `smartthings_local.protocol.dtls_session` GET is enough; `local-tools/probe_device.py` wraps this.
2. Reuse existing `Capability` objects from `ocf/registry/capabilities/` wherever the resource matches one already declared. Most `common.py` capabilities (power, kids lock, remote control, alarms, energy/water meters) are shared verbatim across families; add new ones only for resources unique to the new type.
3. Create `ocf/registry/by_type/<name>.py` with a `DeviceRegistry(name=..., capabilities=_build([...]))`. Use `pattern_capabilities` instead of `capabilities` for any resource whose `href` isn't fixed (for example per-compartment fridge resources); see `refrigerator.py` for the pattern.
4. Register it in `_REGISTRY_BY_KEY` in `ocf/registry/by_type/__init__.py`, keyed on the lowercased, space/hyphen-to-underscore-converted suffix of the device's `oneUiVersion` string (see `_type_key()` in that file for the exact transform).
2. Reuse existing `Capability` objects from `registry/capabilities/` wherever the resource matches one already declared. Most `common.py` capabilities (power, kids lock, remote control, alarms, energy/water meters) are shared verbatim across families; add new ones only for resources unique to the new type.
3. Create `registry/by_type/<name>.py` with a `DeviceRegistry(name=..., capabilities=_build([...]))`. Use `pattern_capabilities` instead of `capabilities` for any resource whose `href` isn't fixed (for example per-compartment fridge resources); see `refrigerator.py` for the pattern.
4. Register it in `_REGISTRY_BY_KEY` in `registry/by_type/__init__.py`, keyed on the lowercased, space/hyphen-to-underscore-converted suffix of the device's `oneUiVersion` string (see `_type_key()` in that file for the exact transform).
5. Add golden-file coverage in `tests/` against a captured `/device/0` dump for the new type.
No config-flow or coordinator changes are needed. Device-type detection and entity wiring are fully driven by the registry.
@@ -6,7 +6,7 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .ocf.registry.entities import BinarySensorDesc
from .registry.entities import BinarySensorDesc
from .const import DOMAIN
from .coordinator import LocalThingsCoordinator
+1 -1
View File
@@ -6,7 +6,7 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .ocf.registry.entities import ButtonDesc
from .registry.entities import ButtonDesc
from .const import DOMAIN
from .coordinator import LocalThingsCoordinator
+1 -1
View File
@@ -124,7 +124,7 @@ def _probe_and_validate(host: str, ca_cert_pem: str, ca_key_pem: str) -> dict:
"""Fetch UUID, mint leaf cert, probe each port. Returns config entry data dict."""
import cbor2
from smartthings_local.protocol.dtls_session import DtlsCoapSession
from .ocf.batch import parse_device0_batch
from .registry.batch import parse_device0_batch
_LOGGER.debug("Fetching Samsung cloud UUID from %s", _SAMSUNG_CLOUD_HOST)
try:
+6 -6
View File
@@ -15,12 +15,12 @@ from homeassistant.helpers.device_registry import DeviceInfo
from smartthings_local.protocol.dtls_session import DtlsCoapSession
from .ocf.batch import parse_device0_batch
from .ocf.registry.by_type import for_device
from .ocf.registry.discovery import discover, BoundEntity
from .ocf.registry import CAPABILITIES
from .ocf.registry.adapter import flatten
from .ocf.registry.identity import read_identity, DeviceIdentity
from .registry.batch import parse_device0_batch
from .registry.by_type import for_device
from .registry.discovery import discover, BoundEntity
from .registry import CAPABILITIES
from .registry.adapter import flatten
from .registry.identity import read_identity, DeviceIdentity
from .const import (
DOMAIN, CONF_HOST, CONF_PORT, CONF_LEAF_CERT_PEM, CONF_LEAF_KEY_PEM,
+2 -2
View File
@@ -7,8 +7,8 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.const import EntityCategory
from .ocf.registry.adapter import _key
from .ocf.registry.discovery import BoundEntity
from .registry.adapter import _key
from .registry.discovery import BoundEntity
from .const import DOMAIN
from .coordinator import LocalThingsCoordinator
+1 -1
View File
@@ -6,7 +6,7 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .ocf.registry.entities import NumberDesc
from .registry.entities import NumberDesc
from .const import DOMAIN
from .coordinator import LocalThingsCoordinator
@@ -1,5 +0,0 @@
"""Samsung appliance local-API library (custom_components/localthings/ocf).
CoAP/DTLS session management, OCF batch resource parsing, and a
capability registry for discovering and reading Samsung appliance state."""
__version__ = "2.0.0"
@@ -1,5 +0,0 @@
from .adapter import flatten, is_active, _key
from .discovery import BoundEntity, discover
from .registry import CAPABILITIES
__all__ = ['CAPABILITIES', 'discover', 'BoundEntity', 'flatten', 'is_active', '_key']
@@ -0,0 +1,13 @@
"""Device capability registry for Samsung appliances.
Declares each appliance family's resources as Capability objects keyed on
their stable OCF resource href, binds a connected device's live resources
to the matching capabilities, and flattens the result into HA-ready entity
state. The DTLS/CoAP transport itself lives in the smartthings-local
package, not here.
"""
from .adapter import flatten, is_active, _key
from .discovery import BoundEntity, discover
from .registry import CAPABILITIES
__all__ = ['CAPABILITIES', 'discover', 'BoundEntity', 'flatten', 'is_active', '_key']
+1 -1
View File
@@ -6,7 +6,7 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .ocf.registry.entities import SelectDesc
from .registry.entities import SelectDesc
from .const import DOMAIN
from .coordinator import LocalThingsCoordinator
+1 -1
View File
@@ -6,7 +6,7 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .ocf.registry.entities import SensorDesc
from .registry.entities import SensorDesc
from .const import DOMAIN
from .coordinator import LocalThingsCoordinator
+1 -1
View File
@@ -6,7 +6,7 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .ocf.registry.entities import SwitchDesc
from .registry.entities import SwitchDesc
from .const import DOMAIN
from .coordinator import LocalThingsCoordinator
+1 -1
View File
@@ -8,7 +8,7 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .ocf.registry.entities import TimeDesc
from .registry.entities import TimeDesc
from .const import DOMAIN
from .coordinator import LocalThingsCoordinator
+1 -1
View File
@@ -7,7 +7,7 @@ FIXTURES = Path(__file__).resolve().parent / 'fixtures'
def _resources_from_dump(dump: dict) -> dict[str, dict]:
from custom_components.localthings.ocf.batch import parse_device0_batch
from custom_components.localthings.registry.batch import parse_device0_batch
return parse_device0_batch(dump['device0'])
+1 -1
View File
@@ -57,7 +57,7 @@ ENTRY_DATA = {
def _load_fridge_resources() -> dict:
from custom_components.localthings.ocf.batch import parse_device0_batch
from custom_components.localthings.registry.batch import parse_device0_batch
data = json.loads((FIXTURES / 'refrigerator_device.json').read_text())
return parse_device0_batch(data['device0'])
+1 -1
View File
@@ -1,6 +1,6 @@
"""Tests for samsung_appliance/registry/by_type."""
import pytest
from custom_components.localthings.ocf.registry.by_type import _type_key, for_device, DeviceRegistry
from custom_components.localthings.registry.by_type import _type_key, for_device, DeviceRegistry
class TestTypeKey:
+4 -4
View File
@@ -4,10 +4,10 @@ from pathlib import Path
import pytest
from custom_components.localthings.ocf.registry.adapter import _key
from custom_components.localthings.ocf.registry.by_type import dishwasher, refrigerator
from custom_components.localthings.ocf.registry.capabilities import fridge
from custom_components.localthings.ocf.registry.discovery import discover
from custom_components.localthings.registry.adapter import _key
from custom_components.localthings.registry.by_type import dishwasher, refrigerator
from custom_components.localthings.registry.capabilities import fridge
from custom_components.localthings.registry.discovery import discover
from tests.conftest import _load_resources
GOLDEN = Path(__file__).parent / 'fixtures' / 'golden'
+2 -2
View File
@@ -1,5 +1,5 @@
from custom_components.localthings.ocf.registry.capability import Capability
from custom_components.localthings.ocf.registry.entities import BinarySensorDesc
from custom_components.localthings.registry.capability import Capability
from custom_components.localthings.registry.entities import BinarySensorDesc
def test_capability_defaults():
+2 -2
View File
@@ -1,5 +1,5 @@
from custom_components.localthings.ocf.registry.capabilities import common
from custom_components.localthings.ocf.registry.discovery import discover
from custom_components.localthings.registry.capabilities import common
from custom_components.localthings.registry.discovery import discover
def _reg():
+3 -3
View File
@@ -1,8 +1,8 @@
from custom_components.localthings.ocf.registry.capability import Capability
from custom_components.localthings.ocf.registry.discovery import (
from custom_components.localthings.registry.capability import Capability
from custom_components.localthings.registry.discovery import (
BoundEntity, discover, instance_suffix,
)
from custom_components.localthings.ocf.registry.entities import BinarySensorDesc
from custom_components.localthings.registry.entities import BinarySensorDesc
LOCK = Capability(
href='/kidslock/vs/0',
+3 -3
View File
@@ -1,8 +1,8 @@
import pytest
from tests.conftest import _load_device
from custom_components.localthings.ocf.registry.by_type import for_device, _type_key
from custom_components.localthings.ocf.registry.discovery import discover
from custom_components.localthings.ocf.registry.adapter import flatten, is_active
from custom_components.localthings.registry.by_type import for_device, _type_key
from custom_components.localthings.registry.discovery import discover
from custom_components.localthings.registry.adapter import flatten, is_active
@pytest.mark.parametrize('name,expected_type_key', [
+1 -1
View File
@@ -1,4 +1,4 @@
from custom_components.localthings.ocf.registry.entities import (
from custom_components.localthings.registry.entities import (
SensorDesc, BinarySensorDesc, SelectDesc, SwitchDesc, ButtonDesc,
NumberDesc, PLATFORM_OF,
)
+3 -3
View File
@@ -7,9 +7,9 @@ pattern capability that auto-derives distinct keys from the href segments.
"""
from tests.conftest import _load_resources
from custom_components.localthings.ocf.registry.adapter import _key
from custom_components.localthings.ocf.registry.by_type import refrigerator
from custom_components.localthings.ocf.registry.discovery import discover
from custom_components.localthings.registry.adapter import _key
from custom_components.localthings.registry.by_type import refrigerator
from custom_components.localthings.registry.discovery import discover
def test_two_doors_get_distinct_keys():
+4 -4
View File
@@ -7,14 +7,14 @@ GOLDEN = Path(__file__).parent / 'fixtures' / 'golden'
def _new_state_keys(name, resources):
from custom_components.localthings.ocf.registry.by_type import for_device
from custom_components.localthings.ocf.registry.discovery import discover
from custom_components.localthings.ocf.registry.adapter import flatten
from custom_components.localthings.registry.by_type import for_device
from custom_components.localthings.registry.discovery import discover
from custom_components.localthings.registry.adapter import flatten
otn = resources.get('/otninformation/vs/0', {})
one_ui = otn.get('swVersionInfo', {}).get('oneUiVersion', '')
reg = for_device(one_ui) if one_ui else None
if reg is None:
from custom_components.localthings.ocf.registry.registry import CAPABILITIES
from custom_components.localthings.registry.registry import CAPABILITIES
caps, pats = CAPABILITIES, []
else:
caps, pats = reg.capabilities, reg.pattern_capabilities
+1 -1
View File
@@ -1,6 +1,6 @@
import cbor2
from custom_components.localthings.ocf.registry.identity import DeviceIdentity, read_identity
from custom_components.localthings.registry.identity import DeviceIdentity, read_identity
class FakeSession:
+2 -2
View File
@@ -1,6 +1,6 @@
"""Write-handler unit tests for laundry-family capabilities."""
from custom_components.localthings.ocf.registry.capabilities import laundry
from custom_components.localthings.ocf.registry.capabilities.operational import OPERATIONAL_STATE
from custom_components.localthings.registry.capabilities import laundry
from custom_components.localthings.registry.capabilities.operational import OPERATIONAL_STATE
def test_start_button_writes_run():
+1 -1
View File
@@ -1,4 +1,4 @@
from custom_components.localthings.ocf.registry.capabilities.operational import OPERATIONAL_STATE
from custom_components.localthings.registry.capabilities.operational import OPERATIONAL_STATE
def test_machine_state_maps_samsung_to_ocf():
+2 -2
View File
@@ -1,6 +1,6 @@
"""Unit tests for oven-family capabilities."""
from custom_components.localthings.ocf.registry.capabilities import oven
from custom_components.localthings.ocf.registry.discovery import discover
from custom_components.localthings.registry.capabilities import oven
from custom_components.localthings.registry.discovery import discover
# ---------------------------------------------------------------------------
+1 -1
View File
@@ -1,5 +1,5 @@
"""Tests for the CAPABILITIES registry."""
from custom_components.localthings.ocf.registry.registry import CAPABILITIES
from custom_components.localthings.registry.registry import CAPABILITIES
def test_registry_is_keyed_by_href():