Skip to content

Commit

Permalink
Add initial support for EMMA
Browse files Browse the repository at this point in the history
  • Loading branch information
wlcrs committed Aug 3, 2024
1 parent 10e3201 commit b51a81f
Show file tree
Hide file tree
Showing 19 changed files with 1,068 additions and 488 deletions.
191 changes: 107 additions & 84 deletions __init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,18 @@
import logging
from typing import TypedDict, TypeVar

from huawei_solar import (
HuaweiEMMABridge,
HuaweiSolarBridge,
HuaweiSolarException,
HuaweiSUN2000Bridge,
InvalidCredentials,
create_rtu_bridge,
create_sub_bridge,
create_tcp_bridge,
register_values as rv,
)

from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONF_HOST,
Expand All @@ -17,12 +29,6 @@
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers.entity import DeviceInfo, Entity
from huawei_solar import (
HuaweiSolarBridge,
HuaweiSolarException,
InvalidCredentials,
register_values as rv,
)

from .const import (
CONF_ENABLE_PARAMETER_CONFIGURATION,
Expand Down Expand Up @@ -82,11 +88,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
# └─────────┘ └─────────┘ └─────────┘

if entry.data[CONF_HOST] is None:
primary_bridge = await HuaweiSolarBridge.create_rtu(
primary_bridge = await create_rtu_bridge(
port=entry.data[CONF_PORT], slave_id=entry.data[CONF_SLAVE_IDS][0]
)
else:
primary_bridge = await HuaweiSolarBridge.create(
primary_bridge = await create_tcp_bridge(
host=entry.data[CONF_HOST],
port=entry.data[CONF_PORT],
slave_id=entry.data[CONF_SLAVE_IDS][0],
Expand All @@ -99,7 +105,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD]
)
except InvalidCredentials as err:
raise ConfigEntryAuthFailed() from err
raise ConfigEntryAuthFailed from err

primary_bridge_device_infos = await compute_device_infos(
primary_bridge,
Expand All @@ -111,9 +117,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
] = [(primary_bridge, primary_bridge_device_infos)]

for extra_slave_id in entry.data[CONF_SLAVE_IDS][1:]:
extra_bridge = await HuaweiSolarBridge.create_extra_slave(
primary_bridge, extra_slave_id
)
extra_bridge = await create_sub_bridge(primary_bridge, extra_slave_id)

extra_bridge_device_infos = await compute_device_infos(
extra_bridge,
Expand All @@ -138,8 +142,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
)

power_meter_update_coordinator = None
if bridge.power_meter_type is not None:
assert device_infos["power_meter"]
if device_infos["power_meter"]:
power_meter_update_coordinator = HuaweiSolarUpdateCoordinator(
hass,
_LOGGER,
Expand All @@ -149,8 +152,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
)

energy_storage_update_coordinator = None
if bridge.battery_type != rv.StorageProductModel.NONE:
assert device_infos["connected_energy_storage"]
if device_infos["connected_energy_storage"]:
energy_storage_update_coordinator = HuaweiSolarUpdateCoordinator(
hass,
_LOGGER,
Expand All @@ -170,7 +172,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
)

optimizer_update_coordinator = None
if bridge.has_optimizers:
if isinstance(bridge, HuaweiSUN2000Bridge) and bridge.has_optimizers:
optimizers_device_infos = {}
try:
optimizer_system_infos = (
Expand Down Expand Up @@ -229,13 +231,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:

raise ConfigEntryNotReady from err

except Exception as err:
except Exception:
# always try to stop the bridge, as it will keep retrying
# in the background otherwise!
if primary_bridge is not None:
await primary_bridge.stop()

raise err
raise

await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
await async_setup_services(hass, entry)
Expand All @@ -262,93 +263,114 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
class HuaweiInverterBridgeDeviceInfos(TypedDict):
"""Device Infos for a specific inverter."""

inverter: DeviceInfo
emma: DeviceInfo | None
inverter: DeviceInfo | None
power_meter: DeviceInfo | None

connected_energy_storage: DeviceInfo | None
battery_1: DeviceInfo | None
battery_2: DeviceInfo | None


def _battery_product_model_to_manufacturer(spm: rv.StorageProductModel):
if spm == rv.StorageProductModel.HUAWEI_LUNA2000:
return "Huawei"
if spm == rv.StorageProductModel.LG_RESU:
return "LG Chem"
return None


def _battery_product_model_to_model(spm: rv.StorageProductModel):
if spm == rv.StorageProductModel.HUAWEI_LUNA2000:
return "LUNA 2000"
if spm == rv.StorageProductModel.LG_RESU:
return "RESU"
return None


async def compute_device_infos(
bridge: HuaweiSolarBridge,
connecting_inverter_device_id: tuple[str, str] | None,
) -> HuaweiInverterBridgeDeviceInfos:
"""Create the correct DeviceInfo-objects, which can be used to correctly assign to entities in this integration."""
inverter_device_info = DeviceInfo(
identifiers={(DOMAIN, bridge.serial_number)},
translation_key="inverter",
manufacturer="Huawei",
model=bridge.model_name,
serial_number=bridge.serial_number,
sw_version=bridge.software_version,
via_device=connecting_inverter_device_id, # type: ignore[typeddict-item]
)

# Add power meter device if a power meter is detected
emma_device_info = None
inverter_device_info = None
power_meter_device_info = None

if bridge.power_meter_type is not None:
power_meter_device_info = DeviceInfo(
identifiers={
(DOMAIN, f"{bridge.serial_number}/power_meter"),
},
translation_key="power_meter",
via_device=(DOMAIN, bridge.serial_number),
)

# Add battery device if a battery is detected
battery_device_info = None
battery_1_device_info = None
battery_2_device_info = None

if bridge.battery_type != rv.StorageProductModel.NONE:
battery_device_info = DeviceInfo(
identifiers={
(DOMAIN, f"{bridge.serial_number}/connected_energy_storage"),
},
translation_key="connected_energy_storage",
manufacturer=inverter_device_info.get("manufacturer"),
via_device=(DOMAIN, bridge.serial_number),
if isinstance(bridge, HuaweiEMMABridge):
emma_device_info = DeviceInfo(
identifiers={(DOMAIN, bridge.serial_number)},
translation_key="emma",
manufacturer="Huawei",
model=bridge.model_name,
serial_number=bridge.serial_number,
sw_version=bridge.software_version,
)
else:
assert isinstance(bridge, HuaweiSUN2000Bridge)
inverter_device_info = DeviceInfo(
identifiers={(DOMAIN, bridge.serial_number)},
translation_key="inverter",
manufacturer="Huawei",
model=bridge.model_name,
serial_number=bridge.serial_number,
sw_version=bridge.software_version,
via_device=connecting_inverter_device_id, # type: ignore[typeddict-item]
)

def _battery_product_model_to_manufacturer(spm: rv.StorageProductModel):
if spm == rv.StorageProductModel.HUAWEI_LUNA2000:
return "Huawei"
if spm == rv.StorageProductModel.LG_RESU:
return "LG Chem"
return None
# Add power meter device if a power meter is detected
if bridge.power_meter_type is not None:
power_meter_device_info = DeviceInfo(
identifiers={
(DOMAIN, f"{bridge.serial_number}/power_meter"),
},
translation_key="power_meter",
via_device=(DOMAIN, bridge.serial_number),
)

def _battery_product_model_to_model(spm: rv.StorageProductModel):
if spm == rv.StorageProductModel.HUAWEI_LUNA2000:
return "LUNA 2000"
if spm == rv.StorageProductModel.LG_RESU:
return "RESU"
return None
# Add battery device if a battery is detected
if bridge.battery_type != rv.StorageProductModel.NONE:
battery_device_info = DeviceInfo(
identifiers={
(DOMAIN, f"{bridge.serial_number}/connected_energy_storage"),
},
translation_key="connected_energy_storage",
manufacturer=inverter_device_info.get("manufacturer"),
via_device=(DOMAIN, bridge.serial_number),
)

battery_1_device_info = None
if bridge.battery_1_type != rv.StorageProductModel.NONE:
battery_1_device_info = DeviceInfo(
identifiers={
(DOMAIN, f"{bridge.serial_number}/battery_1"),
},
translation_key="battery_1",
manufacturer=_battery_product_model_to_manufacturer(bridge.battery_1_type),
model=_battery_product_model_to_model(bridge.battery_1_type),
via_device=(DOMAIN, bridge.serial_number),
)
if bridge.battery_1_type != rv.StorageProductModel.NONE:
battery_1_device_info = DeviceInfo(
identifiers={
(DOMAIN, f"{bridge.serial_number}/battery_1"),
},
translation_key="battery_1",
manufacturer=_battery_product_model_to_manufacturer(
bridge.battery_1_type
),
model=_battery_product_model_to_model(bridge.battery_1_type),
via_device=(DOMAIN, bridge.serial_number),
)

battery_2_device_info = None
if bridge.battery_2_type != rv.StorageProductModel.NONE:
battery_2_device_info = DeviceInfo(
identifiers={
(DOMAIN, f"{bridge.serial_number}/battery_2"),
},
translation_key="battery_2",
manufacturer=_battery_product_model_to_manufacturer(bridge.battery_2_type),
model=_battery_product_model_to_model(bridge.battery_2_type),
via_device=(DOMAIN, bridge.serial_number),
)
if bridge.battery_2_type != rv.StorageProductModel.NONE:
battery_2_device_info = DeviceInfo(
identifiers={
(DOMAIN, f"{bridge.serial_number}/battery_2"),
},
translation_key="battery_2",
manufacturer=_battery_product_model_to_manufacturer(
bridge.battery_2_type
),
model=_battery_product_model_to_model(bridge.battery_2_type),
via_device=(DOMAIN, bridge.serial_number),
)

return HuaweiInverterBridgeDeviceInfos(
emma=emma_device_info,
inverter=inverter_device_info,
power_meter=power_meter_device_info,
connected_energy_storage=battery_device_info,
Expand All @@ -365,6 +387,7 @@ class HuaweiSolarUpdateCoordinators:
device_infos: HuaweiInverterBridgeDeviceInfos

inverter_update_coordinator: HuaweiSolarUpdateCoordinator
"""Also used for EMMA devices."""
power_meter_update_coordinator: HuaweiSolarUpdateCoordinator | None
energy_storage_update_coordinator: HuaweiSolarUpdateCoordinator | None
optimizer_update_coordinator: HuaweiSolarOptimizerUpdateCoordinator | None
Expand Down
39 changes: 32 additions & 7 deletions diagnostics.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@
from importlib.metadata import version
from typing import Any

from huawei_solar import HuaweiEMMABridge, HuaweiSUN2000Bridge

from homeassistant.components.diagnostics import async_redact_data
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_PASSWORD
from homeassistant.core import HomeAssistant
from huawei_solar import HuaweiSolarBridge

from . import HuaweiSolarUpdateCoordinators
from .const import DATA_UPDATE_COORDINATORS, DOMAIN
Expand All @@ -30,9 +31,21 @@ async def async_get_config_entry_diagnostics(
"pymodbus_version": version("pymodbus"),
}
for ucs in coordinators:
diagnostics_data[
f"slave_{ucs.bridge.slave_id}"
] = await _build_bridge_diagnostics_info(ucs.bridge)
if isinstance(ucs.bridge, HuaweiSUN2000Bridge):
diagnostics_data[
f"slave_{ucs.bridge.slave_id}"
] = await _build_sun2000_bridge_diagnostics_info(ucs.bridge)
elif isinstance(ucs.bridge, HuaweiEMMABridge):
diagnostics_data[
f"slave_{ucs.bridge.slave_id}"
] = await _build_emma_bridge_diagnostics_info(ucs.bridge)
else:
diagnostics_data[f"slave_{ucs.bridge.slave_id}"] = {
"_type": "Unknown",
"model_name": ucs.bridge.model_name,
"firmware_version": ucs.bridge.firmware_version,
"software_version": ucs.bridge.software_version,
}

diagnostics_data[f"slave_{ucs.bridge.slave_id}_inverter_data"] = (
ucs.inverter_update_coordinator.data
Expand Down Expand Up @@ -61,8 +74,11 @@ async def async_get_config_entry_diagnostics(
return diagnostics_data


async def _build_bridge_diagnostics_info(bridge: HuaweiSolarBridge) -> dict[str, Any]:
diagnostics_data = {
async def _build_sun2000_bridge_diagnostics_info(
bridge: HuaweiSUN2000Bridge,
) -> dict[str, Any]:
return {
"_type": "SUN2000",
"model_name": bridge.model_name,
"firmware_version": bridge.firmware_version,
"software_version": bridge.software_version,
Expand All @@ -75,4 +91,13 @@ async def _build_bridge_diagnostics_info(bridge: HuaweiSolarBridge) -> dict[str,
"supports_capacity_control": bridge.supports_capacity_control,
}

return diagnostics_data

async def _build_emma_bridge_diagnostics_info(
bridge: HuaweiEMMABridge,
) -> dict[str, Any]:
return {
"_type": "EMMA",
"model_name": bridge.model_name,
"firmware_version": bridge.firmware_version,
"software_version": bridge.software_version,
}
6 changes: 3 additions & 3 deletions manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,15 @@
"documentation": "https://github.com/wlcrs/huawei_solar/wiki",
"issue_tracker": "https://github.com/wlcrs/huawei_solar/issues",
"requirements": [
"huawei-solar==2.3.0"
"huawei-solar==2.4.0a2"
],
"codeowners": [
"@wlcrs"
],
"iot_class": "local_polling",
"version": "1.4.1",
"version": "1.5.0a1",
"loggers": [
"huawei_solar",
"pymodbus"
]
}
}
Loading

0 comments on commit b51a81f

Please sign in to comment.