senertec-dachs 0.1.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 (28) hide show
  1. senertec_dachs-0.1.0/LICENSE +21 -0
  2. senertec_dachs-0.1.0/PKG-INFO +163 -0
  3. senertec_dachs-0.1.0/README.md +134 -0
  4. senertec_dachs-0.1.0/pyproject.toml +50 -0
  5. senertec_dachs-0.1.0/setup.cfg +4 -0
  6. senertec_dachs-0.1.0/src/senertec_dachs/__init__.py +79 -0
  7. senertec_dachs-0.1.0/src/senertec_dachs/_reader.py +52 -0
  8. senertec_dachs-0.1.0/src/senertec_dachs/cloud/__init__.py +1 -0
  9. senertec_dachs-0.1.0/src/senertec_dachs/cloud/client.py +486 -0
  10. senertec_dachs-0.1.0/src/senertec_dachs/cloud/protocol.py +73 -0
  11. senertec_dachs-0.1.0/src/senertec_dachs/data/msr2-local-keys.json +1418 -0
  12. senertec_dachs-0.1.0/src/senertec_dachs/device.py +586 -0
  13. senertec_dachs-0.1.0/src/senertec_dachs/errors.py +74 -0
  14. senertec_dachs-0.1.0/src/senertec_dachs/keys.py +63 -0
  15. senertec_dachs-0.1.0/src/senertec_dachs/local/__init__.py +1 -0
  16. senertec_dachs-0.1.0/src/senertec_dachs/local/client.py +213 -0
  17. senertec_dachs-0.1.0/src/senertec_dachs/local/protocol.py +136 -0
  18. senertec_dachs-0.1.0/src/senertec_dachs/models.py +444 -0
  19. senertec_dachs-0.1.0/src/senertec_dachs/py.typed +1 -0
  20. senertec_dachs-0.1.0/src/senertec_dachs.egg-info/PKG-INFO +163 -0
  21. senertec_dachs-0.1.0/src/senertec_dachs.egg-info/SOURCES.txt +26 -0
  22. senertec_dachs-0.1.0/src/senertec_dachs.egg-info/dependency_links.txt +1 -0
  23. senertec_dachs-0.1.0/src/senertec_dachs.egg-info/requires.txt +5 -0
  24. senertec_dachs-0.1.0/src/senertec_dachs.egg-info/top_level.txt +1 -0
  25. senertec_dachs-0.1.0/tests/test_cloud_client.py +554 -0
  26. senertec_dachs-0.1.0/tests/test_dachs.py +330 -0
  27. senertec_dachs-0.1.0/tests/test_local_client.py +556 -0
  28. senertec_dachs-0.1.0/tests/test_models.py +325 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 skuza.it
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,163 @@
1
+ Metadata-Version: 2.4
2
+ Name: senertec-dachs
3
+ Version: 0.1.0
4
+ Summary: Async Python client for the SenerTec Dachs CHP unit: local MSR2 GLT interface and Dachsportal cloud
5
+ Author: skuza.it
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/skuzait/senertec-dachs
8
+ Project-URL: Repository, https://github.com/skuzait/senertec-dachs
9
+ Project-URL: Issues, https://github.com/skuzait/senertec-dachs/issues
10
+ Keywords: senertec,dachs,msr2,chp,cogeneration,home-assistant
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Framework :: AsyncIO
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Home Automation
20
+ Classifier: Typing :: Typed
21
+ Requires-Python: >=3.11
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Requires-Dist: aiohttp<4,>=3.11
25
+ Provides-Extra: test
26
+ Requires-Dist: pytest<10,>=8.4; extra == "test"
27
+ Requires-Dist: pytest-asyncio<2,>=0.24; extra == "test"
28
+ Dynamic: license-file
29
+
30
+ # senertec-dachs
31
+
32
+ Async Python library for the SenerTec Dachs combined heat and power unit.
33
+
34
+ One `Dachs` device object, two backends, the same API everywhere:
35
+
36
+ - **Local**: the MSR2 GLT interface on your LAN (HTTP, port 8080, Basic
37
+ auth `glt`). Live values, direct writes.
38
+ - **Cloud**: the SenerTec Dachsportal — the backend the official DachsApp
39
+ uses. Works from anywhere; values are the portal's cached polls of the
40
+ device, so freshness is surfaced explicitly (`device_online`,
41
+ `data_fetched_at`).
42
+
43
+ Typed readings for all 118 documented MSR2 keys, honest units
44
+ (unevidenced scales surface as `UNVERIFIED` with the raw value preserved,
45
+ never a guess), and safe writes with `outcome_unknown` semantics —
46
+ uncertain outcomes are never retried blindly.
47
+
48
+ ## Install
49
+
50
+ ```bash
51
+ pip install senertec-dachs
52
+ ```
53
+
54
+ or straight from GitHub:
55
+
56
+ ```bash
57
+ pip install "senertec-dachs @ git+https://github.com/skuzait/senertec-dachs"
58
+ ```
59
+
60
+ Requires Python 3.11+, depends only on aiohttp.
61
+
62
+ ## Local quickstart
63
+
64
+ ```python
65
+ import asyncio
66
+ from senertec_dachs import Dachs
67
+
68
+ async def main() -> None:
69
+ async with Dachs("192.168.1.50") as dachs: # MSR2 address, port 8080 default
70
+ await dachs.update()
71
+
72
+ print(dachs.power_kw) # Decimal or None
73
+ print(dachs.running) # True/False/None
74
+ print(dachs.outdoor_temperature) # °C as Decimal
75
+ print(dachs.electrical_energy_kwh, dachs.thermal_energy_kwh)
76
+ print(dachs.maintenance_due)
77
+
78
+ asyncio.run(main())
79
+ ```
80
+
81
+ `Dachs(host)` accepts an IP or hostname. Properties return `None` unless
82
+ the reading is cleanly available; the ten keys whose scale is still
83
+ unverified have no property — use `dachs.snapshot` (all 118 keys with
84
+ states and raw text) or `dachs.client` (raw reads) for those.
85
+
86
+ ## Cloud quickstart
87
+
88
+ ```python
89
+ async with Dachs.cloud("12345", "portal-password") as dachs:
90
+ await dachs.update()
91
+ print(dachs.power_kw) # same properties as local
92
+ print(dachs.device_online) # could the portal reach the unit?
93
+ print(dachs.data_fetched_at) # UTC timestamp of the portal's last poll
94
+ ```
95
+
96
+ The portal ID is the trailing part of the Dachs serial number; the
97
+ password is the DachsApp password. No local network access needed.
98
+ Cloud extras live on the client:
99
+ `reports()`, `daily_profile()`, `schedules()`, `statistics()`,
100
+ `metainfo()`, `portal_news()`.
101
+
102
+ Differences worth knowing: portal values are cached polls (check
103
+ `device_online`), small rounding differences vs local are normal, and
104
+ the portal's `ulArbeitThermHka` includes the condenser share while the
105
+ local key reports the unit alone.
106
+
107
+ ## Demand operations
108
+
109
+ Both calls work identically on both backends:
110
+
111
+ ```python
112
+ await dachs.request_run() # one-shot "run, please"
113
+ await dachs.request_power(2) # per-module external demand, 0-10
114
+ ```
115
+
116
+ - `request_run()` — local: the documented GLT demand with one module;
117
+ cloud: the app's `dachsAnfordern` (portal default demand).
118
+ - `request_power(n)` — the documented `Stromf_Ew.Anforderung_GLT` write
119
+ on both backends. The cloud path is unconfirmed against a real portal —
120
+ treat its first real use as an experiment.
121
+
122
+ A demand is valid for ten minutes; renew sooner for sustained demand.
123
+ A clean acknowledgement proves acceptance, not that a module started.
124
+ Errors on writes carry `outcome_unknown=True` when the outcome cannot be
125
+ confirmed; nothing is retried automatically.
126
+
127
+ ## Reading states
128
+
129
+ Every reading is a `Reading` with `raw` (wire text), `value`, `state`:
130
+
131
+ | State | Meaning |
132
+ |---|---|
133
+ | `AVAILABLE` | Clean typed value. |
134
+ | `MISSING` | Key absent (optional equipment). |
135
+ | `INVALID` | Malformed wire value; `raw` preserved. |
136
+ | `UNVERIFIED` | Well-formed, but the scale/unit is not evidenced; `raw` preserved. |
137
+ | `UNAVAILABLE` | Device sentinel (temperature sensors return `127` where not connected). |
138
+
139
+ ## MQTT
140
+
141
+ `examples/mqtt_publish.py` publishes a JSON state message on an interval,
142
+ from either backend (`pip install paho-mqtt`, not a library dependency):
143
+
144
+ ```bash
145
+ DACHS_HOST=192.168.1.50 MQTT_HOST=mosquitto.local python examples/mqtt_publish.py
146
+ ```
147
+
148
+ More examples: `examples/read_local.py`, `examples/read_cloud.py`,
149
+ `examples/read_all.py` (full typed dump of all 118 keys).
150
+
151
+ ## Development
152
+
153
+ ```bash
154
+ py -m venv .venv
155
+ .venv\Scripts\pip install -e ".[test]"
156
+ .venv\Scripts\pytest -q
157
+ ```
158
+
159
+ Tests run against loopback fakes — no device, portal, or credentials
160
+ needed. `tests/fixtures/local-protocol.json` doubles as the
161
+ language-neutral protocol contract for the planned Node.js port.
162
+
163
+ MIT license.
@@ -0,0 +1,134 @@
1
+ # senertec-dachs
2
+
3
+ Async Python library for the SenerTec Dachs combined heat and power unit.
4
+
5
+ One `Dachs` device object, two backends, the same API everywhere:
6
+
7
+ - **Local**: the MSR2 GLT interface on your LAN (HTTP, port 8080, Basic
8
+ auth `glt`). Live values, direct writes.
9
+ - **Cloud**: the SenerTec Dachsportal — the backend the official DachsApp
10
+ uses. Works from anywhere; values are the portal's cached polls of the
11
+ device, so freshness is surfaced explicitly (`device_online`,
12
+ `data_fetched_at`).
13
+
14
+ Typed readings for all 118 documented MSR2 keys, honest units
15
+ (unevidenced scales surface as `UNVERIFIED` with the raw value preserved,
16
+ never a guess), and safe writes with `outcome_unknown` semantics —
17
+ uncertain outcomes are never retried blindly.
18
+
19
+ ## Install
20
+
21
+ ```bash
22
+ pip install senertec-dachs
23
+ ```
24
+
25
+ or straight from GitHub:
26
+
27
+ ```bash
28
+ pip install "senertec-dachs @ git+https://github.com/skuzait/senertec-dachs"
29
+ ```
30
+
31
+ Requires Python 3.11+, depends only on aiohttp.
32
+
33
+ ## Local quickstart
34
+
35
+ ```python
36
+ import asyncio
37
+ from senertec_dachs import Dachs
38
+
39
+ async def main() -> None:
40
+ async with Dachs("192.168.1.50") as dachs: # MSR2 address, port 8080 default
41
+ await dachs.update()
42
+
43
+ print(dachs.power_kw) # Decimal or None
44
+ print(dachs.running) # True/False/None
45
+ print(dachs.outdoor_temperature) # °C as Decimal
46
+ print(dachs.electrical_energy_kwh, dachs.thermal_energy_kwh)
47
+ print(dachs.maintenance_due)
48
+
49
+ asyncio.run(main())
50
+ ```
51
+
52
+ `Dachs(host)` accepts an IP or hostname. Properties return `None` unless
53
+ the reading is cleanly available; the ten keys whose scale is still
54
+ unverified have no property — use `dachs.snapshot` (all 118 keys with
55
+ states and raw text) or `dachs.client` (raw reads) for those.
56
+
57
+ ## Cloud quickstart
58
+
59
+ ```python
60
+ async with Dachs.cloud("12345", "portal-password") as dachs:
61
+ await dachs.update()
62
+ print(dachs.power_kw) # same properties as local
63
+ print(dachs.device_online) # could the portal reach the unit?
64
+ print(dachs.data_fetched_at) # UTC timestamp of the portal's last poll
65
+ ```
66
+
67
+ The portal ID is the trailing part of the Dachs serial number; the
68
+ password is the DachsApp password. No local network access needed.
69
+ Cloud extras live on the client:
70
+ `reports()`, `daily_profile()`, `schedules()`, `statistics()`,
71
+ `metainfo()`, `portal_news()`.
72
+
73
+ Differences worth knowing: portal values are cached polls (check
74
+ `device_online`), small rounding differences vs local are normal, and
75
+ the portal's `ulArbeitThermHka` includes the condenser share while the
76
+ local key reports the unit alone.
77
+
78
+ ## Demand operations
79
+
80
+ Both calls work identically on both backends:
81
+
82
+ ```python
83
+ await dachs.request_run() # one-shot "run, please"
84
+ await dachs.request_power(2) # per-module external demand, 0-10
85
+ ```
86
+
87
+ - `request_run()` — local: the documented GLT demand with one module;
88
+ cloud: the app's `dachsAnfordern` (portal default demand).
89
+ - `request_power(n)` — the documented `Stromf_Ew.Anforderung_GLT` write
90
+ on both backends. The cloud path is unconfirmed against a real portal —
91
+ treat its first real use as an experiment.
92
+
93
+ A demand is valid for ten minutes; renew sooner for sustained demand.
94
+ A clean acknowledgement proves acceptance, not that a module started.
95
+ Errors on writes carry `outcome_unknown=True` when the outcome cannot be
96
+ confirmed; nothing is retried automatically.
97
+
98
+ ## Reading states
99
+
100
+ Every reading is a `Reading` with `raw` (wire text), `value`, `state`:
101
+
102
+ | State | Meaning |
103
+ |---|---|
104
+ | `AVAILABLE` | Clean typed value. |
105
+ | `MISSING` | Key absent (optional equipment). |
106
+ | `INVALID` | Malformed wire value; `raw` preserved. |
107
+ | `UNVERIFIED` | Well-formed, but the scale/unit is not evidenced; `raw` preserved. |
108
+ | `UNAVAILABLE` | Device sentinel (temperature sensors return `127` where not connected). |
109
+
110
+ ## MQTT
111
+
112
+ `examples/mqtt_publish.py` publishes a JSON state message on an interval,
113
+ from either backend (`pip install paho-mqtt`, not a library dependency):
114
+
115
+ ```bash
116
+ DACHS_HOST=192.168.1.50 MQTT_HOST=mosquitto.local python examples/mqtt_publish.py
117
+ ```
118
+
119
+ More examples: `examples/read_local.py`, `examples/read_cloud.py`,
120
+ `examples/read_all.py` (full typed dump of all 118 keys).
121
+
122
+ ## Development
123
+
124
+ ```bash
125
+ py -m venv .venv
126
+ .venv\Scripts\pip install -e ".[test]"
127
+ .venv\Scripts\pytest -q
128
+ ```
129
+
130
+ Tests run against loopback fakes — no device, portal, or credentials
131
+ needed. `tests/fixtures/local-protocol.json` doubles as the
132
+ language-neutral protocol contract for the planned Node.js port.
133
+
134
+ MIT license.
@@ -0,0 +1,50 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "senertec-dachs"
7
+ version = "0.1.0"
8
+ description = "Async Python client for the SenerTec Dachs CHP unit: local MSR2 GLT interface and Dachsportal cloud"
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ authors = [{ name = "skuza.it" }]
14
+
15
+ keywords = ["senertec", "dachs", "msr2", "chp", "cogeneration", "home-assistant"]
16
+ classifiers = [
17
+ "Development Status :: 4 - Beta",
18
+ "Framework :: AsyncIO",
19
+ "Intended Audience :: Developers",
20
+ "Operating System :: OS Independent",
21
+ "Programming Language :: Python :: 3",
22
+ "Programming Language :: Python :: 3.11",
23
+ "Programming Language :: Python :: 3.12",
24
+ "Programming Language :: Python :: 3.13",
25
+ "Topic :: Home Automation",
26
+ "Typing :: Typed",
27
+ ]
28
+ dependencies = ["aiohttp>=3.11,<4"]
29
+
30
+ [project.urls]
31
+ Homepage = "https://github.com/skuzait/senertec-dachs"
32
+ Repository = "https://github.com/skuzait/senertec-dachs"
33
+ Issues = "https://github.com/skuzait/senertec-dachs/issues"
34
+
35
+ [project.optional-dependencies]
36
+ test = [
37
+ "pytest>=8.4,<10",
38
+ "pytest-asyncio>=0.24,<2",
39
+ ]
40
+
41
+ [tool.setuptools.packages.find]
42
+ where = ["src"]
43
+
44
+ [tool.setuptools.package-data]
45
+ senertec_dachs = ["data/*.json", "py.typed"]
46
+
47
+ [tool.pytest.ini_options]
48
+ testpaths = ["tests"]
49
+ asyncio_mode = "auto"
50
+ asyncio_default_fixture_loop_scope = "function"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,79 @@
1
+ """Async client library for the SenerTec Dachs MSR2 local GLT interface.
2
+
3
+ Local access uses the documented HTTP interface of the Dachs MSR2
4
+ Ethernet board (SenerTec 11/4798.444.000): HTTP Basic authentication on
5
+ port 8080, ``GET /getKey`` with repeated ``k`` parameters, and
6
+ form-encoded ``POST /setKeys``.
7
+ """
8
+
9
+ from .errors import (
10
+ AuthenticationError,
11
+ ConnectionError,
12
+ DachsError,
13
+ HttpError,
14
+ ProtocolError,
15
+ RequestTimeoutError,
16
+ WriteError,
17
+ )
18
+ from .keys import LOCAL_KEYS
19
+ from .cloud.client import CloudClient, CloudStatus
20
+ from .device import Dachs, ModuleStatus
21
+ from .local.client import LocalClient
22
+ from .models import (
23
+ ActuatorsData,
24
+ DAILY_RUN_SLOT_START_HOURS,
25
+ HydraulicsData,
26
+ IdentityData,
27
+ Key,
28
+ KeyType,
29
+ MaintenanceData,
30
+ MeasurementsData,
31
+ ModuleData,
32
+ ModuleStatisticsData,
33
+ Normalization,
34
+ OperatingData,
35
+ Reading,
36
+ ReadingState,
37
+ SecondGeneratorData,
38
+ Snapshot,
39
+ TemperaturesData,
40
+ TemperatureTargetsData,
41
+ YearEndData,
42
+ )
43
+
44
+ __version__ = "0.1.0"
45
+
46
+ __all__ = [
47
+ "AuthenticationError",
48
+ "ConnectionError",
49
+ "DachsError",
50
+ "HttpError",
51
+ "ProtocolError",
52
+ "RequestTimeoutError",
53
+ "WriteError",
54
+ "LOCAL_KEYS",
55
+ "Dachs",
56
+ "CloudClient",
57
+ "CloudStatus",
58
+ "ModuleStatus",
59
+ "LocalClient",
60
+ "ActuatorsData",
61
+ "DAILY_RUN_SLOT_START_HOURS",
62
+ "HydraulicsData",
63
+ "IdentityData",
64
+ "Key",
65
+ "KeyType",
66
+ "MaintenanceData",
67
+ "MeasurementsData",
68
+ "ModuleData",
69
+ "ModuleStatisticsData",
70
+ "Normalization",
71
+ "OperatingData",
72
+ "Reading",
73
+ "ReadingState",
74
+ "SecondGeneratorData",
75
+ "Snapshot",
76
+ "TemperaturesData",
77
+ "TemperatureTargetsData",
78
+ "YearEndData",
79
+ ]
@@ -0,0 +1,52 @@
1
+ """Shared reading logic used by both the local and the cloud client."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Sequence
6
+ from typing import Any
7
+
8
+ from .keys import LOCAL_KEYS
9
+ from .models import Reading, Snapshot
10
+
11
+
12
+ def normalize_requested_keys(keys: Sequence[str]) -> list[str]:
13
+ """Validate keys and deduplicate while retaining first-seen order."""
14
+ requested: list[str] = []
15
+ seen: set[str] = set()
16
+ for key in keys:
17
+ if not isinstance(key, str) or key == "":
18
+ raise ValueError("keys must be non-empty strings")
19
+ if any(character.isspace() for character in key):
20
+ raise ValueError(f"keys must not contain whitespace: {key!r}")
21
+ if key not in seen:
22
+ seen.add(key)
23
+ requested.append(key)
24
+ return requested
25
+
26
+
27
+ class SnapshotReaderMixin:
28
+ """Adds ``read_readings``/``read_snapshot`` to any client with ``read_keys``.
29
+
30
+ Both backends return raw wire-text values from ``read_keys``; the typed
31
+ model on top of that is shared, so a snapshot means the same thing
32
+ regardless of where the values came from.
33
+ """
34
+
35
+ async def read_readings(self, keys: Sequence[str]) -> dict[str, Reading[Any]]:
36
+ """Read the given documented keys as typed :class:`Reading` objects."""
37
+ requested = normalize_requested_keys(keys)
38
+ unknown = [key for key in requested if key not in LOCAL_KEYS]
39
+ if unknown:
40
+ raise ValueError(
41
+ f"Key is not in the documented catalogue "
42
+ f"(use read_keys for raw access): {unknown[0]!r}"
43
+ )
44
+ raw = await self.read_keys(requested)
45
+ return {
46
+ key: Reading.from_raw(LOCAL_KEYS[key], raw.get(key)) for key in requested
47
+ }
48
+
49
+ async def read_snapshot(self) -> Snapshot:
50
+ """Read all documented keys and return them as one typed snapshot."""
51
+ raw = await self.read_keys(list(LOCAL_KEYS))
52
+ return Snapshot.from_raw(raw)
@@ -0,0 +1 @@
1
+ """Cloud (Dachsportal) transport for the SenerTec Dachs."""