hyxi-cloud-api 1.3.6__tar.gz → 1.3.8__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 (57) hide show
  1. {hyxi_cloud_api-1.3.6/src/hyxi_cloud_api.egg-info → hyxi_cloud_api-1.3.8}/PKG-INFO +1 -1
  2. {hyxi_cloud_api-1.3.6 → hyxi_cloud_api-1.3.8}/pyproject.toml +1 -1
  3. {hyxi_cloud_api-1.3.6 → hyxi_cloud_api-1.3.8}/src/hyxi_cloud_api/__init__.py +1 -1
  4. {hyxi_cloud_api-1.3.6 → hyxi_cloud_api-1.3.8}/src/hyxi_cloud_api/api.py +52 -6
  5. {hyxi_cloud_api-1.3.6 → hyxi_cloud_api-1.3.8/src/hyxi_cloud_api.egg-info}/PKG-INFO +1 -1
  6. {hyxi_cloud_api-1.3.6 → hyxi_cloud_api-1.3.8}/tests/test_compute_battery_metrics.py +52 -0
  7. {hyxi_cloud_api-1.3.6 → hyxi_cloud_api-1.3.8}/tests/test_device_control.py +24 -0
  8. {hyxi_cloud_api-1.3.6 → hyxi_cloud_api-1.3.8}/tests/test_subscriptions.py +57 -0
  9. {hyxi_cloud_api-1.3.6 → hyxi_cloud_api-1.3.8}/LICENSE +0 -0
  10. {hyxi_cloud_api-1.3.6 → hyxi_cloud_api-1.3.8}/README.md +0 -0
  11. {hyxi_cloud_api-1.3.6 → hyxi_cloud_api-1.3.8}/setup.cfg +0 -0
  12. {hyxi_cloud_api-1.3.6 → hyxi_cloud_api-1.3.8}/src/hyxi_cloud_api.egg-info/SOURCES.txt +0 -0
  13. {hyxi_cloud_api-1.3.6 → hyxi_cloud_api-1.3.8}/src/hyxi_cloud_api.egg-info/dependency_links.txt +0 -0
  14. {hyxi_cloud_api-1.3.6 → hyxi_cloud_api-1.3.8}/src/hyxi_cloud_api.egg-info/requires.txt +0 -0
  15. {hyxi_cloud_api-1.3.6 → hyxi_cloud_api-1.3.8}/src/hyxi_cloud_api.egg-info/top_level.txt +0 -0
  16. {hyxi_cloud_api-1.3.6 → hyxi_cloud_api-1.3.8}/tests/test_additional_coverage.py +0 -0
  17. {hyxi_cloud_api-1.3.6 → hyxi_cloud_api-1.3.8}/tests/test_alarm_push.py +0 -0
  18. {hyxi_cloud_api-1.3.6 → hyxi_cloud_api-1.3.8}/tests/test_all_in_one.py +0 -0
  19. {hyxi_cloud_api-1.3.6 → hyxi_cloud_api-1.3.8}/tests/test_api.py +0 -0
  20. {hyxi_cloud_api-1.3.6 → hyxi_cloud_api-1.3.8}/tests/test_build_plant_tasks.py +0 -0
  21. {hyxi_cloud_api-1.3.6 → hyxi_cloud_api-1.3.8}/tests/test_caching.py +0 -0
  22. {hyxi_cloud_api-1.3.6 → hyxi_cloud_api-1.3.8}/tests/test_compute_derived_metrics.py +0 -0
  23. {hyxi_cloud_api-1.3.6 → hyxi_cloud_api-1.3.8}/tests/test_compute_grid_metrics.py +0 -0
  24. {hyxi_cloud_api-1.3.6 → hyxi_cloud_api-1.3.8}/tests/test_compute_load_metrics.py +0 -0
  25. {hyxi_cloud_api-1.3.6 → hyxi_cloud_api-1.3.8}/tests/test_device_entry.py +0 -0
  26. {hyxi_cloud_api-1.3.6 → hyxi_cloud_api-1.3.8}/tests/test_devices_errors.py +0 -0
  27. {hyxi_cloud_api-1.3.6 → hyxi_cloud_api-1.3.8}/tests/test_discovery.py +0 -0
  28. {hyxi_cloud_api-1.3.6 → hyxi_cloud_api-1.3.8}/tests/test_execute_device_tasks.py +0 -0
  29. {hyxi_cloud_api-1.3.6 → hyxi_cloud_api-1.3.8}/tests/test_execute_metric_tasks.py +0 -0
  30. {hyxi_cloud_api-1.3.6 → hyxi_cloud_api-1.3.8}/tests/test_execute_metrics_and_map_alarms.py +0 -0
  31. {hyxi_cloud_api-1.3.6 → hyxi_cloud_api-1.3.8}/tests/test_extract_battery_info.py +0 -0
  32. {hyxi_cloud_api-1.3.6 → hyxi_cloud_api-1.3.8}/tests/test_extract_device_info_metadata.py +0 -0
  33. {hyxi_cloud_api-1.3.6 → hyxi_cloud_api-1.3.8}/tests/test_fetch_and_process_alarms.py +0 -0
  34. {hyxi_cloud_api-1.3.6 → hyxi_cloud_api-1.3.8}/tests/test_fetch_device_list_for_plant.py +0 -0
  35. {hyxi_cloud_api-1.3.6 → hyxi_cloud_api-1.3.8}/tests/test_fetch_plants.py +0 -0
  36. {hyxi_cloud_api-1.3.6 → hyxi_cloud_api-1.3.8}/tests/test_fetch_state.py +0 -0
  37. {hyxi_cloud_api-1.3.6 → hyxi_cloud_api-1.3.8}/tests/test_fetch_sub_device_list.py +0 -0
  38. {hyxi_cloud_api-1.3.6 → hyxi_cloud_api-1.3.8}/tests/test_filter_metrics.py +0 -0
  39. {hyxi_cloud_api-1.3.6 → hyxi_cloud_api-1.3.8}/tests/test_fuzz_parser.py +0 -0
  40. {hyxi_cloud_api-1.3.6 → hyxi_cloud_api-1.3.8}/tests/test_get_f.py +0 -0
  41. {hyxi_cloud_api-1.3.6 → hyxi_cloud_api-1.3.8}/tests/test_handle_back_discovery_alarm.py +0 -0
  42. {hyxi_cloud_api-1.3.6 → hyxi_cloud_api-1.3.8}/tests/test_info_errors.py +0 -0
  43. {hyxi_cloud_api-1.3.6 → hyxi_cloud_api-1.3.8}/tests/test_mask_id.py +0 -0
  44. {hyxi_cloud_api-1.3.6 → hyxi_cloud_api-1.3.8}/tests/test_metrics_errors.py +0 -0
  45. {hyxi_cloud_api-1.3.6 → hyxi_cloud_api-1.3.8}/tests/test_parse_data_list.py +0 -0
  46. {hyxi_cloud_api-1.3.6 → hyxi_cloud_api-1.3.8}/tests/test_parse_ems_kv.py +0 -0
  47. {hyxi_cloud_api-1.3.6 → hyxi_cloud_api-1.3.8}/tests/test_parser.py +0 -0
  48. {hyxi_cloud_api-1.3.6 → hyxi_cloud_api-1.3.8}/tests/test_process_alarms_and_back_discovery.py +0 -0
  49. {hyxi_cloud_api-1.3.6 → hyxi_cloud_api-1.3.8}/tests/test_process_devices_for_plant.py +0 -0
  50. {hyxi_cloud_api-1.3.6 → hyxi_cloud_api-1.3.8}/tests/test_push.py +0 -0
  51. {hyxi_cloud_api-1.3.6 → hyxi_cloud_api-1.3.8}/tests/test_sanitize_dict.py +0 -0
  52. {hyxi_cloud_api-1.3.6 → hyxi_cloud_api-1.3.8}/tests/test_sanitize_list.py +0 -0
  53. {hyxi_cloud_api-1.3.6 → hyxi_cloud_api-1.3.8}/tests/test_security_fix.py +0 -0
  54. {hyxi_cloud_api-1.3.6 → hyxi_cloud_api-1.3.8}/tests/test_token_errors.py +0 -0
  55. {hyxi_cloud_api-1.3.6 → hyxi_cloud_api-1.3.8}/tests/test_token_handling.py +0 -0
  56. {hyxi_cloud_api-1.3.6 → hyxi_cloud_api-1.3.8}/tests/test_validate_post_rate_ms.py +0 -0
  57. {hyxi_cloud_api-1.3.6 → hyxi_cloud_api-1.3.8}/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.6
3
+ Version: 1.3.8
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.6"
7
+ version = "1.3.8"
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.6"
8
+ __version__ = "1.3.8"
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",
@@ -634,10 +636,54 @@ def _compute_battery_metrics(
634
636
  derived["bat_discharging"] = power_source if power_source > 0 else 0.0
635
637
  derived["bat_power_dc"] = bat_p_dc
636
638
 
637
- if "batCharge" in m_raw:
638
- derived["bat_charge_total"] = _get_f("batCharge", m_raw)
639
- if "batDisCharge" in m_raw:
640
- 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
+ total_echg_val = (
650
+ _get_f("totalEchg", m_raw) if _is_valid_metric("totalEchg") else None
651
+ )
652
+ bat_charge_val = (
653
+ _get_f("batCharge", m_raw) if _is_valid_metric("batCharge") else None
654
+ )
655
+
656
+ if total_echg_val is not None:
657
+ derived["totalEchg"] = total_echg_val
658
+ derived["bat_charge_total"] = total_echg_val
659
+ if bat_charge_val is None:
660
+ derived["batCharge"] = total_echg_val
661
+
662
+ if bat_charge_val is not None:
663
+ derived["batCharge"] = bat_charge_val
664
+ if total_echg_val is None:
665
+ derived["totalEchg"] = bat_charge_val
666
+ derived["bat_charge_total"] = bat_charge_val
667
+
668
+ # Unify cumulative battery discharge energy telemetry keys to resolve polling/push mismatch
669
+ total_edchg_val = (
670
+ _get_f("totalEdchg", m_raw) if _is_valid_metric("totalEdchg") else None
671
+ )
672
+ bat_discharge_val = (
673
+ _get_f("batDisCharge", m_raw) if _is_valid_metric("batDisCharge") else None
674
+ )
675
+
676
+ if total_edchg_val is not None:
677
+ derived["totalEdchg"] = total_edchg_val
678
+ derived["bat_discharge_total"] = total_edchg_val
679
+ if bat_discharge_val is None:
680
+ derived["batDisCharge"] = total_edchg_val
681
+
682
+ if bat_discharge_val is not None:
683
+ derived["batDisCharge"] = bat_discharge_val
684
+ if total_edchg_val is None:
685
+ derived["totalEdchg"] = bat_discharge_val
686
+ derived["bat_discharge_total"] = bat_discharge_val
641
687
 
642
688
 
643
689
  def _compute_pv_metrics(m_raw: dict, derived: dict[str, float]) -> None:
@@ -2149,7 +2195,7 @@ class HyxiApiClient: # pylint: disable=too-many-instance-attributes
2149
2195
  # Strip metadata/routing keys
2150
2196
  raw_metrics = {}
2151
2197
  for k, v in device.items():
2152
- if k in ("deviceSn", "reportTimestamp", "collectTime"):
2198
+ if k in _METRICS_EXCLUDED_KEYS:
2153
2199
  continue
2154
2200
  raw_metrics[k] = v
2155
2201
 
@@ -2250,7 +2296,7 @@ class HyxiApiClient: # pylint: disable=too-many-instance-attributes
2250
2296
  if not sn:
2251
2297
  continue
2252
2298
 
2253
- alarm_code = str(item.get("alarmCode", ""))
2299
+ alarm_code = f"{item.get('alarmCode', '')}"
2254
2300
 
2255
2301
  # Resolve alarm name: use payload name if present, fall back to ALARM_CODE_MAP
2256
2302
  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.6
3
+ Version: 1.3.8
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():
@@ -76,3 +107,24 @@ def test_compute_battery_metrics_none_device_type():
76
107
  _compute_battery_metrics(m_raw, derived, None) # type: ignore
77
108
  assert derived["bat_charging"] == 100.0
78
109
  assert derived["bat_power_dc"] == -100.0
110
+
111
+
112
+ def test_compute_battery_metrics_both_present():
113
+ """Test that when both polling and daily keys are present, they are preserved.
114
+
115
+ Cumulative values should take priority for battery totals.
116
+ """
117
+ derived: dict[str, float] = {}
118
+ m_raw = {
119
+ "totalEchg": 366.3,
120
+ "totalEdchg": 371.3,
121
+ "batCharge": 2.3,
122
+ "batDisCharge": 4.4,
123
+ }
124
+ _compute_battery_metrics(m_raw, derived, "OTHER")
125
+ assert derived["bat_charge_total"] == 366.3
126
+ assert derived["bat_discharge_total"] == 371.3
127
+ assert derived["totalEchg"] == 366.3
128
+ assert derived["totalEdchg"] == 371.3
129
+ assert derived["batCharge"] == 2.3
130
+ assert derived["batDisCharge"] == 4.4
@@ -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'."""
@@ -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."""
File without changes
File without changes
File without changes