refactor(fridge): use HA's own translation system for enum entities
Replace the ad-hoc option_names dicts (hardcoded English, manual reverse-mapping) with the idiomatic HA approach: normalize raw Samsung enum values to lowercase at the select/sensor boundary and let strings.json/translations/en.json supply the display text, keyed by the normalized value. select.py maps the chosen (lowercased) option back to the device's original casing before writing. The ice-making status sensor now also declares device_class=enum with an options list, as HA expects for enum sensor state translations. Also drop the hardcoded _BZONE_MODES list in favor of discovering beverage zone modes from the device's own roomSupportedModes field, matching how flex_zone_mode and ice_type already work.
This commit is contained in:
1 parent
b386063e79
commit
3ab0d5553e
6 files changed
+104
-53
No files matched your search
@@ -19,33 +19,11 @@ from ..entities import (
|
||||
SwitchDesc, TimeDesc,
|
||||
)
|
||||
|
||||
# Beverage zone flex modes.
|
||||
_BZONE_MODE_NAMES = {
|
||||
'SP_TTYPE_BEER_DRINKS': 'Beverage',
|
||||
'SP_TTYPE_WINE_DESSERT': 'Wine and Dessert',
|
||||
}
|
||||
_BZONE_MODES = tuple(_BZONE_MODE_NAMES)
|
||||
|
||||
# Flex zone (convertible drawer) modes.
|
||||
_FLEX_ZONE_MODE_NAMES = {
|
||||
'CV_TTYPE_RF9000A_FREEZE': 'Freeze',
|
||||
'CV_TTYPE_RF9000A_SOFTFREEZE': 'Soft Freeze',
|
||||
'CV_TTYPE_RF9000A_MEAT_FISH': 'Meat/Fish',
|
||||
'CV_TTYPE_RF9000A_FRUIT_VEGGIES': 'Fruit & Veggies',
|
||||
'CV_TTYPE_RF9000A_BEVERAGE': 'Beverage',
|
||||
}
|
||||
|
||||
# Icemaker ball-production rate options (whiskey icemaker models).
|
||||
_ICE_TYPE_NAMES = {
|
||||
'WHISKEY_ICEBALL_3': '3 Balls/Day',
|
||||
'WHISKEY_ICEBALL_6': '6 Balls/Day',
|
||||
'WHISKEY_ICEBALL_9': '9 Balls/Day',
|
||||
}
|
||||
|
||||
_ICE_MAKING_STATUS_NAMES = {
|
||||
'ICESTATUS_STOP': 'Idle',
|
||||
'ICESTATUS_RUN': 'Making ice',
|
||||
}
|
||||
# Display names for the beverage zone, flex zone, ice type, and
|
||||
# ice-making-status enums below live in strings.json / translations/en.json,
|
||||
# keyed by the lowercased raw device value — select.py and SensorDesc.options
|
||||
# normalize to lowercase for HA's translation lookup and map back to this
|
||||
# original casing before writing to the device.
|
||||
|
||||
|
||||
def _int(v):
|
||||
@@ -127,17 +105,20 @@ ICEMAKER_GENERIC = Capability(
|
||||
entities=(
|
||||
SensorDesc(key='making_status',
|
||||
field='x.com.samsung.da.iceMaker.iceMakingStatus',
|
||||
name='Making status', icon='mdi:cube-outline',
|
||||
value_fn=lambda v: _ICE_MAKING_STATUS_NAMES.get(v, v)),
|
||||
name=None, icon='mdi:cube-outline',
|
||||
device_class='enum',
|
||||
options=('icestatus_stop', 'icestatus_run'),
|
||||
translation_key='ice_making_status',
|
||||
value_fn=lambda v: v.lower() if isinstance(v, str) else v),
|
||||
SwitchDesc(key='enabled', field='x.com.samsung.da.iceMaker.state',
|
||||
name=None, icon='mdi:cube-outline',
|
||||
value_fn=lambda v: v == 'On',
|
||||
write_fn=_icemaker_write('x.com.samsung.da.iceMaker.state')),
|
||||
SelectDesc(key='type', field='x.com.samsung.da.iceType.desired',
|
||||
name='Ice type', icon='mdi:cube-outline',
|
||||
name=None, icon='mdi:cube-outline',
|
||||
translation_key='ice_type',
|
||||
entity_category='config',
|
||||
options_field='x.com.samsung.da.iceType.supported',
|
||||
option_names=_ICE_TYPE_NAMES,
|
||||
exists_fn=lambda rep: bool(rep.get('x.com.samsung.da.iceType.supported')),
|
||||
write_fn=_icemaker_write('x.com.samsung.da.iceType.desired')),
|
||||
),
|
||||
@@ -482,8 +463,6 @@ SABBATH = Capability(
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _bzone_write(p, rep, href=None):
|
||||
if p not in _BZONE_MODES:
|
||||
return None
|
||||
return ['specialzone', 'one', 'vs', '0'], {'roomDesiredMode': p}
|
||||
|
||||
|
||||
@@ -493,8 +472,9 @@ BEVERAGE_ZONE = Capability(
|
||||
entities=(
|
||||
SelectDesc(key='beverage_zone_mode', field='roomDesiredMode',
|
||||
name='Beverage zone mode', icon='mdi:glass-wine',
|
||||
translation_key='beverage_zone_mode',
|
||||
entity_category='config',
|
||||
options=_BZONE_MODES, option_names=_BZONE_MODE_NAMES,
|
||||
options_field='roomSupportedModes',
|
||||
write_fn=_bzone_write),
|
||||
),
|
||||
)
|
||||
@@ -521,9 +501,9 @@ FLEX_ZONE = Capability(
|
||||
SelectDesc(key='flex_zone_mode',
|
||||
field='x.com.samsung.da.modes',
|
||||
name='Flex zone mode', icon='mdi:thermostat',
|
||||
translation_key='flex_zone_mode',
|
||||
entity_category='config',
|
||||
options_field='x.com.samsung.da.supportedOptions',
|
||||
option_names=_FLEX_ZONE_MODE_NAMES,
|
||||
exists_fn=lambda rep: bool(
|
||||
rep.get('x.com.samsung.da.supportedOptions')),
|
||||
value_fn=lambda modes: next(
|
||||
|
||||
@@ -35,6 +35,7 @@ class SensorDesc(SamsungEntityDescription):
|
||||
device_class: Optional[str] = None
|
||||
state_class: Optional[str] = None
|
||||
unit: Optional[str] = None
|
||||
options: Optional[tuple] = None # required by HA when device_class == 'enum'
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
@@ -46,7 +47,6 @@ class BinarySensorDesc(SamsungEntityDescription):
|
||||
class SelectDesc(SamsungEntityDescription):
|
||||
options: Any = () # tuple[str,...] | Callable[[dict], list[str]]
|
||||
options_field: Optional[str] = None # resource field that contains the live options list
|
||||
option_names: Optional[dict] = None # raw device value -> human-readable name
|
||||
write_fn: WriteFn = None
|
||||
|
||||
|
||||
|
||||
@@ -26,37 +26,46 @@ async def async_setup_entry(
|
||||
)
|
||||
|
||||
|
||||
def _normalize(value):
|
||||
"""HA option/state values must be lowercase to serve as translation keys.
|
||||
|
||||
Samsung's raw enum values are upper snake case (e.g.
|
||||
CV_TTYPE_RF9000A_FREEZE); the device still expects that exact casing
|
||||
back on write, so callers must map the normalized value back via
|
||||
_raw_options() before sending a command.
|
||||
"""
|
||||
return value.lower() if isinstance(value, str) else value
|
||||
|
||||
|
||||
class LocalThingsSelect(LocalThingsEntity, SelectEntity):
|
||||
|
||||
def __init__(self, coordinator: LocalThingsCoordinator, bound) -> None:
|
||||
super().__init__(coordinator, bound)
|
||||
desc: SelectDesc = bound.desc
|
||||
if not desc.options_field:
|
||||
self._attr_options = list(desc.options)
|
||||
self._attr_options = [_normalize(o) for o in desc.options]
|
||||
|
||||
def _raw_options(self) -> list[str]:
|
||||
desc: SelectDesc = self._bound.desc
|
||||
if desc.options_field:
|
||||
rep = self.coordinator.last_resources.get(self._bound.href) or {}
|
||||
return list(rep.get(desc.options_field) or [])
|
||||
return list(desc.options)
|
||||
|
||||
@property
|
||||
def options(self) -> list[str]:
|
||||
desc: SelectDesc = self._bound.desc
|
||||
if desc.options_field:
|
||||
rep = self.coordinator.last_resources.get(self._bound.href) or {}
|
||||
raw = list(rep.get(desc.options_field) or [])
|
||||
else:
|
||||
raw = self._attr_options
|
||||
if desc.option_names:
|
||||
return [desc.option_names.get(o, o) for o in raw]
|
||||
return raw
|
||||
return [_normalize(o) for o in self._raw_options()]
|
||||
return self._attr_options
|
||||
|
||||
@property
|
||||
def current_option(self):
|
||||
desc: SelectDesc = self._bound.desc
|
||||
raw = (self.coordinator.data or {}).get(self._state_key)
|
||||
if desc.option_names:
|
||||
return desc.option_names.get(raw, raw)
|
||||
return raw
|
||||
return _normalize(raw)
|
||||
|
||||
async def async_select_option(self, option: str) -> None:
|
||||
desc: SelectDesc = self._bound.desc
|
||||
if desc.option_names:
|
||||
reverse = {v: k for k, v in desc.option_names.items()}
|
||||
option = reverse.get(option, option)
|
||||
await self.coordinator.async_send_command(self._bound, option)
|
||||
raw = next(
|
||||
(o for o in self._raw_options() if _normalize(o) == option), option
|
||||
)
|
||||
await self.coordinator.async_send_command(self._bound, raw)
|
||||
@@ -34,6 +34,8 @@ class LocalThingsSensor(LocalThingsEntity, SensorEntity):
|
||||
self._attr_native_unit_of_measurement = desc.unit
|
||||
self._attr_device_class = desc.device_class
|
||||
self._attr_state_class = desc.state_class
|
||||
if desc.options:
|
||||
self._attr_options = list(desc.options)
|
||||
|
||||
@property
|
||||
def native_value(self):
|
||||
|
||||
@@ -15,6 +15,36 @@
|
||||
"66": "Medium",
|
||||
"100": "High"
|
||||
}
|
||||
},
|
||||
"ice_type": {
|
||||
"state": {
|
||||
"whiskey_iceball_3": "3 Balls/Day",
|
||||
"whiskey_iceball_6": "6 Balls/Day",
|
||||
"whiskey_iceball_9": "9 Balls/Day"
|
||||
}
|
||||
},
|
||||
"beverage_zone_mode": {
|
||||
"state": {
|
||||
"sp_ttype_beer_drinks": "Beverage",
|
||||
"sp_ttype_wine_dessert": "Wine and Dessert"
|
||||
}
|
||||
},
|
||||
"flex_zone_mode": {
|
||||
"state": {
|
||||
"cv_ttype_rf9000a_freeze": "Freeze",
|
||||
"cv_ttype_rf9000a_softfreeze": "Soft Freeze",
|
||||
"cv_ttype_rf9000a_meat_fish": "Meat/Fish",
|
||||
"cv_ttype_rf9000a_fruit_veggies": "Fruit & Veggies",
|
||||
"cv_ttype_rf9000a_beverage": "Beverage"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
"ice_making_status": {
|
||||
"state": {
|
||||
"icestatus_stop": "Idle",
|
||||
"icestatus_run": "Making ice"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -15,6 +15,36 @@
|
||||
"66": "Medium",
|
||||
"100": "High"
|
||||
}
|
||||
},
|
||||
"ice_type": {
|
||||
"state": {
|
||||
"whiskey_iceball_3": "3 Balls/Day",
|
||||
"whiskey_iceball_6": "6 Balls/Day",
|
||||
"whiskey_iceball_9": "9 Balls/Day"
|
||||
}
|
||||
},
|
||||
"beverage_zone_mode": {
|
||||
"state": {
|
||||
"sp_ttype_beer_drinks": "Beverage",
|
||||
"sp_ttype_wine_dessert": "Wine and Dessert"
|
||||
}
|
||||
},
|
||||
"flex_zone_mode": {
|
||||
"state": {
|
||||
"cv_ttype_rf9000a_freeze": "Freeze",
|
||||
"cv_ttype_rf9000a_softfreeze": "Soft Freeze",
|
||||
"cv_ttype_rf9000a_meat_fish": "Meat/Fish",
|
||||
"cv_ttype_rf9000a_fruit_veggies": "Fruit & Veggies",
|
||||
"cv_ttype_rf9000a_beverage": "Beverage"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
"ice_making_status": {
|
||||
"state": {
|
||||
"icestatus_stop": "Idle",
|
||||
"icestatus_run": "Making ice"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user