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.
- {hyxi_cloud_api-1.3.5/src/hyxi_cloud_api.egg-info → hyxi_cloud_api-1.3.7}/PKG-INFO +1 -1
- {hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7}/pyproject.toml +1 -1
- {hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7}/src/hyxi_cloud_api/__init__.py +1 -1
- {hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7}/src/hyxi_cloud_api/api.py +62 -7
- {hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7/src/hyxi_cloud_api.egg-info}/PKG-INFO +1 -1
- {hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7}/tests/test_compute_battery_metrics.py +31 -0
- hyxi_cloud_api-1.3.7/tests/test_compute_grid_metrics.py +79 -0
- {hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7}/tests/test_device_control.py +24 -0
- {hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7}/tests/test_push.py +14 -0
- {hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7}/tests/test_subscriptions.py +57 -0
- hyxi_cloud_api-1.3.5/tests/test_compute_grid_metrics.py +0 -39
- {hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7}/LICENSE +0 -0
- {hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7}/README.md +0 -0
- {hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7}/setup.cfg +0 -0
- {hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7}/src/hyxi_cloud_api.egg-info/SOURCES.txt +0 -0
- {hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7}/src/hyxi_cloud_api.egg-info/dependency_links.txt +0 -0
- {hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7}/src/hyxi_cloud_api.egg-info/requires.txt +0 -0
- {hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7}/src/hyxi_cloud_api.egg-info/top_level.txt +0 -0
- {hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7}/tests/test_additional_coverage.py +0 -0
- {hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7}/tests/test_alarm_push.py +0 -0
- {hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7}/tests/test_all_in_one.py +0 -0
- {hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7}/tests/test_api.py +0 -0
- {hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7}/tests/test_build_plant_tasks.py +0 -0
- {hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7}/tests/test_caching.py +0 -0
- {hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7}/tests/test_compute_derived_metrics.py +0 -0
- {hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7}/tests/test_compute_load_metrics.py +0 -0
- {hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7}/tests/test_device_entry.py +0 -0
- {hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7}/tests/test_devices_errors.py +0 -0
- {hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7}/tests/test_discovery.py +0 -0
- {hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7}/tests/test_execute_device_tasks.py +0 -0
- {hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7}/tests/test_execute_metric_tasks.py +0 -0
- {hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7}/tests/test_execute_metrics_and_map_alarms.py +0 -0
- {hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7}/tests/test_extract_battery_info.py +0 -0
- {hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7}/tests/test_extract_device_info_metadata.py +0 -0
- {hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7}/tests/test_fetch_and_process_alarms.py +0 -0
- {hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7}/tests/test_fetch_device_list_for_plant.py +0 -0
- {hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7}/tests/test_fetch_plants.py +0 -0
- {hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7}/tests/test_fetch_state.py +0 -0
- {hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7}/tests/test_fetch_sub_device_list.py +0 -0
- {hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7}/tests/test_filter_metrics.py +0 -0
- {hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7}/tests/test_fuzz_parser.py +0 -0
- {hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7}/tests/test_get_f.py +0 -0
- {hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7}/tests/test_handle_back_discovery_alarm.py +0 -0
- {hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7}/tests/test_info_errors.py +0 -0
- {hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7}/tests/test_mask_id.py +0 -0
- {hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7}/tests/test_metrics_errors.py +0 -0
- {hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7}/tests/test_parse_data_list.py +0 -0
- {hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7}/tests/test_parse_ems_kv.py +0 -0
- {hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7}/tests/test_parser.py +0 -0
- {hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7}/tests/test_process_alarms_and_back_discovery.py +0 -0
- {hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7}/tests/test_process_devices_for_plant.py +0 -0
- {hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7}/tests/test_sanitize_dict.py +0 -0
- {hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7}/tests/test_sanitize_list.py +0 -0
- {hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7}/tests/test_security_fix.py +0 -0
- {hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7}/tests/test_token_errors.py +0 -0
- {hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7}/tests/test_token_handling.py +0 -0
- {hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7}/tests/test_validate_post_rate_ms.py +0 -0
- {hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7}/tests/test_validate_subscription_device_sns.py +0 -0
|
@@ -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.
|
|
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
|
-
|
|
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
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
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
|
|
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 =
|
|
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(
|
|
@@ -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
|
|
File without changes
|
{hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7}/src/hyxi_cloud_api.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7}/tests/test_process_alarms_and_back_discovery.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{hyxi_cloud_api-1.3.5 → hyxi_cloud_api-1.3.7}/tests/test_validate_subscription_device_sns.py
RENAMED
|
File without changes
|