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.
Files changed (48) hide show
  1. {hyxi_cloud_api-1.2.2/src/hyxi_cloud_api.egg-info → hyxi_cloud_api-1.2.3}/PKG-INFO +1 -1
  2. {hyxi_cloud_api-1.2.2 → hyxi_cloud_api-1.2.3}/pyproject.toml +1 -1
  3. {hyxi_cloud_api-1.2.2 → hyxi_cloud_api-1.2.3}/src/hyxi_cloud_api/api.py +49 -27
  4. {hyxi_cloud_api-1.2.2 → hyxi_cloud_api-1.2.3/src/hyxi_cloud_api.egg-info}/PKG-INFO +1 -1
  5. {hyxi_cloud_api-1.2.2 → hyxi_cloud_api-1.2.3}/tests/test_api.py +20 -0
  6. {hyxi_cloud_api-1.2.2 → hyxi_cloud_api-1.2.3}/tests/test_device_control.py +5 -14
  7. {hyxi_cloud_api-1.2.2 → hyxi_cloud_api-1.2.3}/LICENSE +0 -0
  8. {hyxi_cloud_api-1.2.2 → hyxi_cloud_api-1.2.3}/README.md +0 -0
  9. {hyxi_cloud_api-1.2.2 → hyxi_cloud_api-1.2.3}/setup.cfg +0 -0
  10. {hyxi_cloud_api-1.2.2 → hyxi_cloud_api-1.2.3}/src/hyxi_cloud_api/__init__.py +0 -0
  11. {hyxi_cloud_api-1.2.2 → hyxi_cloud_api-1.2.3}/src/hyxi_cloud_api.egg-info/SOURCES.txt +0 -0
  12. {hyxi_cloud_api-1.2.2 → hyxi_cloud_api-1.2.3}/src/hyxi_cloud_api.egg-info/dependency_links.txt +0 -0
  13. {hyxi_cloud_api-1.2.2 → hyxi_cloud_api-1.2.3}/src/hyxi_cloud_api.egg-info/requires.txt +0 -0
  14. {hyxi_cloud_api-1.2.2 → hyxi_cloud_api-1.2.3}/src/hyxi_cloud_api.egg-info/top_level.txt +0 -0
  15. {hyxi_cloud_api-1.2.2 → hyxi_cloud_api-1.2.3}/tests/test_all_in_one.py +0 -0
  16. {hyxi_cloud_api-1.2.2 → hyxi_cloud_api-1.2.3}/tests/test_build_plant_tasks.py +0 -0
  17. {hyxi_cloud_api-1.2.2 → hyxi_cloud_api-1.2.3}/tests/test_caching.py +0 -0
  18. {hyxi_cloud_api-1.2.2 → hyxi_cloud_api-1.2.3}/tests/test_compute_derived_metrics.py +0 -0
  19. {hyxi_cloud_api-1.2.2 → hyxi_cloud_api-1.2.3}/tests/test_device_entry.py +0 -0
  20. {hyxi_cloud_api-1.2.2 → hyxi_cloud_api-1.2.3}/tests/test_devices_errors.py +0 -0
  21. {hyxi_cloud_api-1.2.2 → hyxi_cloud_api-1.2.3}/tests/test_discovery.py +0 -0
  22. {hyxi_cloud_api-1.2.2 → hyxi_cloud_api-1.2.3}/tests/test_execute_device_tasks.py +0 -0
  23. {hyxi_cloud_api-1.2.2 → hyxi_cloud_api-1.2.3}/tests/test_execute_metric_tasks.py +0 -0
  24. {hyxi_cloud_api-1.2.2 → hyxi_cloud_api-1.2.3}/tests/test_execute_metrics_and_map_alarms.py +0 -0
  25. {hyxi_cloud_api-1.2.2 → hyxi_cloud_api-1.2.3}/tests/test_extract_battery_info.py +0 -0
  26. {hyxi_cloud_api-1.2.2 → hyxi_cloud_api-1.2.3}/tests/test_extract_device_info_metadata.py +0 -0
  27. {hyxi_cloud_api-1.2.2 → hyxi_cloud_api-1.2.3}/tests/test_fetch_and_process_alarms.py +0 -0
  28. {hyxi_cloud_api-1.2.2 → hyxi_cloud_api-1.2.3}/tests/test_fetch_device_list_for_plant.py +0 -0
  29. {hyxi_cloud_api-1.2.2 → hyxi_cloud_api-1.2.3}/tests/test_fetch_plants.py +0 -0
  30. {hyxi_cloud_api-1.2.2 → hyxi_cloud_api-1.2.3}/tests/test_fetch_state.py +0 -0
  31. {hyxi_cloud_api-1.2.2 → hyxi_cloud_api-1.2.3}/tests/test_fetch_sub_device_list.py +0 -0
  32. {hyxi_cloud_api-1.2.2 → hyxi_cloud_api-1.2.3}/tests/test_filter_metrics.py +0 -0
  33. {hyxi_cloud_api-1.2.2 → hyxi_cloud_api-1.2.3}/tests/test_fuzz_parser.py +0 -0
  34. {hyxi_cloud_api-1.2.2 → hyxi_cloud_api-1.2.3}/tests/test_get_f.py +0 -0
  35. {hyxi_cloud_api-1.2.2 → hyxi_cloud_api-1.2.3}/tests/test_handle_back_discovery_alarm.py +0 -0
  36. {hyxi_cloud_api-1.2.2 → hyxi_cloud_api-1.2.3}/tests/test_info_errors.py +0 -0
  37. {hyxi_cloud_api-1.2.2 → hyxi_cloud_api-1.2.3}/tests/test_mask_id.py +0 -0
  38. {hyxi_cloud_api-1.2.2 → hyxi_cloud_api-1.2.3}/tests/test_metrics_errors.py +0 -0
  39. {hyxi_cloud_api-1.2.2 → hyxi_cloud_api-1.2.3}/tests/test_parse_data_list.py +0 -0
  40. {hyxi_cloud_api-1.2.2 → hyxi_cloud_api-1.2.3}/tests/test_parse_ems_kv.py +0 -0
  41. {hyxi_cloud_api-1.2.2 → hyxi_cloud_api-1.2.3}/tests/test_parser.py +0 -0
  42. {hyxi_cloud_api-1.2.2 → hyxi_cloud_api-1.2.3}/tests/test_process_alarms_and_back_discovery.py +0 -0
  43. {hyxi_cloud_api-1.2.2 → hyxi_cloud_api-1.2.3}/tests/test_process_devices_for_plant.py +0 -0
  44. {hyxi_cloud_api-1.2.2 → hyxi_cloud_api-1.2.3}/tests/test_sanitize_dict.py +0 -0
  45. {hyxi_cloud_api-1.2.2 → hyxi_cloud_api-1.2.3}/tests/test_sanitize_list.py +0 -0
  46. {hyxi_cloud_api-1.2.2 → hyxi_cloud_api-1.2.3}/tests/test_security_fix.py +0 -0
  47. {hyxi_cloud_api-1.2.2 → hyxi_cloud_api-1.2.3}/tests/test_token_errors.py +0 -0
  48. {hyxi_cloud_api-1.2.2 → hyxi_cloud_api-1.2.3}/tests/test_token_handling.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hyxi-cloud-api
3
- Version: 1.2.2
3
+ Version: 1.2.3
4
4
  Summary: An async API client for HYXi Cloud.
5
5
  Author-email: Veldkornet <Veldkornet@users.noreply.github.com>
6
6
  Classifier: Programming Language :: Python :: 3
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "hyxi-cloud-api"
7
- version = "1.2.2"
7
+ version = "1.2.3"
8
8
  authors = [
9
9
  { name="Veldkornet", email="Veldkornet@users.noreply.github.com" },
10
10
  ]
@@ -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 not _COLLECTOR_FILTER_REGEX.search(k)}
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
- Only keys that have relevant base data in m_raw will be included in the
578
- resulting dictionary to avoid 'ghost' sensors for unsupported features.
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
- # 2. Grid Metrics
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
- # 3. Battery Metrics
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 device_type.upper() == "ALL_IN_ONE":
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
- # 4. PV String Powers (Derived if missing)
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
- # 5. Fallback mappings for Micro ESS
635
- # Derive standard Solar Power (ppv) from Micro ESS pvPower if ppv is missing
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 set_micro_power_on(self, device_sn: str) -> dict:
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
- async def set_micro_power_off(self, device_sn: str) -> dict:
1658
- """Turn off a Microinverter (controlId 3011).
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(device_sn, {3011: "0"})
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).
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hyxi-cloud-api
3
- Version: 1.2.2
3
+ Version: 1.2.3
4
4
  Summary: An async API client for HYXi Cloud.
5
5
  Author-email: Veldkornet <Veldkornet@users.noreply.github.com>
6
6
  Classifier: Programming Language :: Python :: 3
@@ -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 test_set_micro_power_on():
180
- """Test set_micro_power_on sends controlId 3011 with value '1'."""
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.set_micro_power_on("SN123")
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
- @pytest.mark.asyncio
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