Skip to content

Commit

Permalink
Add support for reading/writing the Storedge control registers
Browse files Browse the repository at this point in the history
Extend the integration to create number & select entities for the
Storedge control registers.
Through these registers one can control the battery charge / discharge
profile.
  • Loading branch information
mpredfearn committed Sep 14, 2021
1 parent 8c6e089 commit 7e56eaf
Show file tree
Hide file tree
Showing 4 changed files with 356 additions and 2 deletions.
81 changes: 79 additions & 2 deletions custom_components/solaredge_modbus/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,10 @@
DEFAULT_READ_METER3,
DEFAULT_READ_BATTERY1,
DEFAULT_READ_BATTERY2,
BATTERY_STATUSSES
BATTERY_STATUSSES,
STOREDGE_CONTROL_MODE,
STOREDGE_AC_CHARGE_POLICY,
STOREDGE_CHARGE_DISCHARGE_MODE
)

_LOGGER = logging.getLogger(__name__)
Expand All @@ -55,7 +58,7 @@
{DOMAIN: vol.Schema({cv.slug: SOLAREDGE_MODBUS_SCHEMA})}, extra=vol.ALLOW_EXTRA
)

PLATFORMS = ["sensor"]
PLATFORMS = ["number", "select", "sensor"]


async def async_setup(hass, config):
Expand Down Expand Up @@ -194,6 +197,12 @@ def read_holding_registers(self, unit, address, count):
kwargs = {"unit": unit} if unit else {}
return self._client.read_holding_registers(address, count, **kwargs)

def write_registers(self, unit, address, payload):
"""Write registers."""
with self._lock:
kwargs = {"unit": unit} if unit else {}
return self._client.write_registers(address, payload, **kwargs)

def calculate_value(self, value, sf):
return value * 10 ** sf

Expand All @@ -203,6 +212,7 @@ def read_modbus_data_stub(self):
and self.read_modbus_data_meter1_stub()
and self.read_modbus_data_meter2_stub()
and self.read_modbus_data_meter3_stub()
and self.read_modbus_data_storage_stub()
and self.read_modbus_data_battery1_stub()
and self.read_modbus_data_battery2_stub()
)
Expand All @@ -213,6 +223,7 @@ def read_modbus_data(self):
and self.read_modbus_data_meter1()
and self.read_modbus_data_meter2()
and self.read_modbus_data_meter3()
and self.read_modbus_data_storage()
and self.read_modbus_data_battery1()
and self.read_modbus_data_battery2()
)
Expand Down Expand Up @@ -331,6 +342,17 @@ def read_modbus_data_meter_stub(self, meter_prefix):

return True

def read_modbus_data_storage_stub(self):
self.data["storage_contol_mode"] = STATE_UNKNOWN
self.data["storage_ac_charge_policy"] = STATE_UNKNOWN
self.data["storage_ac_charge_limit"] = STATE_UNKNOWN
self.data["storage_backup_reserved"] = STATE_UNKNOWN
self.data["storage_default_mode"] = STATE_UNKNOWN
self.data["storage_remote_command_timeout"] = STATE_UNKNOWN
self.data["storage_remote_command_mode"] = STATE_UNKNOWN
self.data["storage_remote_charge_limit"] = STATE_UNKNOWN
self.data["storage_remote_discharge_limit"] = STATE_UNKNOWN

def read_modbus_data_battery1_stub(self):
return read_modbus_data_battery_stub("battery1_")

Expand Down Expand Up @@ -787,6 +809,61 @@ def read_modbus_data_inverter(self):
else:
return False

def read_modbus_data_storage(self):
if not self.read_battery1 and not self.read_battery2:
return True

storage_data = self.read_holding_registers(unit=1, address=57348, count=14)
if not storage_data.isError():
decoder = BinaryPayloadDecoder.fromRegisters(
storage_data.registers, byteorder=Endian.Big,wordorder=Endian.Little
)

#0xE004 - 1 - storage control mode
storage_control_mode = decoder.decode_16bit_uint()
if storage_control_mode in STOREDGE_CONTROL_MODE:
self.data["storage_contol_mode"] = STOREDGE_CONTROL_MODE[storage_control_mode]
else:
self.data["storage_contol_mode"] = storage_control_mode

#0xE005 - 1 - storage ac charge policy
storage_ac_charge_policy = decoder.decode_16bit_uint()
if storage_ac_charge_policy in STOREDGE_AC_CHARGE_POLICY:
self.data["storage_ac_charge_policy"] = STOREDGE_AC_CHARGE_POLICY[storage_ac_charge_policy]
else:
self.data["storage_ac_charge_policy"] = storage_ac_charge_policy

#0xE006 - 2 - storage AC charge limit (kWh or %)
self.data["storage_ac_charge_limit"] = round(decoder.decode_32bit_float(), 3)

#0xE008 - 2 - storage backup reserved capacity (%)
self.data["storage_backup_reserved"] = round(decoder.decode_32bit_float(), 3)

#0xE00A - 1 - storage charge / discharge default mode
storage_default_mode = decoder.decode_16bit_uint()
if storage_default_mode in STOREDGE_CHARGE_DISCHARGE_MODE:
self.data["storage_default_mode"] = STOREDGE_CHARGE_DISCHARGE_MODE[storage_default_mode]
else:
self.data["storage_default_mode"] = storage_default_mode

#0xE00B - 2- storage remote command timeout (seconds)
self.data["storage_remote_command_timeout"] = decoder.decode_32bit_uint()

#0xE00D - 1 - storage remote command mode
storage_remote_command_mode = decoder.decode_16bit_uint()
if storage_remote_command_mode in STOREDGE_CHARGE_DISCHARGE_MODE:
self.data["storage_remote_command_mode"] = STOREDGE_CHARGE_DISCHARGE_MODE[storage_remote_command_mode]
else:
self.data["storage_remote_command_mode"] = storage_remote_command_mode

#0xE00E - 2- storate remote charge limit
self.data["storage_remote_charge_limit"] = round(decoder.decode_32bit_float(), 3)

#0xE010 - 2- storate remote discharge limit
self.data["storage_remote_discharge_limit"] = round(decoder.decode_32bit_float(), 3)

return True

def read_modbus_data_battery1(self):
if not self.read_battery1:
return True
Expand Down
41 changes: 41 additions & 0 deletions custom_components/solaredge_modbus/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -282,3 +282,44 @@
6: "Idle",
10: "Sleep"
}

STOREDGE_CONTROL_MODE = {
0: "Disabled",
1: "Maximize Self Consumption",
2: "Time of Use",
3: "Backup Only",
4: "Remote Control"
}

STOREDGE_AC_CHARGE_POLICY = {
0: "Disabled",
1: "Always Allowed",
2: "Fixed Energy Limit",
3: "Percent of Production",
}

STOREDGE_CHARGE_DISCHARGE_MODE = {
0: "Off",
1: "Charge from excess PV power only",
2: "Charge from PV first",
3: "Charge from PV and AC",
4: "Maximize export",
5: "Discharge to match load",
7: "Maximize self consumption",
}

STORAGE_SELECT_TYPES = [
["Storage Control Mode", "storage_contol_mode", 0xE004, STOREDGE_CONTROL_MODE],
["Storage AC Charge Policy", "storage_ac_charge_policy", 0xE005, STOREDGE_AC_CHARGE_POLICY],
["Storage Default Mode", "storage_default_mode", 0xE00A, STOREDGE_CHARGE_DISCHARGE_MODE],
["Storage Remote Command Mode", "storage_remote_command_mode", 0xE00D, STOREDGE_CHARGE_DISCHARGE_MODE],
]

# TODO Determine the maximum values properly
STORAGE_NUMBER_TYPES = [
["Storage AC Charge Limit", "storage_ac_charge_limit", 0xE006, "f", {"min": 0, "max": 100000000000}],
["Storage Backup reserved", "storage_backup_reserved", 0xE008, "f", {"min": 0, "max": 100, "unit": "%"}],
["Storage Remote Command Timeout", "storage_remote_command_timeout", 0xE00B, "i", {"min": 0, "max": 86400, "unit": "s"}],
["Storage Remote Charge Limit", "storage_remote_charge_limit", 0xE00E, "f", {"min": 0, "max": 20000, "unit": "W"}],
["Storage Remote Discharge Limit", "storage_remote_discharge_limit", 0xE010, "f", {"min": 0, "max": 20000, "unit": "W"}],
]
122 changes: 122 additions & 0 deletions custom_components/solaredge_modbus/number.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import logging
from typing import Optional, Dict, Any

from .const import (
DOMAIN,
ATTR_MANUFACTURER,
STORAGE_NUMBER_TYPES,
)

from pymodbus.constants import Endian
from pymodbus.payload import BinaryPayloadBuilder

from homeassistant.const import CONF_NAME
from homeassistant.components.number import (
PLATFORM_SCHEMA,
NumberEntity,
)

from homeassistant.core import callback

_LOGGER = logging.getLogger(__name__)

async def async_setup_entry(hass, entry, async_add_entities) -> None:
hub_name = entry.data[CONF_NAME]
hub = hass.data[DOMAIN][hub_name]["hub"]

device_info = {
"identifiers": {(DOMAIN, hub_name)},
"name": hub_name,
"manufacturer": ATTR_MANUFACTURER,
}

entities = []

if hub.read_battery1 == True or hub.read_battery2 == True:
for number_info in STORAGE_NUMBER_TYPES:
number = SolarEdgeNumber(
hub_name,
hub,
device_info,
number_info[0],
number_info[1],
number_info[2],
number_info[3],
number_info[4],
)
entities.append(number)

async_add_entities(entities)
return True

class SolarEdgeNumber(NumberEntity):
"""Representation of an SolarEdge Modbus number."""

def __init__(self,
platform_name,
hub,
device_info,
name,
key,
register,
fmt,
attrs
) -> None:
"""Initialize the selector."""
self._platform_name = platform_name
self._hub = hub
self._device_info = device_info
self._name = name
self._key = key
self._register = register
self._fmt = fmt

self._attr_min_value = attrs["min"]
self._attr_max_value = attrs["max"]
if "unit" in attrs.keys():
self._attr_unit_of_measurement = attrs["unit"]

async def async_added_to_hass(self) -> None:
"""Register callbacks."""
self._hub.async_add_solaredge_sensor(self._modbus_data_updated)

async def async_will_remove_from_hass(self) -> None:
self._hub.async_remove_solaredge_sensor(self._modbus_data_updated)

@callback
def _modbus_data_updated(self) -> None:
self.async_write_ha_state()

@property
def name(self) -> str:
"""Return the name."""
return f"{self._platform_name} ({self._name})"

@property
def unique_id(self) -> Optional[str]:
return f"{self._platform_name}_{self._key}"

@property
def should_poll(self) -> bool:
"""Data is delivered by the hub"""
return False

@property
def value(self) -> float:
if self._key in self._hub.data:
return self._hub.data[self._key]

async def async_set_value(self, value: float) -> None:
"""Change the selected value."""
builder = BinaryPayloadBuilder(byteorder=Endian.Big, wordorder=Endian.Little)

if self._fmt == "i":
builder.add_32bit_uint(int(value))
elif self._fmt == "f":
builder.add_32bit_float(float(value))

self._hub.write_registers(unit=1, address=self._register, payload=builder.to_registers())

self._hub.data[self._key] = value
self.async_write_ha_state()

Loading

0 comments on commit 7e56eaf

Please sign in to comment.