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.
- senertec_dachs-0.1.0/LICENSE +21 -0
- senertec_dachs-0.1.0/PKG-INFO +163 -0
- senertec_dachs-0.1.0/README.md +134 -0
- senertec_dachs-0.1.0/pyproject.toml +50 -0
- senertec_dachs-0.1.0/setup.cfg +4 -0
- senertec_dachs-0.1.0/src/senertec_dachs/__init__.py +79 -0
- senertec_dachs-0.1.0/src/senertec_dachs/_reader.py +52 -0
- senertec_dachs-0.1.0/src/senertec_dachs/cloud/__init__.py +1 -0
- senertec_dachs-0.1.0/src/senertec_dachs/cloud/client.py +486 -0
- senertec_dachs-0.1.0/src/senertec_dachs/cloud/protocol.py +73 -0
- senertec_dachs-0.1.0/src/senertec_dachs/data/msr2-local-keys.json +1418 -0
- senertec_dachs-0.1.0/src/senertec_dachs/device.py +586 -0
- senertec_dachs-0.1.0/src/senertec_dachs/errors.py +74 -0
- senertec_dachs-0.1.0/src/senertec_dachs/keys.py +63 -0
- senertec_dachs-0.1.0/src/senertec_dachs/local/__init__.py +1 -0
- senertec_dachs-0.1.0/src/senertec_dachs/local/client.py +213 -0
- senertec_dachs-0.1.0/src/senertec_dachs/local/protocol.py +136 -0
- senertec_dachs-0.1.0/src/senertec_dachs/models.py +444 -0
- senertec_dachs-0.1.0/src/senertec_dachs/py.typed +1 -0
- senertec_dachs-0.1.0/src/senertec_dachs.egg-info/PKG-INFO +163 -0
- senertec_dachs-0.1.0/src/senertec_dachs.egg-info/SOURCES.txt +26 -0
- senertec_dachs-0.1.0/src/senertec_dachs.egg-info/dependency_links.txt +1 -0
- senertec_dachs-0.1.0/src/senertec_dachs.egg-info/requires.txt +5 -0
- senertec_dachs-0.1.0/src/senertec_dachs.egg-info/top_level.txt +1 -0
- senertec_dachs-0.1.0/tests/test_cloud_client.py +554 -0
- senertec_dachs-0.1.0/tests/test_dachs.py +330 -0
- senertec_dachs-0.1.0/tests/test_local_client.py +556 -0
- 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,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."""
|