Add device support for Samsung dehumidifiers (issue #88)
The AY18CG7500GED dehumidifier (modelNum TP1X_DA_AC_DHM_01001_0000) shares the DA_AC_ board family with the room-AC models but carries the '_DHM_' token instead of '_RAC_'/'_PRAC_'/'_WAC_', so it fell into the unknown-device-type fallback. Add a new dehumidifier registry, routed via a '_DHM_' modelNum fallback rule, distinct from airconditioner since target humidity (not temperature) is the primary control and there's no climate composite. Reuses airconditioner.py's AUTO_CLEAN/AIR_FILTER/MUTE_ONCE capabilities directly (identical resource shapes on the shared board family). Adds a new humidity sensor + target-humidity number pair and an operating-mode select. Per the adding-device-support skill's "never hard-code the one dump's values" rule, the target-humidity number has no hardcoded min/max (falls back to HA's own 0-100 default for a percentage field) and reads its step live from the device's own `increment` field rather than a spec-sheet-derived constant. The operating-mode select's options come live from supportedModes. /mode/convenient/vs/0 is left unmodeled: only supportedModes is present on this dump, with no live current-value field to confirm a read/write contract. Confirmed against the issue #88 diagnostics dump with zero unbound hrefs. Updates the README's supported-appliance-types table for the new device type.
This commit is contained in:
@@ -23,6 +23,7 @@ Your state stays on your LAN: HA talks to the appliance over a direct DTLS sessi
|
||||
|---|---|
|
||||
| Air conditioner | `by_type/airconditioner.py` |
|
||||
| Air purifier | `by_type/air_purifier.py` |
|
||||
| Dehumidifier | `by_type/dehumidifier.py` |
|
||||
| Dryer | `by_type/dryer.py` |
|
||||
| Oven | `by_type/oven.py` |
|
||||
| Cooktop (read-only burner status) | `by_type/cooktop.py` |
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
__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,
|
||||
@@ -122,6 +123,13 @@ 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'
|
||||
# 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 ''):
|
||||
|
||||
@@ -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,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]
|
||||
@@ -118,6 +118,9 @@
|
||||
},
|
||||
"sound_volume": {
|
||||
"name": "Sound volume"
|
||||
},
|
||||
"target_humidity": {
|
||||
"name": "Target humidity"
|
||||
}
|
||||
},
|
||||
"select": {
|
||||
@@ -286,6 +289,9 @@
|
||||
"air_fry": "Air fry"
|
||||
}
|
||||
},
|
||||
"operating_mode": {
|
||||
"name": "Operating mode"
|
||||
},
|
||||
"pantry_zone_mode": {
|
||||
"name": "Pantry zone mode",
|
||||
"state": {
|
||||
@@ -523,6 +529,9 @@
|
||||
"hood_filter_capacity": {
|
||||
"name": "Filter capacity"
|
||||
},
|
||||
"humidity": {
|
||||
"name": "Humidity"
|
||||
},
|
||||
"hood_filter_usage": {
|
||||
"name": "Filter usage"
|
||||
},
|
||||
|
||||
@@ -118,6 +118,9 @@
|
||||
},
|
||||
"sound_volume": {
|
||||
"name": "Geluidsvolume"
|
||||
},
|
||||
"target_humidity": {
|
||||
"name": "Doelvochtigheid"
|
||||
}
|
||||
},
|
||||
"select": {
|
||||
@@ -286,6 +289,9 @@
|
||||
"air_fry": "Airfryen"
|
||||
}
|
||||
},
|
||||
"operating_mode": {
|
||||
"name": "Bedrijfsmodus"
|
||||
},
|
||||
"pantry_zone_mode": {
|
||||
"name": "Modus voorraadzone",
|
||||
"state": {
|
||||
@@ -523,6 +529,9 @@
|
||||
"hood_filter_capacity": {
|
||||
"name": "Filtercapaciteit"
|
||||
},
|
||||
"humidity": {
|
||||
"name": "Luchtvochtigheid"
|
||||
},
|
||||
"hood_filter_usage": {
|
||||
"name": "Filterverbruik"
|
||||
},
|
||||
|
||||
+386
@@ -0,0 +1,386 @@
|
||||
{
|
||||
"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": "100",
|
||||
"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-07-26T10:35:53",
|
||||
"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-07-26T10:35:53",
|
||||
"x.com.samsung.da.state": "Deleted"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/energy/consumption/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.instantaneousPower": "259.000000",
|
||||
"x.com.samsung.da.cumulativePower": "183099",
|
||||
"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": [
|
||||
"Smart"
|
||||
],
|
||||
"x.com.samsung.da.options": [
|
||||
"Volume_Mute",
|
||||
"RacInfo_None",
|
||||
"UpdateAllow_NotAllowed",
|
||||
"WelcomeCoolingState_Off"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/power/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.power": "On",
|
||||
"causeSource": "SMTS"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/sensors/vs/0",
|
||||
"rep": {}
|
||||
},
|
||||
{
|
||||
"href": "/information/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.modelNum": "TP1X_DA_AC_DHM_01001_0000|10253841|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": "02538A24032700,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": "47",
|
||||
"x.com.samsung.da.desiredHumidity": "50",
|
||||
"increment": "5",
|
||||
"x.com.samsung.da.fivepercentHumidity": "46"
|
||||
}
|
||||
},
|
||||
{
|
||||
"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": "20001E46070A5A00000000000000",
|
||||
"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": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"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": "noHistory",
|
||||
"otnList": [
|
||||
{
|
||||
"type": "WIFI",
|
||||
"modelId": "ADH-WW-TP1-23-AY7100",
|
||||
"versions": [
|
||||
"18260401"
|
||||
],
|
||||
"visVersion": "260401"
|
||||
},
|
||||
{
|
||||
"type": "Micom",
|
||||
"modelId": "042110253841FFFFFFFF",
|
||||
"versions": [
|
||||
"24032700",
|
||||
"FFFFFFFF"
|
||||
],
|
||||
"visVersion": "240327"
|
||||
},
|
||||
{
|
||||
"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": "/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": "/wirelessinfo/vs/0",
|
||||
"rep": {
|
||||
"macaddressWiFi": "**REDACTED**",
|
||||
"macaddressBLE": "**REDACTED**"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/quickcontrol/info/vs/0",
|
||||
"rep": {
|
||||
"supportedVersion": "1.0"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"state_keys": [
|
||||
"air_filter_status",
|
||||
"air_filter_usage",
|
||||
"alarm_code",
|
||||
"auto_clean",
|
||||
"energy_kwh",
|
||||
"energy_saved_kwh",
|
||||
"firmware_update",
|
||||
"humidity",
|
||||
"mute_once",
|
||||
"operating_mode",
|
||||
"power_switch",
|
||||
"power_watts",
|
||||
"target_humidity"
|
||||
]
|
||||
}
|
||||
@@ -190,6 +190,20 @@ class TestForDeviceByModel:
|
||||
assert reg is not None
|
||||
assert reg.name == 'airconditioner'
|
||||
|
||||
def test_dehumidifier_via_dhm_token(self):
|
||||
"""Issue #88: a dehumidifier (AY18CG7500GED) shares the DA_AC_ board
|
||||
family with the room-AC models but reports no oneUiVersion and
|
||||
carries the '_DHM_' (DeHuMidifier) token instead of
|
||||
'_RAC_'/'_PRAC_'/'_WAC_'; falls back to the '_DHM_' token in
|
||||
modelNum."""
|
||||
from custom_components.localthings.registry.by_type import for_device_by_model
|
||||
reg = for_device_by_model(
|
||||
'TP1X_DA_AC_DHM_01001_0000|10253841|77000000001700000A00000000000000',
|
||||
'TP1X_DA_AC_DHM_01001_0000',
|
||||
)
|
||||
assert reg is not None
|
||||
assert reg.name == 'dehumidifier'
|
||||
|
||||
def test_cooktop_via_legacy_model_description(self):
|
||||
"""Older cooktops identify themselves as ARTIK051_GLOBAL_COOKTOP."""
|
||||
from custom_components.localthings.registry.by_type import for_device_by_model
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
"""Tests for Samsung dehumidifier support (issue #88, AY18CG7500GED).
|
||||
|
||||
HA-free like the rest of the suite: exercises the registry, discovery/
|
||||
flatten, and the target-humidity/operating-mode write contracts.
|
||||
"""
|
||||
from custom_components.localthings.registry.adapter import flatten
|
||||
from custom_components.localthings.registry.by_type import for_device_by_model
|
||||
from custom_components.localthings.registry.discovery import discover
|
||||
from custom_components.localthings.registry.entities import NumberDesc, SelectDesc
|
||||
|
||||
from tests.conftest import _load_device
|
||||
|
||||
|
||||
def _dehumidifier():
|
||||
resources = _load_device('dehumidifier')
|
||||
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_no_unbound_hrefs():
|
||||
"""Every resource in the issue #88 dump binds or is covered -- clears
|
||||
the coverage-gap repair."""
|
||||
reg, resources = _dehumidifier()
|
||||
unbound = []
|
||||
discover(resources, reg.capabilities, reg.pattern_capabilities, log=unbound.append)
|
||||
assert unbound == []
|
||||
|
||||
|
||||
def test_expected_state_keys_present():
|
||||
state = _state()
|
||||
for key in ('humidity', 'target_humidity', 'operating_mode', 'power_switch',
|
||||
'auto_clean', 'air_filter_status', 'mute_once'):
|
||||
assert key in state, key
|
||||
|
||||
|
||||
def test_humidity_sensor_reads_current_value():
|
||||
state = _state()
|
||||
assert state['humidity'] == 47
|
||||
|
||||
|
||||
def test_target_humidity_number_reads_desired_value():
|
||||
state = _state()
|
||||
assert state['target_humidity'] == 50
|
||||
|
||||
|
||||
def test_target_humidity_write_contract():
|
||||
desc = _desc('target_humidity')
|
||||
assert isinstance(desc, NumberDesc)
|
||||
path, body = desc.write_fn('55', {})
|
||||
assert path == ['humidity', 'vs', '0']
|
||||
assert body == {'x.com.samsung.da.desiredHumidity': '55'}
|
||||
|
||||
|
||||
def test_target_humidity_step_reads_live_increment():
|
||||
"""Step comes from the device's own `increment` field rather than a
|
||||
hardcoded constant -- see the adding-device-support skill's 'never
|
||||
hard-code the one dump's values' section."""
|
||||
desc = _desc('target_humidity')
|
||||
assert desc.step_fn({'increment': '5'}) == 5
|
||||
assert desc.step_fn({'increment': '10'}) == 10
|
||||
# No live field: falls back to a sane default rather than raising.
|
||||
assert desc.step_fn({}) == 1
|
||||
|
||||
|
||||
def test_target_humidity_has_no_hardcoded_bounds():
|
||||
"""No range field is present in any dump seen so far -- native_min/max
|
||||
are deliberately left unset (falls back to HA's own 0-100 default for a
|
||||
percentage field) rather than a bound guessed from a spec sheet."""
|
||||
desc = _desc('target_humidity')
|
||||
assert desc.native_min is None
|
||||
assert desc.native_max is None
|
||||
assert desc.native_min_fn is None
|
||||
assert desc.native_max_fn is None
|
||||
|
||||
|
||||
def test_operating_mode_select_options_come_from_live_supported_modes():
|
||||
"""Options are read live from x.com.samsung.da.supportedModes, not a
|
||||
hardcoded tuple -- so a future device with a different mode set is
|
||||
handled automatically."""
|
||||
desc = _desc('operating_mode')
|
||||
assert isinstance(desc, SelectDesc)
|
||||
assert desc.options_field == 'x.com.samsung.da.supportedModes'
|
||||
assert desc.options == ()
|
||||
|
||||
|
||||
def test_operating_mode_write_contract():
|
||||
desc = _desc('operating_mode')
|
||||
path, body = desc.write_fn('Quiet', {})
|
||||
assert path == ['mode', 'vs', '0']
|
||||
assert body == {'x.com.samsung.da.modes': ['Quiet']}
|
||||
|
||||
|
||||
def test_operating_mode_reads_first_mode():
|
||||
state = _state()
|
||||
assert state['operating_mode'] == 'Smart'
|
||||
@@ -85,6 +85,23 @@ def test_registry_reproduces_golden_state_keys_for_airconditioner():
|
||||
)
|
||||
|
||||
|
||||
def test_registry_reproduces_golden_state_keys_for_dehumidifier():
|
||||
"""TP1X_DA_AC_DHM_01001_0000 (issue #88, AY18CG7500GED) shares the DA_AC_
|
||||
board family with the room-AC models but carries the '_DHM_' token;
|
||||
resolved via the '_DHM_' modelNum fallback in for_device_by_model into a
|
||||
dedicated dehumidifier registry (target humidity, operating mode, reused
|
||||
AC filter/auto-clean/mute-once capabilities)."""
|
||||
from tests.conftest import _load_device
|
||||
resources = _load_device('dehumidifier')
|
||||
golden = json.loads((GOLDEN / 'dehumidifier.json').read_text())
|
||||
state_keys = _new_state_keys('dehumidifier', 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_registry_reproduces_golden_state_keys_for_cooktop():
|
||||
from tests.conftest import _load_device
|
||||
resources = _load_device('cooktop')
|
||||
|
||||
Reference in New Issue
Block a user