Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make storage commands available without a battery #314

3 changes: 2 additions & 1 deletion custom_components/solaredge_modbus_multi/diagnostics.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ async def async_get_config_entry_diagnostics(
"model": format_values(inverter.decoded_model),
"is_mmppt": inverter.is_mmppt,
"mmppt": format_values(inverter.decoded_mmppt),
"storage": format_values(inverter.decoded_storage),
"has_battery": inverter.has_battery,
"storage_control": format_values(inverter.decoded_storage_control),
}
}

Expand Down
62 changes: 33 additions & 29 deletions custom_components/solaredge_modbus_multi/hub.py
Original file line number Diff line number Diff line change
Expand Up @@ -513,8 +513,9 @@ def __init__(self, device_id: int, hub: SolarEdgeModbusMultiHub) -> None:
self.decoded_common = []
self.decoded_model = []
self.decoded_mmppt = []
self.decoded_storage = []
self.decoded_storage_control = []
self.has_parent = False
self.has_battery = None
self.global_power_control = None
self.advanced_power_control = None
self.site_limit_control = None
Expand Down Expand Up @@ -1021,45 +1022,48 @@ def read_modbus_data(self) -> None:
_LOGGER.debug(f"Inverter {self.inverter_unit_id}: {name} {display_value}")

""" Power Control Options: Storage Control """
if self.hub.option_storage_control is True and self.decoded_storage is not None:
for battery in self.hub.batteries:
if self.inverter_unit_id != battery.inverter_unit_id:
continue
if (
self.hub.option_storage_control is True
and self.decoded_storage_control is not False
):
if self.has_battery is None:
self.has_battery = False
for battery in self.hub.batteries:
if self.inverter_unit_id == battery.inverter_unit_id:
self.has_battery = True

inverter_data = self.hub.read_holding_registers(
unit=self.inverter_unit_id, address=57348, count=14
)
if inverter_data.isError():
_LOGGER.debug(f"Inverter {self.inverter_unit_id}: {inverter_data}")
inverter_data = self.hub.read_holding_registers(
unit=self.inverter_unit_id, address=57348, count=14
)
if inverter_data.isError():
_LOGGER.debug(f"Inverter {self.inverter_unit_id}: {inverter_data}")

if type(inverter_data) is ModbusIOException:
raise ModbusReadError(
f"No response from inverter ID {self.inverter_unit_id}"
)
if type(inverter_data) is ModbusIOException:
raise ModbusReadError(
f"No response from inverter ID {self.inverter_unit_id}"
)

if type(inverter_data) is ExceptionResponse:
if (
inverter_data.exception_code
== ModbusExceptions.IllegalAddress
):
self.decoded_storage = False
_LOGGER.debug(
(
f"Inverter {self.inverter_unit_id}: "
"storage control NOT available"
)
if type(inverter_data) is ExceptionResponse:
if inverter_data.exception_code == ModbusExceptions.IllegalAddress:
self.decoded_storage_control = False
_LOGGER.debug(
(
f"Inverter {self.inverter_unit_id}: "
"storage control NOT available"
)
)

if self.decoded_storage is not None:
raise ModbusReadError(inverter_data)
if self.decoded_storage_control is not False:
raise ModbusReadError(inverter_data)

else:
decoder = BinaryPayloadDecoder.fromRegisters(
inverter_data.registers,
byteorder=Endian.Big,
wordorder=Endian.Little,
)

self.decoded_storage = OrderedDict(
self.decoded_storage_control = OrderedDict(
[
("control_mode", decoder.decode_16bit_uint()),
("ac_charge_policy", decoder.decode_16bit_uint()),
Expand All @@ -1073,7 +1077,7 @@ def read_modbus_data(self) -> None:
]
)

for name, value in iter(self.decoded_storage.items()):
for name, value in iter(self.decoded_storage_control.items()):
if isinstance(value, float):
display_value = float_to_hex(value)
else:
Expand Down
111 changes: 59 additions & 52 deletions custom_components/solaredge_modbus_multi/number.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,25 +28,14 @@ async def async_setup_entry(

""" Power Control Options: Storage Control """
if hub.option_storage_control is True:
for battery in hub.batteries:
for inverter in hub.inverters:
if inverter.inverter_unit_id != battery.inverter_unit_id:
continue
entities.append(
StorageACChargeLimit(inverter, config_entry, coordinator)
)
entities.append(
StorageBackupReserve(inverter, config_entry, coordinator)
)
entities.append(
StorageCommandTimeout(inverter, config_entry, coordinator)
)
entities.append(
StorageChargeLimit(inverter, battery, config_entry, coordinator)
)
entities.append(
StorageDischargeLimit(inverter, battery, config_entry, coordinator)
)
for inverter in hub.inverters:
if inverter.decoded_storage_control is False:
continue
entities.append(StorageACChargeLimit(inverter, config_entry, coordinator))
entities.append(StorageBackupReserve(inverter, config_entry, coordinator))
entities.append(StorageCommandTimeout(inverter, config_entry, coordinator))
entities.append(StorageChargeLimit(inverter, config_entry, coordinator))
entities.append(StorageDischargeLimit(inverter, config_entry, coordinator))

""" Power Control Options: Site Limit Control """
if hub.option_export_control is True:
Expand Down Expand Up @@ -114,19 +103,23 @@ def unique_id(self) -> str:
def name(self) -> str:
return "AC Charge Limit"

@property
def entity_registry_enabled_default(self) -> bool:
return self._platform.has_battery is True

@property
def available(self) -> bool:
# Available for AC charge policies 2 & 3
return self._platform.online and self._platform.decoded_storage[
return self._platform.online and self._platform.decoded_storage_control[
"ac_charge_policy"
] in [2, 3]

@property
def native_unit_of_measurement(self) -> str | None:
# kWh in AC policy "Fixed Energy Limit", % in AC policy "Percent of Production"
if self._platform.decoded_storage["ac_charge_policy"] == 2:
if self._platform.decoded_storage_control["ac_charge_policy"] == 2:
return UnitOfEnergy.KILO_WATT_HOUR
elif self._platform.decoded_storage["ac_charge_policy"] == 3:
elif self._platform.decoded_storage_control["ac_charge_policy"] == 3:
return PERCENTAGE
else:
return None
Expand All @@ -138,24 +131,24 @@ def native_min_value(self) -> float:
@property
def native_max_value(self) -> float:
# 100MWh in AC policy "Fixed Energy Limit"
if self._platform.decoded_storage["ac_charge_policy"] == 2:
if self._platform.decoded_storage_control["ac_charge_policy"] == 2:
return 100000000
elif self._platform.decoded_storage["ac_charge_policy"] == 3:
elif self._platform.decoded_storage_control["ac_charge_policy"] == 3:
return 100
else:
return 0

@property
def native_value(self) -> float | None:
if (
self._platform.decoded_storage is False
or float_to_hex(self._platform.decoded_storage["ac_charge_limit"])
self._platform.decoded_storage_control is False
or float_to_hex(self._platform.decoded_storage_control["ac_charge_limit"])
== hex(SunSpecNotImpl.FLOAT32)
or self._platform.decoded_storage["ac_charge_limit"] < 0
or self._platform.decoded_storage_control["ac_charge_limit"] < 0
):
return None

return round(self._platform.decoded_storage["ac_charge_limit"], 3)
return round(self._platform.decoded_storage_control["ac_charge_limit"], 3)

async def async_set_native_value(self, value: float) -> None:
_LOGGER.debug(f"set {self.unique_id} to {value}")
Expand Down Expand Up @@ -184,18 +177,22 @@ def unique_id(self) -> str:
def name(self) -> str:
return "Backup Reserve"

@property
def entity_registry_enabled_default(self) -> bool:
return self._platform.has_battery is True

@property
def native_value(self) -> float | None:
if (
self._platform.decoded_storage is False
or float_to_hex(self._platform.decoded_storage["backup_reserve"])
self._platform.decoded_storage_control is False
or float_to_hex(self._platform.decoded_storage_control["backup_reserve"])
== hex(SunSpecNotImpl.FLOAT32)
or self._platform.decoded_storage["backup_reserve"] < 0
or self._platform.decoded_storage["backup_reserve"] > 100
or self._platform.decoded_storage_control["backup_reserve"] < 0
or self._platform.decoded_storage_control["backup_reserve"] > 100
):
return None

return round(self._platform.decoded_storage["backup_reserve"], 3)
return round(self._platform.decoded_storage_control["backup_reserve"], 3)

async def async_set_native_value(self, value: float) -> None:
_LOGGER.debug(f"set {self.unique_id} to {value}")
Expand Down Expand Up @@ -224,25 +221,29 @@ def unique_id(self) -> str:
def name(self) -> str:
return "Storage Command Timeout"

@property
def entity_registry_enabled_default(self) -> bool:
return self._platform.has_battery is True

@property
def available(self) -> bool:
# Available only in remote control mode
return (
self._platform.online
and self._platform.decoded_storage["control_mode"] == 4
and self._platform.decoded_storage_control["control_mode"] == 4
)

@property
def native_value(self) -> int | None:
if (
self._platform.decoded_storage is False
or self._platform.decoded_storage["command_timeout"]
self._platform.decoded_storage_control is False
or self._platform.decoded_storage_control["command_timeout"]
== SunSpecNotImpl.UINT32
or self._platform.decoded_storage["command_timeout"] > 86400
or self._platform.decoded_storage_control["command_timeout"] > 86400
):
return None

return int(self._platform.decoded_storage["command_timeout"])
return int(self._platform.decoded_storage_control["command_timeout"])

async def async_set_native_value(self, value: int) -> None:
_LOGGER.debug(f"set {self.unique_id} to {value}")
Expand All @@ -260,9 +261,8 @@ class StorageChargeLimit(SolarEdgeNumberBase):
native_unit_of_measurement = UnitOfPower.WATT
icon = "mdi:lightning-bolt"

def __init__(self, inverter, battery, config_entry, coordinator):
def __init__(self, inverter, config_entry, coordinator):
super().__init__(inverter, config_entry, coordinator)
self._battery = battery

@property
def unique_id(self) -> str:
Expand All @@ -272,12 +272,16 @@ def unique_id(self) -> str:
def name(self) -> str:
return "Storage Charge Limit"

@property
def entity_registry_enabled_default(self) -> bool:
return self._platform.has_battery is True

@property
def available(self) -> bool:
# Available only in remote control mode
return (
self._platform.online
and self._platform.decoded_storage["control_mode"] == 4
and self._platform.decoded_storage_control["control_mode"] == 4
)

@property
Expand All @@ -287,14 +291,14 @@ def native_max_value(self) -> float:
@property
def native_value(self) -> float | None:
if (
self._platform.decoded_storage is False
or float_to_hex(self._platform.decoded_storage["charge_limit"])
self._platform.decoded_storage_control is False
or float_to_hex(self._platform.decoded_storage_control["charge_limit"])
== hex(SunSpecNotImpl.FLOAT32)
or self._platform.decoded_storage["charge_limit"] < 0
or self._platform.decoded_storage_control["charge_limit"] < 0
):
return None

return int(self._platform.decoded_storage["charge_limit"])
return int(self._platform.decoded_storage_control["charge_limit"])

async def async_set_native_value(self, value: float) -> None:
_LOGGER.debug(f"set {self.unique_id} to {value}")
Expand All @@ -312,9 +316,8 @@ class StorageDischargeLimit(SolarEdgeNumberBase):
native_unit_of_measurement = UnitOfPower.WATT
icon = "mdi:lightning-bolt"

def __init__(self, inverter, battery, config_entry, coordinator):
def __init__(self, inverter, config_entry, coordinator):
super().__init__(inverter, config_entry, coordinator)
self._battery = battery

@property
def unique_id(self) -> str:
Expand All @@ -324,12 +327,16 @@ def unique_id(self) -> str:
def name(self) -> str:
return "Storage Discharge Limit"

@property
def entity_registry_enabled_default(self) -> bool:
return self._platform.has_battery is True

@property
def available(self) -> bool:
# Available only in remote control mode
return (
self._platform.online
and self._platform.decoded_storage["control_mode"] == 4
and self._platform.decoded_storage_control["control_mode"] == 4
)

@property
Expand All @@ -339,14 +346,14 @@ def native_max_value(self) -> float:
@property
def native_value(self) -> float | None:
if (
self._platform.decoded_storage is False
or float_to_hex(self._platform.decoded_storage["discharge_limit"])
self._platform.decoded_storage_control is False
or float_to_hex(self._platform.decoded_storage_control["discharge_limit"])
== hex(SunSpecNotImpl.FLOAT32)
or self._platform.decoded_storage["discharge_limit"] < 0
or self._platform.decoded_storage_control["discharge_limit"] < 0
):
return None

return int(self._platform.decoded_storage["discharge_limit"])
return int(self._platform.decoded_storage_control["discharge_limit"])

async def async_set_native_value(self, value: float) -> None:
_LOGGER.debug(f"set {self.unique_id} to {value}")
Expand Down