Merge remote-tracking branch 'origin/main' into claude/code-comments-guidelines-jiv4ta

This commit is contained in:
Marc Billow
2026-08-05 01:32:58 +00:00
13 changed files with 1420 additions and 0 deletions
+2
View File
@@ -194,6 +194,8 @@ Samsung's firmware occasionally drops the DTLS session briefly — this is norma
If reconnects become persistent (more than a handful per minute), something's actually wrong. Check the appliance's Wi-Fi link first, then look for a competing DTLS client on the LAN — only one active session per appliance is allowed at a time.
Deregistering a device in SmartThings causes a reset of its network settings as soon as it accesses Samsung's servers, dropping it off Wi-Fi until it's re-onboarded through the SmartThings app. As such, consider keeping devices registered even if egress-blocked, to avoid them resetting upon brief internet access.
### Multi-subdevice ("2-in-1") air conditioner systems
Some Samsung installs run more than one indoor subdevice off a single outdoor unit, all reachable over the *one* IP/DTLS session your config entry connects to (a floor-standing + wall-mounted 2-in-1 is a common shape). The integration discovers any sibling subdevices automatically, once, right after the first successful poll — there's nothing to configure. Each discovered subdevice gets its own HA device (linked to the main one via "via device") and its own `climate` card, so it lands in its own room in the dashboard instead of being invisible or mixed into the master's state.
@@ -14,6 +14,7 @@ from dataclasses import replace
from ..capability import Capability
from ..entities import (
BinarySensorDesc,
ButtonDesc,
ClimateDesc,
NumberDesc,
SelectDesc,
@@ -480,6 +481,45 @@ CLIMATE = Capability(
icon="mdi:fan-auto",
entity_category="config",
),
# A drying cycle the unit runs after cooling, to keep the coil from going
# mouldy. Three tokens describe it and the switch above only covered the
# first: Autoclean_ is the setting, AutocleanProgress_ is how far a
# running cycle has got, and StopAutoClean_ is a channel for ending one
# early -- its presence is what says the appliance takes that at all
# (the app gates its own stop button on exactly that), and the value it
# reports while nothing is running is Idle.
#
# The percentage scale is the app's own: `<progress max="100">` with the
# token rendered as "{{value}}%" beside it. An idle unit here reports 1
# rather than 0, the same floor the laundry firmware's progressPercentage
# sits at when Ready, so 0-vs-1 is not a reliable "is it running" test --
# which is why the button below is not gated on it.
#
# The sensor shares AUTO_CLEAN's catalog entry, like auto_clean_legacy
# above: same figure, different board generation. Distinct key, so
# nothing collides if a board ever reported both.
SensorDesc(
key="auto_clean_progress_legacy",
translation_key="auto_clean_progress",
rep_fn=_option_token_num("AutocleanProgress"),
exists_fn=_has_option_token("AutocleanProgress"),
unit="%",
state_class="measurement",
icon="mdi:progress-check",
entity_category="diagnostic",
),
ButtonDesc(
key="auto_clean_stop",
field="",
payload="StopAutoClean_Set",
icon="mdi:fan-off",
entity_category="config",
exists_fn=_has_option_token("StopAutoClean"),
write_fn=lambda p, rep, href=None: (
["mode", "vs", "0"],
{"x.com.samsung.da.options": [p]},
),
),
SwitchDesc(
key="air_monitoring",
rep_fn=_option_token_on("AirMonitoring"),
@@ -117,6 +117,9 @@
"diagnosis_start": {
"name": "Spustit diagnostiku"
},
"auto_clean_stop": {
"name": "Zastavit samočištění"
},
"pause": {
"name": "Pozastavit"
},
@@ -117,6 +117,9 @@
"diagnosis_start": {
"name": "Start diagnosis"
},
"auto_clean_stop": {
"name": "Stop auto clean"
},
"pause": {
"name": "Pause"
},
@@ -239,6 +239,9 @@
"diagnosis_start": {
"name": "Iniciar diagnóstico"
},
"auto_clean_stop": {
"name": "Detener la autolimpieza"
},
"pause": {
"name": "Pausar"
},
@@ -117,6 +117,9 @@
"diagnosis_start": {
"name": "Avvia diagnosi"
},
"auto_clean_stop": {
"name": "Arresta autopulizia"
},
"pause": {
"name": "Pausa"
},
File diff suppressed because it is too large Load Diff
@@ -117,6 +117,9 @@
"diagnosis_start": {
"name": "Diagnose starten"
},
"auto_clean_stop": {
"name": "Zelfreiniging stoppen"
},
"pause": {
"name": "Pauzeren"
},
@@ -2,6 +2,7 @@
"state_keys": [
"alarm_code",
"auto_clean_legacy",
"auto_clean_progress_legacy",
"beep",
"clean_level",
"climate",
@@ -18,6 +19,7 @@
"super_fine_dust",
"subdevice1_alarm_code",
"subdevice1_auto_clean_legacy",
"subdevice1_auto_clean_progress_legacy",
"subdevice1_beep",
"subdevice1_climate",
"subdevice1_current_temperature_c",
@@ -3,6 +3,7 @@
"air_monitoring",
"alarm_code",
"auto_clean_legacy",
"auto_clean_progress_legacy",
"beep",
"climate",
"current_temperature_c",
@@ -3,6 +3,7 @@
"air_monitoring",
"alarm_code",
"auto_clean_legacy",
"auto_clean_progress_legacy",
"beep",
"climate",
"current_temperature_c",
@@ -3,6 +3,7 @@
"air_monitoring",
"alarm_code",
"auto_clean_legacy",
"auto_clean_progress_legacy",
"beep",
"climate",
"current_temperature_c",
@@ -235,6 +235,32 @@ def test_filter_alarm_time_reads_the_threshold_and_writes_one_token():
)
def test_auto_clean_progress_and_stop_come_off_their_own_tokens():
"""Three tokens describe the drying cycle and the switch only covered the
first. AutocleanProgress_ is a percentage -- the app renders it into a
`<progress max="100">` beside a "{{value}}%" label -- and StopAutoClean_ is
the channel for ending a cycle early, whose presence is what says the
appliance accepts that at all (the fixture reports Idle, this unit's
resting value)."""
assert _state()["auto_clean_progress_legacy"] == 1.0 # the fixture's own value
desc = _desc(_load_device(FIXTURE), "auto_clean_stop")
assert desc is not None
assert desc.write_fn(desc.payload, {}) == (
["mode", "vs", "0"],
{"x.com.samsung.da.options": ["StopAutoClean_Set"]},
)
def test_auto_clean_stop_stays_off_boards_without_the_token():
"""Newer boards run the same cycle off /option/autoclean/vs/0 and say
nothing about stopping it, so writing a legacy token there would be a
guess."""
newer = _load_device("airconditioner_tp1x_rac")
assert _desc(newer, "auto_clean_stop") is None
assert "auto_clean_progress_legacy" not in _state("airconditioner_tp1x_rac")
def test_filter_alarm_time_stays_off_boards_with_a_real_threshold_resource():
"""Newer boards carry air_filter_threshold off supportedFilterDesiredUsage;
two thresholds on one device would be a coin flip for the user.