Commit Graph
86 Commits
Author SHA1 Message Date
Marc Billow eecbc19dde fix(observe): clear stale notifications before re-attempting observe mode 2026-07-09 18:36:36 -05:00
Marc Billow 97260441f1 feat(observe): add subscribe-and-wait mode selection 2026-07-09 18:33:03 -05:00
Marc Billow eba73118e9 feat(observe): add ObserveManager with write-settle guard 2026-07-09 18:28:46 -05:00
Marc Billow 3ab0d5553e refactor(fridge): use HA's own translation system for enum entities
Replace the ad-hoc option_names dicts (hardcoded English, manual
reverse-mapping) with the idiomatic HA approach: normalize raw Samsung
enum values to lowercase at the select/sensor boundary and let
strings.json/translations/en.json supply the display text, keyed by
the normalized value. select.py maps the chosen (lowercased) option
back to the device's original casing before writing. The ice-making
status sensor now also declares device_class=enum with an options
list, as HA expects for enum sensor state translations.

Also drop the hardcoded _BZONE_MODES list in favor of discovering
beverage zone modes from the device's own roomSupportedModes field,
matching how flex_zone_mode and ice_type already work.
2026-07-09 16:22:52 -05:00
Marc Billow b386063e79 fix(fridge): translate enum select/sensor values in Python, not strings.json
hassfest rejects translation keys containing uppercase letters, but the
raw Samsung enum values for flex zone, beverage zone, ice type, and ice
making status are uppercase (e.g. CV_TTYPE_RF9000A_FREEZE). Follow the
existing dryer-course pattern instead: map raw device values to
human-readable names in Python via a new SelectDesc.option_names field
(mirrored read/write in select.py) and a value_fn for the sensor, and
drop the now-unused translation_key entries from strings.json/en.json.
2026-07-09 14:12:58 -05:00
Marc Billow 5b8a079fa6 chore: prepare repo for HACS listing
- Add hacs.json and MIT LICENSE required for HACS/default-store inclusion
- Fill required manifest.json keys (documentation, issue_tracker,
  codeowners) and reorder per hassfest's key-ordering rule
- Add validate.yml workflow running hassfest and HACS validation
- Update README install note now that hacs.json is checked in
2026-07-09 14:02:46 -05:00
Marc Billow 5d1a32aa9a Map remaining coverage-gap hrefs, fix discover() false-gap bug, improve device naming
Maps the fridge/dishwasher hrefs flagged by last session's coverage-gap
Repairs issue: defrost delay (switch) + block status (diagnostic binary
sensor), master ice-maker enable, self-check trigger/status, dishwasher
diagnosis trigger/status, and last-operation-source. Adds fallback
capabilities for /doors, /temperatures, and /icemaker/status that only
bind when the richer per-instance hrefs they duplicate are absent, so
simpler devices without those hrefs still get the data. Everything else
in the original gap list moves to capabilities.ignored (Bixby audio
feedback, inert DR/energy-planner resources, redundant metadata).

Fixes discover() reporting a known href as an unregistered gap whenever
its match_fn declined to bind for that device (e.g. a filter capability
on hardware without that filter) — it's now only a gap when no capability
is registered for the href at all.

Device naming now uses x.com.samsung.da.modelNum (not the OCF /oic/p,d
metadata, which produced inconsistent names like "[dishwasher] Samsung")
to build "Samsung <Type> (<model>)".
2026-07-08 15:24:34 -05:00
Marc Billow d2699f0f5b Add diagnostics platform, config-flow gap warning, issue template
Completes the device-capability-diagnostics spec:

- diagnostics.py: the standard HA diagnostics hook, returning device type,
  one_ui_version, unbound hrefs, and the redacted raw resource tree, plus
  integration and smartthings-local version numbers. This is what the
  Repairs issue (added in the previous commit) points users at, and what
  they attach to a device-support issue.
- config_flow.py: _probe_and_validate now also reports oneUiVersion and
  whether the device type is recognized. Recognized types are unaffected;
  an unrecognized type shows a new confirm_unknown_type step explaining
  that only common capabilities will be available before creating the
  entry, so expectations are set at setup time rather than only after the
  fact via Repairs.
- .github/ISSUE_TEMPLATE/device-support.yml: structured template for
  filing a capability gap, linked from both the Repairs issue and the
  config-flow confirmation step.
- README: short section pointing at this whole mechanism.
2026-07-08 00:10:30 -05:00
Marc Billow ca754cbb01 Re-add redacted device dump fixtures
Companion to the git-filter-repo pass that just stripped every prior
version of these two files (which carried a real Samsung account email,
Bixby access token/device-ID hash, and real WiFi/BLE MAC addresses) out of
history entirely. This commit reintroduces only the already-redacted
content, so the real values are no longer reachable from any commit.
2026-07-07 23:52:38 -05:00
Marc Billow 35b47d6b5a Add coverage-gap detection: ignored-hrefs registry, Repairs issue, redaction
Implements the first half of the device-capability-diagnostics spec:

- registry/capabilities/ignored.py: known-noise hrefs (Bixby/voice
  provisioning, WiFi/BLE info, OTA/region housekeeping, and a couple of
  redundant hrefs) declared as no-entity Capability objects, so
  discover()'s existing unknown-resource reporting treats them as covered
  instead of flagging every device as having gaps. Folded into each
  by_type registry and into the global fallback CAPABILITIES set.
- discovery.py: log() callback now passes the raw href instead of a
  formatted message, so callers can collect a clean unbound-hrefs list.
- coordinator.py: wires that callback into self._unbound_hrefs, tracks
  device_type_name/one_ui_version, and raises (or clears) a Repairs issue
  when a device's type is unrecognized or it has genuinely unmodeled
  hrefs left over.
- registry/redact.py: recursive, substring-keyed redaction for anything
  that looks like account/identity data, ahead of the diagnostics.py
  platform that will consume it.

Verified against the real dishwasher/refrigerator fixtures: known-noise
hrefs no longer show up as gaps, while genuine gaps (e.g. /bespoke/vs/0
on the fridge) still do.
2026-07-07 23:49:58 -05:00
Marc Billow b67e0e5cf5 Rename ocf/ to registry/, it's a capability registry now
The DTLS/CoAP transport code that made "ocf" an accurate name moved out to
the smartthings-local package. What's left here (capability.py, entities.py,
discovery.py, adapter.py, identity.py, capabilities/, by_type/, plus the
/device/0 batch parser) is entirely the device capability registry, so name
the package for what it does.

Flattened the redundant ocf/registry/ nesting into a single top-level
registry/ package and updated every import across the platform modules and
test suite accordingly. Verified: full test suite (80/80) passes, and the
Docker dev container reconnects to both live appliances and rediscovers
their entities cleanly after the rename.
2026-07-07 22:54:57 -05:00
Marc Billow 3502a385cc Drop stale .gitignore entries from the MQTT-bridge era
Companion to the previous commit: the ocf_root_ca.pem exception and the
bridge.log/course_mapper.log ignores referenced files that no longer exist.
2026-07-07 22:45:14 -05:00
Marc Billow ab097ffae2 Remove dead MQTT-bridge-era files and stale .gitignore entries
requirements.txt (cbor2/paho-mqtt/pyOpenSSL for the old standalone bridge),
.env.example (APPLIANCE_*/MQTT_*/deploy.sh env vars), and deploy.sh (ssh+tar
deploy for the old bridge container) all referenced main.py and
samsung_appliance/, which no longer exist. Nothing in the current HA
integration, its tests, or the README depends on them; config now happens
entirely through the HA config flow, and manifest.json/requirements-dev.txt
own the Python dependencies.

Also dropped the .gitignore exception for ocf_root_ca.pem (that vendored
file was deleted along with the rest of the vendored transport) and the
now-inapplicable "Bridge runtime" log ignores.
2026-07-07 22:45:03 -05:00
Marc Billow 4c1497f5ba Bake smartthings-local into the dev Docker image
The dev container was relying on HA's runtime pip-install of manifest.json
requirements, which only fires when the integration is set up and needs
outbound network access at that exact moment; a container that had been
running since before the smartthings-local migration kept the old code
loaded in memory and never went through that install path, so restarting
it failed once it picked up the new manifest.json.

Repurpose the stale MQTT-bridge-era Dockerfile (its own code was already
deleted from this repo) to build on the official HA image with
smartthings-local pre-installed, and point docker-compose.yml at it via
`build: .`. Verified end-to-end: rebuilt the image, recreated the
container, and confirmed both live appliances (fridge, dishwasher)
reconnect and discover entities with no runtime install needed.
2026-07-07 22:38:38 -05:00
Marc Billow fd61e882d1 chore: update readme phrasing and instructions 2026-07-07 22:33:00 -05:00
Marc Billow ee068ed78d Rewrite README prose to remove AI-writing tropes
Replaced all em-dashes with plain punctuation, broke up the mechanical
bold-lead-in bullet list in "What you get" into prose paragraphs, and
removed the "X, not Y" contrastive framing and decorative arrows.
2026-07-07 22:25:01 -05:00
Marc Billow 12d60c295e Frame linked setup_cert.py as a reference example, not an instruction
Part 2 pointed readers to run smartthings-local's setup_cert.py directly;
reword it as an example of how to obtain the CA cert/key rather than a
step to execute, since this repo makes no claim about how that script
behaves or is maintained upstream.
2026-07-07 22:20:15 -05:00
Marc Billow 44afe4f00a Point README at upstream setup_cert.py instead of vendoring a copy
Getting the AC14K_M CA cert+key is a protocol-layer concern, not an
HA-integration concern, and the two copies here had already drifted from
each other. Drop root setup_cert.py + requirements-bootstrap.txt and have
Part 2 explain why the CA cert is needed, then link to the smartthings-local
project's setup_cert.py as the canonical way to obtain it.
2026-07-07 22:19:22 -05:00
Marc Billow 4782878583 Depend on published smartthings-local package, rewrite README
Now that mbillow/localthings#8's protocol fixes are merged upstream and
smartthings-local 0.1.0 is on PyPI, drop the vendored ocf/coap_dtls.py +
ocf/observe_refresh.py transport (dead code, never instantiated) and the
duplicated ocf_root_ca.pem in favor of the real package. manifest.json and
requirements-dev.txt now pin smartthings-local>=0.1.0 instead of the
unmerged git branch.

README rewritten from scratch — it still described the old standalone
MQTT-bridge (samsung_appliance/, main.py, docker-compose bridge) that this
repo moved off of; it now documents the real architecture: a native HA
custom component with config-flow-driven cert minting and a per-device-type
capability registry, with the protocol layer split out to smartthings-local.
2026-07-07 22:08:02 -05:00
Marc Billow db2148467b feat: dishwasher cycle/settings entities, stale-state fixes, LED/sound controls
Dishwasher:
- Revert remote_control to BinarySensorDesc ("Smart Control", read-only)
- Add cycle SelectDesc on /course/vs/0 via options-array RMW (AI Wash,
  Pre blast, Self clean, Normal, Express 60, Heavy, Pots and pans,
  Delicate, Plastic, Baby Care)
- Add Storm Wash+ and Auto release dry SwitchDescs on /course/vs/0
- Add Sanitize SwitchDesc and Smart Dry SelectDesc on /dishwasher/vs/0
- Add Sound volume NumberDesc (0–15 step 5) on /settings/sound/volume/vs/0
- Add Door LED night brightness, night start/end time to DOOR_LED capability
- Add SOUND_VOLUME capability to laundry module

Stale-state fixes (firmware leaves values set after cycle ends):
- finish_time: suppress when machine state is not active
- progress: return 'Idle' when machine state is not active
- water filter: gate WATER_FILTER capability on filterStatus != 'notused'

Translations:
- Add ICESTATUS_RUN → "Making ice" to strings.json and en.json

Tests:
- Update dishwasher and refrigerator golden fixtures to reflect current entity set
- Remove dead test_project_extrapolates_remaining_time (on_observation removed)
2026-07-01 00:36:24 -05:00
Marc Billow 800b113495 feat: mutable child lock, remote control, delay start; stale-data fallback on poll failure
Child lock and remote control promoted from read-only binary sensors to
writable switches. Delay start time promoted from a raw string sensor to
a writable TimeEntity.

On persistent poll failure (both initial attempt and reconnect), fall back
to the last known resource values instead of raising UpdateFailed. This
keeps entities available with stale data rather than flooding history with
Unavailable gaps whenever the device drops a /device/0 block fetch.
UpdateFailed is still raised on the very first poll before any data exists.
2026-06-30 23:29:31 -05:00
Marc Billow eb6e2d34d7 feat: replace completion_time/completion_minutes with finish_time timestamp
Removes two redundant sensors (a raw H:MM:SS string and a duration integer)
in favour of a single SensorDeviceClass.TIMESTAMP that shows the absolute
estimated finish time. The HA frontend auto-renders this as "in X minutes"
and keeps it current without re-polling.

Also removes dead _project / _on_observation / _rem_minutes code from both
operational.py and oven.py — these hooks were declared on the Capability but
never called anywhere in the coordinator or adapter.
2026-06-30 23:16:06 -05:00
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 8d43d54952 chore: add Docker Compose dev environment for HA testing
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
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 0bb27a9204 test: scaffold pytest-homeassistant-custom-component test harness
Adds HA component tests covering the localthings integration layer
(previously only the samsung_appliance library was tested):

- pyproject.toml: asyncio_mode=auto for async HA test patterns
- tests/localthings/conftest.py: hass_config_dir override, mock fixtures
  for probe and coordinator session, fridge fixture loader
- test_config_flow.py: first-device form, second-device cred reuse,
  happy path, cannot_connect error, duplicate-device abort (5 tests)
- test_coordinator.py: discovery on first refresh, device_info populated,
  idle interval default, UpdateFailed on persistent poll error (4 tests)
- test_sensor.py: sensors registered, sensor state from coordinator (2 tests)

Total: 70 library + 11 component = 81 tests
2026-06-30 16:34:10 -05:00
Marc Billow ee779c955b remove TODO_SECURITY.md — all findings resolved 2026-06-30 16:34:10 -05:00
Marc Billow d5dc92e673 fix: enable DTLS peer verification against Samsung OCF Root CA
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.
2026-06-30 16:34:10 -05:00
Marc Billow ceab7585d9 fix: drop @SECLEVEL=0 and SHA-1 cert signing
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
2026-06-30 16:34:10 -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
Marc Billow 785154eb83 refactor: replace exact-href door/temp/icemaker caps with generic pattern caps
Delete dead: DOOR_FRIDGE, DOOR_FREEZER, TEMP_FRIDGE/FREEZER_CURRENT/SETPOINT,
ICEMAKER_ONE, ICEMAKER_TWO, ICE2_MODES, temperature bounds constants.

Add: TEMP_CURRENT_GENERIC (href_prefix=/temperature/current/),
TEMP_SETPOINT_GENERIC (href_prefix=/temperature/desired/, write derives
CoAP path from href), ICEMAKER_GENERIC (href_prefix=/icemaker/,
match_fn gates on iceMaker.state field).

Key derivation strips 'vs' segments alongside digits so icemaker hrefs
like /icemaker/one/vs/0 produce icemaker_one_state not icemaker_one_vs_state.

write_fn signature extended: adapter now passes href as 3rd positional arg;
all existing write_fns updated to accept href=None.

Fridge golden: old ice1_*/ice2_*/fridge_*/freezer_* keys replaced by
icemaker_one_*/icemaker_two_*/temperature_current_*/temperature_desired_*.
2026-06-30 16:33:36 -05:00
Marc Billow 9e5a169c82 refactor: drop resources fallback; fixtures are device0-only 2026-06-30 16:33:36 -05:00
Marc Billow fd2aa9e64d fix: replace IP-named dumps with scrubbed named fixtures
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.
2026-06-30 16:33:36 -05:00
Marc Billow 27d6d09fd9 refactor: auto-derive pattern cap keys from href; move dumps to tests/fixtures
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.
2026-06-30 16:33:36 -05:00
Marc Billow 1c4cb2e272 fix: address final review findings
- 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
2026-06-30 16:33:36 -05:00
Marc Billow 6f681622c2 fix: remove unused request param, use context manager in e2e v2 test 2026-06-30 16:33:36 -05:00
Marc Billow ee7ae10481 test: add v2 e2e smoke test (batch + per-type registry full pipeline) 2026-06-30 16:33:36 -05:00
Marc Billow d4ff29d797 feat: switch conftest + golden tests to batch-first resource loading
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.
2026-06-30 16:33:36 -05:00