Limit changes to translation support

This commit is contained in:
Hmmbob committed 2026-07-24 18:40:44 +02:00
1 parent da49825e2e
commit 82acc05d38
8 files changed
+18 -47

No files matched your search

+5 -3
View File
@@ -5,6 +5,7 @@ import logging
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from .const import DOMAIN, PLATFORMS
from .coordinator import LocalThingsCoordinator
@@ -15,9 +16,10 @@ _LOGGER = logging.getLogger(__name__)
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
hass.data.setdefault(DOMAIN, {})
coordinator = LocalThingsCoordinator(hass, entry)
# DataUpdateCoordinator already raises ConfigEntryNotReady with the
# underlying update context; avoid replacing it with hardcoded UI text.
await coordinator.async_config_entry_first_refresh()
try:
await coordinator.async_config_entry_first_refresh()
except Exception as err:
raise ConfigEntryNotReady(f"Cannot connect to device: {err}") from err
hass.data[DOMAIN][entry.entry_id] = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
+1 -1
View File
@@ -316,7 +316,7 @@ class LocalThingsConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
def _create_entry(self, info: dict) -> FlowResult:
return self.async_create_entry(
title=f"Samsung ({self._host})",
title=f"Samsung Appliance ({self._host})",
data={
CONF_HOST: self._host,
CONF_PORT: info["port"],
+4 -13
View File
@@ -24,7 +24,6 @@ from .registry.batch import parse_device0_batch
from .registry.by_type import for_device, for_device_by_model, for_device_by_resources
from .registry.capabilities.common import remote_control_enabled
from .registry.discovery import discover, BoundEntity
from .registry.entities import ValidationIssue
from .registry import CAPABILITIES
from .registry.adapter import flatten
from .registry.identity import read_identity, DeviceIdentity
@@ -105,7 +104,7 @@ class LocalThingsCoordinator(DataUpdateCoordinator[dict[str, Any]]):
self.device_serial = entry.data[CONF_HOST] # placeholder until first poll
self.device_info = DeviceInfo(
identifiers={(DOMAIN, entry.data[CONF_HOST])},
name=f"Samsung ({entry.data[CONF_HOST]})",
name=f"Samsung Appliance ({entry.data[CONF_HOST]})",
manufacturer="Samsung",
)
self._session_lock = asyncio.Lock()
@@ -318,15 +317,10 @@ class LocalThingsCoordinator(DataUpdateCoordinator[dict[str, Any]]):
self.device_serial = serial
ident = self._identity
device_type = reg.name.replace('_', ' ').title() if reg else 'Appliance'
model_num = info.get('x.com.samsung.da.modelNum', '')
model = model_num.split('|', 1)[0] if model_num else (ident.model if ident else '')
# DeviceInfo has no translation mechanism. Prefer the appliance's own
# OCF name, then proper-noun/model fallbacks without an English family
# label. Existing device/entity registry IDs remain keyed by serial.
name = (
(ident.name if ident else '')
or (f"Samsung {model}" if model else f"Samsung ({self._entry.data[CONF_HOST]})")
)
name = f"Samsung {device_type} ({model})" if model else f"Samsung {device_type}"
mfr = (ident.manufacturer if ident else '') or 'Samsung'
self.device_info = DeviceInfo(
@@ -555,12 +549,9 @@ class LocalThingsCoordinator(DataUpdateCoordinator[dict[str, Any]]):
if validate_fn is not None:
error = validate_fn(payload, rep, resources)
if error:
if not isinstance(error, ValidationIssue):
raise TypeError("validate_fn must return ValidationIssue or None")
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key=error.translation_key,
translation_placeholders=dict(error.translation_placeholders),
translation_key=error,
)
result = write_fn(payload, rep, href)
if result is None:
+1 -5
View File
@@ -43,11 +43,7 @@ def _derive_name(state_key: str) -> str:
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 match: f' {match.group(1)}' if int(match.group(1)) > 0 else '',
state_key,
)
name = re.sub(r'(\d+)$', lambda m: f' {m.group()}' if int(m.group()) > 0 else '', state_key)
return _snake_to_title(name).strip()
@@ -16,7 +16,7 @@ array.
from datetime import datetime, timezone
from ..capability import Capability
from ..entities import BinarySensorDesc, SelectDesc, SensorDesc, ValidationIssue
from ..entities import BinarySensorDesc, SelectDesc, SensorDesc
from .laundry import (
bool_option_exists, bool_option_switch, cycle_options, cycle_select, hex_pairs, option_value,
replace_in_options,
@@ -282,7 +282,7 @@ def _bool_option_switch(key, name, icon, prefix, availability_field):
if len(pairs) != len(courses):
return None
if pairs[courses.index(current)] != 'F0':
return ValidationIssue(f"{key}_unavailable_for_cycle")
return f"{key}_unavailable_for_cycle"
return None
return bool_option_switch(
@@ -8,25 +8,15 @@ where a description declares one.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from dataclasses import dataclass
from typing import Any, Callable, Mapping, Optional
WriteFn = Optional[Callable[[Any, dict], "tuple[list[str], dict] | None"]]
# (payload, rep, resources) -> a translatable rejection, or None to allow the
# (payload, rep, resources) -> a translation key, or None to allow the
# write. resources is the coordinator's full href->rep snapshot, for the same
# cross-resource lookups exists_fn needs (e.g. reading a sibling href's live
# option list).
@dataclass(frozen=True)
class ValidationIssue:
"""A user-facing, translatable rejection from a descriptor validator."""
translation_key: str
translation_placeholders: Mapping[str, str] = field(default_factory=dict)
ValidateFn = Optional[Callable[[Any, dict, dict], "ValidationIssue | None"]]
ValidateFn = Optional[Callable[[Any, dict, dict], "str | None"]]
def _identity(v: Any) -> Any:
-7
View File
@@ -45,10 +45,3 @@ def test_href_instance_name_becomes_translation_placeholder():
)
entity = _make_entity(desc, key_override='icemaker_one_enabled')
assert entity.translation_placeholders == {'instance_name': 'Icemaker One'}
def test_untranslated_vendor_entity_keeps_readable_fallback_name():
desc = BinarySensorDesc(key='vendor_feature')
entity = _make_entity(desc, key_override='vendor_feature_1')
assert entity.translation_key is None
assert entity._attr_name == 'Vendor Feature 1'
+2 -3
View File
@@ -355,11 +355,10 @@ class TestWashOptionToggleValidation:
def test_rejected_on_an_unsupported_course(self):
rep = {'x.com.samsung.da.options': ['Course_1C', _BUBBLE_SOAK_SET]}
issue = self._desc('bubble_soak').validate_fn(
translation_key = self._desc('bubble_soak').validate_fn(
'On', rep, _EDIT_COURSE_RESOURCES
)
assert issue.translation_key == 'bubble_soak_unavailable_for_cycle'
assert issue.translation_placeholders == {}
assert translation_key == 'bubble_soak_unavailable_for_cycle'
def test_pre_wash_and_intensive_use_their_own_availableset_field(self):
rep = {'x.com.samsung.da.options': ['Course_30', _PRE_WASH_AVAILABLE_SET]}