Add dehumidifier display switch and watertank lighting (issues #271, #231)

The TP1X_DA_AC_DHM_01001_0000 revision (model AY70H18100GTD) additionally
reports /display/vs/0 (same shape as air_purifier's screen toggle, reused
directly) and /watertank/lighting/vs/0 (on/off, color, and brightness for
the tank's ambient light, plus a diagnostic alarm-status flag). Both issues
submitted the identical dump, so one fix covers both reports.

Also adds x.com.st.d.dehumidifier to the /oic/d device-type routing table
now that a dump confirms it.
This commit is contained in:
Marc Billow
2026-08-03 18:59:22 +00:00
parent 51103fa341
commit 3ef64eae52
12 changed files with 2018 additions and 1284 deletions
@@ -259,6 +259,7 @@ _OIC_TYPE_TO_KEY: dict[str, str] = {
"oic.d.refrigerator": "refrigerator",
"oic.d.washer": "washer",
"x.com.st.d.airqualitysensor": "air_monitor",
"x.com.st.d.dehumidifier": "dehumidifier",
"x.com.st.d.hood": "range_hood", # AHD-WW-TP1-22-COMMON
"x.com.st.d.stickcleaner": "vacuum_station",
"x.com.st.d.steamcloset": "air_dresser",
@@ -2,10 +2,13 @@
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.
Capability objects are reused directly rather than duplicated. The
TP1X_DA_AC_DHM_01001_0000 revision (issues #271/#231) also reports
air_purifier.py's screen-on/off resource on the identical href/shape, so
that's reused too rather than re-defined.
"""
from ..capabilities import airconditioner, common, dehumidifier, ignored
from ..capabilities import airconditioner, air_purifier, common, dehumidifier, ignored
from ._base import DeviceRegistry, _build
REGISTRY = DeviceRegistry(
@@ -17,9 +20,11 @@ REGISTRY = DeviceRegistry(
*common.POWER,
dehumidifier.MODE,
dehumidifier.HUMIDITY,
dehumidifier.WATERTANK_LIGHTING,
airconditioner.AUTO_CLEAN,
airconditioner.AIR_FILTER,
airconditioner.MUTE_ONCE,
air_purifier.DISPLAY,
*dehumidifier.COVERAGE,
]
),
@@ -9,7 +9,7 @@ entities rather than folded into one card.
"""
from ..capability import Capability
from ..entities import NumberDesc, SelectDesc, SensorDesc
from ..entities import NumberDesc, SelectDesc, SensorDesc, SwitchDesc
from .common import int_or_none
@@ -77,6 +77,60 @@ HUMIDITY = Capability(
),
)
# Water-tank ambient light (issues #271/#231, TP1X_DA_AC_DHM_01001_0000):
# on/off, color, and brightness are three independent controls on this one
# resource. `waterfullAlarmStatus` differs between the two dumps that
# reported this href (On vs. Off) so it's a real live flag, not a constant --
# but its exact meaning (tank actually full vs. the chime feature merely
# enabled) isn't confirmed by either dump alone, and /alarms/vs/0's
# alarm_code already surfaces a live WaterTankFull condition when one fires
# (see common._active_alarm_codes), so this is exposed read-only as a plain
# diagnostic value rather than guessed at as a binary_sensor.
WATERTANK_LIGHTING = Capability(
href="/watertank/lighting/vs/0",
poll_tier="cold",
entities=(
SwitchDesc(
key="watertank_light",
field="status",
icon="mdi:led-on",
entity_category="config",
value_fn=lambda v: v == "On",
write_fn=lambda p, rep, href=None: (
["watertank", "lighting", "vs", "0"],
{"status": "On" if p == "On" else "Off"},
),
),
SelectDesc(
key="watertank_light_color",
field="colorOption",
icon="mdi:palette",
entity_category="config",
options_field="colorSupportedList",
write_fn=lambda p, rep, href=None: (
["watertank", "lighting", "vs", "0"],
{"colorOption": p},
),
),
SelectDesc(
key="watertank_light_brightness",
field="mode",
icon="mdi:brightness-6",
entity_category="config",
options_field="modeSupportedList",
write_fn=lambda p, rep, href=None: (
["watertank", "lighting", "vs", "0"],
{"mode": p},
),
),
SensorDesc(
key="watertank_full_alarm_status",
field="waterfullAlarmStatus",
entity_category="diagnostic",
),
),
)
# ---------------------------------------------------------------------------
# Dehumidifier-scoped coverage: vendor plumbing with no user-actionable state
# or no documented write contract, following the same 'don't guess' rule as
File diff suppressed because it is too large Load Diff
@@ -621,6 +621,22 @@
},
"zone_mode": {
"name": "Zone mode"
},
"watertank_light_color": {
"name": "Water tank light color",
"state": {
"warmwhite": "Warm white",
"naturalwhite": "Natural white",
"coolwhite": "Cool white",
"green": "Green",
"purple": "Purple",
"blue": "Blue",
"orange": "Orange",
"pink": "Pink"
}
},
"watertank_light_brightness": {
"name": "Water tank light brightness"
}
},
"sensor": {
@@ -969,6 +985,9 @@
},
"zone_temperature": {
"name": "Zone temperature"
},
"watertank_full_alarm_status": {
"name": "Water tank full alarm status"
}
},
"switch": {
@@ -1136,6 +1155,9 @@
},
"away_mode": {
"name": "Away mode"
},
"watertank_light": {
"name": "Water tank light"
}
},
"time": {
@@ -735,6 +735,22 @@
},
"zone_mode": {
"name": "Modo de zona"
},
"watertank_light_color": {
"name": "Color de la luz del depósito de agua",
"state": {
"warmwhite": "Blanco cálido",
"naturalwhite": "Blanco natural",
"coolwhite": "Blanco frío",
"green": "Verde",
"purple": "Púrpura",
"blue": "Azul",
"orange": "Naranja",
"pink": "Rosa"
}
},
"watertank_light_brightness": {
"name": "Brillo de la luz del depósito de agua"
}
},
"sensor": {
@@ -1083,6 +1099,9 @@
},
"zone_temperature": {
"name": "Temperatura de zona"
},
"watertank_full_alarm_status": {
"name": "Estado de alarma de depósito de agua lleno"
}
},
"switch": {
@@ -1250,6 +1269,9 @@
},
"zone_power": {
"name": "Alimentación de zona"
},
"watertank_light": {
"name": "Luz del depósito de agua"
}
},
"time": {
@@ -621,6 +621,22 @@
},
"zone_mode": {
"name": "Modalità zona"
},
"watertank_light_color": {
"name": "Colore della luce del serbatoio dell'acqua",
"state": {
"warmwhite": "Bianco caldo",
"naturalwhite": "Bianco naturale",
"coolwhite": "Bianco freddo",
"green": "Verde",
"purple": "Viola",
"blue": "Blu",
"orange": "Arancione",
"pink": "Rosa"
}
},
"watertank_light_brightness": {
"name": "Luminosità della luce del serbatoio dell'acqua"
}
},
"sensor": {
@@ -969,6 +985,9 @@
},
"zone_temperature": {
"name": "Temperatura zona"
},
"watertank_full_alarm_status": {
"name": "Stato allarme serbatoio d'acqua pieno"
}
},
"switch": {
@@ -1136,6 +1155,9 @@
},
"away_mode": {
"name": "Modalità assenza"
},
"watertank_light": {
"name": "Luce del serbatoio dell'acqua"
}
},
"time": {
@@ -621,6 +621,22 @@
},
"zone_mode": {
"name": "Zonemodus"
},
"watertank_light_color": {
"name": "Kleur van het waterreservoirlicht",
"state": {
"warmwhite": "Warm wit",
"naturalwhite": "Natuurlijk wit",
"coolwhite": "Koel wit",
"green": "Groen",
"purple": "Paars",
"blue": "Blauw",
"orange": "Oranje",
"pink": "Roze"
}
},
"watertank_light_brightness": {
"name": "Helderheid van het waterreservoirlicht"
}
},
"sensor": {
@@ -969,6 +985,9 @@
},
"zone_temperature": {
"name": "Zonetemperatuur"
},
"watertank_full_alarm_status": {
"name": "Status alarm waterreservoir vol"
}
},
"switch": {
@@ -1136,6 +1155,9 @@
},
"away_mode": {
"name": "Afwezigheidsmodus"
},
"watertank_light": {
"name": "Waterreservoirlicht"
}
},
"time": {
+426
View File
@@ -0,0 +1,426 @@
{
"device0": [
{
"rt": [
"x.com.samsung.devcol",
"oic.wk.col"
],
"if": [
"oic.if.baseline",
"oic.if.ll",
"oic.if.b"
]
},
{
"href": "/personality/presence/vs/0",
"rep": {
"x.com.samsung.da.items": [
{
"x.com.samsung.da.id": "",
"x.com.samsung.da.deviceId": "**REDACTED**",
"x.com.samsung.da.value": ""
}
]
}
},
{
"href": "/realtimenotiforclient/vs/0",
"rep": {
"x.com.samsung.da.timeforshortnoti": "0",
"x.com.samsung.da.longnotisubscription": "false",
"x.com.samsung.da.periodicnotisubscription": "true"
}
},
{
"href": "/filter/airdustfilter/vs/0",
"rep": {
"x.com.samsung.da.filterUsage": "100",
"x.com.samsung.da.filterUsageResolution": "1",
"x.com.samsung.da.filterDesiredUsage": "224",
"x.com.samsung.da.filterStatus": "wash",
"x.com.samsung.da.filterCapacity": "224",
"x.com.samsung.da.filterCapacityUnit": "Hour",
"x.com.samsung.da.filterResetType": [
"washable"
],
"x.com.samsung.da.supportedFilterDesiredUsage": [
"224",
"300",
"500",
"700"
]
}
},
{
"href": "/mode/convenient/vs/0",
"rep": {
"x.com.samsung.da.supportedModes": [
"Off",
"Sleep"
]
}
},
{
"href": "/option/autoclean/vs/0",
"rep": {
"x.com.samsung.da.status": "Stop",
"x.com.samsung.da.settingStatus": "On",
"x.com.samsung.da.progress": "0",
"cleaningTime": "30",
"minCleaningTime": "10",
"maxCleaningTime": "60",
"cleaningTimeUnit": "10",
"x.com.samsung.da.supportedStatus": [
"Start",
"TimedClean",
"Stop"
],
"x.com.samsung.da.supportedSettingStatus": [
"On",
"TimedClean",
"Off"
]
}
},
{
"href": "/alarms/vs/0",
"rep": {
"x.com.samsung.da.items": [
{
"x.com.samsung.da.id": "0",
"x.com.samsung.da.description": "Alarm",
"x.com.samsung.da.alarmType": "Device",
"x.com.samsung.da.code": "ErrorCode_OFF",
"x.com.samsung.da.triggeredTime": "2026-08-03T14:54:15",
"x.com.samsung.da.state": "Deleted"
},
{
"x.com.samsung.da.id": "2",
"x.com.samsung.da.description": "Alarm",
"x.com.samsung.da.alarmType": "Device",
"x.com.samsung.da.code": "WaterTankFull_OFF",
"x.com.samsung.da.triggeredTime": "2026-08-03T14:54:15",
"x.com.samsung.da.state": "Deleted"
}
]
}
},
{
"href": "/energy/consumption/vs/0",
"rep": {
"x.com.samsung.da.instantaneousPower": "145.000000",
"x.com.samsung.da.cumulativePower": "101100",
"x.com.samsung.da.cumulativeSavedPower": "0",
"x.com.samsung.da.cumulativeUnit": "Wh",
"x.com.samsung.da.instantaneousPowerUnit": "W",
"x.com.samsung.da.cumulativePowerType": "total"
}
},
{
"href": "/mode/vs/0",
"rep": {
"x.com.samsung.da.supportedModes": [
"Smart",
"Max",
"High",
"Medium",
"Quiet",
"ClothesDrying"
],
"x.com.samsung.da.modes": [
"Quiet"
],
"x.com.samsung.da.options": [
"Volume_Mute",
"RacInfo_None",
"UpdateAllow_NotAllowed",
"WelcomeCoolingState_Off"
]
}
},
{
"href": "/power/vs/0",
"rep": {
"x.com.samsung.da.power": "On",
"operationNumber": "1",
"displayCondition": "Enable"
}
},
{
"href": "/sensors/vs/0",
"rep": {}
},
{
"href": "/information/vs/0",
"rep": {
"x.com.samsung.da.modelNum": "TP1X_DA_AC_DHM_01001_0000|10283341|77000000001700000A00000000000000",
"x.com.samsung.da.description": "TP1X_DA_AC_DHM_01001_0000",
"x.com.samsung.da.serialNum": "**REDACTED**",
"x.com.samsung.da.otnDUID": "**REDACTED**",
"x.com.samsung.da.diagProtocolType": "BLE_OCF",
"x.com.samsung.da.diagLogType": [
"errCode",
"dump"
],
"x.com.samsung.da.diagDumpType": "file",
"x.com.samsung.da.diagEndPoint": "SSM",
"x.com.samsung.da.diagMnid": "0AJT",
"x.com.samsung.da.diagSetupid": "AY0",
"x.com.samsung.da.diagMinVersion": "3.0",
"x.com.samsung.da.diagTsId": "DA01",
"x.com.samsung.da.items": [
{
"x.com.samsung.da.id": "0",
"x.com.samsung.da.description": "Version",
"x.com.samsung.da.type": "Software",
"x.com.samsung.da.number": "02544A260401",
"x.com.samsung.da.newVersionAvailable": "0"
},
{
"x.com.samsung.da.id": "1",
"x.com.samsung.da.description": "Version",
"x.com.samsung.da.type": "Firmware",
"x.com.samsung.da.number": "02833A26010600,FFFFFFFFFFFFFF",
"x.com.samsung.da.newVersionAvailable": "0"
},
{
"x.com.samsung.da.id": "2",
"x.com.samsung.da.description": "Version",
"x.com.samsung.da.type": "Outdoor",
"x.com.samsung.da.number": "02590A10000100,FFFFFFFFFFFFFF"
},
{
"x.com.samsung.da.id": "4",
"x.com.samsung.da.description": "Version",
"x.com.samsung.da.type": "Touch IC",
"x.com.samsung.da.number": "02552A23020200,FFFFFFFFFFFFFF"
}
]
}
},
{
"href": "/file/information/vs/0",
"rep": {
"x.com.samsung.timeoffset": "+09:00",
"x.com.samsung.supprtedtype": 1
}
},
{
"href": "/configuration/vs/0",
"rep": {
"x.com.samsung.da.region": "0000000000",
"x.com.samsung.da.airconOptionList": [
"SingleCommand_1",
"DR",
"HOMECARE_WIZARD_V2",
"PRODUCT_GLOBAL",
"AI_RAC_GLOBAL_2.0",
"AI_Heat"
]
}
},
{
"href": "/humidity/vs/0",
"rep": {
"x.com.samsung.da.humidity": "63",
"x.com.samsung.da.desiredHumidity": "60",
"increment": "5",
"x.com.samsung.da.fivepercentHumidity": "64"
}
},
{
"href": "/drlc/vs/0",
"rep": {
"x.com.samsung.da.drlcLevel": "0",
"x.com.samsung.da.durationminutes": "0",
"x.com.samsung.da.start": "1970-01-01T00:00:00Z",
"x.com.samsung.da.override": "Off",
"x.com.samsung.da.realSaving": "Off"
}
},
{
"href": "/availablecontrolsets/vs/0",
"rep": {
"x.com.samsung.da.sets": "04001E46070A5A00000000000000",
"x.com.samsung.da.id": "DHM",
"x.com.samsung.da.version": "1.0"
}
},
{
"href": "/da/softreset/vs/0",
"rep": {
"x.com.samsung.da.softwarereset": "false"
}
},
{
"href": "/keepnormalstate/vs/0",
"rep": {
"x.com.samsung.da.keepnormal": 1
}
},
{
"href": "/otninformation/vs/0",
"rep": {
"x.com.samsung.da.target": "",
"x.com.samsung.da.newVersionAvailable": "false",
"x.com.samsung.da.newVersionNo": "00000000",
"x.com.samsung.da.currentVersionInfo": "00000000",
"otnStatus": "None",
"flashingProgress": "",
"otnTarget": "main",
"otnCompleteDate": "2026-06-24",
"otnList": [
{
"type": "WIFI",
"modelId": "ADH-WW-TP1-23-AY7100",
"versions": [
"18260401"
],
"visVersion": "260401"
},
{
"type": "Micom",
"modelId": "042110283341FFFFFFFF",
"versions": [
"26010600",
"FFFFFFFF"
],
"visVersion": "260106"
},
{
"type": "Micom",
"modelId": "042110259041FFFFFFFF",
"versions": [
"10000100",
"FFFFFFFF"
],
"visVersion": "100001"
},
{
"type": "Micom",
"modelId": "042110255241FFFFFFFF",
"versions": [
"23020200",
"FFFFFFFF"
],
"visVersion": "230202"
}
]
}
},
{
"href": "/timezone/vs/0",
"rep": {
"timezoneid": "Asia/Seoul",
"offset": "+09:00",
"DST": "OFF"
}
},
{
"href": "/option/muteonce/vs/0",
"rep": {
"muteonce": "Off"
}
},
{
"href": "/reserverulesets/vs/0",
"rep": {
"x.com.samsung.da.sets": "3F1E461E461E46FFFFFFFF1E4600",
"x.com.samsung.da.id": "DHM",
"x.com.samsung.da.version": "1.0"
}
},
{
"href": "/display/vs/0",
"rep": {
"mode": "On",
"supportedModes": [
"On",
"Off"
]
}
},
{
"href": "/connectionconfig/vs/0",
"rep": {
"autoReconnectionMinVersion": "1.0",
"autoReconnection": "true",
"autoReconnectionProtocolType": [
"helper_hotspot",
"ble_ocf"
],
"supportedWiFiAuthType": [
"OPEN",
"WEP",
"WPA-PSK",
"WPA2-PSK",
"SAE"
],
"supportedWiFiCryptoType": [
"TKIP",
"AES",
"WEP-64",
"WEP-128"
],
"supportedWiFiFreq": [
"2.4G"
],
"calmConnectionCare": {
"version": "1.0",
"role": [
"things"
]
}
}
},
{
"href": "/welcome/humidity/vs/0",
"rep": {
"requestId": "0000",
"operatingStatus": "None"
}
},
{
"href": "/kidslock/vs/0",
"rep": {
"x.com.samsung.da.kidsLock": "Ready"
}
},
{
"href": "/watertank/lighting/vs/0",
"rep": {
"status": "On",
"colorOption": "NaturalWhite",
"mode": "High",
"waterfullAlarmStatus": "On",
"colorSupportedList": [
"WarmWhite",
"NaturalWhite",
"CoolWhite",
"Green",
"Purple",
"Blue",
"Orange",
"Pink"
],
"modeSupportedList": [
"High",
"Low"
]
}
},
{
"href": "/wirelessinfo/vs/0",
"rep": {
"macaddressWiFi": "**REDACTED**",
"macaddressBLE": "**REDACTED**"
}
},
{
"href": "/quickcontrol/info/vs/0",
"rep": {
"supportedVersion": "1.0"
}
}
]
}
+27
View File
@@ -0,0 +1,27 @@
{
"state_keys": [
"air_filter_status",
"air_filter_threshold",
"air_filter_usage",
"air_filter_usage_hours",
"alarm_code",
"auto_clean",
"auto_clean_progress",
"auto_clean_running",
"child_lock",
"display",
"energy_kwh",
"energy_saved_kwh",
"firmware_update",
"humidity",
"mute_once",
"operating_mode",
"power_switch",
"power_watts",
"target_humidity",
"watertank_full_alarm_status",
"watertank_light",
"watertank_light_brightness",
"watertank_light_color"
]
}
@@ -0,0 +1,94 @@
"""Tests for the TP1X_DA_AC_DHM_01001_0000 dehumidifier revision
(issues #271/#231, model AY70H18100GTD).
Both issues submitted the identical dump: same board, same two previously
unbound hrefs (/display/vs/0, /watertank/lighting/vs/0). Confirms both the
new capabilities and the /oic/d device-type routing added alongside them.
"""
from custom_components.localthings.registry.adapter import flatten
from custom_components.localthings.registry.by_type import (
for_device_by_model,
for_device_by_oic_type,
)
from custom_components.localthings.registry.discovery import discover
from custom_components.localthings.registry.entities import SelectDesc, SwitchDesc
from tests.conftest import _load_device
def _dehumidifier():
resources = _load_device("dehumidifier_tp1x_dhm01001")
info = resources["/information/vs/0"]
reg = for_device_by_model(
info["x.com.samsung.da.modelNum"],
info["x.com.samsung.da.description"],
)
return reg, resources
def _bound():
reg, resources = _dehumidifier()
return discover(resources, reg.capabilities, reg.pattern_capabilities), resources
def _state():
bound, resources = _bound()
return flatten(bound, resources)
def _desc(key):
bound, _ = _bound()
return next(b.desc for b in bound if b.desc.key == key)
def test_model_resolves_to_dehumidifier_registry():
reg, _ = _dehumidifier()
assert reg is not None and reg.name == "dehumidifier"
def test_oic_device_type_resolves_to_dehumidifier_registry():
reg = for_device_by_oic_type(("oic.wk.d", "x.com.st.d.dehumidifier"))
assert reg is not None and reg.name == "dehumidifier"
def test_no_unbound_hrefs():
reg, resources = _dehumidifier()
unbound = []
discover(resources, reg.capabilities, reg.pattern_capabilities, log=unbound.append)
assert unbound == []
def test_display_switch_present_and_reads_on():
state = _state()
assert state["display"] is True
def test_watertank_light_switch_write_contract():
desc = _desc("watertank_light")
assert isinstance(desc, SwitchDesc)
path, body = desc.write_fn("On", {})
assert path == ["watertank", "lighting", "vs", "0"]
assert body == {"status": "On"}
def test_watertank_light_color_options_come_from_live_supported_list():
desc = _desc("watertank_light_color")
assert isinstance(desc, SelectDesc)
assert desc.options_field == "colorSupportedList"
path, body = desc.write_fn("Green", {})
assert path == ["watertank", "lighting", "vs", "0"]
assert body == {"colorOption": "Green"}
def test_watertank_light_brightness_options_come_from_live_supported_list():
desc = _desc("watertank_light_brightness")
assert isinstance(desc, SelectDesc)
assert desc.options_field == "modeSupportedList"
path, body = desc.write_fn("Low", {})
assert path == ["watertank", "lighting", "vs", "0"]
assert body == {"mode": "Low"}
def test_watertank_full_alarm_status_exposed_read_only():
state = _state()
assert state["watertank_full_alarm_status"] == "On"
+17
View File
@@ -1226,6 +1226,23 @@ def test_registry_reproduces_golden_state_keys_for_range_ne6516a():
)
def test_registry_reproduces_golden_state_keys_for_dehumidifier_tp1x_dhm01001():
"""TP1X_DA_AC_DHM_01001_0000 revision (issues #271/#231) additionally
reports /display/vs/0 (air_purifier.DISPLAY's exact shape, reused as-is)
and /watertank/lighting/vs/0 (new WATERTANK_LIGHTING capability), both
unbound on the original issue #88 dump."""
from tests.conftest import _load_device
resources = _load_device("dehumidifier_tp1x_dhm01001")
golden = json.loads((GOLDEN / "dehumidifier_tp1x_dhm01001.json").read_text())
state_keys = _new_state_keys("dehumidifier_tp1x_dhm01001", resources)
assert set(state_keys) == set(golden["state_keys"]), (
f"state_keys mismatch:\n"
f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n"
f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}"
)
def test_resources_from_batch_preferred_over_flat():
from tests.conftest import _resources_from_dump