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 committed 2026-08-03 19:07:03 +00:00
1 parent 0994ca487a
commit cf09247e39
13 files changed
+991 -9

No files matched your search

@@ -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": {