diff --git a/custom_components/localthings/coordinator.py b/custom_components/localthings/coordinator.py index 66bc4a4..67e37aa 100644 --- a/custom_components/localthings/coordinator.py +++ b/custom_components/localthings/coordinator.py @@ -202,6 +202,17 @@ class LocalThingsCoordinator(DataUpdateCoordinator[dict[str, Any]]): _POST_TIMEOUT_S: float = 8.0 _POLL_TIMEOUT_S: float = 35.0 + # First-discovery subdevice enumeration is part of config-entry setup, so + # it must have a finite wall-clock cost. A UUID-prefixed AC whose + # //device/0 Collection is absent falls back to individual property + # probes; some firmware silently drops unknown prefixed paths instead of + # returning 4.04, making the old 10s-per-href scan take several minutes. + # Keep enough time for a real blockwise Collection response, then use + # short timeouts for the small Property resources, all under one budget. + _SUBDEVICE_ENUMERATION_BUDGET_S: float = 15.0 + _SUBDEVICE_COLLECTION_TIMEOUT_S: float = 10.0 + _SUBDEVICE_PROPERTY_TIMEOUT_S: float = 1.0 + def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: # Per-device logger so every log line (including the base # coordinator's and ObserveManager's) identifies which device it's @@ -646,6 +657,40 @@ class LocalThingsCoordinator(DataUpdateCoordinator[dict[str, Any]]): # Discovery (runs once on first successful poll) # ------------------------------------------------------------------ + def _subdevice_probe_priority( + self, + resources: dict[str, dict], + ) -> tuple[str, ...]: + """Return live primary-entity hrefs in hot/warm-first order. + + A prefixed subdevice without a Collection endpoint has to be probed + one Property href at a time. Resolve the master through the same + registry used by discovery and put resources that produce primary + entities first. This is metadata-driven rather than an AC-specific + list: a future composite appliance gets the priority its own registry + declares, while unknown devices simply retain the normal href order. + """ + registry = resolve_registry( + resources, + device_types=self._identity.device_types if self._identity else (), + ) + if registry is None: + return () + + tier_rank = {"hot": 0, "warm": 1, "cold": 2} + ranked = [] + for order, href in enumerate(resources): + primary_caps = [ + capability + for capability in registry.capabilities.get(href, ()) + if any(desc.entity_category is None for desc in capability.entities) + ] + if not primary_caps: + continue + rank = min(tier_rank.get(capability.poll_tier, 2) for capability in primary_caps) + ranked.append((rank, order, href)) + return tuple(href for _, _, href in sorted(ranked)) + def _enumerate_subdevices_blocking(self, resources: dict[str, dict]) -> dict[str, dict]: """One-time (first discovery only) probe for sibling indoor subdevices on this connection (issue #177) -- see @@ -671,6 +716,10 @@ class LocalThingsCoordinator(DataUpdateCoordinator[dict[str, Any]]): resources, oic_res, probe_log=lambda href, found: probes.__setitem__(href, found), + preferred_hrefs=self._subdevice_probe_priority(resources), + time_budget=self._SUBDEVICE_ENUMERATION_BUDGET_S, + collection_timeout=self._SUBDEVICE_COLLECTION_TIMEOUT_S, + property_timeout=self._SUBDEVICE_PROPERTY_TIMEOUT_S, ) self.subdevices = subdevices self._subdevice_probes = probes diff --git a/custom_components/localthings/registry/capabilities/laundry.py b/custom_components/localthings/registry/capabilities/laundry.py index a117439..4205802 100644 --- a/custom_components/localthings/registry/capabilities/laundry.py +++ b/custom_components/localthings/registry/capabilities/laundry.py @@ -323,12 +323,57 @@ def cycle_write(p, rep, href=None): } +def personal_course_labels(resources, href="/wm/personalcourse/vs/0"): + """Return device-provided personal course names keyed by course code. + + Populated entries use a small TLV payload. The leading field is + ``01 ``; later fields contain a description and + settings and are intentionally left uninterpreted. Empty slots are + encoded as ``_00``. Malformed or undecodable entries are ignored so + opaque device data can never become a misleading label. + """ + rep = resources.get(href) or {} + labels = {} + for entry in rep.get("x.com.samsung.da.courses") or []: + if not isinstance(entry, str) or "_" not in entry: + continue + code, encoded = entry.split("_", 1) + try: + payload = bytes.fromhex(encoded) + except ValueError: + continue + if len(payload) < 3 or payload[0] != 0x01: + continue + name_length = payload[1] + if name_length == 0 or len(payload) < 2 + name_length: + continue + try: + name = payload[2 : 2 + name_length].decode("utf-8") + except UnicodeDecodeError: + continue + if name.strip() and name.isprintable(): + labels[code.upper()] = name + return labels + + +def washer_cycle_fallback(value, resources): + """Label a personal washer course from its device-provided name. + + No fallback for an unrecognized standard code -- an invented English + label would defeat translation (PR #251 review); the raw code displays + instead, same as before this function existed. + """ + if not isinstance(value, str): + return None + return personal_course_labels(resources).get(value.upper()) + + def _table_id(resources, table_href): rep = resources.get(table_href) or {} return rep.get("x.com.samsung.da.st.courseTable") -def cycle_select(*, translation_key, icon, table_href=None): +def cycle_select(*, translation_key, icon, table_href=None, display_fn=None): """A 'Cycle' select over /course/vs/0, labelled from `translation_key`. The option list, current value, and write path are shared across @@ -346,6 +391,11 @@ def cycle_select(*, translation_key, icon, table_href=None): instead of borrowing a label from another board generation -- translating a new table is a translations-only change. + The raw course code remains writable regardless; its display uses + display_fn when supplied, otherwise it remains raw. display_fn is an + optional family-specific fallback for untranslated raw values -- + select.py applies it after catalog lookup to both state and options. + Left at its default for dishwasher, which has no equivalent table-id resource and no evidence its codes vary by table the way washer/ dryer's do. @@ -367,6 +417,7 @@ def cycle_select(*, translation_key, icon, table_href=None): options=cycle_options, exists_fn=lambda rep, resources: bool(cycle_options(resources)), rep_fn=lambda rep: option_value(rep.get("x.com.samsung.da.options"), "Course"), + display_fn=display_fn, write_fn=cycle_write, ) diff --git a/custom_components/localthings/registry/capabilities/washer.py b/custom_components/localthings/registry/capabilities/washer.py index 68e464d..4e3f910 100644 --- a/custom_components/localthings/registry/capabilities/washer.py +++ b/custom_components/localthings/registry/capabilities/washer.py @@ -27,6 +27,7 @@ from .laundry import ( hex_pairs, option_value, option_write, + washer_cycle_fallback, ) # Course_XX hex code labels (translations/en.json, @@ -50,6 +51,14 @@ from .laundry import ( # options' MostUsed_* entry was considered as a fallback source (its first # byte matches the selected Course_XX on both dumps), but the remaining # bytes don't decode to any confirmed course code, so it isn't used. +# +# The owner of a Korean Table_02 washer confirmed the names for its newer +# 69/6A-79/88 course-code family, including Course_69 as AI Wash. Those names +# live only in the table-scoped translation catalog; a code not confirmed by +# the owner or device metadata falls back to washer_cycle_fallback, which +# surfaces a personal-course name only -- no invented English label for an +# unrecognized standard code (PR #251 review). +# --------------------------------------------------------------------------- # /washer/vs/0 -- wash temperature, spin speed, rinse cycle count. # Despite the shared href, this is unrelated to dryer.DRYER_SETTINGS (also @@ -250,6 +259,7 @@ WASHER_COURSE = Capability( translation_key="washer_cycle", icon="mdi:washing-machine", table_href="/st/washercourse/vs/0", + display_fn=washer_cycle_fallback, ), SensorDesc( key="drum_clean_cycles_remaining", diff --git a/custom_components/localthings/registry/entities.py b/custom_components/localthings/registry/entities.py index 8dd06eb..fdfdb02 100644 --- a/custom_components/localthings/registry/entities.py +++ b/custom_components/localthings/registry/entities.py @@ -19,6 +19,7 @@ WriteFn = Callable[[Any, dict], "tuple[list[str], dict] | None"] | None # cross-resource lookups exists_fn needs (e.g. reading a sibling href's live # option list). ValidateFn = Callable[[Any, dict, dict], "str | None"] | None +DisplayFn = Callable[[Any, dict], Any] | None def _identity(v: Any) -> Any: @@ -77,6 +78,10 @@ class SelectDesc(SamsungEntityDescription): # snapshot (not just this entity's own href) and returns raw device # option values; see select.py's LocalThingsSelect._raw_options(). options_field: str | None = None # resource field that contains the live options list + # Optional device-specific fallback for values absent from the translation + # catalog. Receives (raw_value, canonical_resources); select.py applies it + # identically to the current state and every option. + display_fn: DisplayFn = None write_fn: WriteFn = None diff --git a/custom_components/localthings/registry/subdevices.py b/custom_components/localthings/registry/subdevices.py index 9c4ea06..9b14503 100644 --- a/custom_components/localthings/registry/subdevices.py +++ b/custom_components/localthings/registry/subdevices.py @@ -37,6 +37,7 @@ appliance's own bookkeeping, not evidence of a second indoor unit -- see from __future__ import annotations import re +import time from collections.abc import Callable, Sequence from dataclasses import dataclass @@ -221,12 +222,12 @@ def _seed_href(path_segs: tuple[str, ...]) -> str: return "/" + "/".join(path_segs) -def _get_raw(sess, path_segs: tuple[str, ...]): +def _get_raw(sess, path_segs: tuple[str, ...], timeout: float = 10.0): """GET `path_segs` and CBOR-decode the payload, or None on any missing/malformed response (a 4.04, a timeout, an empty payload) -- shared tolerated-absence posture for both callers below.""" try: - code, pl = sess.get(list(path_segs), timeout=10.0) + code, pl = sess.get(list(path_segs), timeout=timeout) if code == 0x45 and pl: return cbor2.loads(pl) except Exception: @@ -234,19 +235,29 @@ def _get_raw(sess, path_segs: tuple[str, ...]): return None -def _get_batch(sess, path_segs: tuple[str, ...]) -> dict[str, dict]: +def _get_batch( + sess, + path_segs: tuple[str, ...], + timeout: float = 10.0, +) -> dict[str, dict]: """GET a Samsung Collection resource and parse it the same way /device/0 itself is parsed (parse_device0_batch): a [devcol-rep, {href, rep}, ...] CBOR list, not a bare Property map.""" - body = _get_raw(sess, path_segs) + body = _get_raw(sess, path_segs, timeout) return parse_device0_batch(body) if isinstance(body, list) else {} -def _get_property(sess, path_segs: tuple[str, ...]) -> dict: +def _get_property( + sess, + path_segs: tuple[str, ...], + timeout: float = 10.0, +) -> dict: """GET a plain OCF Property-map resource (a bare dict, not a Collection - batch). Used for `/multidevice/vs/0`: listed in `/oic/res` but absent - from `/device/0`'s batch, so it needs its own RETRIEVE.""" - body = _get_raw(sess, path_segs) + batch). Used for `/multidevice/vs/0` (issue #177 follow-up): listed in + `/oic/res` on the Pattern A reporter's board but absent from + `/device/0`'s batch, so it needs its own RETRIEVE, and it answers a + single Property map, not a [devcol-rep, ...] list.""" + body = _get_raw(sess, path_segs, timeout) return body if isinstance(body, dict) else {} @@ -255,6 +266,11 @@ def enumerate_subdevices( resources: dict[str, dict], oic_res_links, probe_log: Callable[[str, bool], None] | None = None, + *, + preferred_hrefs: Sequence[str] = (), + time_budget: float | None = None, + collection_timeout: float = 10.0, + property_timeout: float = 10.0, ) -> tuple[list[Subdevice], dict[str, dict]]: """Discover every sibling indoor subdevice reachable over `sess`'s connection. @@ -269,6 +285,13 @@ def enumerate_subdevices( or not it answered, so diagnostics can tell "checked, nothing there" apart from "never checked". + `preferred_hrefs` only changes the order of the flat Property fallback; + it never filters the device's resource surface. When `time_budget` is + supplied, probes are bounded by one shared monotonic deadline and this + returns every candidate/resource confirmed before it. This makes first + setup finite even when firmware silently drops unknown paths instead of + returning 4.04. + Every candidate whose seed answers with a non-empty batch is returned here -- this function can't tell a real sibling from an unused SmartThings slot that answers the same shape; that requires @@ -282,6 +305,30 @@ def enumerate_subdevices( # casing, and probing it twice would materialize the same physical # subdevice as two Subdevice candidates. probed_ids: set[str] = set() + deadline = time.monotonic() + max(0.0, time_budget) if time_budget is not None else None + budget_exhausted = False + + def _next_timeout(maximum: float) -> float | None: + """Clamp one probe to the remaining enumeration wall-clock budget.""" + nonlocal budget_exhausted + if deadline is None: + return maximum + remaining = deadline - time.monotonic() + if remaining <= 0: + budget_exhausted = True + return None + return min(maximum, remaining) + + def _flat_probe_hrefs(): + """Preferred live-state hrefs first, then every remaining master href.""" + seen = set() + for href in preferred_hrefs: + if href in resources and href not in seen: + seen.add(href) + yield href + for href in sorted(resources): + if href not in seen: + yield href def _probed(seed_href: str, batch: dict) -> None: if probe_log is not None: @@ -296,7 +343,10 @@ def enumerate_subdevices( return probed_ids.add(sub_id.lower()) seed = (sub_id, "device", "0") - batch = _get_batch(sess, seed) + timeout = _next_timeout(collection_timeout) + if timeout is None: + return + batch = _get_batch(sess, seed, timeout) _probed(_seed_href(seed), batch) if batch: subdevice = Subdevice(kind="prefixed", key=sub_id, seed_path=seed) @@ -307,10 +357,12 @@ def enumerate_subdevices( # doesn't always expose its own `//device/0` Collection. With # no Collection to seed from and no per-UUID entry in /oic/res to # enumerate hrefs from, the only signal left is that a composite - # device's siblings share the master's own resource surface -- so - # probe every href the master answered this cycle, individually, - # under this UUID's prefix, and keep whichever answer. Each is a - # plain tolerated-404 RETRIEVE. + # device's siblings are the same physical board family as the + # subdevice this config entry already talks to -- so probe every + # href the master itself answered this cycle, individually, under + # this UUID's prefix, and keep whichever ones answer before the + # optional enumeration deadline. Each is a plain tolerated-404 + # RETRIEVE, same posture as every other probe in this function. # # Known gap: a firmware that echoes the master's own state back # under an unrecognized prefix, rather than 4.04ing, would pass @@ -321,12 +373,15 @@ def enumerate_subdevices( # reps against the master's own values for the same hrefs. flat_hrefs = [] first = True - for href in sorted(resources): + for href in _flat_probe_hrefs(): if not first: sess.pace() first = False + timeout = _next_timeout(property_timeout) + if timeout is None: + break actual = f"/{sub_id}{href}" - rep = _get_property(sess, tuple(actual.strip("/").split("/"))) + rep = _get_property(sess, tuple(actual.strip("/").split("/")), timeout) _probed(actual, rep) if rep: flat_hrefs.append(href) @@ -353,6 +408,8 @@ def enumerate_subdevices( listed = sorted(i for i in ids if isinstance(i, str) and i) for sub_id in listed: _probe_prefixed(sub_id) + if budget_exhausted: + break # --- Pattern C: UUID prefix advertised only via /oic/res ---------------- # (AWM-WW-AID-26-ONEBODY washer+dryer combo, issue #241.) No @@ -374,6 +431,8 @@ def enumerate_subdevices( ) for sub_id in linked: _probe_prefixed(sub_id) + if budget_exhausted: + break # --- Pattern A: indexed siblings (ARTIK051_DONGLE_FAC_18K) -------------- indices = sorted( @@ -390,8 +449,11 @@ def enumerate_subdevices( # this replaces from identity.py. indices = list(_SPECULATIVE_DEVICE_INDICES) for n in indices: + timeout = _next_timeout(collection_timeout) + if timeout is None: + break seed = ("device", str(n)) - batch = _get_batch(sess, seed) + batch = _get_batch(sess, seed, timeout) _probed(_seed_href(seed), batch) if not batch: continue @@ -399,18 +461,26 @@ def enumerate_subdevices( fetched.update(batch) # already real /x/ hrefs, no normalization needed subdevices.append(subdevice) - # /multidevice/vs/0: listed in /oic/res on some boards but never in - # /device/0's batch, so it needs its own RETRIEVE. A plain corroborating - # count (numofsubdevice), confirmed read-only -- captured for - # diagnostics only, folded into the merged resources dict like any - # other href (see airconditioner._AC_IGNORED). Not a gate: - # discover_partitioned's entity-level liveness check decides - # materialization without it. + # /multidevice/vs/0 (issue #177 follow-up): the Pattern A reporter's + # board lists it in /oic/res but it never appears in /device/0's batch, + # so it needs its own RETRIEVE. It's a plain corroborating count + # (x.com.samsung.da.numofsubdevice), confirmed read-only (a write + # attempt returned CoAP 4.00) -- captured for diagnostics only, folded + # into the merged resources dict like any other href (see + # airconditioner._AC_IGNORED, which is what keeps it from surfacing as + # an unbound-href gap). NOT a gate: discover_partitioned's entity-level + # liveness check decides materialization correctly without it, and only + # this one board family is known to expose it at all. Whether it agrees + # with the number of subdevices actually materialized is the + # coordinator's call to log (it owns the logger; this module doesn't), + # not this function's. multidevice_seed = ("multidevice", "vs", "0") - multidevice = _get_property(sess, multidevice_seed) - _probed(_seed_href(multidevice_seed), multidevice) - if multidevice: - fetched["/multidevice/vs/0"] = multidevice + timeout = _next_timeout(property_timeout) + if timeout is not None: + multidevice = _get_property(sess, multidevice_seed, timeout) + _probed(_seed_href(multidevice_seed), multidevice) + if multidevice: + fetched["/multidevice/vs/0"] = multidevice return subdevices, fetched diff --git a/custom_components/localthings/select.py b/custom_components/localthings/select.py index a58abcb..2ca918a 100644 --- a/custom_components/localthings/select.py +++ b/custom_components/localthings/select.py @@ -49,7 +49,7 @@ def _translation_state(value: str, known: frozenset[str]) -> str | None: return snake if snake in known else None -def _display(value, translation_key: str | None): +def _display(value, translation_key: str | None, fallback_fn=None): """Turn a raw device option/state value into what's shown in the UI. `translation_key` is the entity's already-resolved key (it can itself @@ -70,11 +70,19 @@ def _display(value, translation_key: str | None): if translation_key: known = translated_states("select", translation_key) if not known: - # No state table for this key: either untranslated, or its - # options deliberately aren't (an unrecognized course table). - return value - if translated := _translation_state(value, known): + # No state table for this key: either the entity isn't translated + # at all, or its name is translated but its options deliberately + # aren't (an unrecognized course table, say). Give an explicit + # device-specific fallback the opportunity to make an opaque value + # readable; otherwise the raw device value remains the best choice. + if fallback_fn is None: + return value + elif translated := _translation_state(value, known): return translated + if fallback_fn is not None: + fallback = fallback_fn(value) + if fallback is not None: + return fallback if value.islower(): return value.replace("_", " ").title() return _CAMEL_BOUNDARY_RE.sub(" ", value) @@ -85,7 +93,15 @@ class LocalThingsSelect(LocalThingsEntity, SelectEntity): super().__init__(coordinator, bound) desc = cast(SelectDesc, bound.desc) if not desc.options_field and not callable(desc.options): - self._attr_options = [_display(o, self.translation_key) for o in desc.options] + self._attr_options = [self._display_option(o) for o in desc.options] + + def _display_option(self, value): + """Normalize both current state and options through one path.""" + display_fn = cast(SelectDesc, self._bound.desc).display_fn + fallback_fn = ( + (lambda raw: display_fn(raw, self._resources)) if display_fn is not None else None + ) + return _display(value, self.translation_key, fallback_fn) def _raw_options(self) -> list[str]: desc = cast(SelectDesc, self._bound.desc) @@ -106,17 +122,17 @@ class LocalThingsSelect(LocalThingsEntity, SelectEntity): def options(self) -> list[str]: desc = cast(SelectDesc, self._bound.desc) if desc.options_field or callable(desc.options): - return [_display(o, self.translation_key) for o in self._raw_options()] + return [self._display_option(o) for o in self._raw_options()] return self._attr_options @property def current_option(self): raw = (self.coordinator.data or {}).get(self._state_key) - return _display(raw, self.translation_key) + return self._display_option(raw) async def async_select_option(self, option: str) -> None: raw = next( - (o for o in self._raw_options() if _display(o, self.translation_key) == option), + (o for o in self._raw_options() if self._display_option(o) == option), option, ) await self.coordinator.async_send_command(self._bound, raw) diff --git a/custom_components/localthings/translations/cs.json b/custom_components/localthings/translations/cs.json index 905178d..e83ea00 100644 --- a/custom_components/localthings/translations/cs.json +++ b/custom_components/localthings/translations/cs.json @@ -297,7 +297,16 @@ "07": "Předoplach", "8d": "Hrnce a pánve", "8e": "Plast", - "8f": "Dětské potřeby" + "8f": "Dětské potřeby", + "82": "Automatický", + "8a": "Normální", + "a7": "Intenzivní", + "a8": "Expresní", + "8c": "Extra tichý", + "88": "Samočištění", + "85": "Jemné", + "0c": "Express", + "0d": "Samočištění" } }, "dispense_type": { @@ -339,7 +348,9 @@ "4c": "Osvěžení vzduchem", "51": "Eko bavlna", "53": "AI sušení+", - "4e": "Automatické sušení" + "4e": "Automatické sušení", + "26": "Provětrání", + "2a": "Hygienické sušení+" } }, "favorite_capacity": { @@ -621,14 +632,33 @@ "60": "Samočištění+", "65": "Barevné prádlo", "66": "Džíny", + "69": "AI praní", + "6a": "Vlna", + "6b": "Džíny", + "6c": "Halenky", + "6d": "Jemné prádlo", + "6e": "Sportovní oblečení", + "6f": "Ložní prádlo", + "70": "Ručníky", + "71": "Rychlé praní", + "72": "Košile", + "73": "Dezinfekce", + "74": "Čištění bubnu", + "75": "Outdoor", + "76": "Dětské potřeby", + "77": "Bavlna", + "78": "Máchání + odstřeďování", + "79": "Pouze odstřeďování", "7c": "Bílé prádlo", "7d": "Ložní prádlo/nepromokavé", "7e": "Samočištění", "7f": "Vlna/jemné", "86": "Hloubkové praní", "87": "Stažený program", + "88": "Péče o domácí mazlíčky", "8f": "Intenzivní studená", - "96": "Méně mikrovláken" + "96": "Méně mikrovláken", + "35": "Eko bavlna" } }, "washer_dry_level": { diff --git a/custom_components/localthings/translations/de.json b/custom_components/localthings/translations/de.json new file mode 100644 index 0000000..910e2d8 --- /dev/null +++ b/custom_components/localthings/translations/de.json @@ -0,0 +1,1476 @@ +{ + "entity": { + "binary_sensor": { + "auto_clean_running": { + "name": "Automatische Reinigung läuft" + }, + "after_run_active": { + "name": "Nachlauf aktiv" + }, + "any_burner_active": { + "name": "Ein Brenner aktiv" + }, + "automatic_operation": { + "name": "Automatikbetrieb" + }, + "battery_charging": { + "name": "Wird geladen" + }, + "burner_hot_surface": { + "name": "Brenner {number} heiße Oberfläche" + }, + "burner_pan_detected": { + "name": "Brenner {number} Topf erkannt" + }, + "child_lock": { + "name": "Kindersicherung" + }, + "cloud_connected": { + "name": "Cloud verbunden" + }, + "dustbag_full": { + "name": "Staubbeutel voll" + }, + "cooktop_power": { + "name": "Kochfeld-Leistung" + }, + "cooktop_safety_shutoff_enabled": { + "name": "Automatische Abschaltung bei heißer Oberfläche aktiviert" + }, + "current_limit_enabled": { + "name": "Strombegrenzung aktiviert" + }, + "overload_protection_active": { + "name": "Überlastschutz aktiv" + }, + "odor_controller_active": { + "name": "Geruchskontrolle aktiv" + }, + "cycle_active": { + "name": "Läuft" + }, + "defrost_active": { + "name": "Abtauen aktiv" + }, + "detergent_low": { + "name": "Waschmittel niedrig" + }, + "device_active": { + "name": "Gerät aktiv" + }, + "door_open": { + "name": "Tür" + }, + "filter_door_status": { + "name": "Filtertür" + }, + "firmware_update": { + "name": "Firmware-Update verfügbar" + }, + "instance_open": { + "name": "{instance_name} offen" + }, + "paired_hood_connected": { + "name": "Gekoppelte Dunstabzugshaube verbunden" + }, + "paired_hood_light": { + "name": "Licht der gekoppelten Dunstabzugshaube" + }, + "paired_hood_power": { + "name": "Strom der gekoppelten Dunstabzugshaube" + }, + "periodic_air_sensing": { + "name": "Periodische Luftmessung" + }, + "alarm_in_mute": { + "name": "Alarm stummgeschaltet" + }, + "power_state": { + "name": "Betriebszustand" + }, + "probe_connected": { + "name": "Fühler verbunden" + }, + "power_switch": { + "name": "Betrieb" + }, + "rapid_freezing": { + "name": "Schnellgefrieren" + }, + "rapid_fridge": { + "name": "Schnellkühlen" + }, + "softener_low": { + "name": "Weichspüler niedrig" + }, + "pouring": { + "name": "Ausgabe läuft" + }, + "remote_control": { + "name": "Intelligente Steuerung" + } + }, + "button": { + "after_run_cancel": { + "name": "Nachlauf abbrechen" + }, + "diagnosis_start": { + "name": "Diagnose starten" + }, + "auto_clean_stop": { + "name": "Automatische Reinigung stoppen" + }, + "filter_time_reset": { + "name": "Filterzeit zurücksetzen" + }, + "pause": { + "name": "Pause" + }, + "selfcheck_start": { + "name": "Selbsttest starten" + }, + "start": { + "name": "Start" + }, + "stop": { + "name": "Stopp" + } + }, + "climate": { + "airconditioner": { + "state_attributes": { + "preset_mode": { + "state": { + "ai_comfort": "KI-Komfort", + "quiet": "Leise", + "speed": "Schnell", + "nanosleep": "WindFree Schlaf", + "longwind": "Langer Luftstrom", + "motionindirect": "Bewegung indirekt", + "motiondirect": "Bewegung direkt", + "drycomfort": "Trockenkomfort", + "2step": "2-Stufen", + "smart": "Intelligent", + "nano": "WindFree", + "sleep": "Schlaf", + "dlightcool": "d'light Cool" + } + }, + "fan_mode": { + "state": { + "turbo": "Turbo", + "max": "Max" + } + } + } + } + }, + "fan": { + "air_purifier_fan": { + "state_attributes": { + "preset_mode": { + "state": { + "mid": "Mittel", + "sleep": "Schlaf", + "smart": "Intelligent", + "max": "Max", + "windfree": "WindFree" + } + } + } + } + }, + "number": { + "cook_time": { + "name": "Kochzeit" + }, + "delay_start_hours": { + "name": "Startverzögerung" + }, + "dispense_capacity": { + "name": "Ausgabemenge" + }, + "instance_setpoint": { + "name": "{instance_name} Sollwert" + }, + "oven_setpoint": { + "name": "Sollwert" + }, + "sensing_interval": { + "name": "KI-Reinigungsintervall" + }, + "setpoint": { + "name": "Sollwert" + }, + "sound_volume": { + "name": "Lautstärke" + }, + "target_humidity": { + "name": "Ziel-Luftfeuchtigkeit" + }, + "tropical_night_mode": { + "name": "Tropennacht-Modus" + }, + "zone_target_temperature": { + "name": "Zonen-Zieltemperatur" + }, + "good_sleep": { + "name": "Guter Schlaf" + } + }, + "select": { + "ai_energy_level": { + "name": "KI-Energiesparstufe" + }, + "air_filter_threshold": { + "name": "Filteralarm-Schwellenwert" + }, + "beverage_zone_mode": { + "name": "Getränkezonen-Modus", + "state": { + "sp_ttype_beer_drinks": "Getränke", + "sp_ttype_wine_dessert": "Wein und Dessert" + } + }, + "brightness_level": { + "name": "Nachthelligkeit", + "state": { + "33": "Niedrig", + "66": "Mittel", + "100": "Hoch" + } + }, + "cooler_temperature_setpoint": { + "name": "Kühlertemperatur" + }, + "day_brightness": { + "name": "Schrankhelligkeit", + "state": { + "33": "Niedrig", + "66": "Mittel", + "100": "Hoch" + } + }, + "buzzer_sound": { + "name": "Signalton", + "state": { + "off": "Aus", + "on": "Ein" + } + }, + "discharging_time": { + "name": "Entladezeit", + "state": { + "1": "1 Minute", + "3": "3 Minuten" + } + }, + "cycle": { + "name": "Programm" + }, + "detergent_quantity": { + "name": "Waschmittelmenge", + "state": { + "00": "Keine", + "01": "Niedrig", + "02": "Mittel", + "03": "Hoch" + } + }, + "detergent_water_hardness": { + "name": "Wasserhärte für Waschmittel", + "state": { + "01": "Weich", + "02": "Mittel", + "03": "Hart" + } + }, + "dishwasher_cycle": { + "name": "Programm", + "state": { + "80": "Schonprogramm", + "83": "Normal", + "84": "Intensiv", + "86": "Express 60", + "90": "Selbstreinigung", + "0e": "KI-Spülen", + "07": "Vorspülen", + "8d": "Töpfe und Pfannen", + "8e": "Plastik", + "8f": "Babypflege", + "82": "Automatik", + "8a": "Normal", + "a7": "Intensiv", + "a8": "Express", + "8c": "Extra leise", + "88": "Selbstreinigung", + "85": "Schonprogramm", + "0c": "Express", + "0d": "Selbstreinigung" + } + }, + "dispense_type": { + "name": "Ausgabetyp" + }, + "door_alert": { + "name": "Türalarm", + "state": { + "1": "Alarm 1", + "2": "Alarm 2", + "3": "Alarm 3", + "4": "Alarm 4" + } + }, + "dryer_cycle_table_03": { + "name": "Programm", + "state": { + "01": "Normal", + "06": "Zeittrocknen", + "16": "Baumwolle", + "18": "Synthetik", + "19": "Feinwäsche", + "20": "Bügeltrocken", + "21": "Hygienepflege", + "22": "Leise Trocknen", + "23": "Schnelltrocknen 35'", + "24": "Kaltluft", + "25": "Warmluft", + "27": "Zeittrocknen", + "29": "KI-Trocknen", + "1a": "Wolle", + "1b": "Bettwäsche", + "1c": "Hemden", + "1d": "Handtücher", + "1e": "Outdoor", + "1f": "Mischwäsche", + "2b": "Selbst-Trommeltrocknung", + "4c": "Luftauffrischung", + "51": "Eco Baumwolle", + "53": "KI-Trocknen+", + "4e": "Selbsttrocknung", + "17": "Super Speed", + "26": "Frischluft", + "2a": "Hygienepflege+" + } + }, + "favorite_capacity": { + "name": "Favoritenmenge" + }, + "favorite_hotwater_temperature": { + "name": "Favoriten-Warmwassertemperatur" + }, + "filter_alarm_time": { + "name": "Filteralarm-Intervall", + "state": { + "180": "180 Stunden", + "300": "300 Stunden", + "500": "500 Stunden", + "700": "700 Stunden" + } + }, + "finish_sound": { + "name": "Fertigmeldung", + "state": { + "off": "Aus", + "on": "Ein" + } + }, + "flex_zone_mode": { + "name": "Flex-Zonen-Modus", + "state": { + "cv_ttype_rf9000a_freeze": "Gefrieren", + "cv_ttype_rf9000a_softfreeze": "Sanftgefrieren", + "cv_ttype_rf9000a_meat_fish": "Fleisch/Fisch", + "cv_ttype_rf9000a_fruit_veggies": "Obst & Gemüse", + "cv_ttype_rf9000a_beverage": "Getränke", + "cv_fdr_wine": "Wein", + "cv_fdr_deli": "Feinkost", + "cv_fdr_beverage": "Getränke", + "cv_fdr_meat": "Fleisch", + "cv_fdr_soft_freezer": "Sanftgefrierfach" + } + }, + "heated_dry": { + "name": "Smart-Trocknen", + "state": { + "off": "Aus", + "low": "Niedrig", + "high": "Hoch", + "extra_high": "Extra hoch" + } + }, + "hot_water_temperature": { + "name": "Warmwassertemperatur" + }, + "ice_type": { + "name": "{instance_name} Typ", + "state": { + "off": "Aus", + "whiskey_iceball_3": "3 Kugeln/Tag", + "whiskey_iceball_6": "6 Kugeln/Tag", + "whiskey_iceball_9": "9 Kugeln/Tag" + } + }, + "led_brightness": { + "name": "Tür-LED-Helligkeit", + "state": { + "low": "Niedrig", + "high": "Hoch" + } + }, + "led_night_brightness": { + "name": "Tür-LED-Nachthelligkeit", + "state": { + "low": "Niedrig", + "high": "Hoch" + } + }, + "cooking_mode": { + "name": "Kochmodus", + "state": { + "no_operation": "Kein Betrieb", + "micro_wave": "Mikrowelle", + "micro_wave_grill": "Mikrowelle + Grill", + "micro_wave_convection": "Mikrowelle + Umluft", + "convection": "Umluft", + "air_fryer": "Heißluftfritteuse", + "grill": "Grill", + "autocook": "Automatikgaren", + "autocook_custom": "Automatikgaren (angepasst)", + "deodorization": "Geruchsentfernung", + "keep_warm": "Warmhalten" + } + }, + "oven_mode": { + "name": "Kochmodus", + "state": { + "no_operation": "Kein Betrieb", + "bake": "Backen", + "broil": "Grillen", + "convection": "Umluft", + "convection_bake": "Umluftbacken", + "convection_broil": "Umluftgrillen", + "frozen_pizza_plus": "Tiefkühlpizza+", + "slow_cook": "Langsam garen", + "plate_warm": "Tellerwärmen", + "air_fry": "Heißluftfritteuse" + } + }, + "operating_mode": { + "name": "Betriebsmodus" + }, + "pantry_zone_mode": { + "name": "Vorratszonen-Modus", + "state": { + "fdr_wine": "Wein", + "fdr_deli": "Feinkost", + "fdr_drinks": "Getränke" + } + }, + "range_burner_power_level": { + "name": "Brenner {number} Leistungsstufe", + "state": { + "0": "Aus", + "boost": "Boost", + "simmer": "Köcheln" + } + }, + "range_hood_lamp_brightness": { + "name": "Lampenhelligkeit", + "state": { + "1": "Stufe 1", + "2": "Stufe 2" + } + }, + "rinse_cycles": { + "name": "Spülgänge" + }, + "sensing_mode": { + "name": "KI-Reinigungsaktion", + "state": { + "off": "Nur Messung", + "airpurify": "Automatische Reinigung", + "alarm": "Benachrichtigung erhalten" + } + }, + "softener_concentration": { + "name": "Weichspülerkonzentration", + "state": { + "01": "1x", + "02": "2x", + "03": "3x" + } + }, + "softener_quantity": { + "name": "Weichspülermenge", + "state": { + "00": "Keine", + "01": "Niedrig", + "02": "Mittel", + "03": "Hoch" + } + }, + "sound_mode": { + "name": "Tonmodus", + "state": { + "voice": "Sprache", + "tone": "Ton", + "mute": "Stumm" + } + }, + "air_purifier_sound_mode": { + "name": "Tonmodus", + "state": { + "mute": "Stumm", + "buzzer": "Signalton" + } + }, + "water_purifier_sound_mode": { + "name": "Tonmodus", + "state": { + "voice": "Sprache", + "fixedtone": "Fester Ton", + "mute": "Stumm" + } + }, + "spin_speed": { + "name": "Schleuderdrehzahl", + "state": { + "rinse_hold": "Spülstopp", + "no_spin": "Ohne Schleudern", + "low": "Niedrig", + "medium": "Mittel", + "high": "Hoch" + } + }, + "wash_temperature": { + "name": "Waschtemperatur", + "state": { + "none": "Keine", + "cold": "Kalt", + "cool": "Kühl", + "warm": "Warm", + "hot": "Heiß", + "extra_hot": "Extra heiß" + } + }, + "washer_cycle_table_02": { + "name": "Programm", + "state": { + "01": "Normal", + "04": "Schnellwäsche", + "1b": "Baumwolle", + "1c": "Eco 40-60", + "1d": "Super Speed", + "1e": "Schnelle Wäsche 15'", + "1f": "Kaltwäsche Intensiv", + "20": "Hygiene-Dampf", + "21": "Buntwäsche", + "22": "Wolle", + "23": "Outdoor", + "24": "Handtücher", + "25": "Pflegeleicht", + "26": "Feinwäsche", + "27": "Spülen + Schleudern", + "28": "Abpumpen + Schleudern", + "29": "Trommelreinigung+", + "2a": "Jeans", + "2b": "KI-Wäsche", + "2d": "Super Leise", + "2e": "Babypflege", + "2f": "Sportkleidung", + "30": "Bewölkter Tag", + "32": "Hemden", + "33": "XXL-Wäsche", + "34": "Mischwäsche", + "36": "Waschen + Trocknen", + "37": "Air Wash", + "38": "Trocknen Baumwolle", + "39": "Trocknen Pflegeleicht", + "3a": "Trommelreinigung", + "52": "Eco Kalt", + "53": "Intensiv", + "54": "Handtücher", + "55": "Sportkleidung", + "57": "Feinwäsche", + "5e": "Spülen + Schleudern", + "60": "Selbstreinigung+", + "65": "Buntwäsche", + "66": "Jeansstoff", + "7c": "Weißwäsche", + "7d": "Bettwäsche/Wasserdicht", + "7e": "Selbstreinigung", + "7f": "Wolle/Feinwäsche", + "86": "Tiefenreinigung", + "87": "Download", + "8f": "Kaltwäsche Intensiv", + "96": "Weniger Mikrofasern", + "17": "Heruntergeladen", + "69": "KI-Wäsche", + "6a": "Wolle", + "6b": "Jeansstoff", + "6c": "Blusen", + "6d": "Feinwäsche", + "6e": "Sportkleidung", + "6f": "Bettwäsche", + "70": "Handtücher", + "71": "Schnellwäsche", + "72": "Hemden", + "73": "Hygienespülung", + "74": "Trommelreinigung", + "75": "Outdoor", + "76": "Babypflege", + "77": "Baumwolle", + "78": "Spülen + Schleudern", + "79": "Nur Schleudern", + "88": "Tierpflege", + "35": "Eco Baumwolle" + } + }, + "washer_dry_level": { + "name": "Trockenstufe", + "state": { + "30": "30 Min", + "60": "1 Std", + "90": "1 Std 30", + "120": "2 Std", + "180": "3 Std", + "240": "4 Std", + "none": "Aus", + "cupboard": "Schranktrocken" + } + }, + "zone_mode": { + "name": "Zonenmodus" + }, + "watertank_light_color": { + "name": "Wassertank-Lichtfarbe", + "state": { + "warmwhite": "Warmweiß", + "naturalwhite": "Neutralweiß", + "coolwhite": "Kaltweiß", + "green": "Grün", + "purple": "Violett", + "blue": "Blau", + "orange": "Orange", + "pink": "Pink" + } + }, + "watertank_light_brightness": { + "name": "Wassertank-Lichthelligkeit" + }, + "air_filter_pm1_threshold": { + "name": "PM1-Filteralarm-Schwellenwert" + }, + "freezer_temperature_setpoint": { + "name": "Gefrierschranktemperatur" + }, + "kimchi_zone_mode": { + "name": "{instance_name} Lagermodus", + "state": { + "off": "Aus", + "kimchi_storage_normal": "Kimchi", + "kimchi_storage_cold": "Kimchi, stark", + "kimchi_storage_warm": "Kimchi, schwach", + "kimchi_storage_low_salt_normal": "Kimchi mit wenig Salz", + "kimchi_storage_low_salt_cold": "Kimchi mit wenig Salz, stark", + "kimchi_storage_low_salt_warm": "Kimchi mit wenig Salz, schwach", + "kimchi_storage_crunfch": "Knackiges Kimchi", + "kimchi_storage_buy": "Gekauftes Kimchi", + "storage_fridge_normal": "Kühlen", + "storage_fridge_cold": "Kühlen, stark", + "storage_fridge_warm": "Kühlen, schwach", + "storage_freezer_normal": "Gefrieren", + "storage_freezer_cold": "Gefrieren, stark", + "storage_freezer_warm": "Gefrieren, schwach", + "kimchi_ripe_low_temp": "Kimchi-Reifung, niedrige Temperatur", + "kimchi_ripe_normal_temp": "Kimchi-Reifung, Raumtemperatur", + "kimchi_ripe_kkakdugi": "Kkakdugi-Reifung", + "kimchi_ripe_dongchimi": "Dongchimi-Reifung", + "meat_ripe_normal": "Fleischreifung", + "storage_meat": "Fleisch und Fisch", + "storage_fridge_vegetables_fruit": "Obst und Gemüse", + "storage_fresh_cereal": "Getreide", + "storage_fridge_drink": "Getränke", + "storage_fresh_wine": "Wein", + "storage_fresh_potato_banana": "Kartoffeln und Bananen" + } + }, + "energy_saving_mode": { + "name": "Energiesparmodus" + }, + "edge_lighting_mode": { + "name": "Randbeleuchtungsmodus", + "state": { + "smart": "Intelligent", + "high": "Hoch", + "low": "Niedrig" + } + }, + "edge_lighting_color": { + "name": "Randbeleuchtungsfarbe", + "state": { + "3000k": "3000 K", + "4000k": "4000 K", + "6500k": "6500 K" + } + }, + "indicator_light_mode": { + "name": "Kontrollleuchtenmodus", + "state": { + "smart": "Intelligent", + "high": "Hoch", + "low": "Niedrig" + } + }, + "ventilation_mode": { + "name": "Modus", + "state": { + "purification": "Reinigung", + "ventilation": "Lüftung", + "smartventilation": "Intelligente Lüftung" + } + } + }, + "sensor": { + "auto_clean_progress": { + "name": "Fortschritt automatische Reinigung" + }, + "after_run_progress": { + "name": "Nachlauf-Fortschritt" + }, + "air_filter_usage": { + "name": "Filternutzung" + }, + "air_filter_usage_hours": { + "name": "Filternutzungsstunden" + }, + "air_quality_standard": { + "name": "Luftqualitätsstandard" + }, + "air_sensing_state": { + "name": "Luftmesszustand" + }, + "alarm_code": { + "name": "Alarmcode" + }, + "auto_ventilation_action": { + "name": "Automatische Lüftungsaktion" + }, + "automatic_ventilation_state": { + "name": "Automatischer Lüftungszustand" + }, + "battery": { + "name": "Akku" + }, + "burner_state": { + "name": "Brenner {number} Zustand" + }, + "clean_level": { + "name": "Reinigungsstufe" + }, + "co2": { + "name": "CO2" + }, + "coffee_brew_status": { + "name": "Kaffeebrühstatus" + }, + "connection_mode": { + "name": "Verbindungsmodus", + "state": { + "observe": "Push (Beobachten)", + "poll": "Abfrage" + } + }, + "cooktop_running_state": { + "name": "Kochfeld-Laufzustand" + }, + "cooktop_state": { + "name": "Kochfeldzustand" + }, + "current_limit_level": { + "name": "Strombegrenzungsstufe" + }, + "filter_time": { + "name": "Filterzeit" + }, + "hepa_filter_usage": { + "name": "HEPA-Filternutzung" + }, + "outdoor_temperature": { + "name": "Außentemperatur" + }, + "odor_controller_progress": { + "name": "Fortschritt Geruchskontrolle" + }, + "panel_status": { + "name": "Bedienfeldstatus" + }, + "sound_output": { + "name": "Tonausgabe" + }, + "dustbag_usage": { + "name": "Staubbeutelnutzung" + }, + "cleanstation_status": { + "name": "Reinigungsstation-Status" + }, + "stick_status": { + "name": "Stick-Status" + }, + "uvc_operation_time": { + "name": "UV-C Betriebszeit" + }, + "uvc_total_operation_time": { + "name": "UV-C Gesamtbetriebszeit" + }, + "uvc_finished_time": { + "name": "UV-C Endzeit" + }, + "uvc_emitted_time": { + "name": "UV-C Bestrahlungszeit" + }, + "overload_protection_mode": { + "name": "Überlastschutzmodus", + "state": { + "alarm": "Alarm", + "powersaving": "Energiesparen" + } + }, + "absence_power_saving_mode": { + "name": "Abwesenheits-Energiesparmodus", + "state": { + "eco": "Eco", + "normal": "Normal", + "comfort": "Komfort" + } + }, + "motion_detect_wind_mode": { + "name": "Bewegungserkennung-Windmodus", + "state": { + "direct": "Direkt", + "indirect": "Indirekt" + } + }, + "current_temp_c": { + "name": "Temperatur" + }, + "current_temperature_c": { + "name": "Temperatur" + }, + "diagnosis": { + "name": "Diagnose" + }, + "diagnosis_status": { + "name": "Diagnosestatus" + }, + "drum_clean_cycles_remaining": { + "name": "Trommelreinigung fällig in" + }, + "drum_clean_last_cleaned": { + "name": "Trommel zuletzt gereinigt" + }, + "dry_level": { + "name": "Trockenstufe" + }, + "dry_time": { + "name": "Trockenzeit" + }, + "dryer_type": { + "name": "Trocknertyp" + }, + "dust": { + "name": "Staub" + }, + "energy_kwh": { + "name": "Energie" + }, + "energy_last_month_kwh": { + "name": "Energie (letzter Monat)" + }, + "energy_saved_kwh": { + "name": "Eingesparte Energie" + }, + "energy_this_month_kwh": { + "name": "Energie (dieser Monat)" + }, + "fan_direction": { + "name": "Lüfterrichtung" + }, + "fan_speed_level": { + "name": "Lüfterstufe" + }, + "filter_clean_remain_time": { + "name": "Verbleibende Zeit bis Filterreinigung" + }, + "filter_progress": { + "name": "Filterfortschritt" + }, + "filter_usage": { + "name": "Filternutzung" + }, + "fine_dust": { + "name": "Feinstaub" + }, + "finish_time": { + "name": "Voraussichtliches Ende" + }, + "completion_minutes": { + "name": "Fertigstellungszeit" + }, + "freezer_temperature": { + "name": "Gefrierschranktemperatur" + }, + "fridge_temperature": { + "name": "Kühlschranktemperatur" + }, + "kimchi_ripening_status": { + "name": "{instance_name} Reifestatus" + }, + "kimchi_ripening_remaining": { + "name": "{instance_name} verbleibende Reifezeit" + }, + "kimchi_rack_count": { + "name": "{instance_name} Fachanzahl" + }, + "hood_filter_capacity": { + "name": "Filterkapazität" + }, + "humidity": { + "name": "Luftfeuchtigkeit" + }, + "hood_filter_usage": { + "name": "Filternutzung" + }, + "instance_temperature": { + "name": "{instance_name} Temperatur" + }, + "job_beginning_status": { + "name": "Startstatus" + }, + "last_air_sensing_level": { + "name": "Letzte Luftmessstufe" + }, + "last_air_sensing_time": { + "name": "Letzte Luftmesszeit" + }, + "main_timer_current": { + "name": "Aktueller Timerwert" + }, + "main_timer_state": { + "name": "Timerzustand" + }, + "odor": { + "name": "Geruch" + }, + "operating_mode": { + "name": "Betriebsmodus" + }, + "operation_origin": { + "name": "Letzte Bedienquelle" + }, + "operation_time_minutes": { + "name": "Betriebszeit" + }, + "oven_state": { + "name": "Ofenzustand" + }, + "cavity_state": { + "name": "Garraumzustand" + }, + "power_level": { + "name": "Leistungsstufe" + }, + "paired_hood_fan_speed": { + "name": "Lüfterstufe der gekoppelten Dunstabzugshaube" + }, + "paired_hood_firmware": { + "name": "Firmware der gekoppelten Dunstabzugshaube" + }, + "paired_hood_model": { + "name": "Modell der gekoppelten Dunstabzugshaube" + }, + "power_energy_kwh": { + "name": "Energieverbrauch" + }, + "power_watts": { + "name": "Leistung" + }, + "probe_battery": { + "name": "Fühlerakku" + }, + "probe_target_temperature": { + "name": "Fühler-Zieltemperatur" + }, + "probe_temperature": { + "name": "Fühlertemperatur" + }, + "progress": { + "name": "Fortschritt" + }, + "progress_percentage": { + "name": "Fortschritt in Prozent" + }, + "selfcheck_error": { + "name": "Selbsttestfehler" + }, + "selfcheck_result": { + "name": "Selbsttestergebnis" + }, + "selfcheck_status": { + "name": "Selbstteststatus" + }, + "sterilize_last_time": { + "name": "Letzte Sterilisation" + }, + "sterilize_period": { + "name": "Sterilisationsintervall" + }, + "sterilize_plan_time": { + "name": "Geplante Sterilisationszeit" + }, + "sterilize_run_time": { + "name": "Sterilisationslaufzeit" + }, + "super_fine_dust": { + "name": "Superfeinstaub" + }, + "warming_center_state": { + "name": "Wärmezentrum-Zustand" + }, + "water_liters": { + "name": "Wasserverbrauch" + }, + "waterpurifier_status": { + "name": "Status" + }, + "cup_state": { + "name": "Tassenzustand" + }, + "last_pour_type": { + "name": "Letzter Ausgabetyp" + }, + "last_pour_capacity": { + "name": "Letzte Ausgabemenge" + }, + "machine_state": { + "name": "Maschinenzustand", + "state": { + "idle": "Leerlauf", + "active": "Aktiv", + "pause": "Pausiert" + } + }, + "filter_status": { + "name": "Filterstatus", + "state": { + "normal": "Normal", + "wash": "Waschen", + "replace": "Ersetzen" + } + }, + "ice_making_status": { + "name": "{instance_name} Herstellstatus", + "state": { + "icestatus_stop": "Leerlauf", + "icestatus_run": "Eisbereitung" + } + }, + "zone_temperature": { + "name": "Zonentemperatur" + }, + "watertank_full_alarm_status": { + "name": "Wassertank-voll-Alarmstatus" + }, + "air_filter_pm1_usage": { + "name": "PM1-Filternutzung" + }, + "air_filter_pm1_usage_hours": { + "name": "PM1-Filternutzungsstunden" + }, + "energy_saving_state": { + "name": "Energiesparstatus" + }, + "energy_saving_operating_status": { + "name": "Energiespar-Betriebsstatus" + } + }, + "switch": { + "ai_energy_level": { + "name": "KI-Energiesparmodus" + }, + "air_monitoring": { + "name": "Luftüberwachung" + }, + "air_purify": { + "name": "Luftreinigung" + }, + "auto_clean": { + "name": "Automatische Reinigung" + }, + "absence_power_saving_active": { + "name": "Abwesenheits-Energiesparmodus aktiv" + }, + "motion_detect_wind_active": { + "name": "Bewegungserkennung Windvermeidung aktiv" + }, + "beep": { + "name": "Signalton" + }, + "display": { + "name": "Display" + }, + "periodic_air_sensing": { + "name": "KI-Reinigung" + }, + "periodic_sensing_skip_status": { + "name": "KI-Reinigung Messpause" + }, + "pet_filter_activation": { + "name": "Tierhaarfilter-Aktivierung" + }, + "auto_empty": { + "name": "Automatische Entleerung" + }, + "dustbin_auto_close": { + "name": "Staubbehälter automatisch schließen" + }, + "spi": { + "name": "Reinigungsionen" + }, + "uvc_intensive_mode": { + "name": "UV-C Intensivmodus" + }, + "auto_door_opener": { + "name": "Automatischer Türöffner" + }, + "auto_release_dry": { + "name": "Automatische Trocknungsfreigabe" + }, + "autofill": { + "name": "Krug automatisch befüllen" + }, + "buzz_lock": { + "name": "Signaltonsperre" + }, + "cabinet_light_dim": { + "name": "Allmählich aufhellen" + }, + "cabinet_light_switch": { + "name": "Schrankbeleuchtung" + }, + "child_lock": { + "name": "Kindersicherung" + }, + "coldwater_lock": { + "name": "Kaltwassersperre" + }, + "cooktop_child_lock": { + "name": "Kindersicherung" + }, + "defrost_delay": { + "name": "Abtauverzögerung" + }, + "display_light": { + "name": "Displaybeleuchtung" + }, + "dnd": { + "name": "Nicht stören" + }, + "fast_preheat": { + "name": "Schnellvorheizen" + }, + "favorite_capacity_enabled": { + "name": "Favoritenmenge" + }, + "favorite_coffee_enabled": { + "name": "Favoritenkaffee" + }, + "filter_remind": { + "name": "Filtererinnerung" + }, + "fridge_sound": { + "name": "Ton" + }, + "hotwater_lock": { + "name": "Warmwassersperre" + }, + "ice_maker_enabled": { + "name": "Eisbereiter" + }, + "ice_night_mode": { + "name": "Nachtruhemodus Eisbereitung" + }, + "instance_enabled": { + "name": "{instance_name} aktiviert" + }, + "intensive": { + "name": "Intensiv" + }, + "lamp": { + "name": "Lampe" + }, + "led_night_light": { + "name": "Tür-LED-Nachtlicht" + }, + "mute_once": { + "name": "Einmal stummschalten" + }, + "natural_steam": { + "name": "Natürlicher Dampf" + }, + "energy_saving": { + "name": "Energiesparen" + }, + "cooktop_on_alert": { + "name": "Kochfeld-Ein-Warnung" + }, + "power_switch": { + "name": "Ein/Aus" + }, + "pre_wash": { + "name": "Vorwäsche" + }, + "rapid_freezing": { + "name": "Schnellgefrieren" + }, + "rapid_fridge": { + "name": "Schnellkühlen" + }, + "remind_beep": { + "name": "Signalton-Erinnerung am Ende" + }, + "sabbath_mode": { + "name": "Sabbat-Modus" + }, + "sanitize": { + "name": "Hygienespülung" + }, + "sound": { + "name": "Ton" + }, + "welcome_lighting": { + "name": "Willkommensbeleuchtung" + }, + "wrinkle_prevent": { + "name": "Knitterschutz" + }, + "zone_power": { + "name": "Zonen-Ein/Aus" + }, + "away_mode": { + "name": "Abwesenheitsmodus" + }, + "watertank_light": { + "name": "Wassertankbeleuchtung" + }, + "uv_led": { + "name": "UV-LED" + }, + "ventilation_alarm": { + "name": "Lüftungsalarm" + }, + "absence_clean": { + "name": "Abwesenheitsreinigung" + }, + "bubble_soak": { + "name": "Blasen-Einweichen" + }, + "storm_wash": { + "name": "Storm Wash+" + }, + "edge_lighting": { + "name": "Randbeleuchtung" + }, + "indicator_light": { + "name": "Kontrollleuchte" + }, + "windfree": { + "name": "WindFree-Modus" + }, + "windsleep": { + "name": "Schlafmodus" + } + }, + "time": { + "dnd_end": { + "name": "Nicht-stören-Ende" + }, + "dnd_start": { + "name": "Nicht-stören-Start" + }, + "led_night_end": { + "name": "Tür-LED-Nacht Ende" + }, + "led_night_start": { + "name": "Tür-LED-Nacht Start" + }, + "night_end": { + "name": "Nachtlicht Ende" + }, + "night_start": { + "name": "Nachtlicht Start" + }, + "sensing_skip_end": { + "name": "KI-Reinigung Messpause Ende" + }, + "sensing_skip_start": { + "name": "KI-Reinigung Messpause Start" + } + }, + "water_heater": { + "dhw": { + "name": "Warmwasser" + } + } + }, + "config": { + "step": { + "user": { + "title": "Samsung-Gerät hinzufügen", + "description": "Geben Sie die IP-Adresse Ihres Samsung-Geräts sowie die AC14K_M-CA-Zugangsdaten ein, mit denen Gerätezertifikate erstellt werden.", + "data": { + "host": "IP-Adresse", + "ca_cert_pem": "CA-Zertifikat (PEM)", + "ca_key_pem": "CA-Privatschlüssel (PEM)" + }, + "data_description": { + "host": "Lokale IP-Adresse Ihres Samsung-Geräts (z. B. 192.168.1.50).", + "ca_cert_pem": "Die PEM-codierte AC14K_M-CA-Zertifikatskette. Fügen Sie den vollständigen Inhalt Ihrer Fullchain-PEM-Datei ein, einschließlich aller BEGIN/END CERTIFICATE-Blöcke.", + "ca_key_pem": "Der PEM-codierte AC14K_M-Privatschlüssel. Fügen Sie den vollständigen Inhalt einschließlich der BEGIN/END PRIVATE KEY-Kopf- und Fußzeile ein." + } + }, + "user_reuse": { + "title": "Samsung-Gerät hinzufügen", + "description": "Geben Sie die IP-Adresse Ihres Samsung-Geräts ein. Die CA-Zugangsdaten eines bereits vorhandenen LocalThings-Geräts werden wiederverwendet.", + "data": { + "host": "IP-Adresse" + }, + "data_description": { + "host": "Lokale IP-Adresse Ihres Samsung-Geräts (z. B. 192.168.1.50)." + } + }, + "confirm_unknown_type": { + "title": "Gerätetyp nicht erkannt", + "description": "Dieses Gerät hat das Modell \"{model}\" gemeldet, das LocalThings noch nicht als bekannten Typ erkennt. Es wird trotzdem hinzugefügt, jedoch nur mit allgemeinen Funktionen (z. B. Ein/Aus, Alarme, sofern vorhanden) und nicht mit dem vollständigen Funktionsumfang seiner Gerätefamilie. Sie können später helfen, die vollständige Unterstützung hinzuzufügen, indem Sie die Diagnosedaten für dieses Gerät herunterladen (Einstellungen > Geräte & Dienste > dieses Gerät > das Menü > Diagnose herunterladen) und sie in einem neuen Issue einreichen. Bestätigen Sie, um es trotzdem hinzuzufügen." + } + }, + "error": { + "no_response": "Keine Antwort von dieser IP-Adresse — auf keinem Port im lokalen API-Bereich des Geräts (UDP 49152-49160) kam eine Rückmeldung. Prüfen Sie, ob die IP-Adresse korrekt ist, das Gerät eingeschaltet und im selben Netzwerk wie Home Assistant ist, und ob keine Firewall den UDP-Verkehr zu ihm blockiert.", + "ports_closed": "Diese IP-Adresse ist erreichbar, hat aber jeden Port im lokalen API-Bereich (UDP 49152-49160) aktiv abgelehnt. Entweder handelt es sich nicht um ein Samsung-Gerät, oder es läuft mit älterer Firmware, die nur über TCP 8888 mit der Samsung-Cloud kommuniziert — das kann diese Integration nicht nutzen.", + "no_dtls_server": "Ports im lokalen API-Bereich sind unter dieser Adresse erreichbar, aber nichts hat auf einen DTLS-Handshake reagiert. Prüfen Sie, ob die IP-Adresse tatsächlich zum Gerät gehört und nicht zu einem anderen Gerät in Ihrem Netzwerk.", + "handshake_timeout": "Das Gerät hat auf seinem lokalen API-Port geantwortet, den DTLS-Handshake aber nie abgeschlossen. Meist hält es noch die Sitzung eines vorherigen Versuchs — warten Sie etwa eine Minute und versuchen Sie es erneut.", + "cert_rejected": "Das Gerät hat das Zertifikat abgelehnt. Das CA-Zertifikat und der Schlüssel sind höchstwahrscheinlich nicht die AC14K_M-CA, der dieses Gerät vertraut, oder sie passen nicht zueinander. Weitere Informationen in den Logs.", + "handshake_failed": "Das Gerät hat den DTLS-Handshake aus einem Grund abgelehnt, der nichts mit dem Zertifikat zu tun hat — höchstwahrscheinlich eine falsche Protokoll- oder Verschlüsselungsübereinstimmung. Weitere Informationen in den Logs.", + "cloud_unreachable": "Das Cloud-Gateway von Samsung konnte nicht erreicht werden, um die für die Zertifikatserstellung dieses Geräts benötigte UUID abzurufen. Prüfen Sie den Internetzugang von Home Assistant und versuchen Sie es erneut.", + "unexpected_response": "Die Verbindung zum Gerät war erfolgreich, es hat jedoch keine verwendbare Gerätebeschreibung zurückgegeben. Möglicherweise wird dieses Gerät von der Integration nicht unterstützt. Weitere Informationen in den Logs.", + "cannot_connect": "Verbindung zum Gerät nicht möglich. Prüfen Sie, ob die IP-Adresse erreichbar und die CA-Zugangsdaten korrekt sind.", + "invalid_ca": "Das CA-Zertifikat oder der Privatschlüssel konnte nicht geladen werden. Prüfen Sie, ob der PEM-Inhalt korrekt ist und der Schlüssel zum Zertifikat passt.", + "unknown": "Unerwarteter Fehler. Weitere Informationen in den Logs." + }, + "abort": { + "already_configured": "Gerät ist bereits konfiguriert" + } + }, + "options": { + "step": { + "init": { + "title": "LocalThings-Optionen", + "menu_options": { + "settings": "Geräteeinstellungen", + "debug_write": "Debug: In eine Ressource schreiben", + "forget_learned_modes": "Gemerkte Modi vergessen" + } + }, + "settings": { + "title": "Geräteeinstellungen", + "description": "Manche Geräte akzeptieren bestimmte Schreibvorgänge (z. B. die Standard-Dosierung von Waschmittel/Weichspüler bei einer Waschmaschine) auch dann, wenn sie melden, dass die Fernsteuerung ausgeschaltet ist. Standardmäßig blockiert LocalThings jeden Schreibvorgang mit einer klaren Fehlermeldung, sobald ein Gerät meldet, dass die Fernsteuerung ausgeschaltet ist, statt das Gerät den Vorgang stillschweigend ablehnen zu lassen. Aktivieren Sie dies nur, wenn Sie bestätigt haben, dass Schreibvorgänge bei diesem Gerät auch mit ausgeschalteter Fernsteuerung tatsächlich funktionieren -- andernfalls tauschen Sie die klare Fehlermeldung gegen ein stilles Fehlschlagen ein.\n\nDie voraussichtliche Fertigstellungszeit wird bei jeder Abfrage aus der verbleibenden Zeitschätzung des Geräts neu berechnet, die zwischen den Aktualisierungen um ein bis zwei Minuten schwanken oder korrigiert werden kann. Erhöhen Sie den unten stehenden Mindeständerungswert, um den Sensor bis zu einer Änderung der Schätzung um mindestens diese Anzahl an Minuten auf seinem zuletzt gemeldeten Wert zu halten und so das Rauschen in Verlauf/Logbuch zu verringern. Setzen Sie ihn auf 0, um jede berechnete Änderung zu melden.\n\nManche Modelle melden einen Modus, den sie nie als unterstützt angeben -- etwa eine Klimaanlage im Modus „Leise“, die nur Aus/Schlaf/Schnell anbietet. LocalThings merkt sich jeden solchen gemeldeten Modus und bietet ihn weiterhin an, sodass er auswählbar bleibt, sobald das Gerät mindestens einmal darin war. Schalten Sie dies aus, um nur das anzubieten, was das Gerät selbst angibt; verwenden Sie „Gemerkte Modi vergessen“ auf dem vorherigen Bildschirm, um bereits Gemerktes zu löschen.", + "data": { + "bypass_remote_control_lock": "Schreibvorgänge auch bei ausgeschalteter Fernsteuerung zulassen", + "finish_time_hysteresis_minutes": "Voraussichtliches Ende -- Mindeständerung (Minuten)", + "learn_device_modes": "Vom Gerät gemeldete, aber nicht angegebene Modi merken" + } + }, + "debug_write": { + "title": "Debug: In eine Ressource schreiben", + "description": "Werkzeug für erfahrene Nutzer, um geräteseitiges Schreibverhalten gezielt zu untersuchen. Wählen Sie die Ressource (href), in die Sie schreiben möchten, oder geben Sie eine eigene ein, die nicht aufgeführt ist. Dies umgeht die Sperre bei ausgeschalteter Fernsteuerung und sendet genau die von Ihnen angegebenen Felder -- dies kann Ihr Gerät falsch konfigurieren, verwenden Sie es daher bewusst.", + "data": { + "href": "Ressourcen-href" + } + }, + "debug_edit": { + "title": "Debug-Schreibvorgang: {href}", + "description": "Aktueller Wert von `{href}`:\n```\n{current_value}\n```\nGeben Sie unten NUR das/die Feld(er) ein, das/die Sie ändern möchten. Was Sie eingeben, wird unverändert an das Gerät gesendet (eine Teilaktualisierung); es wird NICHT mit den oben gezeigten Feldern zusammengeführt, fügen Sie also nicht den gesamten Wert wieder ein. Achten Sie auf die Typen: eine numerische Zeichenfolge wie \"1\" muss in Anführungszeichen bleiben -- eine reine 1 wird als Ganzzahl gesendet, was manche Geräte ablehnen.", + "data": { + "payload": "Zu schreibende Daten" + } + }, + "debug_result": { + "title": "Ergebnis des Debug-Schreibvorgangs", + "description": "Das Gerät hat den CoAP-Code {code} zurückgegeben. Eine 2.xx-Klasse bedeutet, dass der Schreibvorgang akzeptiert wurde; 4.xx/5.xx bedeutet, dass er abgelehnt wurde. Die Ressource liest sich jetzt wie folgt:\n```\n{new_value}\n```\nWenn sich der Wert nicht geändert hat, hat das Gerät den Schreibvorgang wahrscheinlich verworfen. Wählen Sie, wie Sie fortfahren möchten:", + "menu_options": { + "debug_write": "Weitere Ressource schreiben", + "finish": "Fertig" + } + }, + "forget_learned_modes": { + "title": "Gemerkte Modi vergessen", + "description": "Aktuell gemerkt: {codes}\n\nDies sind Modi, in denen sich dieses Gerät selbst gemeldet hat, ohne sie als unterstützt anzugeben; sie werden aufbewahrt, damit sie auswählbar bleiben. Das Vergessen ist die Lösung, wenn sich einer davon als falsch herausgestellt hat -- alles, was das Gerät tatsächlich erneut meldet, wird einfach erneut gemerkt, es sei denn, Sie schalten auch „Vom Gerät gemeldete, aber nicht angegebene Modi merken“ in den Geräteeinstellungen aus." + } + }, + "error": { + "empty_payload": "Geben Sie mindestens ein zu schreibendes Feld ein.", + "write_failed": "Der Schreibvorgang ist fehlgeschlagen. Weitere Details finden Sie im Home-Assistant-Protokoll." + }, + "abort": { + "not_loaded": "Dieses Gerät ist noch nicht verbunden. Versuchen Sie es erneut, sobald es geladen wurde." + } + }, + "issues": { + "device_gap": { + "title": "Unvollständige Funktionsabdeckung für {device_name}", + "description": "Für dieses Gerät liegt keine vollständige Funktionsabdeckung vor. Entweder wurde sein Gerätetyp nicht erkannt, oder einige der von ihm bereitgestellten Ressourcen sind noch nicht abgebildet. Es funktioniert weiterhin mit dem, was bereits unterstützt wird. Sie können helfen, die Unterstützung zu erweitern, indem Sie zu Einstellungen > Geräte & Dienste > {device_name} > das Menü (oben rechts) > Diagnose herunterladen gehen und diese anschließend über die verlinkte Issue-Vorlage einreichen." + } + }, + "exceptions": { + "remote_control_disabled": { + "message": "Die Fernsteuerung ist bei diesem Gerät ausgeschaltet. Schauen Sie im Handbuch Ihres Geräts nach, wie Sie die Fernsteuerung aktivieren, bevor Home Assistant es steuern kann." + }, + "debug_payload_empty": { + "message": "Die Debug-Schreibdaten müssen ein nicht leeres Objekt sein." + }, + "resource_href_required": { + "message": "Ein Ressourcen-href ist erforderlich." + }, + "bubble_soak_unavailable_for_cycle": { + "message": "Bubble Soak ist bei dem ausgewählten Programm nicht verfügbar." + }, + "pre_wash_unavailable_for_cycle": { + "message": "Vorwäsche ist bei dem ausgewählten Programm nicht verfügbar." + }, + "intensive_unavailable_for_cycle": { + "message": "Intensiv ist bei dem ausgewählten Programm nicht verfügbar." + }, + "command_failed": { + "message": "Der Befehl an {href} ist auch nach erneutem Verbinden fehlgeschlagen: {error}" + }, + "debug_too_many_writes": { + "message": "Geben Sie zwischen 1 und 10 Schreibvorgänge an." + }, + "debug_settle_out_of_range": { + "message": "settle muss zwischen 0 und 30 Sekunden liegen." + }, + "debug_verify_after_out_of_range": { + "message": "verify_after muss zwischen 0 und 60 Sekunden liegen." + }, + "service_device_target_invalid": { + "message": "Dieser Dienst erfordert genau ein Zielgerät." + }, + "service_device_not_found": { + "message": "Für dieses Ziel wurde kein passendes Gerät gefunden." + }, + "service_device_not_loaded": { + "message": "Dieses Gerät ist noch nicht verbunden. Versuchen Sie es erneut, sobald es geladen wurde." + } + } +} diff --git a/custom_components/localthings/translations/en.json b/custom_components/localthings/translations/en.json index 944c745..9830e8f 100644 --- a/custom_components/localthings/translations/en.json +++ b/custom_components/localthings/translations/en.json @@ -296,8 +296,17 @@ "0e": "AI Wash", "07": "Pre blast", "8d": "Pots and pans", + "85": "Delicate", + "0c": "Express", + "0d": "Self clean", "8e": "Plastic", - "8f": "Baby Care" + "8f": "Baby Care", + "82": "Auto", + "8a": "Normal", + "a7": "Heavy", + "a8": "Express", + "8c": "Extra Silence", + "88": "Self Clean" } }, "dispense_type": { @@ -327,8 +336,10 @@ "23": "Quick Dry 35'", "24": "Cool air", "25": "Warm air", + "26": "Air wash", "27": "Time dry", "29": "AI Dry", + "2a": "Hygiene Care+", "1a": "Wool", "1b": "Bedding", "1c": "Shirts", @@ -607,6 +618,7 @@ "32": "Shirts", "33": "Bedding", "34": "Mixed", + "35": "E Cotton", "36": "Wash+Dry", "37": "Air Wash", "38": "Cotton Dry", @@ -621,12 +633,30 @@ "60": "Self Clean+", "65": "Colors", "66": "Denim", + "69": "AI Wash", + "6a": "Wool", + "6b": "Denim", + "6c": "Blouses", + "6d": "Delicates", + "6e": "Active Wear", + "6f": "Bedding", + "70": "Towels", + "71": "Quick Wash", + "72": "Shirts", + "73": "Sanitize", + "74": "Drum Clean", + "75": "Outdoor", + "76": "Baby Care", + "77": "Cottons", + "78": "Rinse + Spin", + "79": "Spin Only", "7c": "Whites", "7d": "Bedding/Waterproof", "7e": "Self-Clean", "7f": "Wool/Delicate", "86": "Deep Wash", "87": "Download", + "88": "Pet Care", "8f": "Intense Cold", "96": "Less Microfiber" } diff --git a/custom_components/localthings/translations/es.json b/custom_components/localthings/translations/es.json index 02c3080..dceaa23 100644 --- a/custom_components/localthings/translations/es.json +++ b/custom_components/localthings/translations/es.json @@ -446,7 +446,16 @@ "07": "Prelavado intenso", "8d": "Ollas y sartenes", "8e": "Plástico", - "8f": "Cuidado del bebé" + "8f": "Cuidado del bebé", + "82": "Automático", + "8a": "Normal", + "a7": "Intenso", + "a8": "Express", + "8c": "Extra silencioso", + "88": "Autolimpieza", + "85": "Delicado", + "0c": "Express", + "0d": "Autolimpieza" } }, "dispense_type": { @@ -488,7 +497,9 @@ "4c": "Renovación de aire", "51": "Eco algodón", "53": "Secado IA+", - "4e": "Autosecado" + "4e": "Autosecado", + "26": "Aireación", + "2a": "Cuidado higiénico+" } }, "favorite_capacity": { @@ -777,7 +788,26 @@ "86": "Lavado profundo", "87": "Descarga de Programas", "8f": "Lavado en frío", - "96": "Menos microfibras" + "96": "Menos microfibras", + "69": "Lavado IA", + "6a": "Lana", + "6b": "Denim", + "6c": "Blusas", + "6d": "Delicados", + "6e": "Ropa deportiva", + "6f": "Ropa de cama", + "70": "Toallas", + "71": "Lavado rápido", + "72": "Camisas", + "73": "Desinfección", + "74": "Limpieza de Tambor", + "75": "Exterior", + "76": "Cuidado del bebé", + "77": "Algodón", + "78": "Aclarar + Centrifugar", + "79": "Solo centrifugar", + "88": "Cuidado de mascotas", + "35": "Algodón E" } }, "washer_dry_level": { diff --git a/custom_components/localthings/translations/it.json b/custom_components/localthings/translations/it.json index ca02b64..85d5248 100644 --- a/custom_components/localthings/translations/it.json +++ b/custom_components/localthings/translations/it.json @@ -297,7 +297,16 @@ "07": "Prelavaggio intenso", "8d": "Pentole e padelle", "8e": "Plastica", - "8f": "Bambini" + "8f": "Bambini", + "82": "Automatico", + "8a": "Normale", + "a7": "Intenso", + "a8": "Express", + "8c": "Extra silenzioso", + "88": "Autopulizia", + "85": "Delicato", + "0c": "Express", + "0d": "Autopulizia" } }, "dispense_type": { @@ -339,7 +348,9 @@ "4c": "Rinfresca", "51": "Eco cotone", "53": "Asciugatura AI+", - "4e": "Autoasciugatura" + "4e": "Autoasciugatura", + "26": "Arieggiatura", + "2a": "Cura igienica+" } }, "favorite_capacity": { @@ -628,7 +639,26 @@ "86": "Lavaggio profondo", "87": "Scaricato", "8f": "Intenso a freddo", - "96": "Riduci microfibre" + "96": "Riduci microfibre", + "69": "Lavaggio AI", + "6a": "Lana", + "6b": "Jeans", + "6c": "Camicette", + "6d": "Delicati", + "6e": "Abbigliamento sportivo", + "6f": "Biancheria da letto", + "70": "Asciugamani", + "71": "Lavaggio rapido", + "72": "Camicie", + "73": "Igienizzante", + "74": "Pulizia cestello", + "75": "Capi outdoor", + "76": "Bambini", + "77": "Cotone", + "78": "Risciacquo+Centrifuga", + "79": "Solo centrifuga", + "88": "Cura animali", + "35": "Cotone E" } }, "washer_dry_level": { diff --git a/custom_components/localthings/translations/ko.json b/custom_components/localthings/translations/ko.json index aae4012..3fed8be 100644 --- a/custom_components/localthings/translations/ko.json +++ b/custom_components/localthings/translations/ko.json @@ -297,7 +297,16 @@ "07": "애벌 세척", "8d": "냄비 및 팬", "8e": "플라스틱", - "8f": "젖병 살균" + "8f": "젖병 살균", + "82": "자동", + "8a": "표준", + "a7": "강력", + "a8": "급속", + "8c": "저소음", + "88": "내부 세척", + "85": "섬세", + "0c": "급속", + "0d": "내부 세척" } }, "dispense_type": { @@ -339,7 +348,9 @@ "4c": "에어 리프레시", "51": "에코 면", "53": "AI 맞춤건조+", - "4e": "자체 건조" + "4e": "자체 건조", + "26": "송풍", + "2a": "살균건조+" } }, "favorite_capacity": { @@ -628,7 +639,26 @@ "86": "찌든 때 세탁", "87": "다운로드", "8f": "강력 냉수 세탁", - "96": "미세플라스틱저감" + "96": "미세플라스틱저감", + "69": "AI 맞춤세탁", + "6a": "울", + "6b": "데님", + "6c": "블라우스", + "6d": "섬세의류", + "6e": "운동복", + "6f": "이불", + "70": "타월", + "71": "쾌속세탁", + "72": "셔츠", + "73": "살균", + "74": "무세제통세척", + "75": "아웃도어", + "76": "아기옷", + "77": "면", + "78": "헹굼+탈수", + "79": "탈수만", + "88": "펫케어", + "35": "에코 면" } }, "washer_dry_level": { diff --git a/custom_components/localthings/translations/nl.json b/custom_components/localthings/translations/nl.json index 64f493f..c135796 100644 --- a/custom_components/localthings/translations/nl.json +++ b/custom_components/localthings/translations/nl.json @@ -297,7 +297,16 @@ "07": "Voorspoelen", "8d": "Potten en pannen", "8e": "Kunststof", - "8f": "Babyverzorging" + "8f": "Babyverzorging", + "82": "Auto", + "8a": "Normaal", + "a7": "Intensief", + "a8": "Express", + "8c": "Extra stil", + "88": "Zelfreiniging", + "85": "Delicaat", + "0c": "Express", + "0d": "Zelfreiniging" } }, "dispense_type": { @@ -339,7 +348,9 @@ "4c": "Opfrissen", "51": "Eco katoen", "53": "AI drogen+", - "4e": "Zelf drogen" + "4e": "Zelf drogen", + "26": "Luchtverfrissing", + "2a": "Hygiënische verzorging+" } }, "favorite_capacity": { @@ -621,14 +632,33 @@ "60": "Self Clean+", "65": "Bonte was", "66": "Spijkergoed", + "69": "AI wassen", + "6a": "Wol", + "6b": "Spijkergoed", + "6c": "Blouses", + "6d": "Fijne was", + "6e": "Sportkleding", + "6f": "Beddengoed", + "70": "Handdoeken", + "71": "Snelle was", + "72": "Overhemden", + "73": "Hygiëne", + "74": "Trommel reinigen", + "75": "Outdoor", + "76": "Babyverzorging", + "77": "Katoen", + "78": "Spoelen + centrifugeren", + "79": "Alleen centrifugeren", "7c": "Witte was", "7d": "Beddengoed/waterdicht", "7e": "Self Clean", "7f": "Wol/fijne was", "86": "Diep wassen", "87": "Gedownload", + "88": "Huisdierverzorging", "8f": "Intensief koud", - "96": "Minder microvezels" + "96": "Minder microvezels", + "35": "Eco katoen" } }, "washer_dry_level": { diff --git a/tests/test_laundry_capabilities.py b/tests/test_laundry_capabilities.py index 82b359e..2934d6c 100644 --- a/tests/test_laundry_capabilities.py +++ b/tests/test_laundry_capabilities.py @@ -59,6 +59,55 @@ class TestCourseHelpers: assert laundry.option_value(opts, "Course") == "1C" assert laundry.option_value(opts, "Missing") is None + def test_decodes_device_provided_personal_course_names(self): + """Real /wm/personalcourse/vs/0 entries from the reported washer. + + The first TLV field contains a byte-counted UTF-8 name. It is the only + course-name metadata in the diagnostics and is safe to display without + assigning an inferred meaning to any standard course code. + """ + resources = { + "/wm/personalcourse/vs/0": { + "x.com.samsung.da.courses": [ + "F1_0106EC868DEC98B7021EED8CACED8BB020EB93B1", + "F2_00", + "F3_0109EC9A94EAB8B0EBB3B40220ECA084EC9AA9", + ], + }, + } + first_name = bytes.fromhex("EC868DEC98B7").decode("utf-8") + second_name = bytes.fromhex("EC9A94EAB8B0EBB3B4").decode("utf-8") + assert laundry.personal_course_labels(resources) == { + "F1": first_name, + "F3": second_name, + } + + def test_personal_course_names_reject_malformed_payloads(self): + resources = { + "/wm/personalcourse/vs/0": { + "x.com.samsung.da.courses": [ + "F1_not-hex", + "F2_0106AA", + "F3_020141", + None, + ], + }, + } + assert laundry.personal_course_labels(resources) == {} + + def test_washer_cycle_fallback_only_labels_personal_courses(self): + """No invented English label for an unrecognized code (PR #251 review).""" + resources = { + "/wm/personalcourse/vs/0": { + "x.com.samsung.da.courses": ["F1_0106EC868DEC98B7"], + }, + } + expected = bytes.fromhex("EC868DEC98B7").decode("utf-8") + assert laundry.washer_cycle_fallback("F1", resources) == expected + assert laundry.washer_cycle_fallback("69", resources) is None + assert laundry.washer_cycle_fallback("6f", resources) is None + assert laundry.washer_cycle_fallback("Normal", resources) is None + class TestCourseCodesFromSupportedOptions: """cycle_options()'s fallback for boards that populate diff --git a/tests/test_select_display.py b/tests/test_select_display.py index 1294874..1a9165d 100644 --- a/tests/test_select_display.py +++ b/tests/test_select_display.py @@ -46,3 +46,7 @@ def test_known_camel_case_state_uses_snake_case_translation_key(): def test_display_passes_through_non_string_values(): assert _display(None, _UNTRANSLATED) is None + + +def test_display_uses_fallback_when_translation_has_no_state_table(): + assert _display("69", "cycle", lambda value: f"Unknown (0x{value})") == ("Unknown (0x69)") diff --git a/tests/test_select_options.py b/tests/test_select_options.py index 954e589..3ced893 100644 --- a/tests/test_select_options.py +++ b/tests/test_select_options.py @@ -6,6 +6,10 @@ and callable forms of SelectDesc.options. from typing import ClassVar, cast from custom_components.localthings.coordinator import LocalThingsCoordinator +from custom_components.localthings.registry.capabilities.laundry import ( + cycle_select, + washer_cycle_fallback, +) from custom_components.localthings.registry.capability import Capability from custom_components.localthings.registry.discovery import BoundEntity from custom_components.localthings.registry.entities import SelectDesc @@ -113,3 +117,79 @@ async def test_unknown_vendor_option_round_trips_to_exact_raw_value(): assert entity.options[-1] == "Future Vendor Mode" await entity.async_select_option("Future Vendor Mode") assert coordinator.writes == ["FutureVendorMode"] + + +async def test_washer_diagnostic_cycle_values_share_one_display_and_write_path(): + """Regression for Course_69/EditCourseList_696F... from real hardware.""" + + class _WritableCoordinator(_FakeCoordinator): + data: ClassVar[dict] = {"cycle": "69"} + + def __init__(self, last_resources): + super().__init__(last_resources) + self.writes = [] + + async def async_send_command(self, bound, value): + self.writes.append(value) + + desc = cycle_select( + translation_key="washer_cycle", + icon="mdi:washing-machine", + table_href="/st/washercourse/vs/0", + display_fn=washer_cycle_fallback, + ) + capability = Capability(href="/course/vs/0", entities=(desc,)) + bound = BoundEntity(href="/course/vs/0", capability=capability, desc=desc) + resources = { + "/course/vs/0": {"x.com.samsung.da.options": ["Course_69"]}, + "/st/washercourse/vs/0": { + "x.com.samsung.da.st.courseTable": "Table_02", + }, + "/wm/editcourse/vs/0": { + "x.com.samsung.da.editCourseList": ( + "EditCourseList_696F757801719688706D6A7376726C6E6B777479F1F3" + ), + }, + "/wm/personalcourse/vs/0": { + "x.com.samsung.da.courses": [ + "F1_0106EC868DEC98B7", + "F3_0109EC9A94EAB8B0EBB3B4", + ], + }, + } + coordinator = _WritableCoordinator(resources) + entity = LocalThingsSelect(cast(LocalThingsCoordinator, coordinator), bound) + first_name = bytes.fromhex("EC868DEC98B7").decode("utf-8") + second_name = bytes.fromhex("EC9A94EAB8B0EBB3B4").decode("utf-8") + + # Known catalog states stay as HA translation keys; the frontend renders + # this confirmed Table_02 mapping as "AI Wash". + assert entity.current_option == "69" + assert entity.options == [ + "69", + "6f", + "75", + "78", + "01", + "71", + "96", + "88", + "70", + "6d", + "6a", + "73", + "76", + "72", + "6c", + "6e", + "6b", + "77", + "74", + "79", + first_name, + second_name, + ] + + await entity.async_select_option("6f") + await entity.async_select_option(first_name) + assert coordinator.writes == ["6F", "F1"] diff --git a/tests/test_subdevice_discovery.py b/tests/test_subdevice_discovery.py index c839c2c..a8dcb6f 100644 --- a/tests/test_subdevice_discovery.py +++ b/tests/test_subdevice_discovery.py @@ -278,6 +278,19 @@ async def test_fac_bora_2in1_unique_ids_include_subdevice_prefix(hass: HomeAssis # --------------------------------------------------------------------------- +async def test_flat_probe_priority_puts_live_climate_state_before_cold_metrics( + hass: HomeAssistant, +): + """Registry metadata drives fallback order without a model-specific list.""" + resources, _oic_res, _seeds = _load_device_full("airconditioner_fac_bora_205_flat") + coordinator = _coordinator(hass) + + priority = coordinator._subdevice_probe_priority(resources) + + assert "/mode/vs/0" in priority[:4] + assert priority.index("/mode/vs/0") < priority.index("/energy/consumption/vs/0") + + async def test_fac_bora_205_flat_fallback_finds_candidate_but_gate_holds_it_back( hass: HomeAssistant, ): diff --git a/tests/test_subdevices.py b/tests/test_subdevices.py index 1ee6690..3b4dd96 100644 --- a/tests/test_subdevices.py +++ b/tests/test_subdevices.py @@ -10,6 +10,7 @@ from typing import cast import cbor2 +from custom_components.localthings.registry import subdevices as subdevices_module from custom_components.localthings.registry.by_type import DeviceRegistry from custom_components.localthings.registry.capability import Capability from custom_components.localthings.registry.entities import BinarySensorDesc, SensorDesc @@ -350,6 +351,118 @@ def test_enumerate_prefixed_flat_fallback_probe_log_reports_every_href_tried(): assert probes[f"/{_UUID}/mode/vs/0"] is True +def test_enumeration_budget_bounds_silent_prefixed_fallback_and_uses_priority( + monkeypatch, +): + """A firmware that drops unknown prefixed paths cannot stall setup. + + The Collection probe gets the larger blockwise allowance. The preferred + live-state href is then attempted first, and the last Property probe is + clamped to exactly the time left in the shared enumeration budget. + """ + + class Clock: + now = 0.0 + + def monotonic(self): + return self.now + + class SilentSession: + def __init__(self, clock): + self.clock = clock + self.calls = [] + + def get(self, path, timeout=10.0): + self.calls.append((tuple(path), timeout)) + self.clock.now += timeout + raise TimeoutError + + def pace(self): + pass + + clock = Clock() + session = SilentSession(clock) + monkeypatch.setattr(subdevices_module.time, "monotonic", clock.monotonic) + resources = { + "/subdevices/vs/0": { + "x.com.samsung.da.subdeviceIdList": [_UUID], + }, + "/power/vs/0": {"power": "On"}, + "/mode/vs/0": {"mode": "Cool"}, + } + + found, extra = enumerate_subdevices( + session, + resources, + oic_res_links=[], + preferred_hrefs=("/mode/vs/0",), + time_budget=7.0, + collection_timeout=4.0, + property_timeout=2.0, + ) + + assert found == [] + assert extra == {} + assert clock.now == 7.0 + assert session.calls == [ + ((_UUID, "device", "0"), 4.0), + ((_UUID, "mode", "vs", "0"), 2.0), + ((_UUID, "power", "vs", "0"), 1.0), + ] + + +def test_enumeration_keeps_preferred_response_found_before_budget_expires( + monkeypatch, +): + """A useful early response survives later silent probes hitting the cap.""" + + class Clock: + now = 0.0 + + def monotonic(self): + return self.now + + class PartlyResponsiveSession: + def __init__(self, clock): + self.clock = clock + + def get(self, path, timeout=10.0): + if tuple(path) == (_UUID, "mode", "vs", "0"): + return 0x45, cbor2.dumps({"mode": "Cool"}) + self.clock.now += timeout + raise TimeoutError + + def pace(self): + pass + + clock = Clock() + monkeypatch.setattr(subdevices_module.time, "monotonic", clock.monotonic) + resources = { + "/subdevices/vs/0": { + "x.com.samsung.da.subdeviceIdList": [_UUID], + }, + "/power/vs/0": {"power": "On"}, + "/mode/vs/0": {"mode": "Cool"}, + } + + found, extra = enumerate_subdevices( + PartlyResponsiveSession(clock), + resources, + oic_res_links=[], + preferred_hrefs=("/mode/vs/0",), + time_budget=7.0, + collection_timeout=4.0, + property_timeout=2.0, + ) + + assert [(subdevice.kind, subdevice.key) for subdevice in found] == [ + ("prefixed", _UUID), + ] + assert found[0].flat_hrefs == ("/mode/vs/0",) + assert extra == {f"/{_UUID}/mode/vs/0": {"mode": "Cool"}} + assert clock.now == 7.0 + + def test_enumerate_prefixed_flat_fallback_with_no_master_hrefs_to_probe_is_a_no_op(): """The master itself having nothing but /subdevices/vs/0 in its own resources this cycle (e.g. a very first, mostly-empty poll) must not diff --git a/tests/test_translations.py b/tests/test_translations.py index 1191576..a456e51 100644 --- a/tests/test_translations.py +++ b/tests/test_translations.py @@ -114,6 +114,167 @@ def test_no_catalog_carries_unresolved_core_references(): assert unresolved == [], language +def test_confirmed_korean_table_02_washer_course_names(): + states = _load("en")["entity"]["select"]["washer_cycle_table_02"]["state"] + assert { + code: states[code] + for code in ( + "69", + "6a", + "6b", + "6c", + "6d", + "6e", + "6f", + "70", + "71", + "72", + "73", + "74", + "75", + "76", + "77", + "78", + "79", + "88", + ) + } == { + "69": "AI Wash", + "6a": "Wool", + "6b": "Denim", + "6c": "Blouses", + "6d": "Delicates", + "6e": "Active Wear", + "6f": "Bedding", + "70": "Towels", + "71": "Quick Wash", + "72": "Shirts", + "73": "Sanitize", + "74": "Drum Clean", + "75": "Outdoor", + "76": "Baby Care", + "77": "Cottons", + "78": "Rinse + Spin", + "79": "Spin Only", + "88": "Pet Care", + } + + +def test_confirmed_dishwasher_course_names(): + states = _load("en")["entity"]["select"]["dishwasher_cycle"]["state"] + assert { + code: states[code] + for code in ( + "82", + "8a", + "a7", + "a8", + "8c", + "88", + ) + } == { + "82": "Auto", + "8a": "Normal", + "a7": "Heavy", + "a8": "Express", + "8c": "Extra Silence", + "88": "Self Clean", + } + + +def test_confirmed_course_names_are_localized(): + washer_codes = ( + "69", + "6a", + "6b", + "6c", + "6d", + "6e", + "6f", + "70", + "71", + "72", + "73", + "74", + "75", + "76", + "77", + "78", + "79", + "88", + ) + dishwasher_codes = ("82", "8a", "a7", "a8", "8c", "88") + expected = { + "cs": { + "washer": ( + "AI praní", + "Vlna", + "Džíny", + "Halenky", + "Jemné prádlo", + "Sportovní oblečení", + "Ložní prádlo", + "Ručníky", + "Rychlé praní", + "Košile", + "Dezinfekce", + "Čištění bubnu", + "Outdoor", + "Dětské potřeby", + "Bavlna", + "Máchání + odstřeďování", + "Pouze odstřeďování", + "Péče o domácí mazlíčky", + ), + "dishwasher": ( + "Automatický", + "Normální", + "Intenzivní", + "Expresní", + "Extra tichý", + "Samočištění", + ), + }, + "nl": { + "washer": ( + "AI wassen", + "Wol", + "Spijkergoed", + "Blouses", + "Fijne was", + "Sportkleding", + "Beddengoed", + "Handdoeken", + "Snelle was", + "Overhemden", + "Hygiëne", + "Trommel reinigen", + "Outdoor", + "Babyverzorging", + "Katoen", + "Spoelen + centrifugeren", + "Alleen centrifugeren", + "Huisdierverzorging", + ), + "dishwasher": ( + "Auto", + "Normaal", + "Intensief", + "Express", + "Extra stil", + "Zelfreiniging", + ), + }, + } + + for language, translations in expected.items(): + catalog = _load(language)["entity"]["select"] + washer = catalog["washer_cycle_table_02"]["state"] + dishwasher = catalog["dishwasher_cycle"]["state"] + assert tuple(washer[code] for code in washer_codes) == translations["washer"] + assert tuple(dishwasher[code] for code in dishwasher_codes) == (translations["dishwasher"]) + + # The hood fan is its device's primary feature: fan.py sets _attr_name = None # so it presents as the device itself, and never reads a catalog name. Same # for the ARTIK051 air-purifier's airflow_fan (issue #56) -- ordered speed diff --git a/tests/test_washer_capabilities.py b/tests/test_washer_capabilities.py index 2582ed1..bb68900 100644 --- a/tests/test_washer_capabilities.py +++ b/tests/test_washer_capabilities.py @@ -120,6 +120,32 @@ class TestWasherCourse: rep = {"x.com.samsung.da.options": ["DeviceType_0167", "Course_1C", "GMT_04"]} assert desc.rep_fn(rep) == "1C" + def test_reported_table_02_course_codes_are_translated(self): + """The owner confirmed this washer's newer Table_02 course family.""" + from custom_components.localthings.catalog import translated_states + + confirmed = { + "69", + "6a", + "6b", + "6c", + "6d", + "6e", + "6f", + "70", + "71", + "72", + "73", + "74", + "75", + "76", + "77", + "78", + "79", + "88", + } + assert confirmed <= translated_states("select", "washer_cycle_table_02") + def test_missing_course_option_returns_none(self): desc = next(e for e in washer.WASHER_COURSE.entities if e.key == "cycle") assert desc.rep_fn is not None @@ -132,6 +158,7 @@ class TestWasherCourse: if e.key == "cycle" and isinstance(e, SelectDesc) ) assert desc.options is laundry.cycle_options + assert desc.display_fn is laundry.washer_cycle_fallback def test_exists_only_when_edit_course_list_is_live(self): """No hardcoded course table is kept -- the selector only appears