Skip to content
Open
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
13 changes: 12 additions & 1 deletion custom_components/fronius_modbus/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,15 @@
# ['Reserve Target', 'reserve_target', {'min': 0, 'max': 100, 'unit': '%'}],
]

INVERTER_NUMBER_TYPES = [
['Export limit rate', 'export_limit_rate', {'min': 100, 'max': 10000, 'step': 10, 'mode':'box', 'unit': None}],
]

INVERTER_SELECT_TYPES = [
['Export limit enable', 'export_limit_enable', {0: 'Disabled', 1: 'Enabled'}],
['Inverter connection', 'Conn', {0: 'Disabled', 1: 'Enabled'}],
]

INVERTER_SENSOR_TYPES = {
'acpower': ['AC power', 'acpower', SensorDeviceClass.POWER, SensorStateClass.MEASUREMENT, 'W', 'mdi:lightning-bolt', None],
'acenergy': ['AC energy', 'acenergy', SensorDeviceClass.ENERGY, SensorStateClass.TOTAL_INCREASING, 'Wh', 'mdi:lightning-bolt', None],
Expand Down Expand Up @@ -72,7 +81,9 @@
'OutPFSet_Ena': ['Fixed power factor', 'OutPFSet_Ena', None, None, None, None, EntityCategory.DIAGNOSTIC],
'VArPct_Ena': ['Limit VAr control', 'VArPct_Ena', None, None, None, None, EntityCategory.DIAGNOSTIC],
'PhVphA': ['AC voltage L1-N', 'PhVphA', SensorDeviceClass.VOLTAGE, SensorStateClass.MEASUREMENT, 'V', 'mdi:lightning-bolt', None],
'unit_id': ['Modbus ID', 'i_unit_id', None, None, None, None, EntityCategory.DIAGNOSTIC],
'unit_id': ['Modbus ID', 'i_unit_id', None, None, None, None, EntityCategory.DIAGNOSTIC],
'export_limit_rate': ['Export limit rate', 'export_limit_rate', None, SensorStateClass.MEASUREMENT, None, 'mdi:chart-line', None],
'export_limit_enable': ['Export limit enabled', 'export_limit_enable', None, None, None, 'mdi:power-plug', EntityCategory.DIAGNOSTIC],
}

INVERTER_SYMO_SENSOR_TYPES = {
Expand Down
15 changes: 10 additions & 5 deletions custom_components/fronius_modbus/extmodbusclient.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,17 @@
import logging
import operator
#from datetime import timedelta, datetime
from typing import Optional, Literal
from typing import Literal
import struct
import asyncio

from pymodbus.client import AsyncModbusTcpClient
from pymodbus.utilities import unpack_bitstring
try:
# For newer pymodbus versions (3.9.x+)
from pymodbus.pdu.pdu import unpack_bitstring
except ImportError:
# For older pymodbus versions (3.8.x and below)
from pymodbus.utilities import unpack_bitstring
from pymodbus.exceptions import ModbusIOException, ConnectionException
from pymodbus import ExceptionResponse

Expand Down Expand Up @@ -77,7 +82,7 @@ async def read_holding_registers(self, unit_id, address, count, retries = 3):

for attempt in range(retries+1):
try:
data = await self._client.read_holding_registers(address=address, count=count, slave=unit_id)
data = await self._client.read_holding_registers(address=address, count=count, device_id=unit_id)
except ModbusIOException as e:
_LOGGER.error(f'error reading registers. IO error. connected: {self._client.connected} address: {address} count: {count} unit id: {unit_id}')
return None
Expand Down Expand Up @@ -107,7 +112,7 @@ async def read_holding_registers(self, unit_id, address, count, retries = 3):

async def get_registers(self, unit_id, address, count, retries = 0):
data = await self.read_holding_registers(unit_id=unit_id, address=address, count=count)
if data.isError():
if data is None or data.isError():
if isinstance(data,ModbusIOException):
if retries < 1:
_LOGGER.debug(f"IO Error: {data}. Retrying...")
Expand All @@ -125,7 +130,7 @@ async def write_registers(self, unit_id, address, payload):
#_LOGGER.debug(f"write registers a: {address} p: {payload} unit_id: {unit_id}")

try:
result = await self._client.write_registers(address=address, values=payload, slave=unit_id)
result = await self._client.write_registers(address=address, values=payload, device_id=unit_id)
except ModbusIOException as e:
raise Exception(f'write_registers: IO error {self._client.connected} {e.fcode} {e}')
except ConnectionException as e:
Expand Down
59 changes: 58 additions & 1 deletion custom_components/fronius_modbus/froniusmodbusclient.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@
STORAGE_CONTROL_MODE_ADDRESS,
MINIMUM_RESERVE_ADDRESS,
DISCHARGE_RATE_ADDRESS,
CHARGE_RATE_ADDRESS,
CHARGE_RATE_ADDRESS,
EXPORT_LIMIT_RATE_ADDRESS,
EXPORT_LIMIT_ENABLE_ADDRESS,
CONN_ADDRESS,
STORAGE_CONTROL_MODE,
CHARGE_STATUS,
CHARGE_GRID_STATUS,
Expand All @@ -29,6 +32,7 @@
INVERTER_CONTROLS,
INVERTER_EVENTS,
CONTROL_STATUS,
EXPORT_LIMIT_STATUS,
GRID_STATUS,
# INVERTER_STATUS,
# CONNECTION_STATUS,
Expand Down Expand Up @@ -525,6 +529,26 @@ async def read_meter_data(self, meter_prefix, unit_id):

return True

async def read_export_limit_data(self):
"""Read export limit control registers"""
# Read export limit rate register (40232)
rate_regs = await self.get_registers(unit_id=self._inverter_unit_id, address=EXPORT_LIMIT_RATE_ADDRESS, count=1)
if rate_regs is not None:
export_limit_rate = self._client.convert_from_registers(rate_regs[0:1], data_type=self._client.DATATYPE.UINT16)
self.data['export_limit_rate'] = export_limit_rate
else:
self.data['export_limit_rate'] = None

# Read export limit enable register (40236)
enable_regs = await self.get_registers(unit_id=self._inverter_unit_id, address=EXPORT_LIMIT_ENABLE_ADDRESS, count=1)
if enable_regs is not None:
export_limit_enable_raw = self._client.convert_from_registers(enable_regs[0:1], data_type=self._client.DATATYPE.UINT16)
self.data['export_limit_enable'] = EXPORT_LIMIT_STATUS.get(export_limit_enable_raw, 'Unknown')
else:
self.data['export_limit_enable'] = None

return True

async def set_storage_control_mode(self, mode: int):
if not mode in [0,1,2,3]:
_LOGGER.error(f'Attempted to set to unsupported storage control mode. Value: {mode}')
Expand Down Expand Up @@ -676,3 +700,36 @@ async def set_calibrate_mode(self):
await self.change_settings(mode=2, charge_limit=100, discharge_limit=-100, grid_charge_power=100)
self.storage_extended_control_mode = 8
_LOGGER.info(f"Auto mode")

async def set_export_limit_rate(self, rate):
"""Set export limit rate (100-10000, where 10000=100%, minimum 1%)"""
if rate < 100:
rate = 100
elif rate > 10000:
rate = 10000
await self.write_registers(unit_id=self._inverter_unit_id, address=EXPORT_LIMIT_RATE_ADDRESS, payload=[int(rate)])
self.data['export_limit_rate'] = rate
_LOGGER.info(f"Set export limit rate to {rate}")

async def set_export_limit_enable(self, enable):
"""Enable/disable export limit (0=Disabled, 1=Enabled)"""
enable_value = 1 if enable else 0
await self.write_registers(unit_id=self._inverter_unit_id, address=EXPORT_LIMIT_ENABLE_ADDRESS, payload=[enable_value])
self.data['export_limit_enable'] = enable_value
_LOGGER.info(f"Set export limit enable to {enable_value}")

async def apply_export_limit(self, rate):
"""Apply export limit by first disabling, then setting rate, then enabling"""
await self.set_export_limit_enable(0) # Disable first
await asyncio.sleep(1.0)
await self.set_export_limit_rate(rate) # Set new rate
await asyncio.sleep(1.0)
await self.set_export_limit_enable(1) # Enable with new rate
_LOGGER.info(f"Applied export limit: rate={rate}, enabled=1")

async def set_conn_status(self, enable):
"""Enable/disable inverter connection (0=Disconnected/Standby, 1=Connected/Normal)"""
conn_value = 1 if enable else 0
await self.write_registers(unit_id=self._inverter_unit_id, address=CONN_ADDRESS, payload=[conn_value])
self.data['Conn'] = CONTROL_STATUS[conn_value]
_LOGGER.info(f"Set inverter connection status to {conn_value} ({'Connected' if enable else 'Disconnected/Standby'})")
8 changes: 8 additions & 0 deletions custom_components/fronius_modbus/froniusmodbusclient_const.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
MINIMUM_RESERVE_ADDRESS = 40350
DISCHARGE_RATE_ADDRESS = 40355
CHARGE_RATE_ADDRESS = 40356
EXPORT_LIMIT_RATE_ADDRESS = 40232
EXPORT_LIMIT_ENABLE_ADDRESS = 40236
CONN_ADDRESS = 40231

# Manufacturer
# Type
Expand Down Expand Up @@ -108,6 +111,11 @@
1: 'Enabled',
}

EXPORT_LIMIT_STATUS = {
0: 'Disabled',
1: 'Enabled',
}

STORAGE_EXT_CONTROL_MODE = {
0: 'Auto',
1: 'PV Charge Limit',
Expand Down
44 changes: 37 additions & 7 deletions custom_components/fronius_modbus/hub.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from datetime import timedelta
from typing import Optional
from importlib.metadata import version
from packaging import version as pkg_version

from homeassistant.core import callback
from homeassistant.helpers.event import async_track_time_interval
Expand Down Expand Up @@ -67,13 +68,23 @@ async def init_data(self, close = False, read_status_data = False):
return

def check_pymodbus_version(self):
if version('pymodbus') is None:
_LOGGER.warning(f"pymodbus not found")
elif version('pymodbus') < self.PYMODBUS_VERSION:
raise Exception(f"pymodbus {version('pymodbus')} found, please update to {self.PYMODBUS_VERSION} or higher")
elif version('pymodbus') > self.PYMODBUS_VERSION:
_LOGGER.warning(f"newer pymodbus {version('pymodbus')} found")
_LOGGER.debug(f"pymodbus {version('pymodbus')}")
try:
current_version = version('pymodbus')
if current_version is None:
_LOGGER.warning(f"pymodbus not found")
return

current = pkg_version.parse(current_version)
required = pkg_version.parse(self.PYMODBUS_VERSION)

if current < required:
raise Exception(f"pymodbus {current_version} found, please update to {self.PYMODBUS_VERSION} or higher")
elif current > required:
_LOGGER.warning(f"newer pymodbus {current_version} found")
_LOGGER.debug(f"pymodbus {current_version}")
except Exception as e:
_LOGGER.error(f"Error checking pymodbus version: {e}")
raise

@property
def device_info_storage(self) -> dict:
Expand Down Expand Up @@ -187,6 +198,13 @@ async def async_refresh_modbus_data(self, _now: Optional[int] = None) -> dict:
_LOGGER.exception("Error reading inverter storage data", exc_info=True)
update_result = False

# Read export limit data
try:
update_result = await self._client.read_export_limit_data()
except Exception as e:
_LOGGER.exception("Error reading export limit data", exc_info=True)
update_result = False


if update_result:
for update_callback in self._entities:
Expand Down Expand Up @@ -271,4 +289,16 @@ async def set_grid_charge_power(self, value):
async def set_grid_discharge_power(self, value):
await self._client.set_grid_discharge_power(value)

async def set_export_limit_rate(self, value):
await self._client.set_export_limit_rate(value)

async def set_export_limit_enable(self, value):
await self._client.set_export_limit_enable(value)

async def apply_export_limit(self, rate):
await self._client.apply_export_limit(rate)

async def set_conn_status(self, enable):
await self._client.set_conn_status(enable)


6 changes: 3 additions & 3 deletions custom_components/fronius_modbus/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@
"documentation": "https://github.com/redpomodoro/fronius_modbus/",
"iot_class": "local_polling",
"issue_tracker": "https://github.com/redpomodoro/fronius_modbus/issues",
"requirements": ["pymodbus==3.8.3"],
"version": "0.1.7"
}
"requirements": ["pymodbus>=3.10.0"],
"version": "0.1.11"
}
21 changes: 21 additions & 0 deletions custom_components/fronius_modbus/number.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

from .const import (
STORAGE_NUMBER_TYPES,
INVERTER_NUMBER_TYPES,
ENTITY_PREFIX,
)

Expand Down Expand Up @@ -47,6 +48,22 @@ async def async_setup_entry(hass, config_entry, async_add_entities) -> None:
)
entities.append(number)

# Add inverter number entities (export limit controls)
for number_info in INVERTER_NUMBER_TYPES:
number = FroniusModbusNumber(
ENTITY_PREFIX,
hub,
hub.device_info_inverter,
number_info[0],
number_info[1],
min = number_info[2]['min'],
max = number_info[2]['max'],
unit = number_info[2]['unit'],
mode = number_info[2]['mode'],
native_step = number_info[2]['step'],
)
entities.append(number)

async_add_entities(entities)
return True

Expand Down Expand Up @@ -89,6 +106,8 @@ async def async_set_native_value(self, value: float) -> None:
await self._hub.set_grid_charge_power(value)
elif self._key == 'grid_discharge_power':
await self._hub.set_grid_discharge_power(value)
elif self._key == 'export_limit_rate':
await self._hub.apply_export_limit(value)

#_LOGGER.debug(f"Number {self._key} set to {value}")
self.async_write_ha_state()
Expand All @@ -106,4 +125,6 @@ def available(self) -> bool:
return True
if self._key == 'grid_discharge_power' and self._hub.storage_extended_control_mode in [5]:
return True
if self._key == 'export_limit_rate':
return True
return False
22 changes: 20 additions & 2 deletions custom_components/fronius_modbus/select.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

from .const import (
STORAGE_SELECT_TYPES,
INVERTER_SELECT_TYPES,
ENTITY_PREFIX,
)

Expand Down Expand Up @@ -35,6 +36,18 @@ async def async_setup_entry(hass, config_entry, async_add_entities) -> None:
)
entities.append(select)

# Add inverter select entities (export limit enable)
for select_info in INVERTER_SELECT_TYPES:
select = FroniusModbusSelect(
platform_name=ENTITY_PREFIX,
hub=hub,
device_info=hub.device_info_inverter,
name = select_info[0],
key = select_info[1],
options = select_info[2],
)
entities.append(select)

async_add_entities(entities)
return True

Expand All @@ -56,8 +69,13 @@ async def async_select_option(self, option: str) -> None:
"""Change the selected option."""
new_mode = get_key(self._options_dict, option)

await self._hub.set_mode(new_mode)
if self._key == 'ext_control_mode':
await self._hub.set_mode(new_mode)
#self._hub.storage_extended_control_mode = new_mode
elif self._key == 'export_limit_enable':
await self._hub.set_export_limit_enable(new_mode)
elif self._key == 'Conn':
await self._hub.set_conn_status(new_mode)

self._hub.data[self._key] = option
#self._hub.storage_extended_control_mode = new_mode
self.async_write_ha_state()