Skip to content

Commit

Permalink
Merge pull request #79 from krbaker/issue-70-support-reconfigure
Browse files Browse the repository at this point in the history
Issue 70 support reconfigure
  • Loading branch information
krbaker authored May 20, 2024
2 parents b1bc30c + d7df1f2 commit ba749d0
Show file tree
Hide file tree
Showing 10 changed files with 179 additions and 77 deletions.
38 changes: 23 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,6 @@ Notes:
* This network interface has a DHCP server running on it. If you plug it straight into your
home network it will make probably other systems stop working as they will DHCP to the wrong
address.
* If you have a battery you can enable collecting that data with 'use-ess'
* Be careful changing the refresh intervals for ESS or PVS.
The PVS takes a very long time to return data, I've found 120s is really the lowest safe rate.
ESS I'm less sure about though I've seen it as low as 10s in other places
(I have seen home assistant grind to a hault with too many state change entries)
Expand All @@ -75,29 +73,22 @@ When selected during installation, entities added for each device will have the
descriptor added onto the front of their name. This adds 'sunpower' 'sunvault' and 'pvs'
to entities making them even more distinct but *very* long.

## Enable virtual production meter
## Energy storage system

This adds an additional virtual device which sums up the current production and lifetime production
for all microinverters and then averages the volts and frequency across all readings.
This is useful for people who don't have a production meter installed but the data is not as
accurate.
This will now auto-detect and the option to enable/disable has been
removed (This addition thanks to [@CanisUrsa](https://github.com/CanisUrsa))

![Virtual Meter Output](virtual_meter.png)

## Use energy storage system

If you have a SunVault system along side your solar you can select this option to
include data from the energy storage system. (This addition thanks to [@CanisUrsa](https://github.com/CanisUrsa))
## Options (available from 'configure' once integration is setup)

## Solar data update interval (seconds)
### Solar data update interval (seconds)

This sets how fast the integration will try to get updated solar info from the PVS.
The lowest "safe" rate looks like about 120 seconds. I am concerned some PVSs may fail
to work properly over time and I'm guessing it might be request or error logging filling
their memory. I am running with 300 seconds right now as I went through a heck of a time
with a PVS that began to fail pushing to Sunpower's cloud.

## Energy storage update interval (seconds)
### Energy storage update interval (seconds)

Should evenly divide into Solar data update interval or be an even multiple of it (this is due to the
currently silly way polling is handled through one timer). The original author of the ESS addon
Expand Down Expand Up @@ -173,6 +164,23 @@ You should see one of these for every panel you have, they are listed by serial
| `MPPT Amps` | Amps | [MPPT][mppt] optimized panel amperage. This is the actual amperage the panel is driven by inverter to develop currently. |
| `MPPT KW` | KW | [MPPT][mppt] optimized panel output in kw. This is the actual power the panel developing currently. |

## Virtual production meter

![Virtual Meter Output](virtual_meter.png)
This data is a sum of all inverter data to make some things like the energy dashboard easier to
use for people who might be missing the production CT.
Note: this data is less accurate than a production CT.

| Entity | Units | Description |
| ---------------- | ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `State` | String | If any inverter has an error this will be an error, in low light expect errors |
| `Frequency` | Hz | Average Observed AC Frequency across all inverters. |
| `Lifetime Power` | kwh | Lifetime produced across all inverters |
| `Power` | kw | Current power sum from all inverters |
| `Voltage` | Volts | Average voltage across all inverters |
| `Temperature` | F | Average temperature across all inverters |
| `Amps` | Amps | Total amperage produced by all inverters |

### HUB+

This is the data from the HUB+.
Expand Down
54 changes: 37 additions & 17 deletions custom_components/sunpower/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,11 @@
PVS_DEVICE_TYPE,
SETUP_TIMEOUT_MIN,
SUNPOWER_COORDINATOR,
SUNPOWER_ESS,
SUNPOWER_HOST,
SUNPOWER_OBJECT,
SUNPOWER_UPDATE_INTERVAL,
SUNVAULT_DEVICE_TYPE,
SUNVAULT_UPDATE_INTERVAL,
VIRTUAL_PRODUCTION,
)
from .sunpower import (
ConnectionException,
Expand Down Expand Up @@ -98,14 +96,13 @@ def create_vmeter(data):
return data


def convert_sunpower_data(sunpower_data, virtual_production):
def convert_sunpower_data(sunpower_data):
"""Convert PVS data into indexable format data[device_type][serial]"""
data = {}
for device in sunpower_data["devices"]:
data.setdefault(device["DEVICE_TYPE"], {})[device["SERIAL"]] = device

if virtual_production:
create_vmeter(data)
create_vmeter(data)

return data

Expand Down Expand Up @@ -263,10 +260,8 @@ def convert_ess_data(ess_data, data):

def sunpower_fetch(
sunpower_monitor,
use_ess,
sunpower_update_invertal,
sunvault_update_invertal,
virtual_production,
):
"""Basic data fetch routine to get and reformat sunpower data to a dict of device
type and serial #"""
Expand All @@ -277,24 +272,32 @@ def sunpower_fetch(

sunpower_data = PREVIOUS_PVS_SAMPLE
ess_data = PREVIOUS_ESS_SAMPLE
use_ess = False
data = None

try:
if (time.time() - PREVIOUS_PVS_SAMPLE_TIME) >= (sunpower_update_invertal - 1):
PREVIOUS_PVS_SAMPLE_TIME = time.time()
sunpower_data = sunpower_monitor.device_list()
PREVIOUS_PVS_SAMPLE = sunpower_data
_LOGGER.debug("got PVS data %s", sunpower_data)
except (ParseException, ConnectionException) as error:
raise UpdateFailed from error

data = convert_sunpower_data(sunpower_data)
if ESS_DEVICE_TYPE in data: # Look for an ESS in PVS data
use_ess = True

try:
if use_ess and (time.time() - PREVIOUS_ESS_SAMPLE_TIME) >= (sunvault_update_invertal - 1):
PREVIOUS_ESS_SAMPLE_TIME = time.time()
ess_data = sunpower_monitor.energy_storage_system_status()
PREVIOUS_ESS_SAMPLE = sunpower_data
PREVIOUS_ESS_SAMPLE = ess_data
_LOGGER.debug("got ESS data %s", ess_data)
except ConnectionException as error:
except (ParseException, ConnectionException) as error:
raise UpdateFailed from error

try:
data = convert_sunpower_data(sunpower_data, virtual_production)
if use_ess:
convert_ess_data(
ess_data,
Expand Down Expand Up @@ -325,17 +328,16 @@ async def async_setup(hass: HomeAssistant, config: dict):

async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Set up sunpower from a config entry."""
_LOGGER.debug(f"Setting up {entry.entry_id}, Options {entry.options}, Config {entry.data}")
entry_id = entry.entry_id

hass.data[DOMAIN].setdefault(entry_id, {})
sunpower_monitor = SunPowerMonitor(entry.data[SUNPOWER_HOST])
use_ess = entry.data.get(SUNPOWER_ESS, False)
virtual_production = entry.data.get(VIRTUAL_PRODUCTION, True)
sunpower_update_invertal = entry.data.get(
sunpower_update_invertal = entry.options.get(
SUNPOWER_UPDATE_INTERVAL,
DEFAULT_SUNPOWER_UPDATE_INTERVAL,
)
sunvault_update_invertal = entry.data.get(
sunvault_update_invertal = entry.options.get(
SUNVAULT_UPDATE_INTERVAL,
DEFAULT_SUNVAULT_UPDATE_INTERVAL,
)
Expand All @@ -346,20 +348,23 @@ async def async_update_data():
return await hass.async_add_executor_job(
sunpower_fetch,
sunpower_monitor,
use_ess,
sunpower_update_invertal,
sunvault_update_invertal,
virtual_production,
)

# This could be better, taking the shortest time interval as the coordinator update is fine
# if the long interval is an even multiple of the short or *much* smaller
coordinator_interval = (
sunvault_update_invertal
if sunvault_update_invertal < sunpower_update_invertal and use_ess
if sunvault_update_invertal < sunpower_update_invertal
else sunpower_update_invertal
)

_LOGGER.debug(
f"Intervals: Sunpower {sunpower_update_invertal} Sunvault {sunvault_update_invertal}",
)
_LOGGER.debug(f"Coordinator update interval set to {coordinator_interval}")

coordinator = DataUpdateCoordinator(
hass,
_LOGGER,
Expand Down Expand Up @@ -387,9 +392,24 @@ async def async_update_data():
hass.config_entries.async_forward_entry_setup(entry, component),
)

entry.async_on_unload(entry.add_update_listener(update_listener))

return True


async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Update listener."""
_LOGGER.debug(
"Updating: %s with data=%s and options=%s",
entry.entry_id,
entry.data,
entry.options,
)
_LOGGER.debug("Update listener called, reloading")
await hass.config_entries.async_reload(entry.entry_id)
_LOGGER.debug("Update listener done reloading")


async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Unload a config entry."""
unload_ok = all(
Expand Down
12 changes: 7 additions & 5 deletions custom_components/sunpower/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@

from .const import (
DOMAIN,
ESS_DEVICE_TYPE,
PVS_DEVICE_TYPE,
SUNPOWER_BINARY_SENSORS,
SUNPOWER_COORDINATOR,
SUNPOWER_DESCRIPTIVE_NAMES,
SUNPOWER_ESS,
SUNPOWER_PRODUCT_NAMES,
SUNVAULT_BINARY_SENSORS,
)
Expand All @@ -32,13 +32,15 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
if SUNPOWER_PRODUCT_NAMES in config_entry.data:
do_product_names = config_entry.data[SUNPOWER_PRODUCT_NAMES]

do_ess = False
if SUNPOWER_ESS in config_entry.data:
do_ess = config_entry.data[SUNPOWER_ESS]

coordinator = sunpower_state[SUNPOWER_COORDINATOR]
sunpower_data = coordinator.data

do_ess = False
if ESS_DEVICE_TYPE in sunpower_data:
do_ess = True
else:
_LOGGER.debug("Found No ESS Data")

if PVS_DEVICE_TYPE not in sunpower_data:
_LOGGER.error("Cannot find PVS Entry")
else:
Expand Down
78 changes: 64 additions & 14 deletions custom_components/sunpower/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@
DEFAULT_SUNPOWER_UPDATE_INTERVAL,
DEFAULT_SUNVAULT_UPDATE_INTERVAL,
DOMAIN,
MIN_SUNPOWER_UPDATE_INTERVAL,
MIN_SUNVAULT_UPDATE_INTERVAL,
SUNPOWER_DESCRIPTIVE_NAMES,
SUNPOWER_ESS,
SUNPOWER_HOST,
SUNPOWER_PRODUCT_NAMES,
SUNPOWER_UPDATE_INTERVAL,
SUNVAULT_UPDATE_INTERVAL,
VIRTUAL_PRODUCTION,
)
from .sunpower import (
ConnectionException,
Expand All @@ -32,12 +32,8 @@
DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_HOST): str,
vol.Required(SUNPOWER_DESCRIPTIVE_NAMES, default=False): bool,
vol.Required(SUNPOWER_DESCRIPTIVE_NAMES, default=True): bool,
vol.Required(SUNPOWER_PRODUCT_NAMES, default=False): bool,
vol.Required(SUNPOWER_ESS, default=False): bool,
vol.Required(VIRTUAL_PRODUCTION, default=True): bool,
vol.Required(SUNPOWER_UPDATE_INTERVAL, default=DEFAULT_SUNPOWER_UPDATE_INTERVAL): int,
vol.Required(SUNVAULT_UPDATE_INTERVAL, default=DEFAULT_SUNVAULT_UPDATE_INTERVAL): int,
},
)

Expand All @@ -48,11 +44,11 @@ async def validate_input(hass: core.HomeAssistant, data):
Data has the keys from DATA_SCHEMA with values provided by the user.
"""

spm = SunPowerMonitor(data["host"])
name = "PVS {}".format(data["host"])
spm = SunPowerMonitor(data[SUNPOWER_HOST])
name = "PVS {}".format(data[SUNPOWER_HOST])
try:
response = await hass.async_add_executor_job(spm.network_status)
_LOGGER.debug("Got from %s %s", data["host"], response)
_LOGGER.debug("Got from %s %s", data[SUNPOWER_HOST], response)
except ConnectionException as error:
raise CannotConnect from error

Expand All @@ -65,9 +61,18 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL

async def async_step_user(self, user_input=None):
@staticmethod
@core.callback
def async_get_options_flow(
config_entry: config_entries.ConfigEntry,
) -> config_entries.OptionsFlow:
"""Create the options flow."""
return OptionsFlowHandler(config_entry)

async def async_step_user(self, user_input: dict[str, any] | None = None):
"""Handle the initial step."""
errors = {}
_LOGGER.debug(f"User Setup input {user_input}")
if user_input is not None:
try:
info = await validate_input(self.hass, user_input)
Expand All @@ -85,14 +90,59 @@ async def async_step_user(self, user_input=None):
errors=errors,
)

async def async_step_import(self, user_input):
async def async_step_import(self, user_input: dict[str, any] | None = None):
"""Handle import."""
await self.async_set_unique_id(user_input["host"])
await self.async_set_unique_id(user_input[SUNPOWER_HOST])
self._abort_if_unique_id_configured()

return await self.async_step_user(user_input)


class OptionsFlowHandler(config_entries.OptionsFlow):
def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
"""Initialize options flow."""
self.config_entry = config_entry

async def async_step_init(
self,
user_input: dict[str, any] | None = None,
) -> config_entries.FlowResult:
"""Manage the options."""
_LOGGER.debug(f"Options input {user_input} {self.config_entry}")
options = dict(self.config_entry.options)

errors = {}

if user_input is not None:
if user_input[SUNPOWER_UPDATE_INTERVAL] < MIN_SUNPOWER_UPDATE_INTERVAL:
errors[SUNPOWER_UPDATE_INTERVAL] = "MIN_INTERVAL"
if user_input[SUNVAULT_UPDATE_INTERVAL] < MIN_SUNVAULT_UPDATE_INTERVAL:
errors[SUNPOWER_UPDATE_INTERVAL] = "MIN_INTERVAL"
if len(errors) == 0:
options[SUNPOWER_UPDATE_INTERVAL] = user_input[SUNPOWER_UPDATE_INTERVAL]
options[SUNVAULT_UPDATE_INTERVAL] = user_input[SUNVAULT_UPDATE_INTERVAL]
return self.async_create_entry(title="", data=user_input)

current_sunpower_interval = options.get(
SUNPOWER_UPDATE_INTERVAL,
DEFAULT_SUNPOWER_UPDATE_INTERVAL,
)
current_sunvault_interval = options.get(
SUNVAULT_UPDATE_INTERVAL,
DEFAULT_SUNVAULT_UPDATE_INTERVAL,
)

return self.async_show_form(
step_id="init",
data_schema=vol.Schema(
{
vol.Required(SUNPOWER_UPDATE_INTERVAL, default=current_sunpower_interval): int,
vol.Required(SUNVAULT_UPDATE_INTERVAL, default=current_sunvault_interval): int,
},
),
errors=errors,
)


class CannotConnect(exceptions.HomeAssistantError):
"""Error to indicate we cannot connect."""

Expand Down
Loading

0 comments on commit ba749d0

Please sign in to comment.