smartbox 2.5.1__tar.gz → 2.6.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (159) hide show
  1. {smartbox-2.5.1 → smartbox-2.6.2}/.github/FUNDING.yml +1 -2
  2. {smartbox-2.5.1 → smartbox-2.6.2}/.github/workflows/publish-to-pypi.yml +2 -2
  3. {smartbox-2.5.1 → smartbox-2.6.2}/.github/workflows/python-package.yml +7 -7
  4. {smartbox-2.5.1 → smartbox-2.6.2}/.gitignore +1 -1
  5. smartbox-2.6.2/.vscode/settings.json +12 -0
  6. smartbox-2.6.2/CHANGELOG.md +312 -0
  7. {smartbox-2.5.1 → smartbox-2.6.2}/CODEOWNERS +1 -1
  8. {smartbox-2.5.1 → smartbox-2.6.2}/PKG-INFO +27 -15
  9. {smartbox-2.5.1 → smartbox-2.6.2}/README.md +22 -8
  10. smartbox-2.6.2/ajtudela-pr-64-audit.md +497 -0
  11. smartbox-2.6.2/api-notes.md +603 -0
  12. {smartbox-2.5.1 → smartbox-2.6.2}/pyproject.toml +22 -18
  13. {smartbox-2.5.1 → smartbox-2.6.2}/src/smartbox/__init__.py +35 -1
  14. {smartbox-2.5.1 → smartbox-2.6.2}/src/smartbox/cmd.py +182 -31
  15. smartbox-2.6.2/src/smartbox/error.py +41 -0
  16. smartbox-2.6.2/src/smartbox/models.py +325 -0
  17. {smartbox-2.5.1 → smartbox-2.6.2}/src/smartbox/reseller.py +5 -11
  18. smartbox-2.6.2/src/smartbox/session.py +1869 -0
  19. smartbox-2.6.2/src/smartbox/socket.py +523 -0
  20. smartbox-2.6.2/src/smartbox/update_manager.py +764 -0
  21. {smartbox-2.5.1 → smartbox-2.6.2}/tests/common.py +11 -0
  22. {smartbox-2.5.1 → smartbox-2.6.2}/tests/conftest.py +9 -1
  23. smartbox-2.6.2/tests/fixtures/devs/device1/acm/1/samples.json +2839 -0
  24. smartbox-2.6.2/tests/fixtures/devs/device1/acm/1/setup.json +40 -0
  25. smartbox-2.6.2/tests/fixtures/devs/device1/acm/1/status.json +24 -0
  26. smartbox-2.6.2/tests/fixtures/devs/device1/acm/1/version.json +6 -0
  27. smartbox-2.6.2/tests/fixtures/devs/device1/htr/0/samples.json +2839 -0
  28. smartbox-2.6.2/tests/fixtures/devs/device1/htr/0/setup.json +40 -0
  29. smartbox-2.6.2/tests/fixtures/devs/device1/htr/0/status.json +24 -0
  30. smartbox-2.6.2/tests/fixtures/devs/device1/htr/0/version.json +6 -0
  31. smartbox-2.6.2/tests/fixtures/devs/device1/htr_system/power_limit.json +3 -0
  32. smartbox-2.6.2/tests/fixtures/devs/device1/mgr/away_status.json +5 -0
  33. smartbox-2.6.2/tests/fixtures/devs/device1/pmo/3/version.json +6 -0
  34. smartbox-2.6.2/tests/fixtures/devs/device2/htr_mod/0/samples.json +2839 -0
  35. smartbox-2.6.2/tests/fixtures/devs/device2/htr_mod/0/setup.json +40 -0
  36. smartbox-2.6.2/tests/fixtures/devs/device2/htr_mod/0/status.json +24 -0
  37. smartbox-2.6.2/tests/fixtures/devs/device2/htr_mod/0/version.json +6 -0
  38. smartbox-2.6.2/tests/fixtures/devs/device2/htr_mod/1/setup.json +40 -0
  39. smartbox-2.6.2/tests/fixtures/devs/device2/htr_mod/1/status.json +24 -0
  40. smartbox-2.6.2/tests/fixtures/devs/device2/htr_mod/1/version.json +6 -0
  41. smartbox-2.6.2/tests/fixtures/devs/device2/htr_mod/2/setup.json +40 -0
  42. smartbox-2.6.2/tests/fixtures/devs/device2/htr_mod/2/status.json +24 -0
  43. smartbox-2.6.2/tests/fixtures/devs/device2/htr_mod/2/version.json +6 -0
  44. smartbox-2.6.2/tests/fixtures/devs/device2/htr_mod/3/setup.json +40 -0
  45. smartbox-2.6.2/tests/fixtures/devs/device2/htr_mod/3/status.json +24 -0
  46. smartbox-2.6.2/tests/fixtures/devs/device2/htr_mod/3/version.json +6 -0
  47. smartbox-2.6.2/tests/fixtures/devs/device2/htr_mod/4/setup.json +40 -0
  48. smartbox-2.6.2/tests/fixtures/devs/device2/htr_mod/4/status.json +24 -0
  49. smartbox-2.6.2/tests/fixtures/devs/device2/htr_mod/4/version.json +6 -0
  50. smartbox-2.6.2/tests/fixtures/devs/device2/htr_mod/5/setup.json +40 -0
  51. smartbox-2.6.2/tests/fixtures/devs/device2/htr_mod/5/status.json +24 -0
  52. smartbox-2.6.2/tests/fixtures/devs/device2/htr_mod/5/version.json +6 -0
  53. smartbox-2.6.2/tests/fixtures/devs/device2/htr_system/power_limit.json +3 -0
  54. smartbox-2.6.2/tests/fixtures/devs/device2/mgr/away_status.json +5 -0
  55. smartbox-2.6.2/tests/fixtures/devs/device2/mgr/nodes.json +46 -0
  56. smartbox-2.6.2/tests/fixtures/devs.json +19 -0
  57. smartbox-2.6.2/tests/fixtures/grouped_devs.json +29 -0
  58. smartbox-2.6.2/tests/fixtures/live/htr_prog_addr2.json +187 -0
  59. smartbox-2.6.2/tests/fixtures/live/htr_prog_addr3.json +187 -0
  60. smartbox-2.6.2/tests/fixtures/live/htr_prog_addr4.json +187 -0
  61. smartbox-2.6.2/tests/fixtures/live/htr_prog_addr5.json +187 -0
  62. smartbox-2.6.2/tests/fixtures/live/htr_prog_addr6.json +187 -0
  63. smartbox-2.6.2/tests/fixtures/live/htr_setup_addr2.json +24 -0
  64. smartbox-2.6.2/tests/fixtures/live/htr_setup_addr3.json +24 -0
  65. smartbox-2.6.2/tests/fixtures/live/htr_setup_addr4.json +24 -0
  66. smartbox-2.6.2/tests/fixtures/live/htr_setup_addr5.json +24 -0
  67. smartbox-2.6.2/tests/fixtures/live/htr_setup_addr6.json +24 -0
  68. smartbox-2.6.2/tests/fixtures/live/htr_setup_no_boost.json +24 -0
  69. smartbox-2.6.2/tests/fixtures/live/htr_status_boost_off.json +25 -0
  70. smartbox-2.6.2/tests/fixtures/live/socket_update_status_frame.json +28 -0
  71. smartbox-2.6.2/tests/fixtures/live/socket_update_sync_lost_frame.json +6 -0
  72. smartbox-2.6.2/tests/fixtures/live/socket_update_version_frame.json +9 -0
  73. smartbox-2.6.2/tests/fixtures/test_path.json +3 -0
  74. {smartbox-2.5.1 → smartbox-2.6.2}/tests/test_cmd.py +137 -1
  75. smartbox-2.6.2/tests/test_live_payloads.py +133 -0
  76. {smartbox-2.5.1 → smartbox-2.6.2}/tests/test_models.py +51 -0
  77. {smartbox-2.5.1 → smartbox-2.6.2}/tests/test_reseller.py +14 -5
  78. {smartbox-2.5.1 → smartbox-2.6.2}/tests/test_session.py +1406 -264
  79. smartbox-2.6.2/tests/test_session_reliability.py +552 -0
  80. smartbox-2.6.2/tests/test_socket.py +501 -0
  81. smartbox-2.6.2/tests/test_update_manager.py +940 -0
  82. smartbox-2.6.2/tools/api_functional_tests.py +233 -0
  83. smartbox-2.6.2/tools/away_probe.py +109 -0
  84. smartbox-2.6.2/tools/capture_prog.py +84 -0
  85. smartbox-2.6.2/tools/live_check.py +137 -0
  86. smartbox-2.6.2/tools/probe_extra_options.py +145 -0
  87. smartbox-2.6.2/tools/probe_modified_auto.py +214 -0
  88. smartbox-2.6.2/tools/probe_settle_window.py +146 -0
  89. smartbox-2.6.2/tools/prog_functional_tests.py +143 -0
  90. smartbox-2.6.2/tools/watch_status.py +123 -0
  91. smartbox-2.6.2/uv.lock +1424 -0
  92. smartbox-2.6.2/webapi-spec.md +186 -0
  93. smartbox-2.5.1/.vscode/settings.json +0 -7
  94. smartbox-2.5.1/CHANGELOG.md +0 -39
  95. smartbox-2.5.1/api-notes.md +0 -141
  96. smartbox-2.5.1/src/smartbox/error.py +0 -19
  97. smartbox-2.5.1/src/smartbox/models.py +0 -273
  98. smartbox-2.5.1/src/smartbox/session.py +0 -664
  99. smartbox-2.5.1/src/smartbox/socket.py +0 -313
  100. smartbox-2.5.1/src/smartbox/update_manager.py +0 -298
  101. smartbox-2.5.1/tests/fixtures/devs/device1/acm/1/samples.json +0 -2839
  102. smartbox-2.5.1/tests/fixtures/devs/device1/acm/1/setup.json +0 -40
  103. smartbox-2.5.1/tests/fixtures/devs/device1/acm/1/status.json +0 -24
  104. smartbox-2.5.1/tests/fixtures/devs/device1/acm/1/version.json +0 -6
  105. smartbox-2.5.1/tests/fixtures/devs/device1/htr/0/samples.json +0 -2839
  106. smartbox-2.5.1/tests/fixtures/devs/device1/htr/0/setup.json +0 -40
  107. smartbox-2.5.1/tests/fixtures/devs/device1/htr/0/status.json +0 -24
  108. smartbox-2.5.1/tests/fixtures/devs/device1/htr/0/version.json +0 -6
  109. smartbox-2.5.1/tests/fixtures/devs/device1/htr_system/power_limit.json +0 -1
  110. smartbox-2.5.1/tests/fixtures/devs/device1/mgr/away_status.json +0 -1
  111. smartbox-2.5.1/tests/fixtures/devs/device1/pmo/3/version.json +0 -6
  112. smartbox-2.5.1/tests/fixtures/devs/device2/htr_mod/0/samples.json +0 -2839
  113. smartbox-2.5.1/tests/fixtures/devs/device2/htr_mod/0/setup.json +0 -40
  114. smartbox-2.5.1/tests/fixtures/devs/device2/htr_mod/0/status.json +0 -24
  115. smartbox-2.5.1/tests/fixtures/devs/device2/htr_mod/0/version.json +0 -6
  116. smartbox-2.5.1/tests/fixtures/devs/device2/htr_mod/1/setup.json +0 -40
  117. smartbox-2.5.1/tests/fixtures/devs/device2/htr_mod/1/status.json +0 -24
  118. smartbox-2.5.1/tests/fixtures/devs/device2/htr_mod/1/version.json +0 -6
  119. smartbox-2.5.1/tests/fixtures/devs/device2/htr_mod/2/setup.json +0 -40
  120. smartbox-2.5.1/tests/fixtures/devs/device2/htr_mod/2/status.json +0 -24
  121. smartbox-2.5.1/tests/fixtures/devs/device2/htr_mod/2/version.json +0 -6
  122. smartbox-2.5.1/tests/fixtures/devs/device2/htr_mod/3/setup.json +0 -40
  123. smartbox-2.5.1/tests/fixtures/devs/device2/htr_mod/3/status.json +0 -24
  124. smartbox-2.5.1/tests/fixtures/devs/device2/htr_mod/3/version.json +0 -6
  125. smartbox-2.5.1/tests/fixtures/devs/device2/htr_mod/4/setup.json +0 -40
  126. smartbox-2.5.1/tests/fixtures/devs/device2/htr_mod/4/status.json +0 -24
  127. smartbox-2.5.1/tests/fixtures/devs/device2/htr_mod/4/version.json +0 -6
  128. smartbox-2.5.1/tests/fixtures/devs/device2/htr_mod/5/setup.json +0 -40
  129. smartbox-2.5.1/tests/fixtures/devs/device2/htr_mod/5/status.json +0 -24
  130. smartbox-2.5.1/tests/fixtures/devs/device2/htr_mod/5/version.json +0 -6
  131. smartbox-2.5.1/tests/fixtures/devs/device2/htr_system/power_limit.json +0 -1
  132. smartbox-2.5.1/tests/fixtures/devs/device2/mgr/away_status.json +0 -1
  133. smartbox-2.5.1/tests/fixtures/devs/device2/mgr/nodes.json +0 -46
  134. smartbox-2.5.1/tests/fixtures/devs.json +0 -19
  135. smartbox-2.5.1/tests/fixtures/grouped_devs.json +0 -24
  136. smartbox-2.5.1/tests/fixtures/test_path.json +0 -1
  137. smartbox-2.5.1/tests/test_update_manager.py +0 -283
  138. smartbox-2.5.1/uv.lock +0 -1144
  139. {smartbox-2.5.1 → smartbox-2.6.2}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  140. {smartbox-2.5.1 → smartbox-2.6.2}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  141. {smartbox-2.5.1 → smartbox-2.6.2}/.github/ISSUE_TEMPLATE/new-reseller.md +0 -0
  142. {smartbox-2.5.1 → smartbox-2.6.2}/.github/dependabot.yml +0 -0
  143. {smartbox-2.5.1 → smartbox-2.6.2}/.pre-commit-config.yaml +0 -0
  144. {smartbox-2.5.1 → smartbox-2.6.2}/LICENSE +0 -0
  145. {smartbox-2.5.1 → smartbox-2.6.2}/codecov.yml +0 -0
  146. {smartbox-2.5.1 → smartbox-2.6.2}/src/smartbox/py.typed +0 -0
  147. {smartbox-2.5.1 → smartbox-2.6.2}/tests/__init__.py +0 -0
  148. {smartbox-2.5.1 → smartbox-2.6.2}/tests/fixtures/devs/device1/connected.json +0 -0
  149. {smartbox-2.5.1 → smartbox-2.6.2}/tests/fixtures/devs/device1/mgr/nodes.json +0 -0
  150. {smartbox-2.5.1 → smartbox-2.6.2}/tests/fixtures/devs/device1/pmo/3/samples.json +0 -0
  151. {smartbox-2.5.1 → smartbox-2.6.2}/tests/fixtures/devs/device1/pmo/3/setup.json +0 -0
  152. {smartbox-2.5.1 → smartbox-2.6.2}/tests/fixtures/devs/device1/pmo/3/status.json +0 -0
  153. {smartbox-2.5.1 → smartbox-2.6.2}/tests/fixtures/devs/device2/connected.json +0 -0
  154. {smartbox-2.5.1 → smartbox-2.6.2}/tests/fixtures/devs/device2/htr_mod/1/samples.json +0 -0
  155. {smartbox-2.5.1 → smartbox-2.6.2}/tests/fixtures/devs/device2/htr_mod/2/samples.json +0 -0
  156. {smartbox-2.5.1 → smartbox-2.6.2}/tests/fixtures/devs/device2/htr_mod/3/samples.json +0 -0
  157. {smartbox-2.5.1 → smartbox-2.6.2}/tests/fixtures/devs/device2/htr_mod/4/samples.json +0 -0
  158. {smartbox-2.5.1 → smartbox-2.6.2}/tests/fixtures/devs/device2/htr_mod/5/samples.json +0 -0
  159. {smartbox-2.5.1 → smartbox-2.6.2}/tests/fixtures/groups/test_home/guest_users.json +0 -0
@@ -1,4 +1,3 @@
1
1
  # These are supported funding model platforms
2
2
 
3
- buy_me_a_coffee: Delmael
4
-
3
+ buy_me_a_coffee: Delmael
@@ -14,7 +14,7 @@ jobs:
14
14
  permissions:
15
15
  id-token: write
16
16
  steps:
17
- - uses: actions/checkout@v6
17
+ - uses: actions/checkout@v7
18
18
  with:
19
19
  fetch-depth: 0
20
20
  - name: Install uv
@@ -22,7 +22,7 @@ jobs:
22
22
  with:
23
23
  python-version: ${{ env.PYTHON_VERSION }}
24
24
  - name: Set up Python ${{ env.PYTHON_VERSION }}
25
- uses: actions/setup-python@v6.2.0
25
+ uses: actions/setup-python@v7.0.0
26
26
  with:
27
27
  python-version: ${{ env.PYTHON_VERSION }}
28
28
  - name: Build a binary wheel and a source tarball
@@ -18,13 +18,13 @@ jobs:
18
18
  matrix:
19
19
  python-version: [3.14]
20
20
  steps:
21
- - uses: actions/checkout@v6
21
+ - uses: actions/checkout@v7
22
22
  - name: Install uv
23
23
  uses: astral-sh/setup-uv@v7
24
24
  with:
25
25
  python-version: ${{ matrix.python-version }}
26
26
  - name: Set up Python ${{ matrix.python-version }}
27
- uses: actions/setup-python@v6.2.0
27
+ uses: actions/setup-python@v7.0.0
28
28
  with:
29
29
  python-version: ${{ matrix.python-version }}
30
30
  - name: Install the project
@@ -40,13 +40,13 @@ jobs:
40
40
  python-version: [3.14]
41
41
 
42
42
  steps:
43
- - uses: actions/checkout@v6
43
+ - uses: actions/checkout@v7
44
44
  - name: Install uv
45
45
  uses: astral-sh/setup-uv@v7
46
46
  with:
47
47
  python-version: ${{ matrix.python-version }}
48
48
  - name: Set up Python ${{ matrix.python-version }}
49
- uses: actions/setup-python@v6.2.0
49
+ uses: actions/setup-python@v7.0.0
50
50
  with:
51
51
  python-version: ${{ matrix.python-version }}
52
52
  - name: Install the project
@@ -62,14 +62,14 @@ jobs:
62
62
  python-version: [3.14]
63
63
 
64
64
  steps:
65
- - uses: actions/checkout@v6
65
+ - uses: actions/checkout@v7
66
66
  - name: Install uv
67
67
  uses: astral-sh/setup-uv@v7
68
68
  with:
69
69
  python-version: ${{ matrix.python-version }}
70
70
 
71
71
  - name: Set up Python ${{ matrix.python-version }}
72
- uses: actions/setup-python@v6.2.0
72
+ uses: actions/setup-python@v7.0.0
73
73
  with:
74
74
  python-version: ${{ matrix.python-version }}
75
75
  - name: Install the project
@@ -77,7 +77,7 @@ jobs:
77
77
  uv sync --all-extras --dev
78
78
  uv run tox -e py
79
79
  - name: "Upload coverage to Codecov"
80
- uses: codecov/codecov-action@v6.0.0
80
+ uses: codecov/codecov-action@v7.1.1
81
81
  with:
82
82
  token: ${{ secrets.CODECOV_TOKEN }}
83
83
  fail_ci_if_error: true
@@ -72,4 +72,4 @@ venv.bak/
72
72
  junit.xml
73
73
  .DS_Store
74
74
 
75
- *.code-workspace
75
+ *.code-workspace
@@ -0,0 +1,12 @@
1
+ {
2
+ "[python]": {
3
+ "editor.codeActionsOnSave": {
4
+ "source.fixAll.ruff": "explicit",
5
+ "source.organizeImports.ruff": "explicit"
6
+ },
7
+ "editor.defaultFormatter": "charliermarsh.ruff",
8
+ "editor.formatOnSave": true
9
+ },
10
+ "python.testing.pytestEnabled": true,
11
+ "python.testing.unittestEnabled": false
12
+ }
@@ -0,0 +1,312 @@
1
+ # Changelog
2
+
3
+ ## 2.6.2
4
+
5
+ Port round of upstream PR #64 (ajtudela, `improvements/fix_bugs`, 19
6
+ commits) — full per-commit audit, verdicts and evidence in
7
+ ajtudela-pr-64-audit.md. Ten commits were already fixed by the 2.6.x
8
+ round (independent convergence); of the remainder, seven were ported
9
+ (each commit references the upstream source hash), one was declined by
10
+ decision (#7: typed-mode sparse statuses keep the rich
11
+ `SmartboxValidationError` rejection — the bare `{"sync_status": "lost"}`
12
+ dead-node frame is first-class there), and one is upstream-only (#18,
13
+ version bump).
14
+
15
+ ### Bug Fixes
16
+ * `set_node_setup` never serializes the read-modify-write GET through
17
+ the Pydantic models: the internal read is always raw, so keys the
18
+ setup models do not declare (e.g. `counter_offset` on pmo nodes) can
19
+ no longer be wiped on the device by a setup write (upstream `5618aa3`).
20
+ Completed by `extra="allow"` on `DefaultNodeSetup`/`PmoSetup`, so
21
+ typed (`raw_response=False`) reads keep unknown keys and round-trip
22
+ losslessly (upstream `5618aa3`, completion by decision).
23
+ * `InvalidAuthError`, `APIUnavailableError` and `ResellerNotExistError`
24
+ now derive from `SmartboxError` — one `except SmartboxError` catches
25
+ every library failure. `APIUnavailableError` keeps its
26
+ `aiohttp.ClientConnectionError` base for backwards compatibility; the
27
+ data-path 403 → `SmartboxError` mapping is kept deliberately
28
+ (upstream `62efcf8`).
29
+ * CLI: unknown `-d`/`-n` values raise `click.BadParameter` with a
30
+ `param_hint` (exit code 2, usage line, offending option named)
31
+ instead of `ClickException`; malformed wire node payloads stay
32
+ `ClickException` (upstream `6ad2014`).
33
+
34
+ ### Build & Docs
35
+ * Core dependencies bounded to their current major: `aiohttp>=3,<4`,
36
+ `pydantic>=2,<3`, `python-socketio>=5,<6` (upstream `da3bb9c`; the
37
+ commit's unused-dependency removal was already done in 2.6.0).
38
+ * `.vscode/settings.json`: pylint/flake8 editor settings replaced with
39
+ the ruff formatter/code-actions setup (upstream `a78bc3a`; its
40
+ pyproject/ruff halves were already done in 2.6.0).
41
+ * CODEOWNERS co-owner and README badge reflow adopted from upstream
42
+ `2ec1ef0`.
43
+
44
+ ### Tests
45
+ * `test_all_resellers` (real HTTP calls to twelve reseller servers)
46
+ moved behind a `network` marker, deselected by default; run on demand
47
+ with `pytest -m network`. Sessions in it are closed (upstream
48
+ `0bef64c`).
49
+
50
+ ## 2.6.1
51
+
52
+ ### Features
53
+ * Node reachability / write-confirmation tracking on `UpdateManager`:
54
+ `subscribe_to_node_availability` / `get_node_availability` report a
55
+ node Available/Unavailable from its status/setup frames — a bare
56
+ `{"sync_status": "lost"}` frame arms a uniform 6 s grace timer
57
+ (`unavailable_delay` knob, matches the vendor app's unreachable
58
+ window); any ok frame reports Available again. `expect_write_confirmation`
59
+ (knob `write_confirm_timeout`, 6 s) arms a window after an accepted
60
+ write: if no ok frame arrives, a one-shot discriminating GET decides
61
+ "applied (node alive)" vs "silently dropped (node unreachable)" —
62
+ writes to unreachable nodes are ACKed OK with no device ack (observed
63
+ live). Frame taps register lazily; both features share them, each
64
+ enables them independently. Backed by live probes 2026-09-26/30
65
+ (api-notes.md, "Node reachability").
66
+
67
+ ## 2.6.0
68
+
69
+ ### Features
70
+ * Full node programme (schedule) support: `get_node_prog` / `set_node_prog`
71
+ (+ typed `NodeProg` model, sync `Session.get_prog` / `set_prog` wrappers,
72
+ `subscribe_to_node_prog` websocket helper and `prog` / `set-prog` CLI
73
+ commands). Wire note: GET and POST both use the day-keyed object shape;
74
+ the array shape is rejected with 400 (observed live).
75
+ * Full management/fleet surface per webapi-spec §2–3: device discovery
76
+ GET/POST, device rtc/time, device/node/group rename, device/node delete,
77
+ device group move, node select/power, group geo_data/extra_data, invite
78
+ flows (invite/revoke/confirm — confirm is unauthenticated), quiet-home
79
+ notification config GET/POST + test, encrypted-wifi-credentials and
80
+ geocoding helpers. Read-only-verified paths pinned from the vendor app
81
+ bundle; all writes marked **unverified on live hardware** in docstrings.
82
+ New base helpers: `_api_delete`, host-relative `_api_get` (params not
83
+ logged — may carry secrets) and `_api_post_path` (auth optional).
84
+ * Away-status writes default `enabled: true` to match the vendor app shape
85
+ (`{away, enabled}`); the server also honours `{away}` alone (D1 verified
86
+ live).
87
+ * Setup models widened for the fw-1.9 htr family: optional
88
+ `user_duty_factor` / `flash_version` / `extra_options`; new
89
+ `max_stemp_limit` / `priority` / `revision` and
90
+ `factory_options.operating_mode` / `power_factor` / `super_lock_available`.
91
+ Its `factory_options` carries no `prog_resolution` (schedule resolution
92
+ not readable from setup on this family).
93
+
94
+ ### Bug Fixes
95
+ * `SocketSession.cancel()` is now bounded by a timeout (no more unbounded
96
+ hangs on a wedged websocket) and disconnects exactly once (cancel +
97
+ shutdown no longer double-disconnect).
98
+ * The websocket loop checks the exit flag before touching the REST API, so
99
+ cancel is no longer delayed by a slow auth refresh.
100
+ * Dangling background cleanup tasks are awaited (bounded) on shutdown
101
+ instead of dying as "Task was destroyed but it is pending" noise.
102
+ * Routine retry log downgraded from `_LOGGER.exception` to `_LOGGER.warning`.
103
+ * Status models widened for the newer htr firmware family (`error_code`
104
+ str|int; optional `act_duty` / `power_pcb_temp`); optional fields
105
+ `easy` / `runback` / `version` tolerated.
106
+
107
+ * Code-review round 3 (2026-09-27): request-timeouts and HTTP 5xx now map
108
+ to `APIUnavailableError` (aiohttp's total-timeout raises a plain
109
+ `TimeoutError` that previously escaped all error handling; 5xx GETs are
110
+ retried via the existing retry wrapper — 4xx still raises
111
+ `SmartboxError`); request bodies are redacted in debug logs (the
112
+ invite-confirmation `pass` leaked in plaintext); subscription user
113
+ callbacks are isolated — one raising callback can no longer starve the
114
+ other subscriptions or propagate into the socket loop, and jq-emitted
115
+ `null` node payloads no longer fire callbacks; the socket reconnect
116
+ backoff is capped at 30s per attempt (previously up to ~8.5 min of
117
+ futile retries before the token refresh that usually fixes the
118
+ disconnect); `_attempt_connection` also tolerates transient
119
+ `TimeoutError`/`OSError` from the websocket transport instead of
120
+ killing the run loop; the ping task is cancelled **and** reaped
121
+ (bounded) on cancel/shutdown; `get_device_power_limit` raises
122
+ `SmartboxError` instead of `KeyError` on an unexpected payload;
123
+ sync `Session` annotations aligned with the async surface (`set_setup`
124
+ → `None`, `get_status`/`get_setup`/`get_prog` may return models);
125
+ CLI: non-numeric node addresses give a clean error, `resellers` prints
126
+ a readable list without credentials, and `-u`/`-p` accept the
127
+ `SMARTBOX_USERNAME`/`SMARTBOX_PASSWORD` env vars; `__version__` no
128
+ longer explodes when imported uninstalled.
129
+
130
+ * Code-review round 4 (2026-09-27): sync `Session` no longer fails on the
131
+ second consecutive call with "RuntimeError: Session is closed" (the
132
+ per-call cleanup now forgets the closed client session so the next
133
+ `asyncio.run` recreates it — regression test drives the real client
134
+ through a local HTTP server); sync `get_device_power_limit` /
135
+ `set_device_power_limit` regain the async `node` parameter (pmo devices
136
+ were unreachable from the sync API) and sync wrappers were added for
137
+ `health_check`, `api_version`, `get_device_connected`,
138
+ `get_node_samples` and `get_node_version`; CLI `-a/--api-name` defaults
139
+ to `api` and session/API failures (auth, network, unknown reseller)
140
+ surface as clean `ClickException` errors instead of tracebacks;
141
+ `set_node_prog` rejects array-shape / `prog`-less payloads with a clear
142
+ error instead of an `AttributeError` traceback or a silent no-op POST
143
+ (CLI guard included); a token endpoint 200 with a non-JSON body raises
144
+ `SmartboxError` instead of `InvalidAuthError` (it must not trigger the
145
+ consumer's reauth flow); `SocketSession.run` authenticates before the
146
+ first connect attempt (a cold CLI session no longer burns its
147
+ reconnect budget on an empty token); the CLI SIGINT handler is removed
148
+ at teardown and platforms without signal-handler support no longer die
149
+ on connect; package exports completed (`NodeVersion` and the remaining
150
+ public model types).
151
+
152
+ * Code-review round 5 (2026-09-27) — **contract changes**:
153
+ * `raw_response=True` no longer validates responses: `get_devices`,
154
+ `get_homes`, `get_grouped_devices`, `get_home_guests`,
155
+ `get_device_connected` and `get_device_away_status` return the wire
156
+ payload (with only the documented list extraction, e.g. `devs` +
157
+ `invited_to` flattened) instead of model-serialised dicts. Unknown
158
+ keys are kept (live: `grouped_devs` carries `extraData`, previously
159
+ dropped); a payload too malformed to extract from raises
160
+ `SmartboxError`. All raw getters now follow the same rule.
161
+ * New `SmartboxValidationError(SmartboxError)`, exported: with
162
+ `raw_response=False`, payload drift raises it (chained from the
163
+ pydantic error) instead of a bare `pydantic.ValidationError`.
164
+ * Every library call is bounded by a 30s whole-call deadline covering
165
+ token refresh, retries and backoff — also on caller-provided
166
+ websessions (previously aiohttp's 300s default applied there, and 5
167
+ retries could stretch one call to minutes). Expiry raises
168
+ `APIUnavailableError`; a backoff that would overrun it is skipped.
169
+ * A rejected refresh token falls back to one password login before
170
+ `InvalidAuthError` is raised; a mid-session 401 invalidates the
171
+ token and retries the request once after re-authenticating.
172
+ * Code-review round 5 — reliability fixes:
173
+ * The websocket loop survives transient auth-refresh failures
174
+ (`APIUnavailableError`/`SmartboxError` are waited out with capped
175
+ backoff; previously one network blip ended `run()` for good); only
176
+ rejected credentials end it.
177
+ * `cancel()` during reconnect backoff stops the reconnect loop
178
+ immediately (it used to keep attempting), and a connect completing
179
+ after `cancel()` is disconnected instead of leaking a live socket
180
+ that kept `run()` alive.
181
+ * The keepalive ping task survives a failed send (it died for the
182
+ rest of the session on one `BadNamespaceError`).
183
+ * Secrets: the INFO "Successfully connected" log no longer prints the
184
+ access token; error messages and logs no longer embed request query
185
+ strings (aiohttp's exception text leaked the wifi password of
186
+ `get_encrypted_wifi_credentials`).
187
+ * Truncated response bodies (`ClientPayloadError`) map to
188
+ `APIUnavailableError` (retried on GETs) and undecodable JSON to
189
+ `SmartboxError` — both previously escaped the library's error types.
190
+ * `retry_attempts=0` makes one attempt instead of raising `TypeError`.
191
+ * Update subscriptions anchor their paths (`/prog` no longer matches
192
+ `/prog_temps` frames, likewise status/setup/version).
193
+ * Hygiene: 45 unneeded `type: ignore`s removed, sync
194
+ `Session.set_device_away_status` annotated `-> None`, dead
195
+ `_basic_auth_credentials` attribute removed, `socket.py` imports
196
+ `urllib.parse` explicitly.
197
+
198
+ * Code-review round 6 (2026-09-27, every finding reproduced by a probe
199
+ before fixing): the token endpoint (`/client/token`) maps truncated
200
+ bodies (`ClientPayloadError`) and undecodable-JSON 200s to
201
+ `APIUnavailableError` / `SmartboxError` like the REST path — a
202
+ truncated token body previously escaped raw and **ended the websocket
203
+ loop for good** (end-to-end reproduced); `SocketSession` rejects
204
+ `ping_interval <= 0` (hot ping loop) and `reconnect_attempts < 1`
205
+ (endless refresh churn) with a `ValueError` at construction; the CLI
206
+ `set-status` command gives a clean error for `--stemp` without
207
+ `--units` instead of a raw `ValueError` traceback; the token endpoint
208
+ treats HTTP 429 as transient (`APIUnavailableError`) instead of bad
209
+ credentials — rate limiting no longer triggers the password-login
210
+ fallback, the consumer's reauth flow, or the end of the websocket
211
+ loop (400/401 rejections still surface as `InvalidAuthError`).
212
+
213
+ * Code-review round 6 follow-up (2026-09-27, **breaking** — 2.6.0 is the
214
+ alignment window; every fix probe-verified): `set_node_prog` raises
215
+ `ValueError` for a `prog`-less / array-shaped payload (was `TypeError`
216
+ — now aligned with `set_node_status`'s input validation); the sync
217
+ `Session` wrapper return annotations reflect `raw_response=False`
218
+ (e.g. `get_devices` may return a `Devices` model, `get_nodes` a
219
+ `list[Node]`); the sync `Session` no longer leaks "coroutine was never
220
+ awaited" `RuntimeWarning`s when misused inside a running event loop
221
+ (the pre-built coroutines are closed before re-raising); socket
222
+ teardown reaps cancelled disconnect tasks and cancelled pending
223
+ background tasks (bounded) — no more "Task was destroyed but it is
224
+ pending" noise in edge teardown timing.
225
+
226
+ * Dependency refresh (2026-09-27, all gates + live socket round green):
227
+ runtime aiohttp 3.14.0→3.14.3, pydantic 2.13.4→2.13.5,
228
+ python-socketio 5.16.2→5.17.0 (+engineio 4.14.0, bidict), asyncclick
229
+ 8.3.0.7→8.4.2.1, jq 1.11.0→1.12.0; dev pytest 9.1.1, ruff 0.16.9,
230
+ mypy 2.3.1, time-machine 3.5.1, tox 4.64.3, pytest-randomly 5.0.0.
231
+ * Removed dead dependencies `requests` and `websocket-client` (imported
232
+ nowhere; legacy from the pre-asyncio era — the websocket path uses
233
+ python-socketio/simple-websocket, HTTP uses aiohttp).
234
+ * Lint config: ruff `target-version` aligned to the runtime floor
235
+ (py313 → py314); disabled CPY001 (no copyright headers in this
236
+ project) and PLR0917 (positional variant of the already-ignored
237
+ PLR0913); dropped dead mypy override modules (`freezegun`, `test`);
238
+ typing-only imports moved behind `TYPE_CHECKING` (ruff TC rules).
239
+
240
+ * Code-review round (2026-09-27, all gates + contract tests + live smoke
241
+ green): caller-provided websessions are no longer closed on context
242
+ exit (session-ownership model, `aclose_owned_session()`); REST requests
243
+ get bounded timeouts (30s total / 10s connect) on library-created
244
+ sessions; GETs retry on transient connection failures (uses the
245
+ previously dead `retry_attempts`/`backoff_factor` constructor config);
246
+ mid-session HTTP 401 now raises `InvalidAuthError` (consumers can
247
+ trigger reauth); access token is masked in debug logs; sync `Session`
248
+ closes its lazily-created client session per call (no more "Unclosed
249
+ client session" warnings); CLI: clean `ClickException` errors for
250
+ unknown device/node ids and invalid prog JSON, and fixed a latent bug
251
+ where set-* commands could never resolve a node (string vs int address
252
+ comparison); sample-window defaults are computed at call time (they
253
+ were frozen at import and drifted in long-lived processes); docstring
254
+ fixes (`Sample`/`PmoSample` were swapped, `subscribe_to_device_connected`
255
+ claimed to be a power-limit hook); removed redundant field
256
+ redeclarations in status subclasses and 1-tuple call hacks in the
257
+ update manager; dropped dead `[tool.uv.workspace]` and
258
+ `[tool.setuptools.*]` config.
259
+
260
+ * Code-review round 7 — final pre-release pass (2026-09-27, gates +
261
+ contract tests green): `SocketSession` can now be constructed outside
262
+ a running event loop (the REST websession binding moved from
263
+ `__init__` to `run()`, with `external_http=True` so engineio never
264
+ closes the session shared with REST traffic); the websocket run loop
265
+ no longer dies when the first `dev_data` emit races the server drop
266
+ (`BadNamespaceError` is a *sibling* of `ConnectionError` under
267
+ `SocketIOError` — the catch family was widened to `SocketIOError`);
268
+ a token-endpoint 200 with a wrong-shaped body raises `SmartboxError`
269
+ instead of `InvalidAuthError` (consistent with the undecodable-JSON /
270
+ non-JSON mapping: a server-side drift is not "credentials rejected");
271
+ `AsyncSession` rejects a negative `backoff_factor` at construction
272
+ (it previously silently disabled backoff); CLI node resolution gives
273
+ a clean error on malformed wire payloads (missing/non-numeric `addr`)
274
+ instead of a raw `KeyError`/`ValueError` traceback.
275
+
276
+ ## 0.0.5 (alpha)
277
+
278
+ ### Features
279
+ * Rename `away_status` to `device_away_status`
280
+ * Update API docs
281
+ * Add tox, tox and flake8 on github action
282
+
283
+ ### Bug Fixes
284
+ * Pin dependency of python-socketio to match server
285
+
286
+ ## 0.0.4 (alpha)
287
+
288
+ ### Features
289
+ * Refactor socket session and implement reconnect
290
+ * Add note on basic auth credentials
291
+
292
+ ## 0.0.3 (alpha)
293
+
294
+ ### Features
295
+ * Fixed packaging
296
+
297
+ ### Bug Fixes
298
+ * Fixed disconnect handling on token refresh
299
+
300
+ ## 0.0.2 (alpha)
301
+
302
+ ### Features
303
+ * Added `get_api_name` function
304
+ * Added basic tests for REST interactions
305
+ * Added token refresh support
306
+ * Added socket.io interface via `open_socket` function (no tests as yet)
307
+ * Added documentation for known REST and websocket endpoints
308
+
309
+ ## 0.0.1 (alpha)
310
+
311
+ ### Features
312
+ * Initial version supporting some REST endpoints
@@ -2,4 +2,4 @@
2
2
  # the repo. Unless a later match takes precedence,
3
3
  # @Delmael will be requested for
4
4
  # review when someone opens a pull request.
5
- * @Delmael
5
+ * @Delmael @ajtudela
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: smartbox
3
- Version: 2.5.1
3
+ Version: 2.6.2
4
4
  Summary: Python API to control heating 'smart boxes'
5
5
  Project-URL: Homepage, https://github.com/ajtudela/smartbox
6
6
  Project-URL: Issues, https://github.com/ajtudela/smartbox/issues
@@ -11,22 +11,22 @@ Classifier: License :: OSI Approved :: MIT License
11
11
  Classifier: Operating System :: OS Independent
12
12
  Classifier: Programming Language :: Python :: 3
13
13
  Requires-Python: >=3.14.2
14
- Requires-Dist: aiohttp
14
+ Requires-Dist: aiohttp<4,>=3
15
15
  Requires-Dist: asyncclick
16
16
  Requires-Dist: jq
17
- Requires-Dist: pydantic==2.13.2
18
- Requires-Dist: python-socketio
19
- Requires-Dist: requests
20
- Requires-Dist: websocket-client
17
+ Requires-Dist: pydantic<3,>=2
18
+ Requires-Dist: python-socketio<6,>=5
21
19
  Description-Content-Type: text/markdown
22
20
 
23
- # smartbox ![build](https://github.com/ajtudela/smartbox/workflows/Python%20package/badge.svg) [![PyPI version](https://badge.fury.io/py/smartbox.svg)](https://badge.fury.io/py/smartbox) [![codecov](https://codecov.io/gh/ajtudela/smartbox/branch/main/graph/badge.svg?token=ghNZOGVzVv)](https://codecov.io/gh/ajtudela/smartbox) [![PyPI license](https://img.shields.io/pypi/l/smartbox.svg)](https://pypi.python.org/pypi/smartbox/) [![PyPI pyversions](https://img.shields.io/pypi/pyversions/smartbox.svg)](https://pypi.python.org/pypi/smartbox/)
21
+ # smartbox
22
+ ![build](https://github.com/ajtudela/smartbox/workflows/Python%20package/badge.svg)
23
+ [![PyPI version](https://badge.fury.io/py/smartbox.svg)](https://badge.fury.io/py/smartbox)
24
+ [![codecov](https://codecov.io/gh/ajtudela/smartbox/branch/main/graph/badge.svg?token=ghNZOGVzVv)](https://codecov.io/gh/ajtudela/smartbox)
25
+ [![PyPI license](https://img.shields.io/pypi/l/smartbox.svg)](https://pypi.python.org/pypi/smartbox/)
26
+ [![PyPI pyversions](https://img.shields.io/pypi/pyversions/smartbox.svg)](https://pypi.python.org/pypi/smartbox/)
24
27
 
25
28
  Python API to control heating 'smart boxes'
26
29
 
27
-
28
- ## Installation
29
-
30
30
  ## Install
31
31
 
32
32
  To install smartbox simply run:
@@ -55,9 +55,9 @@ Verbose logging can be enabled with the `-v`/`--verbose` flag.
55
55
  ### Optional options
56
56
  These options are useful if your reseller is not configured.
57
57
 
58
- * `-b`/`--base-auth-creds`: An HTTP Basic Auth credential used to do initial
58
+ * `-b`/`--basic-auth-creds`: An HTTP Basic Auth credential used to do initial
59
59
  authentication with the server. Use the base64 encoded string directly. See
60
- 'Basic Auth Credential' section below for more details.
60
+ 'Basic Auth Credential' in [api-notes.md](./api-notes.md) for more details.
61
61
  * `-a`/`--api-name`: The API name for your heater vendor. This is visible in
62
62
  the 'API Host' entry in the 'Version' menu item in the mobile app/web app. If
63
63
  the host name is of the form `api-foo.xxxx` or `api.xxxx` use the values
@@ -97,6 +97,18 @@ node.
97
97
 
98
98
  smartbox <auth options...> set-setup <-d/--device-id> <device id> <-n/--node-addr> <node> <name>=<value> [<name>=<value> ...]
99
99
 
100
+ ### Getting node programme
101
+ The `prog` command lists the weekly schedule across all nodes and devices.
102
+
103
+ smartbox <auth options...> prog
104
+
105
+ ### Setting node programme
106
+ The `set-prog` command can be used to set the weekly schedule (programme)
107
+ of a particular node from a JSON string. The schedule is day-level-merged
108
+ onto the node's current schedule before posting:
109
+
110
+ smartbox <auth options...> set-prog <-d/--device-id> <device id> <-n/--node-addr> <node> '{"prog": {"0": [0, 2, 2, ...], ...}}'
111
+
100
112
  ### Setting node samples
101
113
 
102
114
  The `node-samples` command can be used to get the historical data (temperature and consumption) of a node.
@@ -148,7 +160,7 @@ See [api-notes.md](./api-notes.md) for notes on REST and socket.io endpoints.
148
160
  Prerequisites:
149
161
 
150
162
  uv
151
- python >=3.13
163
+ python >=3.14.2
152
164
 
153
165
  Clone the repo, install dependencies and install pre-commit hooks:
154
166
 
@@ -177,7 +189,7 @@ Another way to run the tests is by using `tox`. This runs the tests against the
177
189
 
178
190
  or by specifying a python version
179
191
 
180
- tox -e py313
192
+ tox -e py314
181
193
 
182
194
  # Support
183
195
  [![Buy a coffee to ajtudela][buymeacoffee-shield]][buymeacoffee-ajtudela]
@@ -1,10 +1,12 @@
1
- # smartbox ![build](https://github.com/ajtudela/smartbox/workflows/Python%20package/badge.svg) [![PyPI version](https://badge.fury.io/py/smartbox.svg)](https://badge.fury.io/py/smartbox) [![codecov](https://codecov.io/gh/ajtudela/smartbox/branch/main/graph/badge.svg?token=ghNZOGVzVv)](https://codecov.io/gh/ajtudela/smartbox) [![PyPI license](https://img.shields.io/pypi/l/smartbox.svg)](https://pypi.python.org/pypi/smartbox/) [![PyPI pyversions](https://img.shields.io/pypi/pyversions/smartbox.svg)](https://pypi.python.org/pypi/smartbox/)
1
+ # smartbox
2
+ ![build](https://github.com/ajtudela/smartbox/workflows/Python%20package/badge.svg)
3
+ [![PyPI version](https://badge.fury.io/py/smartbox.svg)](https://badge.fury.io/py/smartbox)
4
+ [![codecov](https://codecov.io/gh/ajtudela/smartbox/branch/main/graph/badge.svg?token=ghNZOGVzVv)](https://codecov.io/gh/ajtudela/smartbox)
5
+ [![PyPI license](https://img.shields.io/pypi/l/smartbox.svg)](https://pypi.python.org/pypi/smartbox/)
6
+ [![PyPI pyversions](https://img.shields.io/pypi/pyversions/smartbox.svg)](https://pypi.python.org/pypi/smartbox/)
2
7
 
3
8
  Python API to control heating 'smart boxes'
4
9
 
5
-
6
- ## Installation
7
-
8
10
  ## Install
9
11
 
10
12
  To install smartbox simply run:
@@ -33,9 +35,9 @@ Verbose logging can be enabled with the `-v`/`--verbose` flag.
33
35
  ### Optional options
34
36
  These options are useful if your reseller is not configured.
35
37
 
36
- * `-b`/`--base-auth-creds`: An HTTP Basic Auth credential used to do initial
38
+ * `-b`/`--basic-auth-creds`: An HTTP Basic Auth credential used to do initial
37
39
  authentication with the server. Use the base64 encoded string directly. See
38
- 'Basic Auth Credential' section below for more details.
40
+ 'Basic Auth Credential' in [api-notes.md](./api-notes.md) for more details.
39
41
  * `-a`/`--api-name`: The API name for your heater vendor. This is visible in
40
42
  the 'API Host' entry in the 'Version' menu item in the mobile app/web app. If
41
43
  the host name is of the form `api-foo.xxxx` or `api.xxxx` use the values
@@ -75,6 +77,18 @@ node.
75
77
 
76
78
  smartbox <auth options...> set-setup <-d/--device-id> <device id> <-n/--node-addr> <node> <name>=<value> [<name>=<value> ...]
77
79
 
80
+ ### Getting node programme
81
+ The `prog` command lists the weekly schedule across all nodes and devices.
82
+
83
+ smartbox <auth options...> prog
84
+
85
+ ### Setting node programme
86
+ The `set-prog` command can be used to set the weekly schedule (programme)
87
+ of a particular node from a JSON string. The schedule is day-level-merged
88
+ onto the node's current schedule before posting:
89
+
90
+ smartbox <auth options...> set-prog <-d/--device-id> <device id> <-n/--node-addr> <node> '{"prog": {"0": [0, 2, 2, ...], ...}}'
91
+
78
92
  ### Setting node samples
79
93
 
80
94
  The `node-samples` command can be used to get the historical data (temperature and consumption) of a node.
@@ -126,7 +140,7 @@ See [api-notes.md](./api-notes.md) for notes on REST and socket.io endpoints.
126
140
  Prerequisites:
127
141
 
128
142
  uv
129
- python >=3.13
143
+ python >=3.14.2
130
144
 
131
145
  Clone the repo, install dependencies and install pre-commit hooks:
132
146
 
@@ -155,7 +169,7 @@ Another way to run the tests is by using `tox`. This runs the tests against the
155
169
 
156
170
  or by specifying a python version
157
171
 
158
- tox -e py313
172
+ tox -e py314
159
173
 
160
174
  # Support
161
175
  [![Buy a coffee to ajtudela][buymeacoffee-shield]][buymeacoffee-ajtudela]