Commit Graph
15 Commits
Author SHA1 Message Date
Marc Billow 62fc3b44f4 fix: alarm code sensor registers on stub resources, shows all active codes
Two fixes for the alarm_code diagnostic sensor:

1. _is_included now treats an empty rep ({}) from /device/0 as a stub
   meaning the resource exists but hasn't been individually fetched yet.
   Previously the field-presence check against {} returned False, leaving
   the entity unregistered and showing Unavailable from the entity registry.

2. Replace _last_alarm_code (silently dropped all but the final alarm) with
   _active_alarm_codes which joins all codes in the items list. Shows 'none'
   when no alarms are active.
2026-06-30 23:02:11 -05:00
Marc Billow 022a192bca feat: CoAP inter-request pacing to prevent RT-OCF rate limit drops
Samsung appliances silently drop CoAP requests when hit faster than their
firmware ceiling (~8–14 req/s measured; dishwasher unknown). Multi-block
Block2 fetches on LAN hit ~100 req/s instantaneously, causing the observed
burst timeouts on block 4 and block 10 of /device/0.

Adds rate_limit_rps (default 5 req/s / 200 ms) to DtlsCoapSession with a
pace() method that enforces the inter-block delay via _stop.wait() so session
teardown interrupts the sleep cleanly. Coordinator calls pace() between hrefs
in _poll_hrefs_blocking. Also adds device IP to all GET log lines for easier
per-device filtering.
2026-06-30 22:40:14 -05:00
Marc Billow 9ea4ddd3a3 fix: gate entity registration on field presence by default
_is_included now checks that an entity's field exists in the resource
rep before registering it, preventing phantom entities when optional
fields are absent on a shared resource. Explicit exists_fn still takes
priority for cases requiring custom logic. All platforms use the shared
helper; select.py's inline duplicate removed.
2026-06-30 16:59:45 -05:00
Marc Billow da573a725f feat: fridge entity improvements, CoAP reliability fixes, time platform
Fridge entities:
- Add strip_prefix_in_key to pattern caps; rename temp entities to
  "Cooler/Freezer Temperature" and "Cooler/Freezer Setpoint"
- Add number sliders (NumberMode.SLIDER)
- Remove redundant sensors/binary_sensors that shadow switches
- Add entity_category='config' to setpoints, refrigeration modes,
  autofill, sabbath, sound, LED, flex zone, nighttime ice, ice type,
  beverage zone
- Add WELCOME_LIGHTING, STATUS_LOCK (auto door opener + fridge sound),
  DOOR_ALERT (alarm tone select 1-4), CABINET_LIGHT_ENHANCED (daytime
  brightness, night brightness, brighten gradually, start/end time),
  ICEMAKER_NIGHTTIME, FLEX_ZONE
- Add time platform with rep_fn support for derived end-time entity;
  apply timezone.offset when reading/writing night schedule times
- Fix ice_making_status translation: move to entity.sensor section

CoAP/DTLS reliability:
- Fix Block2 SZX negotiation: adopt server's block size for block 1+
- Add per-block retransmission (3 attempts, 4s each)
- Add 5s asyncio.sleep before DTLS reconnect for TizenRT session teardown

Translations:
- Add HA translation_key system for ice type, beverage zone, flex zone,
  night brightness, door alarm; remove Python-level value_fn translation
2026-06-30 16:34:39 -05:00
Marc Billow 98fa7196f2 feat: cleaner temperature entity names and remove redundant sensors
- Add strip_prefix_in_key to Capability so pattern caps can drop their
  href_prefix segments from key derivation; temp caps use it so
  /temperature/desired/cooler/0 → "Cooler Setpoint" and
  /temperature/current/cooler/0 → "Cooler Temperature" instead of the
  verbose "Temperature Desired Cooler Temp F"
- Change TEMP_CURRENT key to 'temperature' and TEMP_SETPOINT to 'setpoint'
  to keep state keys distinct and names readable
- Number entities now render as sliders instead of text boxes
- Remove ICEMAKER_STATUS sensor (redundant with per-unit enabled switches)
- Remove Cabinet light level sensor and Cabinet light on binary sensor
  (redundant with Cabinet light switch)
2026-06-30 16:34:39 -05:00
Marc Billow 57cf1cdd11 fix: address code review findings from post-onboarding audit
- Add button platform (button.py + PLATFORMS) so oven/washer Start/Pause/Stop
  controls actually appear in HA
- Load DTLS cert chain from memory via _load_pem_chain(); eliminate all temp
  file writes for key material in config_flow and coordinator
- Fix PEM boundary: ensure leaf and CA blocks are separated by a newline in
  the stored fullchain regardless of whether the user pasted a trailing newline
- Dryer mode select: add static options tuple and value_fn to decode Course_XX
  hex strings back to human names; was permanently empty before
- Remove callable branch from LocalThingsSelect (options_field covers dynamic
  case; callable contract was broken — called with no args vs Callable[[dict]])
- Guard native_min/max_value overrides with hasattr + super() fallback to
  prevent AttributeError when native_min=None and range_field returns nothing
- Add async_close() public method to coordinator; __init__ now calls it instead
  of reaching in to _close_session directly
- Update tests: rename CONF_CERT_PEM/KEY_PEM → CA/LEAF variants, add
  leaf_cert_pem/leaf_key_pem to mock_probe return, fix mock PEM constants
  (no trailing newlines, matching what .strip() stores)
2026-06-30 16:34:39 -05:00
Marc Billow f25e616950 feat: mint leaf certs from CA keypair, fix DTLS SSL level, polish entity model
Config flow now accepts the AC14K_M CA cert+key, fetches the Samsung cloud
UUID from connect-v2.samsungiotcloud.com, mints a fresh RSA-2048 leaf cert
signed by the CA, and stores both the CA keypair and generated leaf for
reconnect. The DTLS context uses @SECLEVEL=0 in the cipher string — the only
channel that reaches cryptography's bundled OpenSSL — to permit the SHA-1
signed AC14K_M intermediate in Samsung's cert chain.

Entity model fixes:
- EntityCategory string values coerced to EntityCategory enum in entity.py
- SelectDesc gains options_field for live options from resource data; select
  registration skips entities where exists_fn rejects the initial resource
- NumberDesc gains range_field for live min/max from resource data
- Temperature sensors/numbers now carry device_class and unit (°F)
- Icemaker state sensor and on binary_sensor removed (duplicates of switch)
- Ice type select gated on x.com.samsung.da.iceType.supported presence
- Sabbath mode icon updated to mdi:hands-pray
2026-06-30 16:34:39 -05:00
Marc Billow f132d75f72 fix: use RTLD_DEFAULT in ctypes to hit same OpenSSL instance as pyOpenSSL 2026-06-30 16:34:39 -05:00
Marc Billow eb316580ae fix: send only leaf cert to avoid SHA-1 chain rejection
use_certificate_chain_file sends the full PEM chain including the
AC14K_M intermediate CA, which is SHA-1 signed. OpenSSL 3.x at
SECLEVEL=2 raises SSL_R_CA_MD_TOO_WEAK when it sees this cert — before
the handshake starts, before any verify callback fires.

Switch to use_certificate_file, which reads only the first PEM block
(the leaf cert, SHA-256 signed). Samsung's device already has AC14K_M
in its trust store and can verify the leaf without receiving the chain.

Also add verbose debug logging to _set_security_level so the logs show
exactly which API path ran and whether it succeeded, to aid diagnosis
if the error recurs after a library update.
2026-06-30 16:34:39 -05:00
Marc Billow 027768f7d7 fix: lower DTLS context security level to allow SHA-1 signed cert chain
The fullchain.pem we send as our client cert contains the AC14K_M
intermediate CA, which is SHA-1 signed. Samsung's OCF Root CA (trust
anchor) is also SHA-1 self-signed. OpenSSL 3.x SECLEVEL=2 rejects
SHA-1 in any certificate it touches — including our own outbound chain —
raising SSL_R_CA_MD_TOO_WEAK before the verify callback is reached.

Fix: call SSL_CTX_set_security_level(ctx, 1) before loading any certs.
Tries pyOpenSSL >= 24.0 ctx.set_security_level(), falls back to cffi,
then ctypes as a last resort. SECLEVEL=1 (80-bit minimum) allows SHA-1
while still enforcing meaningful key-strength constraints.
2026-06-30 16:34:39 -05:00
Marc Billow df4eeb0de5 fix: handle X509_V_ERR_CA_MD_TOO_WEAK in DTLS verify callback
@SECLEVEL=1 in the cipher string does not reliably lower the security
level for DTLS_METHOD contexts across all pyOpenSSL/OpenSSL builds.

Instead, explicitly allow error 66 (X509_V_ERR_CA_MD_TOO_WEAK) in the
verify callback. This is the error OpenSSL emits when a CA certificate
uses SHA-1 — which Samsung's OCF Root CA does. All other verification
errors (chain trust, expiry, key usage, revocation) are still enforced.
2026-06-30 16:34:39 -05:00
Marc Billow 58ecbaab46 fix: set SECLEVEL=1 to allow SHA-1 signed Samsung OCF Root CA
OpenSSL 3.x defaults to SECLEVEL=2, which rejects SHA-1 digests in the
certificate chain. Samsung's OCF Root CA is SHA-1 signed, so peer
verification failed with "ca md too weak" on every connect attempt.

SECLEVEL=1 (80-bit minimum) allows SHA-1 in the CA chain while still
enforcing meaningful key-strength and cipher constraints. Not going back
to SECLEVEL=0 which removes all restrictions.
2026-06-30 16:34:39 -05:00
Marc Billow b372bca377 feat: add translations and improve config flow UX
- translations/en.json: runtime translation file HA loads (strings.json
  is tooling-only); includes data_description hints for host, cert, key

- config_flow.py: cert_pem and key_pem rendered as multiline text areas
  via TextSelector so users can paste full PEM blocks; host stays single-line

- strings.json: expanded error messages and data_description to match

- entity.py: entities with name=None (pattern-cap doors, temps, icemmakers)
  now derive a human-readable title from their state key instead of
  displaying blank ("door_cooler_open1" → "Door Cooler Open 1")
2026-06-30 16:34:39 -05:00
Marc Billow cc2c1e5a1f refactor: move samsung_appliance/ into custom_components/localthings/ocf/
Makes the integration fully self-contained for HACS distribution.
Users installing via HACS get everything in one directory; no external
samsung_appliance package needed.

All component imports updated to relative (.ocf.*); all test imports
updated to custom_components.localthings.ocf.*. ocf_root_ca.pem moves
with the package and resolves correctly via Path(__file__).parent.
2026-06-30 16:34:20 -05:00
Marc Billow f7b58f00ee feat: replace MQTT bridge with localthings HA custom component
Convert from MQTT bridge architecture to a proper Home Assistant
integration (custom_components/localthings/):

- Config flow: IP-only entry, probes ports [49154,49155], cert/key
  reused from first device on subsequent adds
- Coordinator: DataUpdateCoordinator wrapping blocking DTLS session
  via async_add_executor_job; discovery on first poll; dynamic 5s/30s
  interval based on active_when
- Entity platforms: sensor, binary_sensor, switch, number, select
- Library cleanup: delete bridge.py, main.py, config.py, logger.py,
  keepalive.py, poll_scheduler.py, sensors.py, state_cache.py
- adapter.py stripped to flatten(), is_active(), _key()
- _parse_device0_batch moved to samsung_appliance/batch.py
2026-06-30 16:34:06 -05:00