weheat 2025.1.15rc2__py3-none-any.whl → 2025.2.22__py3-none-any.whl

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.

Potentially problematic release.


This version of weheat might be problematic. Click here for more details.

@@ -1,3 +1,3 @@
1
- from weheat.abstractions.auth import AbstractAuth
2
1
  from weheat.abstractions.discovery import HeatPumpDiscovery
3
2
  from weheat.abstractions.heat_pump import HeatPump
3
+ from weheat.abstractions.user import async_get_user_id_from_token
@@ -10,13 +10,17 @@ from weheat.api_client import ApiClient
10
10
  from weheat.api.heat_pump_log_api import HeatPumpLogApi
11
11
  from weheat.api.energy_log_api import EnergyLogApi
12
12
  from datetime import datetime, timedelta
13
+ from typing import TypeVar, Union, Optional
13
14
 
14
15
  # before this date no energy logs are available, so start from this point onwards
15
16
  START_DATE = datetime(2023, 1, 1, 0, 0, 0)
16
17
 
18
+ T = TypeVar("T", bool, int, float)
19
+
17
20
 
18
21
  class HeatPump:
19
22
  """Heat pump class representing a heat pump."""
23
+
20
24
  class State(Enum):
21
25
  STANDBY = auto()
22
26
  WATER_CHECK = auto()
@@ -28,16 +32,22 @@ class HeatPump:
28
32
  SELF_TEST = auto()
29
33
  MANUAL_CONTROL = auto()
30
34
 
31
- def __init__(self, api_url: str, uuid: str, client_session:aiohttp.ClientSession|None = None) -> None:
35
+ def __init__(self, api_url: str, uuid: str, client_session: aiohttp.ClientSession | None = None) -> None:
32
36
  self._api_url = api_url
33
37
  self._uuid = uuid
34
38
  self._last_log = None
35
- self._energy_consumption = None
36
- self._energy_output = None
37
- self._nominal_max_power = None
39
+ self._energy_consumption: Union[float, None] = None
40
+ self._energy_output: Union[float, None] = None
41
+ self._nominal_max_power: Union[float, None] = None
38
42
  self._client = client_session
39
43
 
40
- async def async_get_status(self, access_token: str):
44
+ async def async_get_status(self, access_token: str) -> None:
45
+ """Updates the heat pump instance with data from the API."""
46
+ await self.async_get_logs(access_token)
47
+ await self.async_get_energy(access_token)
48
+
49
+
50
+ async def async_get_logs(self, access_token: str) -> None:
41
51
  """Updates the heat pump instance with data from the API."""
42
52
  try:
43
53
  config = Configuration(host=self._api_url, access_token=access_token, client_session=self._client)
@@ -45,26 +55,39 @@ class HeatPump:
45
55
  async with ApiClient(configuration=config) as client:
46
56
  # Set the max power once
47
57
  if self._nominal_max_power is None:
48
- response = await HeatPumpApi(client).api_v1_heat_pumps_heat_pump_id_get_with_http_info(heat_pump_id=self._uuid)
58
+ response = await HeatPumpApi(client).api_v1_heat_pumps_heat_pump_id_get_with_http_info(
59
+ heat_pump_id=self._uuid)
49
60
 
50
61
  if response.status_code == 200:
51
62
  self._set_nominal_max_power_for_model(response.data.model)
52
63
 
53
-
54
64
  response = await HeatPumpLogApi(
55
65
  client
56
66
  ).api_v1_heat_pumps_heat_pump_id_logs_latest_get_with_http_info(
57
- heat_pump_id=self._uuid )
67
+ heat_pump_id=self._uuid)
58
68
 
59
69
  if response.status_code == 200:
60
70
  self._last_log = response.data
61
71
 
72
+
73
+ except Exception as e:
74
+ self._last_log = None
75
+ raise e
76
+
77
+
78
+ async def async_get_energy(self, access_token: str) -> None:
79
+ """Updates the heat pump instance with data from the API."""
80
+ try:
81
+ config = Configuration(host=self._api_url, access_token=access_token, client_session=self._client)
82
+
83
+ async with ApiClient(configuration=config) as client:
62
84
  # Also get all energy totals form past years and add them together
63
85
  # As end time pick today + 1 day to avoid issues with timezones
64
- response = await EnergyLogApi(client).api_v1_energy_logs_heat_pump_id_get_with_http_info(heat_pump_id=self._uuid,
65
- start_time=START_DATE,
66
- end_time=datetime.now() + timedelta(days=1),
67
- interval='Month')
86
+ response = await EnergyLogApi(client).api_v1_energy_logs_heat_pump_id_get_with_http_info(
87
+ heat_pump_id=self._uuid,
88
+ start_time=START_DATE,
89
+ end_time=datetime.now() + timedelta(days=1),
90
+ interval='Month')
68
91
 
69
92
  if response.status_code == 200:
70
93
  # aggregate the energy consumption
@@ -82,109 +105,112 @@ class HeatPump:
82
105
  self._energy_output += year.total_e_out_dhw
83
106
  self._energy_output += year.total_e_out_dhw_defrost
84
107
 
85
-
86
-
87
108
  except Exception as e:
88
- self._last_log = None
89
109
  self._energy_consumption = None
90
- raise e
110
+ self._energy_output = None
91
111
 
92
- def _if_available(self, key):
112
+ def _if_available(self, key: str) -> Optional[T]:
93
113
  """Returns the value from the last logged value if available. None otherwise."""
94
114
  if self._last_log is not None and hasattr(self._last_log, key):
95
115
  return getattr(self._last_log, key)
96
116
  return None
97
117
 
98
- def _set_nominal_max_power_for_model(self, model_id:int):
118
+ def _set_nominal_max_power_for_model(self, model_id: int) -> None:
99
119
  # These numbers are the rpm at 100% power in the portal
100
120
  # RPM can go above 100% if the limit is increased in the portal.
101
121
  # except for the Flint, that cannot go above 100%.
102
122
  if model_id == 1:
103
- #BB60
123
+ # BB60
104
124
  self._nominal_max_power = 5280
105
125
  elif 2 <= model_id <= 4:
106
- #SP60
126
+ # SP60
107
127
  self._nominal_max_power = 5280
108
128
  elif model_id == 5:
109
129
  # Flint
110
130
  self._nominal_max_power = 5400
111
131
  else:
112
- #BB80
132
+ # BB80
113
133
  self._nominal_max_power = 4500
114
134
 
115
- def __str__(self):
135
+ def __str__(self) -> str:
116
136
  return f"WeheatHeatPump(uuid={self._uuid}, last update={self._if_available('timestamp')})"
117
137
 
118
- def __repr__(self):
138
+ def __repr__(self) -> str:
119
139
  return self.__str__()
120
140
 
121
141
  @property
122
- def water_inlet_temperature(self):
142
+ def raw_content(self) -> Optional[dict]:
143
+ if self._last_log is not None:
144
+ return vars(self._last_log) # type: ignore[unreachable]
145
+ return None
146
+
147
+ @property
148
+ def water_inlet_temperature(self) -> Union[float, None]:
123
149
  """The heat pump water inlet temperature."""
124
150
  return self._if_available("t_water_in")
125
151
 
126
152
  @property
127
- def water_outlet_temperature(self):
153
+ def water_outlet_temperature(self) -> Union[float, None]:
128
154
  """The heat pump water outlet temperature."""
129
155
  return self._if_available("t_water_out")
130
156
 
131
157
  @property
132
- def water_house_in_temperature(self):
158
+ def water_house_in_temperature(self) -> Union[float, None]:
133
159
  """The water house in temperature."""
134
160
  return self._if_available("t_water_house_in")
135
161
 
136
162
  @property
137
- def air_inlet_temperature(self):
163
+ def air_inlet_temperature(self) -> Union[float, None]:
138
164
  """The heat pump air inlet temperature."""
139
165
  return self._if_available("t_air_in")
140
166
 
141
167
  @property
142
- def air_outlet_temperature(self):
168
+ def air_outlet_temperature(self) -> Union[float, None]:
143
169
  """The heat pump air outlet temperature."""
144
170
  return self._if_available("t_air_out")
145
171
 
146
172
  @property
147
- def thermostat_water_setpoint(self):
173
+ def thermostat_water_setpoint(self) -> Union[float, None]:
148
174
  """The thermostat water setpoint."""
149
175
  return self._if_available("t_thermostat_setpoint")
150
176
 
151
177
  @property
152
- def thermostat_room_temperature(self):
178
+ def thermostat_room_temperature(self) -> Union[float, None]:
153
179
  """The thermostat current room temperature."""
154
180
  return self._if_available("t_room")
155
181
 
156
182
  @property
157
- def thermostat_room_temperature_setpoint(self):
183
+ def thermostat_room_temperature_setpoint(self) -> Union[float, None]:
158
184
  """The thermostat room temperature setpoint."""
159
185
  return self._if_available("t_room_target")
160
186
 
161
187
  @property
162
- def thermostat_on_off_state(self):
188
+ def thermostat_on_off_state(self) -> Union[bool, None]:
163
189
  """The thermostat on/off state."""
164
190
  return self._if_available("on_off_thermostat_state")
165
191
 
166
192
  @property
167
- def power_input(self):
193
+ def power_input(self) -> Union[float, None]:
168
194
  """The heat pumps power input."""
169
195
  return self._if_available("cm_mass_power_in")
170
196
 
171
197
  @property
172
- def power_output(self):
198
+ def power_output(self) -> Union[float, None]:
173
199
  """The heat pumps hydraulic output power."""
174
200
  return self._if_available("cm_mass_power_out")
175
201
 
176
202
  @property
177
- def dhw_top_temperature(self):
203
+ def dhw_top_temperature(self) -> Union[float, None]:
178
204
  """The DHW vessel top temperature."""
179
205
  return self._if_available("t1")
180
206
 
181
207
  @property
182
- def dhw_bottom_temperature(self):
208
+ def dhw_bottom_temperature(self) -> Union[float, None]:
183
209
  """The DHW vessel bottom temperature."""
184
210
  return self._if_available("t2")
185
211
 
186
212
  @property
187
- def cop(self):
213
+ def cop(self) -> Union[float, None]:
188
214
  """
189
215
  Returns the coefficient of performance of the heat pump.
190
216
  Note that this is calculated from a singular log entry and might not be accurate when the
@@ -194,34 +220,35 @@ class HeatPump:
194
220
  output = self.power_output
195
221
  if input is not None and output is not None and input != 0:
196
222
  return output / input
223
+ return None
197
224
 
198
225
  @property
199
- def indoor_unit_water_pump_state(self):
226
+ def indoor_unit_water_pump_state(self) -> Union[bool, None]:
200
227
  """Decoded water pump state."""
201
228
  return self._if_available("control_bridge_status_decoded_water_pump")
202
229
 
203
230
  @property
204
- def indoor_unit_auxiliary_pump_state(self):
231
+ def indoor_unit_auxiliary_pump_state(self) -> Union[bool, None]:
205
232
  """Decoded auxiliary pump state."""
206
233
  return self._if_available("control_bridge_status_decoded_water_pump2")
207
234
 
208
235
  @property
209
- def indoor_unit_dhw_valve_or_pump_state(self):
236
+ def indoor_unit_dhw_valve_or_pump_state(self) -> Union[bool, None]:
210
237
  """Decoded DHW valve or pump state."""
211
238
  return self._if_available("control_bridge_status_decoded_dhw_valve")
212
239
 
213
240
  @property
214
- def indoor_unit_gas_boiler_state(self):
241
+ def indoor_unit_gas_boiler_state(self) -> Union[bool, None]:
215
242
  """Decoded gas boiler state."""
216
243
  return self._if_available("control_bridge_status_decoded_gas_boiler")
217
244
 
218
245
  @property
219
- def indoor_unit_electric_heater_state(self):
246
+ def indoor_unit_electric_heater_state(self) -> Union[bool, None]:
220
247
  """Decoded electric heater state."""
221
248
  return self._if_available("control_bridge_status_decoded_electric_heater")
222
249
 
223
250
  @property
224
- def compressor_percentage(self):
251
+ def compressor_percentage(self) -> Union[int, None]:
225
252
  current_rpm = self._if_available("rpm")
226
253
  if self._nominal_max_power is not None and current_rpm is not None:
227
254
  # calculate percentage of rpm
@@ -229,12 +256,12 @@ class HeatPump:
229
256
  return None
230
257
 
231
258
  @property
232
- def compressor_rpm(self):
259
+ def compressor_rpm(self) -> Union[float, None]:
233
260
  """Compressor RPM."""
234
261
  return self._if_available("rpm")
235
262
 
236
263
  @property
237
- def heat_pump_state(self) -> State | None:
264
+ def heat_pump_state(self) -> Union[State, None]:
238
265
  """The heat pump state."""
239
266
  numeric_state = self._if_available("state")
240
267
  if numeric_state is None:
@@ -259,11 +286,11 @@ class HeatPump:
259
286
  return None
260
287
 
261
288
  @property
262
- def energy_total(self):
289
+ def energy_total(self) -> Union[float, None]:
263
290
  """The total used energy in kWh from 2023 to now."""
264
291
  return self._energy_consumption
265
292
 
266
293
  @property
267
- def energy_output(self):
294
+ def energy_output(self) -> Union[float, None]:
268
295
  """The total energy output in kWh from 2023 to now."""
269
296
  return self._energy_output
@@ -1,4 +1,5 @@
1
1
  import asyncio
2
+ from typing import Union
2
3
 
3
4
  import aiohttp
4
5
 
@@ -7,17 +8,16 @@ from weheat.api_client import ApiClient
7
8
  from weheat.api.user_api import UserApi
8
9
 
9
10
 
10
- async def async_get_user_id_from_token(api_url: str, access_token: str, client_session:aiohttp.ClientSession|None = None):
11
+ async def async_get_user_id_from_token(api_url: str, access_token: str, client_session:aiohttp.ClientSession|None = None) -> Union[str,None]:
11
12
  """ Get the user id from the current logged-in user. """
12
- try:
13
- config = Configuration(host=api_url, access_token=access_token, client_session=client_session)
13
+ config = Configuration(host=api_url, access_token=access_token, client_session=client_session)
14
14
 
15
- async with ApiClient(configuration=config) as client:
16
- response = await UserApi(
17
- client
18
- ).api_v1_users_me_get_with_http_info()
15
+ async with ApiClient(configuration=config) as client:
16
+ response = await UserApi(
17
+ client
18
+ ).api_v1_users_me_get_with_http_info()
19
+
20
+ if response.status_code == 200:
21
+ return response.data.id # type: ignore[no-any-return]
22
+ return None
19
23
 
20
- if response.status_code == 200:
21
- return response.data.id
22
- except Exception as e:
23
- raise e
weheat/exceptions.py CHANGED
@@ -105,9 +105,9 @@ class ApiKeyError(OpenApiException, KeyError):
105
105
  class ApiException(OpenApiException):
106
106
 
107
107
  def __init__(
108
- self,
109
- status=None,
110
- reason=None,
108
+ self,
109
+ status=None,
110
+ reason=None,
111
111
  http_resp=None,
112
112
  *,
113
113
  body: Optional[str] = None,
@@ -133,10 +133,10 @@ class ApiException(OpenApiException):
133
133
 
134
134
  @classmethod
135
135
  def from_response(
136
- cls,
137
- *,
138
- http_resp,
139
- body: Optional[str],
136
+ cls,
137
+ *,
138
+ http_resp,
139
+ body: Optional[str],
140
140
  data: Optional[Any],
141
141
  ) -> Self:
142
142
  if http_resp.status == 400:
@@ -151,6 +151,9 @@ class ApiException(OpenApiException):
151
151
  if http_resp.status == 404:
152
152
  raise NotFoundException(http_resp=http_resp, body=body, data=data)
153
153
 
154
+ if http_resp.status == 429:
155
+ raise TooManyRequestsException(http_resp=http_resp, body=body, data=data)
156
+
154
157
  if 500 <= http_resp.status <= 599:
155
158
  raise ServiceException(http_resp=http_resp, body=body, data=data)
156
159
  raise ApiException(http_resp=http_resp, body=body, data=data)
@@ -189,6 +192,10 @@ class ServiceException(ApiException):
189
192
  pass
190
193
 
191
194
 
195
+ class TooManyRequestsException(ApiException):
196
+ pass
197
+
198
+
192
199
  def render_path(path_to_item):
193
200
  """Returns a string representation of a path"""
194
201
  result = ""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: weheat
3
- Version: 2025.1.15rc2
3
+ Version: 2025.2.22
4
4
  Summary: Weheat Backend client
5
5
  Home-page: https://github.com/wefabricate/wh-python
6
6
  Author: Jesper Raemaekers
@@ -2,14 +2,13 @@ weheat/__init__.py,sha256=C24j_uAbbyEzqh8DmQyORUrVMdFDlzEv4jGClf4TY34,1765
2
2
  weheat/api_client.py,sha256=Bh5xHchRGCcsHbT3rXrcmhKCMiumi1_2L6n0Q57KODw,24803
3
3
  weheat/api_response.py,sha256=A7O_XgliD6y7jEv82fgIaxR3T8KiwaOqHR6djpZyh_o,674
4
4
  weheat/configuration.py,sha256=8D0zAgx7B6YmVNuyS0AarcCAl5TmKKH655jHqWrvSl0,14531
5
- weheat/exceptions.py,sha256=jZjLtEMBKFpLTdV1GPsbQSPriG1ilgMSodGnhEKlWh4,5913
5
+ weheat/exceptions.py,sha256=Wbn9RsKWzTJHpp5sllYbMjqMWMc8ZyFm_J2sfptVG7g,6086
6
6
  weheat/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
7
  weheat/rest.py,sha256=hSt8T6NQfdrgz_7Dxo9U-DO5ZEgUVVG38OUErThWqlU,7086
8
- weheat/abstractions/__init__.py,sha256=cRdA_kyTIooo39I13_mqShSfZMqdzNGHbmrnITqgx6A,161
9
- weheat/abstractions/auth.py,sha256=VCAxJ4OIj7bsYttqJl5-juU0VUlSd3xPu7kUjtHZr3U,979
8
+ weheat/abstractions/__init__.py,sha256=BbA5WW8RE8vtpK0Dq80ayMILN4m6lmCyIPYzYXy3rTE,177
10
9
  weheat/abstractions/discovery.py,sha256=PrhyM29OKvCgKzWig5BAjaEF15CIcTp_AIBZg2lyJ6Y,2384
11
- weheat/abstractions/heat_pump.py,sha256=DFgKF_4Mdv4dOVugECy9xB4y6e85B-HwZatrUBSnhPE,9920
12
- weheat/abstractions/user.py,sha256=L48CGvBe7YrUa-wnIgmv9Hu0DOMN4IXN0fSmufrlJIc,760
10
+ weheat/abstractions/heat_pump.py,sha256=roUDxZZsKTbTmI9ZJqXKy51IoMlyJGY5N1tqSl8TIv8,11322
11
+ weheat/abstractions/user.py,sha256=moeJAmbjdYuXAkL7Xuks5w4zcPpjKUXM9nYJDv8WEXs,775
13
12
  weheat/api/__init__.py,sha256=zZ_Xqek8VY6gARsJK6hRess0qqGii-Ls1uXm92k0jPE,244
14
13
  weheat/api/energy_log_api.py,sha256=plZ9YoQ9O39qufzr3B6CduuzJMY1K6iJS5f5AXN-vUo,17587
15
14
  weheat/api/heat_pump_api.py,sha256=tyFA-Xf-Ld4cAVSYblCq1s5I1CDRQmKJx9UBJivnS0Y,28211
@@ -30,8 +29,8 @@ weheat/models/read_heat_pump_dto.py,sha256=7jH2MwMVKv_dOoJUUsKE8sTR5mROqco71OzOA
30
29
  weheat/models/read_user_dto.py,sha256=OIsWQZcdByN94ViSv0DjFHORRsMnkQ93jc-gJuudRdg,4018
31
30
  weheat/models/read_user_me_dto.py,sha256=Ger6qKlbZBBvG_4MStl6aEMkrBBJIjQtV6pvK1-lw28,4441
32
31
  weheat/models/role.py,sha256=6KUInAmkhoxEdWxBo9jBHXiy_nqJHrKXVeh8RvQPEYM,1101
33
- weheat-2025.1.15rc2.dist-info/LICENSE,sha256=rWmFUq0uth2jpet-RQ2QPd2VhZkcPSUs6Dxfmbqkbis,1068
34
- weheat-2025.1.15rc2.dist-info/METADATA,sha256=lg2CZbj2OGdxFi33MKKI7VrqKmvlBc-Uvf1IIeO3wvA,3877
35
- weheat-2025.1.15rc2.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
36
- weheat-2025.1.15rc2.dist-info/top_level.txt,sha256=hLzdyvGZ9rs4AqK7U48mdHx_-FcP5sDuTSleDUvGAZw,7
37
- weheat-2025.1.15rc2.dist-info/RECORD,,
32
+ weheat-2025.2.22.dist-info/LICENSE,sha256=rWmFUq0uth2jpet-RQ2QPd2VhZkcPSUs6Dxfmbqkbis,1068
33
+ weheat-2025.2.22.dist-info/METADATA,sha256=5rEYpRYuupCQjYhY8pK7KICJ60DTBrQQL3V2Oi5zBkU,3874
34
+ weheat-2025.2.22.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
35
+ weheat-2025.2.22.dist-info/top_level.txt,sha256=hLzdyvGZ9rs4AqK7U48mdHx_-FcP5sDuTSleDUvGAZw,7
36
+ weheat-2025.2.22.dist-info/RECORD,,
@@ -1,34 +0,0 @@
1
- from abc import ABC, abstractmethod
2
- from aiohttp import ClientSession, ClientResponse
3
-
4
-
5
- class AbstractAuth(ABC):
6
- """Abstract class to make authenticated requests."""
7
-
8
- def __init__(self, websession: ClientSession, host: str) -> None:
9
- """Initialize the auth."""
10
- self.websession = websession
11
- self.host = host
12
-
13
- @abstractmethod
14
- async def async_get_access_token(self) -> str:
15
- """Return a valid access token."""
16
-
17
- async def request(self, method, url, **kwargs) -> ClientResponse:
18
- """Make a request."""
19
- headers = kwargs.get("headers")
20
-
21
- if headers is None:
22
- headers = {}
23
- else:
24
- headers = dict(headers)
25
-
26
- access_token = await self.async_get_access_token()
27
- headers["authorization"] = f"Bearer {access_token}"
28
-
29
- return await self.websession.request(
30
- method,
31
- f"{self.host}/{url}",
32
- **kwargs,
33
- headers=headers,
34
- )