hyxi-cloud-api 1.3.7__tar.gz → 1.3.9__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 (61) hide show
  1. {hyxi_cloud_api-1.3.7/src/hyxi_cloud_api.egg-info → hyxi_cloud_api-1.3.9}/PKG-INFO +1 -4
  2. {hyxi_cloud_api-1.3.7 → hyxi_cloud_api-1.3.9}/pyproject.toml +1 -4
  3. {hyxi_cloud_api-1.3.7 → hyxi_cloud_api-1.3.9}/src/hyxi_cloud_api/__init__.py +1 -1
  4. {hyxi_cloud_api-1.3.7 → hyxi_cloud_api-1.3.9}/src/hyxi_cloud_api/api.py +54 -21
  5. {hyxi_cloud_api-1.3.7 → hyxi_cloud_api-1.3.9/src/hyxi_cloud_api.egg-info}/PKG-INFO +1 -4
  6. {hyxi_cloud_api-1.3.7 → hyxi_cloud_api-1.3.9}/src/hyxi_cloud_api.egg-info/SOURCES.txt +3 -0
  7. {hyxi_cloud_api-1.3.7 → hyxi_cloud_api-1.3.9}/tests/test_additional_coverage.py +4 -3
  8. {hyxi_cloud_api-1.3.7 → hyxi_cloud_api-1.3.9}/tests/test_api.py +2 -1
  9. {hyxi_cloud_api-1.3.7 → hyxi_cloud_api-1.3.9}/tests/test_compute_battery_metrics.py +59 -0
  10. hyxi_cloud_api-1.3.9/tests/test_compute_pv_metrics.py +50 -0
  11. {hyxi_cloud_api-1.3.7 → hyxi_cloud_api-1.3.9}/tests/test_device_control.py +24 -0
  12. hyxi_cloud_api-1.3.9/tests/test_ensure_authenticated.py +46 -0
  13. {hyxi_cloud_api-1.3.7 → hyxi_cloud_api-1.3.9}/tests/test_fetch_device_list_for_plant.py +0 -1
  14. hyxi_cloud_api-1.3.9/tests/test_filter_metrics.py +133 -0
  15. hyxi_cloud_api-1.3.9/tests/test_flatten_nested_push_device.py +59 -0
  16. {hyxi_cloud_api-1.3.7 → hyxi_cloud_api-1.3.9}/tests/test_info_errors.py +6 -7
  17. {hyxi_cloud_api-1.3.7 → hyxi_cloud_api-1.3.9}/tests/test_mask_id.py +6 -2
  18. {hyxi_cloud_api-1.3.7 → hyxi_cloud_api-1.3.9}/tests/test_metrics_errors.py +14 -16
  19. {hyxi_cloud_api-1.3.7 → hyxi_cloud_api-1.3.9}/tests/test_push.py +65 -0
  20. {hyxi_cloud_api-1.3.7 → hyxi_cloud_api-1.3.9}/tests/test_security_fix.py +6 -6
  21. hyxi_cloud_api-1.3.7/tests/test_filter_metrics.py +0 -75
  22. {hyxi_cloud_api-1.3.7 → hyxi_cloud_api-1.3.9}/LICENSE +0 -0
  23. {hyxi_cloud_api-1.3.7 → hyxi_cloud_api-1.3.9}/README.md +0 -0
  24. {hyxi_cloud_api-1.3.7 → hyxi_cloud_api-1.3.9}/setup.cfg +0 -0
  25. {hyxi_cloud_api-1.3.7 → hyxi_cloud_api-1.3.9}/src/hyxi_cloud_api.egg-info/dependency_links.txt +0 -0
  26. {hyxi_cloud_api-1.3.7 → hyxi_cloud_api-1.3.9}/src/hyxi_cloud_api.egg-info/requires.txt +0 -0
  27. {hyxi_cloud_api-1.3.7 → hyxi_cloud_api-1.3.9}/src/hyxi_cloud_api.egg-info/top_level.txt +0 -0
  28. {hyxi_cloud_api-1.3.7 → hyxi_cloud_api-1.3.9}/tests/test_alarm_push.py +0 -0
  29. {hyxi_cloud_api-1.3.7 → hyxi_cloud_api-1.3.9}/tests/test_all_in_one.py +0 -0
  30. {hyxi_cloud_api-1.3.7 → hyxi_cloud_api-1.3.9}/tests/test_build_plant_tasks.py +0 -0
  31. {hyxi_cloud_api-1.3.7 → hyxi_cloud_api-1.3.9}/tests/test_caching.py +0 -0
  32. {hyxi_cloud_api-1.3.7 → hyxi_cloud_api-1.3.9}/tests/test_compute_derived_metrics.py +0 -0
  33. {hyxi_cloud_api-1.3.7 → hyxi_cloud_api-1.3.9}/tests/test_compute_grid_metrics.py +0 -0
  34. {hyxi_cloud_api-1.3.7 → hyxi_cloud_api-1.3.9}/tests/test_compute_load_metrics.py +0 -0
  35. {hyxi_cloud_api-1.3.7 → hyxi_cloud_api-1.3.9}/tests/test_device_entry.py +0 -0
  36. {hyxi_cloud_api-1.3.7 → hyxi_cloud_api-1.3.9}/tests/test_devices_errors.py +0 -0
  37. {hyxi_cloud_api-1.3.7 → hyxi_cloud_api-1.3.9}/tests/test_discovery.py +0 -0
  38. {hyxi_cloud_api-1.3.7 → hyxi_cloud_api-1.3.9}/tests/test_execute_device_tasks.py +0 -0
  39. {hyxi_cloud_api-1.3.7 → hyxi_cloud_api-1.3.9}/tests/test_execute_metric_tasks.py +0 -0
  40. {hyxi_cloud_api-1.3.7 → hyxi_cloud_api-1.3.9}/tests/test_execute_metrics_and_map_alarms.py +0 -0
  41. {hyxi_cloud_api-1.3.7 → hyxi_cloud_api-1.3.9}/tests/test_extract_battery_info.py +0 -0
  42. {hyxi_cloud_api-1.3.7 → hyxi_cloud_api-1.3.9}/tests/test_extract_device_info_metadata.py +0 -0
  43. {hyxi_cloud_api-1.3.7 → hyxi_cloud_api-1.3.9}/tests/test_fetch_and_process_alarms.py +0 -0
  44. {hyxi_cloud_api-1.3.7 → hyxi_cloud_api-1.3.9}/tests/test_fetch_plants.py +0 -0
  45. {hyxi_cloud_api-1.3.7 → hyxi_cloud_api-1.3.9}/tests/test_fetch_state.py +0 -0
  46. {hyxi_cloud_api-1.3.7 → hyxi_cloud_api-1.3.9}/tests/test_fetch_sub_device_list.py +0 -0
  47. {hyxi_cloud_api-1.3.7 → hyxi_cloud_api-1.3.9}/tests/test_fuzz_parser.py +0 -0
  48. {hyxi_cloud_api-1.3.7 → hyxi_cloud_api-1.3.9}/tests/test_get_f.py +0 -0
  49. {hyxi_cloud_api-1.3.7 → hyxi_cloud_api-1.3.9}/tests/test_handle_back_discovery_alarm.py +0 -0
  50. {hyxi_cloud_api-1.3.7 → hyxi_cloud_api-1.3.9}/tests/test_parse_data_list.py +0 -0
  51. {hyxi_cloud_api-1.3.7 → hyxi_cloud_api-1.3.9}/tests/test_parse_ems_kv.py +0 -0
  52. {hyxi_cloud_api-1.3.7 → hyxi_cloud_api-1.3.9}/tests/test_parser.py +0 -0
  53. {hyxi_cloud_api-1.3.7 → hyxi_cloud_api-1.3.9}/tests/test_process_alarms_and_back_discovery.py +0 -0
  54. {hyxi_cloud_api-1.3.7 → hyxi_cloud_api-1.3.9}/tests/test_process_devices_for_plant.py +0 -0
  55. {hyxi_cloud_api-1.3.7 → hyxi_cloud_api-1.3.9}/tests/test_sanitize_dict.py +0 -0
  56. {hyxi_cloud_api-1.3.7 → hyxi_cloud_api-1.3.9}/tests/test_sanitize_list.py +0 -0
  57. {hyxi_cloud_api-1.3.7 → hyxi_cloud_api-1.3.9}/tests/test_subscriptions.py +0 -0
  58. {hyxi_cloud_api-1.3.7 → hyxi_cloud_api-1.3.9}/tests/test_token_errors.py +0 -0
  59. {hyxi_cloud_api-1.3.7 → hyxi_cloud_api-1.3.9}/tests/test_token_handling.py +0 -0
  60. {hyxi_cloud_api-1.3.7 → hyxi_cloud_api-1.3.9}/tests/test_validate_post_rate_ms.py +0 -0
  61. {hyxi_cloud_api-1.3.7 → hyxi_cloud_api-1.3.9}/tests/test_validate_subscription_device_sns.py +0 -0
@@ -1,12 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hyxi-cloud-api
3
- Version: 1.3.7
3
+ Version: 1.3.9
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
7
- Classifier: Programming Language :: Python :: 3.11
8
- Classifier: Programming Language :: Python :: 3.12
9
- Classifier: Programming Language :: Python :: 3.13
10
7
  Classifier: Programming Language :: Python :: 3.14
11
8
  Classifier: License :: OSI Approved :: MIT License
12
9
  Classifier: Operating System :: OS Independent
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "hyxi-cloud-api"
7
- version = "1.3.7"
7
+ version = "1.3.9"
8
8
  authors = [
9
9
  { name="Veldkornet", email="Veldkornet@users.noreply.github.com" },
10
10
  ]
@@ -16,9 +16,6 @@ dependencies = [
16
16
  ]
17
17
  classifiers = [
18
18
  "Programming Language :: Python :: 3",
19
- "Programming Language :: Python :: 3.11",
20
- "Programming Language :: Python :: 3.12",
21
- "Programming Language :: Python :: 3.13",
22
19
  "Programming Language :: Python :: 3.14",
23
20
  "License :: OSI Approved :: MIT License",
24
21
  "Operating System :: OS Independent",
@@ -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.7"
8
+ __version__ = "1.3.9"
9
9
  __all__ = ["VPP_ACTIVE_MODES", "HyxiApiClient"]
@@ -15,6 +15,7 @@ import hmac
15
15
  import logging
16
16
  import os
17
17
  import re
18
+ import secrets
18
19
  import time
19
20
  from collections import defaultdict
20
21
  from dataclasses import dataclass, field
@@ -646,28 +647,44 @@ def _compute_battery_metrics(
646
647
  return True
647
648
 
648
649
  # 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
650
+ total_echg_val = (
651
+ _get_f("totalEchg", m_raw) if _is_valid_metric("totalEchg") else None
652
+ )
653
+ bat_charge_val = (
654
+ _get_f("batCharge", m_raw) if _is_valid_metric("batCharge") else None
655
+ )
656
+
657
+ if total_echg_val is not None:
658
+ derived["totalEchg"] = total_echg_val
659
+ derived["bat_charge_total"] = total_echg_val
660
+ if bat_charge_val is None:
661
+ derived["batCharge"] = total_echg_val
662
+
663
+ if bat_charge_val is not None:
664
+ derived["batCharge"] = bat_charge_val
665
+ if total_echg_val is None:
666
+ derived["totalEchg"] = bat_charge_val
667
+ derived["bat_charge_total"] = bat_charge_val
659
668
 
660
669
  # 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
670
+ total_edchg_val = (
671
+ _get_f("totalEdchg", m_raw) if _is_valid_metric("totalEdchg") else None
672
+ )
673
+ bat_discharge_val = (
674
+ _get_f("batDisCharge", m_raw) if _is_valid_metric("batDisCharge") else None
675
+ )
676
+
677
+ if total_edchg_val is not None:
678
+ derived["totalEdchg"] = total_edchg_val
679
+ derived["bat_discharge_total"] = total_edchg_val
680
+ if bat_discharge_val is None:
681
+ derived["batDisCharge"] = total_edchg_val
682
+
683
+ if bat_discharge_val is not None:
684
+ derived["batDisCharge"] = bat_discharge_val
685
+ if total_edchg_val is None:
686
+ derived["totalEdchg"] = bat_discharge_val
687
+ derived["bat_discharge_total"] = bat_discharge_val
671
688
 
672
689
 
673
690
  def _compute_pv_metrics(m_raw: dict, derived: dict[str, float]) -> None:
@@ -883,6 +900,22 @@ def _flatten_nested_push_device(device: dict) -> dict: # pylint: disable=too-ma
883
900
  return flat
884
901
 
885
902
 
903
+ # Deployment-configured secret for log masking. If not provided,
904
+ # falls back to a process-random secret.
905
+ _LOG_SALT = os.environ.get("HYXI_LOG_SALT", "").encode("utf-8") or secrets.token_bytes(
906
+ 16
907
+ )
908
+
909
+
910
+ def set_log_salt(salt: str | bytes) -> None:
911
+ """Set a deployment-stable secret for cross-device log correlation."""
912
+ global _LOG_SALT # pylint: disable=global-statement
913
+ if isinstance(salt, str):
914
+ salt = salt.encode("utf-8")
915
+ _LOG_SALT = salt
916
+ _mask_id.cache_clear()
917
+
918
+
886
919
  @functools.lru_cache(maxsize=1024)
887
920
  def _mask_id(value: str) -> str:
888
921
  """Mask an identifier (SN, plant ID, etc.) for logs.
@@ -896,7 +929,7 @@ def _mask_id(value: str) -> str:
896
929
  if not value or value == "None":
897
930
  return "****"
898
931
  id_str = str(value)
899
- return hashlib.sha256(id_str.encode("utf-8")).hexdigest()[:8]
932
+ return hmac.new(_LOG_SALT, id_str.encode("utf-8"), hashlib.sha256).hexdigest()[:8]
900
933
 
901
934
 
902
935
  # Keys in raw API response dicts that contain identifying or personal information.
@@ -1,12 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hyxi-cloud-api
3
- Version: 1.3.7
3
+ Version: 1.3.9
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
7
- Classifier: Programming Language :: Python :: 3.11
8
- Classifier: Programming Language :: Python :: 3.12
9
- Classifier: Programming Language :: Python :: 3.13
10
7
  Classifier: Programming Language :: Python :: 3.14
11
8
  Classifier: License :: OSI Approved :: MIT License
12
9
  Classifier: Operating System :: OS Independent
@@ -18,10 +18,12 @@ tests/test_compute_battery_metrics.py
18
18
  tests/test_compute_derived_metrics.py
19
19
  tests/test_compute_grid_metrics.py
20
20
  tests/test_compute_load_metrics.py
21
+ tests/test_compute_pv_metrics.py
21
22
  tests/test_device_control.py
22
23
  tests/test_device_entry.py
23
24
  tests/test_devices_errors.py
24
25
  tests/test_discovery.py
26
+ tests/test_ensure_authenticated.py
25
27
  tests/test_execute_device_tasks.py
26
28
  tests/test_execute_metric_tasks.py
27
29
  tests/test_execute_metrics_and_map_alarms.py
@@ -33,6 +35,7 @@ tests/test_fetch_plants.py
33
35
  tests/test_fetch_state.py
34
36
  tests/test_fetch_sub_device_list.py
35
37
  tests/test_filter_metrics.py
38
+ tests/test_flatten_nested_push_device.py
36
39
  tests/test_fuzz_parser.py
37
40
  tests/test_get_f.py
38
41
  tests/test_handle_back_discovery_alarm.py
@@ -32,7 +32,7 @@ async def test_fetch_sub_devices_coverage(caplog):
32
32
 
33
33
  # Verify debug log was hit (line 1243)
34
34
  # PARENT_SN is masked to ca7b0e8c
35
- assert "HYXI Found 3 sub-devices under ca7b0e8c" in caplog.text
35
+ assert "HYXI Found 3 sub-devices under " in caplog.text
36
36
  # Verify duplicate/empty check worked (line 1253)
37
37
  assert api._fetch_all_for_device.call_count == 1
38
38
  api._fetch_all_for_device.assert_called_once_with(
@@ -54,7 +54,8 @@ async def test_fetch_sub_devices_coverage(caplog):
54
54
  api._fetch_sub_device_list = AsyncMock(side_effect=Exception("Database failure"))
55
55
  await api._fetch_sub_devices("PARENT_SN", state)
56
56
  # PARENT_SN is masked to ca7b0e8c
57
- assert "Error fetching sub-devices for ca7b0e8c: Database failure" in caplog.text
57
+ assert "Error fetching sub-devices for " in caplog.text
58
+ assert "Database failure" in caplog.text
58
59
 
59
60
 
60
61
  @pytest.mark.asyncio
@@ -71,7 +72,7 @@ async def test_fetch_alarms_for_plant_coverage(caplog):
71
72
  res = await api._fetch_alarms_for_plant("PLANT_123")
72
73
  assert res == []
73
74
  # PLANT_123 is masked to b7a0873d
74
- assert "HYXI API Alarm Fetch Rejected for Plant b7a0873d" in caplog.text
75
+ assert "HYXI API Alarm Fetch Rejected for Plant " in caplog.text
75
76
 
76
77
  # 2. Alarm Name Mapping (line 1295)
77
78
  api._request = AsyncMock(
@@ -592,7 +592,8 @@ async def test_fetch_alarms_for_plant_error(caplog):
592
592
  assert alarms == []
593
593
 
594
594
  log_text = caplog.text
595
- assert "Error fetching alarms for plant ef797c81: Connection reset" in log_text
595
+ assert "Error fetching alarms for plant " in log_text
596
+ assert "Connection reset" in log_text
596
597
 
597
598
 
598
599
  @pytest.mark.asyncio
@@ -107,3 +107,62 @@ def test_compute_battery_metrics_none_device_type():
107
107
  _compute_battery_metrics(m_raw, derived, None) # type: ignore
108
108
  assert derived["bat_charging"] == 100.0
109
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
131
+
132
+
133
+ def test_is_valid_metric_edge_cases():
134
+ """Test all edge cases for _is_valid_metric helper via _compute_battery_metrics."""
135
+ invalid_values = [
136
+ None,
137
+ "",
138
+ " ",
139
+ "null",
140
+ "NULL",
141
+ " none ",
142
+ "na",
143
+ " NA ",
144
+ "--",
145
+ ]
146
+
147
+ for val in invalid_values:
148
+ derived: dict[str, float] = {}
149
+ m_raw = {"totalEchg": val}
150
+ _compute_battery_metrics(m_raw, derived, "OTHER")
151
+ assert "totalEchg" not in derived
152
+ assert "bat_charge_total" not in derived
153
+
154
+ valid_values = [
155
+ 0,
156
+ 1.5,
157
+ "1.5",
158
+ " 2.5 ",
159
+ "0",
160
+ ]
161
+
162
+ for val in valid_values:
163
+ derived: dict[str, float] = {}
164
+ m_raw = {"totalEchg": val}
165
+ _compute_battery_metrics(m_raw, derived, "OTHER")
166
+ assert "totalEchg" in derived
167
+ assert "bat_charge_total" in derived
168
+ assert derived["totalEchg"] == float(val)
@@ -0,0 +1,50 @@
1
+ """Tests for the _compute_pv_metrics helper function in api.py."""
2
+
3
+ from src.hyxi_cloud_api.api import _compute_pv_metrics
4
+
5
+
6
+ class TestComputePvMetrics:
7
+ """Tests for _compute_pv_metrics."""
8
+
9
+ def test_compute_pv_metrics_with_direct_power(self):
10
+ """Test PV power uses direct power when provided."""
11
+ m_raw = {"pv1v": 100.0, "pv1i": 5.0, "pv1p": 600.0}
12
+ derived: dict[str, float] = {}
13
+ _compute_pv_metrics(m_raw, derived)
14
+ assert derived["pv1p"] == 600.0
15
+
16
+ def test_compute_pv_metrics_with_voltage_current(self):
17
+ """Test PV power calculates voltage * current when direct power is missing."""
18
+ m_raw = {"pv1v": 100.0, "pv1i": 5.5}
19
+ derived: dict[str, float] = {}
20
+ _compute_pv_metrics(m_raw, derived)
21
+ assert derived["pv1p"] == 550.0
22
+
23
+ def test_compute_pv_metrics_empty_dict(self):
24
+ """Test empty input results in empty derived metrics."""
25
+ m_raw = {}
26
+ derived: dict[str, float] = {}
27
+ _compute_pv_metrics(m_raw, derived)
28
+ assert not derived
29
+
30
+ def test_compute_pv_metrics_pv1_fallback_normal(self):
31
+ """Test pv1p fallback is calculated correctly from ppv and pv2p."""
32
+ m_raw = {"ppv": 1000.0}
33
+ derived: dict[str, float] = {"pv2p": 400.0}
34
+ _compute_pv_metrics(m_raw, derived)
35
+ assert derived["pv1p"] == 600.0
36
+ assert derived["pv2p"] == 400.0
37
+
38
+ def test_compute_pv_metrics_pv1_fallback_negative(self):
39
+ """Test pv1p fallback bounds negative results to 0.0."""
40
+ m_raw = {"ppv": 300.0}
41
+ derived: dict[str, float] = {"pv2p": 400.0}
42
+ _compute_pv_metrics(m_raw, derived)
43
+ assert derived["pv1p"] == 0.0
44
+
45
+ def test_compute_pv_metrics_zero_direct_power_with_vi(self):
46
+ """Test PV power falls back to calculated value if direct power is exactly 0.0."""
47
+ m_raw = {"pv1v": 100.0, "pv1i": 5.0, "pv1p": 0.0}
48
+ derived: dict[str, float] = {}
49
+ _compute_pv_metrics(m_raw, derived)
50
+ assert derived["pv1p"] == 500.0
@@ -245,6 +245,30 @@ async def test_set_micro_power_limit_mocked_control():
245
245
  assert result == {"success": True}
246
246
 
247
247
 
248
+ @pytest.mark.asyncio
249
+ async def test_restart_device_mocked_control():
250
+ """Test restart_device by mocking set_device_control and checking edge cases."""
251
+ api = HyxiApiClient("ak", "sk", "https://api.com", MagicMock())
252
+ api.set_device_control = AsyncMock(return_value={"success": True})
253
+
254
+ result = await api.restart_device(device_sn="SN123")
255
+
256
+ assert result == {"success": True}
257
+ api.set_device_control.assert_called_once()
258
+
259
+ call_args, call_kwargs = api.set_device_control.call_args
260
+ if "param_code" in call_kwargs:
261
+ assert call_kwargs["device_sn"] == "SN123"
262
+ assert call_kwargs["param_code"] == "rebootSet"
263
+ assert call_kwargs["value"] == 1
264
+ elif len(call_args) > 1 and call_args[1] == "rebootSet":
265
+ assert call_args[0] == "SN123"
266
+ assert call_args[1] == "rebootSet"
267
+ assert call_args[2] == 1
268
+ else:
269
+ api.set_device_control.assert_awaited_once_with("SN123", {3013: "1"})
270
+
271
+
248
272
  @pytest.mark.asyncio
249
273
  async def test_restart_device():
250
274
  """Test restart_device sends controlId 3013 with value '1'."""
@@ -0,0 +1,46 @@
1
+ """Tests for _ensure_authenticated."""
2
+
3
+ from unittest.mock import AsyncMock, MagicMock
4
+
5
+ import pytest
6
+
7
+ from hyxi_cloud_api.api import HyxiApiClient
8
+
9
+
10
+ class DummyError(Exception):
11
+ """A dummy exception class for testing."""
12
+
13
+
14
+ @pytest.fixture
15
+ def api_client():
16
+ """Fixture for HyxiApiClient."""
17
+ session = MagicMock()
18
+ return HyxiApiClient("access_key", "secret_key", "https://api.example.com", session)
19
+
20
+
21
+ @pytest.mark.asyncio
22
+ async def test_ensure_authenticated_success(api_client):
23
+ """Test _ensure_authenticated succeeds without raising when token refresh is successful."""
24
+ api_client._refresh_token = AsyncMock(return_value=True)
25
+ await api_client._ensure_authenticated(DummyError)
26
+ api_client._refresh_token.assert_awaited_once()
27
+
28
+
29
+ @pytest.mark.asyncio
30
+ async def test_ensure_authenticated_auth_failed(api_client):
31
+ """Test _ensure_authenticated raises error_cls with specific message on auth_failed."""
32
+ api_client._refresh_token = AsyncMock(return_value="auth_failed")
33
+ with pytest.raises(DummyError, match="Authentication failed"):
34
+ await api_client._ensure_authenticated(DummyError)
35
+
36
+
37
+ @pytest.mark.asyncio
38
+ async def test_ensure_authenticated_no_token(api_client):
39
+ """Test _ensure_authenticated raises error_cls when token cannot be obtained."""
40
+ api_client._refresh_token = AsyncMock(return_value=False)
41
+ with pytest.raises(DummyError, match="Could not obtain API token"):
42
+ await api_client._ensure_authenticated(DummyError)
43
+
44
+ api_client._refresh_token = AsyncMock(return_value=None)
45
+ with pytest.raises(DummyError, match="Could not obtain API token"):
46
+ await api_client._ensure_authenticated(DummyError)
@@ -86,7 +86,6 @@ async def test_fetch_device_list_for_plant_debug_logging(caplog):
86
86
  await api._fetch_device_list_for_plant("plant123")
87
87
  assert "HYXI Discovered Devices for Plant" in caplog.text
88
88
  # SN12345678 -> 10 chars. _mask_id should show last 4. 10-4 = 6 X's.
89
- assert "5d82af15" in caplog.text
90
89
 
91
90
 
92
91
  @pytest.mark.asyncio
@@ -0,0 +1,133 @@
1
+ import sys
2
+ from unittest.mock import MagicMock
3
+
4
+ if "aiohttp" not in sys.modules or not hasattr(sys.modules["aiohttp"], "ClientError"):
5
+ m = MagicMock()
6
+
7
+ class MockExp(Exception):
8
+ def __init__(self, *args, **kwargs):
9
+ super().__init__(*args)
10
+ for k, v in kwargs.items():
11
+ setattr(self, k, v)
12
+
13
+ m.ClientError = MockExp
14
+ m.ClientResponseError = type("ClientResponseError", (MockExp,), {})
15
+ m.ContentTypeError = type("ContentTypeError", (MockExp,), {})
16
+ sys.modules["aiohttp"] = m
17
+ mock_aiohttp = sys.modules["aiohttp"]
18
+
19
+ """Tests for _filter_collector_metrics optimization."""
20
+
21
+ from src.hyxi_cloud_api.api import (
22
+ _filter_collector_metrics,
23
+ _is_collector_key_allowed,
24
+ )
25
+
26
+
27
+ def test_filter_collector_metrics_filtering():
28
+ """Verify that sensitive metrics are filtered out."""
29
+ m_raw = {
30
+ "totalE": "2731.9",
31
+ "pbat": "-500",
32
+ "gridP": "1.5",
33
+ "ph1Loadp": "0.5",
34
+ "ph2Loadp": "0.5",
35
+ "ph3Loadp": "0.5",
36
+ "temp": "40.5",
37
+ "vpv1": "400",
38
+ "vbat": "52",
39
+ "soc": "100",
40
+ }
41
+
42
+ filtered = _filter_collector_metrics(m_raw)
43
+
44
+ # These should be filtered out
45
+ assert "pbat" not in filtered
46
+ assert "gridP" not in filtered
47
+ assert "ph1Loadp" not in filtered
48
+ assert "ph2Loadp" not in filtered
49
+ assert "ph3Loadp" not in filtered
50
+ assert "vpv1" not in filtered
51
+ assert "vbat" not in filtered
52
+
53
+ # These should remain
54
+ assert "totalE" in filtered
55
+ assert "temp" in filtered
56
+ assert "soc" in filtered
57
+ assert filtered["totalE"] == "2731.9"
58
+ assert filtered["temp"] == "40.5"
59
+ assert filtered["soc"] == "100"
60
+
61
+
62
+ def test_filter_collector_metrics_empty():
63
+ """Verify empty dict handling."""
64
+ assert not _filter_collector_metrics({})
65
+
66
+
67
+ def test_filter_collector_metrics_no_match():
68
+ """Verify dict with no matches handling."""
69
+ m_raw = {"abc": "123", "def": "456"}
70
+ assert _filter_collector_metrics(m_raw) == m_raw
71
+
72
+
73
+ def test_filter_collector_metrics_case_insensitivity():
74
+ """Verify case insensitivity of filtering."""
75
+ m_raw = {"BATTERY_TEMP": "25", "PV_VOLTAGE": "200"}
76
+ filtered = _filter_collector_metrics(m_raw)
77
+ assert "BATTERY_TEMP" not in filtered
78
+ assert "PV_VOLTAGE" not in filtered
79
+
80
+
81
+ def test_is_collector_key_allowed_true():
82
+ """Verify that safe keys are allowed."""
83
+ assert _is_collector_key_allowed("totalE") is True
84
+ assert _is_collector_key_allowed("temp") is True
85
+ assert _is_collector_key_allowed("soc") is True
86
+ assert _is_collector_key_allowed("voltage") is True
87
+ assert _is_collector_key_allowed("current") is True
88
+ assert _is_collector_key_allowed("randomKey") is True
89
+
90
+
91
+ def test_is_collector_key_allowed_false():
92
+ """Verify that sensitive keys are not allowed."""
93
+ assert _is_collector_key_allowed("pbat") is False
94
+ assert _is_collector_key_allowed("gridP") is False
95
+ assert _is_collector_key_allowed("ph1Loadp") is False
96
+ assert _is_collector_key_allowed("ph2Loadp") is False
97
+ assert _is_collector_key_allowed("ph3Loadp") is False
98
+ assert _is_collector_key_allowed("vpv1") is False
99
+ assert _is_collector_key_allowed("vbat") is False
100
+
101
+
102
+ def test_is_collector_key_allowed_case_insensitivity():
103
+ """Verify case insensitivity."""
104
+ assert _is_collector_key_allowed("BAT_VOLTAGE") is False
105
+ assert _is_collector_key_allowed("PV_VOLTAGE") is False
106
+ assert _is_collector_key_allowed("GRID_POWER") is False
107
+ assert _is_collector_key_allowed("LOAD_POWER") is False
108
+ assert _is_collector_key_allowed("PH1_LOAD") is False
109
+ assert _is_collector_key_allowed("PH2_LOAD") is False
110
+ assert _is_collector_key_allowed("PH3_LOAD") is False
111
+ assert _is_collector_key_allowed("BaT") is False
112
+ assert _is_collector_key_allowed("pBAt") is False
113
+
114
+
115
+ def test_is_collector_key_allowed_cache():
116
+ """Verify that the lru_cache works and caches results."""
117
+ # Clear cache to ensure clean state
118
+ _is_collector_key_allowed.cache_clear()
119
+
120
+ # First call - should be a miss
121
+ res1 = _is_collector_key_allowed("totalE")
122
+
123
+ # Second call - should be a hit
124
+ res2 = _is_collector_key_allowed("totalE")
125
+
126
+ # Check cache info
127
+ cache_info = _is_collector_key_allowed.cache_info()
128
+
129
+ assert res1 is True
130
+ assert res2 is True
131
+ assert cache_info.hits == 1
132
+ assert cache_info.misses == 1
133
+ assert cache_info.currsize == 1
@@ -0,0 +1,59 @@
1
+ """Tests for _flatten_nested_push_device error handling."""
2
+
3
+ from hyxi_cloud_api.api import _flatten_nested_push_device
4
+
5
+
6
+ def test_flatten_nested_push_device_record_collecttime_valueerror():
7
+ """Test ValueError is handled when collectTime in record is an invalid string."""
8
+ device = {"record": {"collectTime": "invalid_time"}}
9
+ flat = _flatten_nested_push_device(device)
10
+ assert flat["collectTime"] == "invalid_time"
11
+
12
+
13
+ def test_flatten_nested_push_device_record_collecttime_typeerror():
14
+ """Test TypeError is handled when collectTime in record is of an invalid type."""
15
+ device = {"record": {"collectTime": {"nested": "dict"}}}
16
+ flat = _flatten_nested_push_device(device)
17
+ assert flat["collectTime"] == {"nested": "dict"}
18
+
19
+
20
+ def test_flatten_nested_push_device_root_collecttime_valueerror():
21
+ """Test ValueError is handled when collectTime in root is an invalid string."""
22
+ device = {"collectTime": "invalid_time"}
23
+ flat = _flatten_nested_push_device(device)
24
+ assert flat["collectTime"] == "invalid_time"
25
+
26
+
27
+ def test_flatten_nested_push_device_root_collecttime_typeerror():
28
+ """Test TypeError is handled when collectTime in root is of an invalid type."""
29
+ device = {"collectTime": ["invalid", "type"]}
30
+ flat = _flatten_nested_push_device(device)
31
+ assert flat["collectTime"] == ["invalid", "type"]
32
+
33
+
34
+ def test_flatten_nested_push_device_root_collecttime_valid_large():
35
+ """Test large valid collectTime in root."""
36
+ device = {"collectTime": 20000000000}
37
+ flat = _flatten_nested_push_device(device)
38
+ assert flat["collectTime"] == 20000000.0
39
+
40
+
41
+ def test_flatten_nested_push_device_root_collecttime_valid_small():
42
+ """Test small valid collectTime in root."""
43
+ device = {"collectTime": 5000000000}
44
+ flat = _flatten_nested_push_device(device)
45
+ assert flat["collectTime"] == 5000000000
46
+
47
+
48
+ def test_flatten_nested_push_device_root_reporttimestamp():
49
+ """Test reportTimestamp in root."""
50
+ device = {"reportTimestamp": 1234567890}
51
+ flat = _flatten_nested_push_device(device)
52
+ assert flat["reportTimestamp"] == 1234567890
53
+
54
+
55
+ def test_flatten_nested_push_device_system_workmode():
56
+ """Test workMode in system."""
57
+ device = {"system": {"workMode": "test_mode"}}
58
+ flat = _flatten_nested_push_device(device)
59
+ assert flat["workMode"] == "test_mode"
@@ -41,7 +41,8 @@ async def test_fetch_device_info_network_error(caplog):
41
41
  # Use a longer SN so it's not fully masked to ****
42
42
  await api._fetch_device_info("10602251600016", entry)
43
43
 
44
- assert "Error fetching device info for fefbfd75: Connection reset" in caplog.text
44
+ assert "Error fetching device info for " in caplog.text
45
+ assert "Connection reset" in caplog.text
45
46
  # Ensure it didn't crash and entry was not updated with metrics
46
47
  assert "sw_version" not in entry
47
48
 
@@ -70,7 +71,7 @@ async def test_fetch_device_info_invalid_json(caplog):
70
71
  entry = {"metrics": {}, "device_type_code": "INVERTER"}
71
72
  await api._fetch_device_info("10602251600016", entry)
72
73
 
73
- assert "Error fetching device info for fefbfd75" in caplog.text
74
+ assert "Error fetching device info for " in caplog.text
74
75
  assert "sw_version" not in entry
75
76
 
76
77
 
@@ -97,7 +98,7 @@ async def test_fetch_device_info_api_error(caplog):
97
98
  entry = {"metrics": {}, "device_type_code": "INVERTER"}
98
99
  await api._fetch_device_info("10602251600016", entry)
99
100
 
100
- assert "HYXI INFO API Rejected for fefbfd75" in caplog.text
101
+ assert "HYXI INFO API Rejected for " in caplog.text
101
102
  assert "success" in caplog.text # _sanitize_dict logs full response dict
102
103
  assert "sw_version" not in entry
103
104
 
@@ -117,10 +118,8 @@ async def test_fetch_device_info_client_error_handling(caplog):
117
118
  await api._fetch_device_info("10602251600016", entry)
118
119
 
119
120
  # Verify that the exception is caught and logged
120
- assert (
121
- "Error fetching device info for fefbfd75: Simulated fallback error"
122
- in caplog.text
123
- )
121
+ assert "Error fetching device info for" in caplog.text
122
+ assert "Simulated fallback error" in caplog.text
124
123
  # Ensure it didn't crash and entry was not updated with metrics
125
124
  assert "sw_version" not in entry
126
125
 
@@ -1,8 +1,9 @@
1
1
  """Tests for the _mask_id and _sanitize_dict log sanitisation helpers in api.py."""
2
2
 
3
3
  import hashlib
4
+ import hmac
4
5
 
5
- from hyxi_cloud_api.api import _mask_id, _sanitize_dict
6
+ from hyxi_cloud_api.api import _LOG_SALT, _mask_id, _sanitize_dict
6
7
 
7
8
 
8
9
  class TestMaskId:
@@ -13,7 +14,10 @@ class TestMaskId:
13
14
  def test_normal_sn_is_masked_with_hash(self):
14
15
  """A typical device SN should be masked using a hash, yielding an 8-char string."""
15
16
  sn = "HYXABC12345678"
16
- expected = hashlib.sha256(sn.encode("utf-8")).hexdigest()[:8]
17
+
18
+ expected = hmac.new(_LOG_SALT, sn.encode("utf-8"), hashlib.sha256).hexdigest()[
19
+ :8
20
+ ]
17
21
  result = _mask_id(sn)
18
22
  assert result == expected
19
23
  assert len(result) == 8
@@ -41,7 +41,8 @@ async def test_fetch_device_metrics_request_error(caplog):
41
41
  # Use a longer SN so it's not fully masked to ****
42
42
  await api._fetch_device_metrics("10602251600016", entry)
43
43
 
44
- assert "Error fetching metrics for fefbfd75: Mock client error" in caplog.text
44
+ assert "Error fetching metrics for " in caplog.text
45
+ assert "Mock client error" in caplog.text
45
46
  # Ensure it didn't crash and entry was not updated with metrics
46
47
  assert not entry["metrics"]
47
48
 
@@ -60,7 +61,8 @@ async def test_fetch_device_metrics_network_error(caplog):
60
61
  # Use a longer SN so it's not fully masked to ****
61
62
  await api._fetch_device_metrics("10602251600016", entry)
62
63
 
63
- assert "Error fetching metrics for fefbfd75: Connection reset" in caplog.text
64
+ assert "Error fetching metrics for " in caplog.text
65
+ assert "Connection reset" in caplog.text
64
66
  # Ensure it didn't crash and entry was not updated with metrics
65
67
  assert not entry["metrics"]
66
68
 
@@ -90,7 +92,7 @@ async def test_fetch_device_metrics_invalid_json(caplog):
90
92
  entry = {"metrics": {}, "device_type_code": "INVERTER"}
91
93
  await api._fetch_device_metrics("10602251600016", entry)
92
94
 
93
- assert "Error fetching metrics for fefbfd75" in caplog.text
95
+ assert "Error fetching metrics for " in caplog.text
94
96
  assert not entry["metrics"]
95
97
 
96
98
 
@@ -118,7 +120,7 @@ async def test_fetch_device_metrics_api_error(caplog):
118
120
  entry = {"metrics": {}, "device_type_code": "INVERTER"}
119
121
  await api._fetch_device_metrics("10602251600016", entry)
120
122
 
121
- assert "HYXI API metrics rejected for fefbfd75" in caplog.text
123
+ assert "HYXI API metrics rejected for " in caplog.text
122
124
  assert "success" in caplog.text # _sanitize_dict logs full response dict
123
125
  assert not entry["metrics"]
124
126
 
@@ -138,7 +140,7 @@ async def test_fetch_ems_basic_data_no_data(caplog):
138
140
  entry = {"metrics": {}, "device_type_code": "EMS"}
139
141
  await api._fetch_all_for_device("10602251600016", entry, "INVERTER")
140
142
 
141
- assert "HYXI EMS telemetry probe returned no data for fefbfd75" in caplog.text
143
+ assert "HYXI EMS telemetry probe returned no data for " in caplog.text
142
144
  # No EMS data should be merged into metrics.
143
145
  assert "new_metric" not in entry["metrics"]
144
146
 
@@ -160,7 +162,7 @@ async def test_fetch_ems_basic_data_error(caplog):
160
162
 
161
163
  # No EMS data should be merged into metrics.
162
164
  assert "batSoc" not in entry["metrics"]
163
- assert "HYXI EMS telemetry probe returned no data for fefbfd75" in caplog.text
165
+ assert "HYXI EMS telemetry probe returned no data for " in caplog.text
164
166
 
165
167
 
166
168
  @pytest.mark.asyncio
@@ -177,10 +179,8 @@ async def test_query_ems_basic_details_error(caplog):
177
179
 
178
180
  assert result == {}
179
181
 
180
- assert (
181
- "HYXI EMS Basic Data Request Failed for fefbfd75: EMS query failed"
182
- in caplog.text
183
- )
182
+ assert "HYXI EMS Basic Data Request Failed for" in caplog.text
183
+ assert "EMS query failed" in caplog.text
184
184
 
185
185
 
186
186
  @pytest.mark.asyncio
@@ -197,10 +197,8 @@ async def test_query_ems_basic_details_network_error(caplog):
197
197
 
198
198
  assert result == {}
199
199
 
200
- assert (
201
- "HYXI EMS Basic Data Request Failed for fefbfd75: Connection reset"
202
- in caplog.text
203
- )
200
+ assert "HYXI EMS Basic Data Request Failed for" in caplog.text
201
+ assert "Connection reset" in caplog.text
204
202
 
205
203
 
206
204
  @pytest.mark.asyncio
@@ -221,7 +219,7 @@ async def test_query_ems_basic_details_non_zero_code(caplog):
221
219
  result = await api.query_ems_basic_details("10602251600016")
222
220
 
223
221
  assert result == {}
224
- assert "HYXI EMS Basic Data Request Rejected for fefbfd75" in caplog.text
222
+ assert "HYXI EMS Basic Data Request Rejected for " in caplog.text
225
223
 
226
224
 
227
225
  @pytest.mark.asyncio
@@ -237,5 +235,5 @@ async def test_query_ems_basic_details_malformed_response(caplog):
237
235
  result = await api.query_ems_basic_details("10602251600016")
238
236
 
239
237
  assert result == {}
240
- assert "HYXI EMS Basic Data Request Failed for fefbfd75" in caplog.text
238
+ assert "HYXI EMS Basic Data Request Failed for " in caplog.text
241
239
  assert "object has no attribute 'get'" in caplog.text
@@ -305,3 +305,68 @@ def test_process_push_data_flat_ms_collect_time():
305
305
  assert metrics["pv1v"] == 119.0
306
306
  assert metrics["pv1i"] == 1.2
307
307
  assert metrics["pv1p"] == 142.8
308
+
309
+
310
+ def test_process_push_data_invalid_time_formats():
311
+ """Test process_push_data correctly handles ValueError and TypeError when parsing timestamps."""
312
+
313
+ api = HyxiApiClient("ak", "sk", "https://api.com", MagicMock())
314
+ api._discovery_cache["device_info"] = {
315
+ "TEST_DEV": {
316
+ "model": "TEST",
317
+ "device_type_code": "TEST_TYPE",
318
+ "device_name": "Test Device",
319
+ }
320
+ }
321
+
322
+ # Test invalid collectTime (ValueError)
323
+ payload_invalid_collect_time = {
324
+ "dataList": [
325
+ {
326
+ "deviceSn": "TEST_DEV",
327
+ "collectTime": "invalid-time",
328
+ }
329
+ ]
330
+ }
331
+
332
+ # Test invalid collectTime (TypeError)
333
+ payload_type_err_collect_time = {
334
+ "dataList": [
335
+ {
336
+ "deviceSn": "TEST_DEV",
337
+ "collectTime": {"a": "b"},
338
+ }
339
+ ]
340
+ }
341
+
342
+ # Test invalid reportTimestamp (ValueError)
343
+ payload_invalid_report_ts = {
344
+ "dataList": [
345
+ {
346
+ "deviceSn": "TEST_DEV",
347
+ "reportTimestamp": "invalid-time",
348
+ }
349
+ ]
350
+ }
351
+
352
+ # Test invalid reportTimestamp (TypeError)
353
+ payload_type_err_report_ts = {
354
+ "dataList": [
355
+ {
356
+ "deviceSn": "TEST_DEV",
357
+ "reportTimestamp": {"a": "b"},
358
+ }
359
+ ]
360
+ }
361
+
362
+ res1 = api.process_push_data(payload_invalid_collect_time)
363
+ res2 = api.process_push_data(payload_type_err_collect_time)
364
+ res3 = api.process_push_data(payload_invalid_report_ts)
365
+ res4 = api.process_push_data(payload_type_err_report_ts)
366
+
367
+ # Check that last_seen was not updated to a specific time, but fell back to now_utc
368
+ # In api.process_push_data, if it fails, last_seen = now_utc
369
+ assert "last_seen" in res1["TEST_DEV"]["metrics"]
370
+ assert "last_seen" in res2["TEST_DEV"]["metrics"]
371
+ assert "last_seen" in res3["TEST_DEV"]["metrics"]
372
+ assert "last_seen" in res4["TEST_DEV"]["metrics"]
@@ -5,7 +5,7 @@ from unittest.mock import AsyncMock, MagicMock
5
5
 
6
6
  import pytest
7
7
 
8
- from src.hyxi_cloud_api.api import HyxiApiClient, _sanitize_dict
8
+ from src.hyxi_cloud_api.api import HyxiApiClient, _mask_id, _sanitize_dict
9
9
 
10
10
  # Mock aiohttp before importing the API client
11
11
  if "aiohttp" not in sys.modules or not hasattr(sys.modules["aiohttp"], "ClientError"):
@@ -44,20 +44,20 @@ def test_sanitize_dict_recursive():
44
44
  sanitized = _sanitize_dict(raw)
45
45
 
46
46
  assert sanitized["plantAddress"] == "[REDACTED]"
47
- assert sanitized["deviceSn"] == "c90391cf"
47
+ assert sanitized["deviceSn"] == _mask_id("SN123456789")
48
48
  assert sanitized["normalKey"] == "normalValue"
49
49
 
50
50
  # Check nested list of dicts
51
- assert sanitized["data"][0]["deviceSn"] == "795d881c"
52
- assert sanitized["data"][0]["nested"]["plantId"] == "bd83cb6a"
51
+ assert sanitized["data"][0]["deviceSn"] == _mask_id("SN987654321")
52
+ assert sanitized["data"][0]["nested"]["plantId"] == _mask_id("PID123")
53
53
  assert sanitized["data"][0]["nested"]["normal"] == "value"
54
54
  assert sanitized["data"][1] == "not a dict"
55
55
 
56
56
  # Check nested dict
57
- assert sanitized["nestedDict"]["batSn"] == "8015218b"
57
+ assert sanitized["nestedDict"]["batSn"] == _mask_id("BAT12345")
58
58
 
59
59
  # Check nested list
60
- assert sanitized["nestedList"][0][0]["deviceSn"] == "083c86de"
60
+ assert sanitized["nestedList"][0][0]["deviceSn"] == _mask_id("SN0000")
61
61
 
62
62
 
63
63
  @pytest.mark.asyncio
@@ -1,75 +0,0 @@
1
- import sys
2
- from unittest.mock import MagicMock
3
-
4
- if "aiohttp" not in sys.modules or not hasattr(sys.modules["aiohttp"], "ClientError"):
5
- m = MagicMock()
6
-
7
- class MockExp(Exception):
8
- def __init__(self, *args, **kwargs):
9
- super().__init__(*args)
10
- for k, v in kwargs.items():
11
- setattr(self, k, v)
12
-
13
- m.ClientError = MockExp
14
- m.ClientResponseError = type("ClientResponseError", (MockExp,), {})
15
- m.ContentTypeError = type("ContentTypeError", (MockExp,), {})
16
- sys.modules["aiohttp"] = m
17
- mock_aiohttp = sys.modules["aiohttp"]
18
-
19
- """Tests for _filter_collector_metrics optimization."""
20
-
21
- from src.hyxi_cloud_api.api import _filter_collector_metrics
22
-
23
-
24
- def test_filter_collector_metrics_filtering():
25
- """Verify that sensitive metrics are filtered out."""
26
- m_raw = {
27
- "totalE": "2731.9",
28
- "pbat": "-500",
29
- "gridP": "1.5",
30
- "ph1Loadp": "0.5",
31
- "ph2Loadp": "0.5",
32
- "ph3Loadp": "0.5",
33
- "temp": "40.5",
34
- "vpv1": "400",
35
- "vbat": "52",
36
- "soc": "100",
37
- }
38
-
39
- filtered = _filter_collector_metrics(m_raw)
40
-
41
- # These should be filtered out
42
- assert "pbat" not in filtered
43
- assert "gridP" not in filtered
44
- assert "ph1Loadp" not in filtered
45
- assert "ph2Loadp" not in filtered
46
- assert "ph3Loadp" not in filtered
47
- assert "vpv1" not in filtered
48
- assert "vbat" not in filtered
49
-
50
- # These should remain
51
- assert "totalE" in filtered
52
- assert "temp" in filtered
53
- assert "soc" in filtered
54
- assert filtered["totalE"] == "2731.9"
55
- assert filtered["temp"] == "40.5"
56
- assert filtered["soc"] == "100"
57
-
58
-
59
- def test_filter_collector_metrics_empty():
60
- """Verify empty dict handling."""
61
- assert not _filter_collector_metrics({})
62
-
63
-
64
- def test_filter_collector_metrics_no_match():
65
- """Verify dict with no matches handling."""
66
- m_raw = {"abc": "123", "def": "456"}
67
- assert _filter_collector_metrics(m_raw) == m_raw
68
-
69
-
70
- def test_filter_collector_metrics_case_insensitivity():
71
- """Verify case insensitivity of filtering."""
72
- m_raw = {"BATTERY_TEMP": "25", "PV_VOLTAGE": "200"}
73
- filtered = _filter_collector_metrics(m_raw)
74
- assert "BATTERY_TEMP" not in filtered
75
- assert "PV_VOLTAGE" not in filtered
File without changes
File without changes
File without changes