81dbc6fa03bfaae80eef8b72d674bb50186b63de
8
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
ba089dbb5c |
Bump smartthings-local floor to 0.1.8
Two releases landed since the 0.1.6 pin, both confirmed by upstream (QuiteYellow, in issue #361) as additive/opt-in with no interface changes on our side: - 0.1.7: server-certificate profiles (SamsungServerProfile), a bounded DTLS handshake deadline (connect() now defaults to a 12s bound instead of none), and a cancellable connect() via ConnectCancellation. Our connect() call sites in coordinator.py and config_flow.py pass no args, so they pick up the bounded handshake for free; the cert-profile and cancellation pieces are opt-in and unused here. - 0.1.8: fixes blockwise OBSERVE notification reassembly (QuiteYellow/SmartThings-Local#39) -- a notification carrying only the first Block2 block was previously handed straight to on_notification instead of being reassembled, and separately, the Block2 loop could append a retransmitted/late block as if it were the next one, or miscompute the next block offset after a mid- transfer size downshift. Both corrupt a multi-block observed resource without necessarily truncating it -- the "premature end of stream" / "error decoding unicode string" CBOR failures reported in issue #361 on /mode/vs/0. All error types stay within the existing compatible-built-in table (ConnectionError/TimeoutError subclasses), so no exception handling changes. `>=0.1.6` already permitted pip to resolve 0.1.8 on a fresh install, but an environment that already has 0.1.6 or 0.1.7 satisfying that floor won't be upgraded by Home Assistant's requirement check -- which is what #361's reporter is very likely still hitting on 0.22.0. Raising the floor to >=0.1.8 forces that upgrade on the next release. Verified against smartthings-local 0.1.8 from PyPI: full suite (1573 tests), ruff, and ty all pass. No source changes needed beyond the three version pins (manifest.json, requirements-dev.txt, Dockerfile). |
||
|
|
3f9789512d |
Update to smartthings-local 0.1.6, handle redacted typed errors
Bumps the smartthings-local floor from >=0.1.2 to >=0.1.6 (manifest,
requirements-dev.txt, Dockerfile) and adopts the interface/behavior
changes introduced along the way:
- 0.1.3 ("redacted typed failures", PR #23) replaced connect()'s
ConnectionError(f"DTLS handshake error: {e}") with fixed, redacted
exceptions (SessionError, SessionTimeoutError, etc.) that never carry
backend text -- including the TLS alert name. The config flow's
_classify_handshake_failure relied on parsing that text out of the
exception (_alert_name) to tell a rejected certificate from any other
handshake failure; against a current library that regex never matches
again, silently downgrading every setup failure to the generic
"cannot_connect" message.
Fixed by adding _resolve_alert: it still tries _alert_name first (a
harmless fallback if it ever matches), then falls back to one bounded
smartthings_local.protocol.dtls_probe.diagnose_dtls_handshake() call
against the specific port that failed, which classifies the fatal
Alert straight from the raw record instead of an exception string.
_handshake_and_read now threads the resolved per-port alerts into
_classify_handshake_failure, so CertRejected vs. HandshakeFailed keeps
working the way it did before the redaction.
- 0.1.3 also moved the DTLS session onto a connected UDP socket (see
endpoint.py's open_connected_udp_socket), which changes why
coordinator._local_source_port needs a unique port per device -- the
kernel now demuxes by the full local-port/remote-peer tuple instead of
relying on an unconnected recvfrom(). Docstring updated to match.
- 0.1.6's reader-thread fail-fast fix (_check_live/_reader_running) makes
a dead reader raise SessionClosedError immediately instead of hanging
a request out to its timeout. No code change needed: SessionClosedError
is a ConnectionError subclass (not TimeoutError), so the coordinator's
existing _defer_reconnect_for/isinstance(e, TimeoutError) split already
routes it to the immediate-reconnect path.
Every other new/changed piece (endpoint.py, dtls_probe.py bounded
probing, auth.py's CertificateAuth/PskAuth providers) stays behind
compatible built-in exception types and unchanged get()/post()/
subscribe()/ping() signatures, per the library's own compatibility
table, so the coordinator's and observe.py's broad exception handling
needed no changes.
Tests: added coverage for _resolve_alert's exception-text vs.
diagnostic-handshake fallback, _classify_handshake_failure with a
resolved alerts mapping, and an end-to-end config-flow re-mint test
against a FakeSession that raises the new redacted SessionError instead
of the old text-bearing ConnectionError.
Full suite (1517 tests), ruff, and ty all pass against smartthings-local
0.1.6 installed from PyPI.
|
||
|
|
15be379243 |
Rebuild device discovery on the ClientHello probe and resolve identity up front
Two problems, one setup path. Port detection (issue #211): the config flow found the DTLS port by elimination -- a 1-byte UDP probe can't tell a silent port from a real DTLS server, so every port it couldn't rule out got a full certificate handshake, and every false positive cost the whole 12s HANDSHAKE_TIMEOUT_S before the next was tried. Adding an appliance took 30-40s. smartthings-local 0.1.2 ships a stateless ClientHello probe that settles this positively: a real DTLS server answers with a HelloVerifyRequest in ~1 RTT, and per RFC 6347 4.2.1 it does so without allocating association state, so the probe leaves nothing behind on the appliance. The whole 49152-49160 range is probed at once and exactly one confirmed port is given a certificate handshake. Fanning out is safe here in a way racing real handshakes is not -- each probe is bounded by a 3s budget, so the pool costs one probe's wall clock rather than the sum of the range, with no losing threads left running behind us. The UDP sweep stays as the fallback for when the probe confirms nothing: it errs in the opposite direction (it reports everything it can't rule out), so it still surfaces a device on a path that eats our ClientHello, and it keeps its issue #192 preferred-port rescue. Port detection now runs first and needs no credentials, so an unreachable host fails before any round trip to Samsung's cloud. And a second appliance reuses the existing entry's leaf cert rather than re-minting -- every device accepts the same one -- which makes adding one independent of Samsung-cloud reachability. A confirmed-live device rejecting the reused leaf (the UUID does rotate) re-mints and retries once, so reuse stays self-correcting; a timeout doesn't, since a fresh cert can't fix nothing answering. Identity (issue #236): the coordinator seeded device_serial with the configured host and only replaced it after the first successful poll. But device_serial mints *permanent* registry keys -- entity unique_ids and device identifiers -- so anything registering before that poll returned was written into the registry keyed on the IP address forever. The connection-mode sensor is added unconditionally rather than from `bound`, so it was the reliable victim: when the serial-keyed identity appeared moments later HA created a second device and entity, and the IP-keyed pair was orphaned. Deleting them didn't help; the next restart that lost the race recreated them. The probe already learns the identity, so store it on the config entry -- serial, model, manufacturer, device type. The coordinator seeds device_serial and its DeviceInfo from those at construction, so keys are correct from the first entity that registers even if the first poll is slow or fails outright. There is no placeholder left to correct. Discovery now treats the registered identity as authoritative rather than re-keying a device that already has registry entries; it adopts and persists the polled identity only for an entry that has none, and warns if a different appliance answers on the same IP. Entry version 1 -> 2 recovers the serial from the entry's unique_id (the flow has always keyed it on the probe's serial) and repairs what the old registration orphaned: IP-keyed devices and entities are rewritten in place where the serial-keyed key is free -- keeping entity_id, name, area and every automation referencing them -- and removed where both exist, since the IP-keyed one has been dead since the restart that made it. Placeholder-serial boards (issues #83/#189) were keyed two ways at once, `host:port` on the entry and `host` in the registry; migration collapses the entry onto the registry's form. One resolve_serial() now serves both sides, so they can't drift apart again. The remaining step in the desired pipeline -- probe for subdevices, then register devices, then populate entities -- already holds: _enumerate_subdevices_blocking runs before _run_discovery, which runs before platforms are forwarded. Duplicating it in the config flow would mean re-running Pattern B's per-href fallback probe, which is the opposite of what issue #211 is about. |
||
|
|
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. |
||
|
|
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.
|
||
|
|
1576d095c8 |
ci: run pytest in CI; complete dev requirements; unblock socket test
The suite was never run in CI (only hassfest/HACS), and requirements-dev listed only pytest + smartthings-local while conftest.py needs the full Home Assistant test harness. - requirements-dev.txt: add pytest-homeassistant-custom-component (pulls in home-assistant + pytest + pytest-socket) and the integration's runtime deps (cbor2, pyOpenSSL, cryptography) needed to import it. - .github/workflows/validate.yml: add a Pytest job on Python 3.14 (current Home Assistant's floor) that installs requirements-dev and runs the suite. - test_config_flow.py: the UDP liveness-sweep test needs real loopback sockets, which the HA harness blocks by default; take the pytest-socket socket_enabled fixture so it runs instead of erroring. Full suite: 249 passed. |
||
|
|
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. |
||
|
|
c6f08ea340 | test: add pytest infra and device-dump fixtures |