Extract usable parts of PR #316 (System Fresh Air Ventilator support)
PR #316 (fork stale by several months, most of its ~2200-line diff was drift against main rather than real changes) proposed device support for the Samsung System Fresh Air Ventilator (ACA-KR-TP2-21-AN9000). Extracted what holds up, adapted to this project's conventions, and left out what doesn't: Extracted: - ventilation_mode select on CLIMATE's own href, gated via _is_ventilation_mode_device so it can only ever bind on a device whose entire supportedModes set is Purification/Ventilation/SmartVentilation -- verified against every real AC fixture in the corpus to confirm it can't false-positive on an actual air conditioner's climate card. - WINDFREE / WINDSLEEP switches on their own dedicated hrefs. - A CO2 sensor on AIR_QUALITY, matching air_monitor.SENSORS' already-bound device_class='carbon_dioxide'/unit='ppm' descriptor for the same field shape rather than guessing fresh. - HEPA_FILTER / DEVICE_ACTIVE reuse from air_purifier.py. - Removing /airlevelcheck/vs/0 from _AC_IGNORED and binding air_purifier.AIR_LEVEL_CHECK in its place: the PR's claim that this project's old "scheduler plumbing" description was wrong turned out to be independently verifiable against two of our own existing fixtures (airconditioner_cac and airconditioner_tp1x_da_ac_rac_01011 both already carry real, populated periodicSensingActivationState/autoExeState values), so this benefits existing users, not just the one new device. Left out: - Unit/device_class ('ug/m3', pm10/pm25/pm1) on the existing dust/ fine_dust/super_fine_dust sensors, sourced from an unverified third-party screenshot description. air_monitor.py already has an explicit, reasoned rejection of this exact mapping for the exact same three fields: Samsung's PM10/PM2.5 convention doesn't confirm where a third tier or a PM1 reading fits, and a wrong guess mislabels the reading forever. - A standalone common.POWER switch -- contradicts this registry's own documented design (power is deliberately the climate entity's job) and would affect every AC user, not just this device. - Promoting wind/swing to independent selects for every AC user -- a UX opinion, not a coverage necessity, and out of scope for this device's own support. - A model-name diagnostic sensor -- /information/vs/0 is already covered via the global ignore list, so this wasn't closing an actual gap. No raw diagnostics dump for this model was ever attached to PR #316, so there's no fixture for it here (fabricating one would violate this project's fixture-integrity rule) -- see tests/test_airconditioner_ventilation_windfree.py's module docstring.
This commit is contained in:
13 files changed
+361
-1
No files matched your search
@@ -58,6 +58,16 @@ REGISTRY = DeviceRegistry(
|
||||
airconditioner.ENERGY_SAVING,
|
||||
airconditioner.EDGE_LIGHTING,
|
||||
airconditioner.LIGHT_STATEFUL,
|
||||
# System Fresh Air Ventilator (PR #316, ACA-KR-TP2-21-AN9000):
|
||||
# WINDFREE/WINDSLEEP are this device's own hrefs; HEPA_FILTER/
|
||||
# DEVICE_ACTIVE reuse air_purifier.py's identical shapes.
|
||||
# AIR_LEVEL_CHECK is not this-device-specific -- see its
|
||||
# removal from _AC_IGNORED above.
|
||||
airconditioner.WINDFREE,
|
||||
airconditioner.WINDSLEEP,
|
||||
air_purifier.HEPA_FILTER,
|
||||
air_purifier.DEVICE_ACTIVE,
|
||||
air_purifier.AIR_LEVEL_CHECK,
|
||||
*airconditioner.COVERAGE,
|
||||
]
|
||||
),
|
||||
|
||||
@@ -206,6 +206,28 @@ def _mode_options(rep):
|
||||
return opts if isinstance(opts, (list, tuple)) else ()
|
||||
|
||||
|
||||
# Samsung's "System Fresh Air Ventilator" (PR #316, model
|
||||
# ACA-KR-TP2-21-AN9000, vid DA-AC-DIFFUSER-01001) self-reports oic.d.
|
||||
# airconditioner and routes through this same CLIMATE capability, but its
|
||||
# /mode/vs/0 supportedModes are Purification/Ventilation/SmartVentilation --
|
||||
# none of which climate.py's HVAC-mode table knows, so hvac_mode collapses
|
||||
# to a single stuck value with no way to tell the three apart. Gated to
|
||||
# devices whose *entire* supported-mode set is this vocabulary, so it can't
|
||||
# false-positive on a real AC's Cool/Heat/Dry list.
|
||||
_VENTILATION_MODE_VALUES = frozenset(("Purification", "Ventilation", "SmartVentilation"))
|
||||
|
||||
|
||||
def _is_ventilation_mode_device(rep, resources):
|
||||
supported = rep.get("x.com.samsung.da.supportedModes")
|
||||
if not isinstance(supported, (list, tuple)) or not supported:
|
||||
return False
|
||||
return set(supported) <= _VENTILATION_MODE_VALUES
|
||||
|
||||
|
||||
def _ventilation_mode_write(payload, rep, href=None):
|
||||
return ["mode", "vs", "0"], {"x.com.samsung.da.modes": [payload]}
|
||||
|
||||
|
||||
def _has_display_light_option(rep, resources):
|
||||
"""True when the panel light lives in /mode/vs/0's `Light_*` option
|
||||
token rather than a dedicated /light/vs/0 switch -- the two encodings
|
||||
@@ -532,6 +554,17 @@ CLIMATE = Capability(
|
||||
rep_fn=_first_mode,
|
||||
write_fn=_climate_write,
|
||||
),
|
||||
# Purification/Ventilation/SmartVentilation mode select (PR #316) --
|
||||
# _is_ventilation_mode_device gates this to devices using that
|
||||
# vocabulary exclusively, so a real AC's climate card is unaffected.
|
||||
SelectDesc(
|
||||
key="ventilation_mode",
|
||||
rep_fn=_first_mode,
|
||||
exists_fn=_is_ventilation_mode_device,
|
||||
options_field="x.com.samsung.da.supportedModes",
|
||||
icon="mdi:air-filter",
|
||||
write_fn=_ventilation_mode_write,
|
||||
),
|
||||
# Panel light switch for boards that encode it in /mode/vs/0's options
|
||||
# instead of a dedicated /light/vs/0 (see _has_display_light_option).
|
||||
# Shares the switch.display_light translation key with DISPLAY_LIGHT
|
||||
@@ -1364,6 +1397,47 @@ LIGHT_STATEFUL = Capability(
|
||||
),
|
||||
)
|
||||
|
||||
# Wind-Free / Wind-Sleep mode toggles (PR #316, ACA-KR-TP2-21-AN9000). Each
|
||||
# on its own dedicated href, so unlike ventilation_mode above these need no
|
||||
# device gating -- absent on every other family's dump. Write contract
|
||||
# extrapolated from this file's other plain On/Off options-array fields
|
||||
# (AIR_PURIFY, AUTO_CLEAN); not confirmed live.
|
||||
WINDFREE = Capability(
|
||||
href="/modeoption/windfree/vs/0",
|
||||
poll_tier="warm",
|
||||
entities=(
|
||||
SwitchDesc(
|
||||
key="windfree",
|
||||
field="x.com.samsung.da.windfree",
|
||||
icon="mdi:leaf",
|
||||
entity_category="config",
|
||||
value_fn=lambda v: v == "On",
|
||||
write_fn=lambda p, rep, href=None: (
|
||||
["modeoption", "windfree", "vs", "0"],
|
||||
{"x.com.samsung.da.windfree": "On" if p == "On" else "Off"},
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
WINDSLEEP = Capability(
|
||||
href="/modeoption/windsleep/vs/0",
|
||||
poll_tier="warm",
|
||||
entities=(
|
||||
SwitchDesc(
|
||||
key="windsleep",
|
||||
field="x.com.samsung.da.windsleep",
|
||||
icon="mdi:sleep",
|
||||
entity_category="config",
|
||||
value_fn=lambda v: v == "On",
|
||||
write_fn=lambda p, rep, href=None: (
|
||||
["modeoption", "windsleep", "vs", "0"],
|
||||
{"x.com.samsung.da.windsleep": "On" if p == "On" else "Off"},
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
# /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
|
||||
@@ -1400,6 +1474,27 @@ AIR_QUALITY = Capability(
|
||||
("super_fine_dust", "mdi:weather-fog", "SuperFineDust"),
|
||||
)
|
||||
),
|
||||
# CO2 (PR #316, ACA-KR-TP2-21-AN9000) -- a type this file's other AC
|
||||
# families don't report. Same field/shape air_monitor.SENSORS
|
||||
# already models with device_class='carbon_dioxide'/unit='ppm', so
|
||||
# this matches that descriptor rather than guessing fresh -- unlike
|
||||
# the pm10/pm25/pm1 mapping air_monitor.py's own docstring
|
||||
# deliberately rejects for the three dust-type keys above (Samsung's
|
||||
# two-tier PM10/PM2.5 convention doesn't confirm where a third tier
|
||||
# or PM1 fits), ppm for a field literally named CO2 isn't a guess of
|
||||
# that kind.
|
||||
SensorDesc(
|
||||
key="co2",
|
||||
field="x.com.samsung.da.items",
|
||||
icon="mdi:molecule-co2",
|
||||
entity_category="diagnostic",
|
||||
device_class="carbon_dioxide",
|
||||
state_class="measurement",
|
||||
unit="ppm",
|
||||
exists_fn=_has_sensor_type("CO2"),
|
||||
enabled_default=False,
|
||||
value_fn=lambda items: _int(_sensor_item_value(items, "CO2")),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -1419,7 +1514,12 @@ _AC_IGNORED = [
|
||||
# state or documented write contract. /option/muteonce/vs/0 and
|
||||
# /selfcheck/vs/0 are deliberately NOT here -- see MUTE_ONCE above and
|
||||
# common.SELF_CHECK, both of which have a confirmed, modelable contract.
|
||||
"/airlevelcheck/vs/0", # periodic air-quality sensing scheduler plumbing
|
||||
# /airlevelcheck/vs/0 is deliberately NOT here either (PR #316):
|
||||
# despite this list's old description of it as "scheduler plumbing",
|
||||
# both the CAC and TP1X_DA_AC_RAC_01011 fixtures already carry real,
|
||||
# populated periodicSensingActivationState/autoExeState values here --
|
||||
# the AI-Purify feature air_purifier.AIR_LEVEL_CHECK already models,
|
||||
# reused below rather than reinvented.
|
||||
"/aisleep/vs/0", # AI-sleep feedback state (no actionable control)
|
||||
"/availablecontrolsets/vs/0", # opaque hex-encoded control-set bitmap
|
||||
"/da/softreset/vs/0", # soft-reset trigger plumbing
|
||||
|
||||
@@ -692,6 +692,14 @@
|
||||
"high": "Vysoký",
|
||||
"low": "Nízký"
|
||||
}
|
||||
},
|
||||
"ventilation_mode": {
|
||||
"name": "Režim",
|
||||
"state": {
|
||||
"purification": "Čištění",
|
||||
"ventilation": "Větrání",
|
||||
"smartventilation": "Chytré větrání"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
@@ -1246,6 +1254,12 @@
|
||||
},
|
||||
"indicator_light": {
|
||||
"name": "Kontrolka"
|
||||
},
|
||||
"windfree": {
|
||||
"name": "Režim Wind-Free"
|
||||
},
|
||||
"windsleep": {
|
||||
"name": "Noční režim"
|
||||
}
|
||||
},
|
||||
"time": {
|
||||
|
||||
@@ -692,6 +692,14 @@
|
||||
"high": "High",
|
||||
"low": "Low"
|
||||
}
|
||||
},
|
||||
"ventilation_mode": {
|
||||
"name": "Mode",
|
||||
"state": {
|
||||
"purification": "Purification",
|
||||
"ventilation": "Ventilation",
|
||||
"smartventilation": "Smart Ventilation"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
@@ -1246,6 +1254,12 @@
|
||||
},
|
||||
"indicator_light": {
|
||||
"name": "Indicator light"
|
||||
},
|
||||
"windfree": {
|
||||
"name": "Wind-Free mode"
|
||||
},
|
||||
"windsleep": {
|
||||
"name": "Sleep mode"
|
||||
}
|
||||
},
|
||||
"time": {
|
||||
|
||||
@@ -817,6 +817,14 @@
|
||||
"high": "Alto",
|
||||
"low": "Bajo"
|
||||
}
|
||||
},
|
||||
"ventilation_mode": {
|
||||
"name": "Modo",
|
||||
"state": {
|
||||
"purification": "Purificación",
|
||||
"ventilation": "Ventilación",
|
||||
"smartventilation": "Ventilación inteligente"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
@@ -1371,6 +1379,12 @@
|
||||
},
|
||||
"indicator_light": {
|
||||
"name": "Luz indicadora"
|
||||
},
|
||||
"windfree": {
|
||||
"name": "Modo Wind-Free"
|
||||
},
|
||||
"windsleep": {
|
||||
"name": "Modo nocturno"
|
||||
}
|
||||
},
|
||||
"time": {
|
||||
|
||||
@@ -692,6 +692,14 @@
|
||||
"high": "Alta",
|
||||
"low": "Bassa"
|
||||
}
|
||||
},
|
||||
"ventilation_mode": {
|
||||
"name": "Modalità",
|
||||
"state": {
|
||||
"purification": "Purificazione",
|
||||
"ventilation": "Ventilazione",
|
||||
"smartventilation": "Ventilazione intelligente"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
@@ -1246,6 +1254,12 @@
|
||||
},
|
||||
"indicator_light": {
|
||||
"name": "Spia luminosa"
|
||||
},
|
||||
"windfree": {
|
||||
"name": "Modalità Wind-Free"
|
||||
},
|
||||
"windsleep": {
|
||||
"name": "Modalità notte"
|
||||
}
|
||||
},
|
||||
"time": {
|
||||
|
||||
@@ -692,6 +692,14 @@
|
||||
"high": "높음",
|
||||
"low": "낮음"
|
||||
}
|
||||
},
|
||||
"ventilation_mode": {
|
||||
"name": "모드",
|
||||
"state": {
|
||||
"purification": "청정",
|
||||
"ventilation": "환기",
|
||||
"smartventilation": "스마트환기"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
@@ -1246,6 +1254,12 @@
|
||||
},
|
||||
"indicator_light": {
|
||||
"name": "표시등"
|
||||
},
|
||||
"windfree": {
|
||||
"name": "무풍 모드"
|
||||
},
|
||||
"windsleep": {
|
||||
"name": "취침 모드"
|
||||
}
|
||||
},
|
||||
"time": {
|
||||
|
||||
@@ -692,6 +692,14 @@
|
||||
"high": "Hoog",
|
||||
"low": "Laag"
|
||||
}
|
||||
},
|
||||
"ventilation_mode": {
|
||||
"name": "Modus",
|
||||
"state": {
|
||||
"purification": "Zuivering",
|
||||
"ventilation": "Ventilatie",
|
||||
"smartventilation": "Slimme ventilatie"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
@@ -1246,6 +1254,12 @@
|
||||
},
|
||||
"indicator_light": {
|
||||
"name": "Indicatorlampje"
|
||||
},
|
||||
"windfree": {
|
||||
"name": "Wind-Free modus"
|
||||
},
|
||||
"windsleep": {
|
||||
"name": "Slaapmodus"
|
||||
}
|
||||
},
|
||||
"time": {
|
||||
|
||||
Reference in new issue
Block a user