Skip to content

Commit 7b8719d

Browse files
committed
kW instead of watts, added local weather calls, capacity sensors
1 parent 3ed6e78 commit 7b8719d

8 files changed

Lines changed: 146 additions & 32 deletions

File tree

‎custom_components/sigenergy_cloud/api.py‎

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
BASE_URLS,
1818
DEFAULT_REGION,
1919
ENDPOINT_AUTH,
20+
ENDPOINT_CURRENT_LOCAL_WEATHER,
2021
ENDPOINT_ENERGY_FLOW,
2122
ENDPOINT_MODE_CURRENT,
2223
ENDPOINT_MODE_SET,
@@ -118,16 +119,23 @@ async def authenticate(self) -> None:
118119
auth=auth,
119120
) as resp:
120121
resp_text = await resp.text()
122+
result: dict[str, Any]
123+
if resp_text:
124+
try:
125+
result = json.loads(resp_text)
126+
except json.JSONDecodeError:
127+
result = await resp.json()
128+
else:
129+
result = await resp.json()
121130
_LOGGER.debug(
122131
"Auth POST response status=%s body=%s",
123132
resp.status,
124-
_redact_auth_payload(resp_text),
133+
_redact_auth_payload(resp_text) if resp_text else json.dumps(result, ensure_ascii=True),
125134
)
126135
if resp.status != 200:
127136
raise SigenCloudAuthError(
128137
f"Authentication failed (HTTP {resp.status}): {resp_text}"
129138
)
130-
result = json.loads(resp_text)
131139
except aiohttp.ClientError as err:
132140
raise SigenCloudApiError(f"Connection error during auth: {err}") from err
133141

@@ -155,14 +163,21 @@ async def _do_refresh_token(self) -> None:
155163
auth=auth,
156164
) as resp:
157165
resp_text = await resp.text()
166+
result: dict[str, Any]
167+
if resp_text:
168+
try:
169+
result = json.loads(resp_text)
170+
except json.JSONDecodeError:
171+
result = await resp.json()
172+
else:
173+
result = await resp.json()
158174
_LOGGER.debug(
159175
"Refresh POST response status=%s body=%s",
160176
resp.status,
161-
_redact_auth_payload(resp_text),
177+
_redact_auth_payload(resp_text) if resp_text else json.dumps(result, ensure_ascii=True),
162178
)
163179
if resp.status != 200:
164180
raise SigenCloudAuthError("Token refresh failed, re-auth needed")
165-
result = json.loads(resp_text)
166181
except aiohttp.ClientError as err:
167182
raise SigenCloudApiError(
168183
f"Connection error during token refresh: {err}"
@@ -295,6 +310,18 @@ async def toggle_smart_load(
295310
json_data={"manualSwitch": 1 if turn_on else 0},
296311
)
297312

313+
# ------------------------------------------------------------------
314+
# Weather
315+
# ------------------------------------------------------------------
316+
317+
async def get_current_local_weather(self, station_sn_code: str) -> dict[str, Any]:
318+
"""Get current local weather for a station serial number code."""
319+
return await self._request(
320+
"GET",
321+
ENDPOINT_CURRENT_LOCAL_WEATHER,
322+
params={"stationSnCode": station_sn_code},
323+
)
324+
298325
# ------------------------------------------------------------------
299326
# Convenience: fetch all data in one coordinator cycle
300327
# ------------------------------------------------------------------

‎custom_components/sigenergy_cloud/const.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
ENDPOINT_SYSTEM_DEVICES = "device/system/device/systemDevice/card"
4141
ENDPOINT_SMART_LOADS = "device/tp-device/smart-loads"
4242
ENDPOINT_SMART_LOAD_TOGGLE = "device/tp-device/smart-loads/control-mode/manual/switch"
43+
ENDPOINT_CURRENT_LOCAL_WEATHER = "device/sigen/device/getCurrentLocalWeather"
4344
ENDPOINT_REALTIME_CONSUMPTION = (
4445
"data-process/sigen/station/statistics/real-time-consumption"
4546
)

‎custom_components/sigenergy_cloud/sensor.py‎

Lines changed: 58 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
SensorStateClass,
1414
)
1515
from homeassistant.config_entries import ConfigEntry
16-
from homeassistant.const import PERCENTAGE, UnitOfPower
16+
from homeassistant.const import PERCENTAGE
1717
from homeassistant.core import HomeAssistant
1818
from homeassistant.helpers.entity_platform import AddEntitiesCallback
1919
from homeassistant.helpers.update_coordinator import CoordinatorEntity, DataUpdateCoordinator
@@ -42,14 +42,42 @@ def _get_ef(data: dict[str, Any], key: str) -> float | None:
4242
return None
4343

4444

45+
def _get_ef_any(data: dict[str, Any], *keys: str) -> float | None:
46+
"""Extract the first available numeric value from energy_flow using fallback keys."""
47+
for key in keys:
48+
value = _get_ef(data, key)
49+
if value is not None:
50+
return value
51+
return None
52+
53+
54+
def _get_ef_kw(data: dict[str, Any], key: str) -> float | None:
55+
"""Extract a power value already reported in kW by the API."""
56+
return _get_ef(data, key)
57+
58+
59+
def _get_station_metric(data: dict[str, Any], key: str) -> float | None:
60+
"""Extract a numeric value from station_info."""
61+
station = data.get("station_info", {})
62+
if not isinstance(station, dict):
63+
return None
64+
value = station.get(key)
65+
if value is None:
66+
return None
67+
try:
68+
return float(value)
69+
except (TypeError, ValueError):
70+
return None
71+
72+
4573
SENSOR_DESCRIPTIONS: tuple[SigenEnergySensorDescription, ...] = (
4674
SigenEnergySensorDescription(
4775
key="pv_power",
4876
translation_key="pv_power",
49-
native_unit_of_measurement=UnitOfPower.WATT,
77+
native_unit_of_measurement="kW",
5078
device_class=SensorDeviceClass.POWER,
5179
state_class=SensorStateClass.MEASUREMENT,
52-
value_fn=lambda d: _get_ef(d, "pvPower"),
80+
value_fn=lambda d: _get_ef_kw(d, "pvPower"),
5381
),
5482
SigenEnergySensorDescription(
5583
key="battery_soc",
@@ -69,32 +97,46 @@ def _get_ef(data: dict[str, Any], key: str) -> float | None:
6997
SigenEnergySensorDescription(
7098
key="battery_power",
7199
translation_key="battery_power",
72-
native_unit_of_measurement=UnitOfPower.WATT,
100+
native_unit_of_measurement="kW",
73101
device_class=SensorDeviceClass.POWER,
74102
state_class=SensorStateClass.MEASUREMENT,
75-
value_fn=lambda d: _get_ef(d, "batteryPower"),
103+
value_fn=lambda d: _get_ef_kw(d, "batteryPower"),
76104
),
77105
SigenEnergySensorDescription(
78106
key="grid_power",
79107
translation_key="grid_power",
80-
native_unit_of_measurement=UnitOfPower.WATT,
108+
native_unit_of_measurement="kW",
81109
device_class=SensorDeviceClass.POWER,
82110
state_class=SensorStateClass.MEASUREMENT,
83-
value_fn=lambda d: _get_ef(d, "gridPower"),
111+
value_fn=lambda d: _get_ef_any(d, "buySellPower", "gridPower"),
84112
),
85113
SigenEnergySensorDescription(
86114
key="load_power",
87115
translation_key="load_power",
88-
native_unit_of_measurement=UnitOfPower.WATT,
116+
native_unit_of_measurement="kW",
89117
device_class=SensorDeviceClass.POWER,
90118
state_class=SensorStateClass.MEASUREMENT,
91-
value_fn=lambda d: _get_ef(d, "loadPower"),
119+
value_fn=lambda d: _get_ef_kw(d, "loadPower"),
92120
),
93121
SigenEnergySensorDescription(
94122
key="system_online",
95123
translation_key="system_online",
96124
value_fn=lambda d: d.get("energy_flow", {}).get("online"),
97125
),
126+
SigenEnergySensorDescription(
127+
key="pv_capacity",
128+
translation_key="pv_capacity",
129+
native_unit_of_measurement="kW",
130+
state_class=SensorStateClass.MEASUREMENT,
131+
value_fn=lambda d: _get_station_metric(d, "pvCapacity"),
132+
),
133+
SigenEnergySensorDescription(
134+
key="battery_capacity",
135+
translation_key="battery_capacity",
136+
native_unit_of_measurement="kWh",
137+
state_class=SensorStateClass.MEASUREMENT,
138+
value_fn=lambda d: _get_station_metric(d, "batteryCapacity"),
139+
),
98140
)
99141

100142

@@ -107,9 +149,10 @@ async def async_setup_entry(
107149
data: SigenEnergyData = hass.data[DOMAIN][entry.entry_id]
108150
coordinator = data.coordinator
109151
station_id = data.station_id
152+
station_info = data.station_info
110153

111154
entities = [
112-
SigenEnergySensor(coordinator, description, station_id)
155+
SigenEnergySensor(coordinator, description, station_id, station_info)
113156
for description in SENSOR_DESCRIPTIONS
114157
]
115158
async_add_entities(entities)
@@ -128,10 +171,12 @@ def __init__(
128171
coordinator: DataUpdateCoordinator[dict[str, Any]],
129172
description: SigenEnergySensorDescription,
130173
station_id: str,
174+
station_info: dict[str, Any],
131175
) -> None:
132176
super().__init__(coordinator)
133177
self.entity_description = description
134178
self._station_id = station_id
179+
self._station_info = station_info
135180
self._attr_unique_id = f"{station_id}_{description.key}"
136181
self._attr_device_info = {
137182
"identifiers": {(DOMAIN, station_id)},
@@ -144,4 +189,6 @@ def native_value(self) -> float | str | None:
144189
"""Return the sensor value."""
145190
if self.coordinator.data is None:
146191
return None
147-
return self.entity_description.value_fn(self.coordinator.data)
192+
merged_data = dict(self.coordinator.data)
193+
merged_data.setdefault("station_info", self._station_info)
194+
return self.entity_description.value_fn(merged_data)

‎custom_components/sigenergy_cloud/strings.json‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,11 @@
3333
"entity": {
3434
"sensor": {
3535
"pv_power": { "name": "PV Power" },
36+
"pv_capacity": { "name": "PV Capacity" },
3637
"battery_soc": { "name": "Battery State of Charge" },
3738
"battery_soh": { "name": "Battery State of Health" },
3839
"battery_power": { "name": "Battery Power" },
40+
"battery_capacity": { "name": "Battery Capacity" },
3941
"grid_power": { "name": "Grid Power" },
4042
"load_power": { "name": "Load Power" },
4143
"system_online": { "name": "System Online" }

‎custom_components/sigenergy_cloud/translations/en.json‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,11 @@
3333
"entity": {
3434
"sensor": {
3535
"pv_power": { "name": "PV Power" },
36+
"pv_capacity": { "name": "PV Capacity" },
3637
"battery_soc": { "name": "Battery State of Charge" },
3738
"battery_soh": { "name": "Battery State of Health" },
3839
"battery_power": { "name": "Battery Power" },
40+
"battery_capacity": { "name": "Battery Capacity" },
3941
"grid_power": { "name": "Grid Power" },
4042
"load_power": { "name": "Load Power" },
4143
"system_online": { "name": "System Online" }

‎tests/conftest.py‎

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,20 @@
2929
}
3030

3131
SAMPLE_ENERGY_FLOW = {
32-
"pvPower": 5230.0,
33-
"batterySoc": 78.5,
32+
"onOffGridStatus": 0,
33+
"pvDayNrg": 15.58,
34+
"pvPower": 7.7,
35+
"buySellPower": -0.0,
36+
"evPower": 0.0,
37+
"acPower": 0.0,
38+
"acRunStatus": None,
39+
"loadPower": 0.8,
40+
"heatPumpPower": 0.0,
41+
"batteryPower": 6.9,
42+
"batterySoc": 77.3,
43+
"generatorPower": 0.0,
44+
"thirdPvPower": 0.0,
3445
"batterySoh": 99.1,
35-
"batteryPower": -1200.0,
36-
"gridPower": 150.0,
37-
"loadPower": 4080.0,
3846
"online": True,
3947
}
4048

@@ -46,6 +54,7 @@
4654
SAMPLE_ALL_DATA = {
4755
"energy_flow": SAMPLE_ENERGY_FLOW,
4856
"current_mode": SAMPLE_CURRENT_MODE,
57+
"station_info": SAMPLE_STATION,
4958
}
5059

5160
SAMPLE_SYSTEM_DEVICES = [

‎tests/test_api.py‎

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -324,8 +324,8 @@ async def test_get_energy_flow(self, mock_session):
324324
mock_session.request.return_value = api_resp
325325

326326
result = await client.get_energy_flow("STATION001")
327-
assert result["pvPower"] == 5230.0
328-
assert result["batterySoc"] == 78.5
327+
assert result["pvPower"] == 7.7
328+
assert result["batterySoc"] == 77.3
329329

330330
# Verify params
331331
call_kwargs = mock_session.request.call_args[1]
@@ -344,6 +344,23 @@ async def test_get_current_mode(self, mock_session):
344344
result = await client.get_current_mode("STATION001")
345345
assert result["currentMode"] == 0
346346

347+
@pytest.mark.asyncio
348+
async def test_get_current_local_weather(self, mock_session):
349+
client = await self._make_authed_client(mock_session)
350+
351+
weather_data = {"temperature": 23.1, "humidity": 65}
352+
api_resp = make_mock_response(
353+
200,
354+
json_data={"code": 0, "data": weather_data, "message": "success"},
355+
)
356+
mock_session.request.return_value = api_resp
357+
358+
result = await client.get_current_local_weather("102026031800194")
359+
assert result == weather_data
360+
361+
call_kwargs = mock_session.request.call_args[1]
362+
assert call_kwargs["params"] == {"stationSnCode": "102026031800194"}
363+
347364
@pytest.mark.asyncio
348365
async def test_set_operational_mode(self, mock_session):
349366
client = await self._make_authed_client(mock_session)
@@ -421,5 +438,5 @@ async def test_get_all_data(self, mock_session):
421438

422439
assert "energy_flow" in result
423440
assert "current_mode" in result
424-
assert result["energy_flow"]["pvPower"] == 5230.0
441+
assert result["energy_flow"]["pvPower"] == 7.7
425442
assert result["current_mode"]["currentMode"] == 0

0 commit comments

Comments
 (0)