Limit changes to translation support
This commit is contained in:
1 parent
da49825e2e
commit
82acc05d38
8 files changed
+18
-47
No files matched your search
@@ -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
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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'
|
||||
@@ -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]}
|
||||
|
||||
Reference in new issue
Block a user