Course codes on the shared /course/vs/0 contract aren't guaranteed consistent across board generations: washer/combo devices report course table Table_02, dryer devices Table_03 (x.com.samsung.da.st.courseTable, previously fully ignored), and every code in washer_cycle/dryer_cycle was confirmed exclusively against those. FlexWash's older DA_WM_A51 board reports Table_00 instead -- applying the same translations there risked showing a wrong name for any code that happens to numerically collide between tables, not just an untranslated one. SelectDesc.translation_key can now be a callable (resources -> key or None), mirroring the existing pattern for `options`. laundry.cycle_select gains optional table_href/validated_table params: when given, the renamed washer_cycle_table_02/dryer_cycle_table_03 keys only apply when the device's own course table matches exactly -- a different table, or no table id at all, gets no translation_key (raw code display) rather than a guess. dishwasher's call site is unchanged (static key, unconditional): no equivalent table-id resource exists in any dump seen, and no evidence its course codes vary by table the way washer/dryer's do. entity.py and select.py resolve a callable translation_key once (via coordinator.last_resources) and reuse that resolved value everywhere _display() needs it, rather than re-checking the raw descriptor field.
79 lines
3.2 KiB
Python
79 lines
3.2 KiB
Python
"""Base entity for Local Things."""
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
|
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
|
from homeassistant.helpers.device_registry import DeviceInfo
|
|
from homeassistant.const import EntityCategory
|
|
|
|
from .registry.adapter import _key
|
|
from .registry.discovery import BoundEntity, _snake_to_title
|
|
|
|
from .const import DOMAIN
|
|
from .coordinator import LocalThingsCoordinator
|
|
|
|
|
|
def _is_included(bound: BoundEntity, coordinator: 'LocalThingsCoordinator') -> bool:
|
|
"""Return False if the entity should not be registered for this device.
|
|
|
|
Explicit exists_fn takes priority. Otherwise, if the entity has a field,
|
|
require that field to be present in the resource rep so that optional
|
|
fields on shared resources don't create phantom entities.
|
|
|
|
An empty rep ({}) means /device/0 returned a stub for this resource —
|
|
the resource exists on the device but data hasn't been fetched yet.
|
|
In that case we include the entity so it can be populated by sub-polls.
|
|
"""
|
|
rep = coordinator.last_resources.get(bound.href)
|
|
if rep is None:
|
|
return False
|
|
if bound.desc.exists_fn is not None:
|
|
return bound.desc.exists_fn(rep, coordinator.last_resources)
|
|
if bound.desc.field:
|
|
if not rep: # stub — resource known to exist, data not yet fetched
|
|
return True
|
|
return bound.desc.field in rep
|
|
return True # rep_fn or no-field entities (ButtonDesc) are always included
|
|
|
|
|
|
def _derive_name(state_key: str) -> str:
|
|
"""Turn a snake_case state key into a title-cased display name.
|
|
|
|
Strips a trailing instance number of 0 (singleton), promotes any other
|
|
instance number with a space: "door_cooler_open1" → "Door Cooler Open 1".
|
|
"""
|
|
name = re.sub(r'(\d+)$', lambda m: f' {m.group()}' if int(m.group()) > 0 else '', state_key)
|
|
return _snake_to_title(name).strip()
|
|
|
|
|
|
class LocalThingsEntity(CoordinatorEntity[LocalThingsCoordinator]):
|
|
"""Base class for all Local Things entities."""
|
|
|
|
_attr_has_entity_name = True
|
|
|
|
def __init__(self, coordinator: LocalThingsCoordinator, bound: BoundEntity) -> None:
|
|
super().__init__(coordinator)
|
|
self._bound = bound
|
|
self._state_key = _key(bound)
|
|
self._attr_unique_id = f"{DOMAIN}_{coordinator.device_serial}_{self._state_key}"
|
|
if bound.desc.name is not None:
|
|
self._attr_name = bound.desc.name
|
|
elif bound.instance_name:
|
|
# A device-given instance name (e.g. an ice maker's "Cubed
|
|
# Ice") takes the place of the href-derived instance label,
|
|
# keeping the same entity-specific suffix (issue #27).
|
|
self._attr_name = f"{bound.instance_name} {_derive_name(bound.desc.key)}".strip()
|
|
else:
|
|
self._attr_name = _derive_name(self._state_key)
|
|
tk = bound.desc.translation_key
|
|
self._attr_translation_key = tk(coordinator.last_resources) if callable(tk) else tk
|
|
self._attr_icon = bound.desc.icon
|
|
raw_cat = bound.desc.entity_category
|
|
self._attr_entity_category = EntityCategory(raw_cat) if raw_cat else None
|
|
self._attr_entity_registry_enabled_default = bound.desc.enabled_default
|
|
|
|
@property
|
|
def device_info(self) -> DeviceInfo:
|
|
return self.coordinator.device_info
|