pyocat 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.
- pyocat-0.1.0/LICENSE +21 -0
- pyocat-0.1.0/PKG-INFO +34 -0
- pyocat-0.1.0/README.md +17 -0
- pyocat-0.1.0/pyocat/__init__.py +5 -0
- pyocat-0.1.0/pyocat/api_client.py +193 -0
- pyocat-0.1.0/pyocat/async_api_client.py +193 -0
- pyocat-0.1.0/pyocat/async_auth.py +63 -0
- pyocat-0.1.0/pyocat/auth.py +63 -0
- pyocat-0.1.0/pyocat/models/__init__.py +7 -0
- pyocat-0.1.0/pyocat/models/event_response.py +34 -0
- pyocat-0.1.0/pyocat/models/measurement_response.py +27 -0
- pyocat-0.1.0/pyocat/models/message_response.py +23 -0
- pyocat-0.1.0/pyocat/models/mode_response.py +21 -0
- pyocat-0.1.0/pyocat/models/state_response.py +44 -0
- pyocat-0.1.0/pyocat/models/statistics_response.py +35 -0
- pyocat-0.1.0/pyocat/models/water_protection_response.py +19 -0
- pyocat-0.1.0/pyproject.toml +27 -0
pyocat-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 WATERCryst GmbH
|
|
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.
|
pyocat-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyocat
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary:
|
|
5
|
+
License-File: LICENSE
|
|
6
|
+
Author: Mathias Hörtnagl
|
|
7
|
+
Author-email: mathias.hoertnagl@watercryst.com
|
|
8
|
+
Requires-Python: >=3.12
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
13
|
+
Requires-Dist: httpx (>=0.28.1,<0.29.0)
|
|
14
|
+
Requires-Dist: pydantic (>=2.13.3,<3.0.0)
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+
Execute to download dependencies:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
poetry install
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Execute to run the unit tests:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
poetry run pytest
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Execute to build the library:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
poetry build
|
|
33
|
+
```
|
|
34
|
+
|
pyocat-0.1.0/README.md
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
from typing import Literal
|
|
2
|
+
|
|
3
|
+
from .auth import Auth
|
|
4
|
+
|
|
5
|
+
from .models import MeasurementResponse
|
|
6
|
+
from .models import StatisticsResponse
|
|
7
|
+
from .models import StateResponse
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ApiClient:
|
|
11
|
+
"""
|
|
12
|
+
Synchronous Biocat REST API v1 client.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def __init__(self, auth: Auth):
|
|
16
|
+
self.auth = auth
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def acknowledge_event(self):
|
|
20
|
+
"""
|
|
21
|
+
Acknowledges the current device warning or error.
|
|
22
|
+
"""
|
|
23
|
+
self.auth.get('v1/ackevent')
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def enable_absence(self):
|
|
27
|
+
"""
|
|
28
|
+
Enables absence mode and raises leakage detector sensitivity.
|
|
29
|
+
"""
|
|
30
|
+
self.auth.get('v1/absence/enable')
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def disable_absence(self):
|
|
34
|
+
"""
|
|
35
|
+
Disables absence mode and reverts leakage detector
|
|
36
|
+
sensitivity to its default level.
|
|
37
|
+
"""
|
|
38
|
+
self.auth.get('v1/absence/disable')
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def pause_leakage_protection(
|
|
42
|
+
self,
|
|
43
|
+
minutes: int
|
|
44
|
+
):
|
|
45
|
+
"""
|
|
46
|
+
Pauses leakage protection for a given duration of minutes.
|
|
47
|
+
|
|
48
|
+
Parameters
|
|
49
|
+
----------
|
|
50
|
+
minutes : int
|
|
51
|
+
The pause duration in minutes. Must be within the range
|
|
52
|
+
[1 .. 4320].
|
|
53
|
+
"""
|
|
54
|
+
self.auth.get('v1/leakageprotection/pause', { 'minutes': minutes })
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def unpause_leakage_protection(self):
|
|
58
|
+
"""
|
|
59
|
+
Reactivates leakage protection.
|
|
60
|
+
"""
|
|
61
|
+
self.auth.get('v1/leakageprotection/unpause')
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def start_self_test(self):
|
|
65
|
+
"""
|
|
66
|
+
Starts the self test routine. Automatically checks all
|
|
67
|
+
actuators and sensors and fills the active unit with drinking
|
|
68
|
+
water over a defined flushing time.
|
|
69
|
+
|
|
70
|
+
Requires 2 minutes for completion.
|
|
71
|
+
|
|
72
|
+
The device will return to water treatment if no errors could
|
|
73
|
+
be detected.
|
|
74
|
+
|
|
75
|
+
In the case of errors, the current error will be reported
|
|
76
|
+
via the webhook endpoint. Additionally it can be queried
|
|
77
|
+
at any time with `GET v1/state`.
|
|
78
|
+
"""
|
|
79
|
+
self.auth.get('v1/selftest')
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def get_measurements(self) -> MeasurementResponse:
|
|
83
|
+
"""
|
|
84
|
+
Fetches current measurement data.
|
|
85
|
+
|
|
86
|
+
Returns
|
|
87
|
+
-------
|
|
88
|
+
The current measurement data.
|
|
89
|
+
"""
|
|
90
|
+
response = self.auth.get('v1/measurements/direct')
|
|
91
|
+
return MeasurementResponse.model_validate(response.json())
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def start_micro_leakage_measurement(self):
|
|
95
|
+
"""
|
|
96
|
+
Starts the micro-leakage measurement to check the leak-
|
|
97
|
+
tightness of the piping. This allows the detection of micro-
|
|
98
|
+
leaks such as dripping taps or pipe fittings.
|
|
99
|
+
For this measurements, water supply is briefly shut off.
|
|
100
|
+
|
|
101
|
+
An unexpected water consumption during the measuring process,
|
|
102
|
+
e.g. flushing the toilet or opening a tap, is automatically
|
|
103
|
+
detected and the water supply is restored within a few seconds.
|
|
104
|
+
However, the test fails and the API call has to be repeated.
|
|
105
|
+
|
|
106
|
+
### Notice
|
|
107
|
+
|
|
108
|
+
If you use a drip irrigation system in your household, this
|
|
109
|
+
can be detected as a micro leak.
|
|
110
|
+
"""
|
|
111
|
+
self.auth.get('v1/mlmeasurement/start')
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def get_daily_statistics(self) -> StatisticsResponse:
|
|
115
|
+
"""
|
|
116
|
+
Fetches the daily statistics.
|
|
117
|
+
|
|
118
|
+
Returns
|
|
119
|
+
-------
|
|
120
|
+
Water consumption statistics of the trailing 30 days.
|
|
121
|
+
"""
|
|
122
|
+
response = self.auth.get('v1/statistics/daily/direct')
|
|
123
|
+
return StatisticsResponse.model_validate(response.json())
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def get_todays_consumption(self) -> float:
|
|
127
|
+
"""
|
|
128
|
+
Fetches the total consumption for today.
|
|
129
|
+
|
|
130
|
+
Returns
|
|
131
|
+
-------
|
|
132
|
+
Todays total water consumption in [L].
|
|
133
|
+
"""
|
|
134
|
+
response = self.auth.get('v1/statistics/cumulative/daily')
|
|
135
|
+
return float(response.json())
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def get_total_consumption(self) -> float:
|
|
139
|
+
"""
|
|
140
|
+
Fetches the total water consumption since the device was
|
|
141
|
+
installed.
|
|
142
|
+
|
|
143
|
+
Returns
|
|
144
|
+
-------
|
|
145
|
+
Total water consumption in [L].
|
|
146
|
+
"""
|
|
147
|
+
response = self.auth.get('v1/statistics/cumulative/total')
|
|
148
|
+
return float(response.json())
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def open_water_supply(self):
|
|
152
|
+
"""
|
|
153
|
+
Connects the device with the water supply.
|
|
154
|
+
Confirms pending warnings and errors.
|
|
155
|
+
"""
|
|
156
|
+
self.auth.get('v1/watersupply/open')
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def close_water_supply(self):
|
|
160
|
+
"""
|
|
161
|
+
Disconnects the device from the water supply.
|
|
162
|
+
"""
|
|
163
|
+
self.auth.get('v1/watersupply/close')
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def get_state(
|
|
167
|
+
self,
|
|
168
|
+
locale: str = 'de',
|
|
169
|
+
format: Literal['plain', 'md', 'html'] = 'plain'
|
|
170
|
+
) -> StateResponse:
|
|
171
|
+
"""
|
|
172
|
+
Returns the current state of the device.
|
|
173
|
+
|
|
174
|
+
Parameters
|
|
175
|
+
----------
|
|
176
|
+
locale : str
|
|
177
|
+
The language of the event message. Defaults to `de`.
|
|
178
|
+
format : `plain` | `md` | `html`
|
|
179
|
+
The format of the event message. Defaults to `plain`.
|
|
180
|
+
* `plain` - Renders the event message as plain text.
|
|
181
|
+
* `md` - Renders the event message as Markdown.
|
|
182
|
+
* `html` - Renders the event message as HTML.
|
|
183
|
+
|
|
184
|
+
Returns
|
|
185
|
+
-------
|
|
186
|
+
The current device state.
|
|
187
|
+
"""
|
|
188
|
+
response = self.auth.get(
|
|
189
|
+
path='v1/state',
|
|
190
|
+
params={ 'locale': locale, 'format': format }
|
|
191
|
+
)
|
|
192
|
+
return StateResponse.model_validate(response.json())
|
|
193
|
+
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
from typing import Literal
|
|
2
|
+
|
|
3
|
+
from .async_auth import AsyncAuth
|
|
4
|
+
|
|
5
|
+
from .models import MeasurementResponse
|
|
6
|
+
from .models import StatisticsResponse
|
|
7
|
+
from .models import StateResponse
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class AsyncApiClient:
|
|
11
|
+
"""
|
|
12
|
+
Asynchronous Biocat REST API v1 client.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def __init__(self, auth: AsyncAuth):
|
|
16
|
+
self.auth = auth
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
async def acknowledge_event(self):
|
|
20
|
+
"""
|
|
21
|
+
Acknowledges the current device warning or error.
|
|
22
|
+
"""
|
|
23
|
+
await self.auth.get('v1/ackevent')
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
async def enable_absence(self):
|
|
27
|
+
"""
|
|
28
|
+
Enables absence mode and raises leakage detector sensitivity.
|
|
29
|
+
"""
|
|
30
|
+
await self.auth.get('v1/absence/enable')
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
async def disable_absence(self):
|
|
34
|
+
"""
|
|
35
|
+
Disables absence mode and reverts leakage detector
|
|
36
|
+
sensitivity to its default level.
|
|
37
|
+
"""
|
|
38
|
+
await self.auth.get('v1/absence/disable')
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
async def pause_leakage_protection(
|
|
42
|
+
self,
|
|
43
|
+
minutes: int
|
|
44
|
+
):
|
|
45
|
+
"""
|
|
46
|
+
Pauses leakage protection for a given duration of minutes.
|
|
47
|
+
|
|
48
|
+
Parameters
|
|
49
|
+
----------
|
|
50
|
+
minutes : int
|
|
51
|
+
The pause duration in minutes. Must be within the range
|
|
52
|
+
[1 .. 4320].
|
|
53
|
+
"""
|
|
54
|
+
await self.auth.get('v1/leakageprotection/pause', { 'minutes': minutes })
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
async def unpause_leakage_protection(self):
|
|
58
|
+
"""
|
|
59
|
+
Reactivates leakage protection.
|
|
60
|
+
"""
|
|
61
|
+
await self.auth.get('v1/leakageprotection/unpause')
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
async def start_self_test(self):
|
|
65
|
+
"""
|
|
66
|
+
Starts the self test routine. Automatically checks all
|
|
67
|
+
actuators and sensors and fills the active unit with drinking
|
|
68
|
+
water over a defined flushing time.
|
|
69
|
+
|
|
70
|
+
Requires 2 minutes for completion.
|
|
71
|
+
|
|
72
|
+
The device will return to water treatment if no errors could
|
|
73
|
+
be detected.
|
|
74
|
+
|
|
75
|
+
In the case of errors, the current error will be reported
|
|
76
|
+
via the webhook endpoint. Additionally it can be queried
|
|
77
|
+
at any time with `GET v1/state`.
|
|
78
|
+
"""
|
|
79
|
+
await self.auth.get('v1/selftest')
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
async def get_measurements(self) -> MeasurementResponse:
|
|
83
|
+
"""
|
|
84
|
+
Fetches current measurement data.
|
|
85
|
+
|
|
86
|
+
Returns
|
|
87
|
+
-------
|
|
88
|
+
The current measurement data.
|
|
89
|
+
"""
|
|
90
|
+
response = await self.auth.get('v1/measurements/direct')
|
|
91
|
+
return MeasurementResponse.model_validate(response.json())
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
async def start_micro_leakage_measurement(self):
|
|
95
|
+
"""
|
|
96
|
+
Starts the micro-leakage measurement to check the leak-
|
|
97
|
+
tightness of the piping. This allows the detection of micro-
|
|
98
|
+
leaks such as dripping taps or pipe fittings.
|
|
99
|
+
For this measurements, water supply is briefly shut off.
|
|
100
|
+
|
|
101
|
+
An unexpected water consumption during the measuring process,
|
|
102
|
+
e.g. flushing the toilet or opening a tap, is automatically
|
|
103
|
+
detected and the water supply is restored within a few seconds.
|
|
104
|
+
However, the test fails and the API call has to be repeated.
|
|
105
|
+
|
|
106
|
+
### Notice
|
|
107
|
+
|
|
108
|
+
If you use a drip irrigation system in your household, this
|
|
109
|
+
can be detected as a micro leak.
|
|
110
|
+
"""
|
|
111
|
+
await self.auth.get('v1/mlmeasurement/start')
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
async def get_daily_statistics(self) -> StatisticsResponse:
|
|
115
|
+
"""
|
|
116
|
+
Fetches the daily statistics.
|
|
117
|
+
|
|
118
|
+
Returns
|
|
119
|
+
-------
|
|
120
|
+
Water consumption statistics of the trailing 30 days.
|
|
121
|
+
"""
|
|
122
|
+
response = await self.auth.get('v1/statistics/daily/direct')
|
|
123
|
+
return StatisticsResponse.model_validate(response.json())
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
async def get_todays_consumption(self) -> float:
|
|
127
|
+
"""
|
|
128
|
+
Fetches the total consumption for today.
|
|
129
|
+
|
|
130
|
+
Returns
|
|
131
|
+
-------
|
|
132
|
+
Todays total water consumption in [L].
|
|
133
|
+
"""
|
|
134
|
+
response = await self.auth.get('v1/statistics/cumulative/daily')
|
|
135
|
+
return float(response.json())
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
async def get_total_consumption(self) -> float:
|
|
139
|
+
"""
|
|
140
|
+
Fetches the total water consumption since the device was
|
|
141
|
+
installed.
|
|
142
|
+
|
|
143
|
+
Returns
|
|
144
|
+
-------
|
|
145
|
+
Total water consumption in [L].
|
|
146
|
+
"""
|
|
147
|
+
response = await self.auth.get('v1/statistics/cumulative/total')
|
|
148
|
+
return float(response.json())
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
async def open_water_supply(self):
|
|
152
|
+
"""
|
|
153
|
+
Connects the device with the water supply.
|
|
154
|
+
Confirms pending warnings and errors.
|
|
155
|
+
"""
|
|
156
|
+
await self.auth.get('v1/watersupply/open')
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
async def close_water_supply(self):
|
|
160
|
+
"""
|
|
161
|
+
Disconnects the device from the water supply.
|
|
162
|
+
"""
|
|
163
|
+
await self.auth.get('v1/watersupply/close')
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
async def get_state(
|
|
167
|
+
self,
|
|
168
|
+
locale: str = 'de',
|
|
169
|
+
format: Literal['plain', 'md', 'html'] = 'plain'
|
|
170
|
+
) -> StateResponse:
|
|
171
|
+
"""
|
|
172
|
+
Returns the current state of the device.
|
|
173
|
+
|
|
174
|
+
Parameters
|
|
175
|
+
----------
|
|
176
|
+
locale : str
|
|
177
|
+
The language of the event message. Defaults to `de`.
|
|
178
|
+
format : `plain` | `md` | `html`
|
|
179
|
+
The format of the event message. Defaults to `plain`.
|
|
180
|
+
* `plain` - Renders the event message as plain text.
|
|
181
|
+
* `md` - Renders the event message as Markdown.
|
|
182
|
+
* `html` - Renders the event message as HTML.
|
|
183
|
+
|
|
184
|
+
Returns
|
|
185
|
+
-------
|
|
186
|
+
The current device state.
|
|
187
|
+
"""
|
|
188
|
+
response = await self.auth.get(
|
|
189
|
+
path='v1/state',
|
|
190
|
+
params={ 'locale': locale, 'format': format }
|
|
191
|
+
)
|
|
192
|
+
return StateResponse.model_validate(response.json())
|
|
193
|
+
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
from typing import Mapping, Union
|
|
2
|
+
|
|
3
|
+
from httpx import AsyncClient, Response
|
|
4
|
+
|
|
5
|
+
class AsyncAuth:
|
|
6
|
+
"""
|
|
7
|
+
Make asynchronous authenticated requests.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
def __init__(
|
|
11
|
+
self,
|
|
12
|
+
client: AsyncClient,
|
|
13
|
+
api_key: str,
|
|
14
|
+
host: str = 'https://appapi.watercryst.com'
|
|
15
|
+
):
|
|
16
|
+
"""
|
|
17
|
+
Creates a new authenticated request sender.
|
|
18
|
+
|
|
19
|
+
Parameters
|
|
20
|
+
----------
|
|
21
|
+
client: `httpx.AsyncClient`
|
|
22
|
+
Instance of an asynchronous HTTP client.
|
|
23
|
+
api_key : str
|
|
24
|
+
The api key.
|
|
25
|
+
host : str
|
|
26
|
+
The Biocat SmartHome API host.
|
|
27
|
+
Defaults to `https://appapi.watercryst.com`.
|
|
28
|
+
"""
|
|
29
|
+
self.client = client
|
|
30
|
+
self.api_key = api_key
|
|
31
|
+
self.host = host
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
async def get(
|
|
35
|
+
self,
|
|
36
|
+
path: str,
|
|
37
|
+
params: Mapping[str, Union[str, int, float, bool]] | None = None,
|
|
38
|
+
) -> Response:
|
|
39
|
+
"""
|
|
40
|
+
Send an asynchronous `GET` request.
|
|
41
|
+
|
|
42
|
+
Parameters
|
|
43
|
+
----------
|
|
44
|
+
path : str
|
|
45
|
+
The relative path.
|
|
46
|
+
params : Mapping[str, Union[str, int, float, bool]]
|
|
47
|
+
Optional query parameters.
|
|
48
|
+
|
|
49
|
+
Returns
|
|
50
|
+
-------
|
|
51
|
+
An `httpx.Response`.
|
|
52
|
+
"""
|
|
53
|
+
headers = dict[str, str]()
|
|
54
|
+
headers['X-API-KEY'] = self.api_key
|
|
55
|
+
|
|
56
|
+
response = await self.client.get(
|
|
57
|
+
url=f'{self.host}/{path}',
|
|
58
|
+
params=params,
|
|
59
|
+
headers=headers
|
|
60
|
+
)
|
|
61
|
+
response.raise_for_status()
|
|
62
|
+
|
|
63
|
+
return response
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
from typing import Mapping, Union
|
|
2
|
+
|
|
3
|
+
from httpx import Client, Response
|
|
4
|
+
|
|
5
|
+
class Auth:
|
|
6
|
+
"""
|
|
7
|
+
Make authenticated requests.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
def __init__(
|
|
11
|
+
self,
|
|
12
|
+
client: Client,
|
|
13
|
+
api_key: str,
|
|
14
|
+
host: str = 'https://appapi.watercryst.com'
|
|
15
|
+
):
|
|
16
|
+
"""
|
|
17
|
+
Creates a new authenticated request sender.
|
|
18
|
+
|
|
19
|
+
Parameters
|
|
20
|
+
----------
|
|
21
|
+
client: `httpx.Client`
|
|
22
|
+
Instance of a synchronous HTTP client.
|
|
23
|
+
api_key : str
|
|
24
|
+
The api key.
|
|
25
|
+
host : str
|
|
26
|
+
The Biocat SmartHome API host.
|
|
27
|
+
Defaults to `https://appapi.watercryst.com`.
|
|
28
|
+
"""
|
|
29
|
+
self.client = client
|
|
30
|
+
self.api_key = api_key
|
|
31
|
+
self.host = host
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def get(
|
|
35
|
+
self,
|
|
36
|
+
path: str,
|
|
37
|
+
params: Mapping[str, Union[str, int, float, bool]] | None = None,
|
|
38
|
+
) -> Response:
|
|
39
|
+
"""
|
|
40
|
+
Send a `GET` request.
|
|
41
|
+
|
|
42
|
+
Parameters
|
|
43
|
+
----------
|
|
44
|
+
path : str
|
|
45
|
+
The relative path.
|
|
46
|
+
params : Mapping[str, Union[str, int, float, bool]]
|
|
47
|
+
Optional query parameters.
|
|
48
|
+
|
|
49
|
+
Returns
|
|
50
|
+
-------
|
|
51
|
+
An `httpx.Response`.
|
|
52
|
+
"""
|
|
53
|
+
headers = dict[str, str]()
|
|
54
|
+
headers['X-API-KEY'] = self.api_key
|
|
55
|
+
|
|
56
|
+
response = self.client.get(
|
|
57
|
+
url=f'{self.host}/{path}',
|
|
58
|
+
params=params,
|
|
59
|
+
headers=headers
|
|
60
|
+
)
|
|
61
|
+
response.raise_for_status()
|
|
62
|
+
|
|
63
|
+
return response
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
from .event_response import EventResponse, EventCategory # type: ignore
|
|
2
|
+
from .measurement_response import MeasurementResponse # type: ignore
|
|
3
|
+
from .message_response import MessageResponse # type: ignore
|
|
4
|
+
from .mode_response import ModeResponse, ModeId # type: ignore
|
|
5
|
+
from .state_response import StateResponse, MlState # type: ignore
|
|
6
|
+
from .statistics_response import StatisticsResponse # type: ignore
|
|
7
|
+
from .water_protection_response import WaterProtectionResponse # type: ignore
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from typing import Annotated, Literal
|
|
3
|
+
from pydantic import BaseModel, Field
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
EventCategory = Literal['error', 'warning', 'info']
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class EventResponse(BaseModel):
|
|
10
|
+
"""
|
|
11
|
+
Represents an event.
|
|
12
|
+
|
|
13
|
+
Attributes
|
|
14
|
+
----------
|
|
15
|
+
type : str
|
|
16
|
+
Denotes the type of the response.
|
|
17
|
+
event_id : int
|
|
18
|
+
Identifies the type of the event.
|
|
19
|
+
category : Literal['error', 'warning', 'info']
|
|
20
|
+
The event category.
|
|
21
|
+
title : str
|
|
22
|
+
Event summary.
|
|
23
|
+
description : str
|
|
24
|
+
Detailed description.
|
|
25
|
+
timestamp : datetime
|
|
26
|
+
UTC date time of the event.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
type: Annotated[str, Field(alias='type')]
|
|
30
|
+
event_id: Annotated[int, Field(alias='eventId')]
|
|
31
|
+
category: Annotated[EventCategory, Field(alias='category')]
|
|
32
|
+
title: Annotated[str, Field(alias='title')]
|
|
33
|
+
description: Annotated[str, Field(alias='description')]
|
|
34
|
+
timestamp: Annotated[datetime, Field(alias='timestamp')]
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from typing import Annotated, Union
|
|
2
|
+
from pydantic import BaseModel, Field
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class MeasurementResponse(BaseModel):
|
|
6
|
+
"""
|
|
7
|
+
Represents current measurement data.
|
|
8
|
+
|
|
9
|
+
Attributes
|
|
10
|
+
----------
|
|
11
|
+
water_temp : int
|
|
12
|
+
Water temperature in degree celsius [°C].
|
|
13
|
+
pressure : float
|
|
14
|
+
Pressure in [bar].
|
|
15
|
+
flow_rate : float
|
|
16
|
+
Flow rate in liters per minute [L/min].
|
|
17
|
+
last_water_tap_volume : float
|
|
18
|
+
Volume of the last water tapping in liters [L].
|
|
19
|
+
last_water_tap_duration : float
|
|
20
|
+
Duration of the last water tapping in seconds [sec].
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
water_temp: Annotated[Union[int, None], Field(alias='waterTemp')] = None
|
|
24
|
+
pressure: Annotated[Union[float, None], Field(alias='pressure')] = None
|
|
25
|
+
flow_rate: Annotated[Union[float, None], Field(alias='flowRate')] = None
|
|
26
|
+
last_water_tap_volume: Annotated[Union[float, None], Field(alias='lastWaterTapVolume')] = None
|
|
27
|
+
last_water_tap_duration: Annotated[Union[float, None], Field(alias='lastWaterTapDuration')] = None
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from typing import Annotated
|
|
3
|
+
from pydantic import BaseModel, Field
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class MessageResponse(BaseModel):
|
|
7
|
+
"""
|
|
8
|
+
Represents a message.
|
|
9
|
+
|
|
10
|
+
Attributes
|
|
11
|
+
----------
|
|
12
|
+
type : str
|
|
13
|
+
Denotes the type of the response.
|
|
14
|
+
absence_mode_enabled : bool
|
|
15
|
+
Indicates the state of the absence mode.
|
|
16
|
+
pause_leakage_protection_until_utc : datetime
|
|
17
|
+
UTC date time string when the leakage protection
|
|
18
|
+
will be active again.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
type: Annotated[str, Field(alias='type')]
|
|
22
|
+
absence_mode_enabled: Annotated[bool, Field(alias='absenceModeEnabled')]
|
|
23
|
+
pause_leakage_protection_until_utc: Annotated[datetime, Field(alias='pauseLeakageProtectionUntilUTC')]
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from typing import Annotated, Literal
|
|
2
|
+
from pydantic import BaseModel, Field
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
ModeId = Literal['ER', 'FS', 'MC', 'RS', 'ST', 'TD', 'UD', 'WO', 'WT']
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ModeResponse(BaseModel):
|
|
9
|
+
"""
|
|
10
|
+
Represents the current mode of operation.
|
|
11
|
+
|
|
12
|
+
Attributes
|
|
13
|
+
----------
|
|
14
|
+
id : Literal['ER', 'FS', 'MC', 'RS', 'ST', 'TD', 'UD', 'WO', 'WT']
|
|
15
|
+
Mode identifier.
|
|
16
|
+
name : str
|
|
17
|
+
Mode display name.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
id: Annotated[ModeId, Field(alias='id')]
|
|
21
|
+
name: Annotated[str, Field(alias='name')]
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from typing import Annotated, Literal, Union
|
|
2
|
+
from pydantic import BaseModel, Field
|
|
3
|
+
|
|
4
|
+
from .event_response import EventResponse
|
|
5
|
+
from .mode_response import ModeResponse
|
|
6
|
+
from .water_protection_response import WaterProtectionResponse
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
MlState = Literal[
|
|
10
|
+
"cancelled",
|
|
11
|
+
"failure-pressure-drop",
|
|
12
|
+
"failure-start-pressure",
|
|
13
|
+
"failure-unknown",
|
|
14
|
+
"failure-water-tap",
|
|
15
|
+
"idle",
|
|
16
|
+
"leakage",
|
|
17
|
+
"running",
|
|
18
|
+
"success"
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class StateResponse(BaseModel):
|
|
23
|
+
"""
|
|
24
|
+
Represents the current device state.
|
|
25
|
+
|
|
26
|
+
Attributes
|
|
27
|
+
----------
|
|
28
|
+
online : bool
|
|
29
|
+
Indicates whether the device is online or offline right now.
|
|
30
|
+
mode : ModeResponse
|
|
31
|
+
Represents the current mode of operation.
|
|
32
|
+
event : EventResponse
|
|
33
|
+
Represents an event.
|
|
34
|
+
water_protection : WaterProtectionResponse | None
|
|
35
|
+
Represents the current state of the water protection subsystem.
|
|
36
|
+
ml_state : Literal["cancelled", "failure-pressure-drop", "failure-start-pressure", "failure-unknown", "failure-water-tap", "idle", "leakage", "running", "success"] | None
|
|
37
|
+
The state of the current (or last) micro leakage measurement.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
online: Annotated[bool, Field(alias='online')]
|
|
41
|
+
mode: Annotated[ModeResponse, Field(alias='mode')]
|
|
42
|
+
event: Annotated[EventResponse, Field(alias='event')]
|
|
43
|
+
water_protection: Annotated[Union[WaterProtectionResponse, None], Field(alias='waterProtection')] = None
|
|
44
|
+
ml_state: Annotated[Union[MlState, None], Field(alias='mlState')] = None
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from typing import Annotated
|
|
3
|
+
from pydantic import BaseModel, Field
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class StatisticsResponseEntry(BaseModel):
|
|
7
|
+
"""
|
|
8
|
+
A consumption statistics data point.
|
|
9
|
+
|
|
10
|
+
Attributes
|
|
11
|
+
----------
|
|
12
|
+
consumption : float
|
|
13
|
+
Water consumption for this day in liters [L].
|
|
14
|
+
date : datetime
|
|
15
|
+
UTC date of the measurement.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
consumption: Annotated[float, Field(alias='consumption')]
|
|
19
|
+
date: Annotated[datetime, Field(alias='date')]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class StatisticsResponse(BaseModel):
|
|
23
|
+
"""
|
|
24
|
+
Represents a list of consumption statistics data points.
|
|
25
|
+
|
|
26
|
+
Attributes
|
|
27
|
+
----------
|
|
28
|
+
type : str
|
|
29
|
+
Denotes the type of the response.
|
|
30
|
+
entries : list[StatisticsResponseEntry]
|
|
31
|
+
List of data points.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
type: Annotated[str, Field(alias='type')]
|
|
35
|
+
entries: Annotated[list[StatisticsResponseEntry], Field(alias='entries')] = []
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from typing import Annotated
|
|
3
|
+
from pydantic import BaseModel, Field
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class WaterProtectionResponse(BaseModel):
|
|
7
|
+
"""
|
|
8
|
+
Represents the current state of the water protection subsystem.
|
|
9
|
+
|
|
10
|
+
Attributes
|
|
11
|
+
----------
|
|
12
|
+
absence_mode_enabled : bool
|
|
13
|
+
Indicates the state of the absence mode.
|
|
14
|
+
pause_leakage_protection_until_utc : datetime
|
|
15
|
+
UTC date time when the leakage protection will be active again.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
absence_mode_enabled: Annotated[bool, Field(alias='absenceModeEnabled')]
|
|
19
|
+
pause_leakage_protection_until_utc: Annotated[datetime, Field(alias='pauseLeakageProtectionUntilUTC')]
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "pyocat"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = ""
|
|
5
|
+
authors = [
|
|
6
|
+
{ name = "Mathias Hörtnagl", email = "mathias.hoertnagl@watercryst.com" },
|
|
7
|
+
]
|
|
8
|
+
readme = "README.md"
|
|
9
|
+
requires-python = ">=3.12"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"httpx (>=0.28.1,<0.29.0)",
|
|
12
|
+
"pydantic (>=2.13.3,<3.0.0)"
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
[tool.poetry.scripts]
|
|
16
|
+
main = "python_biocat_rest_api_client:main"
|
|
17
|
+
|
|
18
|
+
[build-system]
|
|
19
|
+
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
|
20
|
+
build-backend = "poetry.core.masonry.api"
|
|
21
|
+
|
|
22
|
+
[dependency-groups]
|
|
23
|
+
dev = [
|
|
24
|
+
"pytest (>=9.0.3,<10.0.0)",
|
|
25
|
+
"pytest-asyncio (>=1.3.0,<2.0.0)",
|
|
26
|
+
"pytest-httpx (>=0.36.2,<0.37.0)"
|
|
27
|
+
]
|