hyxi-cloud-api 1.2.2__tar.gz → 1.2.3__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.2.2/src/hyxi_cloud_api.egg-info → hyxi_cloud_api-1.2.3}/PKG-INFO +1 -1
- {hyxi_cloud_api-1.2.2 → hyxi_cloud_api-1.2.3}/pyproject.toml +1 -1
- {hyxi_cloud_api-1.2.2 → hyxi_cloud_api-1.2.3}/src/hyxi_cloud_api/api.py +49 -27
- {hyxi_cloud_api-1.2.2 → hyxi_cloud_api-1.2.3/src/hyxi_cloud_api.egg-info}/PKG-INFO +1 -1
- {hyxi_cloud_api-1.2.2 → hyxi_cloud_api-1.2.3}/tests/test_api.py +20 -0
- {hyxi_cloud_api-1.2.2 → hyxi_cloud_api-1.2.3}/tests/test_device_control.py +5 -14
- {hyxi_cloud_api-1.2.2 → hyxi_cloud_api-1.2.3}/LICENSE +0 -0
- {hyxi_cloud_api-1.2.2 → hyxi_cloud_api-1.2.3}/README.md +0 -0
- {hyxi_cloud_api-1.2.2 → hyxi_cloud_api-1.2.3}/setup.cfg +0 -0
- {hyxi_cloud_api-1.2.2 → hyxi_cloud_api-1.2.3}/src/hyxi_cloud_api/__init__.py +0 -0
- {hyxi_cloud_api-1.2.2 → hyxi_cloud_api-1.2.3}/src/hyxi_cloud_api.egg-info/SOURCES.txt +0 -0
- {hyxi_cloud_api-1.2.2 → hyxi_cloud_api-1.2.3}/src/hyxi_cloud_api.egg-info/dependency_links.txt +0 -0
- {hyxi_cloud_api-1.2.2 → hyxi_cloud_api-1.2.3}/src/hyxi_cloud_api.egg-info/requires.txt +0 -0
- {hyxi_cloud_api-1.2.2 → hyxi_cloud_api-1.2.3}/src/hyxi_cloud_api.egg-info/top_level.txt +0 -0
- {hyxi_cloud_api-1.2.2 → hyxi_cloud_api-1.2.3}/tests/test_all_in_one.py +0 -0
- {hyxi_cloud_api-1.2.2 → hyxi_cloud_api-1.2.3}/tests/test_build_plant_tasks.py +0 -0
- {hyxi_cloud_api-1.2.2 → hyxi_cloud_api-1.2.3}/tests/test_caching.py +0 -0
- {hyxi_cloud_api-1.2.2 → hyxi_cloud_api-1.2.3}/tests/test_compute_derived_metrics.py +0 -0
- {hyxi_cloud_api-1.2.2 → hyxi_cloud_api-1.2.3}/tests/test_device_entry.py +0 -0
- {hyxi_cloud_api-1.2.2 → hyxi_cloud_api-1.2.3}/tests/test_devices_errors.py +0 -0
- {hyxi_cloud_api-1.2.2 → hyxi_cloud_api-1.2.3}/tests/test_discovery.py +0 -0
- {hyxi_cloud_api-1.2.2 → hyxi_cloud_api-1.2.3}/tests/test_execute_device_tasks.py +0 -0
- {hyxi_cloud_api-1.2.2 → hyxi_cloud_api-1.2.3}/tests/test_execute_metric_tasks.py +0 -0
- {hyxi_cloud_api-1.2.2 → hyxi_cloud_api-1.2.3}/tests/test_execute_metrics_and_map_alarms.py +0 -0
- {hyxi_cloud_api-1.2.2 → hyxi_cloud_api-1.2.3}/tests/test_extract_battery_info.py +0 -0
- {hyxi_cloud_api-1.2.2 → hyxi_cloud_api-1.2.3}/tests/test_extract_device_info_metadata.py +0 -0
- {hyxi_cloud_api-1.2.2 → hyxi_cloud_api-1.2.3}/tests/test_fetch_and_process_alarms.py +0 -0
- {hyxi_cloud_api-1.2.2 → hyxi_cloud_api-1.2.3}/tests/test_fetch_device_list_for_plant.py +0 -0
- {hyxi_cloud_api-1.2.2 → hyxi_cloud_api-1.2.3}/tests/test_fetch_plants.py +0 -0
- {hyxi_cloud_api-1.2.2 → hyxi_cloud_api-1.2.3}/tests/test_fetch_state.py +0 -0
- {hyxi_cloud_api-1.2.2 → hyxi_cloud_api-1.2.3}/tests/test_fetch_sub_device_list.py +0 -0
- {hyxi_cloud_api-1.2.2 → hyxi_cloud_api-1.2.3}/tests/test_filter_metrics.py +0 -0
- {hyxi_cloud_api-1.2.2 → hyxi_cloud_api-1.2.3}/tests/test_fuzz_parser.py +0 -0
- {hyxi_cloud_api-1.2.2 → hyxi_cloud_api-1.2.3}/tests/test_get_f.py +0 -0
- {hyxi_cloud_api-1.2.2 → hyxi_cloud_api-1.2.3}/tests/test_handle_back_discovery_alarm.py +0 -0
- {hyxi_cloud_api-1.2.2 → hyxi_cloud_api-1.2.3}/tests/test_info_errors.py +0 -0
- {hyxi_cloud_api-1.2.2 → hyxi_cloud_api-1.2.3}/tests/test_mask_id.py +0 -0
- {hyxi_cloud_api-1.2.2 → hyxi_cloud_api-1.2.3}/tests/test_metrics_errors.py +0 -0
- {hyxi_cloud_api-1.2.2 → hyxi_cloud_api-1.2.3}/tests/test_parse_data_list.py +0 -0
- {hyxi_cloud_api-1.2.2 → hyxi_cloud_api-1.2.3}/tests/test_parse_ems_kv.py +0 -0
- {hyxi_cloud_api-1.2.2 → hyxi_cloud_api-1.2.3}/tests/test_parser.py +0 -0
- {hyxi_cloud_api-1.2.2 → hyxi_cloud_api-1.2.3}/tests/test_process_alarms_and_back_discovery.py +0 -0
- {hyxi_cloud_api-1.2.2 → hyxi_cloud_api-1.2.3}/tests/test_process_devices_for_plant.py +0 -0
- {hyxi_cloud_api-1.2.2 → hyxi_cloud_api-1.2.3}/tests/test_sanitize_dict.py +0 -0
- {hyxi_cloud_api-1.2.2 → hyxi_cloud_api-1.2.3}/tests/test_sanitize_list.py +0 -0
- {hyxi_cloud_api-1.2.2 → hyxi_cloud_api-1.2.3}/tests/test_security_fix.py +0 -0
- {hyxi_cloud_api-1.2.2 → hyxi_cloud_api-1.2.3}/tests/test_token_errors.py +0 -0
- {hyxi_cloud_api-1.2.2 → hyxi_cloud_api-1.2.3}/tests/test_token_handling.py +0 -0
|
@@ -566,20 +566,19 @@ def _get_f(key: str, data_map: dict, mult: float = 1.0) -> float:
|
|
|
566
566
|
return 0.0
|
|
567
567
|
|
|
568
568
|
|
|
569
|
+
@functools.lru_cache(maxsize=1024)
|
|
570
|
+
def _is_collector_key_allowed(key: str) -> bool:
|
|
571
|
+
"""Check if metric key is allowed for Collectors and cache the result."""
|
|
572
|
+
return not _COLLECTOR_FILTER_REGEX.search(key)
|
|
573
|
+
|
|
574
|
+
|
|
569
575
|
def _filter_collector_metrics(m_raw: dict) -> dict:
|
|
570
576
|
"""Remove battery/power metrics that shouldn't be present on Collectors."""
|
|
571
|
-
return {k: v for k, v in m_raw.items() if
|
|
572
|
-
|
|
577
|
+
return {k: v for k, v in m_raw.items() if _is_collector_key_allowed(k)}
|
|
573
578
|
|
|
574
|
-
def _compute_derived_metrics(m_raw: dict, device_type: str = "") -> dict:
|
|
575
|
-
"""Calculate derived metrics from raw metrics map.
|
|
576
579
|
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
"""
|
|
580
|
-
derived = {}
|
|
581
|
-
|
|
582
|
-
# 1. Load Calculation
|
|
580
|
+
def _compute_load_metrics(m_raw: dict, derived: dict[str, float]) -> None:
|
|
581
|
+
"""Calculate home load metrics."""
|
|
583
582
|
if "ph1Loadp" in m_raw or "ph2Loadp" in m_raw or "ph3Loadp" in m_raw:
|
|
584
583
|
derived["home_load"] = (
|
|
585
584
|
_get_f("ph1Loadp", m_raw)
|
|
@@ -587,21 +586,28 @@ def _compute_derived_metrics(m_raw: dict, device_type: str = "") -> dict:
|
|
|
587
586
|
+ _get_f("ph3Loadp", m_raw)
|
|
588
587
|
)
|
|
589
588
|
|
|
590
|
-
|
|
589
|
+
|
|
590
|
+
def _compute_grid_metrics(m_raw: dict, derived: dict[str, float]) -> None:
|
|
591
|
+
"""Calculate grid import/export metrics."""
|
|
591
592
|
if "gridP" in m_raw:
|
|
592
593
|
grid = _get_f("gridP", m_raw, 1000.0)
|
|
593
594
|
derived["grid_import"] = abs(grid) if grid < 0 else 0.0
|
|
594
595
|
derived["grid_export"] = grid if grid > 0 else 0.0
|
|
595
596
|
|
|
596
|
-
|
|
597
|
+
|
|
598
|
+
def _compute_battery_metrics(
|
|
599
|
+
m_raw: dict, derived: dict[str, float], device_type: str
|
|
600
|
+
) -> None:
|
|
601
|
+
"""Calculate battery charge/discharge metrics."""
|
|
597
602
|
bat_p_dc = _get_f("batP", m_raw)
|
|
598
603
|
pbat = _get_f("pbat", m_raw)
|
|
604
|
+
device_type_str = str(device_type or "").upper()
|
|
599
605
|
|
|
600
|
-
if "batP" in m_raw or "pbat" in m_raw:
|
|
606
|
+
if "batP" in m_raw or "pbat" in m_raw or device_type_str in ("EMS", "15", "16"):
|
|
601
607
|
# ALL_IN_ONE: prefer pbat — batP can have an inverted sign convention,
|
|
602
608
|
# while pbat is consistently negative-for-charging / positive-for-discharging.
|
|
603
609
|
# Other devices: prefer batP (DC terminals), fall back to pbat.
|
|
604
|
-
if
|
|
610
|
+
if device_type_str == "ALL_IN_ONE":
|
|
605
611
|
power_source = pbat if pbat != 0.0 else bat_p_dc
|
|
606
612
|
else:
|
|
607
613
|
power_source = bat_p_dc if bat_p_dc != 0.0 else pbat
|
|
@@ -614,7 +620,9 @@ def _compute_derived_metrics(m_raw: dict, device_type: str = "") -> dict:
|
|
|
614
620
|
if "batDisCharge" in m_raw:
|
|
615
621
|
derived["bat_discharge_total"] = _get_f("batDisCharge", m_raw)
|
|
616
622
|
|
|
617
|
-
|
|
623
|
+
|
|
624
|
+
def _compute_pv_metrics(m_raw: dict, derived: dict[str, float]) -> None:
|
|
625
|
+
"""Calculate PV string powers."""
|
|
618
626
|
for v_k, i_k, p_k in _PV_KEYS:
|
|
619
627
|
if v_k in m_raw or i_k in m_raw or p_k in m_raw:
|
|
620
628
|
derived[p_k] = _get_f(p_k, m_raw) or round(
|
|
@@ -631,15 +639,30 @@ def _compute_derived_metrics(m_raw: dict, device_type: str = "") -> dict:
|
|
|
631
639
|
ppv_total = _get_f("ppv", m_raw)
|
|
632
640
|
derived["pv1p"] = round(max(ppv_total - derived["pv2p"], 0), 2)
|
|
633
641
|
|
|
634
|
-
|
|
635
|
-
|
|
642
|
+
|
|
643
|
+
def _compute_micro_ess_fallback_metrics(m_raw: dict, derived: dict[str, float]) -> None:
|
|
644
|
+
"""Derive standard metrics from Micro ESS specific metrics."""
|
|
636
645
|
if "pvPower" in m_raw and "ppv" not in m_raw:
|
|
637
646
|
derived["ppv"] = _get_f("pvPower", m_raw)
|
|
638
647
|
|
|
639
|
-
# Derive standard Grid Frequency (f) from Micro ESS gridF if f is missing
|
|
640
648
|
if "gridF" in m_raw and "f" not in m_raw:
|
|
641
649
|
derived["f"] = _get_f("gridF", m_raw)
|
|
642
650
|
|
|
651
|
+
|
|
652
|
+
def _compute_derived_metrics(m_raw: dict, device_type: str = "") -> dict:
|
|
653
|
+
"""Calculate derived metrics from raw metrics map.
|
|
654
|
+
|
|
655
|
+
Only keys that have relevant base data in m_raw will be included in the
|
|
656
|
+
resulting dictionary to avoid 'ghost' sensors for unsupported features.
|
|
657
|
+
"""
|
|
658
|
+
derived: dict[str, float] = {}
|
|
659
|
+
|
|
660
|
+
_compute_load_metrics(m_raw, derived)
|
|
661
|
+
_compute_grid_metrics(m_raw, derived)
|
|
662
|
+
_compute_battery_metrics(m_raw, derived, device_type)
|
|
663
|
+
_compute_pv_metrics(m_raw, derived)
|
|
664
|
+
_compute_micro_ess_fallback_metrics(m_raw, derived)
|
|
665
|
+
|
|
643
666
|
return derived
|
|
644
667
|
|
|
645
668
|
|
|
@@ -1647,19 +1670,18 @@ class HyxiApiClient: # pylint: disable=too-many-instance-attributes
|
|
|
1647
1670
|
|
|
1648
1671
|
# ── Microinverter Controls ───────────────────────────────────────────
|
|
1649
1672
|
|
|
1650
|
-
async def
|
|
1651
|
-
"""Turn on a Microinverter (controlId 3011).
|
|
1673
|
+
async def set_micro_power(self, device_sn: str, power_on: bool) -> dict:
|
|
1674
|
+
"""Turn on or off a Microinverter (controlId 3011).
|
|
1652
1675
|
|
|
1653
1676
|
For **MICRO_INVERTER** devices.
|
|
1654
|
-
"""
|
|
1655
|
-
return await self.set_device_control(device_sn, {3011: "1"})
|
|
1656
1677
|
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
For **MICRO_INVERTER** devices.
|
|
1678
|
+
Args:
|
|
1679
|
+
device_sn: Device serial number.
|
|
1680
|
+
power_on: True to turn on ("1"), False to turn off ("0").
|
|
1661
1681
|
"""
|
|
1662
|
-
return await self.set_device_control(
|
|
1682
|
+
return await self.set_device_control(
|
|
1683
|
+
device_sn, {3011: "1" if power_on else "0"}
|
|
1684
|
+
)
|
|
1663
1685
|
|
|
1664
1686
|
async def set_micro_power_limit(self, device_sn: str, percentage: int) -> dict:
|
|
1665
1687
|
"""Set Maximum Power Limitation for a Microinverter (controlId 3012).
|
|
@@ -117,6 +117,26 @@ async def test_get_all_device_data_retry_exhaustion(monkeypatch, caplog):
|
|
|
117
117
|
assert "HYXI Cloud connection failed after 3 attempts" in caplog.text
|
|
118
118
|
|
|
119
119
|
|
|
120
|
+
@pytest.mark.asyncio
|
|
121
|
+
async def test_get_all_device_data_timeout_error(monkeypatch, caplog):
|
|
122
|
+
"""Test that get_all_device_data exhausts retries on TimeoutError."""
|
|
123
|
+
fake_session = MagicMock()
|
|
124
|
+
api = HyxiApiClient("ak", "sk", "https://api.com", fake_session)
|
|
125
|
+
|
|
126
|
+
# Mock _execute_fetch_all to raise TimeoutError
|
|
127
|
+
api._execute_fetch_all = AsyncMock(side_effect=TimeoutError("Connection timed out"))
|
|
128
|
+
|
|
129
|
+
mock_sleep = AsyncMock()
|
|
130
|
+
monkeypatch.setattr("asyncio.sleep", mock_sleep)
|
|
131
|
+
|
|
132
|
+
result = await api.get_all_device_data()
|
|
133
|
+
|
|
134
|
+
assert result is None
|
|
135
|
+
assert api._execute_fetch_all.call_count == 3
|
|
136
|
+
assert mock_sleep.call_count == 2
|
|
137
|
+
assert "HYXI Cloud connection failed after 3 attempts" in caplog.text
|
|
138
|
+
|
|
139
|
+
|
|
120
140
|
@pytest.mark.asyncio
|
|
121
141
|
async def test_get_all_device_data_auth_failed():
|
|
122
142
|
"""Test that get_all_device_data immediately fails on auth failure."""
|
|
@@ -176,28 +176,19 @@ async def test_set_mode_discharge_invalid_watts():
|
|
|
176
176
|
|
|
177
177
|
|
|
178
178
|
@pytest.mark.asyncio
|
|
179
|
-
async def
|
|
180
|
-
"""Test
|
|
179
|
+
async def test_set_micro_power():
|
|
180
|
+
"""Test set_micro_power sends controlId 3011 with value '1' or '0'."""
|
|
181
181
|
api = HyxiApiClient("ak", "sk", "https://api.com", MagicMock())
|
|
182
182
|
api._refresh_token = AsyncMock(return_value=True)
|
|
183
183
|
api._request = AsyncMock(return_value=(200, {"success": True}))
|
|
184
184
|
|
|
185
|
-
await api.
|
|
186
|
-
|
|
185
|
+
await api.set_micro_power("SN123", power_on=True)
|
|
187
186
|
call_kwargs = api._request.call_args
|
|
188
187
|
body = call_kwargs.kwargs.get("json") or call_kwargs[1].get("json")
|
|
189
188
|
assert body["deviceControlMap"]["SN123"]["3011"] == "1"
|
|
190
189
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
async def test_set_micro_power_off():
|
|
194
|
-
"""Test set_micro_power_off sends controlId 3011 with value '0'."""
|
|
195
|
-
api = HyxiApiClient("ak", "sk", "https://api.com", MagicMock())
|
|
196
|
-
api._refresh_token = AsyncMock(return_value=True)
|
|
197
|
-
api._request = AsyncMock(return_value=(200, {"success": True}))
|
|
198
|
-
|
|
199
|
-
await api.set_micro_power_off("SN123")
|
|
200
|
-
|
|
190
|
+
api._request.reset_mock()
|
|
191
|
+
await api.set_micro_power("SN123", power_on=False)
|
|
201
192
|
call_kwargs = api._request.call_args
|
|
202
193
|
body = call_kwargs.kwargs.get("json") or call_kwargs[1].get("json")
|
|
203
194
|
assert body["deviceControlMap"]["SN123"]["3011"] == "0"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{hyxi_cloud_api-1.2.2 → hyxi_cloud_api-1.2.3}/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
|
{hyxi_cloud_api-1.2.2 → hyxi_cloud_api-1.2.3}/tests/test_process_alarms_and_back_discovery.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|