Skip to content
12 changes: 7 additions & 5 deletions homeassistant/components/growatt_server/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,9 @@ def _login_classic_api(
return login_response


V1_DEVICE_TYPES: dict[int, str] = {5: "sph", 7: "min"}


def get_device_list_v1(
api, config: Mapping[str, str]
) -> tuple[list[dict[str, str]], str]:
Expand All @@ -260,18 +263,17 @@ def get_device_list_v1(
f"API error during device list: {e.error_msg or str(e)} (Code: {e.error_code})"
) from e
devices = devices_dict.get("devices", [])
# Only MIN device (type = 7) support implemented in current V1 API
supported_devices = [
{
"deviceSn": device.get("device_sn", ""),
"deviceType": "min",
"deviceType": V1_DEVICE_TYPES[device.get("type")],
}
for device in devices
if device.get("type") == 7
if device.get("type") in V1_DEVICE_TYPES
]

for device in devices:
if device.get("type") != 7:
if device.get("type") not in V1_DEVICE_TYPES:
_LOGGER.warning(
"Device %s with type %s not supported in Open API V1, skipping",
device.get("device_sn", ""),
Expand Down Expand Up @@ -348,7 +350,7 @@ async def async_setup_entry(
hass, config_entry, device["deviceSn"], device["deviceType"], plant_id
)
for device in devices
if device["deviceType"] in ["inverter", "tlx", "storage", "mix", "min"]
if device["deviceType"] in ["inverter", "tlx", "storage", "mix", "min", "sph"]
}

# Perform the first refresh for the total coordinator
Expand Down
146 changes: 146 additions & 0 deletions homeassistant/components/growatt_server/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,32 @@ def _sync_update_data(self) -> dict[str, Any]:
**storage_info_detail["storageDetailBean"],
**storage_energy_overview,
}
elif self.device_type == "sph":
try:
sph_detail = self.api.sph_detail(self.device_id)
sph_energy = self.api.sph_energy(self.device_id)
except growattServer.GrowattV1ApiError as err:
raise UpdateFailed(f"Error fetching SPH device data: {err}") from err

combined = {**sph_detail, **sph_energy}

# Parse last update timestamp from sph_energy "time" field
time_str = sph_energy.get("time")
if time_str:
try:
parsed = datetime.datetime.strptime(time_str, "%Y-%m-%d %H:%M:%S")
combined["lastdataupdate"] = parsed.replace(
tzinfo=dt_util.get_default_time_zone()
)
except ValueError, TypeError:
_LOGGER.debug(
"Could not parse SPH time field for %s: %r",
self.device_id,
time_str,
)

self.data = combined
_LOGGER.debug("sph_info for device %s: %r", self.device_id, self.data)
elif self.device_type == "mix":
mix_info = self.api.mix_info(self.device_id)
mix_totals = self.api.mix_totals(self.device_id, self.plant_id)
Expand Down Expand Up @@ -448,3 +474,123 @@ def _format_time(self, time_raw: str) -> str:
return "00:00"
else:
return f"{hour:02d}:{minute:02d}"

async def update_ac_charge_times(
self,
charge_power: int,
charge_stop_soc: int,
mains_enabled: bool,
periods: list[dict],
) -> None:
"""Update AC charge time periods for SPH device.

Args:
charge_power: Charge power limit (0-100 %)
charge_stop_soc: Stop charging at this SOC level (0-100 %)
mains_enabled: Whether AC (mains) charging is enabled
periods: List of up to 3 dicts with keys start_time, end_time, enabled
"""
if self.api_version != "v1":
raise ServiceValidationError(
"Updating AC charge times requires token authentication"
)

try:
await self.hass.async_add_executor_job(
self.api.sph_write_ac_charge_times,
self.device_id,
charge_power,
charge_stop_soc,
mains_enabled,
periods,
)
except growattServer.GrowattV1ApiError as err:
raise HomeAssistantError(
f"API error updating AC charge times: {err}"
) from err

if self.data:
self.data["chargePowerCommand"] = charge_power
self.data["wchargeSOCLowLimit"] = charge_stop_soc
self.data["acChargeEnable"] = 1 if mains_enabled else 0
for i, period in enumerate(periods, 1):
self.data[f"forcedChargeTimeStart{i}"] = period["start_time"].strftime(
"%H:%M"
)
self.data[f"forcedChargeTimeStop{i}"] = period["end_time"].strftime(
"%H:%M"
)
self.data[f"forcedChargeStopSwitch{i}"] = (
1 if period.get("enabled", False) else 0
)
self.async_set_updated_data(self.data)

async def update_ac_discharge_times(
self,
discharge_power: int,
discharge_stop_soc: int,
periods: list[dict],
) -> None:
"""Update AC discharge time periods for SPH device.

Args:
discharge_power: Discharge power limit (0-100 %)
discharge_stop_soc: Stop discharging at this SOC level (0-100 %)
periods: List of up to 3 dicts with keys start_time, end_time, enabled
"""
if self.api_version != "v1":
raise ServiceValidationError(
"Updating AC discharge times requires token authentication"
)

try:
await self.hass.async_add_executor_job(
self.api.sph_write_ac_discharge_times,
self.device_id,
discharge_power,
discharge_stop_soc,
periods,
)
except growattServer.GrowattV1ApiError as err:
raise HomeAssistantError(
f"API error updating AC discharge times: {err}"
) from err

if self.data:
self.data["disChargePowerCommand"] = discharge_power
self.data["wdisChargeSOCLowLimit"] = discharge_stop_soc
for i, period in enumerate(periods, 1):
self.data[f"forcedDischargeTimeStart{i}"] = period[
"start_time"
].strftime("%H:%M")
self.data[f"forcedDischargeTimeStop{i}"] = period["end_time"].strftime(
"%H:%M"
)
self.data[f"forcedDischargeStopSwitch{i}"] = (
1 if period.get("enabled", False) else 0
)
self.async_set_updated_data(self.data)

async def read_ac_charge_times(self) -> dict:
"""Read AC charge time settings from SPH device cache."""
if self.api_version != "v1":
raise ServiceValidationError(
"Reading AC charge times requires token authentication"
)

if not self.data:
await self.async_refresh()

return self.api.sph_read_ac_charge_times(settings_data=self.data)

async def read_ac_discharge_times(self) -> dict:
"""Read AC discharge time settings from SPH device cache."""
if self.api_version != "v1":
raise ServiceValidationError(
"Reading AC discharge times requires token authentication"
)

if not self.data:
await self.async_refresh()

return self.api.sph_read_ac_discharge_times(settings_data=self.data)
12 changes: 12 additions & 0 deletions homeassistant/components/growatt_server/icons.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,22 @@
{
"services": {
"read_ac_charge_times": {
"service": "mdi:battery-clock-outline"
},
"read_ac_discharge_times": {
"service": "mdi:battery-clock-outline"
},
"read_time_segments": {
"service": "mdi:clock-outline"
},
"update_time_segment": {
"service": "mdi:clock-edit"
},
"write_ac_charge_times": {
"service": "mdi:battery-clock"
},
"write_ac_discharge_times": {
"service": "mdi:battery-clock"
}
}
}
3 changes: 3 additions & 0 deletions homeassistant/components/growatt_server/sensor/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from .inverter import INVERTER_SENSOR_TYPES
from .mix import MIX_SENSOR_TYPES
from .sensor_entity_description import GrowattSensorEntityDescription
from .sph import SPH_SENSOR_TYPES
from .storage import STORAGE_SENSOR_TYPES
from .tlx import TLX_SENSOR_TYPES
from .total import TOTAL_SENSOR_TYPES
Expand Down Expand Up @@ -57,6 +58,8 @@ async def async_setup_entry(
sensor_descriptions = list(STORAGE_SENSOR_TYPES)
elif device_coordinator.device_type == "mix":
sensor_descriptions = list(MIX_SENSOR_TYPES)
elif device_coordinator.device_type == "sph":
sensor_descriptions = list(SPH_SENSOR_TYPES)
else:
_LOGGER.debug(
"Device type %s was found but is not supported right now",
Expand Down
Loading
Loading