Add device support for AILP_DA-AC-FAC-02011 air conditioner (issue #319)

This board routes purely via /oic/d's oic.d.airconditioner type (no
board-token match) and reports several resources the sibling
TP1X_DA-AC-CAC-01001 board (issue #191) left as a documented gap:

- /display/vs/0, /settings/sound/output/vs/0, /settings/sound/volume/vs/0
  now reuse air_purifier.py's identical-shape capabilities instead of
  duplicating them.
- /settings/sound/mode/vs/0 gets a new airconditioner.SOUND_MODE reading
  the live supportedModes field, sharing laundry.py's existing
  voice/tone/mute translation catalog since the value vocabulary matches.
- /csi/absenceclean/vs/0 and /csi/energysaving/vs/0 are new, genuinely
  useful controls (absence auto-clean toggle, energy-saving mode select
  plus its state/operatingStatus diagnostics).
- /dnd/autosleep/vs/0, /outdoorsharing/vs/0, /lifestyle/survey/vs/0,
  /settings/sound/voice/vs/0 and /csi/information/vs/0 are ignored as
  plumbing/unconfirmed data with no user-actionable state.

Also fixes a latent bug in air_purifier.SOUND_VOLUME: boards that report
minLevel/resolution but no maxLevel (this one) would have produced a
min=0/max=0 number entity instead of self-gating off.

Updates test_airconditioner_cac.py's documented coverage gap now that
sound_mode/sound_output/sound_volume are covered there too.
This commit is contained in:
Marc Billow committed 2026-08-07 14:10:13 +00:00
1 parent f07ae4020e
commit edf77309ba
15 files changed
+1389 -16

No files matched your search

@@ -17,7 +17,7 @@ uses unconditionally.
Reuses dishwasher.DIAGNOSIS for /diagnosis/vs/0.
"""
from ..capabilities import airconditioner, common, dishwasher, ignored
from ..capabilities import air_purifier, airconditioner, common, dishwasher, ignored
from ._base import DeviceRegistry, _build
REGISTRY = DeviceRegistry(
@@ -46,6 +46,15 @@ REGISTRY = DeviceRegistry(
airconditioner.CURRENT_TEMPERATURE,
airconditioner.CURRENT_TEMPERATURE_VS,
airconditioner.HUMIDITY,
# TP1X_DA-AC-FAC-class (issue #319): shares its /display/vs/0,
# /settings/sound/output/vs/0 and /settings/sound/volume/vs/0
# shape with the sibling TP1X_DA-AC-AIR board in air_purifier.py.
air_purifier.DISPLAY,
air_purifier.SOUND_OUTPUT,
air_purifier.SOUND_VOLUME,
airconditioner.SOUND_MODE,
airconditioner.ABSENCE_CLEAN,
airconditioner.ENERGY_SAVING,
*airconditioner.COVERAGE,
]
),
@@ -441,6 +441,10 @@ SOUND_VOLUME = Capability(
field="level",
icon="mdi:volume-medium",
entity_category="config",
# Some boards (issue #319's AC) report minLevel/resolution but no
# maxLevel -- native_max_fn would silently collapse to 0, giving a
# slider with no real range instead of no entity at all.
exists_fn=lambda rep, resources: "maxLevel" in rep,
native_min_fn=lambda rep: int_or_none(rep.get("minLevel")) or 0,
native_max_fn=lambda rep: int_or_none(rep.get("maxLevel")) or 0,
step_fn=lambda rep: int_or_none(rep.get("resolution")) or 1,
@@ -1167,6 +1167,91 @@ HUMIDITY = Capability(
),
)
# TP1X_DA-AC-FAC-class additions (issue #319): resources the CAC-class board
# (issue #191) also reports but left as a documented gap -- this dump gives
# the live supportedModes/mode shapes those needed.
# Same {mode, supportedModes: [On, Off]} shape as air_purifier.DISPLAY on the
# sibling TP1X_DA-AC-AIR board; shares that capability rather than
# duplicating it.
SOUND_MODE = Capability(
href="/settings/sound/mode/vs/0",
poll_tier="cold",
entities=(
# Values seen (mute/tone/voice) are exactly laundry.SOUND_MODE's
# vocabulary, so this shares that catalog entry -- but reads the
# live supportedModes field rather than laundry's static tuple,
# since this resource carries one (issue #319).
SelectDesc(
key="sound_mode",
field="mode",
icon="mdi:volume-high",
entity_category="config",
options_field="supportedModes",
write_fn=lambda p, rep, href=None: (
["settings", "sound", "mode", "vs", "0"],
{"mode": p},
),
),
),
)
# Absence-detection auto air clean (issue #319) -- a plain On/Off toggle,
# sibling feature to ABSENCE_POWER_SAVING above but on its own href.
ABSENCE_CLEAN = Capability(
href="/csi/absenceclean/vs/0",
poll_tier="cold",
entities=(
SwitchDesc(
key="absence_clean",
field="mode",
icon="mdi:broom",
entity_category="config",
value_fn=lambda v: v == "On",
write_fn=lambda p, rep, href=None: (
["csi", "absenceclean", "vs", "0"],
{"mode": "On" if p == "On" else "Off"},
),
),
),
)
# Energy-saving schedule (issue #319). `mode` is a device-chosen preset
# (e.g. Cooling_60/Off_180) with no confirmed unit for the trailing number
# (minutes seen elsewhere on this board are unprefixed ints, not
# underscore-suffixed) -- exposed as a select over the live options rather
# than translating labels we can't confirm. `state`/`operatingStatus` stay
# bare diagnostic passthroughs for the same reason.
ENERGY_SAVING = Capability(
href="/csi/energysaving/vs/0",
poll_tier="cold",
entities=(
SelectDesc(
key="energy_saving_mode",
field="mode",
icon="mdi:leaf",
entity_category="config",
options_field="supportedModes",
write_fn=lambda p, rep, href=None: (
["csi", "energysaving", "vs", "0"],
{"mode": p},
),
),
SensorDesc(
key="energy_saving_state",
field="state",
icon="mdi:leaf",
entity_category="diagnostic",
),
SensorDesc(
key="energy_saving_operating_status",
field="operatingStatus",
icon="mdi:leaf",
entity_category="diagnostic",
),
),
)
# /sensors/vs/0 items[] carry live air-quality readings. CleanLevel is
# corroborated as numeric by a top-level x.com.samsung.da.cleanLevel scalar,
# so it's a measurement; the others stay string diagnostics (see
@@ -1250,6 +1335,20 @@ _AC_IGNORED = [
# registry.subdevices.enumerate_subdevices, hence the entry here rather
# than a coverage gap.
"/multidevice/vs/0",
# TP1X_DA-AC-FAC-class-only (issue #319), same reasoning as
# air_purifier.COVERAGE's duplicate of these hrefs -- scoped here rather
# than promoted to the global ignore list since it'd collide with
# families that do bind some of these.
"/dnd/autosleep/vs/0", # every field its inert default; needs a schedule editor
"/outdoorsharing/vs/0", # empty on this dump -- outdoor-unit sharing plumbing
"/lifestyle/survey/vs/0", # {list: [""]} placeholder, nothing to expose
# supportedVoices list only -- no live selection field to read a current
# value from, unlike SOUND_MODE/SOUND_OUTPUT above.
"/settings/sound/voice/vs/0",
# rssi/wifiFrequency (network housekeeping) plus an unconfirmed
# 48-slot absenceInfo P/A history blob with no documented meaning --
# don't guess what it encodes.
"/csi/information/vs/0",
]
# Built as bare no-entity caps; folded into the AC registry (not global).
@@ -538,6 +538,9 @@
"mute": "Ztlumeno"
}
},
"energy_saving_mode": {
"name": "Režim úspory energie"
},
"air_purifier_sound_mode": {
"name": "Zvukový režim",
"state": {
@@ -787,6 +790,12 @@
"indirect": "Nepřímý"
}
},
"energy_saving_state": {
"name": "Stav úspory energie"
},
"energy_saving_operating_status": {
"name": "Provozní stav úspory energie"
},
"current_temp_c": {
"name": "Teplota"
},
@@ -1040,6 +1049,9 @@
"absence_power_saving_active": {
"name": "Úspora energie při nepřítomnosti aktivní"
},
"absence_clean": {
"name": "Čištění při nepřítomnosti"
},
"motion_detect_wind_active": {
"name": "Vyhýbání se proudu vzduchu při pohybu aktivní"
},
@@ -538,6 +538,9 @@
"mute": "Mute"
}
},
"energy_saving_mode": {
"name": "Energy saving mode"
},
"air_purifier_sound_mode": {
"name": "Sound mode",
"state": {
@@ -787,6 +790,12 @@
"indirect": "Indirect"
}
},
"energy_saving_state": {
"name": "Energy saving state"
},
"energy_saving_operating_status": {
"name": "Energy saving operating status"
},
"current_temp_c": {
"name": "Temperature"
},
@@ -1040,6 +1049,9 @@
"absence_power_saving_active": {
"name": "Absence power saving active"
},
"absence_clean": {
"name": "Absence clean"
},
"motion_detect_wind_active": {
"name": "Motion-detect wind avoidance active"
},
@@ -663,6 +663,9 @@
"mute": "Silencio"
}
},
"energy_saving_mode": {
"name": "Modo de ahorro de energía"
},
"air_purifier_sound_mode": {
"name": "Modo de sonido",
"state": {
@@ -909,6 +912,12 @@
"indirect": "Indirecto"
}
},
"energy_saving_state": {
"name": "Estado de ahorro de energía"
},
"energy_saving_operating_status": {
"name": "Estado de funcionamiento del ahorro de energía"
},
"current_temp_c": {
"name": "Temperatura"
},
@@ -1165,6 +1174,9 @@
"absence_power_saving_active": {
"name": "Ahorro por ausencia activo"
},
"absence_clean": {
"name": "Limpieza por ausencia"
},
"motion_detect_wind_active": {
"name": "Evitación de viento por detección de movimiento activa"
},
@@ -538,6 +538,9 @@
"mute": "Muto"
}
},
"energy_saving_mode": {
"name": "Modalità risparmio energetico"
},
"air_purifier_sound_mode": {
"name": "Modalità audio",
"state": {
@@ -787,6 +790,12 @@
"indirect": "Indiretto"
}
},
"energy_saving_state": {
"name": "Stato risparmio energetico"
},
"energy_saving_operating_status": {
"name": "Stato di funzionamento del risparmio energetico"
},
"current_temp_c": {
"name": "Temperatura"
},
@@ -1040,6 +1049,9 @@
"absence_power_saving_active": {
"name": "Risparmio energetico in assenza attivo"
},
"absence_clean": {
"name": "Pulizia in assenza"
},
"motion_detect_wind_active": {
"name": "Elusione flusso d'aria a rilevamento movimento attiva"
},
@@ -538,6 +538,9 @@
"mute": "음소거"
}
},
"energy_saving_mode": {
"name": "절전 모드"
},
"air_purifier_sound_mode": {
"name": "소리 모드",
"state": {
@@ -787,6 +790,12 @@
"indirect": "간접풍"
}
},
"energy_saving_state": {
"name": "절전 상태"
},
"energy_saving_operating_status": {
"name": "절전 작동 상태"
},
"current_temp_c": {
"name": "온도"
},
@@ -1040,6 +1049,9 @@
"absence_power_saving_active": {
"name": "부재 절전 작동"
},
"absence_clean": {
"name": "부재 청소"
},
"motion_detect_wind_active": {
"name": "모션 감지 바람 회피 작동"
},
@@ -538,6 +538,9 @@
"mute": "Stil"
}
},
"energy_saving_mode": {
"name": "Energiebesparingsmodus"
},
"air_purifier_sound_mode": {
"name": "Geluidsmodus",
"state": {
@@ -787,6 +790,12 @@
"indirect": "Indirect"
}
},
"energy_saving_state": {
"name": "Energiebesparing status"
},
"energy_saving_operating_status": {
"name": "Bedrijfsstatus energiebesparing"
},
"current_temp_c": {
"name": "Temperatuur"
},
@@ -1040,6 +1049,9 @@
"absence_power_saving_active": {
"name": "Energiebesparing bij afwezigheid actief"
},
"absence_clean": {
"name": "Reiniging bij afwezigheid"
},
"motion_detect_wind_active": {
"name": "Bewegingsdetectie luchtstroomvermijding actief"
},