Add AC UV LED, ventilation alarm, and PM1 filter support (issue #270)

TP1X_FAC_TIME_23K reports three previously unbound hrefs: a UV-C
sterilization LED and a ventilation-reminder alarm (both plain On/Off
toggles), and a second PM1-rated dust filter with no live usage/status
fields on this particular dump.

The PM1 filter capability gates each entity on its own field's presence
rather than a blanket ignore, since the TP1X_DA-AC-CAC-01001_0000 cassette
AC (issue #191) reports the same href with full live data -- this also
closes two of that device's ten documented coverage gaps (UV LED and the
PM1 filter) as a side effect.
This commit is contained in:
Marc Billow
2026-08-03 19:07:03 +00:00
parent 0994ca487a
commit cf09247e39
13 changed files with 991 additions and 9 deletions
@@ -33,8 +33,11 @@ REGISTRY = DeviceRegistry(
airconditioner.AIR_PURIFY,
airconditioner.AUTO_CLEAN,
airconditioner.AIR_FILTER,
airconditioner.AIR_FILTER_PM1,
airconditioner.AIR_QUALITY,
airconditioner.DISPLAY_LIGHT,
airconditioner.UV_LED,
airconditioner.VENTILATION_ALARM,
airconditioner.MUTE_ONCE,
airconditioner.CURRENT_LIMIT,
airconditioner.ANOMALY_LOAD,
@@ -909,6 +909,80 @@ AIR_FILTER = Capability(
),
)
def _pm1_threshold_write(payload, rep, href=None):
"""Same contract as _threshold_write, against this filter's own href --
not yet confirmed live (no dump seen advertises
supportedFilterDesiredUsage for this href), so the Select this backs
stays gated behind that field's presence, same as AIR_FILTER's."""
return ["filter", "airdustPM1filter", "vs", "0"], {
"x.com.samsung.da.filterDesiredUsage": payload,
}
def _has_filter_field(field):
return lambda rep, resources: rep.get(field) is not None
# Second, PM1-rated dust filter some TP1X_FAC-class boards report alongside
# AIR_FILTER's /filter/airdustfilter/vs/0. TP1X_FAC_TIME_23K (issue #270)
# reports only filterCapacity/filterCapacityUnit/filterResetType
# ('notresetable') on this href, with no live filterUsage/filterStatus at
# all -- but the TP1X_DA-AC-CAC-01001_0000 cassette AC (issue #191) reports
# the identical href with full live fields. So this stays a real capability
# rather than a blanket ignore, with every entity individually gated on its
# own field's presence per the 'don't guess' rule: no entity on a board that
# has nothing behind it, real readings on one that does.
AIR_FILTER_PM1 = Capability(
href="/filter/airdustPM1filter/vs/0",
poll_tier="cold",
entities=(
SensorDesc(
key="air_filter_pm1_usage",
rep_fn=filter_usage_percent,
unit="%",
state_class="measurement",
icon="mdi:air-filter",
entity_category="diagnostic",
exists_fn=_has_filter_field("x.com.samsung.da.filterUsage"),
),
SensorDesc(
key="air_filter_pm1_usage_hours",
field="x.com.samsung.da.filterUsage",
device_class="duration",
state_class="total_increasing",
unit_fn=_filter_unit,
icon="mdi:air-filter",
entity_category="diagnostic",
value_fn=_int,
exists_fn=_has_filter_field("x.com.samsung.da.filterUsage"),
),
SelectDesc(
key="air_filter_pm1_threshold",
field="x.com.samsung.da.filterDesiredUsage",
options_field="x.com.samsung.da.supportedFilterDesiredUsage",
exists_fn=lambda rep, res: bool(
rep.get("x.com.samsung.da.supportedFilterDesiredUsage")
),
icon="mdi:alarm",
entity_category="config",
write_fn=_pm1_threshold_write,
value_fn=lambda v: str(v) if v is not None else None,
),
SensorDesc(
key="air_filter_pm1_status",
field="x.com.samsung.da.filterStatus",
device_class="enum",
options=("normal", "wash", "replace"),
translation_key="filter_status",
icon="mdi:air-filter",
entity_category="diagnostic",
value_fn=lambda v: v.lower() if isinstance(v, str) else v,
exists_fn=_has_filter_field("x.com.samsung.da.filterStatus"),
),
),
)
DISPLAY_LIGHT = Capability(
href="/light/vs/0",
poll_tier="cold",
@@ -927,6 +1001,51 @@ DISPLAY_LIGHT = Capability(
),
)
# UV-C sterilization LED (issue #270, TP1X_FAC_TIME_23K). Same On/Off
# convention as everything else on this board, and (unlike VENTILATION_ALARM
# below) a real supportedModes list confirms the value set.
UV_LED = Capability(
href="/uvled/vs/0",
poll_tier="cold",
entities=(
SwitchDesc(
key="uv_led",
field="x.com.samsung.da.modes",
icon="mdi:lightbulb-fluorescent-tube",
entity_category="config",
value_fn=lambda v: v == "On",
write_fn=lambda p, rep, href=None: (
["uvled", "vs", "0"],
{"x.com.samsung.da.modes": "On" if p == "On" else "Off"},
),
),
),
)
# Ventilation-reminder alarm toggle (issue #270). Only a bare `alarm` field
# is present -- no supportedModes list to confirm the value set against,
# unlike UV_LED above. Modeled as a switch on the strength of the same
# On/Off convention used everywhere else in this API; a rejected write is
# the worst case for a wrong guess (see the adding-device-support skill),
# but this hasn't been round-trip confirmed on real hardware.
VENTILATION_ALARM = Capability(
href="/ventilation/setting/vs/0",
poll_tier="cold",
entities=(
SwitchDesc(
key="ventilation_alarm",
field="alarm",
icon="mdi:bell-alert",
entity_category="config",
value_fn=lambda v: v == "On",
write_fn=lambda p, rep, href=None: (
["ventilation", "setting", "vs", "0"],
{"alarm": "On" if p == "On" else "Off"},
),
),
),
)
# Confirmed against issue #38's dump (TP1X_DA-AC-RAC-01001_0000): a single
# boolean field, no vendor prefix, mirroring the On/Off convention used
# throughout the rest of this API.
@@ -637,6 +637,9 @@
},
"watertank_light_brightness": {
"name": "Jas osvětlení nádržky na vodu"
},
"air_filter_pm1_threshold": {
"name": "Práh alarmu PM1 filtru"
}
},
"sensor": {
@@ -988,6 +991,12 @@
},
"watertank_full_alarm_status": {
"name": "Stav alarmu plné nádržky na vodu"
},
"air_filter_pm1_usage": {
"name": "Využití PM1 filtru"
},
"air_filter_pm1_usage_hours": {
"name": "Hodiny využití PM1 filtru"
}
},
"switch": {
@@ -1158,6 +1167,12 @@
},
"watertank_light": {
"name": "Osvětlení nádržky na vodu"
},
"uv_led": {
"name": "UV LED"
},
"ventilation_alarm": {
"name": "Alarm větrání"
}
},
"time": {
@@ -637,6 +637,9 @@
},
"watertank_light_brightness": {
"name": "Water tank light brightness"
},
"air_filter_pm1_threshold": {
"name": "PM1 filter alarm threshold"
}
},
"sensor": {
@@ -988,6 +991,12 @@
},
"watertank_full_alarm_status": {
"name": "Water tank full alarm status"
},
"air_filter_pm1_usage": {
"name": "PM1 filter usage"
},
"air_filter_pm1_usage_hours": {
"name": "PM1 filter usage hours"
}
},
"switch": {
@@ -1158,6 +1167,12 @@
},
"watertank_light": {
"name": "Water tank light"
},
"uv_led": {
"name": "UV LED"
},
"ventilation_alarm": {
"name": "Ventilation alarm"
}
},
"time": {
@@ -751,6 +751,9 @@
},
"watertank_light_brightness": {
"name": "Brillo de la luz del depósito de agua"
},
"air_filter_pm1_threshold": {
"name": "Umbral de alarma del filtro PM1"
}
},
"sensor": {
@@ -1102,6 +1105,12 @@
},
"watertank_full_alarm_status": {
"name": "Estado de alarma de depósito de agua lleno"
},
"air_filter_pm1_usage": {
"name": "Uso del filtro PM1"
},
"air_filter_pm1_usage_hours": {
"name": "Horas de uso del filtro PM1"
}
},
"switch": {
@@ -1272,6 +1281,12 @@
},
"watertank_light": {
"name": "Luz del depósito de agua"
},
"uv_led": {
"name": "LED UV"
},
"ventilation_alarm": {
"name": "Alarma de ventilación"
}
},
"time": {
@@ -637,6 +637,9 @@
},
"watertank_light_brightness": {
"name": "Luminosità della luce del serbatoio dell'acqua"
},
"air_filter_pm1_threshold": {
"name": "Soglia di allarme del filtro PM1"
}
},
"sensor": {
@@ -988,6 +991,12 @@
},
"watertank_full_alarm_status": {
"name": "Stato allarme serbatoio d'acqua pieno"
},
"air_filter_pm1_usage": {
"name": "Utilizzo del filtro PM1"
},
"air_filter_pm1_usage_hours": {
"name": "Ore di utilizzo del filtro PM1"
}
},
"switch": {
@@ -1158,6 +1167,12 @@
},
"watertank_light": {
"name": "Luce del serbatoio dell'acqua"
},
"uv_led": {
"name": "LED UV"
},
"ventilation_alarm": {
"name": "Allarme ventilazione"
}
},
"time": {
@@ -637,6 +637,9 @@
},
"watertank_light_brightness": {
"name": "Helderheid van het waterreservoirlicht"
},
"air_filter_pm1_threshold": {
"name": "PM1-filteralarmdrempel"
}
},
"sensor": {
@@ -988,6 +991,12 @@
},
"watertank_full_alarm_status": {
"name": "Status alarm waterreservoir vol"
},
"air_filter_pm1_usage": {
"name": "PM1-filtergebruik"
},
"air_filter_pm1_usage_hours": {
"name": "PM1-filtergebruik in uren"
}
},
"switch": {
@@ -1158,6 +1167,12 @@
},
"watertank_light": {
"name": "Waterreservoirlicht"
},
"uv_led": {
"name": "Uv-led"
},
"ventilation_alarm": {
"name": "Ventilatiealarm"
}
},
"time": {
@@ -0,0 +1,641 @@
{
"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": "true",
"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": [
"112",
"224",
"336",
"448"
]
}
},
{
"href": "/filter/airdustPM1filter/vs/0",
"rep": {
"x.com.samsung.da.filterCapacity": "3",
"x.com.samsung.da.filterCapacityUnit": "Month",
"x.com.samsung.da.filterResetType": [
"notresetable"
]
}
},
{
"href": "/temperature/control/vs/0",
"rep": {
"x.com.samsung.da.increment": "0.5"
}
},
{
"href": "/mode/convenient/vs/0",
"rep": {
"x.com.samsung.da.modes": "Off",
"x.com.samsung.da.supportedModes": [
"Off",
"Sleep",
"Nano",
"NanoSleep"
]
}
},
{
"href": "/option/airpurify/vs/0",
"rep": {
"x.com.samsung.da.modes": "Off",
"x.com.samsung.da.supportedModes": [
"On",
"Off"
]
}
},
{
"href": "/option/autoclean/vs/0",
"rep": {
"x.com.samsung.da.status": "Stop",
"x.com.samsung.da.settingStatus": "On",
"x.com.samsung.da.progress": "0",
"x.com.samsung.da.supportedStatus": [
"Start",
"SpeedClean",
"QuietClean",
"Stop"
],
"x.com.samsung.da.supportedSettingStatus": [
"On",
"SpeedClean",
"QuietClean",
"Off"
]
}
},
{
"href": "/wind/strength/vs/0",
"rep": {
"x.com.samsung.da.modes": "12",
"x.com.samsung.da.supportedModes": [
"0",
"11",
"12",
"13",
"14",
"15",
"16"
],
"x.com.samsung.da.modesName": [
"Auto",
"1",
"2",
"3",
"4",
"5",
"Max"
]
}
},
{
"href": "/wind/direction/vs/0",
"rep": {
"x.com.samsung.da.modes": "NotSupported",
"x.com.samsung.da.supportedModes": [
"NotSupported"
]
}
},
{
"href": "/subdevices/vs/0",
"rep": {
"x.com.samsung.da.subdeviceIdList": "**REDACTED**"
}
},
{
"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:14:25"
},
{
"x.com.samsung.da.id": "2",
"x.com.samsung.da.description": "Alarm",
"x.com.samsung.da.alarmType": "Device",
"x.com.samsung.da.code": "AC_V_0002_OFF",
"x.com.samsung.da.triggeredTime": "2026-08-03T14:14:25"
}
]
}
},
{
"href": "/temperatures/vs/0",
"rep": {
"x.com.samsung.da.items": [
{
"x.com.samsung.da.id": "0",
"x.com.samsung.da.description": "Temperature",
"x.com.samsung.da.desired": "27.0",
"x.com.samsung.da.current": "28.0",
"x.com.samsung.da.maximum": "30",
"x.com.samsung.da.minimum": "18",
"x.com.samsung.da.increment": "0.5",
"x.com.samsung.da.unit": "Celsius"
}
]
}
},
{
"href": "/temperature/current/0",
"rep": {
"range": [
18.0,
30.0
],
"units": "C",
"temperature": 28.0
}
},
{
"href": "/temperature/desired/0",
"rep": {
"range": [
18.0,
30.0
],
"units": "C",
"temperature": 27.0
}
},
{
"href": "/diagnosis/vs/0",
"rep": {}
},
{
"href": "/energy/consumption/vs/0",
"rep": {
"x.com.samsung.da.cumulativeConsumption": "400.000000",
"x.com.samsung.da.instantaneousPower": "14.000000",
"x.com.samsung.da.cumulativePower": "882861",
"x.com.samsung.da.cumulativeSavedPower": "0",
"x.com.samsung.da.cumulativeDate": "1785798000",
"x.com.samsung.da.cumulativeUnit": "Wh",
"x.com.samsung.da.instantaneousPowerUnit": "W",
"x.com.samsung.da.cumulativePowerType": "individual"
}
},
{
"href": "/energy/consumption/0",
"rep": {
"energy": 400.0,
"power": 14.0
}
},
{
"href": "/mode/vs/0",
"rep": {
"x.com.samsung.da.supportedModes": [
"AIComfort",
"Cool",
"Dry",
"Wind"
],
"x.com.samsung.da.modes": [
"Cool"
],
"x.com.samsung.da.options": [
"ArtificialWorking_Off",
"welcomecare_Off",
"Volume_Mute",
"Operation_Family",
"ComfortAICooling_Off",
"OnTimer_0",
"OffTimer_0",
"Sleep_0",
"Booster_Off",
"OutdoorSilence_Off",
"AiTempChanged_Off",
"AiTemp_250",
"StopAutoClean_Idle",
"Weather_Off",
"Service_Off",
"SmartCoolClean_Off",
"ProgressSmartClean_0",
"OutDoorVentil_Off",
"InDoorVentil_Off",
"FreezeAlarmSetting_Off",
"DesiredFreezeAlarm_240",
"ProtectOperation_Off",
"EnergySaveIcon_Off",
"ExtendOptionCode_16975",
"OptionCode_2589",
"RacInfo_First",
"ModelInfo_16K_BORA_VENT2",
"RacInfo_None_Second",
"OutdoorConnection_Connected",
"DurationOn_0",
"UpdateAllow_NotAllowed",
"welcomecareElapsedTime_0",
"welcomecareThresholdTemp_0",
"welcomecareStartDate_0000",
"welcomecareEndDate_0000",
"welcomecareSeason_None"
]
}
},
{
"href": "/power/vs/0",
"rep": {
"x.com.samsung.da.power": "Off",
"causeSource": "SMTS"
}
},
{
"href": "/power/0",
"rep": {
"value": false
}
},
{
"href": "/information/vs/0",
"rep": {
"x.com.samsung.da.modelNum": "TP1X_FAC_TIME_23K|10250841|600007110017112BCE00CE2200937000",
"x.com.samsung.da.description": "TP1X_FAC_TIME_23K",
"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": "AF2",
"x.com.samsung.da.diagMinVersion": "3.0",
"x.com.samsung.da.diagTsId": "DA01",
"x.com.samsung.da.serialNumOption": "**REDACTED**",
"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": "02517A260330",
"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": "02508A23020200,02509A24030701",
"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": "02502A23011300,02299A10000300"
},
{
"x.com.samsung.da.id": "3",
"x.com.samsung.da.description": "Version",
"x.com.samsung.da.type": "LED",
"x.com.samsung.da.number": "02510A22121900,FFFFFFFFFFFFFF"
},
{
"x.com.samsung.da.id": "4",
"x.com.samsung.da.description": "Version",
"x.com.samsung.da.type": "BLERemoteController",
"x.com.samsung.da.number": "22021760"
}
]
}
},
{
"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": "1138000000",
"x.com.samsung.da.airconOptionList": [
"HOMECARE_WIZARD_V2",
"ENERGY_2.0",
"AI_2.0",
"DeviceTypeMaster",
"CLEAN_OPTION_SUPPORT",
"DR",
"SingleCommand_1"
]
}
},
{
"href": "/humidity/0",
"rep": {
"humidity": 0
}
},
{
"href": "/humidity/vs/0",
"rep": {
"x.com.samsung.da.humidity": "0.000000",
"x.com.samsung.da.fivepercentHumidity": "71"
}
},
{
"href": "/drlc/0",
"rep": {
"DRLevel": 0,
"start": "1970-01-01T00:00:00Z",
"durationminutes": 0,
"override": false
}
},
{
"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": "/runn/vs/0",
"rep": {
"x.com.samsung.da.runningMode": 0
}
},
{
"href": "/availablecontrolsets/vs/0",
"rep": {
"x.com.samsung.da.sets": "000000B4012C0000000D04000000",
"x.com.samsung.da.id": "FAC",
"x.com.samsung.da.version": "1.0"
}
},
{
"href": "/keepnormalstate/vs/0",
"rep": {
"x.com.samsung.da.keepnormal": 1
}
},
{
"href": "/uvled/vs/0",
"rep": {
"x.com.samsung.da.modes": "On",
"x.com.samsung.da.supportedModes": [
"On",
"Off"
]
}
},
{
"href": "/remotedatacontrol/vs/0",
"rep": {
"x.com.samsung.da.status": "Off",
"x.com.samsung.da.connectionStatus": "Disconnected"
}
},
{
"href": "/remotetemperature/vs/0",
"rep": {
"x.com.samsung.da.temperature": "",
"x.com.samsung.da.unit": "",
"x.com.samsung.da.error": ""
}
},
{
"href": "/aisleep/vs/0",
"rep": {
"x.com.samsung.da.displayNightMode": "Off",
"x.com.samsung.da.elapsedTime": "0",
"x.com.samsung.da.requestFeedback": "Off",
"x.com.samsung.da.resultFeedback": "0",
"x.com.samsung.da.statusFeedback": "Idle",
"x.com.samsung.da.sleepTime": "14002200"
}
},
{
"href": "/remotedeviceinfo/vs/0",
"rep": {
"x.com.samsung.da.didList": ""
}
},
{
"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": "AFA-WW-TP1-23-AFXX00",
"versions": [
"10260330"
],
"visVersion": "260330"
},
{
"type": "Micom",
"modelId": "845110250241FFFFFFFF",
"versions": [
"23011300",
"FFFFFFFF"
],
"visVersion": "230113"
},
{
"type": "Micom",
"modelId": "04511025084110250941",
"versions": [
"23020200",
"24030701"
],
"visVersion": "240307"
},
{
"type": "Micom",
"modelId": "04511025024110229941",
"versions": [
"23011300",
"10000300"
],
"visVersion": "230113"
},
{
"type": "Micom",
"modelId": "045110240441FFFFFFFF",
"versions": [
"23121900",
"FFFFFFFF"
],
"visVersion": "231219"
},
{
"type": "Micom",
"modelId": "045110251041FFFFFFFF",
"versions": [
"22121900",
"FFFFFFFF"
],
"visVersion": "221219"
}
]
}
},
{
"href": "/option/muteonce/vs/0",
"rep": {
"muteonce": "Off"
}
},
{
"href": "/selfcheck/vs/0",
"rep": {
"x.com.samsung.da.start": "Cancel",
"x.com.samsung.da.status": "Ready",
"x.com.samsung.da.progress": "1",
"x.com.samsung.da.result": "NA",
"x.com.samsung.da.supportedActions": [
"Start",
"Cancel"
],
"x.com.samsung.da.error": [
"DA_ERROR_NONE"
]
}
},
{
"href": "/reserverulesets/vs/0",
"rep": {
"x.com.samsung.da.sets": "1DFFFFFFFF121E121EFFFFFFFFFFFF01000000013F0100013F000000001C0C",
"x.com.samsung.da.id": "FAC",
"x.com.samsung.da.version": "1.0"
}
},
{
"href": "/ventilation/setting/vs/0",
"rep": {
"alarm": "Off"
}
},
{
"href": "/light/vs/0",
"rep": {
"mode": "Off",
"supportedModes": [
"On",
"Off"
]
}
},
{
"href": "/timezone/vs/0",
"rep": {
"timezoneid": "Asia/Seoul",
"offset": "+09:00",
"DST": "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": "/wirelessinfo/vs/0",
"rep": {
"macaddressWiFi": "**REDACTED**",
"macaddressBLE": "**REDACTED**",
"connectedApSsid": "**REDACTED**"
}
},
{
"href": "/quickcontrol/info/vs/0",
"rep": {
"supportedVersion": "1.0"
}
}
]
}
+5 -1
View File
@@ -2,6 +2,9 @@
"state_keys": [
"absence_power_saving_active",
"absence_power_saving_mode",
"air_filter_pm1_status",
"air_filter_pm1_usage",
"air_filter_pm1_usage_hours",
"air_filter_status",
"air_filter_usage",
"air_filter_usage_hours",
@@ -29,6 +32,7 @@
"selfcheck_result",
"selfcheck_status",
"super_fine_dust",
"tropical_night_mode"
"tropical_night_mode",
"uv_led"
]
}
@@ -0,0 +1,33 @@
{
"state_keys": [
"air_filter_status",
"air_filter_threshold",
"air_filter_usage",
"air_filter_usage_hours",
"air_purify",
"alarm_code",
"auto_clean",
"auto_clean_progress",
"auto_clean_running",
"beep",
"climate",
"current_temperature_c",
"diagnosis_status",
"display_light",
"energy_kwh",
"energy_saved_kwh",
"firmware_update",
"humidity",
"mute_once",
"odor_controller_active",
"odor_controller_progress",
"power_energy_kwh",
"power_watts",
"selfcheck_error",
"selfcheck_result",
"selfcheck_status",
"tropical_night_mode",
"uv_led",
"ventilation_alarm"
]
}
+12 -8
View File
@@ -6,12 +6,18 @@ token -- this board was the one exception (its oneUiVersion self-reports
"7.0 Air conditioner", but 'CAC' had never been added to the board-token
table), so it silently fell back to common caps and lost its climate entity.
This dump is NOT fully covered yet -- ten hrefs remain unbound (edge
lighting, PM1 filter, a second stateful light resource, absence-clean, four
`/settings/sound/*` resources, smart-sensing-cooling, UV LED), all genuinely
new to this board generation. That's a real device-support gap, left
documented here rather than guessed at, per the 'don't guess' rule -- fixing
the routing regression was the scope of #191.
This dump is NOT fully covered yet -- eight hrefs remain unbound (edge
lighting, a second stateful light resource, absence-clean, four
`/settings/sound/*` resources, smart-sensing-cooling), all genuinely new to
this board generation. That's a real device-support gap, left documented
here rather than guessed at, per the 'don't guess' rule -- fixing the
routing regression was the scope of #191.
/uvled/vs/0 and /filter/airdustPM1filter/vs/0 used to be on this list too,
until issue #270 (TP1X_FAC_TIME_23K) added real capabilities for both --
this board's own live filterUsage/filterStatus data on the PM1 filter binds
through the same exists_fn-gated entities #270's dump (which has neither
field) leaves empty.
"""
from custom_components.localthings.registry.adapter import flatten
@@ -22,7 +28,6 @@ from tests.conftest import _load_device
_STILL_UNBOUND = frozenset(
{
"/edgelighting/vs/0",
"/filter/airdustPM1filter/vs/0",
"/light/stateful/vs/0",
"/mds/absenceclean/vs/0",
"/settings/sound/mode/vs/0",
@@ -30,7 +35,6 @@ _STILL_UNBOUND = frozenset(
"/settings/sound/output/vs/0",
"/settings/sound/volume/vs/0",
"/smartsensingcooling/vs/0",
"/uvled/vs/0",
}
)
@@ -0,0 +1,83 @@
"""Tests for the TP1X_FAC_TIME_23K air conditioner (issue #270).
A 2-in-1 system (x.com.samsung.da.numofsubdevice='2') whose UUID-prefixed
sibling materializes via the flat fallback (issue #205) -- unrelated to the
three capability gaps this fixture actually locks in (UV LED, ventilation
alarm, and the unmodeled PM1 filter), which live entirely on canonical
hrefs already exercised by the master's own resources.
"""
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 SwitchDesc
from tests.conftest import _load_device
def _airconditioner():
resources = _load_device("airconditioner_tp1x_fac_time_23k")
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 = _airconditioner()
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_airconditioner_registry():
reg, _ = _airconditioner()
assert reg is not None and reg.name == "airconditioner"
def test_no_unbound_hrefs():
reg, resources = _airconditioner()
unbound = []
discover(resources, reg.capabilities, reg.pattern_capabilities, log=unbound.append)
assert unbound == []
def test_uv_led_switch_reads_on_and_has_write_contract():
state = _state()
assert state["uv_led"] is True
desc = _desc("uv_led")
assert isinstance(desc, SwitchDesc)
path, body = desc.write_fn("Off", {})
assert path == ["uvled", "vs", "0"]
assert body == {"x.com.samsung.da.modes": "Off"}
def test_ventilation_alarm_switch_reads_off_and_has_write_contract():
state = _state()
assert state["ventilation_alarm"] is False
desc = _desc("ventilation_alarm")
assert isinstance(desc, SwitchDesc)
path, body = desc.write_fn("On", {})
assert path == ["ventilation", "setting", "vs", "0"]
assert body == {"alarm": "On"}
def test_pm1_filter_has_no_live_fields_so_no_state_is_reported():
"""This dump's /filter/airdustPM1filter/vs/0 has only
filterCapacity/filterCapacityUnit/filterResetType -- no filterUsage or
filterStatus -- so every AIR_FILTER_PM1 entity's exists_fn gates it out.
The href still covers (see test_no_unbound_hrefs above) without
fabricating a reading from nothing (the 'don't guess' rule); contrast
with test_airconditioner_cac.py, whose dump has live data on the same
href and does get real air_filter_pm1_* state."""
state = _state()
assert not any(k.startswith("air_filter_pm1_") for k in state)
+20
View File
@@ -1243,6 +1243,26 @@ def test_registry_reproduces_golden_state_keys_for_dehumidifier_tp1x_dhm01001():
)
def test_registry_reproduces_golden_state_keys_for_airconditioner_tp1x_fac_time_23k():
"""TP1X_FAC_TIME_23K (issue #270) reports a UV-C LED, a ventilation-alarm
toggle, and a second (PM1-rated, no live usage field) dust filter, none
previously modeled. Also a 2-in-1 system on this dump (numofsubdevice=2,
a UUID-prefixed sibling materializes via the flat fallback) -- the three
new/ignored hrefs are fixed on their canonical form so the sibling picks
them up too, per the adding-device-support skill's 'never index or
prefix a registry href' rule."""
from tests.conftest import _load_device
resources = _load_device("airconditioner_tp1x_fac_time_23k")
golden = json.loads((GOLDEN / "airconditioner_tp1x_fac_time_23k.json").read_text())
state_keys = _new_state_keys("airconditioner_tp1x_fac_time_23k", 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