Commit Graph
30 Commits
Author SHA1 Message Date
Marc Billow 676074b2f8 AC: quantize subdevice temperature writes against their own step (Opus review)
async_send_command handed write_fn/validate_fn the raw cache snapshot
(real, on-the-wire hrefs), not a subdevice-scoped view. Every other
consumer of a full resources dict (exists_fn, rep_fn, is_legacy_board,
...) reads through coordinator.canonical_resources() specifically to
avoid this; write_fn/validate_fn didn't, so on a composite AC (issue
#177) _temperature_step's resources.get(HREF_TEMP_CONTROL) saw the
master's /temperature/control/vs/0 instead of the subdevice's own
/temperature/control/vs/1, silently rounding a subdevice's 0.5-degree
write to a whole degree. The remote-control gate stays on the raw
snapshot -- /remotectrl/* is a shared, MAIN-only resource a
subdevice's owned-hrefs-only canonical view would drop entirely.

climate.py's target_temperature_step duplicated this same
read-in-order logic; pointed it at airconditioner._temperature_step
so the read and write paths can't drift again.

Also, from the same review:
- _quantize_temperature rejects non-finite floats (nan/inf survive
  float() but raise out of round()/division, escaping write_fn's
  documented None-on-bad-payload contract).
- Deduplicated the quantize-and-check block shared by the
  temperature_ocf/temperature branches of _climate_write.
- Added the /temperatures/vs/0 items[]-fallback test that was
  previously unreachable (every increment-carrying fixture also has
  /temperature/control/vs/0, which _temperature_step checks first).
- Added a coordinator-level test seeding an indexed subdevice with its
  own step, distinct from the master's, covering the fix above.
- The Towels/Bedding regression test now checks all 7 locale catalogs,
  not just English -- the bug is a code-mapping error, and
  test_every_language_mirrors_the_english_catalog only checks key
  topology, not values.
2026-08-09 16:25:12 +00:00
Marc Billow 846aefbcba Fix ty failures in new AC temperature-step tests (CI)
ClimateDesc.write_fn is typed as WriteFn (Callable[[Any, dict], ...]),
which only covers the (payload, rep) shape every other capability's
write_fn honors -- calling it through that alias with the climate-only
href/resources args, without first narrowing away the | None, failed
ty two ways: the missing "is not None" check and the extra positional
args past WriteFn's declared arity. Call _climate_write directly
instead, same as test_coordinator_send_command.py and
test_airconditioner_artik051_krac.py already do.
2026-08-09 16:06:22 +00:00
Marc Billow 248e473abe Fix AC temperature step quantization (PR #276, code review)
Samsung local AC temperature writes always rounded to the nearest
whole degree, dropping half-degree setpoints on boards that advertise
a 0.5 step (CAC and TP1X FAC). Squashed from moridew's PR #276 with
the review fixes applied:

- The /temperatures/vs/0 fallback never matched: its increment lives
  inside the resource's items[] array, not at the top level (same
  shape _temps_vs_item() already unwraps for current/unit). The
  original fix only ever worked through /temperature/control/vs/0.
- With no increment advertised anywhere (e.g. ARTIK051), writes went
  out unrounded instead of falling back to whole degrees the way
  climate.py's target_temperature_step already does.
- A non-numeric payload now rejects the write (returns None) instead
  of posting {"temperature": null} -- coordinator.py's
  async_send_command already drops a write_fn result of None.
- int/float normalization now happens once, in _quantize_temperature,
  instead of being duplicated (and skipped) per branch; a round(...,
  2) guards against float division noise (e.g. 21.7 / 0.1).

Tests rebuilt against real fixture resources (airconditioner_cac,
airconditioner_artik051_krac_18k) instead of a fabricated flat
resource shape no device produces.
2026-08-09 16:00:14 +00:00
Marc Billow d2787327fd Fix ty type-check failures in new tests (CI)
My local ty runs only covered custom_components, not tests -- CI runs
'ty check custom_components tests', which this branch had been failing
since the version-bump commit. All 13 diagnostics were the same two
established idioms this test suite already uses elsewhere, just missing
here:

- desc.write_fn/options_field are SelectDesc-only fields, unresolved on
  the SamsungEntityDescription base a bare 'next(e for e in ... if
  e.key == ...)' infers -- needs 'and isinstance(e, SelectDesc)' in the
  filter, same as test_fridge_capabilities.py's existing selects.
- rep_fn/match_fn are typed Optional even after narrowing to a concrete
  descriptor/capability, so calling one needs an explicit
  'assert x.rep_fn is not None' first, same as
  test_common_capabilities.py's POWER_VS_FALLBACK.match_fn precedent.

No behavior change -- test bodies are identical, just type-checkable.
2026-08-09 01:25:23 +00:00
Marc Billow 07c20e82aa Stop double-converting filterUsage on AIR_FILTER/HEPA_FILTER (#330)
filterUsage is already a 0-100 percentage on every confirmed family,
including ARTIK051_PRAC: filterStatus flips to 'wash' at
filterUsage == '100' regardless of filterCapacity (60/224/500 across
other fixtures), which only holds if filterUsage is already a percent.
filter_usage_percent() divided by filterCapacity again, reading a
filter due for washing as 20% fresh.

air_filter_usage_hours had the mirror problem: it read filterUsage
directly as an hour count with device_class=duration, when the field
is a percent. It's now derived from the percentage and filterCapacity
(new filter_usage_hours() in common.py) instead.
2026-08-09 00:37:09 +00:00
Marc Billow a7dc1db8ff 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.
2026-08-07 16:38:09 +00:00
Marc Billow 07061c734d Fix more pre-existing ty diagnostics in test files
Continues narrowing SamsungEntityDescription accesses to the correct
subclass and asserting Optional write_fn/match_fn/exists_fn fields are
set before calling them, per the pattern established in the previous
commit.
2026-08-03 00:03:08 +00:00
Marc Billow daf7e3787f Add ruff (lint + format) and ty (type checking) to the project
Adds [tool.ruff] and [tool.ty] config to pyproject.toml with a curated
ruff rule set (E, F, W, I, UP, B, C4, SIM, RUF, ASYNC, LOG, G, PIE, RET,
PERF, N), pins ruff/ty in requirements-dev.txt, reformats the whole tree
with `ruff format`, and fixes the pre-existing lint and type-check debt
those tools surfaced so both run clean.

Production-code type fixes include: HA's ConfigFlowResult vs. the
generic FlowResult in config_flow.py, narrowing BoundEntity.desc to its
platform-specific subclass (SelectDesc/NumberDesc/SensorDesc/etc.) via
cast() instead of an unchecked annotation, converting HA device_class
strings to their proper enum types, a resolve_registry callback typed
as `object` instead of `DeviceRegistry | None`, and a couple of other
narrow correctness fixes (CA key type validation, an index-out-of-bounds
false positive from an empty-tuple fallback, a bool/dict argument swap).

Test-file fixes are mechanical: narrowing SamsungEntityDescription to
the correct subclass via isinstance()/cast() before accessing
subclass-only fields, and asserting Optional write_fn/unit_fn fields
are set before calling them.
2026-08-02 23:56:38 +00:00
Petr Divis d0c68fb5ac AC: bind SmartCoolClean odor-controller option tokens; add Czech translation
- airconditioner.py: odor_controller_active binary_sensor + odor_controller_progress
  sensor, read from the /mode/vs/0 SmartCoolClean_/ProgressSmartClean_ option tokens
  (SmartThings cloud custom.airConditionerOdorController capability). Read-only --
  no confirmed write command.
- Reporter's TP1X_DA-AC-RAC-01001_0000 dump: fan speed and WindFree preset already
  bind via the existing composite climate entity, no code change needed there.
- en.json/nl.json: new entity names for the two additions.
- cs.json: new, full Czech translation catalog (mirrors en.json topology 1:1).
- New fixture + golden + tests locking in the odor-controller behavior.
2026-07-31 16:02:13 +02:00
Marc Billow 487ac748bd fix(airconditioner): model legacy-board beep as a switch, not a volume (#136)
Every ARTIK051_KRAC_18K-generation unit confirmed on hardware (three units
across two reporters) only ever carries Volume_100 or Volume_Mute in
/mode/vs/0's options -- never an intermediate value -- so the existing
buzzer_volume Number (0-100, step 10) modeled a control this firmware
doesn't have. Worse, its write path could never produce the literal
'Mute' token needed to actually turn the beep off, since it only ever
wrote a plain integer string. The 'beep' switch already used on newer
boards is the correct model here too; it's no longer gated off the legacy
board generation, and the Number entity is removed.

The WindFree-preset-not-applying report earlier in this issue self-resolved
per the reporter's own follow-up testing, so no code change was needed for
that part.
2026-07-29 13:16:09 +00:00
Marc Billow d83733a670 fix(airconditioner): scale legacy ARTIK051 cumulativePower correctly (#193)
ARTIK051_KRAC_18K-generation boards report /energy/consumption/vs/0's
cumulativePower in centiwatt-hours, not the plain Wh every other AC board
family reports -- confirmed against the reporter's own SmartThings-app
reading (raw 117430000 vs. the app's authoritative 1,174.30 kWh is exactly
a /100000 factor, not the shared wh_to_kwh's /1000 alone). Split
common.ENERGY_METER into generic/legacy variants on the airconditioner
registry, discriminated by the existing is_legacy_board() check, so every
other AC family keeps the unmodified shared capability.
2026-07-29 13:04:33 +00:00
Marc Billow a7aea3764b feat(airconditioner): make absence/motion-detect enable switches writable
status on both /mds/absencepowersaving/vs/0 and
/option/motiondetectwind/stateful/vs/0 is a bare On/Off boolean, the same
shape already shipped writable elsewhere in this file (MUTE_ONCE,
AUTO_CLEAN, AIR_PURIFY) without a live-confirmed write either -- worst
case a wrong token no-ops. The paired mode selects (switchPowerSaveMode,
motion-detect modes) stay read-only: their behavioral effect on live HVAC
isn't inferable from the dump, same reasoning as ANOMALY_LOAD's mode field.
2026-07-29 03:34:18 +00:00
Marc Billow 2b24152276 feat(registry): cover absence-power-saving and motion-detect-wind on AC (#173)
Lennox-branded heat pump on the Samsung RAC board family (modelNum
TP1X_LNX-AC-RAC-01001_0000) already routes correctly via the existing
'-RAC-' token, but its dump has two resources no prior AC fixture
carried: /mds/absencepowersaving/vs/0 and
/option/motiondetectwind/stateful/vs/0. Bind both as read-only
sensors, matching the CURRENT_LIMIT/ANOMALY_LOAD precedent -- nothing
in the dump confirms write safety on live HVAC hardware.
2026-07-29 02:52:33 +00:00
Marc Billow cafd7d5afa refactor(registry): drop oneUiVersion from device-type detection
oneUiVersion looks like the signal you'd want -- the device naming its own
type, '7.0 Dishwasher' -- and it was the first thing detection consulted. It
never earned the position:

- Only 7 of 49 fixtures report it at all.
- All 7 resolve to the same registry from their modelNum board token alone.
- No device-support issue has ever been fixed by adding a mapping for it.
  Every one went through modelNum. The alias keys it needed in
  _REGISTRY_BY_KEY ('airpurifier', 'air_conditioner', 'hood') were
  speculative when the registries were first written and never used since.

So it bought a key-normalizing helper (_type_key), a lookup with a suffix
fallback (for_device), three alias keys, and a second config-flow step whose
only reason to exist was phrasing a sentence about oneUiVersion -- for a
signal that has never once been decisive.

Remove it from detection. It stays in diagnostics, where it's genuinely
useful: it names the firmware generation ('7.0 Air conditioner' is Tizen
Lite), which matters when triaging an issue.

Detection order was also duplicated in four places -- the coordinator, the
config flow's probe, the golden-regression harness, and the skill -- which
is how the harness and the shipped order drift apart. Collapse it into
by_type.resolve(resources), and call that everywhere.

The two "appliance type not recognized" config steps become one. They
differed only in whether they blamed a missing oneUiVersion, which is not a
distinction a user can act on, and never was.

Verified by the full suite (795 passing), including every golden regression
-- so entity output is byte-identical for all 49 device fixtures.

TestOneUiVersionIsNotConsulted locks in the premise rather than just the
outcome: for every dump that reports a oneUiVersion, the model strings alone
must still reach a registry. If a future device breaks that, the test says
so instead of the device silently losing half its entities.

Also note in requirements-dev.txt that Python 3.13 resolves the pinned
harness floor -- 3.12 and older resolve nothing and fail the whole install.
2026-07-29 02:14:13 +00:00
Marc Billow bbf3f3f833 Revert unsound air-quality gating from #170, cover remaining fan-speed icons
An Opus review of merged PR #170 found the cleanLevel-scalar existence
gate on AIR_QUALITY doesn't hold up as a general rule: three fixtures
in this repo (air_purifier_device.json, air_purifier_vtww_device.json,
range_hood_device.json) carry genuinely populated Dust/FineDust/
SuperFineDust readings with no such scalar, so requiring it risks
silently dropping real air-quality readings on AC hardware this repo
hasn't seen yet. Reverted _has_sensor_type to item-type presence only
(as before #170) and moved the #166 fix to enabled_default=False on
all five entities instead -- same conservative, non-existence-gated
treatment already used for tropical_night_mode and the fridge/cooktop
precedents it was modeled on. Golden fixtures and tests updated to
match; the five sensors are bound-but-disabled on windfree/#17-style
boards again rather than unbound.

Also added icons for the AC fan_mode values #170 missed -- the raw
numeric labels ("1".."5") that TP1X_DA-AC-RAC-01001 and the window-AC
board report instead of turbo/max -- and swapped the whole fan-speed
icon family to mdi:fan-speed-1/2/3 for a more purpose-built look than
the generic speedometer, applied consistently to both the AC climate
card and the air purifier fan. Fixed motiondirect/motionindirect to
match core's smartthings integration's arrow pairing (previously
inverted).

Known limitation, not fixed here: enabled_default only affects newly
registered entities. Anyone who already has tropical_night_mode or the
five air-quality sensors enabled from #164 (a narrow window before
this fix, but real) won't see them auto-disable -- they'd need to
disable them by hand in Settings > Devices > Entities. A real fix
needs a one-time entity-registry migration, which this integration has
no existing infrastructure or test coverage for; scoping that felt
like its own follow-up rather than something to bolt on here.
2026-07-28 19:07:59 +00:00
Marc Billow 739881de16 Gate AC tropical night mode and air-quality sensors on real capability signals (#166)
Issue #166 (ARxxTXFCAWKNEU, board ARTIK051_PRAC_20K) reported tropical
night mode, clean level, dust, fine dust, odor, and super fine dust
entities showing up even though the reporter's units have no such
physical features. All six were added in #164.

The Sleep_<N> options token backing tropical_night_mode is present in
every AC dump on record regardless of confirmed reality, so there's no
usable signal at boot time -- it's now registered but disabled by
default (matching the precedent already set by fridge.rack_count /
cooktop.paired_hood_model), letting units that do have it opt in.

/sensors/vs/0's item-type list has the same problem (all five types
always listed, permanently zero on this board), but there turned out
to be a real tell: a top-level x.com.samsung.da.cleanLevel scalar is
present only alongside genuinely populated readings on every dump on
record (tp1x_da_ac_rac_01011, the tp1x_da_ac_air air purifier fixture)
and absent on every all-zero ARTIK051_PRAC_20K dump, including both
#166 units and the original windfree/#17 fixtures this capability was
first verified against -- which, per their /information/vs/0, turn out
to be the same board revision as #166's units, so that "verification"
never actually proved a real sensor either. AIR_QUALITY's exists_fn now
requires that scalar, and the windfree/airconditioner golden fixtures
are updated to match (those five entities no longer bind there).
2026-07-28 18:12:02 +00:00
Marc Billow 65b0326d62 Merge remote-tracking branch 'origin/main' into claude/issue-triage-138-latest-y90g78
# Conflicts:
#	tests/test_airconditioner_capabilities.py
2026-07-28 14:51:29 +00:00
Marc Billow daa7546208 Add device support for Wind-Free 2-in-1 AC TP2X_FAC_BORA_21K (#150, #153)
This board (a floor-standing + wall-mounted indoor unit pair sharing one
outdoor unit and one local IP) reports no oneUiVersion and carries the
'_FAC_' modelNum token, which no existing routing rule matched -- it fell
back to 'unknown' and exposed nothing but a power switch, with no climate
entity generated at all (both issues' reported symptom).

Once routed to the existing airconditioner registry, it binds cleanly
against the exact same CLIMATE composite every other room-AC family uses --
same Cool/Dry/Wind/AIComfort mode vocabulary, same wind-strength/humidity/
filter/energy resource shapes already modeled. Only two hrefs are unique to
this board: /subdevices/vs/0 (an opaque paired-subdevice id list -- issue
#150 asked whether the second indoor unit can be controlled separately;
it can't through this or any other resource in the dump, the same
"remote device ids, not locally actionable" role as the existing
/remotedeviceinfo/vs/0 ignore) and /runn/vs/0 (a single undocumented int
with no supported-values list to interpret). Both added to _AC_IGNORED
rather than guessed at.
2026-07-28 14:09:21 +00:00
Marc Billow c8597532b0 Stop collapsing a genuine fivepercentHumidity=0 reading to unknown (#160)
#146's zero-as-"not measuring" carve-out was meant for ARTIK051 boards'
plain x.com.samsung.da.humidity field, which only populates while Air
monitoring is briefly on and zeroes out afterward. It was accidentally
applied to fivepercentHumidity too, which every other AC board relies on
and which has never been documented getting stuck at zero -- so a real 0%
reading on those boards silently became "unknown". Only the humidity
fallback field now collapses 0; fivepercentHumidity passes 0 through as a
real reading.
2026-07-28 13:28:46 +00:00
Marc Billow 00c1d78373 Merge main into ac-additive-entities, resolve conflicts with #146
Both PRs independently modeled the same /mode/vs/0 Volume_*/Sleep_*
option tokens: #129 as beep/tropical_night_mode, #146 (already merged)
as buzzer_volume/good_sleep gated to the legacy ARTIK051 board
generation. They also each defined a helper named _option_token with
different return semantics (full token vs. value half) in
non-overlapping parts of the file, so git didn't flag it as a
conflict even though the second definition silently shadowed the
first.

Keep a single _option_token (value-half, the one already used by
buzzer_volume/good_sleep/spi/etc.), adjust beep's read/write to that
convention, and gate beep/tropical_night_mode off the legacy board so
they don't duplicate buzzer_volume/good_sleep on ARTIK051_KRAC-class
devices. Added a regression test locking in the gate.
2026-07-28 13:28:20 +00:00
blka 1bbecfa5c3 Drop /information/vs/0 version entities per review
mbillow's follow-up review: exposing read-only Software/Firmware (and
Outdoor/Touch IC) version strings is "data for the sake of exposing it" --
no user control, just clutter, and every other registry leaves
/information/vs/0 in the global ignore as identity plumbing. Conforming to
that stance rather than expanding the entity surface for no user story.

- by_type/airconditioner.py: revert to *ignored.IGNORED (no _IGNORED_LESS_INFO
  filter); drop airconditioner.INFO from the registry.
- capabilities/airconditioner.py: remove the INFO capability, the _info_version
  /_info_items_of_type /_has_info_version helpers, and HREF_INFORMATION.
- translations/{en,nl}.json: drop software_version, firmware_version,
  firmware_version_2/3, outdoor_unit_version, touch_ic_version.
- tests: drop the six INFO tests; golden regenerated for 8 AC + dehumidifier.
625 tests pass.
2026-07-28 10:07:39 +02:00
perseus177 55c7b88a8f fix(airconditioner): label the 2Step preset and test presets by their HA value
The legacy Comode codes resolve through the same dynamic resolver as a real
convenient resource, so Nano lands on the existing 'nano' preset (already
labelled WindFree) rather than on a 'windfree' value of its own -- the new
tests asserted the label instead of the value. 2Step had no catalog entry in
either language and would have surfaced as the raw code.

Also updates the existing five-percent-humidity test, which reached into the
descriptor's field/value_fn directly, to the rep_fn the fallback needs, and
covers the fallback itself.
2026-07-28 02:38:28 +02:00
blka 51fa1c5ea8 Address review: per-item versions, writable threshold, unit_fn, gating
Per mbillow's review on PR #129 (CHANGES_REQUESTED). Items 1-7 + smaller.

1. INFO: expose each /information/vs/0 version item per (type, ordinal) instead
   of collapsing to a single first-Firmware value. Boards carry 1-3 Firmware
   items (separate MCUs) plus an Outdoor unit and (window AC) a Touch IC item;
   each is a distinct version string. _info_version(items, type_, ordinal)
   + _has_info_version exists_fn gate, so an item with no number (tp2x_rac_20k's
   second Firmware) suppresses the entity rather than binding unknown.

2. air_filter_threshold is locally writable, not cloud-only -- confirmed live
   on ARTIK051_PRAC: POST filterDesiredUsage=700 to /filter/airdustfilter/vs/0
   -> 2.04, read-back 700, persists; restored to 500. Converted from a
   read-only sensor to a SelectDesc keyed to the device's
   supportedFilterDesiredUsage enum (options_field), with a write_fn that
   POSTs the scalar field. Only binds where the enum is advertised; boards
   without it leave this writable field unexposed rather than guess the valid
   set (don't-guess).

3. air_filter_usage_hours uses unit_fn reading filterCapacityUnit ('Hour'->'h')
   instead of a hardcoded unit, so a board advertising a different unit doesn't
   mislabel a duration statistic.

4. air_filter_usage_hours state_class is total_increasing, not measurement --
   filterUsage is a lifetime hour counter that resets on filter replacement.

5. INFO and air_filter_threshold now carry exists_fn (AIR_QUALITY already did),
   so they don't bind a permanently-unknown entity when their item/field is
   absent. Also fixes the caww_tp2 golden nuance (filterDesiredUsage absent
   -> no threshold key, matching what HA would actually create).

6. AIR_QUALITY: CleanLevel is corroborated as numeric by a top-level
   x.com.samsung.da.cleanLevel scalar (tp1x_da_ac_rac_01011 reports both as
   '1'), so clean_level is now an int measurement; odor/dust/fine_dust stay
   string diagnostics. Reinstated the 2-element-array ambiguity note (Dust/
   FineDust/SuperFineDust report ['0','0']; v[1] meaning unconfirmed, v[0]
   taken as the reading). SuperFineDust is now modeled for consistency with
   Dust/FineDust (same shape), rather than skipped without reason.

7. _beep_write restores the last non-Mute Volume level on 'On' instead of
   forcing Volume_100, so an intermediate setting (e.g. Volume_50 set via
   the cloud) survives an off/on cycle; falls back to 100 when no prior level.

Smaller: test_air_quality now asserts the tp1x_da_ac_rac_01011 clean_level==1
non-zero reading (the one value_fn-regression catch in the corpus); renamed
by_type's _AC_IGNORED -> _IGNORED_LESS_INFO to resolve the two-meaning
collision with the capabilities module's _AC_IGNORED (href strings); golden
regenerated for the 8 AC fixtures + dehumidifier.

612 tests pass.
2026-07-27 22:22:56 +02:00
blka 3da01f80d0 feat(airconditioner): add beep, tropical night, filter hours/threshold, air-quality, sw/fw version
Layer the ARTIK051_PRAC additive entities onto the upstream registry on a
fresh branch (additive-only; the round-1 display-light/mute-once/WindFree
work was independently shipped upstream and is not redone here).

New entities, all driven by single-token option_write or item reads:
- beep (switch): Volume_Mute/Volume_100 option token; single-token merge --
  a full options RMW reverts on ARTIK051_PRAC. Cloud counterpart:
  samsungce.airConditionerAudioFeedback (on/off only; level is cloud-only).
- tropical_night_mode (number 0-16): Sleep_<N> option token. Cloud:
  custom.airConditionerTropicalNightMode.
- air_filter_usage_hours / air_filter_threshold (duration sensors, h):
  raw filterUsage count and filterDesiredUsage alarm threshold. Threshold
  SET is cloud-only (samsungce.dustFilterAlarm); local read-only.
- clean_level / odor / dust / fine_dust (diagnostic sensors): /sensors/vs/0
  items[]. No unit advertised on the resource, so no device_class until a
  populated reading + unit is observed (don't-guess rule). SuperFineDust
  intentionally not modeled.
- software_version / firmware_version (diagnostic sensors):
  /information/vs/0 items[]. The href is globally ignored as identity
  plumbing; the AC registry drops that ignore so INFO is the sole cap on it.

Translations: en.json + nl.json (every-language-mirrors-english invariant).
Golden regression: regenerated the 8 AC fixtures + dehumidifier (reuses
AIR_FILTER) for the new state keys.

608 tests pass.
2026-07-27 14:43:07 +02:00
Marc Billow fa8f1b8675 Add cool-only global RAC support: WindFree, Auto mode, display light
Finishes PR #91's contribution (pedroperosin) with the requested review
changes applied, on our own branch:

- Preset resolution is now fully dynamic, read from each unit's own
  /mode/convenient/vs/0 supportedModes instead of a static per-model
  table -- any board's convenient modes (including WindFree's Nano/
  NanoSleep) surface without code changes. Unlabelled codes across
  existing fixtures (longwind, motionindirect, motiondirect, drycomfort)
  plus the two new WindFree ones are added to en.json/nl.json.
- 'Auto' now maps to HVACMode.AUTO instead of HEAT_COOL: these are
  single-setpoint "device decides" units, not two-setpoint heat+cool
  ones. 'Fan' is added alongside 'Wind' as a second FAN_ONLY spelling;
  a new _device_code_for_hvac() picks the code from the unit's own
  supportedModes since the flat map can't disambiguate two device codes
  mapping to one HA value.
- Detection gains a hyphenated '-RAC-' modelNum fallback (alongside the
  existing '_RAC_') for cool-only global RAC variants whose
  /otninformation/vs/0 ships no swVersionInfo block.
- Adds a display_light switch sourced from /mode/vs/0's opaque options
  blob (inverted Light_On/Light_Off token) for boards with no dedicated
  /light/vs/0 switch.

Write-path behavioural change (touches the path iterated on across
#9/#17/#27/#38/#54): power now targets the vendor /power/vs/0 instead of
the OCF /power/0, and _is_on() reads vendor-first. /power/0 is absent on
several known AC boards, so the previous OCF-first read/write pair could
report and act on stale state -- consistent with #53's "can turn on but
not off". Target temperature now picks its channel (OCF pair vs. vendor
items[]) based on which the unit actually reports, reading and writing
the same one.

The vendor temperature write and the new display-light write both carry
only the changed field(s), not the whole resource -- confirmed sufficient
on the wire, the device merges the rest itself. That requires the
coordinator's optimistic cache to do the same merge on the read side so a
setpoint change doesn't blank out current/min/max/unit for the settle
window: common.py gains merge_items_field() next to the existing
merge_options_field(), and async_send_command wires it in for any write
touching x.com.samsung.da.items.

New fixture/golden for the cool-only global RAC variant (TP1X_DA-AC-RAC-
01001, AI_RAC_GLOBAL_COOLONLY_3.0); display_light added to the goldens
for boards that gain the new options-based switch. Regenerated against
current main rather than copied from the original PR, since those
goldens had already shifted (current_temperature_c/humidity from issue
#75).
2026-07-27 05:00:06 +00:00
Marc Billow 3afbe6c6c1 Add humidity/temperature sensors and horizontal swing for AC (issue #75)
Three gaps reported against an ARTIK051_PRAC_20K WindFree unit vs. the
SmartThings integration:

1. Missing WindFree/motion convenient-mode presets -- left alone here.
   PR #91 replaces climate.py's static _DEVICE_TO_PRESET table with a
   generic resolver that reads any preset code straight off the unit's
   own supportedModes, which already covers this (and more generically
   than a per-model dict would) -- adding one here would just conflict.

2. No horizontal oscillation: /wind/direction/vs/0's supportedModes
   includes Left_And_Right, which _DEVICE_TO_SWING had no mapping for.
   Add it to HA's standard 'horizontal' swing constant.

3. No standalone humidity/current-temperature sensors: the climate card
   already reads both internally, but nothing exposed them as entities
   for history/automations. Add CURRENT_TEMPERATURE (OCF
   /temperature/current/0) with a CURRENT_TEMPERATURE_VS vendor fallback
   (same match_fn-gated pair shape as common.py's POWER_GENERIC/
   POWER_VS_FALLBACK), and a HUMIDITY sensor reading /humidity/vs/0's
   fivepercentHumidity field -- the only one of the three
   humidity-shaped fields across /humidity/0 and /humidity/vs/0 that
   isn't permanently stuck at 0 on every dump seen.

Regenerates the five existing AC goldens (all pick up
current_temperature_c; most pick up humidity) and adds a dedicated
fixture from the issue's WindFree dump, whose /humidity/vs/0 actually
has live fivepercentHumidity data.
2026-07-27 00:33:35 +00:00
Marc Billow 8e25e6dc71 Recognize A-CAWW-TP2-20-COMMON system air conditioners (issue #52)
New '-CAWW-' modelNum token for multi-indoor-unit commercial AC
installs -- these report no oneUiVersion, same as the other RAC/PRAC
boards. Once routed to the existing airconditioner registry, every
resource in the reporter's dump already binds except one new
SAC-specific installation-topology blob, now ignored.
2026-07-24 04:06:31 +00:00
Marc Billow e942c822a6 feat: extend AC support for TP2X_RAC_20K and TP1X_DA-AC-RAC-01001_0000
Closes the coverage-gap repair for two more room-AC boards (issues #37,

- TP2X_RAC_20K reports no oneUiVersion and no '_PRAC_' modelNum token, so
  its device type went unrecognized -- add a '_RAC_' fallback token to
  for_device_by_model.
- Bind the resources both dumps actually populate: mute-once (switch),
  display light (switch), self-check (reused from fridge.SELF_CHECK), and
  the circuit-breaker current-limit setting (read-only diagnostic sensors,
  since its write contract isn't confirmed from the dump).
- Ignore the rest as plumbing/opaque/unconfirmed: control-set descriptor,
  keepnormalstate flag, software-reset trigger, AI Sleep handshake,
  absence-monitoring stubs, remote-data-control, remote-temperature,
  reservation ruleset, and the welcome-temperature tracker.

Adds scrubbed fixtures + goldens for both dumps and extends the AC
capability/golden-regression tests to assert zero unbound hrefs.
2026-07-22 14:20:55 +00:00
paddawan2002 bc9d4f4cf5 Fix AC coverage for TP1X-class (oneUiVersion "7.0 Air conditioner") 2026-07-22 11:26:01 +02:00
Marc Billow 296535d0f2 feat: air conditioner support with a composite climate entity (#17)
Add support for Samsung room air conditioners (ARTIK051_PRAC-class), the
first device whose core controls map onto a single Home Assistant `climate`
entity rather than a scatter of switches/selects/numbers.

- New `climate` platform + `ClimateDesc`: one composite entity that reads
  power, HVAC mode, current/target temperature, fan (wind) strength, swing
  (wind direction) and the convenient-mode preset across several OCF
  resources and writes back to each. On/off folds into HVACMode.OFF /
  TURN_ON/OFF; convenient mode folds into preset_mode. The entity binds one
  primary resource (/mode/vs/0) and reads its siblings from the coordinator
  snapshot, reusing the cross-resource read pattern from number/select.
- New `airconditioner` capability module + by_type registry, routed via the
  `_PRAC_` modelNum token. Reuses common ALARMS/ENERGY_METER,
  fridge.FIRMWARE_UPDATE and dishwasher.DIAGNOSIS; air purify and auto clean
  as config switches; air dust filter status/usage as diagnostics (usage
  normalized to a percentage of rated capacity).
- Climate-consumed and all-zero/ambiguous resources (temperature/wind,
  /sensors, /humidity) are declared as AC-scoped coverage so every href in
  the dump binds or is covered -- no coverage-gap repair.
- Fan/swing modes map onto HA standard constants (auto-localized); the
  custom fan `turbo`, presets `quiet/smart/speed`, and the filter-status
  enum get translations in strings.json + translations/en.json.
- Scrubbed fixture, golden, and tests: registry routing, zero unbound
  hrefs, the climate write contract, and filter-% normalization.
2026-07-21 14:38:14 +00:00