Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions custom_components/solaredge_modbus_multi/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,10 @@ def __init__(

async def _async_update_data(self):
try:
while self._hub.has_write:
_LOGGER.debug(f"Waiting for write {self._hub.has_write}")
await asyncio.sleep(1)

return await self._refresh_modbus_data_with_retry(
ex_type=DataUpdateFailed,
limit=RetrySettings.Limit,
Expand Down
293 changes: 158 additions & 135 deletions custom_components/solaredge_modbus_multi/hub.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,10 +137,7 @@ def __init__(
self.batteries = []
self.inverter_common = {}
self.mmppt_common = {}

self._wr_unit = None
self._wr_address = None
self._wr_payload = None
self.has_write = None

self._initalized = False
self._online = True
Expand All @@ -165,6 +162,8 @@ def __init__(
)

async def _async_init_solaredge(self) -> None:
"""Detect devices and load initial modbus data from inverters."""

if not self.is_connected:
ir.async_create_issue(
self._hass,
Expand Down Expand Up @@ -208,7 +207,7 @@ async def _async_init_solaredge(self) -> None:
raise HubInitFailed(f"{e}")

except DeviceInvalid as e:
"""Inverters are required"""
# Inverters are mandatory
_LOGGER.error(f"Inverter at {self.hub_host} ID {inverter_unit_id}: {e}")
raise HubInitFailed(f"{e}")

Expand Down Expand Up @@ -374,17 +373,22 @@ async def _async_init_solaredge(self) -> None:
self.initalized = True

async def async_refresh_modbus_data(self) -> bool:
"""Refresh modbus data from inverters."""

if not self.is_connected:
await self.connect()

if not self.initalized:
try:
await self._async_init_solaredge()
async with self._lock:
await self._async_init_solaredge()

except ConnectionException as e:
self.disconnect()
raise HubInitFailed(f"Setup failed: {e}")

return True

if not self.is_connected:
self.online = False
ir.async_create_issue(
Expand All @@ -406,12 +410,13 @@ async def async_refresh_modbus_data(self) -> bool:
self.online = True

try:
for inverter in self.inverters:
await inverter.read_modbus_data()
for meter in self.meters:
await meter.read_modbus_data()
for battery in self.batteries:
await battery.read_modbus_data()
async with self._lock:
for inverter in self.inverters:
await inverter.read_modbus_data()
for meter in self.meters:
await meter.read_modbus_data()
for battery in self.batteries:
await battery.read_modbus_data()

except ModbusReadError as e:
self.disconnect()
Expand All @@ -435,6 +440,140 @@ async def async_refresh_modbus_data(self) -> bool:

return True

async def connect(self) -> None:
"""Connect to inverter."""

if self._client is None:
self._client = AsyncModbusTcpClient(
host=self._host,
port=self._port,
reconnect_delay=ModbusDefaults.ReconnectDelay,
timeout=ModbusDefaults.Timeout,
)

await self._client.connect()

def disconnect(self) -> None:
"""Disconnect from inverter."""

if self._client is not None:
self._client.close()

async def shutdown(self) -> None:
"""Shut down the hub and disconnect."""
async with self._lock:
self.online = False
self.disconnect()
self._client = None

async def modbus_read_holding_registers(self, unit, address, rcount):
"""Read modbus registers from inverter."""

self._rr_unit = unit
self._rr_address = address
self._rr_count = rcount

kwargs = {"slave": self._rr_unit} if self._rr_unit else {}

result = await self._client.read_holding_registers(
self._rr_address, self._rr_count, **kwargs
)

if result.isError():
_LOGGER.debug(f"Unit {unit}: {result}")

if type(result) is ModbusIOException:
raise ModbusIOError(result)

if type(result) is ExceptionResponse:
if result.exception_code == ModbusExceptions.IllegalAddress:
raise ModbusIllegalAddress(result)

if result.exception_code == ModbusExceptions.IllegalFunction:
raise ModbusIllegalFunction(result)

if result.exception_code == ModbusExceptions.IllegalValue:
raise ModbusIllegalValue(result)

raise ModbusReadError(result)

return result

async def write_registers(self, unit: int, address: int, payload) -> None:
"""Write modbus registers to inverter."""

self._wr_unit = unit
self._wr_address = address
self._wr_payload = payload

try:
async with self._lock:
if not self.is_connected:
await self.connect()

kwargs = {"slave": self._wr_unit} if self._wr_unit else {}
result = await self._client.write_registers(
self._wr_address, self._wr_payload, **kwargs
)

self.has_write = address

if self.sleep_after_write > 0:
_LOGGER.debug(
f"Sleep {self.sleep_after_write} seconds after write {address}."
)
await asyncio.sleep(self.sleep_after_write)

self.has_write = None
_LOGGER.debug(f"Finished with write {address}.")

except asyncio.TimeoutError:
self.disconnect()

raise HomeAssistantError(
f"Timeout while sending command to inverter ID {self._wr_unit}."
)

except ConnectionException as e:
self.disconnect()

_LOGGER.error(f"Connection failed: {e}")
raise HomeAssistantError(
f"Connection to inverter ID {self._wr_unit} failed."
)

if result.isError():
if type(result) is ModbusIOException:
self.disconnect()
_LOGGER.error(
f"Write failed: No response from inverter ID {self._wr_unit}."
)
raise HomeAssistantError(
"No response from inverter ID {self._wr_unit}."
)

if type(result) is ExceptionResponse:
if result.exception_code == ModbusExceptions.IllegalAddress:
_LOGGER.debug(f"Write IllegalAddress: {result}")
raise HomeAssistantError(
"Address not supported at device at ID {self._wr_unit}."
)

if result.exception_code == ModbusExceptions.IllegalFunction:
_LOGGER.debug(f"Write IllegalFunction: {result}")
raise HomeAssistantError(
"Function not supported by device at ID {self._wr_unit}."
)

if result.exception_code == ModbusExceptions.IllegalValue:
_LOGGER.debug(f"Write IllegalValue: {result}")
raise HomeAssistantError(
"Value invalid for device at ID {self._wr_unit}."
)

self.disconnect()
raise ModbusWriteError(result)

@property
def online(self):
return self._online
Expand Down Expand Up @@ -464,14 +603,17 @@ def name(self):

@property
def hub_id(self) -> str:
"""Return the ID of this hub."""
return self._id

@property
def hub_host(self) -> str:
"""Return the modbus client host."""
return self._host

@property
def hub_port(self) -> int:
"""Return the modbus client port."""
return self._port

@property
Expand Down Expand Up @@ -530,6 +672,10 @@ def keep_modbus_open(self, value: bool) -> None:

_LOGGER.debug(f"keep_modbus_open={self._keep_modbus_open}")

@property
def sleep_after_write(self) -> int:
return self._sleep_after_write

@property
def coordinator_timeout(self) -> int:
if not self.initalized:
Expand All @@ -556,129 +702,6 @@ def is_connected(self) -> bool:

return self._client.connected

def disconnect(self) -> None:
if self._client is not None:
self._client.close()

async def connect(self) -> None:
"""Connect modbus client."""
async with self._lock:
if self._client is None:
self._client = AsyncModbusTcpClient(
host=self._host,
port=self._port,
reconnect_delay=ModbusDefaults.ReconnectDelay,
timeout=ModbusDefaults.Timeout,
)

await self._client.connect()

async def shutdown(self) -> None:
"""Shut down the hub."""
async with self._lock:
self.online = False
self.disconnect()
self._client = None

async def modbus_read_holding_registers(self, unit, address, rcount):
self._rr_unit = unit
self._rr_address = address
self._rr_count = rcount

async with self._lock:
kwargs = {"slave": self._rr_unit} if self._rr_unit else {}

result = await self._client.read_holding_registers(
self._rr_address, self._rr_count, **kwargs
)

if result.isError():
_LOGGER.debug(f"Unit {unit}: {result}")

if type(result) is ModbusIOException:
raise ModbusIOError(result)

if type(result) is ExceptionResponse:
if result.exception_code == ModbusExceptions.IllegalAddress:
raise ModbusIllegalAddress(result)

if result.exception_code == ModbusExceptions.IllegalFunction:
raise ModbusIllegalFunction(result)

if result.exception_code == ModbusExceptions.IllegalValue:
raise ModbusIllegalValue(result)

raise ModbusReadError(result)

return result

async def write_registers(self, unit: int, address: int, payload) -> None:
self._wr_unit = unit
self._wr_address = address
self._wr_payload = payload

try:
if not self.is_connected:
await self.connect()

async with self._lock:
kwargs = {"slave": self._wr_unit} if self._wr_unit else {}
result = await self._client.write_registers(
self._wr_address, self._wr_payload, **kwargs
)

if self._sleep_after_write > 0:
_LOGGER.debug(
f"Sleeping {self._sleep_after_write} seconds after write."
)
await asyncio.sleep(self._sleep_after_write)

except asyncio.TimeoutError:
raise HomeAssistantError(
f"Timeout while tyring to send command to inverter ID {self._wr_unit}."
)

except ConnectionException as e:
_LOGGER.error(f"Connection failed: {e}")
raise HomeAssistantError(
f"Connection to inverter ID {self._wr_unit} failed."
)

if result.isError():
if not self.keep_modbus_open:
self.disconnect()

if type(result) is ModbusIOException:
_LOGGER.error(
f"Write failed: No response from inverter ID {self._wr_unit}."
)

raise HomeAssistantError(
"No response from inverter ID {self._wr_unit}."
)

if type(result) is ExceptionResponse:
if result.exception_code == ModbusExceptions.IllegalAddress:
_LOGGER.debug(f"Write IllegalAddress: {result}")

raise HomeAssistantError(
"Address not supported at device at ID {self._wr_unit}."
)

if result.exception_code == ModbusExceptions.IllegalFunction:
_LOGGER.debug(f"Write IllegalFunction: {result}")
raise HomeAssistantError(
"Function not supported by device at ID {self._wr_unit}."
)

if result.exception_code == ModbusExceptions.IllegalValue:
_LOGGER.debug(f"Write IllegalValue: {result}")
raise HomeAssistantError(
"Value invalid for device at ID {self._wr_unit}."
)

raise ModbusWriteError(result)


class SolarEdgeInverter:
def __init__(self, device_id: int, hub: SolarEdgeModbusMultiHub) -> None:
Expand Down