Repository: briancmpbll/home_assistant_custom_envoy
Branch: main
Commit: 81b2f135ae63
Files: 35
Total size: 159.4 KB
Directory structure:
gitextract_t8pci746/
├── .gitignore
├── README.md
├── custom_components/
│ └── enphase_envoy_custom/
│ ├── __init__.py
│ ├── binary_sensor.py
│ ├── config_flow.py
│ ├── const.py
│ ├── diagnostics.py
│ ├── envoy_reader.py
│ ├── manifest.json
│ ├── sensor.py
│ ├── strings.json
│ └── translations/
│ ├── ca.json
│ ├── cs.json
│ ├── de.json
│ ├── el.json
│ ├── en.json
│ ├── es-419.json
│ ├── es.json
│ ├── et.json
│ ├── fr.json
│ ├── he.json
│ ├── hu.json
│ ├── id.json
│ ├── it.json
│ ├── ja.json
│ ├── ko.json
│ ├── nl.json
│ ├── no.json
│ ├── pl.json
│ ├── ru.json
│ ├── sv.json
│ ├── tr.json
│ ├── zh-Hans.json
│ └── zh-Hant.json
└── hacs.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
#vscode
.vscode/
================================================
FILE: README.md
================================================
[](https://github.com/hacs/integration#readme)
This is a HACS custom integration for [enphase envoys/IQ Gateways](https://enphase.com/en-us/products-and-services/envoy-and-combiner) with firmware version 7.X. This integration is based off work done by @jesserizzo, @gtdiehl and @DanBeard, with some changes to report individual battery status.
It still supports Envoys with firmware versions before 7.x including R3 versions for legacy models.
Works with older models that only have (some) production metrics (ie. Envoy-C R or LCD), newer models with only production metrics (IQ Gateway / ENVOY-S Standard) and models that offer both production and consumption metrics (ie. Envoy-S metered).
# Installation
1. Install [HACS](https://hacs.xyz/) if you haven't already
2. Add this repository as a [custom integration repository](https://hacs.xyz/docs/faq/custom_repositories) in HACS
4. Restart home assistant
5. Add the integration through the home assistant configuration flow, [specify settings as needed](#initial-configuration-details)
7. The integration has some run-time configuration options, [set these as desired](#runtime-configuration) after startup.
8. The integration has some hidden entities, [enable these](#disabled-entities) to use these.
# Usage
## Initial Configuration details
The initial configuration window requires you to enter the details how to access the Envoy. Following fields have to be filled.
### Host
Enter the IP address of the Envoy. If the Envoy is auto-discovered it will be pre-filled. The IP address may be an ipv4 or ipv6 address.
### Username and Password
Specify the username and password to access the envoy data. What username and password to use depends on the ENVOY type and/or it's firmware version:
- If your IQ Gateway / Envoy-S is on Firmware 7.x or later, use your Enphase Enlighten username and password. Make sure to enable the 'Use Enlighten' option at the bottom of the form.
- For older models and ENVOY-S with firmware before 7.x use `envoy` without a password, `installer` without a password or a valid username and password for the type.
- For older models that require username `installer` with a password, this can be obtained with this: [tool](https://thecomputerperson.wordpress.com/2016/08/28/reverse-engineering-the-enphase-installer-toolkit/).
- In some cases, you need to use the username `envoy` with the last 6 digits of the unit's serial number as password.
See [the Enphase documentation](https://support.enphase.com/s/article/What-is-the-Username-and-Password-for-the-Administration-page-of-the-Envoy-local-interface) for more details on various units.
**Note** This integration does not provide the additional data accessible by Enphase Installer or DIY accounts, only data accessible by Home owner accounts is provided. Using an Installer or DIY account may or may not work work, but currently just the Home owner data is retrieved.
## Serial number
Specify the Envoy serial number. If the Envoy is auto-discovered it will be pre-filled.
### Use Enlighten
Enable this option with IQ Gateway/ENVOY-S Firmware 7.x or later that require Enphase tokens for authentication. It will use the username and password to retrieve the authentication token from the Enphase website, cache it and use it to access the Envoy.
## Runtime configuration
Once the Envoy has been running and is operational, the following configuration items are available:
### Scan Interval - Time between Entity updates
By default the Envoy data is collected every 60 seconds. One can change the setting to what is desired with a minimum of every 5 seconds. Upon changing the value, reload the integration (or restart Home Assistant).
What the optimal scan frequency is depends on the Envoy model. Models without meters typically update the inverter data every 5 minutes. Models using meters update measurements way more frequent, probably every second or so. Hence the default of 60 seconds as starting point. Some models may have capacity issues running at high refresh rates, so no single recipe is available.
**Note** Envoy Metered has a data streaming option to bring in data as it comes available which is not currently supported by this integration.
### Timeout for single Envoy Page
Specifies the timeout for each single data get to the Envoy. Defaults to 30 seconds. Shortening this time does not make the Envoy response faster, lengthening it will allow a slow Envoy more time to respond before time-out occurs.
### How many retries in getting an Envoy response
Specifies how many times a retry should be attempted after the first one failed. Must be at least 1 to allow for automatic token updates. Typically do not change this setting. Increasing may help in poor network conditions.
### Time between 2 retries
Optionally a wait time can be inserted between 2 retries. Only change this in special circumstances.
### Overall Timeout
When getting data from the Envoy, an overall timer is started. If not all data is returned when the timer expires, the data collection is considered timed-out and all data is set to unavailable. Intent is catch all if data collection is never returning. Do not change this setting, unless Timeout for single envoy page or number of retries needs to be increased. In that case increase this overall timer value as well to prevent it to timeout the data collection. To get a feel for needed time, enable the debug mode on the envoy and inspect timing of a full collection cycle.
### Do not use production json
This switch, intended for use with the Envoy-s Metered only, will tell the integration not to use production endpoint on the Envoy. The production endpoint is a relatively slow endpoint on the Envoy and reportedly crashes or restarts at times resulting in timeouts.
The Envoy-s Metered (only) has other, faster endpoints that provide a subset of what production endpoint offers. This subset is lacking the daily total and last 7 day total values which are only provided by the production endpoint. If you are more interested in faster updates from the CT clamps and have less interest in the Daily total or last 7 day total then this may be an option to consider. The values for today total and last 7 day total will show as unavailable. The values for production and consumption CT clamps will update with every collection cycle. The values for the inverters will continue to update every 5 minutes as before.
## Disabled entities
The integration comes with some entities disabled by default. These only apply when using metered Envoy with CT clamps. If desired enable these by opening the HA entities window in the settings menu. Remove the filter for not shown entities by pushing the `clear` button. Then enter disabled or enphase in the search filter to find the disabled entities. Use the selector box to select the ones to enable and use the `enable selected` button to enable them.
# Firmware and its impact
Enphase model offering differs in various countries as does firmware versions and releases. Not all firmware is released in all countries and as a result firmware versions may differ largely in time. Enphase does push new firmware to the IQ Gateway / Envoy, 'not necessarily letting the home owner know'. In the past this has broken this integration as API details and change information is limited available. See the [Enphase documentation website](https://enphase.com/installers/resources/documentation/communication) for information available.
# Different models have different features
This integration supports various models but as models have different features they will not all provide the same data. Brief list of reported data below.
## ENVOY C / R / LCD
- Current power production, today's, last 7 days and lifetime energy production. And Active inverter count, which is disabled by default.
## IQ Gateway / ENVOY S standard (non metered)
- Current power production, today's, last 7 days and lifetime energy production.
- Current power production for each connected inverter.
## IQ Gateway / ENVOY S standard metered
What data is available depends on how many current transformer clamps (CT) are installed and what currents they measure. Both production and consumption clamps can be installed, each for up to 3 phases or multiple circuits on their own breaker in single phase setup. The consumption clamps can be installed in 2 modes, 'Load with Solar'or 'Load only'. To measure net-consumption (energy import/export to the grid) it should be installedin Load with Solar mode. If in 'Load only' mode only total-consumption (to the house) can be reported.
### with connected current transformer clamps
- Current power production and consumption, today's, last 7 days and lifetime energy production and consumption over all phases.
- Current power production and consumption, today's, last 7 days and lifetime energy production and consumption for each individual phase named L1, L2 and L3.
- Current net power consumption and lifetime net energy production (export) and consumption (import) over all phases.
- Current net power consumption and lifetime net energy production (export) and consumption (import) for each individual phase named L1, L2 and L3.
- Next entities are disabled by default and need to be enabled in the entities configuration screen
- Power production for each connected inverter.
- Power factor over all phases.
- Power factor for each individual phase named L1, L2 and L3.
- Voltage over all phases. (Be aware this is the summed Voltage of all measured phases!)
- Voltage for each individual phase named L1, L2 and L3.
- Frequency over all phases.
- Frequency for each individual phase named L1, L2 and L3.
- Production and consumption Current (amps) over all phases.
- Production and consumption Current (amps) for each individual phase named L1, L2 and L3.
**Note** If you have CT clamps on a single phase / breaker circuit only, the L1 production and consumption phase sensors will show same data as the over all phases sensors.
### without connected current transformer clamps
The current firmware (D7.6.175 and probably some other right before and after it) without CT clamps connected and configured does obviously not report these measurements. But for some reason it is only reporting:
- Current power production and lifetime energy production. Today's and last 7 day energy production reportedly are both solid 0.
- Lifetime Energy production reportedly resets to zero roughly every 1.19 MWh.
- Current power production for each connected inverter.
**Note** - When adding (or removing) CT clamps after use witouth CT clamps this will cause (huge) step changes/spikes in life time values when CT readings are now from the CT clamps (or longer available) and the wrapping value is no longer/now used.
# Device and Entities
The naming scheme used is based on the Envoy and inverter Serial numbers.
## Device
A device `Envoy <serialnumber>` is created with sensor entities for accessible data.
## Envoy Sensors
|Sensor name|Sensor ID|Units|remarks|
|-----|-----|----|----|
|Envoy \<sn\> Current Power Production|sensor.Envoy_\<sn\>_current_power_production|W||
|Envoy \<sn\> Today's Energy production|sensor.Envoy_\<sn\>_todays_energy_production|Wh|1|
|Envoy \<sn\> Last Seven Days Energy Production|sensor.Envoy_\<sn\>_last_seven_days_energy_production|Wh|1|
|Envoy \<sn\> Lifetime Energy Production|sensor.Envoy_\<sn\>_lifetime_energy_production|Wh|2|
|Envoy \<sn\> Lifetime Net Energy Production|sensor.Envoy_\<sn\>_lifetime_net_energy_production|Wh|4|
|Envoy \<sn\> Current Power Consumption|sensor.Envoy_\<sn\>_current_power_consumption|W||
|Envoy \<sn\> Current Net Power Consumption|sensor.Envoy_\<sn\>_current_net_power_consumption|W|4|
|Envoy \<sn\> Today's Energy Consumption|sensor.Envoy_\<sn\>_todays_energy_consumption|Wh|4,5|
|Envoy \<sn\> Last Seven Days Energy Consumption|sensor.Envoy_\<sn\>_last_seven_days_energy_consumption|Wh|4|
|Envoy \<sn\> Lifetime Energy Consumption|sensor.Envoy_\<sn\>_lifetime_energy_consumption|Wh|4|
|Envoy \<sn\> Lifetime Net Energy Consumption|sensor.Envoy_\<sn\>_lifetime_net_energy_consumption|Wh|4,7,8|
|Envoy \<sn\> Power Factor|sensor.Envoy_\<sn\>_pf||4,9|
|Envoy \<sn\> Voltage|sensor.Envoy_\<sn\>_voltage|V|4,9|
|Envoy \<sn\> Frequency|sensor.Envoy_\<sn\>_frequency|Wh|4,9|
|Envoy \<sn\> Consumption Current|sensor.Envoy_\<sn\>_consumption_Current|A|4,9|
|Envoy \<sn\> Production Current|sensor.Envoy_\<sn\>_production_Current|A|4,9|
|Envoy \<sn\> Active Inverter Count|sensor.Envoy_\<sn\>_active_inverter_count||9,10|
||||
|Grid Status |binary_sensor.grid_status|On/Off|3|
||||
|Envoy \<sn\> Current Power Production L\<n\>|sensor.Envoy_\<sn\>_current_power_production_l\<n\>|W|4,5|
|Envoy \<sn\> Today's Energy production L\<n\>|sensor.Envoy_\<sn\>_todays_energy_production_l\<n\>|Wh|4,5|
|Envoy \<sn\> Last Seven Days Energy Production L\<n\>|sensor.Envoy_\<sn\>_last_seven_days_energy_production_l\<n\>|Wh|4,5|
|Envoy \<sn\> Lifetime Energy Production L\<n\>|sensor.Envoy_\<sn\>_lifetime_energy_consumption_l\<n\>|Wh|4,5|
|Envoy \<sn\> Lifetime Net Energy Production L\<n\>|sensor.Envoy_\<sn\>_lifetime_net_energy_production_l\<n\>|Wh|4,5,7,8|
|Envoy \<sn\> Current Power Consumption L\<n\>|sensor.Envoy_\<sn\>_current_power_consumption_l\<n\>|W|4,5|
|Envoy \<sn\> Current Net Power Consumption L\<n\>|sensor.Envoy_\<sn\>_current_net_power_consumption_l\<n\>|W|4,5|
|Envoy \<sn\> Today's Energy Consumption L\<n\>|sensor.Envoy_\<sn\>_todays_energy_consumption_l\<n\>|Wh|4,5,6|
|Envoy \<sn\> Last Seven Days Energy Consumption L\<n\>|sensor.Envoy_\<sn\>_last_seven_days_energy_consumption L\<n\>|Wh|4,5,6|
|Envoy \<sn\> Lifetime Energy Consumption L\<n\>|sensor.Envoy_\<sn\>_lifetime_energy_consumption_l\<n\>|Wh|4,5,6|
|Envoy \<sn\> Lifetime Net Energy Consumption L\<n\>|sensor.Envoy_\<sn\>_lifetime_net_energy_consumption_l\<n\>|Wh|4,5,6,7,8|
|Envoy \<sn\> Power Factor L\<n\>|sensor.Envoy_\<sn\>_pf||4,5,9|
|Envoy \<sn\> Voltage L\<n\>|sensor.Envoy_\<sn\>_voltage|V|4,5,9|
|Envoy \<sn\> Frequency L\<n\>|sensor.Envoy_\<sn\>_frequency|Wh|4,5,9|
|Envoy \<sn\> Consumption Current L\<n\>|sensor.Envoy_\<sn\>_consumption_Current|A|4,5,9|
|Envoy \<sn\> Production Current L\<n\>|sensor.Envoy_\<sn\>_production_Current|A|4,5,9|
|Envoy \<sn\> |sensor.Envoy_\<sn\>_|Wh|4,5|
1 Always zero for Envoy Metered without meters.
2 Reportedly resets to zero when reaching ~1.92MWh for Envoy Metered without meters.
3 Not available on Legacy models and ENVOY Standard with recent firmware.
4 Only on Envoy metered with configured and connected meters.
5 L\<n\> L1,L2,L3, availability depends on which and how many phases are connected and configured.
6 Reportedly always zero on Envoy metered with Firmware D8.
7 In V0.0.18 renamed to Lifetime Net Energy Consumption /Production from Export Index/Import Import in v0.0.17. Old Entities will show as unavailable.
8 Only when consumption CT is installed in 'Load with Solar' mode. In 'Load only' mode values have no meaning.
9 Disabled by default and must be enabled in the entities configuration screen. These are values from the consumption CT.
10 Only available on legacy Envoy.
## Inverter Sensors
For each inverter a sensor for current power production is created.
|Sensor name|Sensor ID|UNits|remarks|
|-----|-----|----|----|
|Envoy \<sn\> Inverter \<sn\>|sensor.Envoy_\<sn\>\_Inverter_\<sn\>|W|1|
1: Not available on Legacy models
**Note** the entity 'Last Updated' for each inverter is currently not provided.
**Note** As can be noted the Envoy serial number is part of the inverter sensor id and name. Internally the unique_id for it is the inverter serial number. When changing your setup by moving inverters to a new/different Envoy it will require some preparation/research how this will work out. (Statistics (history) is stored by sensor id)
## Battery Sensors
For each battery a sensor for percent full is created as well as sensors for overall battery percentage, overall battery capacity, overall energy charged and discharged are created.
|Sensor name|Sensor ID|Units|remarks|
|-----|-----|----|----|
|Envoy \<sn\> Battery \<sn\>|sensor.Envoy_\<sn\>\_Battery_\<sn\>|%|1|
|Envoy \<sn\> Total Battery Percentage|sensor.Envoy_\<sn\>\_total_battery_percentage|%|1|
|Envoy \<sn\> Current Battery Capacity|sensor.Envoy_\<sn\>\_current_battery_capacity|Wh|1|
|Envoy \<sn\> Battery Energy Charged|sensor.Envoy_\<sn\>\_battery_energy_charged|Wh|1|
|Envoy \<sn\> Battery Energy Discharged|sensor.Envoy_\<sn\>\_battery_energy_charged|Wh|1|
1: Not available on Legacy models and ENVOYS-S Standard
# How to switch to Enphase token authorization
Once the envoy received the new firmware that requires token authorization, data collection will fail. To switch to the token usage execute next steps:
- [Install](#installation) the Custom integration using HACS
- In Home Assistant go to the Enphase Envoy integration and delete it.
- Restart Home Assistant
- The envoy will be auto-discovered again. If not add an Envoy Integration manually.
- In the configuration screen now use your Enphase Enlighten username and password and enable the 'Use Enlighten' option.
- Once it's configured it will continue reporting data in the same entities.
- Optionally change the default time interval from 60 to what is preferred.
# Troubleshooting
When issues occur with this integration some items to check are:
- Use the `Download Diagnostics` button in the Envoy Device page or the Enphase Integration page menu. It will download settings and recent data of the Envoy and provide some key information.
- What model are you using. This will drive what can be expected.
- What firmware is your model using, Was a firmware update recently pushed to the device?
- Enable debug logging and let it run for a couple of minutes, disable it again and the log file will download. Check for obvious errors and be prepared to share it as needed for troubleshooting. Any tokens, usernames or passwords for the Envoy integration are not visible, but there may be sensitive information of other integrations that are being used.
- All data collected is logged in lines like `Fetched from https://192.168.01.10/some_url: <Response [200 OK]>:`. Inspecting these provides insight in what and how successful data is collected.
- The Envoy model it thinks its dealing with is reported in a line containing: `Using Model: P (HTTPs, Metering enabled: False, Get Inverters: True)`. (Model PC is envoy metered, P is Standard and R/LCD with FW >= R3.9 and P0 is Legacy/C/R/LCD with FW < R3.9>)
- When configuring the Envoy for token use it will reach out to the Enphase Enlighten website to obtain a token. Reportedly the Enphase website is not equally responsive every moment of the day, week, moth, year and the setup will fail. At this moment the only answer to that is your perseverance or just try at another moment.
- The token lifetime for Home Owner accounts is currently 1 year. The token is cached, eliminating the need to connect to Enphase each reload or restart. When the token is expired or some other authorization hiccup occurs a new token will be obtained. If that is needed at a moment it can't connect to Enphase it will try until success but in the mean time no data is collected from the Envoy. When using an Installer or DIY account this may work as well but the lifetime is 12 hours and refresh is way more frequent.
- The Envoy integration supports zeroconf for auto detection and changes of IP addresses for the Envoy. It will not switch to an IPV6 address if the default network interface is running ipv4 or the other way around.It supports both IPV4 and IPV6. To change between these when default interface change IP type, remove and re-add the Envoy.
- When an error is reported during the initial configuration, inspect the home-assistant.log file in the /config folder. It will reveal what happened:
- Validate input, getdata returned RuntimeError: Could not Authenticate with Enlighten, status: 401, <Response [401 Unauthorized]> : Check if Enphase username and/or password are correct
- Validate input, getdata returned RuntimeError: Could not get enlighten token, status: 403, <Response [403 Forbidden]> : Make sure envoy serialnumber is correct and connected to your Enphase account
- Validate input, getdata returned HTTPError: All connection attempts failed : failure to connect to envoy. : Validate if correct IP address of the Envoy is used
- Fetched (1 of 2) in 0.0 sec from http://x.x.x.x/production.json?details=1: <Response [301 Moved Permanently]>: <html> : Was 'use Enlighten' checked when using tokens or validate username/pw used for legacy devices.
- Lifetime Net Energy Consumption / Production shows 0 or incorrect values. This is the case when the Consumption CT is not available or installed in Load only mode.
================================================
FILE: custom_components/enphase_envoy_custom/__init__.py
================================================
"""The Enphase Envoy integration."""
from __future__ import annotations
from datetime import timedelta
import logging
import async_timeout
from .envoy_reader import EnvoyReader
import httpx
from numpy import isin
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from homeassistant.helpers.storage import Store
from .const import COORDINATOR, DOMAIN, NAME, PLATFORMS, SENSORS, CONF_USE_ENLIGHTEN, CONF_SERIAL, PHASE_SENSORS, DEFAULT_SCAN_INTERVAL
SCAN_INTERVAL = timedelta(seconds=60)
STORAGE_KEY = "envoy"
STORAGE_VERSION = 1
FETCH_RETRIES = 1
FETCH_TIMEOUT_SECONDS = 30
FETCH_HOLDOFF_SECONDS = 0
COLLECTION_TIMEOUT_SECONDS = 55
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Enphase Envoy from a config entry."""
config = entry.data
options = entry.options
name = config[CONF_NAME]
# Setup persistent storage, to save tokens between home assistant restarts
store = Store(hass, STORAGE_VERSION, ".".join([STORAGE_KEY, entry.entry_id]))
envoy_reader = EnvoyReader(
config[CONF_HOST],
username=config[CONF_USERNAME],
password=config[CONF_PASSWORD],
enlighten_user=config[CONF_USERNAME],
enlighten_pass=config[CONF_PASSWORD],
inverters=True,
# async_client=get_async_client(hass),
use_enlighten_owner_token=config.get(CONF_USE_ENLIGHTEN, False),
enlighten_serial_num=config[CONF_SERIAL],
https_flag='s' if config.get(CONF_USE_ENLIGHTEN, False) else '',
store=store,
fetch_retries=options.get("data_fetch_retry_count", FETCH_RETRIES),
fetch_timeout_seconds=options.get("data_fetch_timeout_seconds", FETCH_TIMEOUT_SECONDS),
fetch_holdoff_seconds=options.get("data_fetch_holdoff_seconds", FETCH_HOLDOFF_SECONDS),
do_not_use_production_json=options.get("do_not_use_production_json",False),
)
await envoy_reader._sync_store()
async def async_update_data():
"""Fetch data from API endpoint."""
data = {}
async with async_timeout.timeout(options.get("data_collection_timeout_seconds", COLLECTION_TIMEOUT_SECONDS)):
try:
await envoy_reader.getData()
except httpx.HTTPStatusError as err:
raise ConfigEntryAuthFailed from err
except httpx.HTTPError as err:
raise UpdateFailed(f"Error communicating with API: {err}") from err
for description in SENSORS:
if description.key == "inverters":
data[
"inverters_production"
] = await envoy_reader.inverters_production()
elif description.key == "batteries":
battery_data = await envoy_reader.battery_storage()
if isinstance(battery_data, list) and len(battery_data) > 0:
battery_dict = {}
for item in battery_data:
battery_dict[item["serial_num"]] = item
data[description.key] = battery_dict
elif (description.key not in ["current_battery_capacity", "total_battery_percentage"]):
data[description.key] = await getattr(
envoy_reader, description.key
)()
for description in PHASE_SENSORS:
if description.key[:-2] in [
"none_known_at_this_time_"
]:
# call phase function for these
data[description.key] = await getattr(envoy_reader, description.key[:-3]+"_phase")( description.key[-2:].lower())
else:
#catchall for non-specified phase sensors
#get attributes for phase sensors based on key name
#Removes _L1, _L2 or _L3 from key to call base non-phased function
#Pass l1, l2 or l3 as parameter to _phase function
data[description.key] = await getattr(envoy_reader, description.key[:-3])( description.key[-2:].lower())
data["grid_status"] = await envoy_reader.grid_status()
data["envoy_info"] = await envoy_reader.envoy_info()
_LOGGER.debug("Retrieved data from API: %s", data)
await envoy_reader._sync_store()
return data
coordinator = DataUpdateCoordinator(
hass,
_LOGGER,
name=f"envoy {name}",
update_method=async_update_data,
update_interval=timedelta(
seconds=options.get("data_interval", DEFAULT_SCAN_INTERVAL)
)
)
try:
await coordinator.async_config_entry_first_refresh()
except ConfigEntryAuthFailed:
envoy_reader.get_inverters = False
await coordinator.async_config_entry_first_refresh()
if not entry.unique_id:
try:
serial = await envoy_reader.get_full_serial_number()
except httpx.HTTPError:
pass
else:
hass.config_entries.async_update_entry(entry, unique_id=serial)
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {
COORDINATOR: coordinator,
NAME: name,
}
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok
================================================
FILE: custom_components/enphase_envoy_custom/binary_sensor.py
================================================
from homeassistant.core import HomeAssistant
from homeassistant.config_entries import ConfigEntry
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.components.binary_sensor import BinarySensorEntity
from homeassistant.helpers.entity import DeviceInfo
from .const import COORDINATOR, DOMAIN, NAME, BINARY_SENSORS, ICON
async def async_setup_entry(
hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
data = hass.data[DOMAIN][config_entry.entry_id]
coordinator = data[COORDINATOR]
name = data[NAME]
entities = []
for sensor_description in BINARY_SENSORS:
if sensor_description.key == "grid_status":
if coordinator.data.get("grid_status") is not None:
entities.append(
EnvoyGridStatusEntity(
sensor_description,
sensor_description.name,
name,
config_entry.unique_id,
None,
coordinator,
)
)
async_add_entities(entities)
class EnvoyGridStatusEntity(CoordinatorEntity, BinarySensorEntity):
def __init__(
self,
description,
name,
device_name,
device_serial_number,
serial_number,
coordinator,
):
self.entity_description = description
self._name = name
self._serial_number = serial_number
self._device_name = device_name
self._device_serial_number = device_serial_number
CoordinatorEntity.__init__(self, coordinator)
@property
def icon(self):
"""Icon to use in the frontend, if any."""
return ICON
@property
def name(self):
"""Return the name of the sensor."""
return self._name
@property
def unique_id(self):
"""Return the unique id of the sensor."""
if self._serial_number:
return self._serial_number
if self._device_serial_number:
return f"{self._device_serial_number}_{self.entity_description.key}"
@property
def device_info(self) -> DeviceInfo or None:
"""Return the device_info of the device."""
if not self._device_serial_number:
return None
return DeviceInfo(
identifiers={(DOMAIN, str(self._device_serial_number))},
manufacturer="Enphase",
model="Envoy",
name=self._device_name,
)
@property
def is_on(self) -> bool:
"""Return the status of the requested attribute."""
return self.coordinator.data.get("grid_status") == "closed"
================================================
FILE: custom_components/enphase_envoy_custom/config_flow.py
================================================
"""Config flow for Enphase Envoy integration."""
from __future__ import annotations
import contextlib
import logging
from typing import Any
from .envoy_reader import EnvoyReader
import httpx
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.components import network
from homeassistant.components import zeroconf
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant, callback
from homeassistant.data_entry_flow import FlowResult
from homeassistant.exceptions import HomeAssistantError
from homeassistant.util.network import is_ipv4_address
from .const import DOMAIN, CONF_SERIAL, CONF_USE_ENLIGHTEN, DEFAULT_SCAN_INTERVAL
_LOGGER = logging.getLogger(__name__)
ENVOY = "Envoy"
async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> EnvoyReader:
"""Validate the user input allows us to connect."""
envoy_reader = EnvoyReader(
data[CONF_HOST],
username=data[CONF_USERNAME],
password=data[CONF_PASSWORD],
enlighten_user=data[CONF_USERNAME],
enlighten_pass=data[CONF_PASSWORD],
inverters=False,
# async_client=get_async_client(hass),
use_enlighten_owner_token=data.get(CONF_USE_ENLIGHTEN, False),
enlighten_serial_num=data[CONF_SERIAL],
https_flag='s' if data.get(CONF_USE_ENLIGHTEN,False) else '',
fetch_timeout_seconds=60
)
try:
await envoy_reader.getData()
except httpx.HTTPStatusError as err:
_LOGGER.warning("Validate input, getdata returned HTTPStatusError: %s",err)
raise InvalidAuth from err
except (httpx.HTTPError) as err:
_LOGGER.warning("Validate input, getdata returned HTTPError: %s",err)
raise CannotConnect from err
except (RuntimeError) as err:
_LOGGER.warning("Validate input, getdata returned RuntimeError: %s",err)
raise
return envoy_reader
async def ipv4asdefault(hass: HomeAssistant):
adapters = await network.async_get_adapters(hass)
for adapter in adapters:
if adapter["default"]:
return adapter["ipv4"] is not None
return False
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Enphase Envoy."""
VERSION = 1
def __init__(self):
"""Initialize an envoy flow."""
self.ip_address = None
self.username = None
self._reauth_entry = None
@callback
def _async_generate_schema(self):
"""Generate schema."""
schema = {}
if self.ip_address:
schema[vol.Required(CONF_HOST, default=self.ip_address)] = vol.In(
[self.ip_address]
)
else:
schema[vol.Required(CONF_HOST)] = str
schema[vol.Optional(CONF_USERNAME, default=self.username)] = str
schema[vol.Optional(CONF_PASSWORD, default="")] = str
schema[vol.Optional(CONF_SERIAL, default=self.unique_id)] = str
schema[vol.Optional(CONF_USE_ENLIGHTEN)] = bool
return vol.Schema(schema)
@callback
def _async_current_hosts(self):
"""Return a set of hosts."""
return {
entry.data[CONF_HOST]
for entry in self._async_current_entries(include_ignore=False)
if CONF_HOST in entry.data
}
async def async_step_zeroconf(
self, discovery_info: zeroconf.ZeroconfServiceInfo
) -> FlowResult:
"""Handle a flow initialized by zeroconf discovery."""
serial = discovery_info.properties["serialnum"]
await self.async_set_unique_id(serial)
ipv4_default = await ipv4asdefault(self.hass)
if ipv4_default and not is_ipv4_address(discovery_info.host):
return self.async_abort(reason="not_ipv4_address")
# autodiscovery is updating the ip address of an existing envoy with matching serial to new detected ip adress
self.ip_address = discovery_info.host
self._abort_if_unique_id_configured({CONF_HOST: self.ip_address})
for entry in self._async_current_entries(include_ignore=False):
if (
entry.unique_id is None
and CONF_HOST in entry.data
and entry.data[CONF_HOST] == self.ip_address
):
title = f"{ENVOY} {serial}" if entry.title == ENVOY else ENVOY
self.hass.config_entries.async_update_entry(
entry, title=title, unique_id=serial
)
self.hass.async_create_task(
self.hass.config_entries.async_reload(entry.entry_id)
)
return self.async_abort(reason="already_configured")
return await self.async_step_user()
async def async_step_reauth(self, user_input):
"""Handle configuration by re-auth."""
self._reauth_entry = self.hass.config_entries.async_get_entry(
self.context["entry_id"]
)
return await self.async_step_user()
def _async_envoy_name(self) -> str:
"""Return the name of the envoy."""
if self.unique_id:
return f"{ENVOY} {self.unique_id}"
return ENVOY
async def _async_set_unique_id_from_envoy(self, envoy_reader: EnvoyReader) -> bool:
"""Set the unique id by fetching it from the envoy."""
serial = None
with contextlib.suppress(httpx.HTTPError):
serial = await envoy_reader.get_full_serial_number()
if serial:
await self.async_set_unique_id(serial)
return True
return False
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle the initial step."""
errors = {}
if user_input is not None:
if (
not self._reauth_entry
and user_input[CONF_HOST] in self._async_current_hosts()
):
return self.async_abort(reason="already_configured")
try:
envoy_reader = await validate_input(self.hass, user_input)
except RuntimeError as rerr:
errors["base"] = "invalid_auth"
except CannotConnect as cerr:
errors["base"] = "cannot_connect"
except InvalidAuth:
errors["base"] = "invalid_auth"
except Exception as exc: # pylint: disable=broad-except
_LOGGER.exception("Unexpected exception in validate input %s",exc)
errors["base"] = "unknown"
else:
data = user_input.copy()
data[CONF_NAME] = self._async_envoy_name()
if self._reauth_entry:
self.hass.config_entries.async_update_entry(
self._reauth_entry,
data=data,
)
return self.async_abort(reason="reauth_successful")
if not self.unique_id and await self._async_set_unique_id_from_envoy(
envoy_reader
):
data[CONF_NAME] = self._async_envoy_name()
if self.unique_id:
self._abort_if_unique_id_configured({CONF_HOST: data[CONF_HOST]})
return self.async_create_entry(title=data[CONF_NAME], data=data)
if self.unique_id:
self.context["title_placeholders"] = {
CONF_SERIAL: self.unique_id,
CONF_HOST: self.ip_address,
}
return self.async_show_form(
step_id="user",
data_schema=self._async_generate_schema(),
errors=errors,
)
@staticmethod
@callback
def async_get_options_flow(config_entry):
return EnvoyOptionsFlowHandler(config_entry)
class EnvoyOptionsFlowHandler(config_entries.OptionsFlow):
"""Envoy config flow options handler."""
def __init__(self, config_entry):
"""Initialize Envoy options flow."""
self.config_entry = config_entry
async def async_step_init(self, _user_input=None):
"""Manage the options."""
return await self.async_step_user()
async def async_step_user(self, user_input=None):
"""Handle a flow initialized by the user."""
if user_input is not None:
return self.async_create_entry(title="", data=user_input)
schema = {
vol.Optional(
"data_interval",
default=self.config_entry.options.get(
"data_interval", DEFAULT_SCAN_INTERVAL
),
): vol.All(vol.Coerce(int), vol.Range(min=5)),
vol.Optional(
"data_fetch_timeout_seconds",
default=self.config_entry.options.get(
"data_fetch_timeout_seconds", 30
),
): vol.All(vol.Coerce(int), vol.Range(min=5)),
vol.Optional(
"data_fetch_retry_count",
default=self.config_entry.options.get(
"data_fetch_retry_count", 1
),
): vol.All(vol.Coerce(int), vol.Range(min=1)),
vol.Optional(
"data_fetch_holdoff_seconds",
default=self.config_entry.options.get(
"data_fetch_holdoff_seconds", 0
),
): vol.All(vol.Coerce(int), vol.Range(min=0)),
vol.Optional(
"data_collection_timeout_seconds",
default=self.config_entry.options.get(
"data_collection_timeout_seconds", 55
),
): vol.All(vol.Coerce(int), vol.Range(min=30)),
vol.Optional(
"do_not_use_production_json",
default=self.config_entry.options.get(
"do_not_use_production_json", False
),
): bool,
}
return self.async_show_form(step_id="user", data_schema=vol.Schema(schema))
class CannotConnect(HomeAssistantError):
"""Error to indicate we cannot connect."""
class InvalidAuth(HomeAssistantError):
"""Error to indicate there is invalid auth."""
================================================
FILE: custom_components/enphase_envoy_custom/const.py
================================================
"""The enphase_envoy component."""
from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
BinarySensorEntityDescription
)
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntityDescription,
SensorStateClass
)
from homeassistant.const import (
UnitOfEnergy,
UnitOfPower,
UnitOfElectricPotential,
UnitOfFrequency,
Platform,
PERCENTAGE
)
DOMAIN = "enphase_envoy"
PLATFORMS = [Platform.SENSOR, Platform.BINARY_SENSOR]
ICON = "mdi:flash"
COORDINATOR = "coordinator"
NAME = "name"
DEFAULT_SCAN_INTERVAL = 60 # default in seconds
CONF_SERIAL = "serial"
CONF_USE_ENLIGHTEN = "use_enlighten"
SENSORS = (
SensorEntityDescription(
key="production",
name="Current Power Production",
native_unit_of_measurement=UnitOfPower.WATT,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.POWER,
),
SensorEntityDescription(
key="daily_production",
name="Today's Energy Production",
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
state_class=SensorStateClass.TOTAL_INCREASING,
device_class=SensorDeviceClass.ENERGY,
),
SensorEntityDescription(
key="seven_days_production",
name="Last Seven Days Energy Production",
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
state_class=SensorStateClass.TOTAL,
device_class=SensorDeviceClass.ENERGY,
),
SensorEntityDescription(
key="lifetime_production",
name="Lifetime Energy Production",
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
state_class=SensorStateClass.TOTAL_INCREASING,
device_class=SensorDeviceClass.ENERGY,
),
SensorEntityDescription(
key="lifetime_net_production",
name="Lifetime Net Energy Production",
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
state_class=SensorStateClass.TOTAL_INCREASING,
device_class=SensorDeviceClass.ENERGY,
),
SensorEntityDescription(
key="consumption",
name="Current Power Consumption",
native_unit_of_measurement=UnitOfPower.WATT,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.POWER,
),
SensorEntityDescription(
key="net_consumption",
name="Current Net Power Consumption",
native_unit_of_measurement=UnitOfPower.WATT,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.POWER,
),
SensorEntityDescription(
key="daily_consumption",
name="Today's Energy Consumption",
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
state_class=SensorStateClass.TOTAL_INCREASING,
device_class=SensorDeviceClass.ENERGY,
),
SensorEntityDescription(
key="seven_days_consumption",
name="Last Seven Days Energy Consumption",
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
state_class=SensorStateClass.TOTAL,
device_class=SensorDeviceClass.ENERGY,
),
SensorEntityDescription(
key="lifetime_consumption",
name="Lifetime Energy Consumption",
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
state_class=SensorStateClass.TOTAL_INCREASING,
device_class=SensorDeviceClass.ENERGY,
),
SensorEntityDescription(
key="lifetime_net_consumption",
name="Lifetime Net Energy Consumption",
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
state_class=SensorStateClass.TOTAL_INCREASING,
device_class=SensorDeviceClass.ENERGY,
),
SensorEntityDescription(
key="inverters",
name="Inverter",
native_unit_of_measurement=UnitOfPower.WATT,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.POWER,
),
SensorEntityDescription(
key="batteries",
name="Battery",
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.BATTERY
),
SensorEntityDescription(
key="total_battery_percentage",
name="Total Battery Percentage",
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT
),
SensorEntityDescription(
key="current_battery_capacity",
name="Current Battery Capacity",
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
state_class=SensorStateClass.TOTAL,
device_class=SensorDeviceClass.ENERGY
),
SensorEntityDescription(
key="pf",
name="Power Factor",
device_class=SensorDeviceClass.POWER_FACTOR,
entity_registry_enabled_default=False,
),
SensorEntityDescription(
key="voltage",
name="Voltage",
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
device_class=SensorDeviceClass.VOLTAGE,
entity_registry_enabled_default=False,
),
SensorEntityDescription(
key="frequency",
name="Frequency",
native_unit_of_measurement=UnitOfFrequency.HERTZ,
device_class=SensorDeviceClass.FREQUENCY,
entity_registry_enabled_default=False,
),
SensorEntityDescription(
key="consumption_Current",
name="Consumption Current",
native_unit_of_measurement="A",
device_class=SensorDeviceClass.CURRENT,
entity_registry_enabled_default=False,
),
SensorEntityDescription(
key="production_Current",
name="Production Current",
native_unit_of_measurement="A",
device_class=SensorDeviceClass.CURRENT,
entity_registry_enabled_default=False,
),
SensorEntityDescription(
key="active_inverter_count",
name="Active Inverter Count",
state_class=SensorStateClass.MEASUREMENT,
entity_registry_enabled_default=False,
),
)
BINARY_SENSORS = (
BinarySensorEntityDescription(
key="grid_status",
name="Grid Status",
device_class=BinarySensorDeviceClass.CONNECTIVITY,
),
)
PHASE_SENSORS = (
SensorEntityDescription(
key="production_l1",
name="Current Power Production L1",
native_unit_of_measurement=UnitOfPower.WATT,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.POWER,
),
SensorEntityDescription(
key="daily_production_l1",
name="Today's Energy Production L1",
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
state_class=SensorStateClass.TOTAL_INCREASING,
device_class=SensorDeviceClass.ENERGY,
),
SensorEntityDescription(
key="lifetime_production_l1",
name="Lifetime Energy Production L1",
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
state_class=SensorStateClass.TOTAL_INCREASING,
device_class=SensorDeviceClass.ENERGY,
),
SensorEntityDescription(
key="lifetime_net_production_l1",
name="Lifetime Net Energy Production L1",
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
state_class=SensorStateClass.TOTAL_INCREASING,
device_class=SensorDeviceClass.ENERGY,
),
SensorEntityDescription(
key="production_l2",
name="Current Power Production L2",
native_unit_of_measurement=UnitOfPower.WATT,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.POWER,
),
SensorEntityDescription(
key="daily_production_l2",
name="Today's Energy Production L2",
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
state_class=SensorStateClass.TOTAL_INCREASING,
device_class=SensorDeviceClass.ENERGY,
),
SensorEntityDescription(
key="lifetime_production_l2",
name="Lifetime Energy Production L2",
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
state_class=SensorStateClass.TOTAL_INCREASING,
device_class=SensorDeviceClass.ENERGY,
),
SensorEntityDescription(
key="lifetime_net_production_l2",
name="Lifetime Net Energy Production L2",
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
state_class=SensorStateClass.TOTAL_INCREASING,
device_class=SensorDeviceClass.ENERGY,
),
SensorEntityDescription(
key="production_l3",
name="Current Power Production L3",
native_unit_of_measurement=UnitOfPower.WATT,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.POWER,
),
SensorEntityDescription(
key="daily_production_l3",
name="Today's Energy Production L3",
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
state_class=SensorStateClass.TOTAL_INCREASING,
device_class=SensorDeviceClass.ENERGY,
),
SensorEntityDescription(
key="lifetime_production_l3",
name="Lifetime Energy Production L3",
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
state_class=SensorStateClass.TOTAL_INCREASING,
device_class=SensorDeviceClass.ENERGY,
),
SensorEntityDescription(
key="lifetime_net_production_l3",
name="Lifetime Net Energy Production L3",
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
state_class=SensorStateClass.TOTAL_INCREASING,
device_class=SensorDeviceClass.ENERGY,
),
SensorEntityDescription(
key="consumption_l1",
name="Current Power Consumption L1",
native_unit_of_measurement=UnitOfPower.WATT,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.POWER,
),
SensorEntityDescription(
key="net_consumption_l1",
name="Current Net Power Consumption L1",
native_unit_of_measurement=UnitOfPower.WATT,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.POWER,
),
SensorEntityDescription(
key="daily_consumption_l1",
name="Today's Energy Consumption L1",
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
state_class=SensorStateClass.TOTAL_INCREASING,
device_class=SensorDeviceClass.ENERGY,
),
SensorEntityDescription(
key="lifetime_consumption_l1",
name="Lifetime Energy Consumption L1",
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
state_class=SensorStateClass.TOTAL_INCREASING,
device_class=SensorDeviceClass.ENERGY,
),
SensorEntityDescription(
key="lifetime_net_consumption_l1",
name="Lifetime Net Energy Consumption L1",
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
state_class=SensorStateClass.TOTAL_INCREASING,
device_class=SensorDeviceClass.ENERGY,
),
SensorEntityDescription(
key="consumption_l2",
name="Current Power Consumption L2",
native_unit_of_measurement=UnitOfPower.WATT,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.POWER,
),
SensorEntityDescription(
key="net_consumption_l2",
name="Current Net Power Consumption L2",
native_unit_of_measurement=UnitOfPower.WATT,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.POWER,
),
SensorEntityDescription(
key="daily_consumption_l2",
name="Today's Energy Consumption L2",
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
state_class=SensorStateClass.TOTAL_INCREASING,
device_class=SensorDeviceClass.ENERGY,
),
SensorEntityDescription(
key="lifetime_consumption_l2",
name="Lifetime Energy Consumption L2",
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
state_class=SensorStateClass.TOTAL_INCREASING,
device_class=SensorDeviceClass.ENERGY,
),
SensorEntityDescription(
key="lifetime_net_consumption_l2",
name="Lifetime Net Energy Consumption L2",
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
state_class=SensorStateClass.TOTAL_INCREASING,
device_class=SensorDeviceClass.ENERGY,
),
SensorEntityDescription(
key="consumption_l3",
name="Current Power Consumption L3",
native_unit_of_measurement=UnitOfPower.WATT,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.POWER,
),
SensorEntityDescription(
key="net_consumption_l3",
name="Current Net Power Consumption L3",
native_unit_of_measurement=UnitOfPower.WATT,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.POWER,
),
SensorEntityDescription(
key="daily_consumption_l3",
name="Today's Energy Consumption L3",
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
state_class=SensorStateClass.TOTAL_INCREASING,
device_class=SensorDeviceClass.ENERGY,
),
SensorEntityDescription(
key="lifetime_consumption_l3",
name="Lifetime Energy Consumption L3",
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
state_class=SensorStateClass.TOTAL_INCREASING,
device_class=SensorDeviceClass.ENERGY,
),
SensorEntityDescription(
key="lifetime_net_consumption_l3",
name="Lifetime Net Energy Consumption L3",
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
state_class=SensorStateClass.TOTAL_INCREASING,
device_class=SensorDeviceClass.ENERGY,
),
SensorEntityDescription(
key="pf_l1",
name="Power Factor L1",
device_class=SensorDeviceClass.POWER_FACTOR,
entity_registry_enabled_default=False,
),
SensorEntityDescription(
key="pf_l2",
name="Power Factor L2",
device_class=SensorDeviceClass.POWER_FACTOR,
entity_registry_enabled_default=False,
),
SensorEntityDescription(
key="pf_l3",
name="Power Factor L3",
device_class=SensorDeviceClass.POWER_FACTOR,
entity_registry_enabled_default=False,
),
SensorEntityDescription(
key="voltage_l1",
name="Voltage L1",
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
device_class=SensorDeviceClass.VOLTAGE,
entity_registry_enabled_default=False,
),
SensorEntityDescription(
key="voltage_l2",
name="Voltage L2",
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
device_class=SensorDeviceClass.VOLTAGE,
entity_registry_enabled_default=False,
),
SensorEntityDescription(
key="voltage_l3",
name="Voltage L3",
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
device_class=SensorDeviceClass.VOLTAGE,
entity_registry_enabled_default=False,
),
SensorEntityDescription(
key="frequency_l1",
name="Frequency L1",
native_unit_of_measurement=UnitOfFrequency.HERTZ,
device_class=SensorDeviceClass.FREQUENCY,
entity_registry_enabled_default=False,
),
SensorEntityDescription(
key="frequency_l2",
name="Frequency L2",
native_unit_of_measurement=UnitOfFrequency.HERTZ,
device_class=SensorDeviceClass.FREQUENCY,
entity_registry_enabled_default=False,
),
SensorEntityDescription(
key="frequency_l3",
name="Frequency L3",
native_unit_of_measurement=UnitOfFrequency.HERTZ,
device_class=SensorDeviceClass.FREQUENCY,
entity_registry_enabled_default=False,
),
SensorEntityDescription(
key="consumption_Current_l1",
name="Consumption Current L1",
native_unit_of_measurement="A",
device_class=SensorDeviceClass.CURRENT,
entity_registry_enabled_default=False,
),
SensorEntityDescription(
key="consumption_Current_l2",
name="Consumption Current L2",
native_unit_of_measurement="A",
device_class=SensorDeviceClass.CURRENT,
entity_registry_enabled_default=False,
),
SensorEntityDescription(
key="consumption_Current_l3",
name="Consumption Current L3",
native_unit_of_measurement="A",
device_class=SensorDeviceClass.CURRENT,
entity_registry_enabled_default=False,
),
SensorEntityDescription(
key="production_Current_l1",
name="Production Current L1",
native_unit_of_measurement="A",
device_class=SensorDeviceClass.CURRENT,
entity_registry_enabled_default=False,
),
SensorEntityDescription(
key="production_Current_l2",
name="Production Current L2",
native_unit_of_measurement="A",
device_class=SensorDeviceClass.CURRENT,
entity_registry_enabled_default=False,
),
SensorEntityDescription(
key="production_Current_l3",
name="Production Current L3",
native_unit_of_measurement="A",
device_class=SensorDeviceClass.CURRENT,
entity_registry_enabled_default=False,
),
)
BATTERY_ENERGY_DISCHARGED_SENSOR = SensorEntityDescription(
key="battery_energy_discharged",
name="Battery Energy Discharged",
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
state_class=SensorStateClass.TOTAL,
device_class=SensorDeviceClass.ENERGY
)
BATTERY_ENERGY_CHARGED_SENSOR = SensorEntityDescription(
key="battery_energy_charged",
name="Battery Energy Charged",
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
state_class=SensorStateClass.TOTAL,
device_class=SensorDeviceClass.ENERGY
)
================================================
FILE: custom_components/enphase_envoy_custom/diagnostics.py
================================================
"""Diagnostics support for Enphase Envoy."""
from __future__ import annotations
from typing import Any
from attr import asdict
from homeassistant.components.diagnostics import async_redact_data
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from .const import COORDINATOR, DOMAIN
CONF_TITLE = "title"
TO_REDACT = {
# CONF_NAME,
CONF_PASSWORD,
# Config entry title and unique ID may contain sensitive data:
# CONF_TITLE,
# CONF_UNIQUE_ID,
CONF_USERNAME,
}
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, entry: ConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
coordinator: DataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][COORDINATOR]
device_registry = dr.async_get(hass)
entity_registry = er.async_get(hass)
devices = []
registry_devices = dr.async_entries_for_config_entry(
device_registry, entry.entry_id
)
for device in registry_devices:
entities = []
registry_entities = er.async_entries_for_device(
entity_registry,
device_id=device.id,
include_disabled_entities=True,
)
for entity in registry_entities:
state_dict = None
if state := hass.states.get(entity.entity_id):
state_dict = dict(state.as_dict())
state_dict.pop("context", None)
entities.append({"entry": asdict(entity), "state": state_dict})
devices.append({"device": asdict(device), "entities": entities})
return async_redact_data(
{
"entry": entry.as_dict(),
"data": coordinator.data,
"Note": "Entities that show as null are not available for your Envoy",
"devices": devices,
},
TO_REDACT,
)
================================================
FILE: custom_components/enphase_envoy_custom/envoy_reader.py
================================================
"""Module to read production and consumption values from an Enphase Envoy on the local network."""
import argparse
import datetime
import logging
import time
from json.decoder import JSONDecodeError
import json
from ipaddress import IPv4Address, IPv6Address
import sys
import getpass
#Modules not in standard Python Library - add to manifest requirements
import re
import jwt
import asyncio
import httpx
import xmltodict
from envoy_utils.envoy_utils import EnvoyUtils
#
# Legacy parser is only used on ancient firmwares
#
PRODUCTION_REGEX = r"<td>Currentl.*</td>\s+<td>\s*(\d+|\d+\.\d+)\s*(W|kW|MW)</td>"
DAY_PRODUCTION_REGEX = r"<td>Today</td>\s+<td>\s*(\d+|\d+\.\d+)\s*(Wh|kWh|MWh)</td>"
WEEK_PRODUCTION_REGEX = (
r"<td>Past Week</td>\s+<td>\s*(\d+|\d+\.\d+)\s*(Wh|kWh|MWh)</td>"
)
LIFE_PRODUCTION_REGEX = (
r"<td>Since Installation</td>\s+<td>\s*(\d+|\d+\.\d+)\s*(Wh|kWh|MWh)</td>"
)
SERIAL_REGEX = re.compile(r"Envoy\s*Serial\s*Number:\s*([0-9]+)")
ACTIVE_INVERTER_COUNT_REGEX = r"<td>Number of Microinverters Online</td>\s*<td>\s*(\d*)\s*</td>"
ENDPOINT_URL_PRODUCTION_JSON = "http{}://{}/production.json?details=1"
ENDPOINT_URL_PRODUCTION_V1 = "http{}://{}/api/v1/production"
ENDPOINT_URL_PRODUCTION_INVERTERS = "http{}://{}/api/v1/production/inverters"
ENDPOINT_URL_PRODUCTION = "http{}://{}/production"
ENDPOINT_URL_CHECK_JWT = "https://{}/auth/check_jwt"
ENDPOINT_URL_ENSEMBLE_INVENTORY = "http{}://{}/ivp/ensemble/inventory"
ENDPOINT_URL_HOME_JSON = "http{}://{}/home.json"
ENDPOINT_URL_HOME = "http{}://{}/home"
ENDPOINT_URL_INFO_XML = "http{}://{}/info"
ENDPOINT_URL_METERS = "http{}://{}/ivp/meters"
ENDPOINT_URL_METERS_REPORTS = "http{}://{}/ivp/meters/reports"
ENDPOINT_URL_METERS_READINGS = "http{}://{}/ivp/meters/readings"
# pylint: disable=pointless-string-statement
ENVOY_MODEL_S = "PC"
ENVOY_MODEL_C = "P"
ENVOY_MODEL_LEGACY = "P0"
LOGIN_URL = "https://entrez.enphaseenergy.com/login_main_page"
TOKEN_URL = "https://entrez.enphaseenergy.com/entrez_tokens"
# paths for the enlighten 1 year owner token
ENLIGHTEN_AUTH_URL = "https://enlighten.enphaseenergy.com/login/login.json"
ENLIGHTEN_TOKEN_URL = "https://entrez.enphaseenergy.com/tokens"
_LOGGER = logging.getLogger(__name__)
def has_production_and_consumption(json):
"""Check if json has keys for both production and consumption."""
return "production" in json and "consumption" in json
def has_metering_setup(json):
"""Check if Active Count of Production CTs (eim) installed is greater than one."""
return json["production"][1]["activeCount"] > 0
def has_production_metering_setup(json):
"""Check if Production CTs (eim) are installed."""
return json[0]["state"] == "enabled"
def has_consumption_metering_setup(json):
"""Check if Consumption CTs (eim) are installed."""
return json[1]["state"] == "enabled"
def has_net_consumption_meters_type(json):
"""Check if Consumption measurement type is net-consumption."""
return json[1]["measurementType"] == "net-consumption"
def get_production_meters_phase_count(json):
"""Get Count of Production CTs (eim) installed."""
return json[0]["phaseCount"]
def get_consumption_meters_phase_count(json):
"""Get Count of Consumption CTs (eim) installed."""
return json[1]["phaseCount"]
def is_ipv6_address(address: str) -> bool:
"""Check if a given string is an IPv6 address."""
try:
IPv6Address(address)
except ValueError:
return False
return True
class SwitchToHTTPS(Exception):
pass
class EnvoyReader: # pylint: disable=too-many-instance-attributes
"""Instance of EnvoyReader"""
# P0 for older Envoy model C, s/w < R3.9 no json pages
# P for production data only (ie. Envoy model C, s/w >= R3.9)
# PC for production and consumption data (ie. Envoy model S)
message_battery_not_available = (
"Battery storage data not available for your Envoy device."
)
message_production_not_available = (
"CTs production data not available for your Envoy device."
)
message_consumption_not_available = (
"CTs consumption data not available for your Envoy device."
)
message_grid_status_not_available = (
"Grid status not available for your Envoy device."
)
message_frequency_not_available = (
"Frequency data not available for your Envoy device."
)
message_voltage_not_available = (
"Voltage data not available for your Envoy device."
)
message_pf_not_available = (
"Power Factor data not available for your Envoy device."
)
message_current_consumption_not_available = (
"Amps consumption data not available for your Envoy device."
)
message_current_production_not_available = (
"Amps production data not available for your Envoy device."
)
message_active_inverters_not_available = (
"Active Inverter count not available for your Envoy device."
)
def __init__( # pylint: disable=too-many-arguments
self,
host,
username="envoy",
password="",
inverters=False,
async_client=None,
enlighten_user=None,
enlighten_pass=None,
commissioned=False,
enlighten_site_id=None,
enlighten_serial_num=None,
https_flag="",
use_enlighten_owner_token=False,
token_refresh_buffer_seconds=0,
store=None,
info_refresh_buffer_seconds=3600,
fetch_timeout_seconds=30,
fetch_holdoff_seconds=0,
fetch_retries=1,
do_not_use_production_json=False,
):
"""Init the EnvoyReader."""
self.host = host.lower().replace('[','').replace(']','')
# IPv6 addresses need to be enclosed in brackets
if is_ipv6_address(self.host):
self.host = f"[{self.host}]"
self.username = username
self.password = password
self.get_inverters = inverters
self.endpoint_type = None
self.has_grid_status = True
self.serial_number_last_six = None
self.endpoint_meters_reports_json_results = None
self.endpoint_meters_readings_json_results = None
self.endpoint_production_json_results = None
self.endpoint_production_v1_results = None
self.endpoint_production_inverters = None
self.endpoint_production_results = None
self.endpoint_ensemble_json_results = None
self.endpoint_home_json_results = None
self.endpoint_home_results = None
self.isProductionMeteringEnabled = False # pylint: disable=invalid-name
self.isConsumptionMeteringEnabled = False # pylint: disable=invalid-name
self.net_consumption_meters_type = False
self.production_meters_phase_count = 0
self.consumption_meters_phase_count = 0
self._async_client = async_client
self._authorization_header = None
self._cookies = None
self.enlighten_user = enlighten_user
self.enlighten_pass = enlighten_pass
self.commissioned = commissioned
self.enlighten_site_id = enlighten_site_id
self.enlighten_serial_num = enlighten_serial_num
self.https_flag = https_flag
self.use_enlighten_owner_token = use_enlighten_owner_token
self.token_refresh_buffer_seconds = token_refresh_buffer_seconds
self.endpoint_info_results = None
self.endpoint_meters_json_results = None
self.info_refresh_buffer_seconds = info_refresh_buffer_seconds
self.info_next_refresh_time = datetime.datetime.now()
self.meters_next_refresh_time = datetime.datetime.now()
self._store = store
self._store_data = {}
self._store_update_pending = False
self._fetch_timeout_seconds = fetch_timeout_seconds
self._fetch_holdoff_seconds = fetch_holdoff_seconds
self._fetch_retries = max(fetch_retries,1)
self._do_not_use_production_json=do_not_use_production_json
@property
def _token(self):
return self._store_data.get("token", "")
@_token.setter
def _token(self, token_value):
self._store_data["token"] = token_value
self._store_update_pending = True
async def _sync_store(self):
if self._store and not self._store_data:
self._store_data = await self._store.async_load() or {}
if self._store and self._store_update_pending:
self._store_update_pending = False
await self._store.async_save(self._store_data)
@property
def async_client(self):
"""Return the httpx client."""
return self._async_client or httpx.AsyncClient(verify=False,
headers=self._authorization_header,
cookies=self._cookies)
@property
def non_local_async_client(self):
"""Return the httpx client for non-local usage."""
return self._async_client or httpx.AsyncClient(verify=True,
headers=self._authorization_header,
cookies=self._cookies)
async def _update(self):
"""Update the data."""
_LOGGER.debug("_update running")
if self.endpoint_type == ENVOY_MODEL_S:
await self._update_meters_endpoint()
await self._update_from_pc_endpoint()
if self.endpoint_type == ENVOY_MODEL_C or (
self.endpoint_type == ENVOY_MODEL_S and not self.isProductionMeteringEnabled
):
await self._update_from_p_endpoint()
if self.endpoint_type == ENVOY_MODEL_LEGACY:
await self._update_from_p0_endpoint()
await self._update_info_endpoint()
async def _update_from_meters_reports_endpoint(self):
"""Update from ivp/meters endpoint."""
if self.endpoint_type == ENVOY_MODEL_S:
#only touch meters reports if confirmed envoy model S, other type choke up on this request
await self._update_endpoint(
"endpoint_meters_reports_json_results", ENDPOINT_URL_METERS_REPORTS
)
async def _update_from_meters_readings_endpoint(self):
"""Update from ivp/meters/readings endpoint."""
if self.endpoint_type == ENVOY_MODEL_S:
#only touch meters reports if confirmed envoy model S, other type choke up on this request
await self._update_endpoint(
"endpoint_meters_readings_json_results", ENDPOINT_URL_METERS_READINGS
)
async def _update_from_pc_endpoint(self,detectmode=False):
"""Update from PC endpoint."""
if not self._do_not_use_production_json or detectmode:
await self._update_endpoint(
"endpoint_production_json_results", ENDPOINT_URL_PRODUCTION_JSON
)
await self._update_endpoint(
"endpoint_ensemble_json_results", ENDPOINT_URL_ENSEMBLE_INVENTORY
)
if self.has_grid_status:
await self._update_endpoint(
"endpoint_home_json_results", ENDPOINT_URL_HOME_JSON
)
async def _update_from_p_endpoint(self):
"""Update from P endpoint."""
await self._update_endpoint(
"endpoint_production_v1_results", ENDPOINT_URL_PRODUCTION_V1
)
async def _update_from_p0_endpoint(self):
"""Update from P0 endpoint."""
await self._update_endpoint(
"endpoint_production_results", ENDPOINT_URL_PRODUCTION
)
await self._update_endpoint(
"endpoint_home_results", ENDPOINT_URL_HOME
)
async def _update_info_endpoint(self):
"""Update from info endpoint if next time expired."""
if self.info_next_refresh_time <= datetime.datetime.now():
await self._update_endpoint("endpoint_info_results", ENDPOINT_URL_INFO_XML)
self.info_next_refresh_time = datetime.datetime.now() + datetime.timedelta(
seconds=self.info_refresh_buffer_seconds
)
_LOGGER.debug(
"Info endpoint updated, set next update time: %s using interval: %s",
self.info_next_refresh_time,
self.info_refresh_buffer_seconds,
)
else:
_LOGGER.debug(
"Info endpoint next update time is: %s using interval: %s",
self.info_next_refresh_time,
self.info_refresh_buffer_seconds,
)
async def _update_meters_endpoint(self):
"""Update from meters endpoint if next time expried."""
if self.meters_next_refresh_time <= datetime.datetime.now():
await self._update_endpoint("endpoint_meters_json_results", ENDPOINT_URL_METERS)
#some devices return [] for ivp/meters
if self.endpoint_meters_json_results and self.endpoint_meters_json_results.text != "[]":
self.isProductionMeteringEnabled = has_production_metering_setup(
self.endpoint_meters_json_results.json()
)
self.isConsumptionMeteringEnabled = has_consumption_metering_setup(
self.endpoint_meters_json_results.json()
)
self.net_consumption_meters_type = has_net_consumption_meters_type(
self.endpoint_meters_json_results.json()
)
self.production_meters_phase_count = get_production_meters_phase_count(
self.endpoint_meters_json_results.json()
)
self.consumption_meters_phase_count = get_consumption_meters_phase_count(
self.endpoint_meters_json_results.json()
)
self.meters_next_refresh_time = datetime.datetime.now() + datetime.timedelta(
seconds=self.info_refresh_buffer_seconds
)
_LOGGER.debug(
"Meters endpoint updated, set next update time: %s using interval: %s",
self.meters_next_refresh_time,
self.info_refresh_buffer_seconds,
)
else:
_LOGGER.debug(
"Meters endpoint next update time is: %s using interval: %s",
self.meters_next_refresh_time,
self.info_refresh_buffer_seconds,
)
await self._update_from_meters_reports_endpoint()
await self._update_from_meters_readings_endpoint()
async def _update_endpoint(self, attr, url):
"""Update a property from an endpoint."""
formatted_url = url.format(self.https_flag, self.host)
response = await self._async_fetch_with_retry(
formatted_url, follow_redirects=False
)
setattr(self, attr, response)
async def _async_fetch_with_retry(self, url, **kwargs):
"""Retry 3 times to fetch the url if there is a transport error."""
for attempt in range(self._fetch_retries + 1):
header = " <Blank Header> "
if self._authorization_header:
header = " <Token hidden> "
_LOGGER.debug(
"HTTP GET Attempt #%s of %s: %s: use token: %s: Header:%s Timeout: %s Holdoff: %s",
attempt + 1,
self._fetch_retries + 1,
url,
self.use_enlighten_owner_token,
header,
self._fetch_timeout_seconds,
self._fetch_holdoff_seconds,
)
async with self.async_client as client:
try:
getstart = time.time()
resp = await client.get(
url, headers=self._authorization_header, timeout=self._fetch_timeout_seconds, **kwargs
)
getend = time.time()
if resp.status_code == 401 and attempt < self._fetch_retries:
if self.use_enlighten_owner_token:
_LOGGER.debug(
"Received 401 from Envoy; refreshing cookies, in attempt %s of %s:",
attempt+1,
self._fetch_retries + 1
)
could_refresh_cookies = await self._refresh_token_cookies()
if not could_refresh_cookies:
_LOGGER.debug(
"cookie refresh failed, getting token, in attempt %s of %s:",
attempt+1,
self._fetch_retries + 1
)
await self._getEnphaseToken()
continue
# don't try token and cookies refresh for legacy envoy
else:
_LOGGER.debug(
"Received 401 from Envoy; retrying, attempt %s of %s",
attempt+1,
self._fetch_retries + 1
)
continue
_LOGGER.debug("Fetched (%s of %s) in %s sec from %s: %s: %s",
attempt + 1,
self._fetch_retries + 1,
round(getend - getstart,1),
url,
resp,
resp.text
)
if resp.status_code == 404:
return None
return resp
except httpx.TimeoutException as exc:
if attempt == self._fetch_retries:
_LOGGER.warning("HTTP Timeout in fetch_with_retry, raising: %s",exc)
raise
# Sleep a bit and try once more
_LOGGER.warning("HTTP Timeout in fetch_with_retry, waiting %s sec: %s",self._fetch_holdoff_seconds,exc)
await asyncio.sleep(self._fetch_holdoff_seconds)
except Exception as exc:
if attempt == self._fetch_retries:
_LOGGER.warning("Error in fetch_with_retry, raising: %s",exc)
raise
# Sleep a bit and try once more
_LOGGER.warning("Error in fetch_with_retry, waiting %s sec: %s",self._fetch_holdoff_seconds,exc)
await asyncio.sleep(self._fetch_holdoff_seconds)
async def _async_post(self, url, data, cookies=None, client=None, **kwargs):
_LOGGER.debug("HTTP POST Attempt: %s", url)
if client is None:
client = self.async_client
# _LOGGER.debug("HTTP POST Data: %s", data)
try:
async with client:
resp = await client.post(
url, cookies=cookies, data=data, timeout=30, **kwargs
)
_LOGGER.debug("HTTP POST %s: %s: %s", url, resp, resp.text)
_LOGGER.debug("HTTP POST Cookie: %s", resp.cookies)
return resp
except httpx.TransportError: # pylint: disable=try-except-raise
raise
async def _fetch_owner_token_json(self) :
"""Try to fetch the owner token json from Enlighten API"""
async with self.non_local_async_client as client:
# login to the enlighten website
payload_login = {
'user[email]': self.enlighten_user,
'user[password]': self.enlighten_pass,
}
resp = await client.post(ENLIGHTEN_AUTH_URL, data=payload_login, timeout=30)
if resp.status_code >= 400:
raise RuntimeError(f"Could not Authenticate with Enlighten, status: {resp.status_code}, {resp}")
# now that we're in a logged in session, we can request the 1 year owner token via enlighten
login_data = resp.json()
payload_token = {
"session_id": login_data["session_id"],
"serial_num": self.enlighten_serial_num,
"username": self.enlighten_user,
}
resp = await client.post(
ENLIGHTEN_TOKEN_URL, json=payload_token, timeout=30
)
if resp.status_code != 200:
raise RuntimeError(f"Could not get enlighten token, status: {resp.status_code}, {resp}")
return resp.text
async def _getEnphaseToken(self):
self._token = await self._fetch_owner_token_json()
_LOGGER.debug("Obtained Token")
if self._is_enphase_token_expired(self._token):
raise RuntimeError("Just received token already expired")
await self._refresh_token_cookies()
async def _refresh_token_cookies(self):
"""
Refresh the client's cookie with the token (if valid)
:returns True if cookie refreshed, False if it couldn't be
"""
# Create HTTP Header
self._authorization_header = {"Authorization": "Bearer " + self._token}
# Fetch the Enphase Token status from the local Envoy
token_validation = await self._async_fetch_with_retry(
ENDPOINT_URL_CHECK_JWT.format(self.host)
)
if token_validation.status_code == 200:
# set the cookies for future clients
self._cookies = token_validation.cookies
return True
# token not valid if we get here
return False
def _is_enphase_token_valid(self, response):
if response == "Valid token.":
_LOGGER.debug("Token is valid")
return True
else:
_LOGGER.debug("Invalid token!")
return False
def _is_enphase_token_expired(self, token):
decode = jwt.decode(
token, options={"verify_signature": False}, algorithms="ES256"
)
exp_epoch = decode["exp"]
# allow a buffer so we can try and grab it sooner
exp_epoch -= self.token_refresh_buffer_seconds
exp_time = datetime.datetime.fromtimestamp(exp_epoch)
if datetime.datetime.now() < exp_time:
_LOGGER.debug("Token expires at: %s", exp_time)
return False
else:
_LOGGER.debug("Token expired on: %s", exp_time)
return True
async def check_connection(self):
"""Check if the Envoy is reachable. Also check if HTTP or"""
"""HTTPS is needed."""
_LOGGER.debug("Checking Host: %s", self.host)
resp = await self._async_fetch_with_retry(
ENDPOINT_URL_PRODUCTION_V1.format(self.https_flag, self.host)
)
_LOGGER.debug("Check connection HTTP Code: %s", resp.status_code)
if resp.status_code == 301:
raise SwitchToHTTPS
async def getData(self, getInverters=True): # pylint: disable=invalid-name
"""Fetch data from the endpoint and if inverters selected default"""
"""to fetching inverter data."""
# Check if the Secure flag is set
if self.https_flag == "s":
_LOGGER.debug(
"Checking Token value: %s (Only first 10 characters shown)",
self._token[1:10],
)
# Check if a token has already been retrieved
if self._token == "":
_LOGGER.debug("Found empty token: %s", self._token)
await self._getEnphaseToken()
else:
_LOGGER.debug(
"Token is populated: %s (Only first 10 characters shown)",
self._token[1:10],
)
if self._is_enphase_token_expired(self._token):
_LOGGER.debug("Found Expired token - Retrieving new token")
await self._getEnphaseToken()
if not self.endpoint_type:
await self.detect_model()
else:
await self._update()
_LOGGER.debug(
"Using Model: %s (HTTP%s, Production Metering: %s phases: %s, Consumption Metering: %s phases: %s, Net consumption CT: %s, Get Inverters: %s)",
self.endpoint_type,
self.https_flag,
self.isProductionMeteringEnabled,
self.production_meters_phase_count,
self.isConsumptionMeteringEnabled,
self.consumption_meters_phase_count,
self.net_consumption_meters_type,
self.get_inverters
)
if not self.get_inverters or not getInverters:
return
inverters_url = ENDPOINT_URL_PRODUCTION_INVERTERS.format(
self.https_flag, self.host
)
if self.use_enlighten_owner_token:
response = await self._async_fetch_with_retry(inverters_url)
else:
# Inverter page on envoy with old firmware requires username/password
inverters_auth = httpx.DigestAuth(self.username, self.password)
response = await self._async_fetch_with_retry(
inverters_url, auth=inverters_auth
)
if response.status_code in [401,404]:
if self.endpoint_type in [ENVOY_MODEL_C, ENVOY_MODEL_LEGACY]:
self.get_inverters = False
_LOGGER.debug("Error %s in Getdata for getting invertors, disabling inverters",response.status_code)
return
response.raise_for_status()
self.endpoint_production_inverters = response
return
async def detect_model(self):
"""Method to determine if the Envoy supports consumption values or only production."""
# If a password was not given as an argument when instantiating
# the EnvoyReader object than use the last six numbers of the serial
# number as the password. Otherwise use the password argument value.
_LOGGER.debug("Detect Model running")
if self.password == "" and not self.serial_number_last_six:
await self.get_serial_number()
try:
await self._update_from_pc_endpoint(detectmode=True)
except httpx.HTTPError:
pass
# If self.endpoint_production_json_results.status_code is set with
# 401 then we will give an error
if (
self.endpoint_production_json_results
and self.endpoint_production_json_results.status_code == 401
):
raise RuntimeError(
"Could not connect to Envoy model. "
+ "Appears your Envoy is running firmware that requires secure communcation. "
+ "Please enter in the needed Enlighten credentials during setup."
)
await self._update_info_endpoint()
if (
self.endpoint_production_json_results
and self.endpoint_production_json_results.status_code == 200
and has_production_and_consumption(
self.endpoint_production_json_results.json()
)
):
_LOGGER.debug("Detect Model found production and consumption")
#only access meters endpoint if envoy metered, other type may choke up
self.endpoint_type = ENVOY_MODEL_S
await self._update_meters_endpoint()
if not self.isProductionMeteringEnabled:
await self._update_from_p_endpoint()
return
try:
await self._update_from_p_endpoint()
except httpx.HTTPError:
pass
if (
self.endpoint_production_v1_results
and self.endpoint_production_v1_results.status_code == 200
):
self.endpoint_type = ENVOY_MODEL_C # Envoy-C, production only
return
try:
await self._update_from_p0_endpoint()
except httpx.HTTPError:
pass
if (
self.endpoint_production_results
and self.endpoint_production_results.status_code == 200
):
self.endpoint_type = ENVOY_MODEL_LEGACY # older Envoy-C
self.get_inverters = False # don't get inverters for this model
return
raise RuntimeError(
"Could not connect or determine Envoy model. "
+ "Check that the device is up at 'http://"
+ self.host
+ "'."
)
async def get_serial_number(self):
"""Method to get last six digits of Envoy serial number for auth"""
full_serial = await self.get_full_serial_number()
if full_serial:
gen_passwd = EnvoyUtils.get_password(full_serial, self.username)
if self.username == "envoy" or self.username != "installer":
self.password = self.serial_number_last_six = full_serial[-6:]
else:
self.password = gen_passwd
async def get_full_serial_number(self):
"""Method to get the Envoy serial number."""
response = await self._async_fetch_with_retry(
f"http{self.https_flag}://{self.host}/info.xml",
follow_redirects=True,
)
if not response.text:
return None
if "<sn>" in response.text:
return response.text.split("<sn>")[1].split("</sn>")[0]
match = SERIAL_REGEX.search(response.text)
if match:
# if info.xml is in html format we're dealing with ENVOY R
_LOGGER.debug("Legacy model identified by info.xml being html. Disabling inverters")
self.get_inverters = False
return match.group(1)
def create_connect_errormessage(self):
"""Create error message if unable to connect to Envoy"""
return (
"Unable to connect to Envoy. "
+ "Check that the device is up at 'http://"
+ self.host
+ "'."
)
def create_json_errormessage(self):
"""Create error message if unable to parse JSON response"""
return (
"Got a response from '"
+ self.host
+ "', but metric could not be found. "
+ "Maybe your model of Envoy doesn't "
+ "support the requested metric."
)
async def _meters_readings_value(self,field,report="net-consumption",phase=None):
"""Extract value from meters readings json"""
report_map = {"production": 0, "net-consumption": 1, "total-consumption": 1}
phase_map = {"l1": 0, "l2": 1, "l3": 2}
#meters readings is only available for ENVOY Metered with CT configured
if (self.endpoint_type == ENVOY_MODEL_S) and (
#net-consumption requires consumption CT installed is Solar power included mode
(report == "net-consumption"
and self.isConsumptionMeteringEnabled
and self.net_consumption_meters_type)
# production data requires production CT installed
or (report == "production" and self.isProductionMeteringEnabled )
#if at least consumption CT is installed total-consumption will be available even in Load only mode install
or (report == "total-consumption"
and self.isConsumptionMeteringEnabled
and not self.net_consumption_meters_type )
):
if self.endpoint_meters_readings_json_results:
raw_json = self.endpoint_meters_readings_json_results.json()
if phase == None:
try:
jsondata = raw_json[report_map[report]][field]
return jsondata
except (KeyError, IndexError):
return None
#if production data requested and multiple phases are configured and requested phase is in count of configured phases return data or
#if consumption data requested and multiple phases are configured and requested phase is in count of configured phases return date
if ((self.production_meters_phase_count > 1 and phase_map[phase] < self.production_meters_phase_count and report=="production")
or (self.consumption_meters_phase_count > 1 and phase_map[phase] < self.consumption_meters_phase_count and report!="production")):
try:
jsondata = raw_json[report_map[report]]["channels"][phase_map[phase]][field]
return jsondata
except (KeyError, IndexError):
return None
return None
async def _meters_report_value(self,field,report="production",phase=None):
"""Extract value from meters reports json if consumption meter is available"""
report_map = {"production": 0, "net-consumption": 1, "total-consumption": 2}
phase_map = {"l1": 0, "l2": 1, "l3": 2}
#meters reports is only available for ENVOY Metered with CT configured
if (self.endpoint_type == ENVOY_MODEL_S) and (
#net-consumption requires consumption CT installed is Solar power included mode
(report == "net-consumption" and self.isConsumptionMeteringEnabled and self.net_consumption_meters_type)
# production data requires production CT installed
or (report == "production" and self.isProductionMeteringEnabled)
#if at least consumption CT is installed total-consumption will be available even in Load only mode install
or (report == "total-consumption" and self.isConsumptionMeteringEnabled)
):
if self.endpoint_meters_reports_json_results:
raw_json = self.endpoint_meters_reports_json_results.json()
if phase == None:
jsondata = raw_json[report_map[report]]["cumulative"][field]
return jsondata
#if production data requested and multiple phases are configured and requested phase is in count of configured phases return data or
#if consumption data requested and multiple phases are configured and requested phase is in count of configured phases return date
if ((self.production_meters_phase_count > 1 and phase_map[phase] < self.production_meters_phase_count and report=="production")
or (self.consumption_meters_phase_count > 1 and phase_map[phase] < self.consumption_meters_phase_count and report!="production")):
try:
jsondata = raw_json[report_map[report]]["lines"][phase_map[phase]][field]
return jsondata
except (KeyError, IndexError):
return None
return None
async def production(self,phase=None):
"""Report System or Phase Power Production data from sources for various Envoy types"""
if phase is not None:
# if phase is specified return phase data rather then system data
return await self.production_phase(phase)
if self.endpoint_type == ENVOY_MODEL_S:
if self.isProductionMeteringEnabled:
raw_json = self.endpoint_meters_reports_json_results.json()
production = raw_json[0]["cumulative"]["currW"]
else:
raw_json = self.endpoint_production_json_results.json()
production = raw_json["production"][0]["wNow"]
elif self.endpoint_type == ENVOY_MODEL_C:
raw_json = self.endpoint_production_v1_results.json()
production = raw_json["wattsNow"]
elif self.endpoint_type == ENVOY_MODEL_LEGACY:
text = self.endpoint_production_results.text
match = re.search(PRODUCTION_REGEX, text, re.MULTILINE)
if match:
if match.group(2) == "kW":
production = float(match.group(1)) * 1000
else:
if match.group(2) == "mW":
production = float(match.group(1)) * 1000000
else:
production = float(match.group(1))
else:
raise RuntimeError("No match for production, check REGEX " + text)
return int(production)
async def production_phase(self, phase):
"""Report Phase Power Production data from meters report json"""
jsondata = await self._meters_report_value("currW",report="production",phase=phase)
if jsondata is None:
return self.message_consumption_not_available if phase is None else None
return int(jsondata)
async def consumption(self,phase=None):
"""Report cumulative or phase Power consumption (to house) from consumption CT meters report"""
jsondata = await self._meters_report_value("currW",report="total-consumption",phase=phase)
if jsondata is None:
return self.message_consumption_not_available if phase is None else None
return int(jsondata)
async def net_consumption(self,phase=None):
"""Report cumulative or phase Power consumption (to/from grid) from consumption CT meters report"""
jsondata = await self._meters_readings_value("instantaneousDemand",report="net-consumption",phase=phase)
if jsondata is None:
return self.message_consumption_not_available if phase is None else None
return int(jsondata)
async def daily_production(self,phase=None):
"""Report System or Phase Daily energy Production data from sources for various Envoy types"""
if phase is not None:
# if phase is specified return phase data rather then system data
return await self.daily_production_phase(phase)
if self.endpoint_type == ENVOY_MODEL_S and self.isProductionMeteringEnabled:
if self._do_not_use_production_json:
return self.message_production_not_available
raw_json = self.endpoint_production_json_results.json()
daily_production = raw_json["production"][1]["whToday"]
elif self.endpoint_type == ENVOY_MODEL_C or (
self.endpoint_type == ENVOY_MODEL_S and not self.isProductionMeteringEnabled
):
raw_json = self.endpoint_production_v1_results.json()
daily_production = raw_json["wattHoursToday"]
elif self.endpoint_type == ENVOY_MODEL_LEGACY:
text = self.endpoint_production_results.text
match = re.search(DAY_PRODUCTION_REGEX, text, re.MULTILINE)
if match:
if match.group(2) == "kWh":
daily_production = float(match.group(1)) * 1000
else:
if match.group(2) == "MWh":
daily_production = float(match.group(1)) * 1000000
else:
daily_production = float(match.group(1))
else:
raise RuntimeError(
"No match for Day production, " "check REGEX " + text
)
return int(daily_production)
async def daily_production_phase(self, phase):
"""Report Phase Daily energy Production data from production json"""
phase_map = {"l1": 0,"l2": 1,"l3": 2}
if (self.endpoint_type == ENVOY_MODEL_S and self.isProductionMeteringEnabled and
self.production_meters_phase_count > 1 and phase_map[phase] < self.production_meters_phase_count
and not self._do_not_use_production_json):
raw_json = self.endpoint_production_json_results.json()
try:
return int(
raw_json["production"][1]["lines"][phase_map[phase]]["whToday"]
)
except (KeyError, IndexError):
return None
return None
async def daily_consumption(self,phase=None):
"""Report System or Phase Daily energy Consumption data from production json"""
if phase is not None:
# if phase is specified return phase data rather then system data
return await self.daily_consumption_phase(phase)
"""Only return data if Envoy supports Consumption"""
if self.endpoint_type == ENVOY_MODEL_S and self.isConsumptionMeteringEnabled:
if self._do_not_use_production_json:
return self.message_consumption_not_available
raw_json = self.endpoint_production_json_results.json()
daily_consumption = raw_json["consumption"][0]["whToday"]
return int(daily_consumption)
return self.message_consumption_not_available
async def daily_consumption_phase(self, phase):
"""Report Phase Daily energy Consumption data from production json"""
phase_map = {"l1": 0,"l2": 1,"l3": 2}
"""Only return data if Envoy supports Consumption"""
if (self.endpoint_type == ENVOY_MODEL_S and self.isConsumptionMeteringEnabled and
self.consumption_meters_phase_count > 1 and phase_map[phase] < self.consumption_meters_phase_count):
if self._do_not_use_production_json:
return None
raw_json = self.endpoint_production_json_results.json()
try:
return int(
raw_json["consumption"][0]["lines"][phase_map[phase]]["whToday"]
)
except (KeyError, IndexError):
return None
return None
async def seven_days_production(self):
"""Report Last seven day energy production data from production json"""
if self.endpoint_type == ENVOY_MODEL_S and self.isProductionMeteringEnabled:
if self._do_not_use_production_json:
return self.message_production_not_available
raw_json = self.endpoint_production_json_results.json()
seven_days_production = raw_json["production"][1]["whLastSevenDays"]
elif self.endpoint_type == ENVOY_MODEL_C or (
self.endpoint_type == ENVOY_MODEL_S and not self.isProductionMeteringEnabled
):
raw_json = self.endpoint_production_v1_results.json()
seven_days_production = raw_json["wattHoursSevenDays"]
elif self.endpoint_type == ENVOY_MODEL_LEGACY:
text = self.endpoint_production_results.text
match = re.search(WEEK_PRODUCTION_REGEX, text, re.MULTILINE)
if match:
if match.group(2) == "kWh":
seven_days_production = float(match.group(1)) * 1000
else:
if match.group(2) == "MWh":
seven_days_production = float(match.group(1)) * 1000000
else:
seven_days_production = float(match.group(1))
else:
raise RuntimeError(
"No match for 7 Day production, " "check REGEX " + text
)
return int(seven_days_production)
async def seven_days_consumption(self):
"""Report Last seven day energy consumption data from production json"""
"""Only return data if Envoy supports Consumption"""
if self.endpoint_type == ENVOY_MODEL_S and self.isConsumptionMeteringEnabled:
if self._do_not_use_production_json:
return self.message_production_not_available
raw_json = self.endpoint_production_json_results.json()
seven_days_consumption = raw_json["consumption"][0]["whLastSevenDays"]
return int(seven_days_consumption)
return self.message_consumption_not_available
async def lifetime_production(self,phase=None):
"""Report system or Phase lifetime Energy production from sources for various Envoy types"""
if phase is not None:
# if phase is specified return phase data rather then system data
return await self.lifetime_production_phase(phase)
if self.endpoint_type == ENVOY_MODEL_S:
if self.isProductionMeteringEnabled:
raw_json = self.endpoint_meters_reports_json_results.json()
lifetime_production = raw_json[0]["cumulative"]["whDlvdCum"]
else:
raw_json = self.endpoint_production_json_results.json()
lifetime_production = raw_json["production"][0]["whLifetime"]
elif self.endpoint_type == ENVOY_MODEL_C:
raw_json = self.endpoint_production_v1_results.json()
lifetime_production = raw_json["wattHoursLifetime"]
elif self.endpoint_type == ENVOY_MODEL_LEGACY:
text = self.endpoint_production_results.text
match = re.search(LIFE_PRODUCTION_REGEX, text, re.MULTILINE)
if match:
if match.group(2) == "kWh":
lifetime_production = float(match.group(1)) * 1000
else:
if match.group(2) == "MWh":
lifetime_production = float(match.group(1)) * 1000000
else:
lifetime_production = float(match.group(1))
else:
raise RuntimeError(
"No match for Lifetime production, " "check REGEX " + text
)
return int(lifetime_production)
async def lifetime_production_phase(self, phase):
"""Report Phase lifetime Energy production from meters repors json"""
jsondata = await self._meters_report_value("whDlvdCum",report="production",phase=phase)
if jsondata is None:
return self.message_production_not_available if phase is None else None
return int(jsondata)
async def lifetime_net_production(self,phase=None):
"""Report cumulative or phase lifetime net production (exported to grid) from consumption CT meters report"""
jsondata = await self._meters_readings_value("actEnergyRcvd",report="net-consumption",phase=phase)
if jsondata is None:
return self.message_consumption_not_available if phase is None else None
return int(jsondata)
async def lifetime_consumption(self,phase=None):
"""Report cumulative or phase lifetime total-consumption from consumption CT meters report"""
jsondata = await self._meters_report_value("whDlvdCum",report="total-consumption",phase=phase)
if jsondata is None:
return self.message_consumption_not_available if phase is None else None
return int(jsondata)
async def lifetime_net_consumption(self,phase=None):
"""Report cumulative or phase lifetime net-consumption from consumption CT meters report"""
jsondata = await self._meters_readings_value("actEnergyDlvd",report="net-consumption",phase=phase)
if jsondata is None:
return self.message_consumption_not_available if phase is None else None
return int(jsondata)
async def inverters_production(self):
"""Running getData() beforehand will set self.enpoint_type and self.isDataRetrieved"""
"""so that this method will only read data from stored variables"""
"""Only return data if Envoy supports retrieving Inverter data"""
if not self.get_inverters:
return None
response_dict = {}
try:
for item in self.endpoint_production_inverters.json():
response_dict[item["serialNumber"]] = [
item["lastReportWatts"],
time.strftime(
"%Y-%m-%d %H:%M:%S", time.localtime(item["lastReportDate"])
),
]
except (JSONDecodeError, KeyError, IndexError, TypeError, AttributeError):
return None
return response_dict
async def battery_storage(self):
"""Return battery data from Envoys that support and have batteries installed"""
if self.endpoint_type in [ENVOY_MODEL_C,ENVOY_MODEL_LEGACY]:
return self.message_battery_not_available
try:
raw_json = self.endpoint_production_json_results.json()
except JSONDecodeError:
return None
"""For Envoys that support batteries but do not have them installed the"""
"""percentFull will not be available in the JSON results. The API will"""
"""only return battery data if batteries are installed."""
if "percentFull" not in raw_json["storage"][0].keys():
# "ENCHARGE" batteries are part of the "ENSEMBLE" api instead
# Check to see if it's there. Enphase has too much fun with these names
if self.endpoint_ensemble_json_results is not None:
ensemble_json = self.endpoint_ensemble_json_results.json()
if len(ensemble_json) > 0 and "devices" in ensemble_json[0].keys():
return ensemble_json[0]["devices"]
return self.message_battery_not_available
return raw_json["storage"][0]
async def pf(self,phase=None):
"""Report cumulative or phase PowerFactor from consumption CT meters report"""
jsondata = await self._meters_report_value("pwrFactor",report="net-consumption",phase=phase)
if jsondata is None:
return self.message_pf_not_available if phase is None else None
return float(str(jsondata))
async def voltage(self,phase=None):
"""Report cumulative or phase Voltage from consumption CT meters report"""
jsondata = await self._meters_report_value("rmsVoltage",report="net-consumption",phase=phase)
if jsondata is None:
return self.message_voltage_not_available if phase is None else None
return float(str(jsondata))
async def frequency(self,phase=None):
"""Report cumulative or phase Frequency from consumption CT meters report"""
jsondata = await self._meters_report_value("freqHz",report="net-consumption",phase=phase)
if jsondata is None:
return self.message_frequency_not_available if phase is None else None
return float(str(jsondata))
async def consumption_Current(self,phase=None):
"""Report cumulative or phase rmsCurrent from consumption CT meters report"""
jsondata = await self._meters_report_value("rmsCurrent",report="net-consumption",phase=phase)
if jsondata is None:
return self.message_current_consumption_not_available if phase is None else None
return float(str(jsondata))
async def production_Current(self,phase=None):
"""Report cumulative or phase rmsCurrent from production CT meters report"""
jsondata = await self._meters_report_value("rmsCurrent",report="production",phase=phase)
if jsondata is None:
return self.message_current_production_not_available if phase is None else None
return float(str(jsondata))
async def grid_status(self):
"""Return grid status reported by Envoy"""
if self.has_grid_status and self.endpoint_home_json_results is not None:
if self.endpoint_home_json_results.status_code == 200:
home_json = self.endpoint_home_json_results.json()
if ("enpower" in home_json.keys() and "grid_status" in home_json["enpower"].keys()):
return home_json["enpower"]["grid_status"]
self.has_grid_status = False
return None
async def active_inverter_count(self) -> int|str:
"""Return active inverter count from /home html for legacy envoy"""
if (self.endpoint_type == ENVOY_MODEL_LEGACY
and self.endpoint_home_results
and self.endpoint_home_results.status_code == 200):
text = self.endpoint_home_results.text
match = re.search(ACTIVE_INVERTER_COUNT_REGEX, text, re.MULTILINE)
if match:
active_count = int(match.group(1))
return active_count
return self.message_active_inverters_not_available
async def envoy_info(self):
"""Return information reported by Envoy info.xml."""
device_data = {}
if self.endpoint_info_results:
try:
data = xmltodict.parse(self.endpoint_info_results.text)
device_data["software"] = data["envoy_info"]["device"]["software"]
device_data["pn"] = data["envoy_info"]["device"]["pn"]
device_data["metered"] = data["envoy_info"]["device"]["imeter"]
except Exception: # pylint: disable=broad-except
pass
# add internal key information for envoy class
device_data["Using-model"] = self.endpoint_type
device_data["Using-httpsflag"] = self.https_flag
device_data["Using-ProductionMeteringEnabled"] = self.isProductionMeteringEnabled
device_data["Using-ConsumptionMeteringEnabled"] = self.isConsumptionMeteringEnabled
device_data["Using-GetInverters"] = self.get_inverters
device_data["Using-UseEnligthen"] = self.use_enlighten_owner_token
device_data["Using-InfoUpdateInterval"] = self.info_refresh_buffer_seconds
device_data["Using-hasgridstatus"] = self.has_grid_status
device_data["Using-FetchRetryCount"] = self._fetch_retries
device_data["Using-FetchTimeOut"] = self._fetch_timeout_seconds
device_data["Using-FetchHoldoff"] = self._fetch_holdoff_seconds
if self.endpoint_meters_json_results:
device_data["Endpoint-meters"] = self.endpoint_meters_json_results.text
else:
device_data["Endpoint-meters"] = self.endpoint_meters_json_results
if self.endpoint_meters_readings_json_results:
device_data["Endpoint-meters-readings"] = self.endpoint_meters_readings_json_results.text
else:
device_data["Endpoint-meters-readings"] = self.endpoint_meters_readings_json_results
if self.endpoint_meters_reports_json_results:
device_data["Endpoint-meters-reports"] = self.endpoint_meters_reports_json_results.text
else:
device_data["Endpoint-meters-reports"] = self.endpoint_meters_reports_json_results
if self.endpoint_production_json_results:
device_data[
"Endpoint-production_json"
] = self.endpoint_production_json_results.text
else:
device_data[
"Endpoint-production_json"
] = self.endpoint_production_json_results
if self.endpoint_production_v1_results:
device_data[
"Endpoint-production_v1"
] = self.endpoint_production_v1_results.text
else:
device_data["Endpoint-production_v1"] = self.endpoint_production_v1_results
if self.endpoint_production_results:
device_data["Endpoint-production"] = self.endpoint_production_results.text
else:
device_data["Endpoint-production"] = self.endpoint_production_results
if self.endpoint_production_inverters:
device_data[
"Endpoint-production_inverters"
] = self.endpoint_production_inverters.text
else:
device_data[
"Endpoint-production_inverters"
] = self.endpoint_production_inverters
if self.endpoint_ensemble_json_results:
device_data[
"Endpoint-ensemble_json"
] = self.endpoint_ensemble_json_results.text
else:
device_data["Endpoint-ensemble_json"] = self.endpoint_ensemble_json_results
if self.endpoint_home_json_results:
device_data["Endpoint-home"] = self.endpoint_home_json_results.text
else:
device_data["Endpoint-home"] = self.endpoint_home_json_results
if self.endpoint_info_results:
device_data["Endpoint-info"] = self.endpoint_info_results.text
else:
device_data["Endpoint-info"] = self.endpoint_info_results
if self.endpoint_home_results:
device_data["legacy-home"] = self.endpoint_home_results.text
else:
device_data["legacy-home"] = self.endpoint_home_results
return device_data
def run_in_console(self, dumpraw=False,loopcount=1,waittime=1):
"""If running this module directly, print all the values in the console."""
loop = asyncio.get_event_loop()
for attempt in range(0,loopcount):
if attempt > 0:
print("Sleeping...")
time.sleep(waittime)
print("Reading...")
data_results = loop.run_until_complete(
asyncio.gather(self.getData(), return_exceptions=False)
)
loop = asyncio.get_event_loop()
results = loop.run_until_complete(
asyncio.gather(
self.production(), #0
self.consumption(),
self.net_consumption(),
self.daily_production(),
self.daily_consumption(),
self.seven_days_production(),
self.seven_days_consumption(),
self.lifetime_production(),
self.lifetime_net_production(),
self.lifetime_consumption(),
self.lifetime_net_consumption(), #10
self.battery_storage(),
self.inverters_production(),
self.envoy_info(),
self.pf(),
self.voltage(),
self.frequency(),
self.consumption_Current(),
self.production_Current(),
#get values for phase L2
self.production_phase("l2"),
self.consumption("l2"), #20
self.net_consumption("l2"),
self.daily_production_phase("l2"),
self.daily_consumption_phase("l2"),
self.lifetime_production_phase("l2"),
self.lifetime_net_production("l2"),
self.lifetime_consumption("l2"),
self.lifetime_net_consumption("l2"),
self.pf("l2"),
self.voltage("l2"),
self.frequency("l2"), #30
self.consumption_Current("l2"),
self.production_Current("l2"),
self.grid_status(),
self.active_inverter_count(), #34
return_exceptions=False,
)
)
print("--System values--")
print(f"production: {results[0]}")
print(f"consumption: {results[1]}")
print(f"net_consumption: {results[2]}")
print(f"daily_production: {results[3]}")
print(f"daily_consumption: {results[4]}")
print(f"seven_days_production: {results[5]}")
print(f"seven_days_consumption: {results[6]}")
print(f"lifetime_production: {results[7]}")
print(f"lifetime_net_production: {results[8]}")
print(f"lifetime_consumption: {results[9]}")
print(f"lifetime_net_consumption: {results[10]}")
print(f"battery_storage: {results[11]}")
print(f"pf: {results[14]}")
print(f"voltage: {results[15]}")
print(f"frequency: {results[16]}")
print(f"consumption_Current: {results[17]}")
print(f"production_Current: {results[18]}")
print("--Phase L2 values--")
print(f"production: {results[19]}")
print(f"consumption: {results[20]}")
print(f"net_consumption: {results[21]}")
print(f"daily_production: {results[22]}")
print(f"daily_consumption: {results[23]}")
print(f"lifetime_production: {results[24]}")
print(f"lifetime_net_production: {results[25]}")
print(f"lifetime_consumption: {results[26]}")
print(f"lifetime_net_consumption: {results[27]}")
print(f"pf: {results[28]}")
print(f"voltage: {results[29]}")
print(f"frequency: {results[30]}")
print(f"consumption_Current: {results[31]}")
print(f"production_Current: {results[32]}")
print(f"grid_status: {results[33]}")
print(f"active_inverters: {results[34]}")
if "401" in str(data_results):
print(
"inverters_production: Unable to retrieve inverter data - Authentication failure"
)
elif results[12] is None:
print(
"inverters_production: Inverter data not available for your Envoy device."
)
else:
print(f"inverters_production: {results[12]}")
if dumpraw:
print(f"envoy_info: {json.dumps(results[13],indent=2)}")
if __name__ == "__main__":
SECURE = ""
parser = argparse.ArgumentParser(
description="Retrieve energy information from the Enphase Envoy device."
)
parser.add_argument(
"-u", "--user", dest="username", help="Username (Envoy or Enphase)"
)
parser.add_argument(
"-p", "--pass", dest="password", help="Password (Envoy or Enphase)"
)
parser.add_argument(
"-o",
"--ownertoken",
dest="ownertoken",
help="Use Enphase owner token from enlighten",
action='store_true'
)
parser.add_argument(
"-s",
"--serialnum",
dest="enlighten_serial_num",
help="Envoy Serial Number. Needed to get Token from Enphase",
)
parser.add_argument(
"-i",
"--ipaddress",
dest="host_ip",
help="Envoy IP address.",
)
parser.add_argument(
"-r",
"--rawdump",
dest="rawdump",
help="Dump raw json content of envoy info",
action='store_true'
)
parser.add_argument(
"-d",
"--debuglog",
dest="debuglog",
help="Enable Debug log output",
action='store_true'
)
parser.add_argument(
"-n",
"--number",
dest="loopcount",
help="NUmber of loops to executet",
)
parser.add_argument(
"-w",
"--waittime",
dest="waittime",
help="TIme to wait between loops [sec]",
)
args = parser.parse_args()
if args.debuglog:
_LOGGER.setLevel(logging.DEBUG)
_LOGGER.addHandler(logging.StreamHandler(sys.stdout))
if args.host_ip is None:
HOST = input(
"Enter the Envoy IP address or host name, "
+ "or press enter to use 'envoy' as default: "
)
else:
HOST = args.host_ip
if args.username is None:
USERNAME = input(
"Enter the Username for Enphase site or Envoy, "
+ "or press enter to use 'envoy' as default: "
)
else:
USERNAME = args.username
if args.password is None:
PASSWORD = getpass.getpass(
"Enter the Password for Enphase site or Envoy, "
+ "or press enter to use the default password: "
)
else:
PASSWORD = args.password
if (
args.username is None
and args.password is None
and args.ownertoken == False
and USERNAME != ""
and PASSWORD != ""
):
OWNERTOKEN = (input(
"Use Token from Enphase to login to Envoy (Y/N):"
).lower()[0]=="y")
else:
OWNERTOKEN = args.ownertoken
if OWNERTOKEN and args.enlighten_serial_num is None:
SERIALNUM = input(
"Enter the Envoy serialnumber: "
)
else:
SERIALNUM = args.enlighten_serial_num
if OWNERTOKEN:
SECURE = "s"
else:
SECURE = ""
if HOST == "":
HOST = "envoy"
if USERNAME == "":
USERNAME = "envoy"
LOOPCOUNT = 1
if (args.loopcount is not None):
LOOPCOUNT = int(args.loopcount)
WAITTIME = 1
if (args.waittime is not None):
WAITTIME = int(args.waittime)
_LOGGER.debug("Host %s",HOST)
_LOGGER.debug("Username %s",USERNAME)
_LOGGER.debug("Password specified %s",PASSWORD!="")
_LOGGER.debug("serialnum %s",SERIALNUM)
_LOGGER.debug("Secure %s",SECURE)
_LOGGER.debug("Loopcount %s",LOOPCOUNT)
_LOGGER.debug("waittime %s",WAITTIME)
TESTREADER = EnvoyReader(
HOST,
username=USERNAME,
password=PASSWORD,
enlighten_user=USERNAME,
enlighten_pass=PASSWORD,
inverters=True,
enlighten_serial_num=SERIALNUM,
https_flag=SECURE,
use_enlighten_owner_token=OWNERTOKEN,
)
TESTREADER.run_in_console(args.rawdump,LOOPCOUNT,WAITTIME)
================================================
FILE: custom_components/enphase_envoy_custom/manifest.json
================================================
{
"domain": "enphase_envoy",
"name": "Enphase Envoy (DEV)",
"documentation": "https://github.com/briancmpbll/home_assistant_custom_envoy#readme",
"requirements": [
"pyjwt",
"xmltodict",
"httpx",
"envoy_utils"
],
"codeowners": ["@briancmpbll"],
"config_flow": true,
"iot_class": "local_polling",
"version": "0.0.20"
}
================================================
FILE: custom_components/enphase_envoy_custom/sensor.py
================================================
"""Support for Enphase Envoy solar energy monitor."""
from __future__ import annotations
import datetime
from time import strftime, localtime
from homeassistant.components.sensor import SensorEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.event import async_track_state_change_event
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import BATTERY_ENERGY_DISCHARGED_SENSOR, BATTERY_ENERGY_CHARGED_SENSOR, COORDINATOR, DOMAIN, NAME, SENSORS, ICON, PHASE_SENSORS
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up envoy sensor platform."""
data = hass.data[DOMAIN][config_entry.entry_id]
coordinator = data[COORDINATOR]
name = data[NAME]
entities = []
for sensor_description in SENSORS:
if (sensor_description.key == "inverters"):
if (coordinator.data.get("inverters_production") is not None):
for inverter in coordinator.data["inverters_production"]:
entity_name = f"{name} {sensor_description.name} {inverter}"
split_name = entity_name.split(" ")
serial_number = split_name[-1]
entities.append(
EnvoyInverterEntity(
sensor_description,
entity_name,
name,
config_entry.unique_id,
serial_number,
coordinator,
)
)
elif (sensor_description.key == "batteries"):
if (coordinator.data.get("batteries") is not None):
for battery in coordinator.data["batteries"]:
entity_name = f"{name} {sensor_description.name} {battery}"
serial_number = battery
entities.append(
EnvoyBatteryEntity(
sensor_description,
entity_name,
name,
config_entry.unique_id,
serial_number,
coordinator
)
)
elif (sensor_description.key == "current_battery_capacity"):
if (coordinator.data.get("batteries") is not None):
battery_capacity_entity = TotalBatteryCapacityEntity(
sensor_description,
f"{name} {sensor_description.name}",
name,
config_entry.unique_id,
None,
coordinator
)
entities.append(battery_capacity_entity)
entities.append(
BatteryEnergyChangeEntity(
BATTERY_ENERGY_CHARGED_SENSOR,
f"{name} {BATTERY_ENERGY_CHARGED_SENSOR.name}",
name,
config_entry.unique_id,
None,
battery_capacity_entity,
True
)
)
entities.append(
BatteryEnergyChangeEntity(
BATTERY_ENERGY_DISCHARGED_SENSOR,
f"{name} {BATTERY_ENERGY_DISCHARGED_SENSOR.name}",
name,
config_entry.unique_id,
None,
battery_capacity_entity,
False
)
)
elif (sensor_description.key == "total_battery_percentage"):
if (coordinator.data.get("batteries") is not None):
entities.append(TotalBatteryPercentageEntity(
sensor_description,
f"{name} {sensor_description.name}",
name,
config_entry.unique_id,
None,
coordinator
))
else:
data = coordinator.data.get(sensor_description.key)
if isinstance(data, str) and "not available" in data:
continue
entity_name = f"{name} {sensor_description.name}"
entities.append(
CoordinatedEnvoyEntity(
sensor_description,
entity_name,
name,
config_entry.unique_id,
None,
coordinator,
)
)
for sensor_description in PHASE_SENSORS:
data = coordinator.data.get(sensor_description.key)
if data == None:
continue
entity_name = f"{name} {sensor_description.name}"
entities.append(
CoordinatedEnvoyEntity(
sensor_description,
entity_name,
name,
config_entry.unique_id,
None,
coordinator,
)
)
async_add_entities(entities)
class EnvoyEntity(SensorEntity):
"""Envoy entity"""
def __init__(
self,
description,
name,
device_name,
device_serial_number,
serial_number,
):
"""Initialize Envoy entity."""
self.entity_description = description
self._name = name
self._serial_number = serial_number
self._device_name = device_name
self._device_serial_number = device_serial_number
@property
def name(self):
"""Return the name of the sensor."""
return self._name
@property
def unique_id(self):
"""Return the unique id of the sensor."""
if self._serial_number:
return self._serial_number
if self._device_serial_number:
return f"{self._device_serial_number}_{self.entity_description.key}"
@property
def icon(self):
"""Icon to use in the frontend, if any."""
return ICON
@property
def extra_state_attributes(self):
"""Return the state attributes."""
return None
@property
def device_info(self) -> DeviceInfo | None:
"""Return the device_info of the device."""
if not self._device_serial_number:
return None
sw_version = None
hw_version = None
if hasattr(self, 'coordinator'):
if self.coordinator.data.get("envoy_info"):
sw_version = self.coordinator.data.get("envoy_info").get("software", None)
hw_version = self.coordinator.data.get("envoy_info").get("pn", None)
return DeviceInfo(
identifiers={(DOMAIN, str(self._device_serial_number))},
manufacturer="Enphase",
model="Envoy",
name=self._device_name,
sw_version=sw_version,
hw_version=hw_version,
)
class CoordinatedEnvoyEntity(EnvoyEntity, CoordinatorEntity):
def __init__(
self,
description,
name,
device_name,
device_serial_number,
serial_number,
coordinator,
):
EnvoyEntity.__init__(self, description, name, device_name, device_serial_number, serial_number)
CoordinatorEntity.__init__(self, coordinator)
@property
def native_value(self):
"""Return the state of the sensor."""
return self.coordinator.data.get(self.entity_description.key)
class EnvoyInverterEntity(CoordinatedEnvoyEntity):
"""Envoy inverter entity."""
def __init__(
self,
description,
name,
device_name,
device_serial_number,
serial_number,
coordinator,
):
super().__init__(
description=description,
name=name,
device_name=device_name,
device_serial_number=device_serial_number,
serial_number=serial_number,
coordinator=coordinator
)
@property
def native_value(self):
"""Return the state of the sensor."""
if (
self.coordinator.data.get("inverters_production") is not None
):
return self.coordinator.data.get("inverters_production").get(
self._serial_number
)[0]
return None
@property
def extra_state_attributes(self):
"""Return the state attributes."""
if (
self.coordinator.data.get("inverters_production") is not None
):
value = self.coordinator.data.get("inverters_production").get(
self._serial_number
)[1]
return {"last_reported": value}
return None
class EnvoyBatteryEntity(CoordinatedEnvoyEntity):
"""Envoy battery entity."""
def __init__(
self,
description,
name,
device_name,
device_serial_number,
serial_number,
coordinator,
):
super().__init__(
description=description,
name=name,
device_name=device_name,
device_serial_number=device_serial_number,
serial_number=serial_number,
coordinator=coordinator
)
@property
def native_value(self):
"""Return the state of the sensor."""
if (
self.coordinator.data.get("batteries") is not None
):
return self.coordinator.data.get("batteries").get(
self._serial_number
).get("percentFull")
return None
@property
def extra_state_attributes(self):
"""Return the state attributes."""
if (
self.coordinator.data.get("batteries") is not None
):
battery = self.coordinator.data.get("batteries").get(
self._serial_number
)
last_reported = strftime(
"%Y-%m-%d %H:%M:%S", localtime(battery.get("last_rpt_date"))
)
return {
"last_reported": last_reported,
"capacity": battery.get("encharge_capacity")
}
return None
class TotalBatteryCapacityEntity(CoordinatedEnvoyEntity):
def __init__(
self,
description,
name,
device_name,
device_serial_number,
serial_number,
coordinator,
):
super().__init__(
description=description,
name=name,
device_name=device_name,
device_serial_number=device_serial_number,
serial_number=serial_number,
coordinator=coordinator
)
@property
def native_value(self):
"""Return the state of the sensor."""
batteries = self.coordinator.data.get("batteries")
if (
batteries is not None
):
total = 0
for battery in batteries:
percentage = batteries.get(battery).get("percentFull")
capacity = batteries.get(battery).get("encharge_capacity")
total += round(capacity * (percentage / 100.0))
return total
return None
class TotalBatteryPercentageEntity(CoordinatedEnvoyEntity):
def __init__(
self,
description,
name,
device_name,
device_serial_number,
serial_number,
coordinator,
):
super().__init__(
description=description,
name=name,
device_name=device_name,
device_serial_number=device_serial_number,
serial_number=serial_number,
coordinator=coordinator
)
@property
def native_value(self):
"""Return the state of the sensor."""
batteries = self.coordinator.data.get("batteries")
if (
batteries is not None
):
battery_sum = 0
for battery in batteries:
battery_sum += batteries.get(battery).get("percentFull", 0)
return round(battery_sum / len(batteries), 2)
return None
class BatteryEnergyChangeEntity(EnvoyEntity):
def __init__(
self,
description,
name,
device_name,
device_serial_number,
serial_number,
total_battery_capacity_entity,
positive: bool
):
super().__init__(
description=description,
name=name,
device_name=device_name,
device_serial_number=device_serial_number,
serial_number=serial_number,
)
self._sensor_source = total_battery_capacity_entity
self._positive = positive
self._state = 0
self._attr_last_reset = datetime.datetime.now()
async def async_added_to_hass(self):
"""Handle entity which will be added."""
await super().async_added_to_hass()
@callback
def calc_change(event):
"""Handle the sensor state changes."""
old_state = event.data.get("old_state")
new_state = event.data.get("new_state")
if (
old_state is None
or old_state.state in (STATE_UNKNOWN, STATE_UNAVAILABLE)
or new_state.state in (STATE_UNKNOWN, STATE_UNAVAILABLE)
):
self._state = 0
else:
old_state_value = int(old_state.state)
new_state_value = int(new_state.state)
if (self._positive):
if (new_state_value > old_state_value):
self._state = new_state_value - old_state_value
else:
self._state = 0
else:
if (old_state_value > new_state_value):
self._state = old_state_value - new_state_value
else:
self._state = 0
self._attr_last_reset = datetime.datetime.now()
self.async_write_ha_state()
self.async_on_remove(
async_track_state_change_event(
self.hass, self._sensor_source.entity_id, calc_change
)
)
@property
def native_value(self):
"""Return the state of the sensor."""
return self._state
================================================
FILE: custom_components/enphase_envoy_custom/strings.json
================================================
{
"config": {
"flow_title": "{serial} ({host})",
"step": {
"user": {
"description": "- If your Envoy is on Firmware 7.x or later, validate or enter the Host IP address of your Envoy, your Enphase website username, password, Envoy serial number, and tick the `Use Enlighten` box.\n- For older models do not tick `Use Enlighten` and use either usernames `envoy` without a password, `installer` without a password or a valid username and password and validate or enter the Host ip address.",
"data": {
"host": "[%key:common::config_flow::data::host%]",
"username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]",
"serial": "envoy serial number",
"use_enlighten": "Use Enlighten"
}
}
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
}
},
"options": {
"step": {
"user": {
"title": "Envoy options",
"data": {
"data_interval": "Time between entity updates [s].",
"data_fetch_timeout_seconds": "Timeout for getting single Envoy data page [s], minimum 5.",
"data_fetch_retry_count": "How many retries in getting single Envoy data page. minium 1.",
"data_fetch_holdoff_seconds": "Time between 2 retries to get single Envoy data page[s], minimum 0.",
"data_collection_timeout_seconds": "Overall Timeout on getting all Envoy data pages[s], minimum 30."
},
"data_description": {
"data_interval": "Time between data updates, minimum 5 sec. After any change here or below reload the envoy.",
"data_collection_timeout_seconds": "If overall data collection takes more then this time it will be cancelled. Account for retries."
}
}
}
}
}
================================================
FILE: custom_components/enphase_envoy_custom/translations/ca.json
================================================
{
"config": {
"abort": {
"already_configured": "El dispositiu ja est\u00e0 configurat",
"reauth_successful": "Re-autenticaci\u00f3 realitzada correctament"
},
"error": {
"cannot_connect": "Ha fallat la connexi\u00f3",
"invalid_auth": "Autenticaci\u00f3 inv\u00e0lida",
"unknown": "Error inesperat"
},
"flow_title": "{serial} ({host})",
"step": {
"user": {
"data": {
"host": "Amfitri\u00f3",
"password": "Contrasenya",
"username": "Nom d'usuari"
}
}
}
}
}
================================================
FILE: custom_components/enphase_envoy_custom/translations/cs.json
================================================
{
"config": {
"abort": {
"already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno",
"reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9"
},
"error": {
"cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit",
"invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed",
"unknown": "Neo\u010dek\u00e1van\u00e1 chyba"
},
"flow_title": "Envoy {serial} ({host})",
"step": {
"user": {
"data": {
"host": "Hostitel",
"password": "Heslo",
"username": "U\u017eivatelsk\u00e9 jm\u00e9no"
}
}
}
}
}
================================================
FILE: custom_components/enphase_envoy_custom/translations/de.json
================================================
{
"config": {
"abort": {
"already_configured": "Ger\u00e4t ist bereits konfiguriert",
"reauth_successful": "Die erneute Authentifizierung war erfolgreich"
},
"error": {
"cannot_connect": "Verbindung fehlgeschlagen",
"invalid_auth": "Ung\u00fcltige Authentifizierung",
"unknown": "Unerwarteter Fehler"
},
"flow_title": "{serial} ({host})",
"step": {
"user": {
"data": {
"host": "Host",
"password": "Passwort",
"username": "Benutzername"
}
}
}
}
}
================================================
FILE: custom_components/enphase_envoy_custom/translations/el.json
================================================
{
"config": {
"step": {
"user": {
}
}
}
}
================================================
FILE: custom_components/enphase_envoy_custom/translations/en.json
================================================
{
"config": {
"abort": {
"already_configured": "Device is already configured",
"reauth_successful": "Re-authentication was successful"
},
"error": {
"cannot_connect": "Failed to connect",
"invalid_auth": "Invalid authentication",
"unknown": "Unexpected error"
},
"flow_title": "{serial} ({host})",
"step": {
"user": {
"data": {
"host": "Host",
"password": "Password",
"username": "Username",
"serial": "envoy serial number",
"use_enlighten": "Use Enlighten"
},
"description": "- If your Envoy is on Firmware 7.x or later, validate or enter the Host IP address of your Envoy, your Enphase website username, password, Envoy serial number, and tick the `Use Enlighten` box.\n- For older models do not tick `Use Enlighten` and use either usernames `envoy` without a password, `installer` without a password or a valid username and password and validate or enter the Host ip address."
}
}
},
"options": {
"step": {
"user": {
"title": "Envoy options",
"data": {
"data_interval": "Time between entity updates [s].",
"data_fetch_timeout_seconds": "Timeout for getting single Envoy data page [s], minimum 5.",
"data_fetch_retry_count": "How many retries in getting single Envoy data page. minimum 1.",
"data_fetch_holdoff_seconds": "Time between 2 retries to get single Envoy data page[s], minimum 0.",
"data_collection_timeout_seconds": "Overall Timeout on getting all Envoy data pages[s], minimum 30.",
"do_not_use_production_json": "Do not use production json. (For use with Envoy-S Meter with CT only. Faster, but todays total and Last 7 day total will be unavailable, current and lifetime data is available)"
},
"data_description": {
"data_interval": "Time between data updates, minimum 5 sec. After any change here or below reload the envoy.",
"data_collection_timeout_seconds": "If overall data collection takes more then this time it will be cancelled. Account for retries."
}
}
}
}
}
================================================
FILE: custom_components/enphase_envoy_custom/translations/es-419.json
================================================
{
"config": {
"flow_title": "{serial} ({host})"
}
}
================================================
FILE: custom_components/enphase_envoy_custom/translations/es.json
================================================
{
"config": {
"abort": {
"already_configured": "El dispositivo ya est\u00e1 configurado",
"reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente"
},
"error": {
"cannot_connect": "No se pudo conectar",
"invalid_auth": "Autenticaci\u00f3n no v\u00e1lida",
"unknown": "Error inesperado"
},
"flow_title": "Envoy {serial} ({host})",
"step": {
"user": {
"data": {
"host": "Host",
"password": "Contrase\u00f1a",
"username": "Usuario"
}
}
}
}
}
================================================
FILE: custom_components/enphase_envoy_custom/translations/et.json
================================================
{
"config": {
"abort": {
"already_configured": "Seade on juba h\u00e4\u00e4lestatud",
"reauth_successful": "Taastuvastamine \u00f5nnestus"
},
"error": {
"cannot_connect": "\u00dchendamine nurjus",
"invalid_auth": "Tuvastamise viga",
"unknown": "Tundmatu viga"
},
"flow_title": "{serial} ({host})",
"step": {
"user": {
"data": {
"host": "Host",
"password": "Salas\u00f5na",
"username": "Kasutajanimi"
}
}
}
}
}
================================================
FILE: custom_components/enphase_envoy_custom/translations/fr.json
================================================
{
"config": {
"abort": {
"already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9",
"reauth_successful": "La r\u00e9-authentification a r\u00e9ussi"
},
"error": {
"cannot_connect": "\u00c9chec de connexion",
"invalid_auth": "Authentification invalide",
"unknown": "Erreur inattendue"
},
"flow_title": "{serial} ({host})",
"step": {
"user": {
"data": {
"host": "H\u00f4te",
"password": "Mot de passe",
"username": "Nom d'utilisateur",
"serial": "Num\u00e9ro de s\u00e9rie envoy",
"use_enlighten": "R\u00e9cup\u00e9ration d'un Token"
},
"description": "- Si votre Envoy est \u00e9quip\u00e9 du micrologiciel 7.x ou version ultérieure, validez ou saisissez l'adresse IP de l'hôte de votre Envoy, votre nom d'utilisateur de site Web Enphase, votre mot de passe, le num\u00e9ro de série de votre Envoy et cochez la case « G\u00e9n\u00e9ration d'un Token ».\n- Pour les modèles plus anciens. ne cochez pas cette case et utilisez soit les noms d'utilisateur « envoy » sans mot de passe, « installer » sans mot de passe ou un nom d'utilisateur et un mot de passe valides et validez ou entrez l'adresse IP de l'hôte."
}
}
},
"options": {
"step": {
"user": {
"title": "Options Envoy",
"data": {
"data_interval": "Temps entre 2 rafraichissement [s]",
"data_fetch_timeout_seconds": "TimeOut pour obtenir une page de donn\u00e9es Envoy [s], minimum 5.",
"data_fetch_retry_count": "Nombre de tentatives max pour obtenir une page de donn\u00e9es Envoy [s]. au minimum 1.",
"data_fetch_holdoff_seconds": "D\u00e9lai entre 2 tentatives pour obtenir une page de donn\u00e9es Envoy [s], minimum 0.",
"data_collection_timeout_seconds": "TimeOut totale pour obtenir toutes les donn\u00e9es Envoy [s], minimum 30."
},
"data_description": {
"data_interval": "Temps entre 2 rafraichissement, minimum 5 sec. Relancer apres un changement",
"data_collection_timeout_seconds": "Si la récupération des donn\u00e9es prend plus de temps que le TimeOut, elle sera annul\u00e9e"
}
}
}
}
}
================================================
FILE: custom_components/enphase_envoy_custom/translations/he.json
================================================
{
"config": {
"abort": {
"already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4",
"reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7"
},
"error": {
"cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4",
"invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9",
"unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4"
},
"flow_title": "{serial} ({host})",
"step": {
"user": {
"data": {
"host": "\u05de\u05d0\u05e8\u05d7",
"password": "\u05e1\u05d9\u05e1\u05de\u05d4",
"username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
}
}
}
}
}
================================================
FILE: custom_components/enphase_envoy_custom/translations/hu.json
================================================
{
"config": {
"abort": {
"already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van",
"reauth_successful": "Az \u00fajhiteles\u00edt\u00e9s sikeres volt"
},
"error": {
"cannot_connect": "Sikertelen csatlakoz\u00e1s",
"invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s",
"unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
},
"flow_title": "{serial} ({host})",
"step": {
"user": {
"data": {
"host": "C\u00edm",
"password": "Jelsz\u00f3",
"username": "Felhaszn\u00e1l\u00f3n\u00e9v"
}
}
}
}
}
================================================
FILE: custom_components/enphase_envoy_custom/translations/id.json
================================================
{
"config": {
"abort": {
"already_configured": "Perangkat sudah dikonfigurasi",
"reauth_successful": "Autentikasi ulang berhasil"
},
"error": {
"cannot_connect": "Gagal terhubung",
"invalid_auth": "Autentikasi tidak valid",
"unknown": "Kesalahan yang tidak diharapkan"
},
"flow_title": "{serial} ({host})",
"step": {
"user": {
"data": {
"host": "Host",
"password": "Kata Sandi",
"username": "Nama Pengguna"
}
}
}
}
}
================================================
FILE: custom_components/enphase_envoy_custom/translations/it.json
================================================
{
"config": {
"abort": {
"already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato",
"reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente"
},
"error": {
"cannot_connect": "Impossibile connettersi",
"invalid_auth": "Autenticazione non valida",
"unknown": "Errore imprevisto"
},
"flow_title": "{serial} ({host})",
"step": {
"user": {
"data": {
"host": "Host",
"password": "Password",
"username": "Nome utente"
}
}
}
}
}
================================================
FILE: custom_components/enphase_envoy_custom/translations/ja.json
================================================
{
"config": {
"abort": {
"already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059",
"reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f"
},
"error": {
"cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f",
"invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c",
"unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc"
},
"flow_title": "{serial} ({host})",
"step": {
"user": {
"data": {
"host": "\u30db\u30b9\u30c8",
"password": "\u30d1\u30b9\u30ef\u30fc\u30c9",
"username": "\u30e6\u30fc\u30b6\u30fc\u540d"
}
}
}
}
}
================================================
FILE: custom_components/enphase_envoy_custom/translations/ko.json
================================================
{
"config": {
"abort": {
"already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
"reauth_successful": "\uc7ac\uc778\uc99d\uc5d0 \uc131\uacf5\ud588\uc2b5\ub2c8\ub2e4"
},
"error": {
"cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
"invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
"unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
},
"flow_title": "Envoy {serial} ({host})",
"step": {
"user": {
"data": {
"host": "\ud638\uc2a4\ud2b8",
"password": "\ube44\ubc00\ubc88\ud638",
"username": "\uc0ac\uc6a9\uc790 \uc774\ub984"
}
}
}
}
}
================================================
FILE: custom_components/enphase_envoy_custom/translations/nl.json
================================================
{
"config": {
"abort": {
"already_configured": "Apparaat is al geconfigureerd",
"reauth_successful": "Herauthenticatie was succesvol"
},
"error": {
"cannot_connect": "Kan geen verbinding maken",
"invalid_auth": "Ongeldige authenticatie",
"unknown": "Onverwachte fout"
},
"flow_title": "{serial} ({host})",
"step": {
"user": {
"data": {
"host": "Host",
"password": "Wachtwoord",
"username": "Gebruikersnaam",
"serial": "Envoy serienummer",
"use_enlighten": "Gebruik Enlighten"
},
"description": "- Voor firmware D7.0.0 en hoger controleer of voer Host IP adres, Enphase website gebruikersnaam en wachtwoord, Envoy serienummer in en schakel `Gebruik Enlighten` in. \n- Voor oudere modellen schakel `Gebruik Enlighten` niet in en voer gebruikersnaam `envoy` zonder wachtwoord of `installer` zonder wachtwoord of een geldige gebruikersnaam en wachtwoord in en controleer of voer het Host IP adres in."
}
}
},
"options": {
"step": {
"user": {
"title": "Envoy opties",
"data": {
"data_interval": "Tijd tussen entity updates [s].",
"data_fetch_timeout_seconds": "Timeout voor het lezen van Envoy gegevens [s], minimaal 5.",
"data_fetch_retry_count": "Aantal extra pogingen Envoy gegevens te lezen als timeout optreed, minimaal 1.",
"data_fetch_holdoff_seconds": "Wachttijd tussen 2 pogingen om Envoy gegevens te lezen [s], minimaal 0.",
"data_collection_timeout_seconds": "Maximale tijd voor het lezen van alle benodigde Envoy gegevens[s], minimaal 30.",
"do_not_use_production_json": "Maak geen gebruik van production json. (Alleen voor gebruik met Envoy-s metered met CT klemmen. Sneller, maar todays total en Last 7 day total zijn niet beschikbaar, current en lifetime gegevens zijn wel beschikbaar)"
},
"data_description": {
"data_interval": "Tijd tussen 2 gegevens verversingen, minimaal 5 sec. Bij wijziging van een instelling op deze pagina de Envoy opnieuw laden.",
"data_collection_timeout_seconds": "Als het langer duurt dan deze tijd om alle gegevens op te halen wordt de poging gestaakt. Hou rekening met herhalingen en ander instellingen."
}
}
}
}
}
================================================
FILE: custom_components/enphase_envoy_custom/translations/no.json
================================================
{
"config": {
"abort": {
"already_configured": "Enheten er allerede konfigurert",
"reauth_successful": "Godkjenning p\u00e5 nytt var vellykket"
},
"error": {
"cannot_connect": "Tilkobling mislyktes",
"invalid_auth": "Ugyldig godkjenning",
"unknown": "Uventet feil"
},
"flow_title": "{serial} ({host})",
"step": {
"user": {
"data": {
"host": "Vert",
"password": "Passord",
"username": "Brukernavn"
}
}
}
}
}
================================================
FILE: custom_components/enphase_envoy_custom/translations/pl.json
================================================
{
"config": {
"abort": {
"already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane",
"reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119"
},
"error": {
"cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
"invalid_auth": "Niepoprawne uwierzytelnienie",
"unknown": "Nieoczekiwany b\u0142\u0105d"
},
"flow_title": "{serial} ({host})",
"step": {
"user": {
"data": {
"host": "Nazwa hosta lub adres IP",
"password": "Has\u0142o",
"username": "Nazwa u\u017cytkownika"
}
}
}
}
}
================================================
FILE: custom_components/enphase_envoy_custom/translations/ru.json
================================================
{
"config": {
"abort": {
"already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.",
"reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e."
},
"error": {
"cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
"invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.",
"unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
},
"flow_title": "{serial} ({host})",
"step": {
"user": {
"data": {
"host": "\u0425\u043e\u0441\u0442",
"password": "\u041f\u0430\u0440\u043e\u043b\u044c",
"username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f"
}
}
}
}
}
================================================
FILE: custom_components/enphase_envoy_custom/translations/sv.json
================================================
{
"config": {
"abort": {
"already_configured": "Enheten \u00e4r redan konfigurerad"
},
"error": {
"cannot_connect": "Kunde inte ansluta",
"unknown": "Ov\u00e4ntat fel"
},
"step": {
"user": {
"data": {
"password": "L\u00f6senord",
"username": "Anv\u00e4ndarnamn"
}
}
}
}
}
================================================
FILE: custom_components/enphase_envoy_custom/translations/tr.json
================================================
{
"config": {
"abort": {
"already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f",
"reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu"
},
"error": {
"cannot_connect": "Ba\u011flanma hatas\u0131",
"invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama",
"unknown": "Beklenmeyen hata"
},
"flow_title": "{serial} ( {host} )",
"step": {
"user": {
"data": {
"host": "Sunucu",
"password": "Parola",
"username": "Kullan\u0131c\u0131 Ad\u0131"
}
}
}
}
}
================================================
FILE: custom_components/enphase_envoy_custom/translations/zh-Hans.json
================================================
{
"config": {
"step": {
"user": {
"data": {
"password": "\u5bc6\u7801"
}
}
}
}
}
================================================
FILE: custom_components/enphase_envoy_custom/translations/zh-Hant.json
================================================
{
"config": {
"abort": {
"already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210",
"reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f"
},
"error": {
"cannot_connect": "\u9023\u7dda\u5931\u6557",
"invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548",
"unknown": "\u672a\u9810\u671f\u932f\u8aa4"
},
"flow_title": "{serial} ({host})",
"step": {
"user": {
"data": {
"host": "\u4e3b\u6a5f\u7aef",
"password": "\u5bc6\u78bc",
"username": "\u4f7f\u7528\u8005\u540d\u7a31"
}
}
}
}
}
================================================
FILE: hacs.json
================================================
{
"name": "Enphase Envoy (DEV)",
"render_readme": true,
"content_in_root": false
}
gitextract_t8pci746/ ├── .gitignore ├── README.md ├── custom_components/ │ └── enphase_envoy_custom/ │ ├── __init__.py │ ├── binary_sensor.py │ ├── config_flow.py │ ├── const.py │ ├── diagnostics.py │ ├── envoy_reader.py │ ├── manifest.json │ ├── sensor.py │ ├── strings.json │ └── translations/ │ ├── ca.json │ ├── cs.json │ ├── de.json │ ├── el.json │ ├── en.json │ ├── es-419.json │ ├── es.json │ ├── et.json │ ├── fr.json │ ├── he.json │ ├── hu.json │ ├── id.json │ ├── it.json │ ├── ja.json │ ├── ko.json │ ├── nl.json │ ├── no.json │ ├── pl.json │ ├── ru.json │ ├── sv.json │ ├── tr.json │ ├── zh-Hans.json │ └── zh-Hant.json └── hacs.json
SYMBOL INDEX (125 symbols across 6 files)
FILE: custom_components/enphase_envoy_custom/__init__.py
function async_setup_entry (line 32) | async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> ...
function async_unload_entry (line 151) | async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) ->...
FILE: custom_components/enphase_envoy_custom/binary_sensor.py
function async_setup_entry (line 10) | async def async_setup_entry(
class EnvoyGridStatusEntity (line 35) | class EnvoyGridStatusEntity(CoordinatorEntity, BinarySensorEntity):
method __init__ (line 36) | def __init__(
method icon (line 53) | def icon(self):
method name (line 58) | def name(self):
method unique_id (line 63) | def unique_id(self):
method device_info (line 71) | def device_info(self) -> DeviceInfo or None:
method is_on (line 83) | def is_on(self) -> bool:
FILE: custom_components/enphase_envoy_custom/config_flow.py
function validate_input (line 28) | async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> E...
function ipv4asdefault (line 58) | async def ipv4asdefault(hass: HomeAssistant):
class ConfigFlow (line 65) | class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
method __init__ (line 70) | def __init__(self):
method _async_generate_schema (line 77) | def _async_generate_schema(self):
method _async_current_hosts (line 95) | def _async_current_hosts(self):
method async_step_zeroconf (line 103) | async def async_step_zeroconf(
method async_step_reauth (line 135) | async def async_step_reauth(self, user_input):
method _async_envoy_name (line 142) | def _async_envoy_name(self) -> str:
method _async_set_unique_id_from_envoy (line 148) | async def _async_set_unique_id_from_envoy(self, envoy_reader: EnvoyRea...
method async_step_user (line 158) | async def async_step_user(
method async_get_options_flow (line 215) | def async_get_options_flow(config_entry):
class EnvoyOptionsFlowHandler (line 218) | class EnvoyOptionsFlowHandler(config_entries.OptionsFlow):
method __init__ (line 221) | def __init__(self, config_entry):
method async_step_init (line 225) | async def async_step_init(self, _user_input=None):
method async_step_user (line 229) | async def async_step_user(self, user_input=None):
class CannotConnect (line 275) | class CannotConnect(HomeAssistantError):
class InvalidAuth (line 279) | class InvalidAuth(HomeAssistantError):
FILE: custom_components/enphase_envoy_custom/diagnostics.py
function async_get_config_entry_diagnostics (line 29) | async def async_get_config_entry_diagnostics(
FILE: custom_components/enphase_envoy_custom/envoy_reader.py
function has_production_and_consumption (line 63) | def has_production_and_consumption(json):
function has_metering_setup (line 68) | def has_metering_setup(json):
function has_production_metering_setup (line 73) | def has_production_metering_setup(json):
function has_consumption_metering_setup (line 78) | def has_consumption_metering_setup(json):
function has_net_consumption_meters_type (line 83) | def has_net_consumption_meters_type(json):
function get_production_meters_phase_count (line 88) | def get_production_meters_phase_count(json):
function get_consumption_meters_phase_count (line 93) | def get_consumption_meters_phase_count(json):
function is_ipv6_address (line 98) | def is_ipv6_address(address: str) -> bool:
class SwitchToHTTPS (line 106) | class SwitchToHTTPS(Exception):
class EnvoyReader (line 110) | class EnvoyReader: # pylint: disable=too-many-instance-attributes
method __init__ (line 155) | def __init__( # pylint: disable=too-many-arguments
method _token (line 227) | def _token(self):
method _token (line 231) | def _token(self, token_value):
method _sync_store (line 235) | async def _sync_store(self):
method async_client (line 244) | def async_client(self):
method non_local_async_client (line 251) | def non_local_async_client(self):
method _update (line 257) | async def _update(self):
method _update_from_meters_reports_endpoint (line 272) | async def _update_from_meters_reports_endpoint(self):
method _update_from_meters_readings_endpoint (line 280) | async def _update_from_meters_readings_endpoint(self):
method _update_from_pc_endpoint (line 288) | async def _update_from_pc_endpoint(self,detectmode=False):
method _update_from_p_endpoint (line 302) | async def _update_from_p_endpoint(self):
method _update_from_p0_endpoint (line 308) | async def _update_from_p0_endpoint(self):
method _update_info_endpoint (line 317) | async def _update_info_endpoint(self):
method _update_meters_endpoint (line 336) | async def _update_meters_endpoint(self):
method _update_endpoint (line 377) | async def _update_endpoint(self, attr, url):
method _async_fetch_with_retry (line 385) | async def _async_fetch_with_retry(self, url, **kwargs):
method _async_post (line 459) | async def _async_post(self, url, data, cookies=None, client=None, **kw...
method _fetch_owner_token_json (line 475) | async def _fetch_owner_token_json(self) :
method _getEnphaseToken (line 501) | async def _getEnphaseToken(self):
method _refresh_token_cookies (line 510) | async def _refresh_token_cookies(self):
method _is_enphase_token_valid (line 532) | def _is_enphase_token_valid(self, response):
method _is_enphase_token_expired (line 540) | def _is_enphase_token_expired(self, token):
method check_connection (line 555) | async def check_connection(self):
method getData (line 566) | async def getData(self, getInverters=True): # pylint: disable=invalid...
method detect_model (line 629) | async def detect_model(self):
method get_serial_number (line 704) | async def get_serial_number(self):
method get_full_serial_number (line 714) | async def get_full_serial_number(self):
method create_connect_errormessage (line 731) | def create_connect_errormessage(self):
method create_json_errormessage (line 740) | def create_json_errormessage(self):
method _meters_readings_value (line 750) | async def _meters_readings_value(self,field,report="net-consumption",p...
method _meters_report_value (line 788) | async def _meters_report_value(self,field,report="production",phase=No...
method production (line 818) | async def production(self,phase=None):
method production_phase (line 849) | async def production_phase(self, phase):
method consumption (line 856) | async def consumption(self,phase=None):
method net_consumption (line 863) | async def net_consumption(self,phase=None):
method daily_production (line 870) | async def daily_production(self,phase=None):
method daily_production_phase (line 903) | async def daily_production_phase(self, phase):
method daily_consumption (line 920) | async def daily_consumption(self,phase=None):
method daily_consumption_phase (line 936) | async def daily_consumption_phase(self, phase):
method seven_days_production (line 955) | async def seven_days_production(self):
method seven_days_consumption (line 985) | async def seven_days_consumption(self):
method lifetime_production (line 998) | async def lifetime_production(self,phase=None):
method lifetime_production_phase (line 1031) | async def lifetime_production_phase(self, phase):
method lifetime_net_production (line 1038) | async def lifetime_net_production(self,phase=None):
method lifetime_consumption (line 1045) | async def lifetime_consumption(self,phase=None):
method lifetime_net_consumption (line 1052) | async def lifetime_net_consumption(self,phase=None):
method inverters_production (line 1059) | async def inverters_production(self):
method battery_storage (line 1081) | async def battery_storage(self):
method pf (line 1105) | async def pf(self,phase=None):
method voltage (line 1112) | async def voltage(self,phase=None):
method frequency (line 1119) | async def frequency(self,phase=None):
method consumption_Current (line 1126) | async def consumption_Current(self,phase=None):
method production_Current (line 1133) | async def production_Current(self,phase=None):
method grid_status (line 1140) | async def grid_status(self):
method active_inverter_count (line 1150) | async def active_inverter_count(self) -> int|str:
method envoy_info (line 1164) | async def envoy_info(self):
method run_in_console (line 1248) | def run_in_console(self, dumpraw=False,loopcount=1,waittime=1):
FILE: custom_components/enphase_envoy_custom/sensor.py
function async_setup_entry (line 19) | async def async_setup_entry(
class EnvoyEntity (line 146) | class EnvoyEntity(SensorEntity):
method __init__ (line 149) | def __init__(
method name (line 165) | def name(self):
method unique_id (line 170) | def unique_id(self):
method icon (line 178) | def icon(self):
method extra_state_attributes (line 183) | def extra_state_attributes(self):
method device_info (line 188) | def device_info(self) -> DeviceInfo | None:
class CoordinatedEnvoyEntity (line 210) | class CoordinatedEnvoyEntity(EnvoyEntity, CoordinatorEntity):
method __init__ (line 211) | def __init__(
method native_value (line 224) | def native_value(self):
class EnvoyInverterEntity (line 228) | class EnvoyInverterEntity(CoordinatedEnvoyEntity):
method __init__ (line 231) | def __init__(
method native_value (line 250) | def native_value(self):
method extra_state_attributes (line 262) | def extra_state_attributes(self):
class EnvoyBatteryEntity (line 274) | class EnvoyBatteryEntity(CoordinatedEnvoyEntity):
method __init__ (line 277) | def __init__(
method native_value (line 296) | def native_value(self):
method extra_state_attributes (line 308) | def extra_state_attributes(self):
class TotalBatteryCapacityEntity (line 326) | class TotalBatteryCapacityEntity(CoordinatedEnvoyEntity):
method __init__ (line 327) | def __init__(
method native_value (line 346) | def native_value(self):
class TotalBatteryPercentageEntity (line 363) | class TotalBatteryPercentageEntity(CoordinatedEnvoyEntity):
method __init__ (line 364) | def __init__(
method native_value (line 383) | def native_value(self):
class BatteryEnergyChangeEntity (line 397) | class BatteryEnergyChangeEntity(EnvoyEntity):
method __init__ (line 398) | def __init__(
method async_added_to_hass (line 421) | async def async_added_to_hass(self):
method native_value (line 464) | def native_value(self):
Condensed preview — 35 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (174K chars).
[
{
"path": ".gitignore",
"chars": 91,
"preview": "# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n#vscode\n.vscode/"
},
{
"path": "README.md",
"chars": 20993,
"preview": "[](https://github.com/hacs/integra"
},
{
"path": "custom_components/enphase_envoy_custom/__init__.py",
"chars": 5974,
"preview": "\"\"\"The Enphase Envoy integration.\"\"\"\nfrom __future__ import annotations\n\nfrom datetime import timedelta\nimport logging\n\n"
},
{
"path": "custom_components/enphase_envoy_custom/binary_sensor.py",
"chars": 2651,
"preview": "from homeassistant.core import HomeAssistant\nfrom homeassistant.config_entries import ConfigEntry\nfrom homeassistant.hel"
},
{
"path": "custom_components/enphase_envoy_custom/config_flow.py",
"chars": 10269,
"preview": "\"\"\"Config flow for Enphase Envoy integration.\"\"\"\nfrom __future__ import annotations\n\nimport contextlib\nimport logging\nfr"
},
{
"path": "custom_components/enphase_envoy_custom/const.py",
"chars": 17730,
"preview": "\"\"\"The enphase_envoy component.\"\"\"\n\nfrom homeassistant.components.binary_sensor import (\n BinarySensorDeviceClass,\n "
},
{
"path": "custom_components/enphase_envoy_custom/diagnostics.py",
"chars": 2089,
"preview": "\"\"\"Diagnostics support for Enphase Envoy.\"\"\"\nfrom __future__ import annotations\n\nfrom typing import Any\n\nfrom attr impor"
},
{
"path": "custom_components/enphase_envoy_custom/envoy_reader.py",
"chars": 65344,
"preview": "\"\"\"Module to read production and consumption values from an Enphase Envoy on the local network.\"\"\"\nimport argparse\nimpor"
},
{
"path": "custom_components/enphase_envoy_custom/manifest.json",
"chars": 351,
"preview": "{\n \"domain\": \"enphase_envoy\",\n \"name\": \"Enphase Envoy (DEV)\",\n \"documentation\": \"https://github.com/briancmpbll/home_"
},
{
"path": "custom_components/enphase_envoy_custom/sensor.py",
"chars": 14733,
"preview": "\"\"\"Support for Enphase Envoy solar energy monitor.\"\"\"\nfrom __future__ import annotations\n\nimport datetime\n\nfrom time imp"
},
{
"path": "custom_components/enphase_envoy_custom/strings.json",
"chars": 2200,
"preview": "{\n \"config\": {\n \"flow_title\": \"{serial} ({host})\",\n \"step\": {\n \"user\": {\n \"description\": \"- If your E"
},
{
"path": "custom_components/enphase_envoy_custom/translations/ca.json",
"chars": 694,
"preview": "{\n \"config\": {\n \"abort\": {\n \"already_configured\": \"El dispositiu ja est\\u00e0 configurat\",\n "
},
{
"path": "custom_components/enphase_envoy_custom/translations/cs.json",
"chars": 775,
"preview": "{\n \"config\": {\n \"abort\": {\n \"already_configured\": \"Za\\u0159\\u00edzen\\u00ed je ji\\u017e nastaveno\",\n"
},
{
"path": "custom_components/enphase_envoy_custom/translations/de.json",
"chars": 685,
"preview": "{\n \"config\": {\n \"abort\": {\n \"already_configured\": \"Ger\\u00e4t ist bereits konfiguriert\",\n "
},
{
"path": "custom_components/enphase_envoy_custom/translations/el.json",
"chars": 89,
"preview": "{\n \"config\": {\n \"step\": {\n \"user\": {\n }\n }\n }\n}"
},
{
"path": "custom_components/enphase_envoy_custom/translations/en.json",
"chars": 2177,
"preview": "{\n \"config\": {\n \"abort\": {\n \"already_configured\": \"Device is already configured\",\n \"reauth_successful\": \"R"
},
{
"path": "custom_components/enphase_envoy_custom/translations/es-419.json",
"chars": 67,
"preview": "{\n \"config\": {\n \"flow_title\": \"{serial} ({host})\"\n }\n}"
},
{
"path": "custom_components/enphase_envoy_custom/translations/es.json",
"chars": 697,
"preview": "{\n \"config\": {\n \"abort\": {\n \"already_configured\": \"El dispositivo ya est\\u00e1 configurado\",\n "
},
{
"path": "custom_components/enphase_envoy_custom/translations/et.json",
"chars": 649,
"preview": "{\n \"config\": {\n \"abort\": {\n \"already_configured\": \"Seade on juba h\\u00e4\\u00e4lestatud\",\n "
},
{
"path": "custom_components/enphase_envoy_custom/translations/fr.json",
"chars": 2410,
"preview": "{\n \"config\": {\n \"abort\": {\n \"already_configured\": \"L'appareil est d\\u00e9j\\u00e0 configur\\u00e9\",\n "
},
{
"path": "custom_components/enphase_envoy_custom/translations/he.json",
"chars": 1038,
"preview": "{\n \"config\": {\n \"abort\": {\n \"already_configured\": \"\\u05ea\\u05e6\\u05d5\\u05e8\\u05ea \\u05d4\\u05d4\\u05e"
},
{
"path": "custom_components/enphase_envoy_custom/translations/hu.json",
"chars": 748,
"preview": "{\n \"config\": {\n \"abort\": {\n \"already_configured\": \"Az eszk\\u00f6z m\\u00e1r konfigur\\u00e1lva van\",\n"
},
{
"path": "custom_components/enphase_envoy_custom/translations/id.json",
"chars": 656,
"preview": "{\n \"config\": {\n \"abort\": {\n \"already_configured\": \"Perangkat sudah dikonfigurasi\",\n \"rea"
},
{
"path": "custom_components/enphase_envoy_custom/translations/it.json",
"chars": 694,
"preview": "{\n \"config\": {\n \"abort\": {\n \"already_configured\": \"Il dispositivo \\u00e8 gi\\u00e0 configurato\",\n "
},
{
"path": "custom_components/enphase_envoy_custom/translations/ja.json",
"chars": 871,
"preview": "{\n \"config\": {\n \"abort\": {\n \"already_configured\": \"\\u30c7\\u30d0\\u30a4\\u30b9\\u306f\\u3059\\u3067\\u306b"
},
{
"path": "custom_components/enphase_envoy_custom/translations/ko.json",
"chars": 922,
"preview": "{\n \"config\": {\n \"abort\": {\n \"already_configured\": \"\\uae30\\uae30\\uac00 \\uc774\\ubbf8 \\uad6c\\uc131\\ub4"
},
{
"path": "custom_components/enphase_envoy_custom/translations/nl.json",
"chars": 2534,
"preview": "{\n \"config\": {\n \"abort\": {\n \"already_configured\": \"Apparaat is al geconfigureerd\",\n \"rea"
},
{
"path": "custom_components/enphase_envoy_custom/translations/no.json",
"chars": 646,
"preview": "{\n \"config\": {\n \"abort\": {\n \"already_configured\": \"Enheten er allerede konfigurert\",\n \"r"
},
{
"path": "custom_components/enphase_envoy_custom/translations/pl.json",
"chars": 762,
"preview": "{\n \"config\": {\n \"abort\": {\n \"already_configured\": \"Urz\\u0105dzenie jest ju\\u017c skonfigurowane\",\n "
},
{
"path": "custom_components/enphase_envoy_custom/translations/ru.json",
"chars": 1443,
"preview": "{\n \"config\": {\n \"abort\": {\n \"already_configured\": \"\\u042d\\u0442\\u043e \\u0443\\u0441\\u0442\\u0440\\u043"
},
{
"path": "custom_components/enphase_envoy_custom/translations/sv.json",
"chars": 459,
"preview": "{\n \"config\": {\n \"abort\": {\n \"already_configured\": \"Enheten \\u00e4r redan konfigurerad\"\n },\n "
},
{
"path": "custom_components/enphase_envoy_custom/translations/tr.json",
"chars": 733,
"preview": "{\n \"config\": {\n \"abort\": {\n \"already_configured\": \"Cihaz zaten yap\\u0131land\\u0131r\\u0131lm\\u0131\\u"
},
{
"path": "custom_components/enphase_envoy_custom/translations/zh-Hans.json",
"chars": 180,
"preview": "{\n \"config\": {\n \"step\": {\n \"user\": {\n \"data\": {\n \"password\": \"\\u5"
},
{
"path": "custom_components/enphase_envoy_custom/translations/zh-Hant.json",
"chars": 733,
"preview": "{\n \"config\": {\n \"abort\": {\n \"already_configured\": \"\\u88dd\\u7f6e\\u5df2\\u7d93\\u8a2d\\u5b9a\\u5b8c\\u6210"
},
{
"path": "hacs.json",
"chars": 89,
"preview": "{\n \"name\": \"Enphase Envoy (DEV)\",\n \"render_readme\": true,\n \"content_in_root\": false\n}\n"
}
]
About this extraction
This page contains the full source code of the briancmpbll/home_assistant_custom_envoy GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 35 files (159.4 KB), approximately 37.3k tokens, and a symbol index with 125 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.