72 Commits
Author SHA1 Message Date
Quite Yellow b0d51abcc8 Merge pull request #38 from QuiteYellow/fix/reader-death-visible
fix(dtls): make reader-thread death visible and fail fast
v0.1.6
2026-08-14 17:38:41 +01:00
Jack Nagy 7a74a955f3 fix(dtls): make reader-thread death visible and fail fast
The reader loop exited silently on any socket error, leaving conn/sock
set so the session still looked open. Every later get()/post()/ping()
then waited out its full request timeout on a session nobody was
reading, raising SessionTimeoutError on repeat, forever.

This started biting in v0.1.3 (d677c72), which moved to connected UDP
sockets: a connected socket surfaces ICMP errors on recv, so one
ECONNREFUSED from a rebooting appliance now killed the reader.

- Advisory ICMP errnos (ECONNREFUSED/EHOSTUNREACH/...) no longer kill
  the reader; the next datagram usually works.
- Real reader exits log at WARNING; close()-driven exits stay quiet.
- A _reader_running Event lets get/post/ping/subscribe/refresh_observes
  fail fast via _check_live() with SessionClosedError instead of waiting
  out a timeout. Callers that never start a reader are unaffected.

Refs QuiteYellow/SmartThings-Local#37
2026-08-14 17:33:13 +01:00
Quite Yellow d4aebebca4 Merge pull request #32 from Moballo-LLC/codex/py-06-psk-auth
feat(protocol): add PSK authentication provider
v0.1.5
2026-08-09 14:21:54 +01:00
Quite Yellow 7f0f1e5531 Merge pull request #31 from Moballo-LLC/codex/py-05-certificate-auth
refactor(protocol): add certificate authentication provider
v0.1.4
2026-08-09 10:41:16 +01:00
Jason Morcos 2a6fc627f2 feat(protocol): add PSK authentication provider 2026-08-08 16:06:53 -07:00
Jason Morcos 8fb37ca2ed refactor(protocol): add certificate authentication provider 2026-08-08 14:29:30 -07:00
Jack Nagy 74d78543e1 chore: allow NOTICE in distribution check allowlist
The distribution checker enforces an exact-contents allowlist; add the
NOTICE file to the wheel dist-info/licenses expectation and the sdist
required set so the packaged trademark notice passes verification.
2026-08-07 16:50:26 +01:00
Quite Yellow da8f917541 Merge pull request #29 from QuiteYellow/docs/trademark-notice
docs: add trademark non-affiliation notice (README + NOTICE)
2026-08-07 16:47:27 +01:00
Jack Nagy a63bc0bb01 docs: add trademark non-affiliation notice (README + NOTICE)
Add a Trademarks & disclaimer section to the README and a root NOTICE
file stating this is an independent, unofficial project not affiliated
with Samsung, and that Samsung/SmartThings marks are used nominatively.

Ship NOTICE inside the distributed artifacts by adding it to
license-files (wheel .dist-info/licenses/) and the sdist include list.
2026-08-07 16:46:47 +01:00
Quite Yellow 11c87b275b Merge pull request #25 from Moballo-LLC/codex/py-04-bounded-probe
feat(protocol): add bounded DTLS endpoint probing
v0.1.3
2026-08-07 15:53:19 +01:00
Quite Yellow 7ec999b3ff Merge pull request #24 from Moballo-LLC/codex/py-03-connected-endpoints
feat(protocol): resolve and connect UDP endpoints
2026-08-07 15:37:36 +01:00
Quite Yellow 31a52d6ff6 Merge pull request #23 from Moballo-LLC/codex/py-02-typed-errors
feat(errors): add redacted typed transport failures
2026-08-07 15:29:25 +01:00
Jack Nagy 2a82a764bb chore: add Buy Me a Coffee funding link 2026-08-05 16:52:10 +01:00
Jason Morcos dd453ebdfb feat(protocol): bound DTLS endpoint probing 2026-08-03 12:45:14 -07:00
Jason Morcos d677c72f89 feat(protocol): add resolved connected UDP endpoints 2026-08-03 12:45:04 -07:00
Jason Morcos c7e15a7dd3 feat(errors): add redacted typed failures 2026-08-03 12:44:51 -07:00
Quite Yellow 280939d646 Merge pull request #27 from QuiteYellow/fix/sdist-check-hgignore-optional
fix(ci): treat sdist VCS-ignore files as optional in distribution check
2026-08-03 19:51:01 +01:00
Jack Nagy 98e0020e2f fix(ci): treat sdist VCS-ignore files as optional in distribution check
hatchling 1.31.0 ships only .gitignore in the sdist; older releases also
bundled .hgignore. check_sdist required an exact member set including
.hgignore, so the Validate workflow's package job failed on main once CI
resolved the newer hatchling.

Require the tracked source set plus the fixed metadata files, and accept
.gitignore/.hgignore as optional members either way.
2026-08-03 19:45:09 +01:00
Quite Yellow 597a88ff25 Merge pull request #21 from Moballo-LLC/codex/py-01-ci-foundation
ci: add validation and package smoke tests
2026-08-03 19:33:28 +01:00
Quite Yellow 93e39de079 Merge pull request #22 from Moballo-LLC/codex/libressl-sha1-retry
fix(setup-cert): keep SHA-1 retry compatible with LibreSSL
2026-08-03 19:33:25 +01:00
Jason Morcos fc6240b72e test(safety): allow public GitHub attachments 2026-08-02 16:31:15 -07:00
Jason Morcos 6dc9dca339 fix(setup-cert): keep SHA-1 retry compatible with LibreSSL 2026-08-02 10:43:40 -07:00
Jason Morcos 2c93cb3097 test: make worker cleanup checks deterministic 2026-08-02 10:13:56 -07:00
Jason Morcos 23338995bf build: require pyOpenSSL DTLS timeout support 2026-08-02 10:07:49 -07:00
Jason Morcos 119c114daa ci: add pull request validation 2026-08-02 10:02:52 -07:00
Jack Nagy e5bd9456d4 docs(readme): document SHA-1 crypto-policy handling + list dtls_probe
- Note the Fedora/RHEL SHA-1 crypto-policy block in Part 2 and how
  setup_cert.py auto-retries / the manual update-crypto-policies remedy.
- Add dtls_probe.py to the repo-layout tree (it was referenced in three
  places but missing from the file listing).
- Drop the .venv/ prefix from the Part 1 probe command so it runs against
  the pip-installed package before the Part 4 venv exists.
- Expand the tests parenthetical to name the probe, port-resolution, and
  cert-signing suites.
2026-08-01 12:05:57 +01:00
Quite Yellow a494e73e89 fix(setup_cert): surface openssl errors and work around SHA-1 crypto policy (#19)
* fix(setup_cert): surface openssl errors and work around SHA-1 crypto policy

The signing step forces -sha1 (the AC14K_M chain requires SHA-1-signed
leaves), which Fedora/RHEL's default crypto policy rejects on OpenSSL
3.x. run() also swallowed stderr, so the failure surfaced as an opaque
non-zero-exit traceback with no diagnostic.

- run() now raises CommandError carrying the command and openssl stderr
- mint_cert retries signing with a scoped OPENSSL_CONF enabling
  rh-allow-sha1-signatures when the first attempt fails
- main() prints the update-crypto-policies fallback on failure

Fixes #15

* test(setup_cert): cover SHA-1 signing, error surfacing, and crypto-policy retry

Regression tests for the #15 fix:
- full mint_cert flow (SHA-1 leaf, UUID SAN, custom OIDs, chain assembly)
- CommandError surfaces openssl stderr on a genuine signing failure
- signing retries via the SHA-1 override when the plain attempt is blocked
- run() raises CommandError with detail
2026-08-01 11:47:09 +01:00
Quite Yellow e0622eb087 ci(publish): bump actions off deprecated Node 20 runtimes (#18)
GitHub is deprecating the Node 20 action runtime; checkout@v4,
setup-python@v5, and upload/download-artifact@v4 all run on it and
were being auto-forced to Node 24 with a warning. Bump each to its
current major (checkout@v7, setup-python@v7, upload-artifact@v7,
download-artifact@v8), all of which run natively on Node 24.
2026-08-01 11:28:14 +01:00
Quite Yellow b7f2f20f29 Merge pull request #17 from QuiteYellow/feat/dtls-clienthello-probe
Add a DTLS ClientHello probe as the liveness + diagnostic primitive
v0.1.2
2026-08-01 11:17:24 +01:00
Jack Nagy 46041bfb2c docs(readme): anti-AI writing pass
Convert em-dash prose splices to varied punctuation (periods, colons,
semicolons, commas, parens), turn **Label.**-period bullets into
**Label:** colons, drop sentence-spanning bold in "Traps to avoid", and
cut a couple of hollow intensifiers.

No content, facts, tables, code, or links changed (50/50 line diff). Left
as-is: the `## Part N —` headings (heading-anchor stability), everything
inside code/log fences, table N/A cells, and numbered-list
`**Bold** — desc` carve-outs.
2026-08-01 11:10:58 +01:00
Jack Nagy 1a35cd59a1 feat(protocol): add DTLS ClientHello liveness probe + wire it into the bridge
A stateless-by-default DTLS ClientHello probe that classifies a host:port
as DEAD/LIVE/COMPLETED/REJECTED in ~1 RTT off the server's first flight,
sitting in front of the full handshake.

Probe (smartthings_local/protocol/dtls_probe.py):
- Stateless liveness mode (default): stops at HelloVerifyRequest and never
  sends the cookie'd second ClientHello, so by RFC 6347 §4.2.1 it leaves
  no association on the device — safe to run before a real connect.
- Diagnostic mode (stateless=False): drives the handshake further to
  capture cipher/cert-chain/CertificateRequest or a fatal Alert, for
  OCF-PKI-wall characterization (#16). Kept out of hot reconnect paths.
- Retransmit + retries: services OpenSSL's DTLS retransmit timer so a
  single dropped ClientHello no longer reads as a false DEAD.

MQTT bridge (mqtt_demo):
- Stateless pre-flight gate in session_once() rejects a silent/rebooting
  device or wrong port in ~3s (retries=1) instead of eating the 12s
  HANDSHAKE_TIMEOUT_S per reconnect.
- OCF-band port autodiscovery when OCF_PORT is unset: races the band in
  parallel and returns on the first port to answer LIVE (~1 RTT, abandoning
  the dead-port probes), cached across reconnects; the stateless gate
  leaves no orphan, preserving the fixed-source-port §4.2.8 invariant.

Validated on real hardware (dryer 49155 / oven 49154): parallel discovery
resolves both ports in <1s, connect with no orphan cooldown, and a wrong
pinned port rejected in ~3s.

Tests: probe behaviour (retransmit recovery, stateless single-flight
guard, silent-port flight budget, diagnostic continuation) and bridge
port-resolution (pinned gate, parallel discovery early-exit, cache).
2026-08-01 10:57:54 +01:00
Jack Nagyandvmvarga 8c2108a510 feat(protocol): fixed DTLS source port so reconnects evict orphaned sessions
Root-cause fix for the stale-session stall on always-on appliances (#14).
When the client dies without close_notify (crash, SIGKILL), the device
keeps an orphaned DTLS association keyed to the old 5-tuple; a reconnect
from a fresh ephemeral port presents as a brand-new peer, so the orphan
lingers until the device's own timer reaps it (observed 5-15 min).

RFC 6347 §4.2.8 covers exactly this: a ClientHello arriving on an existing
association's 5-tuple means the peer rebooted, and the server must complete
the new handshake and discard the old association. Add an optional
local_port to DtlsCoapSession that binds the UDP source port, and have the
bridge bind base+appliance-index, so every reconnect re-handshakes over the
same 5-tuple and the orphan is evicted instead of waited out.

Bench-verified on live hardware (2026-07-26): RT-OCF accepts the
same-5-tuple rehandshake (oven, dryer: handshake completes over a
crash-orphaned association, reads work immediately). The oven does not
reproduce the fridge stall even with 11 crash-orphaned OBSERVE
registrations, so fridge-side confirmation of the eviction is still needed.

Co-authored-by: vmvarga <garrysuchiy@gmail.com>
v0.1.1
2026-07-26 20:53:56 +01:00
Jack Nagy 6653de3b2c docs(readme): refine tested-combos after PR #13
- credit @indykoning + note localthings test path for the washer row
- soften DV90T mnid grouping (mnid=0AJT confirmed on DV5000T only)
- de-speculate the same-family note now that a washer is confirmed
2026-07-26 18:44:42 +01:00
Quite Yellow a2760eaa11 Merge pull request #13 from indykoning/patch-1
Added tested machines
2026-07-26 18:43:28 +01:00
indykoning 231d2b5f1a Added tested machines 2026-07-23 13:19:05 +02:00
Jack Nagy 072af1bfa1 docs(readme): reframe around the smartthings-local library
- Lead with the pip-installable library; frame the MQTT bridge as a
  reference demo. Add a library quick-start (install, DtlsCoapSession
  example, in-memory cert_pem/key_pem variant).
- Fix stale protocol/ + ocf/ references to smartthings_local/*; update
  the repo-layout tree (nested package, ocf_root_ca.pem, pyproject.toml,
  tests/, publish.yml); drop the non-existent auth.py.
- Correct the write-surface trap: reconciliation is a deferred poll, not
  a post-write fetch-back (which itself triggered Samsung's revert).
  Distinguish hardware-gated parity (power/child-lock/RC-enable) from
  the open oven remote-start problem.
- Note the few write surfaces the cloud HA integration doesn't expose
  (dryer course, oven setpoint). Drop the achieved collaborators-wanted
  callout.
2026-07-07 19:41:06 +01:00
Quite Yellow c46dda9766 Merge pull request #12 from mbillow/cert-pem-support
Support in-memory PEM cert/key alongside file paths in DtlsCoapSession
v0.1.0
2026-07-07 17:33:15 +01:00
Marc Billow a2dc524c0b feat: support in-memory PEM cert/key alongside file paths in DtlsCoapSession
localthings mints its client cert at runtime through the HA config flow
and never writes it to disk. DtlsCoapSession only accepted cert_path/
key_path (file-based), which would have forced localthings to write its
in-memory cert/key to disk on every connect just to migrate off its
vendored copy of this transport layer.

Adds an alternate cert_pem/key_pem constructor path (ported from
localthings' own _load_pem_chain), validated so exactly one cert source
(file pair or PEM pair) is required. Existing file-path callers
(mqtt_demo, setup_cert.py) are unaffected — verified against both real
appliances with each constructor path.
2026-07-06 15:04:20 -05:00
Jack Nagy 3fdc735141 feat(packaging): nest protocol/ + ocf/ under smartthings_local, add PyPI packaging
Nest the two library packages under a single import namespace so they
can ship as one distribution:

  protocol/ -> smartthings_local/protocol/
  ocf/      -> smartthings_local/ocf/   (git mv, history preserved)

- Rewrite all imports protocol.* -> smartthings_local.protocol.*,
  ocf.* -> smartthings_local.ocf.* across the ocf modules, mqtt_demo/
  (bridge, descriptor, samples), and tests.
- Add pyproject.toml: dist name `smartthings-local`, hatch-vcs versioning
  from v* tags, wheel ships only smartthings_local/.
- Add .github/workflows/publish.yml: build + PyPI Trusted Publishing on
  v* tags (OIDC, no stored token).
- Force-include protocol/ocf_root_ca.pem via [tool.hatch.build] artifacts:
  it is tracked but matches .gitignore's *.pem, so hatchling's VCS file
  selection would drop it — and dtls_session.py loads it at runtime.
- Update mqtt_demo Dockerfile COPY and deploy.sh tar allowlist to the
  single smartthings_local/ package.
- .gitignore: build artifacts (_version.py, dist/, *.egg-info/).

Validated: pytest tests/ (11 passed), python -m build produces sdist +
wheel with the pem bundled, fresh pip install resolves all nested imports
with the pem readable from site-packages.
2026-07-06 20:29:19 +01:00
Quite Yellow 9004bac729 Merge pull request #8 from mbillow/protocol-library-reorg
Reorg into protocol/ + ocf/ + mqtt_demo/, port 3 DTLS reliability fixes
2026-07-06 17:33:00 +01:00
Marc Billow ce6985ca3f fix: restore Block2 retry debug logging dropped during port
Task 8's port of retry/retransmit from localthings dropped the per-attempt
timeout/retry log lines (present in coap_dtls.py) — found while gathering
real-device pacing evidence, where the missing logs made retry frequency
impossible to see. Restores both the retry and final-timeout log lines.
2026-07-05 16:51:44 -05:00
Marc Billow ff688f5d5d fix: residual inter-request pacing instead of blind full-interval sleep
pace() slept the entire rate-limit interval every call regardless of how
much of it had already elapsed since the last real send (e.g. spent
processing the previous block's response). Track the last send timestamp
and only sleep the remainder, recovering time already spent between
requests without changing the enforced minimum interval.

Verified against 10.0.0.129/10.0.0.254: multi-block /device/0 fetches
still complete cleanly, faster per sweep, with the same request spacing
guarantee (patch and measurement from QuiteYellow, PR #8 review).
2026-07-05 16:51:17 -05:00
Marc Billow 27480dcb1d fix: pace inter-tier polling; fix stale package-rename references in docs 2026-07-04 17:29:22 -05:00
Marc Billow 3a2d3da3b2 docs: point HA users at localthings, freeze mqtt_demo/samples as reference-only 2026-07-04 17:29:22 -05:00
Marc Billow 70baa3baf0 fix: rate-limit inter-request pacing to avoid RT-OCF request drops 2026-07-04 17:29:22 -05:00
Marc Billow 58df1b242f fix: retry Block2 GETs on timeout, track server-negotiated block size
Ported from localthings fork: bounded per-block retransmission
(_BLOCK_MAX_ATTEMPTS/_BLOCK_ACK_TIMEOUT) instead of a single attempt
per block, SZX renegotiation tracking, and a bugfix moving
conn.send(datagram) inside the try that catches its own SSL errors.
2026-07-04 17:29:22 -05:00
Marc Billow 0fc65339b5 fix: validate device cert chain against OCF root CA instead of VERIFY_NONE 2026-07-04 17:29:22 -05:00
Marc Billow c7232b3df6 fix: isolate subprocess PYTHONPATH in import-isolation test to prevent false pass 2026-07-04 17:29:22 -05:00
Marc Billow 8a6bc8d594 test: verify protocol/ + ocf/ import in isolation from mqtt_demo/
- Add test_import_isolation.py: isolation test that copies protocol/ and ocf/
  to a temp directory without mqtt_demo/ and verifies all modules import
- Add conftest.py: pytest configuration to add repo root to sys.path,
  enabling tests to import protocol/ and ocf/ packages
2026-07-04 17:29:22 -05:00
Marc Billow 4789ac8f55 fix: include .dockerignore in deploy.sh's remote tar package to prevent .env leaking into the image 2026-07-04 16:22:31 -05:00