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.
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.
@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.
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.
- 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")
docker-compose.yml:
- Uses ghcr.io/home-assistant/home-assistant:stable
- network_mode: host (required for DTLS/UDP to reach LAN appliances)
- custom_components/localthings mounted read-only into /config/custom_components/
- ha_config/ volume for persistent HA state
ha_config/configuration.yaml:
- Minimal config (frontend, http, logger)
- localthings logged at DEBUG, everything else at warning
ha_config/ gitignored — contains auth tokens and secrets after onboarding
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.
Bundle certs/ocf_root_ca.pem as samsung_appliance/ocf_root_ca.pem and
switch from SSL.VERIFY_NONE to SSL.VERIFY_PEER. The appliance's server
certificate must now chain to the Samsung Electronics OCF Root CA,
closing the ARP-spoof / MITM attack surface on the local network.
.gitignore: carve out explicit exception for the bundled public CA cert.
Firmware accepts SHA-256-signed leaves (confirmed in TODO_SECURITY.md §3).
@SECLEVEL=0 was only needed to allow SHA-1 signatures past OpenSSL 3.x's
default security level — no longer required.
- coap_dtls.py: cipher string is now ECDHE-ECDSA-AES128-GCM-SHA256 (no seclevel override)
- local-tools/setup_cert.py: sign with sha256 instead of sha1
- TODO_SECURITY.md: mark MQTT finding resolved (bridge removed), mark SHA-1/SECLEVEL resolved
Rename tests/fixtures/dumps/{IP}.json to tests/fixtures/{name}_device.json.
Scrub PII: serialNum, otnDUID, modelNum, diagEndPoint replaced with
TEST-* placeholders. conftest: _load_device(name) as primary API;
_load_resources(ip) kept as legacy shim for existing test call sites.
Remove key_fn from Capability — pattern cap keys are now always derived
automatically: strip digits from href segments, join with '_', append
'_' + desc.key. /door/cooler/0 + 'open' -> door_cooler_open.
DOOR_GENERIC entity key: 'open' (not 'door_open'). Remove _door_key helper.
Fridge registry: DOOR_FRIDGE/DOOR_FREEZER replaced by DOOR_GENERIC pattern cap.
Golden baseline: door_fridge_open -> door_cooler_open.
Move device dumps to tests/fixtures/dumps/ (tracked) so tests run in CI
without local-tools/ dependencies.
- for_device(): suffix fallback for descriptive names
("french_door_refrigerator" → refrigerator registry)
- Capability.href_prefix: replaces rt_filter for pattern caps;
DOOR_GENERIC now uses href_prefix='/door/' (batch reps have no rt)
- discover(): apply href_prefix gate before rt_filter in pattern loop
- Remove dead bound_hrefs variable from discover()
- Remove vestigial default_observe_port from RuntimeDescriptor
conftest: _resources_from_dump() prefers /device/0 batch reps; falls
back to flat resources dict. Fixes _parse_device0_batch to include
stub reps (rep={href:...}) as {} so capabilities still bind on
resources with no current data (e.g. /alarms/vs/0 when no alarm active).
test_golden_regression: _new_state_and_uids now uses by_type.for_device
to select per-device-type capabilities + pattern caps.
Replace PollScheduler + OBSERVE with:
- _parse_device0_batch() extracts {href: rep} from /device/0 list response
- _discover_and_publish() uses by_type.for_device(oneUiVersion) for
device-specific capability registry; falls back to global CAPABILITIES
- _poll_loop thread: batch GET /device/0 at active_interval_s or
idle_interval_s; applies each rep to the state cache
- All OBSERVE machinery removed (subscribe, notifications, fetchback)
Remove observe_paths and poll_tiers from RuntimeDescriptor; add
active_interval_s (default 5s) and idle_interval_s (default 30s).
Delete _make_observe_paths() and _make_poll_tiers() helpers.
Bridge rewire in Task 6 will consume the new fields.
Registry changes from dict[str,Capability] to dict[str,list[Capability]].
discover() gains pattern_caps parameter (fallback for unmatched hrefs)
and gates each candidate on rt_filter and match_fn before binding.
Duplicate hrefs in registry allowed only if all caps set a discriminator.
OVEN_SETPOINT (/temperatures/vs/0) and OVEN_MODE (/mode/vs/0) collide
with fridge hrefs. Remove from ALL; only OVEN_CAVITY (/oven/vs/0) is
oven-unique. Per-device-type registries (v2 plan) will restore them in
the oven-only registry.
Also updates golden baselines to include legitimate new entities added
since original capture (cycle_active, firmware_update, power_switch for
dishwasher; cabinet_light_switch, ice1 for refrigerator).
- Make href Optional[str] = None for pattern capabilities
- Remove observe field (v2 architecture observes all hrefs)
- Add rt_filter, match_fn, key_fn fields to Capability
- Add entities default value tuple[()] to Capability
- Add key_override field to BoundEntity
- Update _key() to use key_override when present
- Update _make_observe_paths() to observe all unique hrefs (OBSERVE as accelerator)
- Guard _build() to skip pattern capabilities (href is None)
- Add test_bound_entity_key_override_takes_precedence test
- Update test_capability_defaults to remove observe assertion
All 61 tests passing (60 existing + 1 new).
C1: SwitchDesc MQTT discovery now emits payload_on/off='On'/'Off',
state_on/off='On'/'Off', and a Jinja2 boolean-to-string value_template
so HA can correctly interpret and command switch entities.
C2: POWER write_fn corrected — path is ['power','vs','0'] segment list
(not a single slash-prefixed string), and payload comparison is
p == 'On' (not truthiness) so 'Off' can actually be sent.
I2: Golden regression oracle strengthened to exact equality for both
state_keys and discovery_unique_ids; prior subset-only check allowed
over-production to go undetected. Two golden tests now fail revealing
stale fixtures (dishwasher: +3 keys; refrigerator: +9 keys) — fixtures
intentionally NOT updated, human review required.
PushBridge no longer takes a descriptor at construction time. Instead it
discovers entities from the live device cache immediately after the seed
(via registry.discover + build_runtime_descriptor), publishes HA discovery
payloads, wires on_observation, and builds command handlers — all in the
new idempotent _discover_and_publish(sess) method called from
_run_session_inner.
Changes:
- bridge.py: remove descriptor param; self.descriptor=None until first
connect; SEED_PATH constant replaces self.descriptor.seed_path; all
self.descriptor accesses guarded or moved post-discovery; inlined
_bridge_diagnostic_discovery (was in appliances/base.py); subscribe to
observe paths now happens after discovery (seed → discover → subscribe).
- config.py: klass is now optional (defaults to ''); topic_prefix/name
defaults handle empty klass.
- main.py: drop get_descriptor import and pairs loop; build bridges as
PushBridge(shared, app, cli); on_connect no longer publishes
discovery_payloads (bridge self-publishes at discovery time).
- Delete samsung_appliance/appliances/ entirely (6 files, ~2600 lines).
Generates regression oracle from DISHWASHER and REFRIGERATOR descriptors:
- tests/fixtures/golden/dishwasher.json: 21 state keys, 23 discovery entities
- tests/fixtures/golden/refrigerator.json: 26 state keys, 24 discovery entities
- tests/test_golden_regression.py: parametrized test that will turn green as
Tasks 9-12 port capabilities into the new registry
OCF rt is a schema category shared by many unrelated resources.
The stable unique resource address is the href. Update Capability,
discovery, common capabilities, tests, and plan accordingly.
Implement discovery module for converting OCF resource metadata into bound
entities, with support for multi-instance device resources and logging of
unknown capabilities.