Merge remote-tracking branch 'origin/main' into claude/device-support-issue-77-freezer

# Conflicts:
#	custom_components/localthings/registry/by_type/__init__.py
#	tests/test_by_type.py
This commit is contained in:
Marc Billow
2026-07-27 04:03:38 +00:00
28 changed files with 2585 additions and 14 deletions
+13 -1
View File
@@ -197,6 +197,18 @@ def _find_live_ports(host: str, ports: list[int], timeout: float) -> list[int]:
return _order_candidates(live)
def _is_placeholder_serial(serial: str) -> bool:
"""True for a non-empty serialNum that isn't actually a real identity.
The ARTIK051_DONGLE_REF firmware family reports the literal string
'Nothing(SVC)' for every unit -- non-empty, so the plain `if not
serial` check here (and the equivalent one in coordinator.py's
`_run_discovery`) doesn't catch it, and two such units get the same
config-entry unique_id / entity unique_ids and collide (issue #83).
"""
return serial.strip().lower().startswith('nothing')
def _probe_and_validate(host: str, ca_cert_pem: str, ca_key_pem: str) -> dict:
"""Fetch UUID, mint leaf cert, probe each port. Returns config entry data dict."""
import cbor2
@@ -261,7 +273,7 @@ def _probe_and_validate(host: str, ca_cert_pem: str, ca_key_pem: str) -> dict:
.get('/information/vs/0', {})
.get('x.com.samsung.da.serialNum', '')
)
if not serial:
if not serial or _is_placeholder_serial(serial):
serial = f"{host}:{port}"
one_ui_version = (
resources
+17 -1
View File
@@ -53,6 +53,22 @@ class _NoOpDescriptor:
_RECOVERY_RETRY_S = 600.0 # re-attempt observe mode this often while polling
def _is_placeholder_serial(serial: str) -> bool:
"""True for a non-empty serialNum that isn't actually a real identity.
The ARTIK051_DONGLE_REF firmware family reports the literal string
'Nothing(SVC)' for every unit -- non-empty, so the plain `if not
serial` check below doesn't catch it, and `device_serial` feeds both
the HA device-registry identifier and every entity's unique_id
(entity.py), so two such units on the same install silently collide
and the second one's entities get dropped (issue #83). Mirrors the
identical helper in config_flow.py's `_probe_and_validate` -- kept
separate rather than imported to avoid pulling the config-flow module
into the runtime coordinator's import graph for a two-line check.
"""
return serial.strip().lower().startswith('nothing')
class LocalThingsCoordinator(DataUpdateCoordinator[dict[str, Any]]):
"""Manages one Samsung appliance: session, discovery, polling."""
@@ -316,7 +332,7 @@ class LocalThingsCoordinator(DataUpdateCoordinator[dict[str, Any]]):
self._unbound_hrefs = unbound
serial = info.get('x.com.samsung.da.serialNum', '')
if not serial:
if not serial or _is_placeholder_serial(serial):
serial = self._entry.data[CONF_HOST]
self.device_serial = serial
@@ -3,8 +3,8 @@ from typing import Optional
from ._base import DeviceRegistry
from . import (
air_purifier, airconditioner, cooktop, dishwasher, dryer, oven,
range as _range, range_hood, refrigerator, washer,
air_purifier, airconditioner, cooktop, dehumidifier, dishwasher, dryer,
oven, range as _range, range_hood, refrigerator, washer, water_purifier,
)
__all__ = [
@@ -19,6 +19,7 @@ _REGISTRY_BY_KEY: dict[str, DeviceRegistry] = {
'airconditioner': airconditioner.REGISTRY,
'air_conditioner': airconditioner.REGISTRY,
'cooktop': cooktop.REGISTRY,
'dehumidifier': dehumidifier.REGISTRY,
'dishwasher': dishwasher.REGISTRY,
'dryer': dryer.REGISTRY,
'oven': oven.REGISTRY,
@@ -27,6 +28,7 @@ _REGISTRY_BY_KEY: dict[str, DeviceRegistry] = {
'range_hood': range_hood.REGISTRY,
'refrigerator': refrigerator.REGISTRY,
'washer': washer.REGISTRY,
'water_purifier': water_purifier.REGISTRY,
}
@@ -103,6 +105,26 @@ def _model_num_segments(model_num: str) -> list[str]:
return prefix.split('_')
def _consumer_model_key(description: str) -> Optional[str]:
"""Registry key from the consumer-model token in `description`, or None.
Usually that token is the last '_'-delimited segment before any
'/board-info' suffix (e.g. '..._WW90DG6U25LEU4' -> 'WW90DG6U25LEU4').
But issue #79's dryer pairs two model numbers in one description --
'..._DVE50A8800_8600/DC92-...' -- so the true consumer token
('DVE50A8800') sits one segment *before* the actual last segment
('8600', a bare second model number with no recognizable prefix). Scan
segments from the end and take the first one that resolves, rather
than assuming the last segment is always it.
"""
segments = (description or '').split('/', 1)[0].split('_')
for segment in reversed(segments):
key = _CONSUMER_PREFIX_TO_KEY.get(segment[:2].upper())
if key is not None:
return key
return None
def for_device_by_model(model_num: str, description: str) -> Optional[DeviceRegistry]:
"""Fallback device-type detection for hardware that never reports
oneUiVersion (confirmed for washers -- their /otninformation/vs/0 has
@@ -116,8 +138,7 @@ def for_device_by_model(model_num: str, description: str) -> Optional[DeviceRegi
DeviceRegistry if the consumer-model code or modelNum resolves to a
known type, None otherwise.
"""
token = (description or '').split('/', 1)[0].rsplit('_', 1)[-1]
key = _CONSUMER_PREFIX_TO_KEY.get(token[:2].upper())
key = _consumer_model_key(description)
if key is None and 'REF' in _model_num_segments(model_num):
key = 'refrigerator'
# Room air conditioners (e.g. ARTIK051_PRAC_20K) report no oneUiVersion and
@@ -138,6 +159,22 @@ def for_device_by_model(model_num: str, description: str) -> Optional[DeviceRegi
# resource (see airconditioner.py's _AC_IGNORED).
if key is None and '-CAWW-' in (model_num or '').upper():
key = 'airconditioner'
# Dehumidifiers (e.g. TP1X_DA_AC_DHM_01001_0000, issue #88) share the
# DA_AC_ board family with the room-AC models above but carry the
# '_DHM_' (DeHuMidifier) token instead of '_RAC_'/'_PRAC_'. Distinct
# registry: target humidity, not temperature, is the primary control,
# and there's no climate composite.
if key is None and '_DHM_' in (model_num or ''):
key = 'dehumidifier'
# Window air conditioners (e.g. TP1X_DA_AC_WAC_01001_0000, issue #87)
# report no oneUiVersion and carry the '_WAC_' (Window Air Conditioner)
# token instead of '_RAC_'/'_PRAC_'. Same TP1X-class resource surface
# as the room-AC models above (mode/convenient/wind/temperature/power/
# filter/humidity all confirmed against the issue #87 dump binding
# cleanly against the existing airconditioner registry once routed
# here) -- no WAC-specific resources needed.
if key is None and '_WAC_' in (model_num or ''):
key = 'airconditioner'
# Air purifiers (e.g. ARTIK051_TVTL_18K, issue #56) report no
# oneUiVersion either, and carry the '_TVTL_' board-family token.
if key is None and '_TVTL_' in (model_num or ''):
@@ -145,6 +182,10 @@ def for_device_by_model(model_num: str, description: str) -> Optional[DeviceRegi
model_identity = f'{model_num} {description}'.upper()
if key is None and ('_COOKTOP' in model_identity or '_GB_CT_' in model_identity):
key = 'cooktop'
# Water purifiers (e.g. TP2X_WATERPURIFIER_20K, issue #90) report no
# oneUiVersion and carry the 'WATERPURIFIER' board-family token.
if key is None and 'WATERPURIFIER' in model_identity:
key = 'water_purifier'
if key is None and model_identity.startswith('AHD-'):
key = 'range_hood'
# Range/cooktop-oven combos (e.g. TP1X_DA-KS-RANGE-0102X, issue #44) --
@@ -1,11 +1,21 @@
"""Cooktop device registry."""
"""Gas cooktop device registry (NA9300K-class, PR #23).
Named 'gas_cooktop' (not 'cooktop') so its diagnostics/device-info label
doesn't collide with the unrelated induction_cooktop family (issue #86,
by_type/induction_cooktop.py) -- two different OCF surfaces that happen to
share the English word "cooktop". The `_REGISTRY_BY_KEY['cooktop']` lookup
key is unchanged: it's relied on by real devices reporting
oneUiVersion "Cooktop" (for_device), the legacy ARTIK051 modelNum rule
(for_device_by_model), and the resource-signature fallback
(for_device_by_resources) alike.
"""
from ..capabilities import common, cooktop, ignored
from ._base import DeviceRegistry, _build
REGISTRY = DeviceRegistry(
name='cooktop',
name='gas_cooktop',
capabilities=_build([
*ignored.IGNORED,
cooktop.COOKTOP_POWER,
@@ -0,0 +1,23 @@
"""Dehumidifier device registry (Samsung TP1X_DA_AC_DHM-class, issue #88).
Shares the DA_AC_ board family with airconditioner.py (power, air filter,
auto-clean, mute-once all use the identical resource shapes), so those three
Capability objects are reused directly rather than duplicated.
"""
from ..capabilities import airconditioner, common, dehumidifier, ignored
from ._base import DeviceRegistry, _build
REGISTRY = DeviceRegistry(
name='dehumidifier',
capabilities=_build([
*ignored.IGNORED,
*common.UNIVERSAL,
*common.POWER,
dehumidifier.MODE,
dehumidifier.HUMIDITY,
airconditioner.AUTO_CLEAN,
airconditioner.AIR_FILTER,
airconditioner.MUTE_ONCE,
*dehumidifier.COVERAGE,
]),
)
@@ -0,0 +1,18 @@
"""Water-purifier device registry (Samsung TP2X_WATERPURIFIER-class, issue #90).
"""
from ..capabilities import common, ignored, water_purifier
from ._base import DeviceRegistry, _build
REGISTRY = DeviceRegistry(
name='water_purifier',
capabilities=_build([
*ignored.IGNORED,
*common.UNIVERSAL,
common.WATER_FILTER,
water_purifier.DISPENSE,
water_purifier.STATUS,
water_purifier.FAVORITE_CAPACITY,
water_purifier.LOCK,
*water_purifier.COVERAGE,
]),
)
@@ -0,0 +1,85 @@
"""Capabilities for the Samsung dehumidifier family (TP1X_DA_AC_DHM-class,
issue #88, model AY18CG7500GED).
Same DA_AC_ board family as the room-AC models in airconditioner.py (shared
power/energy/filter/auto-clean/mute-once resource shapes), but target
humidity -- not temperature -- is this device's primary control, and there
is no climate composite: power, mode, and humidity are exposed as separate
entities rather than folded into one card.
"""
from ..capability import Capability
from ..entities import NumberDesc, SelectDesc, SensorDesc
from .common import int_or_none
def _first_mode(rep):
"""Representative scalar for the operating-mode select. `modes` is a
single-element list on every dump seen so far, mirroring
airconditioner._first_mode's handling of the same field shape."""
modes = rep.get('x.com.samsung.da.modes')
if isinstance(modes, (list, tuple)):
return modes[0] if modes else None
return modes
MODE = Capability(
href='/mode/vs/0',
poll_tier='warm',
entities=(
SelectDesc(key='operating_mode', rep_fn=_first_mode,
icon='mdi:tune-variant',
options_field='x.com.samsung.da.supportedModes',
write_fn=lambda p, rep, href=None: (
['mode', 'vs', '0'], {'x.com.samsung.da.modes': [p]})),
),
)
# Target humidity is this device's primary control (the issue-#88 dump's
# equivalent of a thermostat setpoint). No min/max range field is present in
# any dump seen so far -- native_min/native_max are deliberately left unset
# so the number entity falls back to HA's own 0-100 default, the natural
# bound for a percentage field, rather than a bound guessed from one unit's
# spec sheet (see the adding-device-support skill's "never hard-code the one
# dump's values" section). Step comes live from the device's own `increment`
# field.
HUMIDITY = Capability(
href='/humidity/vs/0',
poll_tier='warm',
entities=(
SensorDesc(key='humidity', field='x.com.samsung.da.humidity',
device_class='humidity', unit='%', state_class='measurement',
value_fn=int_or_none),
NumberDesc(key='target_humidity', field='x.com.samsung.da.desiredHumidity',
device_class='humidity', unit='%',
icon='mdi:water-percent',
entity_category='config',
value_fn=int_or_none,
step_fn=lambda rep: int_or_none(rep.get('increment')) or 1,
write_fn=lambda p, rep, href=None: (
['humidity', 'vs', '0'],
{'x.com.samsung.da.desiredHumidity': str(int(round(float(p))))})),
),
)
# ---------------------------------------------------------------------------
# Dehumidifier-scoped coverage: vendor plumbing with no user-actionable state
# or no documented write contract, following the same 'don't guess' rule as
# airconditioner._AC_IGNORED (this is the same DA_AC_ board family). Not in
# the global ignored.IGNORED since some of these hrefs collide with other
# families' schemas.
# ---------------------------------------------------------------------------
_DHM_IGNORED = [
'/availablecontrolsets/vs/0', # opaque hex-encoded control-set bitmap (id: DHM)
'/da/softreset/vs/0', # soft-reset trigger plumbing
'/keepnormalstate/vs/0', # internal keep-normal flag
'/personality/presence/vs/0', # presence-personalization plumbing (empty item value)
'/reserverulesets/vs/0', # opaque hex-encoded schedule reservation blob
'/sensors/vs/0', # empty {} on this dump
'/welcome/humidity/vs/0', # welcome-mode plumbing (requestId/operatingStatus, inert)
# Only supportedModes ([Off, Sleep]) is present -- no live "current
# value" field on this dump to confirm the read/write contract, so per
# the 'don't guess' rule this is left unmodeled rather than assumed.
'/mode/convenient/vs/0',
]
COVERAGE = [Capability(href=h) for h in _DHM_IGNORED]
@@ -42,7 +42,10 @@ DRYER_SETTINGS = Capability(
# under entity.select.dryer_cycle (Table_03, DV5000-class, captured
# 2026-05-29). Codes 0x21 and 0x4C appear in the issue #14 DV90BB5245AES1
# editCourseList but aren't identified yet -- they render as the raw code
# until named. The /st/dryercourse/vs/0 resource re-encodes the same selected
# until named. Codes '01' Normal and '06' Time dry were confirmed on a
# DVE50A8600V/A3 (also Table_03) by selecting each cycle on the physical
# appliance and reading back the raw code from the entity's state (issue
# #80). The /st/dryercourse/vs/0 resource re-encodes the same selected
# course and is ignored (ignored.py) -- the mirror of how /st/washercourse/vs/0
# is ignored for washers.
DRYER_COURSE = Capability(
@@ -45,6 +45,14 @@ from .laundry import (
# (issue #22). Combo units carry their own course set, so these codes
# don't imply anything about '1F' on a plain washer.
#
# Three more -- '52' Eco Cold, '54' Towels, '60' Self Clean+ -- came from a
# WF50A8600AV/US, verified directly rather than by inference: the reporter
# selected each cycle on the physical appliance and read back the resulting
# raw code from the cycle_select entity's state (issue #80). '54' shares a
# display name with the existing '24' Towels -- a different code on a
# different course table legitimately landing on the same label, not a typo
# (same pattern as '21'/'65' Colors and '27'/'5E' Rinse+Spin above).
#
# No static fallback list of those codes is kept here, deliberately: other
# washer models have a different actual course set (a second dump's active
# course, '65', isn't even in the list above; models with 'AI Wash'/'Mixed
@@ -0,0 +1,165 @@
"""Capabilities for the Samsung water-purifier family (TP2X_WATERPURIFIER-class,
issue #90, model TP2X_WATERPURIFIER_20K).
Resources verified against the issue #90 diagnostics dump.
"""
from datetime import datetime, timezone
from ..capability import Capability
from ..entities import BinarySensorDesc, NumberDesc, SelectDesc, SensorDesc, SwitchDesc
from .common import int_or_none
def _parse_iso_utc(raw):
"""Bare ISO datetime with no timezone field alongside it -- treated as
UTC, matching this integration's convention for other bare ISO datetime
fields (see washer.py's drum-clean-log comment)."""
if not raw:
return None
try:
return datetime.fromisoformat(raw).replace(tzinfo=timezone.utc)
except ValueError:
return None
DISPENSE = Capability(
href='/setting/waterpurifier/vs/0',
poll_tier='warm',
entities=(
SelectDesc(key='dispense_type', field='x.com.samsung.da.desiredType',
icon='mdi:cup-water',
options_field='x.com.samsung.da.supportedTypes',
write_fn=lambda p, rep, href=None: (
['setting', 'waterpurifier', 'vs', '0'],
{'x.com.samsung.da.desiredType': p})),
# Only a handful of discrete temperatures are selectable (not a
# continuous range) -- a select over the live-reported set, not a
# number with invented bounds.
SelectDesc(key='hot_water_temperature', field='x.com.samsung.da.tempDesiredHotWater',
icon='mdi:thermometer',
entity_category='config',
options_field='x.com.samsung.da.supportedHotTemperatures',
write_fn=lambda p, rep, href=None: (
['setting', 'waterpurifier', 'vs', '0'],
{'x.com.samsung.da.tempDesiredHotWater': p})),
# Bounds and step come live from the device's own
# desiredCapacityRange/capacityResolution fields, not a hardcoded
# constant -- see the adding-device-support skill's "never hard-code
# the one dump's values" section. No unit is set: capacityUnit reads
# "C" on this dump, which can't be right for a volume field, so per
# the 'don't guess' rule the unit is left unset rather than assumed
# to be mL.
NumberDesc(key='dispense_capacity', field='x.com.samsung.da.desiredCapacity',
icon='mdi:cup-water',
value_fn=int_or_none,
range_field='x.com.samsung.da.desiredCapacityRange',
step_fn=lambda rep: int_or_none(
rep.get('x.com.samsung.da.capacityResolution')) or 1,
write_fn=lambda p, rep, href=None: (
['setting', 'waterpurifier', 'vs', '0'],
{'x.com.samsung.da.desiredCapacity': str(int(round(float(p))))})),
BinarySensorDesc(key='pouring', field='x.com.samsung.da.pourStatus',
icon='mdi:cup-water',
value_fn=lambda v: v == 'On'),
),
)
STATUS = Capability(
href='/status/waterpurifier/vs/0',
poll_tier='warm',
entities=(
SensorDesc(key='waterpurifier_status', field='x.com.samsung.da.status',
icon='mdi:water-pump',
entity_category='diagnostic'),
BinarySensorDesc(key='filter_door_status', field='x.com.samsung.da.filterDoorStatus',
device_class='door',
entity_category='diagnostic',
value_fn=lambda v: v == 'Open'),
SensorDesc(key='sterilize_period', field='x.com.samsung.da.sterilizePeriod',
icon='mdi:calendar-sync',
entity_category='diagnostic'),
SensorDesc(key='sterilize_run_time', field='x.com.samsung.da.sterilizeRunTime',
icon='mdi:timer-outline',
entity_category='diagnostic'),
SensorDesc(key='sterilize_last_time', device_class='timestamp',
entity_category='diagnostic',
rep_fn=lambda rep: _parse_iso_utc(
rep.get('x.com.samsung.da.sterilizeLastTime'))),
SensorDesc(key='sterilize_plan_time', device_class='timestamp',
entity_category='diagnostic',
rep_fn=lambda rep: _parse_iso_utc(
rep.get('x.com.samsung.da.sterilizePlanTime'))),
SensorDesc(key='filter_clean_remain_time', field='x.com.samsung.da.filterCleanRemainTime',
icon='mdi:timer-sand',
entity_category='diagnostic'),
),
)
FAVORITE_CAPACITY = Capability(
href='/favorite/capacity/vs/0',
poll_tier='cold',
entities=(
SwitchDesc(key='favorite_capacity_enabled', field='x.com.samsung.da.switchCapacity',
icon='mdi:star-outline',
entity_category='config',
value_fn=lambda v: v == 'On',
write_fn=lambda p, rep, href=None: (
['favorite', 'capacity', 'vs', '0'],
{'x.com.samsung.da.switchCapacity': 'On' if p == 'On' else 'Off'})),
SelectDesc(key='favorite_capacity', field='x.com.samsung.da.defaultCapacity',
icon='mdi:cup-water',
entity_category='config',
options_field='x.com.samsung.da.capacityList',
write_fn=lambda p, rep, href=None: (
['favorite', 'capacity', 'vs', '0'],
{'x.com.samsung.da.defaultCapacity': p})),
),
)
LOCK = Capability(
href='/status/lock/vs/0',
poll_tier='warm',
entities=(
SwitchDesc(key='hotwater_lock', field='x.com.samsung.da.hotwaterLock',
device_class='lock',
entity_category='config',
value_fn=lambda v: v != 'Unlocked',
write_fn=lambda p, rep, href=None: (
['status', 'lock', 'vs', '0'],
{'x.com.samsung.da.hotwaterLock': 'Locked' if p == 'On' else 'Unlocked'})),
SwitchDesc(key='coldwater_lock', field='x.com.samsung.da.coldwaterLock',
device_class='lock',
entity_category='config',
value_fn=lambda v: v != 'Unlocked',
write_fn=lambda p, rep, href=None: (
['status', 'lock', 'vs', '0'],
{'x.com.samsung.da.coldwaterLock': 'Locked' if p == 'On' else 'Unlocked'})),
SwitchDesc(key='buzz_lock', field='x.com.samsung.da.buzzLock',
device_class='lock',
entity_category='config',
value_fn=lambda v: v != 'Unlocked',
write_fn=lambda p, rep, href=None: (
['status', 'lock', 'vs', '0'],
{'x.com.samsung.da.buzzLock': 'Locked' if p == 'On' else 'Unlocked'})),
),
)
# ---------------------------------------------------------------------------
# Water-purifier-scoped coverage: hrefs with no user-actionable state or no
# confirmed contract, following the 'don't guess' rule.
# ---------------------------------------------------------------------------
_WP_IGNORED = [
# supportedModes carries a single opaque wizard-workflow token
# ('HOMECARE_WIZARD_V2') and modes reports a completely different,
# unrelated value ('WATERFILTER_DISABLE') not even present in
# supportedModes -- internal plumbing, not a real user-facing mode
# select. OCF-standard /mode/0 mirrors the same vendor resource but is
# already covered by the global ignored.IGNORED (fridge's OCF-native
# vacation-mode flag shares that href).
'/mode/vs/0',
# Static support-flags blob (automation.supported.modes/options) -- no
# live "current automation setting" field to expose.
'/automation/waterpurifier/vs/0',
]
COVERAGE = [Capability(href=h) for h in _WP_IGNORED]
@@ -34,6 +34,9 @@
"door_open": {
"name": "Door"
},
"filter_door_status": {
"name": "Filter door"
},
"firmware_update": {
"name": "Firmware update available"
},
@@ -52,6 +55,9 @@
"periodic_air_sensing": {
"name": "Periodic air sensing"
},
"pouring": {
"name": "Pouring"
},
"power_state": {
"name": "Power state"
},
@@ -114,6 +120,9 @@
"delay_start_hours": {
"name": "Delay start"
},
"dispense_capacity": {
"name": "Dispense capacity"
},
"instance_setpoint": {
"name": "{instance_name} setpoint"
},
@@ -122,6 +131,9 @@
},
"sound_volume": {
"name": "Sound volume"
},
"target_humidity": {
"name": "Target humidity"
}
},
"select": {
@@ -193,6 +205,9 @@
"8f": "Baby Care"
}
},
"dispense_type": {
"name": "Dispense type"
},
"door_alert": {
"name": "Door alarm",
"state": {
@@ -205,6 +220,8 @@
"dryer_cycle_table_03": {
"name": "Cycle",
"state": {
"01": "Normal",
"06": "Time dry",
"16": "Cotton",
"18": "Synthetics",
"19": "Delicates",
@@ -221,6 +238,9 @@
"1f": "Mixed load"
}
},
"favorite_capacity": {
"name": "Favorite capacity"
},
"finish_sound": {
"name": "Finish sound",
"state": {
@@ -252,6 +272,9 @@
"extra_high": "Extra high"
}
},
"hot_water_temperature": {
"name": "Hot water temperature"
},
"ice_type": {
"name": "{instance_name} type",
"state": {
@@ -290,6 +313,9 @@
"air_fry": "Air fry"
}
},
"operating_mode": {
"name": "Operating mode"
},
"pantry_zone_mode": {
"name": "Pantry zone mode",
"state": {
@@ -397,10 +423,13 @@
"38": "Cotton Dry",
"39": "Synthetics Dry",
"3a": "Drum Clean",
"52": "Eco Cold",
"53": "Heavy Duty",
"54": "Towels",
"55": "Activewear",
"57": "Delicate",
"5e": "Rinse+Spin",
"60": "Self Clean+",
"65": "Colors",
"66": "Denim",
"7c": "Whites",
@@ -513,6 +542,9 @@
"fan_speed_level": {
"name": "Fan speed level"
},
"filter_clean_remain_time": {
"name": "Filter clean remaining time"
},
"filter_progress": {
"name": "Filter progress"
},
@@ -537,6 +569,9 @@
"hood_filter_capacity": {
"name": "Filter capacity"
},
"humidity": {
"name": "Humidity"
},
"hood_filter_usage": {
"name": "Filter usage"
},
@@ -606,6 +641,18 @@
"selfcheck_status": {
"name": "Self-check status"
},
"sterilize_last_time": {
"name": "Sterilize last time"
},
"sterilize_period": {
"name": "Sterilize period"
},
"sterilize_plan_time": {
"name": "Sterilize plan time"
},
"sterilize_run_time": {
"name": "Sterilize run time"
},
"super_fine_dust": {
"name": "Super fine dust"
},
@@ -615,6 +662,9 @@
"water_liters": {
"name": "Water consumption"
},
"waterpurifier_status": {
"name": "Status"
},
"machine_state": {
"name": "Machine state",
"state": {
@@ -661,6 +711,9 @@
"bubble_soak": {
"name": "Bubble soak"
},
"buzz_lock": {
"name": "Buzzer lock"
},
"cabinet_light_dim": {
"name": "Brighten gradually"
},
@@ -670,6 +723,9 @@
"child_lock": {
"name": "Child lock"
},
"coldwater_lock": {
"name": "Cold water lock"
},
"defrost_delay": {
"name": "Defrost delay"
},
@@ -679,9 +735,15 @@
"fast_preheat": {
"name": "Fast preheat"
},
"favorite_capacity_enabled": {
"name": "Favorite capacity"
},
"fridge_sound": {
"name": "Sound"
},
"hotwater_lock": {
"name": "Hot water lock"
},
"ice_maker_enabled": {
"name": "Ice maker"
},
@@ -34,6 +34,9 @@
"door_open": {
"name": "Deur"
},
"filter_door_status": {
"name": "Filterdeur"
},
"firmware_update": {
"name": "Firmware-update beschikbaar"
},
@@ -52,6 +55,9 @@
"periodic_air_sensing": {
"name": "Periodieke luchtmeting"
},
"pouring": {
"name": "Schenken"
},
"power_state": {
"name": "Voedingsstatus"
},
@@ -114,6 +120,9 @@
"delay_start_hours": {
"name": "Uitgestelde start"
},
"dispense_capacity": {
"name": "Schenkhoeveelheid"
},
"instance_setpoint": {
"name": "Instelpunt {instance_name}"
},
@@ -122,6 +131,9 @@
},
"sound_volume": {
"name": "Geluidsvolume"
},
"target_humidity": {
"name": "Doelvochtigheid"
}
},
"select": {
@@ -193,6 +205,9 @@
"8f": "Babyverzorging"
}
},
"dispense_type": {
"name": "Type dispenser"
},
"door_alert": {
"name": "Deuralarm",
"state": {
@@ -205,6 +220,8 @@
"dryer_cycle_table_03": {
"name": "Programma",
"state": {
"01": "Normaal",
"06": "Tijdprogramma",
"16": "Katoen",
"18": "Synthetisch",
"19": "Fijne was",
@@ -221,6 +238,9 @@
"1f": "Gemengde was"
}
},
"favorite_capacity": {
"name": "Favoriete capaciteit"
},
"finish_sound": {
"name": "Eindgeluid",
"state": {
@@ -252,6 +272,9 @@
"extra_high": "Extra hoog"
}
},
"hot_water_temperature": {
"name": "Temperatuur warm water"
},
"ice_type": {
"name": "Type {instance_name}",
"state": {
@@ -290,6 +313,9 @@
"air_fry": "Airfryen"
}
},
"operating_mode": {
"name": "Bedrijfsmodus"
},
"pantry_zone_mode": {
"name": "Modus voorraadzone",
"state": {
@@ -397,10 +423,13 @@
"38": "Katoen drogen",
"39": "Synthetisch drogen",
"3a": "Trommel reinigen",
"52": "Eco koud",
"53": "Intensief",
"54": "Handdoeken",
"55": "Sportkleding",
"57": "Fijn",
"5e": "Spoelen+centrifugeren",
"60": "Zelfreinigend+",
"65": "Bonte was",
"66": "Spijkergoed",
"7c": "Witte was",
@@ -513,6 +542,9 @@
"fan_speed_level": {
"name": "Ventilatorniveau"
},
"filter_clean_remain_time": {
"name": "Resterende tijd filterreiniging"
},
"filter_progress": {
"name": "Filterverbruik"
},
@@ -537,6 +569,9 @@
"hood_filter_capacity": {
"name": "Filtercapaciteit"
},
"humidity": {
"name": "Luchtvochtigheid"
},
"hood_filter_usage": {
"name": "Filterverbruik"
},
@@ -606,6 +641,18 @@
"selfcheck_status": {
"name": "Status zelfcontrole"
},
"sterilize_last_time": {
"name": "Laatste sterilisatie"
},
"sterilize_period": {
"name": "Sterilisatie-interval"
},
"sterilize_plan_time": {
"name": "Geplande sterilisatie"
},
"sterilize_run_time": {
"name": "Sterilisatieduur"
},
"super_fine_dust": {
"name": "Ultrafijnstof"
},
@@ -615,6 +662,9 @@
"water_liters": {
"name": "Waterverbruik"
},
"waterpurifier_status": {
"name": "Status"
},
"machine_state": {
"name": "Machinestatus",
"state": {
@@ -661,6 +711,9 @@
"bubble_soak": {
"name": "Bubble Soak"
},
"buzz_lock": {
"name": "Zoemerslot"
},
"cabinet_light_dim": {
"name": "Geleidelijk feller"
},
@@ -670,6 +723,9 @@
"child_lock": {
"name": "Kinderslot"
},
"coldwater_lock": {
"name": "Koudwaterslot"
},
"defrost_delay": {
"name": "Ontdooien uitstellen"
},
@@ -679,9 +735,15 @@
"fast_preheat": {
"name": "Snel voorverwarmen"
},
"favorite_capacity_enabled": {
"name": "Favoriete capaciteit"
},
"fridge_sound": {
"name": "Geluid"
},
"hotwater_lock": {
"name": "Heetwaterslot"
},
"ice_maker_enabled": {
"name": "IJsmaker"
},