smartbox 2.5.2__tar.gz → 2.7.0__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 (167) hide show
  1. smartbox-2.7.0/.env.example +16 -0
  2. {smartbox-2.5.2 → smartbox-2.7.0}/.github/FUNDING.yml +1 -2
  3. smartbox-2.7.0/.github/ISSUE_TEMPLATE/new-reseller.md +19 -0
  4. {smartbox-2.5.2 → smartbox-2.7.0}/.github/workflows/publish-to-pypi.yml +2 -2
  5. {smartbox-2.5.2 → smartbox-2.7.0}/.github/workflows/python-package.yml +9 -9
  6. {smartbox-2.5.2 → smartbox-2.7.0}/.gitignore +7 -1
  7. smartbox-2.7.0/.vscode/settings.json +12 -0
  8. smartbox-2.7.0/CHANGELOG.md +366 -0
  9. {smartbox-2.5.2 → smartbox-2.7.0}/CODEOWNERS +1 -1
  10. smartbox-2.7.0/PKG-INFO +254 -0
  11. smartbox-2.7.0/README.md +234 -0
  12. smartbox-2.7.0/ajtudela-pr-64-audit.md +497 -0
  13. smartbox-2.7.0/api-notes.md +861 -0
  14. {smartbox-2.5.2 → smartbox-2.7.0}/codecov.yml +0 -2
  15. {smartbox-2.5.2 → smartbox-2.7.0}/pyproject.toml +27 -19
  16. {smartbox-2.5.2 → smartbox-2.7.0}/src/smartbox/__init__.py +40 -1
  17. {smartbox-2.5.2 → smartbox-2.7.0}/src/smartbox/cmd.py +266 -33
  18. smartbox-2.7.0/src/smartbox/error.py +54 -0
  19. smartbox-2.7.0/src/smartbox/models.py +325 -0
  20. {smartbox-2.5.2 → smartbox-2.7.0}/src/smartbox/reseller.py +5 -11
  21. smartbox-2.7.0/src/smartbox/retry.py +78 -0
  22. smartbox-2.7.0/src/smartbox/session.py +1872 -0
  23. smartbox-2.7.0/src/smartbox/socket.py +509 -0
  24. smartbox-2.7.0/src/smartbox/update_manager.py +774 -0
  25. smartbox-2.7.0/src/smartbox/ws_user.py +758 -0
  26. {smartbox-2.5.2 → smartbox-2.7.0}/tests/common.py +11 -0
  27. {smartbox-2.5.2 → smartbox-2.7.0}/tests/conftest.py +9 -1
  28. smartbox-2.7.0/tests/fixtures/devs/device1/acm/1/samples.json +2839 -0
  29. smartbox-2.7.0/tests/fixtures/devs/device1/acm/1/setup.json +40 -0
  30. smartbox-2.7.0/tests/fixtures/devs/device1/acm/1/status.json +24 -0
  31. smartbox-2.7.0/tests/fixtures/devs/device1/acm/1/version.json +6 -0
  32. smartbox-2.7.0/tests/fixtures/devs/device1/htr/0/samples.json +2839 -0
  33. smartbox-2.7.0/tests/fixtures/devs/device1/htr/0/setup.json +40 -0
  34. smartbox-2.7.0/tests/fixtures/devs/device1/htr/0/status.json +24 -0
  35. smartbox-2.7.0/tests/fixtures/devs/device1/htr/0/version.json +6 -0
  36. smartbox-2.7.0/tests/fixtures/devs/device1/htr_system/power_limit.json +3 -0
  37. smartbox-2.7.0/tests/fixtures/devs/device1/mgr/away_status.json +5 -0
  38. smartbox-2.7.0/tests/fixtures/devs/device1/pmo/3/version.json +6 -0
  39. smartbox-2.7.0/tests/fixtures/devs/device2/htr_mod/0/samples.json +2839 -0
  40. smartbox-2.7.0/tests/fixtures/devs/device2/htr_mod/0/setup.json +40 -0
  41. smartbox-2.7.0/tests/fixtures/devs/device2/htr_mod/0/status.json +24 -0
  42. smartbox-2.7.0/tests/fixtures/devs/device2/htr_mod/0/version.json +6 -0
  43. smartbox-2.7.0/tests/fixtures/devs/device2/htr_mod/1/setup.json +40 -0
  44. smartbox-2.7.0/tests/fixtures/devs/device2/htr_mod/1/status.json +24 -0
  45. smartbox-2.7.0/tests/fixtures/devs/device2/htr_mod/1/version.json +6 -0
  46. smartbox-2.7.0/tests/fixtures/devs/device2/htr_mod/2/setup.json +40 -0
  47. smartbox-2.7.0/tests/fixtures/devs/device2/htr_mod/2/status.json +24 -0
  48. smartbox-2.7.0/tests/fixtures/devs/device2/htr_mod/2/version.json +6 -0
  49. smartbox-2.7.0/tests/fixtures/devs/device2/htr_mod/3/setup.json +40 -0
  50. smartbox-2.7.0/tests/fixtures/devs/device2/htr_mod/3/status.json +24 -0
  51. smartbox-2.7.0/tests/fixtures/devs/device2/htr_mod/3/version.json +6 -0
  52. smartbox-2.7.0/tests/fixtures/devs/device2/htr_mod/4/setup.json +40 -0
  53. smartbox-2.7.0/tests/fixtures/devs/device2/htr_mod/4/status.json +24 -0
  54. smartbox-2.7.0/tests/fixtures/devs/device2/htr_mod/4/version.json +6 -0
  55. smartbox-2.7.0/tests/fixtures/devs/device2/htr_mod/5/setup.json +40 -0
  56. smartbox-2.7.0/tests/fixtures/devs/device2/htr_mod/5/status.json +24 -0
  57. smartbox-2.7.0/tests/fixtures/devs/device2/htr_mod/5/version.json +6 -0
  58. smartbox-2.7.0/tests/fixtures/devs/device2/htr_system/power_limit.json +3 -0
  59. smartbox-2.7.0/tests/fixtures/devs/device2/mgr/away_status.json +5 -0
  60. smartbox-2.7.0/tests/fixtures/devs/device2/mgr/nodes.json +46 -0
  61. smartbox-2.7.0/tests/fixtures/devs.json +19 -0
  62. smartbox-2.7.0/tests/fixtures/grouped_devs.json +29 -0
  63. smartbox-2.7.0/tests/fixtures/live/htr_prog_addr2.json +187 -0
  64. smartbox-2.7.0/tests/fixtures/live/htr_prog_addr3.json +187 -0
  65. smartbox-2.7.0/tests/fixtures/live/htr_prog_addr4.json +187 -0
  66. smartbox-2.7.0/tests/fixtures/live/htr_prog_addr5.json +187 -0
  67. smartbox-2.7.0/tests/fixtures/live/htr_prog_addr6.json +187 -0
  68. smartbox-2.7.0/tests/fixtures/live/htr_setup_addr2.json +24 -0
  69. smartbox-2.7.0/tests/fixtures/live/htr_setup_addr3.json +24 -0
  70. smartbox-2.7.0/tests/fixtures/live/htr_setup_addr4.json +24 -0
  71. smartbox-2.7.0/tests/fixtures/live/htr_setup_addr5.json +24 -0
  72. smartbox-2.7.0/tests/fixtures/live/htr_setup_addr6.json +24 -0
  73. smartbox-2.7.0/tests/fixtures/live/htr_setup_no_boost.json +24 -0
  74. smartbox-2.7.0/tests/fixtures/live/htr_status_boost_off.json +25 -0
  75. smartbox-2.7.0/tests/fixtures/live/socket_update_status_frame.json +28 -0
  76. smartbox-2.7.0/tests/fixtures/live/socket_update_sync_lost_frame.json +6 -0
  77. smartbox-2.7.0/tests/fixtures/live/socket_update_version_frame.json +9 -0
  78. smartbox-2.7.0/tests/fixtures/test_path.json +3 -0
  79. {smartbox-2.5.2 → smartbox-2.7.0}/tests/test_cmd.py +323 -2
  80. smartbox-2.7.0/tests/test_live_payloads.py +133 -0
  81. {smartbox-2.5.2 → smartbox-2.7.0}/tests/test_models.py +51 -0
  82. {smartbox-2.5.2 → smartbox-2.7.0}/tests/test_reseller.py +14 -5
  83. {smartbox-2.5.2 → smartbox-2.7.0}/tests/test_session.py +1414 -264
  84. smartbox-2.7.0/tests/test_session_reliability.py +552 -0
  85. smartbox-2.7.0/tests/test_socket.py +673 -0
  86. smartbox-2.7.0/tests/test_update_manager.py +974 -0
  87. smartbox-2.7.0/tests/test_ws_user.py +887 -0
  88. smartbox-2.7.0/tools/api_functional_tests.py +233 -0
  89. smartbox-2.7.0/tools/away_probe.py +109 -0
  90. smartbox-2.7.0/tools/capture_prog.py +84 -0
  91. smartbox-2.7.0/tools/live_check.py +137 -0
  92. smartbox-2.7.0/tools/probe_extra_options.py +145 -0
  93. smartbox-2.7.0/tools/probe_modified_auto.py +214 -0
  94. smartbox-2.7.0/tools/probe_settle_window.py +146 -0
  95. smartbox-2.7.0/tools/probe_ws_user.py +491 -0
  96. smartbox-2.7.0/tools/prog_functional_tests.py +143 -0
  97. smartbox-2.7.0/tools/watch_status.py +123 -0
  98. smartbox-2.7.0/uv.lock +1424 -0
  99. smartbox-2.7.0/webapi-spec.md +186 -0
  100. smartbox-2.5.2/.github/ISSUE_TEMPLATE/new-reseller.md +0 -18
  101. smartbox-2.5.2/.vscode/settings.json +0 -7
  102. smartbox-2.5.2/CHANGELOG.md +0 -39
  103. smartbox-2.5.2/PKG-INFO +0 -190
  104. smartbox-2.5.2/README.md +0 -168
  105. smartbox-2.5.2/api-notes.md +0 -141
  106. smartbox-2.5.2/src/smartbox/error.py +0 -19
  107. smartbox-2.5.2/src/smartbox/models.py +0 -273
  108. smartbox-2.5.2/src/smartbox/session.py +0 -664
  109. smartbox-2.5.2/src/smartbox/socket.py +0 -313
  110. smartbox-2.5.2/src/smartbox/update_manager.py +0 -298
  111. smartbox-2.5.2/tests/fixtures/devs/device1/acm/1/samples.json +0 -2839
  112. smartbox-2.5.2/tests/fixtures/devs/device1/acm/1/setup.json +0 -40
  113. smartbox-2.5.2/tests/fixtures/devs/device1/acm/1/status.json +0 -24
  114. smartbox-2.5.2/tests/fixtures/devs/device1/acm/1/version.json +0 -6
  115. smartbox-2.5.2/tests/fixtures/devs/device1/htr/0/samples.json +0 -2839
  116. smartbox-2.5.2/tests/fixtures/devs/device1/htr/0/setup.json +0 -40
  117. smartbox-2.5.2/tests/fixtures/devs/device1/htr/0/status.json +0 -24
  118. smartbox-2.5.2/tests/fixtures/devs/device1/htr/0/version.json +0 -6
  119. smartbox-2.5.2/tests/fixtures/devs/device1/htr_system/power_limit.json +0 -1
  120. smartbox-2.5.2/tests/fixtures/devs/device1/mgr/away_status.json +0 -1
  121. smartbox-2.5.2/tests/fixtures/devs/device1/pmo/3/version.json +0 -6
  122. smartbox-2.5.2/tests/fixtures/devs/device2/htr_mod/0/samples.json +0 -2839
  123. smartbox-2.5.2/tests/fixtures/devs/device2/htr_mod/0/setup.json +0 -40
  124. smartbox-2.5.2/tests/fixtures/devs/device2/htr_mod/0/status.json +0 -24
  125. smartbox-2.5.2/tests/fixtures/devs/device2/htr_mod/0/version.json +0 -6
  126. smartbox-2.5.2/tests/fixtures/devs/device2/htr_mod/1/setup.json +0 -40
  127. smartbox-2.5.2/tests/fixtures/devs/device2/htr_mod/1/status.json +0 -24
  128. smartbox-2.5.2/tests/fixtures/devs/device2/htr_mod/1/version.json +0 -6
  129. smartbox-2.5.2/tests/fixtures/devs/device2/htr_mod/2/setup.json +0 -40
  130. smartbox-2.5.2/tests/fixtures/devs/device2/htr_mod/2/status.json +0 -24
  131. smartbox-2.5.2/tests/fixtures/devs/device2/htr_mod/2/version.json +0 -6
  132. smartbox-2.5.2/tests/fixtures/devs/device2/htr_mod/3/setup.json +0 -40
  133. smartbox-2.5.2/tests/fixtures/devs/device2/htr_mod/3/status.json +0 -24
  134. smartbox-2.5.2/tests/fixtures/devs/device2/htr_mod/3/version.json +0 -6
  135. smartbox-2.5.2/tests/fixtures/devs/device2/htr_mod/4/setup.json +0 -40
  136. smartbox-2.5.2/tests/fixtures/devs/device2/htr_mod/4/status.json +0 -24
  137. smartbox-2.5.2/tests/fixtures/devs/device2/htr_mod/4/version.json +0 -6
  138. smartbox-2.5.2/tests/fixtures/devs/device2/htr_mod/5/setup.json +0 -40
  139. smartbox-2.5.2/tests/fixtures/devs/device2/htr_mod/5/status.json +0 -24
  140. smartbox-2.5.2/tests/fixtures/devs/device2/htr_mod/5/version.json +0 -6
  141. smartbox-2.5.2/tests/fixtures/devs/device2/htr_system/power_limit.json +0 -1
  142. smartbox-2.5.2/tests/fixtures/devs/device2/mgr/away_status.json +0 -1
  143. smartbox-2.5.2/tests/fixtures/devs/device2/mgr/nodes.json +0 -46
  144. smartbox-2.5.2/tests/fixtures/devs.json +0 -19
  145. smartbox-2.5.2/tests/fixtures/grouped_devs.json +0 -24
  146. smartbox-2.5.2/tests/fixtures/test_path.json +0 -1
  147. smartbox-2.5.2/tests/test_update_manager.py +0 -283
  148. smartbox-2.5.2/uv.lock +0 -1155
  149. {smartbox-2.5.2 → smartbox-2.7.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  150. {smartbox-2.5.2 → smartbox-2.7.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  151. {smartbox-2.5.2 → smartbox-2.7.0}/.github/dependabot.yml +0 -0
  152. {smartbox-2.5.2 → smartbox-2.7.0}/.pre-commit-config.yaml +0 -0
  153. {smartbox-2.5.2 → smartbox-2.7.0}/LICENSE +0 -0
  154. {smartbox-2.5.2 → smartbox-2.7.0}/src/smartbox/py.typed +0 -0
  155. {smartbox-2.5.2 → smartbox-2.7.0}/tests/__init__.py +0 -0
  156. {smartbox-2.5.2 → smartbox-2.7.0}/tests/fixtures/devs/device1/connected.json +0 -0
  157. {smartbox-2.5.2 → smartbox-2.7.0}/tests/fixtures/devs/device1/mgr/nodes.json +0 -0
  158. {smartbox-2.5.2 → smartbox-2.7.0}/tests/fixtures/devs/device1/pmo/3/samples.json +0 -0
  159. {smartbox-2.5.2 → smartbox-2.7.0}/tests/fixtures/devs/device1/pmo/3/setup.json +0 -0
  160. {smartbox-2.5.2 → smartbox-2.7.0}/tests/fixtures/devs/device1/pmo/3/status.json +0 -0
  161. {smartbox-2.5.2 → smartbox-2.7.0}/tests/fixtures/devs/device2/connected.json +0 -0
  162. {smartbox-2.5.2 → smartbox-2.7.0}/tests/fixtures/devs/device2/htr_mod/1/samples.json +0 -0
  163. {smartbox-2.5.2 → smartbox-2.7.0}/tests/fixtures/devs/device2/htr_mod/2/samples.json +0 -0
  164. {smartbox-2.5.2 → smartbox-2.7.0}/tests/fixtures/devs/device2/htr_mod/3/samples.json +0 -0
  165. {smartbox-2.5.2 → smartbox-2.7.0}/tests/fixtures/devs/device2/htr_mod/4/samples.json +0 -0
  166. {smartbox-2.5.2 → smartbox-2.7.0}/tests/fixtures/devs/device2/htr_mod/5/samples.json +0 -0
  167. {smartbox-2.5.2 → smartbox-2.7.0}/tests/fixtures/groups/test_home/guest_users.json +0 -0
@@ -0,0 +1,16 @@
1
+ # Copy this file to ".env" and fill in your values so the `smartbox` CLI does
2
+ # not need the credentials on every invocation. A real shell environment
3
+ # variable always overrides the value set here, and an explicit command-line
4
+ # option overrides both.
5
+
6
+ # Your username/password as used for the mobile app / web app (required).
7
+ SMARTBOX_USERNAME=
8
+ SMARTBOX_PASSWORD=
9
+
10
+ # API name for your heater vendor, e.g. "api" or "api-foo" (see README).
11
+ SMARTBOX_API_NAME=api
12
+
13
+ # Only needed if your reseller is not built into the package.
14
+ SMARTBOX_BASIC_AUTH_CREDS=
15
+ SMARTBOX_X_REFERER=
16
+ SMARTBOX_X_SERIAL_ID=
@@ -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
@@ -0,0 +1,19 @@
1
+ ---
2
+ name: New reseller
3
+ about: Add a new available reseller.
4
+ title: "[RESELLER] New available reseller"
5
+ labels: reseller
6
+ assignees: Delmael
7
+
8
+ ---
9
+
10
+ > [!NOTE]
11
+ > If you do not see your current reseller in the available list, you can ask to add it. We cannot ensure your device go through this platform.
12
+ > See [How to capture the values](https://github.com/ajtudela/smartbox/blob/main/api-notes.md#basic-auth-credential) — open your reseller's web app, watch the network tab, log in and read the `POST .../api/v2/client/token` request headers.
13
+
14
+ * [ ] I have checked with the `smartbox resellers` command that my reseller is not already present
15
+ * **Website url (mandatory)** (the `x-referer` header):
16
+ * Name of the reseller:
17
+ * API name (the `api-<...>` part of the request host, e.g. `api-foo`):
18
+ * Basic auth credential (the base64 string after `Basic ` in the `authorization` header):
19
+ * Serial id (the `x-serialid` header):
@@ -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,19 +18,19 @@ 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
31
31
  run: uv sync --all-extras --dev
32
32
  - name: Lint with ruff
33
- run: uv run ruff check src/smartbox
33
+ run: uv run ruff check src/smartbox tests
34
34
 
35
35
  typechecker:
36
36
  runs-on: ubuntu-latest
@@ -40,19 +40,19 @@ 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
53
53
  run: uv sync --all-extras --dev
54
54
  - name: Typecheck with mypy
55
- run: uv run mypy src/smartbox
55
+ run: uv run mypy src/smartbox tests
56
56
 
57
57
  build:
58
58
  runs-on: ubuntu-latest
@@ -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,10 @@ venv.bak/
72
72
  junit.xml
73
73
  .DS_Store
74
74
 
75
- *.code-workspace
75
+ *.code-workspace
76
+
77
+ # ws_user probe token stores (contain live credentials)
78
+ *token*store*.json
79
+ wsu_store.json
80
+ *token*store*.json.tmp
81
+ wsu_store.json.tmp
@@ -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,366 @@
1
+ # Changelog
2
+
3
+ ## 2.7.0
4
+
5
+ ### New
6
+ * Per-user `ws_user` websocket transport (`WsUserSocketSession`,
7
+ `check_ws_user_support`): one connection serves every device of the
8
+ account, replacing N per-device `socket_io` sessions. Wire protocol
9
+ and every failure mode verified live (api-notes.md, "Transports —
10
+ socket_io vs ws_user"). Selected via the new `ws_user_socket=` kwarg
11
+ on `UpdateManager`; the default transport is unchanged.
12
+ * `WsUserUnsupportedError` + the fallback doctrine: only deterministic
13
+ handshake rejections (HTTP 401/403/404/410 in the one-shot probe on a
14
+ fresh token; 404/410 in the run loop, or 401/403 that keep rejecting
15
+ freshly minted tokens) signal "endpoint unsupported"; transient
16
+ trouble is retried and never demotes.
17
+ * In `ws_user` mode `UpdateManager` ignores the SocketSession
18
+ pass-through kwargs (warned at construction) — the shared socket's
19
+ liveness is heartbeat-driven, not knob-driven.
20
+
21
+ ### Breaking
22
+ * `UpdateManager.socket_session` now returns `None` when the manager
23
+ runs in `ws_user` mode (previously always a `SocketSession`); use the
24
+ new `ws_user_socket` accessor to distinguish modes.
25
+
26
+ ### Features
27
+ * CLI: every authentication option also resolves from a `SMARTBOX_*`
28
+ environment variable, and the `smartbox` console script loads a `.env`
29
+ file (searched upward from the working directory) before parsing
30
+ options. Precedence: command line > shell environment > `.env`. See
31
+ `.env.example`.
32
+
33
+ ### Fixes
34
+ * CLI: `.env` parsing now follows python-dotenv semantics — an inline
35
+ `#` starts a comment only when it is preceded by whitespace and lies
36
+ outside quotes, and quoted values unquote even when a trailing
37
+ comment follows them. Previously inline comments silently became
38
+ part of the value and quote-stripping was defeated, corrupting
39
+ credentials written in the conventional quoted style.
40
+
41
+ ### Refactor
42
+ * `UpdateManager`: `subscribe_to_node_status`/`_setup`/`_version`/`_prog`
43
+ share one `_subscribe_to_node_key` implementation.
44
+
45
+ ### Tests & CI
46
+ * Socket tests for the namespace callbacks, websocket cleanup, SIGINT
47
+ handler and cancellation during connect; coverage floor of 95%
48
+ (`fail_under`) enforced.
49
+ * ruff and mypy run over `tests/` in CI; `socket.py` is no longer
50
+ excluded from codecov.
51
+
52
+ ### Docs
53
+ * README rewritten: library usage, command table, `.env` section and an
54
+ actionable Support section. api-notes documents how to capture the
55
+ Basic Auth credential and the new-reseller issue template points to it.
56
+
57
+ ## 2.6.2
58
+
59
+ Port round of upstream PR #64 (ajtudela, `improvements/fix_bugs`, 19
60
+ commits) — full per-commit audit, verdicts and evidence in
61
+ ajtudela-pr-64-audit.md. Ten commits were already fixed by the 2.6.x
62
+ round (independent convergence); of the remainder, seven were ported
63
+ (each commit references the upstream source hash), one was declined by
64
+ decision (#7: typed-mode sparse statuses keep the rich
65
+ `SmartboxValidationError` rejection — the bare `{"sync_status": "lost"}`
66
+ dead-node frame is first-class there), and one is upstream-only (#18,
67
+ version bump).
68
+
69
+ ### Bug Fixes
70
+ * `set_node_setup` never serializes the read-modify-write GET through
71
+ the Pydantic models: the internal read is always raw, so keys the
72
+ setup models do not declare (e.g. `counter_offset` on pmo nodes) can
73
+ no longer be wiped on the device by a setup write (upstream `5618aa3`).
74
+ Completed by `extra="allow"` on `DefaultNodeSetup`/`PmoSetup`, so
75
+ typed (`raw_response=False`) reads keep unknown keys and round-trip
76
+ losslessly (upstream `5618aa3`, completion by decision).
77
+ * `InvalidAuthError`, `APIUnavailableError` and `ResellerNotExistError`
78
+ now derive from `SmartboxError` — one `except SmartboxError` catches
79
+ every library failure. `APIUnavailableError` keeps its
80
+ `aiohttp.ClientConnectionError` base for backwards compatibility; the
81
+ data-path 403 → `SmartboxError` mapping is kept deliberately
82
+ (upstream `62efcf8`).
83
+ * CLI: unknown `-d`/`-n` values raise `click.BadParameter` with a
84
+ `param_hint` (exit code 2, usage line, offending option named)
85
+ instead of `ClickException`; malformed wire node payloads stay
86
+ `ClickException` (upstream `6ad2014`).
87
+
88
+ ### Build & Docs
89
+ * Core dependencies bounded to their current major: `aiohttp>=3,<4`,
90
+ `pydantic>=2,<3`, `python-socketio>=5,<6` (upstream `da3bb9c`; the
91
+ commit's unused-dependency removal was already done in 2.6.0).
92
+ * `.vscode/settings.json`: pylint/flake8 editor settings replaced with
93
+ the ruff formatter/code-actions setup (upstream `a78bc3a`; its
94
+ pyproject/ruff halves were already done in 2.6.0).
95
+ * CODEOWNERS co-owner and README badge reflow adopted from upstream
96
+ `2ec1ef0`.
97
+
98
+ ### Tests
99
+ * `test_all_resellers` (real HTTP calls to twelve reseller servers)
100
+ moved behind a `network` marker, deselected by default; run on demand
101
+ with `pytest -m network`. Sessions in it are closed (upstream
102
+ `0bef64c`).
103
+
104
+ ## 2.6.1
105
+
106
+ ### Features
107
+ * Node reachability / write-confirmation tracking on `UpdateManager`:
108
+ `subscribe_to_node_availability` / `get_node_availability` report a
109
+ node Available/Unavailable from its status/setup frames — a bare
110
+ `{"sync_status": "lost"}` frame arms a uniform 6 s grace timer
111
+ (`unavailable_delay` knob, matches the vendor app's unreachable
112
+ window); any ok frame reports Available again. `expect_write_confirmation`
113
+ (knob `write_confirm_timeout`, 6 s) arms a window after an accepted
114
+ write: if no ok frame arrives, a one-shot discriminating GET decides
115
+ "applied (node alive)" vs "silently dropped (node unreachable)" —
116
+ writes to unreachable nodes are ACKed OK with no device ack (observed
117
+ live). Frame taps register lazily; both features share them, each
118
+ enables them independently. Backed by live probes 2026-09-26/30
119
+ (api-notes.md, "Node reachability").
120
+
121
+ ## 2.6.0
122
+
123
+ ### Features
124
+ * Full node programme (schedule) support: `get_node_prog` / `set_node_prog`
125
+ (+ typed `NodeProg` model, sync `Session.get_prog` / `set_prog` wrappers,
126
+ `subscribe_to_node_prog` websocket helper and `prog` / `set-prog` CLI
127
+ commands). Wire note: GET and POST both use the day-keyed object shape;
128
+ the array shape is rejected with 400 (observed live).
129
+ * Full management/fleet surface per webapi-spec §2–3: device discovery
130
+ GET/POST, device rtc/time, device/node/group rename, device/node delete,
131
+ device group move, node select/power, group geo_data/extra_data, invite
132
+ flows (invite/revoke/confirm — confirm is unauthenticated), quiet-home
133
+ notification config GET/POST + test, encrypted-wifi-credentials and
134
+ geocoding helpers. Read-only-verified paths pinned from the official
135
+ vendor app; all writes marked **unverified on live hardware** in docstrings.
136
+ New base helpers: `_api_delete`, host-relative `_api_get` (params not
137
+ logged — may carry secrets) and `_api_post_path` (auth optional).
138
+ * Away-status writes default `enabled: true` to match the vendor app shape
139
+ (`{away, enabled}`); the server also honours `{away}` alone (D1 verified
140
+ live).
141
+ * Setup models widened for the fw-1.9 htr family: optional
142
+ `user_duty_factor` / `flash_version` / `extra_options`; new
143
+ `max_stemp_limit` / `priority` / `revision` and
144
+ `factory_options.operating_mode` / `power_factor` / `super_lock_available`.
145
+ Its `factory_options` carries no `prog_resolution` (schedule resolution
146
+ not readable from setup on this family).
147
+
148
+ ### Bug Fixes
149
+ * `SocketSession.cancel()` is now bounded by a timeout (no more unbounded
150
+ hangs on a wedged websocket) and disconnects exactly once (cancel +
151
+ shutdown no longer double-disconnect).
152
+ * The websocket loop checks the exit flag before touching the REST API, so
153
+ cancel is no longer delayed by a slow auth refresh.
154
+ * Dangling background cleanup tasks are awaited (bounded) on shutdown
155
+ instead of dying as "Task was destroyed but it is pending" noise.
156
+ * Routine retry log downgraded from `_LOGGER.exception` to `_LOGGER.warning`.
157
+ * Status models widened for the newer htr firmware family (`error_code`
158
+ str|int; optional `act_duty` / `power_pcb_temp`); optional fields
159
+ `easy` / `runback` / `version` tolerated.
160
+
161
+ * Code-review round 3 (2026-09-27): request-timeouts and HTTP 5xx now map
162
+ to `APIUnavailableError` (aiohttp's total-timeout raises a plain
163
+ `TimeoutError` that previously escaped all error handling; 5xx GETs are
164
+ retried via the existing retry wrapper — 4xx still raises
165
+ `SmartboxError`); request bodies are redacted in debug logs (the
166
+ invite-confirmation `pass` leaked in plaintext); subscription user
167
+ callbacks are isolated — one raising callback can no longer starve the
168
+ other subscriptions or propagate into the socket loop, and jq-emitted
169
+ `null` node payloads no longer fire callbacks; the socket reconnect
170
+ backoff is capped at 30s per attempt (previously up to ~8.5 min of
171
+ futile retries before the token refresh that usually fixes the
172
+ disconnect); `_attempt_connection` also tolerates transient
173
+ `TimeoutError`/`OSError` from the websocket transport instead of
174
+ killing the run loop; the ping task is cancelled **and** reaped
175
+ (bounded) on cancel/shutdown; `get_device_power_limit` raises
176
+ `SmartboxError` instead of `KeyError` on an unexpected payload;
177
+ sync `Session` annotations aligned with the async surface (`set_setup`
178
+ → `None`, `get_status`/`get_setup`/`get_prog` may return models);
179
+ CLI: non-numeric node addresses give a clean error, `resellers` prints
180
+ a readable list without credentials, and `-u`/`-p` accept the
181
+ `SMARTBOX_USERNAME`/`SMARTBOX_PASSWORD` env vars; `__version__` no
182
+ longer explodes when imported uninstalled.
183
+
184
+ * Code-review round 4 (2026-09-27): sync `Session` no longer fails on the
185
+ second consecutive call with "RuntimeError: Session is closed" (the
186
+ per-call cleanup now forgets the closed client session so the next
187
+ `asyncio.run` recreates it — regression test drives the real client
188
+ through a local HTTP server); sync `get_device_power_limit` /
189
+ `set_device_power_limit` regain the async `node` parameter (pmo devices
190
+ were unreachable from the sync API) and sync wrappers were added for
191
+ `health_check`, `api_version`, `get_device_connected`,
192
+ `get_node_samples` and `get_node_version`; CLI `-a/--api-name` defaults
193
+ to `api` and session/API failures (auth, network, unknown reseller)
194
+ surface as clean `ClickException` errors instead of tracebacks;
195
+ `set_node_prog` rejects array-shape / `prog`-less payloads with a clear
196
+ error instead of an `AttributeError` traceback or a silent no-op POST
197
+ (CLI guard included); a token endpoint 200 with a non-JSON body raises
198
+ `SmartboxError` instead of `InvalidAuthError` (it must not trigger the
199
+ consumer's reauth flow); `SocketSession.run` authenticates before the
200
+ first connect attempt (a cold CLI session no longer burns its
201
+ reconnect budget on an empty token); the CLI SIGINT handler is removed
202
+ at teardown and platforms without signal-handler support no longer die
203
+ on connect; package exports completed (`NodeVersion` and the remaining
204
+ public model types).
205
+
206
+ * Code-review round 5 (2026-09-27) — **contract changes**:
207
+ * `raw_response=True` no longer validates responses: `get_devices`,
208
+ `get_homes`, `get_grouped_devices`, `get_home_guests`,
209
+ `get_device_connected` and `get_device_away_status` return the wire
210
+ payload (with only the documented list extraction, e.g. `devs` +
211
+ `invited_to` flattened) instead of model-serialised dicts. Unknown
212
+ keys are kept (live: `grouped_devs` carries `extraData`, previously
213
+ dropped); a payload too malformed to extract from raises
214
+ `SmartboxError`. All raw getters now follow the same rule.
215
+ * New `SmartboxValidationError(SmartboxError)`, exported: with
216
+ `raw_response=False`, payload drift raises it (chained from the
217
+ pydantic error) instead of a bare `pydantic.ValidationError`.
218
+ * Every library call is bounded by a 30s whole-call deadline covering
219
+ token refresh, retries and backoff — also on caller-provided
220
+ websessions (previously aiohttp's 300s default applied there, and 5
221
+ retries could stretch one call to minutes). Expiry raises
222
+ `APIUnavailableError`; a backoff that would overrun it is skipped.
223
+ * A rejected refresh token falls back to one password login before
224
+ `InvalidAuthError` is raised; a mid-session 401 invalidates the
225
+ token and retries the request once after re-authenticating.
226
+ * Code-review round 5 — reliability fixes:
227
+ * The websocket loop survives transient auth-refresh failures
228
+ (`APIUnavailableError`/`SmartboxError` are waited out with capped
229
+ backoff; previously one network blip ended `run()` for good); only
230
+ rejected credentials end it.
231
+ * `cancel()` during reconnect backoff stops the reconnect loop
232
+ immediately (it used to keep attempting), and a connect completing
233
+ after `cancel()` is disconnected instead of leaking a live socket
234
+ that kept `run()` alive.
235
+ * The keepalive ping task survives a failed send (it died for the
236
+ rest of the session on one `BadNamespaceError`).
237
+ * Secrets: the INFO "Successfully connected" log no longer prints the
238
+ access token; error messages and logs no longer embed request query
239
+ strings (aiohttp's exception text leaked the wifi password of
240
+ `get_encrypted_wifi_credentials`).
241
+ * Truncated response bodies (`ClientPayloadError`) map to
242
+ `APIUnavailableError` (retried on GETs) and undecodable JSON to
243
+ `SmartboxError` — both previously escaped the library's error types.
244
+ * `retry_attempts=0` makes one attempt instead of raising `TypeError`.
245
+ * Update subscriptions anchor their paths (`/prog` no longer matches
246
+ `/prog_temps` frames, likewise status/setup/version).
247
+ * Hygiene: 45 unneeded `type: ignore`s removed, sync
248
+ `Session.set_device_away_status` annotated `-> None`, dead
249
+ `_basic_auth_credentials` attribute removed, `socket.py` imports
250
+ `urllib.parse` explicitly.
251
+
252
+ * Code-review round 6 (2026-09-27, every finding reproduced by a probe
253
+ before fixing): the token endpoint (`/client/token`) maps truncated
254
+ bodies (`ClientPayloadError`) and undecodable-JSON 200s to
255
+ `APIUnavailableError` / `SmartboxError` like the REST path — a
256
+ truncated token body previously escaped raw and **ended the websocket
257
+ loop for good** (end-to-end reproduced); `SocketSession` rejects
258
+ `ping_interval <= 0` (hot ping loop) and `reconnect_attempts < 1`
259
+ (endless refresh churn) with a `ValueError` at construction; the CLI
260
+ `set-status` command gives a clean error for `--stemp` without
261
+ `--units` instead of a raw `ValueError` traceback; the token endpoint
262
+ treats HTTP 429 as transient (`APIUnavailableError`) instead of bad
263
+ credentials — rate limiting no longer triggers the password-login
264
+ fallback, the consumer's reauth flow, or the end of the websocket
265
+ loop (400/401 rejections still surface as `InvalidAuthError`).
266
+
267
+ * Code-review round 6 follow-up (2026-09-27, **breaking** — 2.6.0 is the
268
+ alignment window; every fix probe-verified): `set_node_prog` raises
269
+ `ValueError` for a `prog`-less / array-shaped payload (was `TypeError`
270
+ — now aligned with `set_node_status`'s input validation); the sync
271
+ `Session` wrapper return annotations reflect `raw_response=False`
272
+ (e.g. `get_devices` may return a `Devices` model, `get_nodes` a
273
+ `list[Node]`); the sync `Session` no longer leaks "coroutine was never
274
+ awaited" `RuntimeWarning`s when misused inside a running event loop
275
+ (the pre-built coroutines are closed before re-raising); socket
276
+ teardown reaps cancelled disconnect tasks and cancelled pending
277
+ background tasks (bounded) — no more "Task was destroyed but it is
278
+ pending" noise in edge teardown timing.
279
+
280
+ * Dependency refresh (2026-09-27, all gates + live socket round green):
281
+ runtime aiohttp 3.14.0→3.14.3, pydantic 2.13.4→2.13.5,
282
+ python-socketio 5.16.2→5.17.0 (+engineio 4.14.0, bidict), asyncclick
283
+ 8.3.0.7→8.4.2.1, jq 1.11.0→1.12.0; dev pytest 9.1.1, ruff 0.16.9,
284
+ mypy 2.3.1, time-machine 3.5.1, tox 4.64.3, pytest-randomly 5.0.0.
285
+ * Removed dead dependencies `requests` and `websocket-client` (imported
286
+ nowhere; legacy from the pre-asyncio era — the websocket path uses
287
+ python-socketio/simple-websocket, HTTP uses aiohttp).
288
+ * Lint config: ruff `target-version` aligned to the runtime floor
289
+ (py313 → py314); disabled CPY001 (no copyright headers in this
290
+ project) and PLR0917 (positional variant of the already-ignored
291
+ PLR0913); dropped dead mypy override modules (`freezegun`, `test`);
292
+ typing-only imports moved behind `TYPE_CHECKING` (ruff TC rules).
293
+
294
+ * Code-review round (2026-09-27, all gates + contract tests + live smoke
295
+ green): caller-provided websessions are no longer closed on context
296
+ exit (session-ownership model, `aclose_owned_session()`); REST requests
297
+ get bounded timeouts (30s total / 10s connect) on library-created
298
+ sessions; GETs retry on transient connection failures (uses the
299
+ previously dead `retry_attempts`/`backoff_factor` constructor config);
300
+ mid-session HTTP 401 now raises `InvalidAuthError` (consumers can
301
+ trigger reauth); access token is masked in debug logs; sync `Session`
302
+ closes its lazily-created client session per call (no more "Unclosed
303
+ client session" warnings); CLI: clean `ClickException` errors for
304
+ unknown device/node ids and invalid prog JSON, and fixed a latent bug
305
+ where set-* commands could never resolve a node (string vs int address
306
+ comparison); sample-window defaults are computed at call time (they
307
+ were frozen at import and drifted in long-lived processes); docstring
308
+ fixes (`Sample`/`PmoSample` were swapped, `subscribe_to_device_connected`
309
+ claimed to be a power-limit hook); removed redundant field
310
+ redeclarations in status subclasses and 1-tuple call hacks in the
311
+ update manager; dropped dead `[tool.uv.workspace]` and
312
+ `[tool.setuptools.*]` config.
313
+
314
+ * Code-review round 7 — final pre-release pass (2026-09-27, gates +
315
+ contract tests green): `SocketSession` can now be constructed outside
316
+ a running event loop (the REST websession binding moved from
317
+ `__init__` to `run()`, with `external_http=True` so engineio never
318
+ closes the session shared with REST traffic); the websocket run loop
319
+ no longer dies when the first `dev_data` emit races the server drop
320
+ (`BadNamespaceError` is a *sibling* of `ConnectionError` under
321
+ `SocketIOError` — the catch family was widened to `SocketIOError`);
322
+ a token-endpoint 200 with a wrong-shaped body raises `SmartboxError`
323
+ instead of `InvalidAuthError` (consistent with the undecodable-JSON /
324
+ non-JSON mapping: a server-side drift is not "credentials rejected");
325
+ `AsyncSession` rejects a negative `backoff_factor` at construction
326
+ (it previously silently disabled backoff); CLI node resolution gives
327
+ a clean error on malformed wire payloads (missing/non-numeric `addr`)
328
+ instead of a raw `KeyError`/`ValueError` traceback.
329
+
330
+ ## 0.0.5 (alpha)
331
+
332
+ ### Features
333
+ * Rename `away_status` to `device_away_status`
334
+ * Update API docs
335
+ * Add tox, tox and flake8 on github action
336
+
337
+ ### Bug Fixes
338
+ * Pin dependency of python-socketio to match server
339
+
340
+ ## 0.0.4 (alpha)
341
+
342
+ ### Features
343
+ * Refactor socket session and implement reconnect
344
+ * Add note on basic auth credentials
345
+
346
+ ## 0.0.3 (alpha)
347
+
348
+ ### Features
349
+ * Fixed packaging
350
+
351
+ ### Bug Fixes
352
+ * Fixed disconnect handling on token refresh
353
+
354
+ ## 0.0.2 (alpha)
355
+
356
+ ### Features
357
+ * Added `get_api_name` function
358
+ * Added basic tests for REST interactions
359
+ * Added token refresh support
360
+ * Added socket.io interface via `open_socket` function (no tests as yet)
361
+ * Added documentation for known REST and websocket endpoints
362
+
363
+ ## 0.0.1 (alpha)
364
+
365
+ ### Features
366
+ * 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