hyxi-cloud-api 1.4.1__tar.gz → 1.4.2__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.
- {hyxi_cloud_api-1.4.1/src/hyxi_cloud_api.egg-info → hyxi_cloud_api-1.4.2}/PKG-INFO +1 -1
- {hyxi_cloud_api-1.4.1 → hyxi_cloud_api-1.4.2}/pyproject.toml +1 -1
- {hyxi_cloud_api-1.4.1 → hyxi_cloud_api-1.4.2}/src/hyxi_cloud_api/__init__.py +1 -1
- {hyxi_cloud_api-1.4.1 → hyxi_cloud_api-1.4.2}/src/hyxi_cloud_api/api.py +78 -59
- {hyxi_cloud_api-1.4.1 → hyxi_cloud_api-1.4.2/src/hyxi_cloud_api.egg-info}/PKG-INFO +1 -1
- {hyxi_cloud_api-1.4.1 → hyxi_cloud_api-1.4.2}/tests/test_additional_coverage.py +2 -2
- {hyxi_cloud_api-1.4.1 → hyxi_cloud_api-1.4.2}/tests/test_api.py +6 -4
- {hyxi_cloud_api-1.4.1 → hyxi_cloud_api-1.4.2}/tests/test_device_control.py +3 -3
- {hyxi_cloud_api-1.4.1 → hyxi_cloud_api-1.4.2}/tests/test_execute_metric_tasks.py +12 -7
- {hyxi_cloud_api-1.4.1 → hyxi_cloud_api-1.4.2}/tests/test_execute_metrics_and_map_alarms.py +11 -9
- {hyxi_cloud_api-1.4.1 → hyxi_cloud_api-1.4.2}/tests/test_handle_back_discovery_alarm.py +9 -14
- {hyxi_cloud_api-1.4.1 → hyxi_cloud_api-1.4.2}/tests/test_process_alarms_and_back_discovery.py +2 -2
- {hyxi_cloud_api-1.4.1 → hyxi_cloud_api-1.4.2}/tests/test_process_devices_for_plant.py +2 -2
- {hyxi_cloud_api-1.4.1 → hyxi_cloud_api-1.4.2}/tests/test_subscriptions.py +1 -1
- {hyxi_cloud_api-1.4.1 → hyxi_cloud_api-1.4.2}/LICENSE +0 -0
- {hyxi_cloud_api-1.4.1 → hyxi_cloud_api-1.4.2}/README.md +0 -0
- {hyxi_cloud_api-1.4.1 → hyxi_cloud_api-1.4.2}/setup.cfg +0 -0
- {hyxi_cloud_api-1.4.1 → hyxi_cloud_api-1.4.2}/src/hyxi_cloud_api.egg-info/SOURCES.txt +0 -0
- {hyxi_cloud_api-1.4.1 → hyxi_cloud_api-1.4.2}/src/hyxi_cloud_api.egg-info/dependency_links.txt +0 -0
- {hyxi_cloud_api-1.4.1 → hyxi_cloud_api-1.4.2}/src/hyxi_cloud_api.egg-info/requires.txt +0 -0
- {hyxi_cloud_api-1.4.1 → hyxi_cloud_api-1.4.2}/src/hyxi_cloud_api.egg-info/top_level.txt +0 -0
- {hyxi_cloud_api-1.4.1 → hyxi_cloud_api-1.4.2}/tests/test_alarm_push.py +0 -0
- {hyxi_cloud_api-1.4.1 → hyxi_cloud_api-1.4.2}/tests/test_all_in_one.py +0 -0
- {hyxi_cloud_api-1.4.1 → hyxi_cloud_api-1.4.2}/tests/test_api_imports.py +0 -0
- {hyxi_cloud_api-1.4.1 → hyxi_cloud_api-1.4.2}/tests/test_build_plant_tasks.py +0 -0
- {hyxi_cloud_api-1.4.1 → hyxi_cloud_api-1.4.2}/tests/test_caching.py +0 -0
- {hyxi_cloud_api-1.4.1 → hyxi_cloud_api-1.4.2}/tests/test_compute_battery_metrics.py +0 -0
- {hyxi_cloud_api-1.4.1 → hyxi_cloud_api-1.4.2}/tests/test_compute_derived_metrics.py +0 -0
- {hyxi_cloud_api-1.4.1 → hyxi_cloud_api-1.4.2}/tests/test_compute_grid_metrics.py +0 -0
- {hyxi_cloud_api-1.4.1 → hyxi_cloud_api-1.4.2}/tests/test_compute_load_metrics.py +0 -0
- {hyxi_cloud_api-1.4.1 → hyxi_cloud_api-1.4.2}/tests/test_compute_pv_metrics.py +0 -0
- {hyxi_cloud_api-1.4.1 → hyxi_cloud_api-1.4.2}/tests/test_device_entry.py +0 -0
- {hyxi_cloud_api-1.4.1 → hyxi_cloud_api-1.4.2}/tests/test_devices_errors.py +0 -0
- {hyxi_cloud_api-1.4.1 → hyxi_cloud_api-1.4.2}/tests/test_discovery.py +0 -0
- {hyxi_cloud_api-1.4.1 → hyxi_cloud_api-1.4.2}/tests/test_ensure_authenticated.py +0 -0
- {hyxi_cloud_api-1.4.1 → hyxi_cloud_api-1.4.2}/tests/test_execute_device_tasks.py +0 -0
- {hyxi_cloud_api-1.4.1 → hyxi_cloud_api-1.4.2}/tests/test_extract_battery_info.py +0 -0
- {hyxi_cloud_api-1.4.1 → hyxi_cloud_api-1.4.2}/tests/test_extract_device_info_metadata.py +0 -0
- {hyxi_cloud_api-1.4.1 → hyxi_cloud_api-1.4.2}/tests/test_fetch_and_process_alarms.py +0 -0
- {hyxi_cloud_api-1.4.1 → hyxi_cloud_api-1.4.2}/tests/test_fetch_device_list_for_plant.py +0 -0
- {hyxi_cloud_api-1.4.1 → hyxi_cloud_api-1.4.2}/tests/test_fetch_plants.py +0 -0
- {hyxi_cloud_api-1.4.1 → hyxi_cloud_api-1.4.2}/tests/test_fetch_state.py +0 -0
- {hyxi_cloud_api-1.4.1 → hyxi_cloud_api-1.4.2}/tests/test_fetch_sub_device_list.py +0 -0
- {hyxi_cloud_api-1.4.1 → hyxi_cloud_api-1.4.2}/tests/test_filter_metrics.py +0 -0
- {hyxi_cloud_api-1.4.1 → hyxi_cloud_api-1.4.2}/tests/test_flatten_nested_push_device.py +0 -0
- {hyxi_cloud_api-1.4.1 → hyxi_cloud_api-1.4.2}/tests/test_fuzz_parser.py +0 -0
- {hyxi_cloud_api-1.4.1 → hyxi_cloud_api-1.4.2}/tests/test_get_f.py +0 -0
- {hyxi_cloud_api-1.4.1 → hyxi_cloud_api-1.4.2}/tests/test_info_errors.py +0 -0
- {hyxi_cloud_api-1.4.1 → hyxi_cloud_api-1.4.2}/tests/test_mask_id.py +0 -0
- {hyxi_cloud_api-1.4.1 → hyxi_cloud_api-1.4.2}/tests/test_metrics_errors.py +0 -0
- {hyxi_cloud_api-1.4.1 → hyxi_cloud_api-1.4.2}/tests/test_parse_data_list.py +0 -0
- {hyxi_cloud_api-1.4.1 → hyxi_cloud_api-1.4.2}/tests/test_parse_ems_kv.py +0 -0
- {hyxi_cloud_api-1.4.1 → hyxi_cloud_api-1.4.2}/tests/test_parser.py +0 -0
- {hyxi_cloud_api-1.4.1 → hyxi_cloud_api-1.4.2}/tests/test_push.py +0 -0
- {hyxi_cloud_api-1.4.1 → hyxi_cloud_api-1.4.2}/tests/test_sanitize_dict.py +0 -0
- {hyxi_cloud_api-1.4.1 → hyxi_cloud_api-1.4.2}/tests/test_sanitize_list.py +0 -0
- {hyxi_cloud_api-1.4.1 → hyxi_cloud_api-1.4.2}/tests/test_security_fix.py +0 -0
- {hyxi_cloud_api-1.4.1 → hyxi_cloud_api-1.4.2}/tests/test_set_log_salt.py +0 -0
- {hyxi_cloud_api-1.4.1 → hyxi_cloud_api-1.4.2}/tests/test_token_errors.py +0 -0
- {hyxi_cloud_api-1.4.1 → hyxi_cloud_api-1.4.2}/tests/test_token_handling.py +0 -0
- {hyxi_cloud_api-1.4.1 → hyxi_cloud_api-1.4.2}/tests/test_validate_post_rate_ms.py +0 -0
- {hyxi_cloud_api-1.4.1 → hyxi_cloud_api-1.4.2}/tests/test_validate_subscription_device_sns.py +0 -0
|
@@ -5,5 +5,5 @@ from .api import HyxiApiClient
|
|
|
5
5
|
# Module-level alias so callers can do: from hyxi_cloud_api import VPP_ACTIVE_MODES
|
|
6
6
|
VPP_ACTIVE_MODES: frozenset[str] = HyxiApiClient.VPP_ACTIVE_MODES
|
|
7
7
|
|
|
8
|
-
__version__ = "1.4.
|
|
8
|
+
__version__ = "1.4.2"
|
|
9
9
|
__all__ = ["VPP_ACTIVE_MODES", "HyxiApiClient"]
|
|
@@ -526,6 +526,23 @@ INTERNAL_ERROR_MAP = {
|
|
|
526
526
|
"C999999": "Service exception, please contact the service provide",
|
|
527
527
|
}
|
|
528
528
|
|
|
529
|
+
_TOKEN_REJECTION_CODES = frozenset(
|
|
530
|
+
(
|
|
531
|
+
"A000001",
|
|
532
|
+
"A000002",
|
|
533
|
+
"A000003",
|
|
534
|
+
"A000004",
|
|
535
|
+
"A000005",
|
|
536
|
+
"A000006",
|
|
537
|
+
"A000007",
|
|
538
|
+
"A000008",
|
|
539
|
+
"A000009",
|
|
540
|
+
"A000010",
|
|
541
|
+
"A000011",
|
|
542
|
+
"C000006",
|
|
543
|
+
)
|
|
544
|
+
)
|
|
545
|
+
|
|
529
546
|
|
|
530
547
|
# Official HYXI Device Type Reference Table
|
|
531
548
|
DEVICE_TYPE_MAP = {
|
|
@@ -1212,21 +1229,16 @@ class HyxiApiClient: # pylint: disable=too-many-instance-attributes
|
|
|
1212
1229
|
response.raise_for_status()
|
|
1213
1230
|
res = await response.json()
|
|
1214
1231
|
|
|
1215
|
-
if (
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
"
|
|
1225
|
-
res.get("code"),
|
|
1226
|
-
)
|
|
1227
|
-
self.token = None
|
|
1228
|
-
self.token_expires_at = 0
|
|
1229
|
-
raise TokenRejectedError("Server rejected token")
|
|
1232
|
+
if not is_token_request and not res.get("success") and res.get("code"):
|
|
1233
|
+
api_code = res.get("code")
|
|
1234
|
+
if api_code in _TOKEN_REJECTION_CODES:
|
|
1235
|
+
_LOGGER.debug(
|
|
1236
|
+
"HYXI Server rejected our token (%s). Forcing immediate token refresh...",
|
|
1237
|
+
api_code,
|
|
1238
|
+
)
|
|
1239
|
+
self.token = None
|
|
1240
|
+
self.token_expires_at = 0
|
|
1241
|
+
raise TokenRejectedError("Server rejected token")
|
|
1230
1242
|
|
|
1231
1243
|
return status, res
|
|
1232
1244
|
|
|
@@ -1302,6 +1314,28 @@ class HyxiApiClient: # pylint: disable=too-many-instance-attributes
|
|
|
1302
1314
|
if not token_status:
|
|
1303
1315
|
raise error_cls("Could not obtain API token")
|
|
1304
1316
|
|
|
1317
|
+
async def _execute_with_auth_retry(
|
|
1318
|
+
self, method: str, path: str, error_cls: type[Exception], **kwargs
|
|
1319
|
+
) -> dict:
|
|
1320
|
+
"""Execute a request with automatic re-authentication if the token is rejected."""
|
|
1321
|
+
await self._ensure_authenticated(error_cls)
|
|
1322
|
+
try:
|
|
1323
|
+
_, res = await self._request(method, path, **kwargs)
|
|
1324
|
+
except TokenRejectedError:
|
|
1325
|
+
_LOGGER.debug(
|
|
1326
|
+
"Token rejected, forcing re-authentication and retrying request to %s...",
|
|
1327
|
+
path,
|
|
1328
|
+
)
|
|
1329
|
+
await self._ensure_authenticated(error_cls)
|
|
1330
|
+
_, res = await self._request(method, path, **kwargs)
|
|
1331
|
+
|
|
1332
|
+
if res is None or not res.get("success"):
|
|
1333
|
+
code = res.get("code", "unknown") if res else "no_response"
|
|
1334
|
+
msg = res.get("msg", "") if res else ""
|
|
1335
|
+
raise error_cls(f"request failed (code={code}): {msg}")
|
|
1336
|
+
|
|
1337
|
+
return res
|
|
1338
|
+
|
|
1305
1339
|
async def _fetch_device_metrics(self, sn, entry):
|
|
1306
1340
|
"""Helper to fetch detailed metrics for a single device."""
|
|
1307
1341
|
q_path = "/api/device/v1/queryDeviceData"
|
|
@@ -1539,7 +1573,7 @@ class HyxiApiClient: # pylint: disable=too-many-instance-attributes
|
|
|
1539
1573
|
|
|
1540
1574
|
self._update_discovery_cache(sn, entry)
|
|
1541
1575
|
|
|
1542
|
-
state.metric_tasks.append(
|
|
1576
|
+
state.metric_tasks.append((sn, entry, dev_type))
|
|
1543
1577
|
|
|
1544
1578
|
# 🚀 DEEP DISCOVERY: If this is a Collector, DMU, or Inverter, find its children!
|
|
1545
1579
|
if dev_type in _parent_device_types:
|
|
@@ -1606,10 +1640,8 @@ class HyxiApiClient: # pylint: disable=too-many-instance-attributes
|
|
|
1606
1640
|
|
|
1607
1641
|
self._update_discovery_cache(sn, entry)
|
|
1608
1642
|
|
|
1609
|
-
# These are real devices, so
|
|
1610
|
-
state.metric_tasks.append(
|
|
1611
|
-
self._fetch_all_for_device(sn, entry, raw_type)
|
|
1612
|
-
)
|
|
1643
|
+
# These are real devices, so store args for later metric/info fetch
|
|
1644
|
+
state.metric_tasks.append((sn, entry, raw_type))
|
|
1613
1645
|
|
|
1614
1646
|
except TokenRejectedError: # pylint: disable=try-except-raise
|
|
1615
1647
|
raise
|
|
@@ -1767,11 +1799,10 @@ class HyxiApiClient: # pylint: disable=too-many-instance-attributes
|
|
|
1767
1799
|
if device_fetch_tasks:
|
|
1768
1800
|
await asyncio.gather(*device_fetch_tasks)
|
|
1769
1801
|
|
|
1770
|
-
|
|
1771
|
-
async def _execute_metric_tasks(plant_alarms, state: FetchState):
|
|
1802
|
+
async def _execute_metric_tasks(self, plant_alarms, state: FetchState):
|
|
1772
1803
|
"""Helper to conditionally execute metrics and map alarms."""
|
|
1773
1804
|
if state.metric_tasks:
|
|
1774
|
-
await
|
|
1805
|
+
await self._execute_metrics_and_map_alarms(plant_alarms, state)
|
|
1775
1806
|
|
|
1776
1807
|
async def _process_plants_data(
|
|
1777
1808
|
self, state: FetchState, allow_back_discovery: bool = False
|
|
@@ -1808,11 +1839,11 @@ class HyxiApiClient: # pylint: disable=too-many-instance-attributes
|
|
|
1808
1839
|
state.discovered_sns.add(sn)
|
|
1809
1840
|
|
|
1810
1841
|
entry, dev_type = HyxiApiClient._build_device_entry(sn, a, state.now)
|
|
1811
|
-
state.metric_tasks.append(
|
|
1842
|
+
state.metric_tasks.append((sn, entry, dev_type))
|
|
1812
1843
|
|
|
1813
1844
|
# 🚀 DEEP BACK-DISCOVERY: If this is a parent, search for ITS children too!
|
|
1814
1845
|
if dev_type in _parent_device_types:
|
|
1815
|
-
sub_device_tasks.append(
|
|
1846
|
+
sub_device_tasks.append((sn, state))
|
|
1816
1847
|
|
|
1817
1848
|
async def _process_alarms_and_back_discovery(
|
|
1818
1849
|
self,
|
|
@@ -1841,12 +1872,12 @@ class HyxiApiClient: # pylint: disable=too-many-instance-attributes
|
|
|
1841
1872
|
)
|
|
1842
1873
|
|
|
1843
1874
|
if sub_device_tasks:
|
|
1844
|
-
|
|
1875
|
+
tasks = [self._fetch_sub_devices(sn, s) for sn, s in sub_device_tasks]
|
|
1876
|
+
await asyncio.gather(*tasks)
|
|
1845
1877
|
|
|
1846
1878
|
return plant_alarms
|
|
1847
1879
|
|
|
1848
|
-
|
|
1849
|
-
async def _execute_metrics_and_map_alarms(plant_alarms, state: FetchState):
|
|
1880
|
+
async def _execute_metrics_and_map_alarms(self, plant_alarms, state: FetchState):
|
|
1850
1881
|
"""Helper to execute metric tasks and map alarms to devices."""
|
|
1851
1882
|
# Precompute alarm mapping to optimize from O(N*M) to O(N+M)
|
|
1852
1883
|
alarms_by_sn = defaultdict(list)
|
|
@@ -1855,12 +1886,19 @@ class HyxiApiClient: # pylint: disable=too-many-instance-attributes
|
|
|
1855
1886
|
if sn:
|
|
1856
1887
|
alarms_by_sn[sn].append(a)
|
|
1857
1888
|
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1889
|
+
# Convert argument tuples to coroutines just in time
|
|
1890
|
+
tasks = [
|
|
1891
|
+
self._fetch_all_for_device(sn, entry, dev_type)
|
|
1892
|
+
for sn, entry, dev_type in state.metric_tasks
|
|
1893
|
+
]
|
|
1894
|
+
|
|
1895
|
+
if tasks:
|
|
1896
|
+
updated_entries = await asyncio.gather(*tasks)
|
|
1897
|
+
for sn, entry in updated_entries:
|
|
1898
|
+
if sn:
|
|
1899
|
+
# Map the relevant active alarms to this specific device
|
|
1900
|
+
entry["alarms"] = alarms_by_sn.get(sn, [])
|
|
1901
|
+
state.results[sn] = entry
|
|
1864
1902
|
|
|
1865
1903
|
async def _execute_fetch_all(
|
|
1866
1904
|
self, allow_back_discovery: bool = False, force_discovery: bool = False
|
|
@@ -1907,9 +1945,7 @@ class HyxiApiClient: # pylint: disable=too-many-instance-attributes
|
|
|
1907
1945
|
"hw_version": info.get("hw_version"),
|
|
1908
1946
|
"metrics": {"last_seen": state.now},
|
|
1909
1947
|
}
|
|
1910
|
-
state.metric_tasks.append(
|
|
1911
|
-
self._fetch_all_for_device(sn, entry, info["device_type_code"])
|
|
1912
|
-
)
|
|
1948
|
+
state.metric_tasks.append((sn, entry, info["device_type_code"]))
|
|
1913
1949
|
state.discovered_sns = set(info_cache.keys())
|
|
1914
1950
|
|
|
1915
1951
|
# Fetch alarms (to allow back-discovery if enabled) and metrics
|
|
@@ -1985,8 +2021,6 @@ class HyxiApiClient: # pylint: disable=too-many-instance-attributes
|
|
|
1985
2021
|
_LOGGER.warning("set_device_control called with empty settings")
|
|
1986
2022
|
return {}
|
|
1987
2023
|
|
|
1988
|
-
await self._ensure_authenticated(self.ControlError)
|
|
1989
|
-
|
|
1990
2024
|
path = "/api/device/v2/control"
|
|
1991
2025
|
body = {
|
|
1992
2026
|
"deviceControlMap": {device_sn: {str(k): v for k, v in control_map.items()}}
|
|
@@ -1996,33 +2030,18 @@ class HyxiApiClient: # pylint: disable=too-many-instance-attributes
|
|
|
1996
2030
|
_mask_id(device_sn),
|
|
1997
2031
|
body["deviceControlMap"][device_sn],
|
|
1998
2032
|
)
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
code = res.get("code", "unknown") if res else "no_response"
|
|
2002
|
-
msg = res.get("msg", "") if res else ""
|
|
2003
|
-
raise self.ControlError(f"controlMap write failed (code={code}): {msg}")
|
|
2004
|
-
_LOGGER.debug(
|
|
2005
|
-
"HYXI CONTROL response for %s: success=%s",
|
|
2006
|
-
_mask_id(device_sn),
|
|
2007
|
-
res.get("success"),
|
|
2033
|
+
return await self._execute_with_auth_retry(
|
|
2034
|
+
"POST", path, self.ControlError, json=body
|
|
2008
2035
|
)
|
|
2009
|
-
return res
|
|
2010
2036
|
|
|
2011
2037
|
# ── Subscription API ────────────────────────────────────────────────
|
|
2012
2038
|
|
|
2013
2039
|
async def _post_subscription(self, path: str, body: dict) -> dict:
|
|
2014
2040
|
"""Send an authenticated subscription request."""
|
|
2015
|
-
await self._ensure_authenticated(self.SubscriptionError)
|
|
2016
|
-
|
|
2017
2041
|
_LOGGER.debug("HYXI subscription request to %s", path)
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
msg = res.get("msg", "") if res else ""
|
|
2022
|
-
raise self.SubscriptionError(
|
|
2023
|
-
f"subscription request failed (code={code}): {msg}"
|
|
2024
|
-
)
|
|
2025
|
-
return res
|
|
2042
|
+
return await self._execute_with_auth_retry(
|
|
2043
|
+
"POST", path, self.SubscriptionError, json=body
|
|
2044
|
+
)
|
|
2026
2045
|
|
|
2027
2046
|
@staticmethod
|
|
2028
2047
|
def _validate_subscription_device_sns(device_sn_list: list[str]) -> None:
|
|
@@ -34,8 +34,8 @@ async def test_fetch_sub_devices_coverage(caplog):
|
|
|
34
34
|
# PARENT_SN is masked to ca7b0e8c
|
|
35
35
|
assert "HYXI Found 3 sub-devices under " in caplog.text
|
|
36
36
|
# Verify duplicate/empty check worked (line 1253)
|
|
37
|
-
assert
|
|
38
|
-
|
|
37
|
+
assert len(state.metric_tasks) == 1
|
|
38
|
+
assert state.metric_tasks[0] == (
|
|
39
39
|
"NEW_SN",
|
|
40
40
|
{
|
|
41
41
|
"sn": "NEW_SN",
|
|
@@ -337,13 +337,15 @@ async def test_execute_fetch_all_concurrent():
|
|
|
337
337
|
# Mock the _fetch_devices_for_plant internal call
|
|
338
338
|
# It must return an awaitable AND add an awaitable to state.metric_tasks
|
|
339
339
|
async def mock_fetch_devices(plant_id, state):
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
state.metric_tasks.append(mock_metric_task())
|
|
340
|
+
state.metric_tasks.append(
|
|
341
|
+
(f"SN_{plant_id}", {"device_name": f"Device {plant_id}"}, "MOCK")
|
|
342
|
+
)
|
|
344
343
|
return None
|
|
345
344
|
|
|
346
345
|
api._fetch_devices_for_plant = MagicMock(side_effect=mock_fetch_devices)
|
|
346
|
+
api._fetch_all_for_device = AsyncMock(
|
|
347
|
+
side_effect=lambda sn, entry, dev_type: (sn, entry)
|
|
348
|
+
)
|
|
347
349
|
|
|
348
350
|
# Configure the mock response to simulate aiohttp's async context manager.
|
|
349
351
|
mock_response = MagicMock()
|
|
@@ -145,7 +145,7 @@ async def test_control_error_on_api_failure():
|
|
|
145
145
|
)
|
|
146
146
|
)
|
|
147
147
|
|
|
148
|
-
with pytest.raises(api.ControlError, match="
|
|
148
|
+
with pytest.raises(api.ControlError, match="request failed"):
|
|
149
149
|
await api.set_mode_charge("SN123", watts=3000)
|
|
150
150
|
|
|
151
151
|
|
|
@@ -372,7 +372,7 @@ async def test_set_device_control_no_response():
|
|
|
372
372
|
api._request = AsyncMock(return_value=(200, None))
|
|
373
373
|
|
|
374
374
|
with pytest.raises(
|
|
375
|
-
api.ControlError, match="
|
|
375
|
+
api.ControlError, match="request failed \\(code=no_response\\):"
|
|
376
376
|
):
|
|
377
377
|
await api.set_device_control("SN123", {"1062": "1"})
|
|
378
378
|
|
|
@@ -387,7 +387,7 @@ async def test_set_device_control_api_failure():
|
|
|
387
387
|
)
|
|
388
388
|
|
|
389
389
|
with pytest.raises(
|
|
390
|
-
api.ControlError, match="
|
|
390
|
+
api.ControlError, match="request failed \\(code=E123\\): API Error"
|
|
391
391
|
):
|
|
392
392
|
await api.set_device_control("SN123", {"1062": "1"})
|
|
393
393
|
|
|
@@ -23,22 +23,27 @@ from src.hyxi_cloud_api.api import FetchState, HyxiApiClient
|
|
|
23
23
|
@pytest.mark.asyncio
|
|
24
24
|
async def test_execute_metric_tasks_with_tasks():
|
|
25
25
|
"""Test _execute_metric_tasks when metric tasks are present."""
|
|
26
|
+
api = HyxiApiClient("ak", "sk", "https://api.com", MagicMock())
|
|
26
27
|
state = FetchState(plants=[{"id": 1}], now="2024-01-01T00:00:00Z")
|
|
27
|
-
state.metric_tasks = [
|
|
28
|
+
state.metric_tasks = [("SN", {}, "TYPE")]
|
|
29
|
+
api._execute_metrics_and_map_alarms = AsyncMock()
|
|
28
30
|
plant_alarms = {}
|
|
29
31
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
32
|
+
api._fetch_all_for_device = AsyncMock()
|
|
33
|
+
with patch("asyncio.gather", new_callable=AsyncMock):
|
|
34
|
+
await api._execute_metric_tasks(plant_alarms, state)
|
|
35
|
+
api._execute_metrics_and_map_alarms.assert_awaited_once()
|
|
33
36
|
|
|
34
37
|
|
|
35
38
|
@pytest.mark.asyncio
|
|
36
39
|
async def test_execute_metric_tasks_without_tasks():
|
|
37
40
|
"""Test _execute_metric_tasks when metric tasks are absent."""
|
|
41
|
+
api = HyxiApiClient("ak", "sk", "https://api.com", MagicMock())
|
|
38
42
|
state = FetchState(plants=[{"id": 1}], now="2024-01-01T00:00:00Z")
|
|
39
43
|
state.metric_tasks = []
|
|
40
44
|
plant_alarms = {}
|
|
45
|
+
api._execute_metrics_and_map_alarms = AsyncMock()
|
|
41
46
|
|
|
42
|
-
with patch("asyncio.gather", new_callable=AsyncMock)
|
|
43
|
-
await
|
|
44
|
-
|
|
47
|
+
with patch("asyncio.gather", new_callable=AsyncMock):
|
|
48
|
+
await api._execute_metric_tasks(plant_alarms, state)
|
|
49
|
+
api._execute_metrics_and_map_alarms.assert_not_awaited()
|
|
@@ -16,7 +16,6 @@ if "aiohttp" not in sys.modules or not hasattr(sys.modules["aiohttp"], "ClientEr
|
|
|
16
16
|
sys.modules["aiohttp"] = m
|
|
17
17
|
mock_aiohttp = sys.modules["aiohttp"]
|
|
18
18
|
|
|
19
|
-
import asyncio
|
|
20
19
|
|
|
21
20
|
import pytest
|
|
22
21
|
|
|
@@ -34,17 +33,20 @@ async def test_execute_metrics_and_map_alarms():
|
|
|
34
33
|
|
|
35
34
|
state = FetchState(now="2023-10-27")
|
|
36
35
|
|
|
37
|
-
async def mock_metric_task(sn, result):
|
|
38
|
-
return (sn, result)
|
|
39
|
-
|
|
40
36
|
state.metric_tasks = [
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
37
|
+
("SN1", {"data": 1}, "TYPE"),
|
|
38
|
+
("SN2", {"data": 2}, "TYPE"),
|
|
39
|
+
("SN3", {"data": 3}, "TYPE"), # No alarms
|
|
40
|
+
(None, {"data": 4}, "TYPE"), # None SN
|
|
45
41
|
]
|
|
46
42
|
|
|
47
|
-
|
|
43
|
+
api = HyxiApiClient("ak", "sk", "https://api.com", MagicMock())
|
|
44
|
+
|
|
45
|
+
async def mock_fetch(sn, entry, t):
|
|
46
|
+
return (sn, entry)
|
|
47
|
+
|
|
48
|
+
api._fetch_all_for_device = mock_fetch
|
|
49
|
+
await api._execute_metrics_and_map_alarms(plant_alarms, state)
|
|
48
50
|
|
|
49
51
|
assert state.results["SN1"]["data"] == 1
|
|
50
52
|
assert len(state.results["SN1"]["alarms"]) == 2
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
# pylint: disable=redefined-outer-name
|
|
3
3
|
|
|
4
4
|
import sys
|
|
5
|
-
from unittest.mock import
|
|
5
|
+
from unittest.mock import AsyncMock, MagicMock
|
|
6
6
|
|
|
7
7
|
if "aiohttp" not in sys.modules or not hasattr(sys.modules["aiohttp"], "ClientError"):
|
|
8
8
|
m = MagicMock()
|
|
@@ -73,17 +73,16 @@ def test_handle_back_discovery_alarm_success_non_parent(mock_api, mock_state):
|
|
|
73
73
|
"""Test successful discovery of a non-parent device."""
|
|
74
74
|
sub_device_tasks = []
|
|
75
75
|
alarm = {"deviceSn": "SN12345", "deviceType": "METER", "deviceName": "My Meter"}
|
|
76
|
-
mock_api._fetch_all_for_device =
|
|
76
|
+
mock_api._fetch_all_for_device = AsyncMock()
|
|
77
77
|
|
|
78
78
|
mock_api._handle_back_discovery_alarm(alarm, "plant1", mock_state, sub_device_tasks)
|
|
79
79
|
|
|
80
80
|
assert "SN12345" in mock_state.discovered_sns
|
|
81
81
|
assert len(mock_state.metric_tasks) == 1
|
|
82
|
-
assert mock_state.metric_tasks[0] == "
|
|
82
|
+
assert mock_state.metric_tasks[0][0] == "SN12345"
|
|
83
83
|
assert not sub_device_tasks
|
|
84
84
|
|
|
85
|
-
|
|
86
|
-
args, _ = mock_api._fetch_all_for_device.call_args
|
|
85
|
+
args = mock_state.metric_tasks[0]
|
|
87
86
|
assert args[0] == "SN12345"
|
|
88
87
|
assert args[1]["sn"] == "SN12345"
|
|
89
88
|
assert args[1]["device_name"] == "My Meter"
|
|
@@ -95,17 +94,13 @@ def test_handle_back_discovery_alarm_success_parent(mock_api, mock_state):
|
|
|
95
94
|
"""Test successful discovery of a parent device."""
|
|
96
95
|
sub_device_tasks = []
|
|
97
96
|
alarm = {"deviceSn": "SN_COLL_1", "deviceType": "COLLECTOR"}
|
|
98
|
-
mock_api._fetch_all_for_device =
|
|
99
|
-
mock_api._fetch_sub_devices = MagicMock(return_value="sub_task")
|
|
97
|
+
mock_api._fetch_all_for_device = AsyncMock()
|
|
100
98
|
|
|
101
99
|
mock_api._handle_back_discovery_alarm(alarm, "plant1", mock_state, sub_device_tasks)
|
|
102
100
|
|
|
103
101
|
assert "SN_COLL_1" in mock_state.discovered_sns
|
|
104
|
-
assert mock_state.metric_tasks ==
|
|
105
|
-
assert sub_device_tasks ==
|
|
106
|
-
|
|
107
|
-
mock_api._fetch_all_for_device.assert_called_with("SN_COLL_1", ANY, "COLLECTOR")
|
|
108
|
-
mock_api._fetch_sub_devices.assert_called_with("SN_COLL_1", mock_state)
|
|
102
|
+
assert len(mock_state.metric_tasks) == 1
|
|
103
|
+
assert len(sub_device_tasks) == 1
|
|
109
104
|
|
|
110
105
|
|
|
111
106
|
def test_handle_back_discovery_alarm_fallback_name(mock_api, mock_state):
|
|
@@ -116,7 +111,7 @@ def test_handle_back_discovery_alarm_fallback_name(mock_api, mock_state):
|
|
|
116
111
|
|
|
117
112
|
mock_api._handle_back_discovery_alarm(alarm, "plant1", mock_state, sub_device_tasks)
|
|
118
113
|
|
|
119
|
-
args
|
|
114
|
+
args = mock_state.metric_tasks[0]
|
|
120
115
|
assert args[1]["device_name"] == "Hybrid Inverter SN_INV_1"
|
|
121
116
|
assert args[1]["model"] == "Hybrid Inverter"
|
|
122
117
|
|
|
@@ -129,6 +124,6 @@ def test_handle_back_discovery_alarm_unknown_type(mock_api, mock_state):
|
|
|
129
124
|
|
|
130
125
|
mock_api._handle_back_discovery_alarm(alarm, "plant1", mock_state, sub_device_tasks)
|
|
131
126
|
|
|
132
|
-
args
|
|
127
|
+
args = mock_state.metric_tasks[0]
|
|
133
128
|
assert args[1]["model"] == "Unknown Type"
|
|
134
129
|
assert args[1]["device_name"] == "Unknown Type SN_UNKNOWN"
|
{hyxi_cloud_api-1.4.1 → hyxi_cloud_api-1.4.2}/tests/test_process_alarms_and_back_discovery.py
RENAMED
|
@@ -114,7 +114,7 @@ async def test_process_alarms_gathers_sub_device_tasks(
|
|
|
114
114
|
"""Test that sub-device tasks created during back-discovery are awaited."""
|
|
115
115
|
|
|
116
116
|
def side_effect(a, plant_id, state, sub_device_tasks):
|
|
117
|
-
sub_device_tasks.append("
|
|
117
|
+
sub_device_tasks.append(("sn", state))
|
|
118
118
|
|
|
119
119
|
mock_api._handle_back_discovery_alarm = MagicMock(side_effect=side_effect)
|
|
120
120
|
alarm_results = [[{"id": "a1"}]]
|
|
@@ -123,4 +123,4 @@ async def test_process_alarms_gathers_sub_device_tasks(
|
|
|
123
123
|
alarm_results, mock_state, allow_back_discovery=True
|
|
124
124
|
)
|
|
125
125
|
|
|
126
|
-
mock_gather.
|
|
126
|
+
mock_gather.assert_called_once()
|
|
@@ -23,7 +23,7 @@ def mock_api():
|
|
|
23
23
|
"""Fixture for a mock API client."""
|
|
24
24
|
api = HyxiApiClient("ak", "sk", "https://api.com", MagicMock())
|
|
25
25
|
api._update_discovery_cache = MagicMock()
|
|
26
|
-
api._fetch_all_for_device =
|
|
26
|
+
api._fetch_all_for_device = AsyncMock()
|
|
27
27
|
api._fetch_sub_devices = MagicMock(return_value="fetch_sub_device_task")
|
|
28
28
|
return api
|
|
29
29
|
|
|
@@ -85,7 +85,7 @@ async def test_process_devices_for_plant_normal_devices(mock_api, mock_state):
|
|
|
85
85
|
assert "SN_NORMAL_2" in mock_state.discovered_sns
|
|
86
86
|
assert mock_api._update_discovery_cache.call_count == 2
|
|
87
87
|
assert len(mock_state.metric_tasks) == 2
|
|
88
|
-
assert mock_state.metric_tasks ==
|
|
88
|
+
assert len(mock_state.metric_tasks) == 2
|
|
89
89
|
|
|
90
90
|
|
|
91
91
|
@pytest.mark.asyncio
|
|
@@ -188,7 +188,7 @@ async def test_subscription_error_on_api_failure():
|
|
|
188
188
|
)
|
|
189
189
|
)
|
|
190
190
|
|
|
191
|
-
with pytest.raises(api.SubscriptionError, match="
|
|
191
|
+
with pytest.raises(api.SubscriptionError, match="request failed"):
|
|
192
192
|
await api.subscribe_real_time_data("https://example.com/hyxi", ["SN1"], 60000)
|
|
193
193
|
|
|
194
194
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{hyxi_cloud_api-1.4.1 → hyxi_cloud_api-1.4.2}/src/hyxi_cloud_api.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{hyxi_cloud_api-1.4.1 → hyxi_cloud_api-1.4.2}/tests/test_validate_subscription_device_sns.py
RENAMED
|
File without changes
|