hyponcloud 0.9.2__tar.gz → 0.9.4__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.
- {hyponcloud-0.9.2 → hyponcloud-0.9.4}/.github/workflows/ci.yml +1 -1
- {hyponcloud-0.9.2 → hyponcloud-0.9.4}/.gitignore +3 -0
- {hyponcloud-0.9.2/hyponcloud.egg-info → hyponcloud-0.9.4}/PKG-INFO +1 -1
- {hyponcloud-0.9.2 → hyponcloud-0.9.4}/example.py +16 -12
- {hyponcloud-0.9.2 → hyponcloud-0.9.4}/hyponcloud/_version.py +3 -3
- {hyponcloud-0.9.2 → hyponcloud-0.9.4}/hyponcloud/client.py +58 -27
- {hyponcloud-0.9.2 → hyponcloud-0.9.4}/hyponcloud/exceptions.py +1 -1
- {hyponcloud-0.9.2 → hyponcloud-0.9.4}/hyponcloud/models.py +4 -4
- {hyponcloud-0.9.2 → hyponcloud-0.9.4/hyponcloud.egg-info}/PKG-INFO +1 -1
- {hyponcloud-0.9.2 → hyponcloud-0.9.4}/tests/test_client.py +415 -292
- {hyponcloud-0.9.2 → hyponcloud-0.9.4}/.github/dependabot.yml +0 -0
- {hyponcloud-0.9.2 → hyponcloud-0.9.4}/.github/workflows/codeql.yml +0 -0
- {hyponcloud-0.9.2 → hyponcloud-0.9.4}/.github/workflows/publish.yml +0 -0
- {hyponcloud-0.9.2 → hyponcloud-0.9.4}/.github/workflows/release.yml +0 -0
- {hyponcloud-0.9.2 → hyponcloud-0.9.4}/.pre-commit-config.yaml +0 -0
- {hyponcloud-0.9.2 → hyponcloud-0.9.4}/AGENTS.md +0 -0
- {hyponcloud-0.9.2 → hyponcloud-0.9.4}/API.md +0 -0
- {hyponcloud-0.9.2 → hyponcloud-0.9.4}/LICENSE +0 -0
- {hyponcloud-0.9.2 → hyponcloud-0.9.4}/MANIFEST.in +0 -0
- {hyponcloud-0.9.2 → hyponcloud-0.9.4}/README.md +0 -0
- {hyponcloud-0.9.2 → hyponcloud-0.9.4}/hyponcloud/__init__.py +0 -0
- {hyponcloud-0.9.2 → hyponcloud-0.9.4}/hyponcloud/py.typed +0 -0
- {hyponcloud-0.9.2 → hyponcloud-0.9.4}/hyponcloud.egg-info/SOURCES.txt +0 -0
- {hyponcloud-0.9.2 → hyponcloud-0.9.4}/hyponcloud.egg-info/dependency_links.txt +0 -0
- {hyponcloud-0.9.2 → hyponcloud-0.9.4}/hyponcloud.egg-info/requires.txt +0 -0
- {hyponcloud-0.9.2 → hyponcloud-0.9.4}/hyponcloud.egg-info/top_level.txt +0 -0
- {hyponcloud-0.9.2 → hyponcloud-0.9.4}/pyproject.toml +0 -0
- {hyponcloud-0.9.2 → hyponcloud-0.9.4}/setup.cfg +0 -0
- {hyponcloud-0.9.2 → hyponcloud-0.9.4}/setup_instructions.md +0 -0
- {hyponcloud-0.9.2 → hyponcloud-0.9.4}/tests/__init__.py +0 -0
|
@@ -68,10 +68,10 @@ async def main() -> None:
|
|
|
68
68
|
if plants:
|
|
69
69
|
# Table header
|
|
70
70
|
print(
|
|
71
|
-
f"{'Name':<20} {'Location':<25} {'Status':<10} "
|
|
71
|
+
f"{'Name':<20} {'Location':<25} {'Status':<10} {'Type':<12} "
|
|
72
72
|
f"{'Power':<15} {'Today':<10} {'Total':<10}"
|
|
73
73
|
)
|
|
74
|
-
print("-" *
|
|
74
|
+
print("-" * 112)
|
|
75
75
|
# Table rows
|
|
76
76
|
for plant in plants:
|
|
77
77
|
location = f"{plant.city}, {plant.country}"
|
|
@@ -80,16 +80,21 @@ async def main() -> None:
|
|
|
80
80
|
f"{plant.plant_name:<20} "
|
|
81
81
|
f"{location:<25} "
|
|
82
82
|
f"{plant.status:<10} "
|
|
83
|
+
f"{plant.plant_type:<12} "
|
|
83
84
|
f"{power_str:<15} "
|
|
84
85
|
f"{plant.e_today:<10.2f} "
|
|
85
86
|
f"{plant.e_total:<10.2f}"
|
|
86
87
|
)
|
|
87
88
|
|
|
88
|
-
# Get inverters
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
print(f"
|
|
92
|
-
|
|
89
|
+
# Get inverters and monitor data for each plant
|
|
90
|
+
for plant in plants:
|
|
91
|
+
print(f"\n{'#' * 60}")
|
|
92
|
+
print(f"# Plant: {plant.plant_name} (ID: {plant.plant_id})")
|
|
93
|
+
print(f"{'#' * 60}")
|
|
94
|
+
|
|
95
|
+
# Inverters for this plant
|
|
96
|
+
print(f"\nFetching inverters for plant: {plant.plant_name}...")
|
|
97
|
+
inverters = await client.get_inverters(plant.plant_id)
|
|
93
98
|
print(f"\n=== Inverters ({len(inverters)}) ===")
|
|
94
99
|
if inverters:
|
|
95
100
|
# Table header
|
|
@@ -110,11 +115,9 @@ async def main() -> None:
|
|
|
110
115
|
f"{inverter.software_version:<12}"
|
|
111
116
|
)
|
|
112
117
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
print(f"\nFetching monitor data for plant: {first_plant.plant_name}...")
|
|
117
|
-
monitor = await client.get_monitor(first_plant.plant_id)
|
|
118
|
+
# Monitor data for this plant
|
|
119
|
+
print(f"\nFetching monitor data for plant: {plant.plant_name}...")
|
|
120
|
+
monitor = await client.get_monitor(plant.plant_id)
|
|
118
121
|
print("\n=== Plant Monitor ===")
|
|
119
122
|
print(f"{'Field':<25} {'Value':<30}")
|
|
120
123
|
print("-" * 55)
|
|
@@ -125,6 +128,7 @@ async def main() -> None:
|
|
|
125
128
|
print(f"{'PV Power':<25} {monitor.power_pv} W")
|
|
126
129
|
print(f"{'Load Power':<25} {monitor.power_load} W")
|
|
127
130
|
print(f"{'Grid Power':<25} {monitor.meter_power} W")
|
|
131
|
+
print(f"{'Battery Discharge':<25} {monitor.w_cha} W")
|
|
128
132
|
print(f"{'Battery SOC':<25} {monitor.soc}%")
|
|
129
133
|
print(f"{'Performance':<25} {monitor.percent}%")
|
|
130
134
|
print(f"{'CO2 Saved':<25} {monitor.total_co2} kg")
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '0.9.
|
|
22
|
-
__version_tuple__ = version_tuple = (0, 9,
|
|
21
|
+
__version__ = version = '0.9.4'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 9, 4)
|
|
23
23
|
|
|
24
|
-
__commit_id__ = commit_id = '
|
|
24
|
+
__commit_id__ = commit_id = 'g176e02645'
|
|
@@ -4,7 +4,7 @@ import asyncio
|
|
|
4
4
|
import json
|
|
5
5
|
import logging
|
|
6
6
|
from time import time
|
|
7
|
-
from typing import Any
|
|
7
|
+
from typing import Any, cast
|
|
8
8
|
|
|
9
9
|
import aiohttp
|
|
10
10
|
|
|
@@ -45,10 +45,10 @@ class HyponCloud:
|
|
|
45
45
|
|
|
46
46
|
self._session = session
|
|
47
47
|
self._own_session = session is None
|
|
48
|
-
self.
|
|
49
|
-
self.
|
|
50
|
-
self.
|
|
51
|
-
self.
|
|
48
|
+
self._username = username
|
|
49
|
+
self._password = password
|
|
50
|
+
self._token = ""
|
|
51
|
+
self._token_expires_at = 0
|
|
52
52
|
|
|
53
53
|
async def __aenter__(self) -> "HyponCloud":
|
|
54
54
|
"""Async context manager entry."""
|
|
@@ -73,7 +73,7 @@ class HyponCloud:
|
|
|
73
73
|
AuthenticationError: If authentication fails.
|
|
74
74
|
ConnectionError: If connection to API fails.
|
|
75
75
|
"""
|
|
76
|
-
if self.
|
|
76
|
+
if self._token and self._token_expires_at > time():
|
|
77
77
|
return
|
|
78
78
|
|
|
79
79
|
if not self._session:
|
|
@@ -81,14 +81,12 @@ class HyponCloud:
|
|
|
81
81
|
self._own_session = True
|
|
82
82
|
|
|
83
83
|
url = f"{self.base_url}/login"
|
|
84
|
-
data = {"username": self.
|
|
84
|
+
data = {"username": self._username, "password": self._password}
|
|
85
85
|
|
|
86
86
|
try:
|
|
87
87
|
async with self._session.post(
|
|
88
88
|
url, json=data, timeout=self.timeout
|
|
89
89
|
) as response:
|
|
90
|
-
result = await self._parse_response(response, "POST", url)
|
|
91
|
-
|
|
92
90
|
if response.status == 401:
|
|
93
91
|
raise AuthenticationError("Invalid credentials")
|
|
94
92
|
if response.status == 429:
|
|
@@ -100,8 +98,9 @@ class HyponCloud:
|
|
|
100
98
|
f"Connection failed with status {response.status}"
|
|
101
99
|
)
|
|
102
100
|
|
|
103
|
-
self.
|
|
104
|
-
self.
|
|
101
|
+
result = await self._parse_response(response, "POST", url)
|
|
102
|
+
self._token = result["data"]["token"]
|
|
103
|
+
self._token_expires_at = int(time()) + self.token_validity
|
|
105
104
|
except aiohttp.ClientError as e:
|
|
106
105
|
raise RequestError(f"Failed to connect to Hypon Cloud: {e}") from e
|
|
107
106
|
except KeyError as e:
|
|
@@ -119,8 +118,8 @@ class HyponCloud:
|
|
|
119
118
|
print(f"Headers: {dict(response.headers)}")
|
|
120
119
|
raw_text = await response.text()
|
|
121
120
|
print(f"Response: {raw_text}")
|
|
122
|
-
return dict
|
|
123
|
-
return dict
|
|
121
|
+
return cast(dict, json.loads(raw_text))
|
|
122
|
+
return cast(dict, await response.json())
|
|
124
123
|
|
|
125
124
|
async def _request(
|
|
126
125
|
self, url: str, endpoint_name: str, retries: int | None = None
|
|
@@ -143,13 +142,11 @@ class HyponCloud:
|
|
|
143
142
|
await self.connect()
|
|
144
143
|
assert self._session is not None # connect() ensures session exists
|
|
145
144
|
|
|
146
|
-
headers = {"authorization": f"Bearer {self.
|
|
145
|
+
headers = {"authorization": f"Bearer {self._token}"}
|
|
147
146
|
try:
|
|
148
147
|
async with self._session.get(
|
|
149
148
|
url, headers=headers, timeout=self.timeout
|
|
150
149
|
) as response:
|
|
151
|
-
result = await self._parse_response(response, "GET", url)
|
|
152
|
-
|
|
153
150
|
if response.status == 429:
|
|
154
151
|
if retries > 0:
|
|
155
152
|
await asyncio.sleep(10)
|
|
@@ -164,8 +161,11 @@ class HyponCloud:
|
|
|
164
161
|
f"Failed to get {endpoint_name}: HTTP {response.status}"
|
|
165
162
|
)
|
|
166
163
|
|
|
167
|
-
return
|
|
164
|
+
return await self._parse_response(response, "GET", url)
|
|
168
165
|
except aiohttp.ClientError as e:
|
|
166
|
+
if retries > 0:
|
|
167
|
+
await asyncio.sleep(10)
|
|
168
|
+
return await self._request(url, endpoint_name, retries - 1)
|
|
169
169
|
raise RequestError(f"Failed to get {endpoint_name}: {e}") from e
|
|
170
170
|
|
|
171
171
|
async def get_overview(self, retries: int | None = None) -> OverviewData:
|
|
@@ -182,35 +182,55 @@ class HyponCloud:
|
|
|
182
182
|
AuthenticationError: If authentication fails.
|
|
183
183
|
ConnectionError: If connection to API fails.
|
|
184
184
|
"""
|
|
185
|
+
retries = retries if retries is not None else self.retries
|
|
185
186
|
url = f"{self.base_url}/plant/overview"
|
|
186
187
|
try:
|
|
187
188
|
result = await self._request(url, "plant overview", retries)
|
|
188
189
|
return OverviewData.from_dict(result["data"])
|
|
189
190
|
except KeyError as e:
|
|
190
191
|
_LOGGER.error("Error parsing plant overview data: %s", e)
|
|
192
|
+
if retries > 0:
|
|
193
|
+
return await self.get_overview(retries - 1)
|
|
191
194
|
return OverviewData()
|
|
192
195
|
|
|
193
196
|
async def get_list(self, retries: int | None = None) -> list[PlantData]:
|
|
194
197
|
"""Get plant list.
|
|
195
198
|
|
|
199
|
+
This method automatically fetches all pages of plants.
|
|
200
|
+
|
|
196
201
|
Args:
|
|
197
202
|
retries: Number of retry attempts if request fails. If None,
|
|
198
203
|
uses the client's default retry setting.
|
|
199
204
|
|
|
200
205
|
Returns:
|
|
201
|
-
List of PlantData objects.
|
|
206
|
+
List of all PlantData objects across all pages.
|
|
202
207
|
|
|
203
208
|
Raises:
|
|
204
209
|
AuthenticationError: If authentication fails.
|
|
205
210
|
ConnectionError: If connection to API fails.
|
|
206
211
|
"""
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
212
|
+
retries = retries if retries is not None else self.retries
|
|
213
|
+
all_plants: list[PlantData] = []
|
|
214
|
+
page = 1
|
|
215
|
+
total_pages = 1
|
|
216
|
+
|
|
217
|
+
while page <= total_pages:
|
|
218
|
+
url = (
|
|
219
|
+
f"{self.base_url}/plant/list2" f"?page={page}&page_size=10&refresh=true"
|
|
220
|
+
)
|
|
221
|
+
try:
|
|
222
|
+
result = await self._request(url, "plant list", retries)
|
|
223
|
+
if "totalPage" in result:
|
|
224
|
+
total_pages = result["totalPage"]
|
|
225
|
+
all_plants.extend(PlantData.from_dict(item) for item in result["data"])
|
|
226
|
+
page += 1
|
|
227
|
+
except KeyError as e:
|
|
228
|
+
_LOGGER.error("Error parsing plant list data: %s", e)
|
|
229
|
+
if retries > 0:
|
|
230
|
+
return await self.get_list(retries - 1)
|
|
231
|
+
return []
|
|
232
|
+
|
|
233
|
+
return all_plants
|
|
214
234
|
|
|
215
235
|
async def get_inverters(
|
|
216
236
|
self, plant_id: str, retries: int | None = None
|
|
@@ -231,6 +251,7 @@ class HyponCloud:
|
|
|
231
251
|
AuthenticationError: If authentication fails.
|
|
232
252
|
ConnectionError: If connection to API fails.
|
|
233
253
|
"""
|
|
254
|
+
retries = retries if retries is not None else self.retries
|
|
234
255
|
all_inverters: list[InverterData] = []
|
|
235
256
|
page = 1
|
|
236
257
|
total_pages = 1
|
|
@@ -247,6 +268,8 @@ class HyponCloud:
|
|
|
247
268
|
page += 1
|
|
248
269
|
except KeyError as e:
|
|
249
270
|
_LOGGER.error("Error parsing inverter list data: %s", e)
|
|
271
|
+
if retries > 0:
|
|
272
|
+
return await self.get_inverters(plant_id, retries - 1)
|
|
250
273
|
return []
|
|
251
274
|
|
|
252
275
|
return all_inverters
|
|
@@ -268,12 +291,15 @@ class HyponCloud:
|
|
|
268
291
|
AuthenticationError: If authentication fails.
|
|
269
292
|
ConnectionError: If connection to API fails.
|
|
270
293
|
"""
|
|
294
|
+
retries = retries if retries is not None else self.retries
|
|
271
295
|
url = f"{self.base_url}/plant/{plant_id}/monitor?refresh=true"
|
|
272
296
|
try:
|
|
273
297
|
result = await self._request(url, "plant monitor", retries)
|
|
274
298
|
return PlantMonitorData.from_dict(result["data"])
|
|
275
299
|
except KeyError as e:
|
|
276
300
|
_LOGGER.error("Error parsing plant monitor data: %s", e)
|
|
301
|
+
if retries > 0:
|
|
302
|
+
return await self.get_monitor(plant_id, retries - 1)
|
|
277
303
|
return PlantMonitorData()
|
|
278
304
|
|
|
279
305
|
async def get_admin_info(self, retries: int | None = None) -> AdminInfo:
|
|
@@ -290,15 +316,20 @@ class HyponCloud:
|
|
|
290
316
|
AuthenticationError: If authentication fails.
|
|
291
317
|
ConnectionError: If connection to API fails.
|
|
292
318
|
"""
|
|
319
|
+
retries = retries if retries is not None else self.retries
|
|
293
320
|
url = f"{self.base_url}/administrator/admininfo"
|
|
294
321
|
try:
|
|
295
322
|
result = await self._request(url, "admin info", retries)
|
|
296
323
|
data = result["data"]
|
|
297
324
|
# Flatten nested "info" object into the main data dict
|
|
298
325
|
if "info" in data and isinstance(data["info"], dict):
|
|
299
|
-
|
|
300
|
-
|
|
326
|
+
data = {
|
|
327
|
+
**{k: v for k, v in data.items() if k != "info"},
|
|
328
|
+
**data["info"],
|
|
329
|
+
}
|
|
301
330
|
return AdminInfo.from_dict(data)
|
|
302
331
|
except KeyError as e:
|
|
303
332
|
_LOGGER.error("Error parsing admin info data: %s", e)
|
|
333
|
+
if retries > 0:
|
|
334
|
+
return await self.get_admin_info(retries - 1)
|
|
304
335
|
return AdminInfo()
|
|
@@ -174,10 +174,10 @@ class PlantMonitorData(DataClassDictMixin):
|
|
|
174
174
|
total_co2: float = 0.0
|
|
175
175
|
total_diesel: float = 0.0
|
|
176
176
|
percent: int = 0
|
|
177
|
-
meter_power: float = 0.0
|
|
178
|
-
power_load: float = 0.0
|
|
179
|
-
w_cha: float = 0.0
|
|
180
|
-
power_pv: float = 0.0
|
|
177
|
+
meter_power: float = 0.0 # Power drawn from the grid (W)
|
|
178
|
+
power_load: float = 0.0 # Home/load power consumption (W)
|
|
179
|
+
w_cha: float = 0.0 # Battery discharging power (W)
|
|
180
|
+
power_pv: float = 0.0 # PV (solar) generation power (W)
|
|
181
181
|
soc: float = 0.0
|
|
182
182
|
micro: int = 0
|
|
183
183
|
warning: str = ""
|