smartbox 2.3.1__tar.gz → 2.4.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 (76) hide show
  1. {smartbox-2.3.1 → smartbox-2.4.0}/.github/workflows/publish-to-pypi.yml +2 -2
  2. {smartbox-2.3.1 → smartbox-2.4.0}/.github/workflows/python-package.yml +7 -7
  3. {smartbox-2.3.1 → smartbox-2.4.0}/.pre-commit-config.yaml +3 -3
  4. {smartbox-2.3.1 → smartbox-2.4.0}/PKG-INFO +1 -1
  5. {smartbox-2.3.1 → smartbox-2.4.0}/pyproject.toml +1 -1
  6. {smartbox-2.3.1 → smartbox-2.4.0}/src/smartbox/models.py +7 -1
  7. {smartbox-2.3.1 → smartbox-2.4.0}/src/smartbox/reseller.py +17 -1
  8. {smartbox-2.3.1 → smartbox-2.4.0}/src/smartbox/session.py +1 -1
  9. {smartbox-2.3.1 → smartbox-2.4.0}/src/smartbox/update_manager.py +27 -0
  10. {smartbox-2.3.1 → smartbox-2.4.0}/tests/test_reseller.py +2 -4
  11. {smartbox-2.3.1 → smartbox-2.4.0}/tests/test_update_manager.py +58 -0
  12. {smartbox-2.3.1 → smartbox-2.4.0}/tox.ini +0 -2
  13. {smartbox-2.3.1 → smartbox-2.4.0}/uv.lock +283 -244
  14. {smartbox-2.3.1 → smartbox-2.4.0}/.github/FUNDING.yml +0 -0
  15. {smartbox-2.3.1 → smartbox-2.4.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  16. {smartbox-2.3.1 → smartbox-2.4.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  17. {smartbox-2.3.1 → smartbox-2.4.0}/.github/ISSUE_TEMPLATE/new-reseller.md +0 -0
  18. {smartbox-2.3.1 → smartbox-2.4.0}/.github/dependabot.yml +0 -0
  19. {smartbox-2.3.1 → smartbox-2.4.0}/.gitignore +0 -0
  20. {smartbox-2.3.1 → smartbox-2.4.0}/.vscode/settings.json +0 -0
  21. {smartbox-2.3.1 → smartbox-2.4.0}/CHANGELOG.md +0 -0
  22. {smartbox-2.3.1 → smartbox-2.4.0}/CODEOWNERS +0 -0
  23. {smartbox-2.3.1 → smartbox-2.4.0}/LICENSE +0 -0
  24. {smartbox-2.3.1 → smartbox-2.4.0}/README.md +0 -0
  25. {smartbox-2.3.1 → smartbox-2.4.0}/api-notes.md +0 -0
  26. {smartbox-2.3.1 → smartbox-2.4.0}/codecov.yml +0 -0
  27. {smartbox-2.3.1 → smartbox-2.4.0}/src/smartbox/__init__.py +0 -0
  28. {smartbox-2.3.1 → smartbox-2.4.0}/src/smartbox/cmd.py +0 -0
  29. {smartbox-2.3.1 → smartbox-2.4.0}/src/smartbox/error.py +0 -0
  30. {smartbox-2.3.1 → smartbox-2.4.0}/src/smartbox/py.typed +0 -0
  31. {smartbox-2.3.1 → smartbox-2.4.0}/src/smartbox/socket.py +0 -0
  32. {smartbox-2.3.1 → smartbox-2.4.0}/tests/__init__.py +0 -0
  33. {smartbox-2.3.1 → smartbox-2.4.0}/tests/common.py +0 -0
  34. {smartbox-2.3.1 → smartbox-2.4.0}/tests/conftest.py +0 -0
  35. {smartbox-2.3.1 → smartbox-2.4.0}/tests/fixtures/devs/device1/acm/1/samples.json +0 -0
  36. {smartbox-2.3.1 → smartbox-2.4.0}/tests/fixtures/devs/device1/acm/1/setup.json +0 -0
  37. {smartbox-2.3.1 → smartbox-2.4.0}/tests/fixtures/devs/device1/acm/1/status.json +0 -0
  38. {smartbox-2.3.1 → smartbox-2.4.0}/tests/fixtures/devs/device1/connected.json +0 -0
  39. {smartbox-2.3.1 → smartbox-2.4.0}/tests/fixtures/devs/device1/htr/0/samples.json +0 -0
  40. {smartbox-2.3.1 → smartbox-2.4.0}/tests/fixtures/devs/device1/htr/0/setup.json +0 -0
  41. {smartbox-2.3.1 → smartbox-2.4.0}/tests/fixtures/devs/device1/htr/0/status.json +0 -0
  42. {smartbox-2.3.1 → smartbox-2.4.0}/tests/fixtures/devs/device1/htr_system/power_limit.json +0 -0
  43. {smartbox-2.3.1 → smartbox-2.4.0}/tests/fixtures/devs/device1/mgr/away_status.json +0 -0
  44. {smartbox-2.3.1 → smartbox-2.4.0}/tests/fixtures/devs/device1/mgr/nodes.json +0 -0
  45. {smartbox-2.3.1 → smartbox-2.4.0}/tests/fixtures/devs/device1/pmo/3/samples.json +0 -0
  46. {smartbox-2.3.1 → smartbox-2.4.0}/tests/fixtures/devs/device1/pmo/3/setup.json +0 -0
  47. {smartbox-2.3.1 → smartbox-2.4.0}/tests/fixtures/devs/device1/pmo/3/status.json +0 -0
  48. {smartbox-2.3.1 → smartbox-2.4.0}/tests/fixtures/devs/device2/connected.json +0 -0
  49. {smartbox-2.3.1 → smartbox-2.4.0}/tests/fixtures/devs/device2/htr_mod/0/samples.json +0 -0
  50. {smartbox-2.3.1 → smartbox-2.4.0}/tests/fixtures/devs/device2/htr_mod/0/setup.json +0 -0
  51. {smartbox-2.3.1 → smartbox-2.4.0}/tests/fixtures/devs/device2/htr_mod/0/status.json +0 -0
  52. {smartbox-2.3.1 → smartbox-2.4.0}/tests/fixtures/devs/device2/htr_mod/1/samples.json +0 -0
  53. {smartbox-2.3.1 → smartbox-2.4.0}/tests/fixtures/devs/device2/htr_mod/1/setup.json +0 -0
  54. {smartbox-2.3.1 → smartbox-2.4.0}/tests/fixtures/devs/device2/htr_mod/1/status.json +0 -0
  55. {smartbox-2.3.1 → smartbox-2.4.0}/tests/fixtures/devs/device2/htr_mod/2/samples.json +0 -0
  56. {smartbox-2.3.1 → smartbox-2.4.0}/tests/fixtures/devs/device2/htr_mod/2/setup.json +0 -0
  57. {smartbox-2.3.1 → smartbox-2.4.0}/tests/fixtures/devs/device2/htr_mod/2/status.json +0 -0
  58. {smartbox-2.3.1 → smartbox-2.4.0}/tests/fixtures/devs/device2/htr_mod/3/samples.json +0 -0
  59. {smartbox-2.3.1 → smartbox-2.4.0}/tests/fixtures/devs/device2/htr_mod/3/setup.json +0 -0
  60. {smartbox-2.3.1 → smartbox-2.4.0}/tests/fixtures/devs/device2/htr_mod/3/status.json +0 -0
  61. {smartbox-2.3.1 → smartbox-2.4.0}/tests/fixtures/devs/device2/htr_mod/4/samples.json +0 -0
  62. {smartbox-2.3.1 → smartbox-2.4.0}/tests/fixtures/devs/device2/htr_mod/4/setup.json +0 -0
  63. {smartbox-2.3.1 → smartbox-2.4.0}/tests/fixtures/devs/device2/htr_mod/4/status.json +0 -0
  64. {smartbox-2.3.1 → smartbox-2.4.0}/tests/fixtures/devs/device2/htr_mod/5/samples.json +0 -0
  65. {smartbox-2.3.1 → smartbox-2.4.0}/tests/fixtures/devs/device2/htr_mod/5/setup.json +0 -0
  66. {smartbox-2.3.1 → smartbox-2.4.0}/tests/fixtures/devs/device2/htr_mod/5/status.json +0 -0
  67. {smartbox-2.3.1 → smartbox-2.4.0}/tests/fixtures/devs/device2/htr_system/power_limit.json +0 -0
  68. {smartbox-2.3.1 → smartbox-2.4.0}/tests/fixtures/devs/device2/mgr/away_status.json +0 -0
  69. {smartbox-2.3.1 → smartbox-2.4.0}/tests/fixtures/devs/device2/mgr/nodes.json +0 -0
  70. {smartbox-2.3.1 → smartbox-2.4.0}/tests/fixtures/devs.json +0 -0
  71. {smartbox-2.3.1 → smartbox-2.4.0}/tests/fixtures/grouped_devs.json +0 -0
  72. {smartbox-2.3.1 → smartbox-2.4.0}/tests/fixtures/groups/test_home/guest_users.json +0 -0
  73. {smartbox-2.3.1 → smartbox-2.4.0}/tests/fixtures/test_path.json +0 -0
  74. {smartbox-2.3.1 → smartbox-2.4.0}/tests/test_cmd.py +0 -0
  75. {smartbox-2.3.1 → smartbox-2.4.0}/tests/test_models.py +0 -0
  76. {smartbox-2.3.1 → smartbox-2.4.0}/tests/test_session.py +0 -0
@@ -14,7 +14,7 @@ jobs:
14
14
  permissions:
15
15
  id-token: write
16
16
  steps:
17
- - uses: actions/checkout@v5
17
+ - uses: actions/checkout@v6
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.0.0
25
+ uses: actions/setup-python@v6.1.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.13, 3.14]
20
20
  steps:
21
- - uses: actions/checkout@v5
21
+ - uses: actions/checkout@v6
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.0.0
27
+ uses: actions/setup-python@v6.1.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.13, 3.14]
41
41
 
42
42
  steps:
43
- - uses: actions/checkout@v5
43
+ - uses: actions/checkout@v6
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.0.0
49
+ uses: actions/setup-python@v6.1.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.13, 3.14]
63
63
 
64
64
  steps:
65
- - uses: actions/checkout@v5
65
+ - uses: actions/checkout@v6
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.0.0
72
+ uses: actions/setup-python@v6.1.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@v5.5.1
80
+ uses: codecov/codecov-action@v5.5.2
81
81
  with:
82
82
  token: ${{ secrets.CODECOV_TOKEN }}
83
83
  fail_ci_if_error: true
@@ -2,13 +2,13 @@ repos:
2
2
 
3
3
  - repo: https://github.com/astral-sh/uv-pre-commit
4
4
  # uv version.
5
- rev: 0.5.26
5
+ rev: 0.9.25
6
6
  hooks:
7
7
  # Update the uv lockfile
8
8
  - id: uv-lock
9
9
 
10
10
  - repo: https://github.com/pre-commit/pre-commit-hooks
11
- rev: v5.0.0
11
+ rev: v6.0.0
12
12
  hooks:
13
13
  - id: trailing-whitespace
14
14
  - id: end-of-file-fixer
@@ -22,7 +22,7 @@ repos:
22
22
  - "--indent=4"
23
23
 
24
24
  - repo: https://github.com/astral-sh/ruff-pre-commit
25
- rev: v0.9.4
25
+ rev: v0.14.11
26
26
  hooks:
27
27
  - id: ruff
28
28
  args: [--fix, --exit-non-zero-on-fix]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: smartbox
3
- Version: 2.3.1
3
+ Version: 2.4.0
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "smartbox"
3
- version = "2.3.1"
3
+ version = "2.4.0"
4
4
  requires-python = ">=3.13"
5
5
  authors = [
6
6
  { name="Alberto Tudela", email="ajtudela@gmail.com" },
@@ -83,6 +83,13 @@ class NodeSetup(RootModel[DefaultNodeSetup | PmoSetup]):
83
83
  """Get the root model directly."""
84
84
  return getattr(self.root, name)
85
85
 
86
+ class NodeVersion(BaseModel):
87
+ """NodeVersion model."""
88
+
89
+ hw_version: str
90
+ fw_version: str
91
+ uid: str
92
+ pid: str
86
93
 
87
94
  class DefaultNodeStatus(BaseModel):
88
95
  """Default Node Status."""
@@ -166,7 +173,6 @@ class Node(BaseModel):
166
173
  installed: bool
167
174
  lost: bool | None = False
168
175
 
169
-
170
176
  class Nodes(BaseModel):
171
177
  """Nodes model."""
172
178
 
@@ -31,6 +31,12 @@ class AvailableResellers:
31
31
  api_url="api",
32
32
  serial_id=1,
33
33
  ),
34
+ "api-lhz": SmartboxReseller(
35
+ name="Lucht LHZ",
36
+ web_url="https://smartcontrol.lucht-lhz.de/",
37
+ api_url="api-lhz",
38
+ serial_id=2,
39
+ ),
34
40
  "api-ehc": SmartboxReseller(
35
41
  name="Electric Heating Company",
36
42
  web_url="https://control.electric-heatingcompany.co.uk/",
@@ -55,6 +61,12 @@ class AvailableResellers:
55
61
  api_url="api-climastar",
56
62
  serial_id=8,
57
63
  ),
64
+ "api-iheatcontrol": SmartboxReseller(
65
+ name="iHeat Control",
66
+ web_url="https://app.iheatcontrol.com/",
67
+ api_url="api-evconfort",
68
+ serial_id=9,
69
+ ),
58
70
  "api-hjm": SmartboxReseller(
59
71
  name="HJM",
60
72
  web_url="https://api.calorhjm.com/",
@@ -107,7 +119,11 @@ class AvailableResellers:
107
119
  def reseller(self) -> SmartboxReseller:
108
120
  """Get the reseller."""
109
121
  reseller = next(
110
- (r for r in self.resellers.values() if r.api_url == self._api_url),
122
+ (
123
+ value
124
+ for key, value in self.resellers.items()
125
+ if key == self._api_url
126
+ ),
111
127
  None,
112
128
  )
113
129
  if reseller is None:
@@ -192,7 +192,7 @@ class AsyncSession:
192
192
  self.expiry_time,
193
193
  )
194
194
  except ValidationError as e:
195
- msg = "Received invalid auth response"
195
+ msg = f"Received invalid auth response {response.status} with msg: {response.reason}"
196
196
  raise InvalidAuthError(msg) from e
197
197
 
198
198
  async def check_refresh_auth(self) -> None:
@@ -251,6 +251,33 @@ class UpdateManager:
251
251
  update_wrapper,
252
252
  )
253
253
 
254
+ def subscribe_to_node_version(
255
+ self,
256
+ callback: Callable[[str, int, dict[str, Any]], None],
257
+ ) -> None:
258
+ """Subscribe to node version updates."""
259
+
260
+ def dev_data_wrapper(data: dict[str, Any]) -> None:
261
+ (callback(data["type"], int(data["addr"]), data["version"]),) # type: ignore[func-returns-value]
262
+
263
+ self.subscribe_to_dev_data(
264
+ "(.nodes[] | {addr, type, version})?",
265
+ dev_data_wrapper,
266
+ )
267
+
268
+ def update_wrapper(
269
+ data: dict[str, Any],
270
+ node_type: str,
271
+ addr: str,
272
+ ) -> None:
273
+ (callback(node_type, int(addr), data),) # type: ignore[func-returns-value]
274
+
275
+ self.subscribe_to_updates(
276
+ r"^/(?P<node_type>[^/]+)/(?P<addr>\d+)/version",
277
+ self.BODY_PATH,
278
+ update_wrapper,
279
+ )
280
+
254
281
  def _dev_data_cb(self, data: dict[str, Any]) -> None:
255
282
  for sub in self._dev_data_subscriptions:
256
283
  sub.match(data)
@@ -48,10 +48,8 @@ def test_reseller_invalid_data():
48
48
 
49
49
  @pytest.mark.asyncio
50
50
  async def test_all_resellers():
51
- for reseller in AvailableResellers.resellers.values():
52
- _session = AsyncSmartboxSession(
53
- username="", password="", api_name=reseller.api_url
54
- )
51
+ for key in AvailableResellers.resellers:
52
+ _session = AsyncSmartboxSession(username="", password="", api_name=key)
55
53
  check = await _session.health_check()
56
54
  assert check is not None
57
55
  version = await _session.api_version()
@@ -224,3 +224,61 @@ def test_update_manager_subscribe_to_device_connected(update_manager):
224
224
  update_data = {"path": "/connected", "body": {"connected": True}}
225
225
  update_manager._update_cb(update_data)
226
226
  callback.assert_called_once_with(update_data["body"]["connected"])
227
+
228
+
229
+
230
+ def test_update_manager_subscribe_to_node_version(update_manager):
231
+ callback = MagicMock()
232
+ update_manager.subscribe_to_node_version(callback)
233
+ assert len(update_manager._dev_data_subscriptions) == 1
234
+ assert len(update_manager._update_subscriptions) == 1
235
+
236
+ # Test dev data callback
237
+ dev_data = {
238
+ "nodes": [
239
+ {
240
+ "type": "acm",
241
+ "addr": 2,
242
+ "version": {
243
+ "pid": "081c",
244
+ "fw_version": "1.6",
245
+ "hw_version": "1.0",
246
+ "uid": "test123",
247
+ },
248
+ }
249
+ ],
250
+ }
251
+ update_manager._dev_data_cb(dev_data)
252
+ callback.assert_called_once_with(
253
+ "acm",
254
+ 2,
255
+ {
256
+ "pid": "081c",
257
+ "fw_version": "1.6",
258
+ "hw_version": "1.0",
259
+ "uid": "test123",
260
+ },
261
+ )
262
+
263
+ # Test update callback
264
+ callback.reset_mock()
265
+ update_data = {
266
+ "path": "/acm/2/version",
267
+ "body": {
268
+ "pid": "081c",
269
+ "fw_version": "1.7",
270
+ "hw_version": "1.0",
271
+ "uid": "test123",
272
+ },
273
+ }
274
+ update_manager._update_cb(update_data)
275
+ callback.assert_called_once_with(
276
+ "acm",
277
+ 2,
278
+ {
279
+ "pid": "081c",
280
+ "fw_version": "1.7",
281
+ "hw_version": "1.0",
282
+ "uid": "test123",
283
+ },
284
+ )
@@ -1,8 +1,6 @@
1
1
  [tox]
2
2
  envlist =
3
-
4
3
  py313
5
-
6
4
  [testenv]
7
5
  deps =
8
6
  freezegun