hyxi-cloud-api 1.3.5__tar.gz → 1.3.7__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 (58) hide show
  1. {hyxi_cloud_api-1.3.5/src/hyxi_cloud_api.egg-info → hyxi_cloud_api-1.3.7}/PKG-INFO +1 -1
  2. {hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7}/pyproject.toml +1 -1
  3. {hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7}/src/hyxi_cloud_api/__init__.py +1 -1
  4. {hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7}/src/hyxi_cloud_api/api.py +62 -7
  5. {hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7/src/hyxi_cloud_api.egg-info}/PKG-INFO +1 -1
  6. {hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7}/tests/test_compute_battery_metrics.py +31 -0
  7. hyxi_cloud_api-1.3.7/tests/test_compute_grid_metrics.py +79 -0
  8. {hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7}/tests/test_device_control.py +24 -0
  9. {hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7}/tests/test_push.py +14 -0
  10. {hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7}/tests/test_subscriptions.py +57 -0
  11. hyxi_cloud_api-1.3.5/tests/test_compute_grid_metrics.py +0 -39
  12. {hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7}/LICENSE +0 -0
  13. {hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7}/README.md +0 -0
  14. {hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7}/setup.cfg +0 -0
  15. {hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7}/src/hyxi_cloud_api.egg-info/SOURCES.txt +0 -0
  16. {hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7}/src/hyxi_cloud_api.egg-info/dependency_links.txt +0 -0
  17. {hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7}/src/hyxi_cloud_api.egg-info/requires.txt +0 -0
  18. {hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7}/src/hyxi_cloud_api.egg-info/top_level.txt +0 -0
  19. {hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7}/tests/test_additional_coverage.py +0 -0
  20. {hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7}/tests/test_alarm_push.py +0 -0
  21. {hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7}/tests/test_all_in_one.py +0 -0
  22. {hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7}/tests/test_api.py +0 -0
  23. {hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7}/tests/test_build_plant_tasks.py +0 -0
  24. {hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7}/tests/test_caching.py +0 -0
  25. {hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7}/tests/test_compute_derived_metrics.py +0 -0
  26. {hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7}/tests/test_compute_load_metrics.py +0 -0
  27. {hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7}/tests/test_device_entry.py +0 -0
  28. {hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7}/tests/test_devices_errors.py +0 -0
  29. {hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7}/tests/test_discovery.py +0 -0
  30. {hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7}/tests/test_execute_device_tasks.py +0 -0
  31. {hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7}/tests/test_execute_metric_tasks.py +0 -0
  32. {hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7}/tests/test_execute_metrics_and_map_alarms.py +0 -0
  33. {hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7}/tests/test_extract_battery_info.py +0 -0
  34. {hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7}/tests/test_extract_device_info_metadata.py +0 -0
  35. {hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7}/tests/test_fetch_and_process_alarms.py +0 -0
  36. {hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7}/tests/test_fetch_device_list_for_plant.py +0 -0
  37. {hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7}/tests/test_fetch_plants.py +0 -0
  38. {hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7}/tests/test_fetch_state.py +0 -0
  39. {hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7}/tests/test_fetch_sub_device_list.py +0 -0
  40. {hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7}/tests/test_filter_metrics.py +0 -0
  41. {hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7}/tests/test_fuzz_parser.py +0 -0
  42. {hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7}/tests/test_get_f.py +0 -0
  43. {hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7}/tests/test_handle_back_discovery_alarm.py +0 -0
  44. {hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7}/tests/test_info_errors.py +0 -0
  45. {hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7}/tests/test_mask_id.py +0 -0
  46. {hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7}/tests/test_metrics_errors.py +0 -0
  47. {hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7}/tests/test_parse_data_list.py +0 -0
  48. {hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7}/tests/test_parse_ems_kv.py +0 -0
  49. {hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7}/tests/test_parser.py +0 -0
  50. {hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7}/tests/test_process_alarms_and_back_discovery.py +0 -0
  51. {hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7}/tests/test_process_devices_for_plant.py +0 -0
  52. {hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7}/tests/test_sanitize_dict.py +0 -0
  53. {hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7}/tests/test_sanitize_list.py +0 -0
  54. {hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7}/tests/test_security_fix.py +0 -0
  55. {hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7}/tests/test_token_errors.py +0 -0
  56. {hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7}/tests/test_token_handling.py +0 -0
  57. {hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7}/tests/test_validate_post_rate_ms.py +0 -0
  58. {hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7}/tests/test_validate_subscription_device_sns.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hyxi-cloud-api
3
- Version: 1.3.5
3
+ Version: 1.3.7
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.3.5"
7
+ version = "1.3.7"
8
8
  authors = [
9
9
  { name="Veldkornet", email="Veldkornet@users.noreply.github.com" },
10
10
  ]
@@ -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.3.5"
8
+ __version__ = "1.3.7"
9
9
  __all__ = ["VPP_ACTIVE_MODES", "HyxiApiClient"]
@@ -72,6 +72,8 @@ _COLLECTOR_FILTER_REGEX = re.compile(
72
72
 
73
73
  _PV_KEYS = tuple((f"pv{i}v", f"pv{i}i", f"pv{i}p") for i in range(1, 5))
74
74
 
75
+ _METRICS_EXCLUDED_KEYS = frozenset({"deviceSn", "reportTimestamp", "collectTime"})
76
+
75
77
  # Official HYXI Alarm Code Reference Table
76
78
  ALARM_CODE_MAP = {
77
79
  "704": "The ambient temperature is too high",
@@ -600,8 +602,16 @@ def _compute_load_metrics(m_raw: dict, derived: dict[str, float]) -> None:
600
602
 
601
603
  def _compute_grid_metrics(m_raw: dict, derived: dict[str, float]) -> None:
602
604
  """Calculate grid import/export metrics."""
603
- if "gridP" in m_raw:
605
+ grid = None
606
+ if "gridP" in m_raw and m_raw["gridP"] is not None and m_raw["gridP"] != "":
604
607
  grid = _get_f("gridP", m_raw, 1000.0)
608
+ elif "ph1p" in m_raw or "ph2p" in m_raw or "ph3p" in m_raw:
609
+ # Sum up individual phase powers (which are in Watts) to compute grid power fallback
610
+ grid = _get_f("ph1p", m_raw) + _get_f("ph2p", m_raw) + _get_f("ph3p", m_raw)
611
+ # Store the derived gridP back in metrics (in kW to align with API standard format)
612
+ derived["gridP"] = round(grid / 1000.0, 4)
613
+
614
+ if grid is not None:
605
615
  derived["grid_import"] = abs(grid) if grid < 0 else 0.0
606
616
  derived["grid_export"] = grid if grid > 0 else 0.0
607
617
 
@@ -626,10 +636,38 @@ def _compute_battery_metrics(
626
636
  derived["bat_discharging"] = power_source if power_source > 0 else 0.0
627
637
  derived["bat_power_dc"] = bat_p_dc
628
638
 
629
- if "batCharge" in m_raw:
630
- derived["bat_charge_total"] = _get_f("batCharge", m_raw)
631
- if "batDisCharge" in m_raw:
632
- derived["bat_discharge_total"] = _get_f("batDisCharge", m_raw)
639
+ def _is_valid_metric(k: str) -> bool:
640
+ v = m_raw.get(k)
641
+ if v is None:
642
+ return False
643
+ if isinstance(v, str):
644
+ v_cleaned = v.strip().lower()
645
+ return bool(v_cleaned and v_cleaned not in ("null", "none", "na", "--"))
646
+ return True
647
+
648
+ # Unify cumulative battery charge energy telemetry keys to resolve polling/push mismatch
649
+ charge_val = None
650
+ if _is_valid_metric("batCharge"):
651
+ charge_val = _get_f("batCharge", m_raw)
652
+ elif _is_valid_metric("totalEchg"):
653
+ charge_val = _get_f("totalEchg", m_raw)
654
+
655
+ if charge_val is not None:
656
+ derived["bat_charge_total"] = charge_val
657
+ derived["totalEchg"] = charge_val
658
+ derived["batCharge"] = charge_val
659
+
660
+ # Unify cumulative battery discharge energy telemetry keys to resolve polling/push mismatch
661
+ discharge_val = None
662
+ if _is_valid_metric("batDisCharge"):
663
+ discharge_val = _get_f("batDisCharge", m_raw)
664
+ elif _is_valid_metric("totalEdchg"):
665
+ discharge_val = _get_f("totalEdchg", m_raw)
666
+
667
+ if discharge_val is not None:
668
+ derived["bat_discharge_total"] = discharge_val
669
+ derived["totalEdchg"] = discharge_val
670
+ derived["batDisCharge"] = discharge_val
633
671
 
634
672
 
635
673
  def _compute_pv_metrics(m_raw: dict, derived: dict[str, float]) -> None:
@@ -820,6 +858,23 @@ def _flatten_nested_push_device(device: dict) -> dict: # pylint: disable=too-ma
820
858
  if "epsPowerW" in phi:
821
859
  flat[f"ph{i}Loadp"] = phi["epsPowerW"]
822
860
 
861
+ # 9. grid
862
+ if "grid" in device and isinstance(device["grid"], dict):
863
+ grd = device["grid"]
864
+ if "powerW" in grd and grd["powerW"] is not None:
865
+ try:
866
+ flat["gridP"] = float(grd["powerW"]) / 1000.0
867
+ except ValueError, TypeError:
868
+ flat["gridP"] = grd["powerW"]
869
+ if "frequencyHz" in grd:
870
+ flat["gridF"] = grd["frequencyHz"]
871
+ if "powerFactor" in grd:
872
+ flat["gridPfd"] = grd["powerFactor"]
873
+ if "energyInKwh" in grd:
874
+ flat["gridEIn"] = grd["energyInKwh"]
875
+ if "energyOutKwh" in grd:
876
+ flat["gridEOut"] = grd["energyOutKwh"]
877
+
823
878
  # Copy any other keys at the root that aren't dictionaries
824
879
  for k, v in device.items():
825
880
  if k not in flat and not isinstance(v, dict):
@@ -2124,7 +2179,7 @@ class HyxiApiClient: # pylint: disable=too-many-instance-attributes
2124
2179
  # Strip metadata/routing keys
2125
2180
  raw_metrics = {}
2126
2181
  for k, v in device.items():
2127
- if k in ("deviceSn", "reportTimestamp", "collectTime"):
2182
+ if k in _METRICS_EXCLUDED_KEYS:
2128
2183
  continue
2129
2184
  raw_metrics[k] = v
2130
2185
 
@@ -2225,7 +2280,7 @@ class HyxiApiClient: # pylint: disable=too-many-instance-attributes
2225
2280
  if not sn:
2226
2281
  continue
2227
2282
 
2228
- alarm_code = str(item.get("alarmCode", ""))
2283
+ alarm_code = f"{item.get('alarmCode', '')}"
2229
2284
 
2230
2285
  # Resolve alarm name: use payload name if present, fall back to ALARM_CODE_MAP
2231
2286
  alarm_name = item.get("alarmName") or ALARM_CODE_MAP.get(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hyxi-cloud-api
3
- Version: 1.3.5
3
+ Version: 1.3.7
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
@@ -60,6 +60,37 @@ def test_compute_battery_metrics_totals():
60
60
  _compute_battery_metrics(m_raw, derived, "OTHER")
61
61
  assert derived["bat_charge_total"] == 12.5
62
62
  assert derived["bat_discharge_total"] == 45.6
63
+ assert derived["totalEchg"] == 12.5
64
+ assert derived["totalEdchg"] == 45.6
65
+ assert derived["batCharge"] == 12.5
66
+ assert derived["batDisCharge"] == 45.6
67
+
68
+
69
+ def test_compute_battery_metrics_polling_keys():
70
+ """Test fallback and synchronization when only polling keys are present."""
71
+ derived: dict[str, float] = {}
72
+ m_raw = {"totalEchg": 100.5, "totalEdchg": 200.2}
73
+ _compute_battery_metrics(m_raw, derived, "OTHER")
74
+ assert derived["bat_charge_total"] == 100.5
75
+ assert derived["bat_discharge_total"] == 200.2
76
+ assert derived["totalEchg"] == 100.5
77
+ assert derived["totalEdchg"] == 200.2
78
+ assert derived["batCharge"] == 100.5
79
+ assert derived["batDisCharge"] == 200.2
80
+
81
+
82
+ def test_compute_battery_metrics_null_and_empty_handling():
83
+ """Verify that null-equivalent and empty values do not pollute derived metrics."""
84
+ derived: dict[str, float] = {}
85
+ m_raw = {
86
+ "batCharge": 150.0,
87
+ "totalEchg": "null",
88
+ "batDisCharge": "None",
89
+ "totalEdchg": 250.0,
90
+ }
91
+ _compute_battery_metrics(m_raw, derived, "OTHER")
92
+ assert derived["bat_charge_total"] == 150.0
93
+ assert derived["bat_discharge_total"] == 250.0
63
94
 
64
95
 
65
96
  def test_compute_battery_metrics_empty():
@@ -0,0 +1,79 @@
1
+ """Tests for the _compute_grid_metrics helper function in api.py."""
2
+
3
+ from src.hyxi_cloud_api.api import _compute_grid_metrics
4
+
5
+
6
+ class TestComputeGridMetrics:
7
+ """Tests for _compute_grid_metrics."""
8
+
9
+ def test_grid_import(self):
10
+ """Test grid_import when gridP is negative."""
11
+ m_raw = {"gridP": -1.5}
12
+ derived: dict[str, float] = {}
13
+ _compute_grid_metrics(m_raw, derived)
14
+ assert derived["grid_import"] == 1500.0
15
+ assert derived["grid_export"] == 0.0
16
+
17
+ def test_grid_export(self):
18
+ """Test grid_export when gridP is positive."""
19
+ m_raw = {"gridP": 2.25}
20
+ derived: dict[str, float] = {}
21
+ _compute_grid_metrics(m_raw, derived)
22
+ assert derived["grid_import"] == 0.0
23
+ assert derived["grid_export"] == 2250.0
24
+
25
+ def test_grid_zero(self):
26
+ """Test grid import/export when gridP is zero."""
27
+ m_raw = {"gridP": 0.0}
28
+ derived: dict[str, float] = {}
29
+ _compute_grid_metrics(m_raw, derived)
30
+ assert derived["grid_import"] == 0.0
31
+ assert derived["grid_export"] == 0.0
32
+
33
+ def test_grid_missing(self):
34
+ """Test missing gridP."""
35
+ m_raw = {}
36
+ derived: dict[str, float] = {}
37
+ _compute_grid_metrics(m_raw, derived)
38
+ assert "grid_import" not in derived
39
+ assert "grid_export" not in derived
40
+
41
+ def test_grid_fallback_phases_positive(self):
42
+ """Test grid fallback to phase active powers when gridP is missing."""
43
+ m_raw = {
44
+ "ph1p": 200.0,
45
+ "ph2p": 150.0,
46
+ "ph3p": 50.0,
47
+ }
48
+ derived: dict[str, float] = {}
49
+ _compute_grid_metrics(m_raw, derived)
50
+ assert derived["gridP"] == 0.4 # (200 + 150 + 50) / 1000 = 0.4 kW
51
+ assert derived["grid_import"] == 0.0
52
+ assert derived["grid_export"] == 400.0
53
+
54
+ def test_grid_fallback_phases_negative(self):
55
+ """Test grid fallback to phase active powers when importing (negative)."""
56
+ m_raw = {
57
+ "ph1p": -300.0,
58
+ "ph2p": -200.0,
59
+ "ph3p": -100.0,
60
+ }
61
+ derived: dict[str, float] = {}
62
+ _compute_grid_metrics(m_raw, derived)
63
+ assert derived["gridP"] == -0.6 # -600W / 1000 = -0.6 kW
64
+ assert derived["grid_import"] == 600.0
65
+ assert derived["grid_export"] == 0.0
66
+
67
+ def test_grid_priority(self):
68
+ """Test that gridP takes priority over phase fallback if present."""
69
+ m_raw = {
70
+ "gridP": 1.5,
71
+ "ph1p": -300.0,
72
+ "ph2p": -200.0,
73
+ "ph3p": -100.0,
74
+ }
75
+ derived: dict[str, float] = {}
76
+ _compute_grid_metrics(m_raw, derived)
77
+ assert "gridP" not in derived # gridP was in m_raw, not computed/derived
78
+ assert derived["grid_import"] == 0.0
79
+ assert derived["grid_export"] == 1500.0
@@ -221,6 +221,30 @@ async def test_set_micro_power_limit_invalid_percentage():
221
221
  await api.set_micro_power_limit("SN123", percentage=-1)
222
222
 
223
223
 
224
+ @pytest.mark.asyncio
225
+ async def test_set_micro_power_limit_mocked_control():
226
+ """Test set_micro_power_limit by mocking set_device_control and checking edge cases."""
227
+ api = HyxiApiClient("ak", "sk", "https://api.com", MagicMock())
228
+ api.set_device_control = AsyncMock(return_value={"success": True})
229
+
230
+ # Test 80%
231
+ result = await api.set_micro_power_limit("SN123", percentage=80)
232
+ api.set_device_control.assert_awaited_once_with("SN123", {3012: "80"})
233
+ assert result == {"success": True}
234
+
235
+ # Test boundary 0%
236
+ api.set_device_control.reset_mock()
237
+ result = await api.set_micro_power_limit("SN123", percentage=0)
238
+ api.set_device_control.assert_awaited_once_with("SN123", {3012: "0"})
239
+ assert result == {"success": True}
240
+
241
+ # Test boundary 100%
242
+ api.set_device_control.reset_mock()
243
+ result = await api.set_micro_power_limit("SN123", percentage=100)
244
+ api.set_device_control.assert_awaited_once_with("SN123", {3012: "100"})
245
+ assert result == {"success": True}
246
+
247
+
224
248
  @pytest.mark.asyncio
225
249
  async def test_restart_device():
226
250
  """Test restart_device sends controlId 3013 with value '1'."""
@@ -218,6 +218,13 @@ def test_process_push_data_nested_format():
218
218
  "epsPowerW": 269.0,
219
219
  }
220
220
  },
221
+ "grid": {
222
+ "powerW": -1500.0,
223
+ "frequencyHz": 50.02,
224
+ "powerFactor": 0.98,
225
+ "energyInKwh": 1023.4,
226
+ "energyOutKwh": 504.2,
227
+ },
221
228
  }
222
229
  ]
223
230
  }
@@ -257,6 +264,13 @@ def test_process_push_data_nested_format():
257
264
  assert metrics["ph1i"] == 1.88
258
265
  assert metrics["ph1p"] == 450.0
259
266
  assert metrics["ph1Loadp"] == 269.0
267
+ assert metrics["gridP"] == -1.5
268
+ assert metrics["gridF"] == 50.02
269
+ assert metrics["gridPfd"] == 0.98
270
+ assert metrics["gridEIn"] == 1023.4
271
+ assert metrics["gridEOut"] == 504.2
272
+ assert metrics["grid_import"] == 1500.0
273
+ assert metrics["grid_export"] == 0.0
260
274
 
261
275
 
262
276
  def test_process_push_data_flat_ms_collect_time():
@@ -57,6 +57,46 @@ async def test_subscribe_real_time_data():
57
57
  }
58
58
 
59
59
 
60
+ @pytest.mark.asyncio
61
+ async def test_subscribe_real_time_data_mock_post_subscription():
62
+ """Test real-time data subscription by mocking _post_subscription."""
63
+ api = _client()
64
+ api._post_subscription = AsyncMock(return_value={"success": True, "data": "mocked"})
65
+
66
+ # Test with data_code_list
67
+ result = await api.subscribe_real_time_data(
68
+ callback_url="https://example.com/hyxi",
69
+ device_sn_list=["SN1"],
70
+ post_rate=5000,
71
+ data_code_list=["pv1p"],
72
+ )
73
+ assert result == {"success": True, "data": "mocked"}
74
+ api._post_subscription.assert_called_with(
75
+ "/api/subscribe/v1/realTimeData",
76
+ {
77
+ "callBackUrl": "https://example.com/hyxi",
78
+ "deviceSnList": ["SN1"],
79
+ "postRate": 5000,
80
+ "dataCodeList": ["pv1p"],
81
+ },
82
+ )
83
+
84
+ # Test without data_code_list
85
+ result = await api.subscribe_real_time_data(
86
+ callback_url="https://example.com/hyxi",
87
+ device_sn_list=["SN1"],
88
+ post_rate=5000,
89
+ )
90
+ api._post_subscription.assert_called_with(
91
+ "/api/subscribe/v1/realTimeData",
92
+ {
93
+ "callBackUrl": "https://example.com/hyxi",
94
+ "deviceSnList": ["SN1"],
95
+ "postRate": 5000,
96
+ },
97
+ )
98
+
99
+
60
100
  @pytest.mark.asyncio
61
101
  async def test_subscribe_alarm():
62
102
  """Test alarm subscription payload."""
@@ -109,6 +149,23 @@ async def test_cancel_subscription():
109
149
  assert call_args.kwargs["json"] == {"subscribeCode": "sub-code"}
110
150
 
111
151
 
152
+ @pytest.mark.asyncio
153
+ async def test_cancel_subscription_return_value():
154
+ """Test subscription cancellation return value matches expected API response."""
155
+ api = _client()
156
+
157
+ # Execute the method
158
+ result = await api.cancel_subscription("valid-sub-code")
159
+
160
+ # Verify the return payload matches the API response
161
+ assert result == {
162
+ "code": "0",
163
+ "msg": "Success",
164
+ "data": {"subscribeCode": "sub-code"},
165
+ "success": True,
166
+ }
167
+
168
+
112
169
  @pytest.mark.asyncio
113
170
  async def test_subscription_error_on_auth_failed():
114
171
  """Test SubscriptionError is raised when authentication fails."""
@@ -1,39 +0,0 @@
1
- """Tests for the _compute_grid_metrics helper function in api.py."""
2
-
3
- from src.hyxi_cloud_api.api import _compute_grid_metrics
4
-
5
-
6
- class TestComputeGridMetrics:
7
- """Tests for _compute_grid_metrics."""
8
-
9
- def test_grid_import(self):
10
- """Test grid_import when gridP is negative."""
11
- m_raw = {"gridP": -1.5}
12
- derived: dict[str, float] = {}
13
- _compute_grid_metrics(m_raw, derived)
14
- assert derived["grid_import"] == 1500.0
15
- assert derived["grid_export"] == 0.0
16
-
17
- def test_grid_export(self):
18
- """Test grid_export when gridP is positive."""
19
- m_raw = {"gridP": 2.25}
20
- derived: dict[str, float] = {}
21
- _compute_grid_metrics(m_raw, derived)
22
- assert derived["grid_import"] == 0.0
23
- assert derived["grid_export"] == 2250.0
24
-
25
- def test_grid_zero(self):
26
- """Test grid import/export when gridP is zero."""
27
- m_raw = {"gridP": 0.0}
28
- derived: dict[str, float] = {}
29
- _compute_grid_metrics(m_raw, derived)
30
- assert derived["grid_import"] == 0.0
31
- assert derived["grid_export"] == 0.0
32
-
33
- def test_grid_missing(self):
34
- """Test missing gridP."""
35
- m_raw = {}
36
- derived: dict[str, float] = {}
37
- _compute_grid_metrics(m_raw, derived)
38
- assert "grid_import" not in derived
39
- assert "grid_export" not in derived
File without changes
File without changes
File without changes