feat: add HA-shaped entity description dataclasses

This commit is contained in:
Marc Billow
2026-06-30 16:32:37 -05:00
parent c6f08ea340
commit 9acc6bc8ce
3 changed files with 113 additions and 0 deletions
+79
View File
@@ -0,0 +1,79 @@
"""HA-shaped entity descriptions. The subclass *type* selects the HA platform.
Frozen dataclasses so the future native HA component can consume them as
EntityDescription subclasses unchanged. Read transforms live in value_fn;
presence gating in exists_fn; write logic in write_fn on command platforms.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any, Callable, Optional
WriteFn = Optional[Callable[[Any, dict], "tuple[list[str], dict] | None"]]
def _identity(v: Any) -> Any:
return v
@dataclass(frozen=True, kw_only=True)
class SamsungEntityDescription:
key: str
field: str = ''
name: Optional[str] = None
translation_key: Optional[str] = None
icon: Optional[str] = None
entity_category: Optional[str] = None # 'diagnostic' | 'config' | None
enabled_default: bool = True
value_fn: Callable[[Any], Any] = _identity
exists_fn: Optional[Callable[[dict], bool]] = None
@dataclass(frozen=True, kw_only=True)
class SensorDesc(SamsungEntityDescription):
device_class: Optional[str] = None
state_class: Optional[str] = None
unit: Optional[str] = None
@dataclass(frozen=True, kw_only=True)
class BinarySensorDesc(SamsungEntityDescription):
device_class: Optional[str] = None # value_fn must return bool
@dataclass(frozen=True, kw_only=True)
class SelectDesc(SamsungEntityDescription):
options: Any = () # tuple[str,...] | Callable[[dict], list[str]]
write_fn: WriteFn = None
@dataclass(frozen=True, kw_only=True)
class SwitchDesc(SamsungEntityDescription):
device_class: Optional[str] = None
write_fn: WriteFn = None
@dataclass(frozen=True, kw_only=True)
class ButtonDesc(SamsungEntityDescription):
payload: str = ''
write_fn: WriteFn = None
@dataclass(frozen=True, kw_only=True)
class NumberDesc(SamsungEntityDescription):
device_class: Optional[str] = None
unit: Optional[str] = None
native_min: Optional[float] = None
native_max: Optional[float] = None
step: Optional[float] = None
write_fn: WriteFn = None
PLATFORM_OF: dict[type, str] = {
SensorDesc: 'sensor',
BinarySensorDesc: 'binary_sensor',
SelectDesc: 'select',
SwitchDesc: 'switch',
ButtonDesc: 'button',
NumberDesc: 'number',
}
+34
View File
@@ -0,0 +1,34 @@
from samsung_appliance.registry.entities import (
SensorDesc, BinarySensorDesc, SelectDesc, SwitchDesc, ButtonDesc,
NumberDesc, PLATFORM_OF,
)
def test_value_fn_defaults_to_identity():
d = SensorDesc(key='power', field='x.com.samsung.da.instantaneousPower')
assert d.value_fn(42) == 42
def test_descriptions_are_frozen():
d = SensorDesc(key='power', field='f')
try:
d.key = 'other'
except Exception as e:
assert 'frozen' in str(type(e)).lower() or 'cannot' in str(e).lower()
else:
raise AssertionError("expected frozen dataclass")
def test_platform_mapping_covers_all_subclasses():
assert PLATFORM_OF[SensorDesc] == 'sensor'
assert PLATFORM_OF[BinarySensorDesc] == 'binary_sensor'
assert PLATFORM_OF[SelectDesc] == 'select'
assert PLATFORM_OF[SwitchDesc] == 'switch'
assert PLATFORM_OF[ButtonDesc] == 'button'
assert PLATFORM_OF[NumberDesc] == 'number'
def test_select_carries_options_and_write_fn():
d = SelectDesc(key='sound', field='mode', options=('voice', 'tone', 'mute'),
write_fn=lambda p, rep: (['settings', 'sound', 'mode', 'vs', '0'], {'mode': p}))
assert d.write_fn('tone', {}) == (['settings', 'sound', 'mode', 'vs', '0'], {'mode': 'tone'})