[
  {
    "path": ".gitignore",
    "content": "# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n#vscode\n.vscode/"
  },
  {
    "path": "README.md",
    "content": "[![hacs_badge](https://img.shields.io/badge/HACS-Custom-41BDF5.svg?style=for-the-badge)](https://github.com/hacs/integration#readme)\n\nThis 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. \n\nIt still supports Envoys with firmware versions before 7.x including R3 versions for legacy models.\n\nWorks 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).\n\n# Installation\n\n1. Install [HACS](https://hacs.xyz/) if you haven't already\n2. Add this repository as a [custom integration repository](https://hacs.xyz/docs/faq/custom_repositories) in HACS\n4. Restart home assistant\n5. Add the integration through the home assistant configuration flow, [specify settings as needed](#initial-configuration-details)\n7. The integration has some run-time configuration options, [set these as desired](#runtime-configuration) after startup.\n8. The integration has some hidden entities, [enable these](#disabled-entities) to use these.\n\n\n# Usage\n\n## Initial Configuration details\n\nThe initial configuration window requires you to enter the details how to access the Envoy. Following fields have to be filled.\n\n### Host\n\nEnter 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.\n\n### Username and Password\n\nSpecify 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:\n\n- 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.\n\n- 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.\n\n- 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/).\n- In some cases, you need to use the username `envoy` with the last 6 digits of the unit's serial number as password.\n\nSee [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.\n\n**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.\n\n## Serial number\n\nSpecify the Envoy serial number. If the Envoy is auto-discovered it will be pre-filled.\n\n### Use Enlighten\n\nEnable 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.\n\n## Runtime configuration\n\nOnce the Envoy has been running and is operational, the following configuration items are available:\n\n### Scan Interval - Time between Entity updates\n\nBy 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).\n\nWhat 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.\n\n**Note** Envoy Metered has a data streaming option to bring in data as it comes available which is not currently supported by this integration.\n\n### Timeout for single Envoy Page\nSpecifies 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.\n\n### How many retries in getting an Envoy response\nSpecifies 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.\n\n### Time between 2 retries\nOptionally a wait time can be inserted between 2 retries. Only change this in special circumstances.\n\n### Overall Timeout\nWhen 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.\n\n### Do not use production json\nThis 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.  \n\nThe 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.\n\n## Disabled entities\nThe 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.\n\n# Firmware and its impact\n\nEnphase 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.\n\n# Different models have different features\n\nThis integration supports various models but as models have different features they will not all provide the same data. Brief list of reported data below.\n\n## ENVOY C / R / LCD\n\n- Current power production, today's, last 7 days and lifetime energy production. And Active inverter count, which is disabled by default.\n\n## IQ Gateway / ENVOY S standard (non metered)\n\n- Current power production, today's, last 7 days and lifetime energy production.\n- Current power production for each connected inverter.\n\n## IQ Gateway / ENVOY S standard metered\n\nWhat 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.\n\n### with connected current transformer clamps\n\n- Current power production and consumption, today's, last 7 days and lifetime energy production and consumption over all phases.\n- 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.\n- Current net power consumption and lifetime net energy production (export) and consumption (import) over all phases.\n- Current net power consumption and lifetime net energy production (export) and consumption (import) for each individual phase named L1, L2 and L3.\n- Next entities are disabled by default and need to be enabled in the entities configuration screen\n  - Power production for each connected inverter.\n  - Power factor over all phases.\n  - Power factor for each individual phase named L1, L2 and L3.\n  - Voltage over all phases. (Be aware this is the summed Voltage of all measured phases!)\n  - Voltage for each individual phase named L1, L2 and L3.\n  - Frequency over all phases.\n  - Frequency for each individual phase named L1, L2 and L3.\n  - Production and consumption Current (amps) over all phases.\n  - Production and consumption Current (amps) for each individual phase named L1, L2 and L3.\n\n**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.\n\n### without connected current transformer clamps\n\nThe 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:\n\n- Current power production and lifetime energy production. Today's and last 7 day energy production reportedly are both solid 0.\n- Lifetime Energy production reportedly resets to zero roughly every 1.19 MWh.\n- Current power production for each connected inverter.\n\n**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.  \n\n# Device and Entities\n\nThe naming scheme used is based on the Envoy and inverter Serial numbers.\n\n## Device\n\nA device `Envoy <serialnumber>` is created with sensor entities for accessible data.\n\n## Envoy Sensors\n\n|Sensor name|Sensor ID|Units|remarks|\n|-----|-----|----|----|\n|Envoy \\<sn\\> Current Power Production|sensor.Envoy_\\<sn\\>_current_power_production|W||\n|Envoy \\<sn\\> Today's Energy production|sensor.Envoy_\\<sn\\>_todays_energy_production|Wh|1|\n|Envoy \\<sn\\> Last Seven Days Energy Production|sensor.Envoy_\\<sn\\>_last_seven_days_energy_production|Wh|1|\n|Envoy \\<sn\\> Lifetime Energy Production|sensor.Envoy_\\<sn\\>_lifetime_energy_production|Wh|2|\n|Envoy \\<sn\\> Lifetime Net Energy Production|sensor.Envoy_\\<sn\\>_lifetime_net_energy_production|Wh|4|\n|Envoy \\<sn\\> Current Power Consumption|sensor.Envoy_\\<sn\\>_current_power_consumption|W||\n|Envoy \\<sn\\> Current Net Power Consumption|sensor.Envoy_\\<sn\\>_current_net_power_consumption|W|4|\n|Envoy \\<sn\\> Today's Energy Consumption|sensor.Envoy_\\<sn\\>_todays_energy_consumption|Wh|4,5|\n|Envoy \\<sn\\> Last Seven Days Energy Consumption|sensor.Envoy_\\<sn\\>_last_seven_days_energy_consumption|Wh|4|\n|Envoy \\<sn\\> Lifetime Energy Consumption|sensor.Envoy_\\<sn\\>_lifetime_energy_consumption|Wh|4|\n|Envoy \\<sn\\> Lifetime Net Energy Consumption|sensor.Envoy_\\<sn\\>_lifetime_net_energy_consumption|Wh|4,7,8|\n|Envoy \\<sn\\> Power Factor|sensor.Envoy_\\<sn\\>_pf||4,9|\n|Envoy \\<sn\\> Voltage|sensor.Envoy_\\<sn\\>_voltage|V|4,9|\n|Envoy \\<sn\\> Frequency|sensor.Envoy_\\<sn\\>_frequency|Wh|4,9|\n|Envoy \\<sn\\> Consumption Current|sensor.Envoy_\\<sn\\>_consumption_Current|A|4,9|\n|Envoy \\<sn\\> Production Current|sensor.Envoy_\\<sn\\>_production_Current|A|4,9|\n|Envoy \\<sn\\> Active Inverter Count|sensor.Envoy_\\<sn\\>_active_inverter_count||9,10|\n||||\n|Grid Status |binary_sensor.grid_status|On/Off|3|\n||||\n|Envoy \\<sn\\> Current Power Production L\\<n\\>|sensor.Envoy_\\<sn\\>_current_power_production_l\\<n\\>|W|4,5|\n|Envoy \\<sn\\> Today's Energy production L\\<n\\>|sensor.Envoy_\\<sn\\>_todays_energy_production_l\\<n\\>|Wh|4,5|\n|Envoy \\<sn\\> Last Seven Days Energy Production L\\<n\\>|sensor.Envoy_\\<sn\\>_last_seven_days_energy_production_l\\<n\\>|Wh|4,5|\n|Envoy \\<sn\\> Lifetime Energy Production L\\<n\\>|sensor.Envoy_\\<sn\\>_lifetime_energy_consumption_l\\<n\\>|Wh|4,5|\n|Envoy \\<sn\\> Lifetime Net Energy Production L\\<n\\>|sensor.Envoy_\\<sn\\>_lifetime_net_energy_production_l\\<n\\>|Wh|4,5,7,8|\n|Envoy \\<sn\\> Current Power Consumption L\\<n\\>|sensor.Envoy_\\<sn\\>_current_power_consumption_l\\<n\\>|W|4,5|\n|Envoy \\<sn\\> Current Net Power Consumption L\\<n\\>|sensor.Envoy_\\<sn\\>_current_net_power_consumption_l\\<n\\>|W|4,5|\n|Envoy \\<sn\\> Today's Energy Consumption L\\<n\\>|sensor.Envoy_\\<sn\\>_todays_energy_consumption_l\\<n\\>|Wh|4,5,6|\n|Envoy \\<sn\\> Last Seven Days Energy Consumption L\\<n\\>|sensor.Envoy_\\<sn\\>_last_seven_days_energy_consumption L\\<n\\>|Wh|4,5,6|\n|Envoy \\<sn\\> Lifetime Energy Consumption L\\<n\\>|sensor.Envoy_\\<sn\\>_lifetime_energy_consumption_l\\<n\\>|Wh|4,5,6|\n|Envoy \\<sn\\> Lifetime Net Energy Consumption L\\<n\\>|sensor.Envoy_\\<sn\\>_lifetime_net_energy_consumption_l\\<n\\>|Wh|4,5,6,7,8|\n|Envoy \\<sn\\> Power Factor L\\<n\\>|sensor.Envoy_\\<sn\\>_pf||4,5,9|\n|Envoy \\<sn\\> Voltage L\\<n\\>|sensor.Envoy_\\<sn\\>_voltage|V|4,5,9|\n|Envoy \\<sn\\> Frequency L\\<n\\>|sensor.Envoy_\\<sn\\>_frequency|Wh|4,5,9|\n|Envoy \\<sn\\> Consumption Current L\\<n\\>|sensor.Envoy_\\<sn\\>_consumption_Current|A|4,5,9|\n|Envoy \\<sn\\> Production Current L\\<n\\>|sensor.Envoy_\\<sn\\>_production_Current|A|4,5,9|\n|Envoy \\<sn\\> |sensor.Envoy_\\<sn\\>_|Wh|4,5|\n\n1 Always zero for Envoy Metered without meters.  \n2 Reportedly resets to zero when reaching ~1.92MWh for Envoy Metered without meters.  \n3 Not available on Legacy models and ENVOY Standard with recent firmware.  \n4 Only on Envoy metered with configured and connected meters.  \n5 L\\<n\\> L1,L2,L3, availability depends on which and how many phases are connected and configured.  \n6 Reportedly always zero on Envoy metered with Firmware D8.  \n7 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.  \n8 Only when consumption CT is installed in 'Load with Solar' mode. In 'Load only' mode values have no meaning.  \n9 Disabled by default and must be enabled in the entities configuration screen. These are values from the consumption CT.  \n10 Only available on legacy Envoy.\n\n## Inverter Sensors\n\nFor each inverter a sensor for current power production is created.\n\n|Sensor name|Sensor ID|UNits|remarks|\n|-----|-----|----|----|\n|Envoy \\<sn\\> Inverter \\<sn\\>|sensor.Envoy_\\<sn\\>\\_Inverter_\\<sn\\>|W|1|\n\n1: Not available on Legacy models\n\n**Note** the entity 'Last Updated' for each inverter is currently not provided.  \n\n**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)\n\n## Battery Sensors\n\nFor 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.\n\n|Sensor name|Sensor ID|Units|remarks|\n|-----|-----|----|----|\n|Envoy \\<sn\\> Battery \\<sn\\>|sensor.Envoy_\\<sn\\>\\_Battery_\\<sn\\>|%|1|\n|Envoy \\<sn\\> Total Battery Percentage|sensor.Envoy_\\<sn\\>\\_total_battery_percentage|%|1|\n|Envoy \\<sn\\> Current Battery Capacity|sensor.Envoy_\\<sn\\>\\_current_battery_capacity|Wh|1|\n|Envoy \\<sn\\> Battery Energy Charged|sensor.Envoy_\\<sn\\>\\_battery_energy_charged|Wh|1|\n|Envoy \\<sn\\> Battery Energy Discharged|sensor.Envoy_\\<sn\\>\\_battery_energy_charged|Wh|1|\n\n1: Not available on Legacy models and ENVOYS-S Standard\n\n# How to switch to Enphase token authorization\n\nOnce the envoy received the new firmware that requires token authorization, data collection will fail. To switch to the token usage execute next steps:\n\n- [Install](#installation) the Custom integration using HACS\n- In Home Assistant go to the Enphase Envoy integration and delete it.\n- Restart Home Assistant\n- The envoy will be auto-discovered again. If not add an Envoy Integration manually.\n- In the configuration screen now use your Enphase Enlighten username and password and enable the 'Use Enlighten' option.\n- Once it's configured it will continue reporting data in the same entities.\n- Optionally change the default time interval from 60 to what is preferred.\n\n# Troubleshooting\n\nWhen issues occur with this integration some items to check are:\n\n- 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.\n- What model are you using. This will drive what can be expected.\n- What firmware is your model using, Was a firmware update recently pushed to the device?\n- 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.\n  - 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.\n  - 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>)\n- 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.\n- 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.\n- 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.\n- When an error is reported during the initial configuration, inspect the home-assistant.log file in the /config folder. It will reveal what happened:\n  - Validate input, getdata returned RuntimeError: Could not Authenticate with Enlighten, status: 401, <Response [401 Unauthorized]> : Check if Enphase username and/or password are correct\n  - 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\n  - Validate input, getdata returned HTTPError: All connection attempts failed  : failure to connect to envoy.  : Validate if correct IP address of the Envoy is used\n  - 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.\n- 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.  \n"
  },
  {
    "path": "custom_components/enphase_envoy_custom/__init__.py",
    "content": "\"\"\"The Enphase Envoy integration.\"\"\"\nfrom __future__ import annotations\n\nfrom datetime import timedelta\nimport logging\n\nimport async_timeout\nfrom .envoy_reader import EnvoyReader\nimport httpx\nfrom numpy import isin\n\nfrom homeassistant.config_entries import ConfigEntry\nfrom homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME\nfrom homeassistant.core import HomeAssistant\nfrom homeassistant.exceptions import ConfigEntryAuthFailed\nfrom homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed\nfrom homeassistant.helpers.storage import Store\n\nfrom .const import COORDINATOR, DOMAIN, NAME, PLATFORMS, SENSORS, CONF_USE_ENLIGHTEN, CONF_SERIAL, PHASE_SENSORS, DEFAULT_SCAN_INTERVAL\n\nSCAN_INTERVAL = timedelta(seconds=60)\nSTORAGE_KEY = \"envoy\"\nSTORAGE_VERSION = 1\nFETCH_RETRIES = 1\nFETCH_TIMEOUT_SECONDS = 30\nFETCH_HOLDOFF_SECONDS = 0\nCOLLECTION_TIMEOUT_SECONDS = 55\n\n_LOGGER = logging.getLogger(__name__)\n\n\nasync def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:\n    \"\"\"Set up Enphase Envoy from a config entry.\"\"\"\n\n    config = entry.data\n    options = entry.options\n    name = config[CONF_NAME]\n\n    # Setup persistent storage, to save tokens between home assistant restarts\n    store = Store(hass, STORAGE_VERSION, \".\".join([STORAGE_KEY, entry.entry_id]))\n\n    envoy_reader = EnvoyReader(\n        config[CONF_HOST],\n        username=config[CONF_USERNAME],\n        password=config[CONF_PASSWORD],\n        enlighten_user=config[CONF_USERNAME],\n        enlighten_pass=config[CONF_PASSWORD],\n        inverters=True,\n#        async_client=get_async_client(hass),\n        use_enlighten_owner_token=config.get(CONF_USE_ENLIGHTEN, False),\n        enlighten_serial_num=config[CONF_SERIAL],\n        https_flag='s' if config.get(CONF_USE_ENLIGHTEN, False) else '',\n        store=store,\n        fetch_retries=options.get(\"data_fetch_retry_count\", FETCH_RETRIES),\n        fetch_timeout_seconds=options.get(\"data_fetch_timeout_seconds\", FETCH_TIMEOUT_SECONDS),\n        fetch_holdoff_seconds=options.get(\"data_fetch_holdoff_seconds\", FETCH_HOLDOFF_SECONDS),\n        do_not_use_production_json=options.get(\"do_not_use_production_json\",False),\n    )\n    await envoy_reader._sync_store()\n\n    async def async_update_data():\n        \"\"\"Fetch data from API endpoint.\"\"\"\n        data = {}\n        async with async_timeout.timeout(options.get(\"data_collection_timeout_seconds\", COLLECTION_TIMEOUT_SECONDS)):\n            try:\n                await envoy_reader.getData()\n            except httpx.HTTPStatusError as err:\n                raise ConfigEntryAuthFailed from err\n            except httpx.HTTPError as err:\n                raise UpdateFailed(f\"Error communicating with API: {err}\") from err\n\n            for description in SENSORS:\n                if description.key == \"inverters\":\n                    data[\n                        \"inverters_production\"\n                    ] = await envoy_reader.inverters_production()\n\n                elif description.key == \"batteries\":\n                    battery_data = await envoy_reader.battery_storage()\n                    if isinstance(battery_data, list) and len(battery_data) > 0:\n                        battery_dict = {}\n                        for item in battery_data:\n                            battery_dict[item[\"serial_num\"]] = item\n\n                        data[description.key] = battery_dict\n\n                elif (description.key not in [\"current_battery_capacity\", \"total_battery_percentage\"]):\n                    data[description.key] = await getattr(\n                        envoy_reader, description.key\n                    )()\n\n            for description in PHASE_SENSORS:\n                if description.key[:-2] in [\n                    \"none_known_at_this_time_\"\n                ]:\n                    # call phase function for these\n                    data[description.key] = await getattr(envoy_reader, description.key[:-3]+\"_phase\")( description.key[-2:].lower())\n\n                else:\n                \n                    #catchall for non-specified phase sensors\n                    #get attributes for phase sensors based on key name\n                    #Removes _L1, _L2 or _L3 from key to call base non-phased function\n                    #Pass l1, l2 or l3 as parameter to _phase function\n                    data[description.key] = await getattr(envoy_reader, description.key[:-3])( description.key[-2:].lower())\n \n                        \n            data[\"grid_status\"] = await envoy_reader.grid_status()\n            data[\"envoy_info\"] = await envoy_reader.envoy_info()\n\n            _LOGGER.debug(\"Retrieved data from API: %s\", data)\n\n            await envoy_reader._sync_store()\n            \n            return data\n\n    coordinator = DataUpdateCoordinator(\n        hass,\n        _LOGGER,\n        name=f\"envoy {name}\",\n        update_method=async_update_data,\n        update_interval=timedelta(\n            seconds=options.get(\"data_interval\", DEFAULT_SCAN_INTERVAL)\n        )\n    )\n\n    try:\n        await coordinator.async_config_entry_first_refresh()\n    except ConfigEntryAuthFailed:\n        envoy_reader.get_inverters = False\n        await coordinator.async_config_entry_first_refresh()\n\n    if not entry.unique_id:\n        try:\n            serial = await envoy_reader.get_full_serial_number()\n        except httpx.HTTPError:\n            pass\n        else:\n            hass.config_entries.async_update_entry(entry, unique_id=serial)\n\n    hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {\n        COORDINATOR: coordinator,\n        NAME: name,\n    }\n\n    await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)\n\n    return True\n\n\nasync def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:\n    \"\"\"Unload a config entry.\"\"\"\n    unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)\n    if unload_ok:\n        hass.data[DOMAIN].pop(entry.entry_id)\n    return unload_ok\n"
  },
  {
    "path": "custom_components/enphase_envoy_custom/binary_sensor.py",
    "content": "from homeassistant.core import HomeAssistant\nfrom homeassistant.config_entries import ConfigEntry\nfrom homeassistant.helpers.entity_platform import AddEntitiesCallback\nfrom homeassistant.helpers.update_coordinator import CoordinatorEntity\nfrom homeassistant.components.binary_sensor import BinarySensorEntity\nfrom homeassistant.helpers.entity import DeviceInfo\n\nfrom .const import COORDINATOR, DOMAIN, NAME, BINARY_SENSORS, ICON\n\nasync def async_setup_entry(\n    hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback\n) -> None:\n  data = hass.data[DOMAIN][config_entry.entry_id]\n  coordinator = data[COORDINATOR]\n  name = data[NAME]\n\n  entities = []\n  for sensor_description in BINARY_SENSORS:\n    if sensor_description.key == \"grid_status\":\n        if coordinator.data.get(\"grid_status\") is not None:\n            entities.append(\n                EnvoyGridStatusEntity(\n                    sensor_description,\n                    sensor_description.name,\n                    name,\n                    config_entry.unique_id,\n                    None,\n                    coordinator,\n                )\n            )\n\n  async_add_entities(entities)\n\n\nclass EnvoyGridStatusEntity(CoordinatorEntity, BinarySensorEntity):\n  def __init__(\n      self,\n      description,\n      name,\n      device_name,\n      device_serial_number,\n      serial_number,\n      coordinator,\n  ):\n      self.entity_description = description\n      self._name = name\n      self._serial_number = serial_number\n      self._device_name = device_name\n      self._device_serial_number = device_serial_number\n      CoordinatorEntity.__init__(self, coordinator)\n\n  @property\n  def icon(self):\n      \"\"\"Icon to use in the frontend, if any.\"\"\"\n      return ICON\n\n  @property\n  def name(self):\n      \"\"\"Return the name of the sensor.\"\"\"\n      return self._name\n\n  @property\n  def unique_id(self):\n      \"\"\"Return the unique id of the sensor.\"\"\"\n      if self._serial_number:\n          return self._serial_number\n      if self._device_serial_number:\n          return f\"{self._device_serial_number}_{self.entity_description.key}\"\n\n  @property\n  def device_info(self) -> DeviceInfo or None:\n      \"\"\"Return the device_info of the device.\"\"\"\n      if not self._device_serial_number:\n          return None\n      return DeviceInfo(\n          identifiers={(DOMAIN, str(self._device_serial_number))},\n          manufacturer=\"Enphase\",\n          model=\"Envoy\",\n          name=self._device_name,\n      )\n\n  @property\n  def is_on(self) -> bool:\n      \"\"\"Return the status of the requested attribute.\"\"\"\n      return self.coordinator.data.get(\"grid_status\") == \"closed\"\n"
  },
  {
    "path": "custom_components/enphase_envoy_custom/config_flow.py",
    "content": "\"\"\"Config flow for Enphase Envoy integration.\"\"\"\nfrom __future__ import annotations\n\nimport contextlib\nimport logging\nfrom typing import Any\n\nfrom .envoy_reader import EnvoyReader\nimport httpx\nimport voluptuous as vol\n\nfrom homeassistant import config_entries\nfrom homeassistant.components import network\nfrom homeassistant.components import zeroconf\nfrom homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME\nfrom homeassistant.core import HomeAssistant, callback\nfrom homeassistant.data_entry_flow import FlowResult\nfrom homeassistant.exceptions import HomeAssistantError\nfrom homeassistant.util.network import is_ipv4_address\n\nfrom .const import DOMAIN, CONF_SERIAL, CONF_USE_ENLIGHTEN, DEFAULT_SCAN_INTERVAL\n\n_LOGGER = logging.getLogger(__name__)\n\nENVOY = \"Envoy\"\n\n\nasync def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> EnvoyReader:\n    \"\"\"Validate the user input allows us to connect.\"\"\"\n    envoy_reader = EnvoyReader(\n        data[CONF_HOST],\n        username=data[CONF_USERNAME],\n        password=data[CONF_PASSWORD],\n        enlighten_user=data[CONF_USERNAME],\n        enlighten_pass=data[CONF_PASSWORD],\n        inverters=False,\n#        async_client=get_async_client(hass),\n        use_enlighten_owner_token=data.get(CONF_USE_ENLIGHTEN, False),\n        enlighten_serial_num=data[CONF_SERIAL],\n        https_flag='s' if data.get(CONF_USE_ENLIGHTEN,False) else '',\n        fetch_timeout_seconds=60\n    )\n\n    try:\n        await envoy_reader.getData()\n    except httpx.HTTPStatusError as err:\n        _LOGGER.warning(\"Validate input, getdata returned HTTPStatusError: %s\",err)\n        raise InvalidAuth from err\n    except (httpx.HTTPError) as err:\n        _LOGGER.warning(\"Validate input, getdata returned HTTPError: %s\",err)\n        raise CannotConnect from err\n    except (RuntimeError) as err:\n        _LOGGER.warning(\"Validate input, getdata returned RuntimeError: %s\",err)\n        raise\n\n    return envoy_reader\n\nasync def ipv4asdefault(hass: HomeAssistant):\n    adapters = await network.async_get_adapters(hass)\n    for adapter in adapters:\n        if adapter[\"default\"]:\n            return adapter[\"ipv4\"] is not None\n    return False\n\nclass ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):\n    \"\"\"Handle a config flow for Enphase Envoy.\"\"\"\n\n    VERSION = 1\n\n    def __init__(self):\n        \"\"\"Initialize an envoy flow.\"\"\"\n        self.ip_address = None\n        self.username = None\n        self._reauth_entry = None\n\n    @callback\n    def _async_generate_schema(self):\n        \"\"\"Generate schema.\"\"\"\n        schema = {}\n\n        if self.ip_address:\n            schema[vol.Required(CONF_HOST, default=self.ip_address)] = vol.In(\n                [self.ip_address]\n            )\n        else:\n            schema[vol.Required(CONF_HOST)] = str\n\n        schema[vol.Optional(CONF_USERNAME, default=self.username)] = str\n        schema[vol.Optional(CONF_PASSWORD, default=\"\")] = str\n        schema[vol.Optional(CONF_SERIAL, default=self.unique_id)] = str\n        schema[vol.Optional(CONF_USE_ENLIGHTEN)] = bool\n        return vol.Schema(schema)\n\n    @callback\n    def _async_current_hosts(self):\n        \"\"\"Return a set of hosts.\"\"\"\n        return {\n            entry.data[CONF_HOST]\n            for entry in self._async_current_entries(include_ignore=False)\n            if CONF_HOST in entry.data\n        }\n\n    async def async_step_zeroconf(\n        self, discovery_info: zeroconf.ZeroconfServiceInfo\n    ) -> FlowResult:\n        \"\"\"Handle a flow initialized by zeroconf discovery.\"\"\"\n        serial = discovery_info.properties[\"serialnum\"]\n        await self.async_set_unique_id(serial)\n\n        ipv4_default = await ipv4asdefault(self.hass)\n\n        if ipv4_default and not is_ipv4_address(discovery_info.host):\n            return self.async_abort(reason=\"not_ipv4_address\")\n                \n        # autodiscovery is updating the ip address of an existing envoy with matching serial to new detected ip adress\n        self.ip_address = discovery_info.host\n        self._abort_if_unique_id_configured({CONF_HOST: self.ip_address})\n        for entry in self._async_current_entries(include_ignore=False):\n            if (\n                entry.unique_id is None\n                and CONF_HOST in entry.data\n                and entry.data[CONF_HOST] == self.ip_address\n            ):\n                title = f\"{ENVOY} {serial}\" if entry.title == ENVOY else ENVOY\n                self.hass.config_entries.async_update_entry(\n                    entry, title=title, unique_id=serial\n                )\n                self.hass.async_create_task(\n                    self.hass.config_entries.async_reload(entry.entry_id)\n                )\n                return self.async_abort(reason=\"already_configured\")\n\n        return await self.async_step_user()\n\n    async def async_step_reauth(self, user_input):\n        \"\"\"Handle configuration by re-auth.\"\"\"\n        self._reauth_entry = self.hass.config_entries.async_get_entry(\n            self.context[\"entry_id\"]\n        )\n        return await self.async_step_user()\n\n    def _async_envoy_name(self) -> str:\n        \"\"\"Return the name of the envoy.\"\"\"\n        if self.unique_id:\n            return f\"{ENVOY} {self.unique_id}\"\n        return ENVOY\n\n    async def _async_set_unique_id_from_envoy(self, envoy_reader: EnvoyReader) -> bool:\n        \"\"\"Set the unique id by fetching it from the envoy.\"\"\"\n        serial = None\n        with contextlib.suppress(httpx.HTTPError):\n            serial = await envoy_reader.get_full_serial_number()\n        if serial:\n            await self.async_set_unique_id(serial)\n            return True\n        return False\n\n    async def async_step_user(\n        self, user_input: dict[str, Any] | None = None\n    ) -> FlowResult:\n        \"\"\"Handle the initial step.\"\"\"\n        errors = {}\n\n        if user_input is not None:\n            if (\n                not self._reauth_entry\n                and user_input[CONF_HOST] in self._async_current_hosts()\n            ):\n                return self.async_abort(reason=\"already_configured\")\n            try:\n                envoy_reader = await validate_input(self.hass, user_input)\n            except RuntimeError as rerr:\n                errors[\"base\"] = \"invalid_auth\"\n            except CannotConnect as cerr:\n                errors[\"base\"] = \"cannot_connect\"\n            except InvalidAuth:\n                errors[\"base\"] = \"invalid_auth\"\n            except Exception as exc:  # pylint: disable=broad-except\n                _LOGGER.exception(\"Unexpected exception in validate input %s\",exc)\n                errors[\"base\"] = \"unknown\"\n            else:\n                data = user_input.copy()\n                data[CONF_NAME] = self._async_envoy_name()\n\n                if self._reauth_entry:\n                    self.hass.config_entries.async_update_entry(\n                        self._reauth_entry,\n                        data=data,\n                    )\n                    return self.async_abort(reason=\"reauth_successful\")\n\n                if not self.unique_id and await self._async_set_unique_id_from_envoy(\n                    envoy_reader\n                ):\n                    data[CONF_NAME] = self._async_envoy_name()\n\n                if self.unique_id:\n                    self._abort_if_unique_id_configured({CONF_HOST: data[CONF_HOST]})\n\n                return self.async_create_entry(title=data[CONF_NAME], data=data)\n\n        if self.unique_id:\n            self.context[\"title_placeholders\"] = {\n                CONF_SERIAL: self.unique_id,\n                CONF_HOST: self.ip_address,\n            }\n        return self.async_show_form(\n            step_id=\"user\",\n            data_schema=self._async_generate_schema(),\n            errors=errors,\n        )\n\n    @staticmethod\n    @callback\n    def async_get_options_flow(config_entry):\n        return EnvoyOptionsFlowHandler(config_entry)\n\nclass EnvoyOptionsFlowHandler(config_entries.OptionsFlow):\n    \"\"\"Envoy config flow options handler.\"\"\"\n\n    def __init__(self, config_entry):\n        \"\"\"Initialize Envoy options flow.\"\"\"\n        self.config_entry = config_entry\n\n    async def async_step_init(self, _user_input=None):\n        \"\"\"Manage the options.\"\"\"\n        return await self.async_step_user()\n\n    async def async_step_user(self, user_input=None):\n        \"\"\"Handle a flow initialized by the user.\"\"\"\n\n        if user_input is not None:\n            return self.async_create_entry(title=\"\", data=user_input)\n\n        schema = {\n            vol.Optional(\n                \"data_interval\",\n                default=self.config_entry.options.get(\n                    \"data_interval\", DEFAULT_SCAN_INTERVAL\n                ),\n            ): vol.All(vol.Coerce(int), vol.Range(min=5)),\n            vol.Optional(\n                \"data_fetch_timeout_seconds\",\n                default=self.config_entry.options.get(\n                    \"data_fetch_timeout_seconds\", 30\n                ),\n            ): vol.All(vol.Coerce(int), vol.Range(min=5)),\n            vol.Optional(\n                \"data_fetch_retry_count\",\n                default=self.config_entry.options.get(\n                    \"data_fetch_retry_count\", 1\n                ),\n            ): vol.All(vol.Coerce(int), vol.Range(min=1)),\n            vol.Optional(\n                \"data_fetch_holdoff_seconds\",\n                default=self.config_entry.options.get(\n                    \"data_fetch_holdoff_seconds\", 0\n                ),\n            ): vol.All(vol.Coerce(int), vol.Range(min=0)),\n            vol.Optional(\n                \"data_collection_timeout_seconds\",\n                default=self.config_entry.options.get(\n                    \"data_collection_timeout_seconds\", 55\n                ),\n            ): vol.All(vol.Coerce(int), vol.Range(min=30)),\n            vol.Optional(\n                \"do_not_use_production_json\",\n                default=self.config_entry.options.get(\n                    \"do_not_use_production_json\", False\n                ),\n            ): bool,\n        }\n        return self.async_show_form(step_id=\"user\", data_schema=vol.Schema(schema))\n\nclass CannotConnect(HomeAssistantError):\n    \"\"\"Error to indicate we cannot connect.\"\"\"\n\n\nclass InvalidAuth(HomeAssistantError):\n    \"\"\"Error to indicate there is invalid auth.\"\"\"\n"
  },
  {
    "path": "custom_components/enphase_envoy_custom/const.py",
    "content": "\"\"\"The enphase_envoy component.\"\"\"\n\nfrom homeassistant.components.binary_sensor import (\n    BinarySensorDeviceClass,\n    BinarySensorEntityDescription\n)\n\nfrom homeassistant.components.sensor import (\n    SensorDeviceClass,\n    SensorEntityDescription,\n    SensorStateClass\n\n)\nfrom homeassistant.const import (\n    UnitOfEnergy, \n    UnitOfPower, \n    UnitOfElectricPotential,\n    UnitOfFrequency,\n    Platform, \n    PERCENTAGE\n)\n\nDOMAIN = \"enphase_envoy\"\n\nPLATFORMS = [Platform.SENSOR, Platform.BINARY_SENSOR]\n\nICON = \"mdi:flash\"\n\nCOORDINATOR = \"coordinator\"\nNAME = \"name\"\n\nDEFAULT_SCAN_INTERVAL = 60  # default in seconds\n\nCONF_SERIAL = \"serial\"\nCONF_USE_ENLIGHTEN = \"use_enlighten\"\n\nSENSORS = (\n    SensorEntityDescription(\n        key=\"production\",\n        name=\"Current Power Production\",\n        native_unit_of_measurement=UnitOfPower.WATT,\n        state_class=SensorStateClass.MEASUREMENT,\n        device_class=SensorDeviceClass.POWER,\n    ),\n    SensorEntityDescription(\n        key=\"daily_production\",\n        name=\"Today's Energy Production\",\n        native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,\n        state_class=SensorStateClass.TOTAL_INCREASING,\n        device_class=SensorDeviceClass.ENERGY,\n    ),\n    SensorEntityDescription(\n        key=\"seven_days_production\",\n        name=\"Last Seven Days Energy Production\",\n        native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,\n        state_class=SensorStateClass.TOTAL,\n        device_class=SensorDeviceClass.ENERGY,\n    ),\n    SensorEntityDescription(\n        key=\"lifetime_production\",\n        name=\"Lifetime Energy Production\",\n        native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,\n        state_class=SensorStateClass.TOTAL_INCREASING,\n        device_class=SensorDeviceClass.ENERGY,\n    ),\n    SensorEntityDescription(\n        key=\"lifetime_net_production\",\n        name=\"Lifetime Net Energy Production\",\n        native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,\n        state_class=SensorStateClass.TOTAL_INCREASING,\n        device_class=SensorDeviceClass.ENERGY,\n    ),\n    SensorEntityDescription(\n        key=\"consumption\",\n        name=\"Current Power Consumption\",\n        native_unit_of_measurement=UnitOfPower.WATT,\n        state_class=SensorStateClass.MEASUREMENT,\n        device_class=SensorDeviceClass.POWER,\n    ),\n    SensorEntityDescription(\n        key=\"net_consumption\",\n        name=\"Current Net Power Consumption\",\n        native_unit_of_measurement=UnitOfPower.WATT,\n        state_class=SensorStateClass.MEASUREMENT,\n        device_class=SensorDeviceClass.POWER,\n    ),\n    SensorEntityDescription(\n        key=\"daily_consumption\",\n        name=\"Today's Energy Consumption\",\n        native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,\n        state_class=SensorStateClass.TOTAL_INCREASING,\n        device_class=SensorDeviceClass.ENERGY,\n    ),\n    SensorEntityDescription(\n        key=\"seven_days_consumption\",\n        name=\"Last Seven Days Energy Consumption\",\n        native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,\n        state_class=SensorStateClass.TOTAL,\n        device_class=SensorDeviceClass.ENERGY,\n    ),\n    SensorEntityDescription(\n        key=\"lifetime_consumption\",\n        name=\"Lifetime Energy Consumption\",\n        native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,\n        state_class=SensorStateClass.TOTAL_INCREASING,\n        device_class=SensorDeviceClass.ENERGY,\n    ),\n    SensorEntityDescription(\n        key=\"lifetime_net_consumption\",\n        name=\"Lifetime Net Energy Consumption\",\n        native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,\n        state_class=SensorStateClass.TOTAL_INCREASING,\n        device_class=SensorDeviceClass.ENERGY,\n    ),\n    SensorEntityDescription(\n        key=\"inverters\",\n        name=\"Inverter\",\n        native_unit_of_measurement=UnitOfPower.WATT,\n        state_class=SensorStateClass.MEASUREMENT,\n        device_class=SensorDeviceClass.POWER,\n    ),\n    SensorEntityDescription(\n        key=\"batteries\",\n        name=\"Battery\",\n        native_unit_of_measurement=PERCENTAGE,\n        state_class=SensorStateClass.MEASUREMENT,\n        device_class=SensorDeviceClass.BATTERY\n    ),\n    SensorEntityDescription(\n        key=\"total_battery_percentage\",\n        name=\"Total Battery Percentage\",\n        native_unit_of_measurement=PERCENTAGE,\n        state_class=SensorStateClass.MEASUREMENT\n    ),\n    SensorEntityDescription(\n        key=\"current_battery_capacity\",\n        name=\"Current Battery Capacity\",\n        native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,\n        state_class=SensorStateClass.TOTAL,\n        device_class=SensorDeviceClass.ENERGY\n    ),\n    SensorEntityDescription(\n        key=\"pf\",\n        name=\"Power Factor\",\n        device_class=SensorDeviceClass.POWER_FACTOR,\n        entity_registry_enabled_default=False,\n    ),\n    SensorEntityDescription(\n        key=\"voltage\",\n        name=\"Voltage\",\n        native_unit_of_measurement=UnitOfElectricPotential.VOLT,\n        device_class=SensorDeviceClass.VOLTAGE,\n        entity_registry_enabled_default=False,\n    ),\n    SensorEntityDescription(\n        key=\"frequency\",\n        name=\"Frequency\",\n        native_unit_of_measurement=UnitOfFrequency.HERTZ,\n        device_class=SensorDeviceClass.FREQUENCY,\n        entity_registry_enabled_default=False,\n    ),\n    SensorEntityDescription(\n        key=\"consumption_Current\",\n        name=\"Consumption Current\",\n        native_unit_of_measurement=\"A\",\n        device_class=SensorDeviceClass.CURRENT,\n        entity_registry_enabled_default=False,\n    ),\n    SensorEntityDescription(\n        key=\"production_Current\",\n        name=\"Production Current\",\n        native_unit_of_measurement=\"A\",\n        device_class=SensorDeviceClass.CURRENT,\n        entity_registry_enabled_default=False,\n    ),\n    SensorEntityDescription(\n        key=\"active_inverter_count\",\n        name=\"Active Inverter Count\",\n        state_class=SensorStateClass.MEASUREMENT,\n        entity_registry_enabled_default=False,\n    ),\n\n)\n\nBINARY_SENSORS = (\n    BinarySensorEntityDescription(\n        key=\"grid_status\",\n        name=\"Grid Status\",\n        device_class=BinarySensorDeviceClass.CONNECTIVITY,\n    ),\n)\n\nPHASE_SENSORS = (\n    SensorEntityDescription(\n        key=\"production_l1\",\n        name=\"Current Power Production L1\",\n        native_unit_of_measurement=UnitOfPower.WATT,\n        state_class=SensorStateClass.MEASUREMENT,\n        device_class=SensorDeviceClass.POWER,\n    ),\n    SensorEntityDescription(\n        key=\"daily_production_l1\",\n        name=\"Today's Energy Production L1\",\n        native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,\n        state_class=SensorStateClass.TOTAL_INCREASING,\n        device_class=SensorDeviceClass.ENERGY,\n    ),\n    SensorEntityDescription(\n        key=\"lifetime_production_l1\",\n        name=\"Lifetime Energy Production L1\",\n        native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,\n        state_class=SensorStateClass.TOTAL_INCREASING,\n        device_class=SensorDeviceClass.ENERGY,\n    ),\n    SensorEntityDescription(\n        key=\"lifetime_net_production_l1\",\n        name=\"Lifetime Net Energy Production L1\",\n        native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,\n        state_class=SensorStateClass.TOTAL_INCREASING,\n        device_class=SensorDeviceClass.ENERGY,\n    ),\n    SensorEntityDescription(\n        key=\"production_l2\",\n        name=\"Current Power Production L2\",\n        native_unit_of_measurement=UnitOfPower.WATT,\n        state_class=SensorStateClass.MEASUREMENT,\n        device_class=SensorDeviceClass.POWER,\n    ),\n    SensorEntityDescription(\n        key=\"daily_production_l2\",\n        name=\"Today's Energy Production L2\",\n        native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,\n        state_class=SensorStateClass.TOTAL_INCREASING,\n        device_class=SensorDeviceClass.ENERGY,\n    ),\n    SensorEntityDescription(\n        key=\"lifetime_production_l2\",\n        name=\"Lifetime Energy Production L2\",\n        native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,\n        state_class=SensorStateClass.TOTAL_INCREASING,\n        device_class=SensorDeviceClass.ENERGY,\n    ),\n    SensorEntityDescription(\n        key=\"lifetime_net_production_l2\",\n        name=\"Lifetime Net Energy Production L2\",\n        native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,\n        state_class=SensorStateClass.TOTAL_INCREASING,\n        device_class=SensorDeviceClass.ENERGY,\n    ),\n    SensorEntityDescription(\n        key=\"production_l3\",\n        name=\"Current Power Production L3\",\n        native_unit_of_measurement=UnitOfPower.WATT,\n        state_class=SensorStateClass.MEASUREMENT,\n        device_class=SensorDeviceClass.POWER,\n    ),\n    SensorEntityDescription(\n        key=\"daily_production_l3\",\n        name=\"Today's Energy Production L3\",\n        native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,\n        state_class=SensorStateClass.TOTAL_INCREASING,\n        device_class=SensorDeviceClass.ENERGY,\n    ),\n    SensorEntityDescription(\n        key=\"lifetime_production_l3\",\n        name=\"Lifetime Energy Production L3\",\n        native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,\n        state_class=SensorStateClass.TOTAL_INCREASING,\n        device_class=SensorDeviceClass.ENERGY,\n    ),\n    SensorEntityDescription(\n        key=\"lifetime_net_production_l3\",\n        name=\"Lifetime Net Energy Production L3\",\n        native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,\n        state_class=SensorStateClass.TOTAL_INCREASING,\n        device_class=SensorDeviceClass.ENERGY,\n    ),\n    SensorEntityDescription(\n        key=\"consumption_l1\",\n        name=\"Current Power Consumption L1\",\n        native_unit_of_measurement=UnitOfPower.WATT,\n        state_class=SensorStateClass.MEASUREMENT,\n        device_class=SensorDeviceClass.POWER,\n    ),\n    SensorEntityDescription(\n        key=\"net_consumption_l1\",\n        name=\"Current Net Power Consumption L1\",\n        native_unit_of_measurement=UnitOfPower.WATT,\n        state_class=SensorStateClass.MEASUREMENT,\n        device_class=SensorDeviceClass.POWER,\n    ),\n    SensorEntityDescription(\n        key=\"daily_consumption_l1\",\n        name=\"Today's Energy Consumption L1\",\n        native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,\n        state_class=SensorStateClass.TOTAL_INCREASING,\n        device_class=SensorDeviceClass.ENERGY,\n    ),\n    SensorEntityDescription(\n        key=\"lifetime_consumption_l1\",\n        name=\"Lifetime Energy Consumption L1\",\n        native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,\n        state_class=SensorStateClass.TOTAL_INCREASING,\n        device_class=SensorDeviceClass.ENERGY,\n    ),\n    SensorEntityDescription(\n        key=\"lifetime_net_consumption_l1\",\n        name=\"Lifetime Net Energy Consumption L1\",\n        native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,\n        state_class=SensorStateClass.TOTAL_INCREASING,\n        device_class=SensorDeviceClass.ENERGY,\n    ),\n    SensorEntityDescription(\n        key=\"consumption_l2\",\n        name=\"Current Power Consumption L2\",\n        native_unit_of_measurement=UnitOfPower.WATT,\n        state_class=SensorStateClass.MEASUREMENT,\n        device_class=SensorDeviceClass.POWER,\n    ),\n    SensorEntityDescription(\n        key=\"net_consumption_l2\",\n        name=\"Current Net Power Consumption L2\",\n        native_unit_of_measurement=UnitOfPower.WATT,\n        state_class=SensorStateClass.MEASUREMENT,\n        device_class=SensorDeviceClass.POWER,\n    ),\n    SensorEntityDescription(\n        key=\"daily_consumption_l2\",\n        name=\"Today's Energy Consumption L2\",\n        native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,\n        state_class=SensorStateClass.TOTAL_INCREASING,\n        device_class=SensorDeviceClass.ENERGY,\n    ),\n    SensorEntityDescription(\n        key=\"lifetime_consumption_l2\",\n        name=\"Lifetime Energy Consumption L2\",\n        native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,\n        state_class=SensorStateClass.TOTAL_INCREASING,\n        device_class=SensorDeviceClass.ENERGY,\n    ),\n    SensorEntityDescription(\n        key=\"lifetime_net_consumption_l2\",\n        name=\"Lifetime Net Energy Consumption L2\",\n        native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,\n        state_class=SensorStateClass.TOTAL_INCREASING,\n        device_class=SensorDeviceClass.ENERGY,\n    ),\n    SensorEntityDescription(\n        key=\"consumption_l3\",\n        name=\"Current Power Consumption L3\",\n        native_unit_of_measurement=UnitOfPower.WATT,\n        state_class=SensorStateClass.MEASUREMENT,\n        device_class=SensorDeviceClass.POWER,\n    ),\n    SensorEntityDescription(\n        key=\"net_consumption_l3\",\n        name=\"Current Net Power Consumption L3\",\n        native_unit_of_measurement=UnitOfPower.WATT,\n        state_class=SensorStateClass.MEASUREMENT,\n        device_class=SensorDeviceClass.POWER,\n    ),\n    SensorEntityDescription(\n        key=\"daily_consumption_l3\",\n        name=\"Today's Energy Consumption L3\",\n        native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,\n        state_class=SensorStateClass.TOTAL_INCREASING,\n        device_class=SensorDeviceClass.ENERGY,\n    ),\n    SensorEntityDescription(\n        key=\"lifetime_consumption_l3\",\n        name=\"Lifetime Energy Consumption L3\",\n        native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,\n        state_class=SensorStateClass.TOTAL_INCREASING,\n        device_class=SensorDeviceClass.ENERGY,\n    ),\n    \n    SensorEntityDescription(\n        key=\"lifetime_net_consumption_l3\",\n        name=\"Lifetime Net Energy Consumption L3\",\n        native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,\n        state_class=SensorStateClass.TOTAL_INCREASING,\n        device_class=SensorDeviceClass.ENERGY,\n    ),\n    SensorEntityDescription(\n        key=\"pf_l1\",\n        name=\"Power Factor L1\",\n        device_class=SensorDeviceClass.POWER_FACTOR,\n        entity_registry_enabled_default=False,\n    ),\n    SensorEntityDescription(\n        key=\"pf_l2\",\n        name=\"Power Factor L2\",\n        device_class=SensorDeviceClass.POWER_FACTOR,\n        entity_registry_enabled_default=False,\n    ),\n    SensorEntityDescription(\n        key=\"pf_l3\",\n        name=\"Power Factor L3\",\n        device_class=SensorDeviceClass.POWER_FACTOR,\n        entity_registry_enabled_default=False,\n    ),\n    SensorEntityDescription(\n        key=\"voltage_l1\",\n        name=\"Voltage L1\",\n        native_unit_of_measurement=UnitOfElectricPotential.VOLT,\n        device_class=SensorDeviceClass.VOLTAGE,\n        entity_registry_enabled_default=False,\n    ),\n    SensorEntityDescription(\n        key=\"voltage_l2\",\n        name=\"Voltage L2\",\n        native_unit_of_measurement=UnitOfElectricPotential.VOLT,\n        device_class=SensorDeviceClass.VOLTAGE,\n        entity_registry_enabled_default=False,\n    ),\n    SensorEntityDescription(\n        key=\"voltage_l3\",\n        name=\"Voltage L3\",\n        native_unit_of_measurement=UnitOfElectricPotential.VOLT,\n        device_class=SensorDeviceClass.VOLTAGE,\n        entity_registry_enabled_default=False,\n    ),\n    SensorEntityDescription(\n        key=\"frequency_l1\",\n        name=\"Frequency L1\",\n        native_unit_of_measurement=UnitOfFrequency.HERTZ,\n        device_class=SensorDeviceClass.FREQUENCY,\n        entity_registry_enabled_default=False,\n    ),\n    SensorEntityDescription(\n        key=\"frequency_l2\",\n        name=\"Frequency L2\",\n        native_unit_of_measurement=UnitOfFrequency.HERTZ,\n        device_class=SensorDeviceClass.FREQUENCY,\n        entity_registry_enabled_default=False,\n    ),\n    SensorEntityDescription(\n        key=\"frequency_l3\",\n        name=\"Frequency L3\",\n        native_unit_of_measurement=UnitOfFrequency.HERTZ,\n        device_class=SensorDeviceClass.FREQUENCY,\n        entity_registry_enabled_default=False,\n    ),\n    SensorEntityDescription(\n        key=\"consumption_Current_l1\",\n        name=\"Consumption Current L1\",\n        native_unit_of_measurement=\"A\",\n        device_class=SensorDeviceClass.CURRENT,\n        entity_registry_enabled_default=False,\n    ),\n     SensorEntityDescription(\n        key=\"consumption_Current_l2\",\n        name=\"Consumption Current L2\",\n        native_unit_of_measurement=\"A\",\n        device_class=SensorDeviceClass.CURRENT,\n        entity_registry_enabled_default=False,\n    ),\n     SensorEntityDescription(\n        key=\"consumption_Current_l3\",\n        name=\"Consumption Current L3\",\n        native_unit_of_measurement=\"A\",\n        device_class=SensorDeviceClass.CURRENT,\n        entity_registry_enabled_default=False,\n    ),\n    SensorEntityDescription(\n        key=\"production_Current_l1\",\n        name=\"Production Current L1\",\n        native_unit_of_measurement=\"A\",\n        device_class=SensorDeviceClass.CURRENT,\n        entity_registry_enabled_default=False,\n    ),\n     SensorEntityDescription(\n        key=\"production_Current_l2\",\n        name=\"Production Current L2\",\n        native_unit_of_measurement=\"A\",\n        device_class=SensorDeviceClass.CURRENT,\n        entity_registry_enabled_default=False,\n    ),\n     SensorEntityDescription(\n        key=\"production_Current_l3\",\n        name=\"Production Current L3\",\n        native_unit_of_measurement=\"A\",\n        device_class=SensorDeviceClass.CURRENT,\n        entity_registry_enabled_default=False,\n    ),\n\n)\n\nBATTERY_ENERGY_DISCHARGED_SENSOR = SensorEntityDescription(\n    key=\"battery_energy_discharged\",\n    name=\"Battery Energy Discharged\",\n    native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,\n    state_class=SensorStateClass.TOTAL,\n    device_class=SensorDeviceClass.ENERGY\n)\n\nBATTERY_ENERGY_CHARGED_SENSOR = SensorEntityDescription(\n    key=\"battery_energy_charged\",\n    name=\"Battery Energy Charged\",\n    native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,\n    state_class=SensorStateClass.TOTAL,\n    device_class=SensorDeviceClass.ENERGY\n)\n"
  },
  {
    "path": "custom_components/enphase_envoy_custom/diagnostics.py",
    "content": "\"\"\"Diagnostics support for Enphase Envoy.\"\"\"\nfrom __future__ import annotations\n\nfrom typing import Any\n\nfrom attr import asdict\n\nfrom homeassistant.components.diagnostics import async_redact_data\nfrom homeassistant.config_entries import ConfigEntry\nfrom homeassistant.const import CONF_PASSWORD, CONF_USERNAME\nfrom homeassistant.core import HomeAssistant\nfrom homeassistant.helpers import device_registry as dr, entity_registry as er\nfrom homeassistant.helpers.update_coordinator import DataUpdateCoordinator\n\nfrom .const import COORDINATOR, DOMAIN\n\nCONF_TITLE = \"title\"\n\nTO_REDACT = {\n    # CONF_NAME,\n    CONF_PASSWORD,\n    # Config entry title and unique ID may contain sensitive data:\n    # CONF_TITLE,\n    # CONF_UNIQUE_ID,\n    CONF_USERNAME,\n}\n\n\nasync def async_get_config_entry_diagnostics(\n    hass: HomeAssistant, entry: ConfigEntry\n) -> dict[str, Any]:\n    \"\"\"Return diagnostics for a config entry.\"\"\"\n    coordinator: DataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][COORDINATOR]\n\n    device_registry = dr.async_get(hass)\n    entity_registry = er.async_get(hass)\n\n    devices = []\n\n    registry_devices = dr.async_entries_for_config_entry(\n        device_registry, entry.entry_id\n    )\n\n    for device in registry_devices:\n        entities = []\n\n        registry_entities = er.async_entries_for_device(\n            entity_registry,\n            device_id=device.id,\n            include_disabled_entities=True,\n        )\n\n        for entity in registry_entities:\n            state_dict = None\n            if state := hass.states.get(entity.entity_id):\n                state_dict = dict(state.as_dict())\n                state_dict.pop(\"context\", None)\n\n            entities.append({\"entry\": asdict(entity), \"state\": state_dict})\n\n        devices.append({\"device\": asdict(device), \"entities\": entities})\n\n    return async_redact_data(\n        {\n            \"entry\": entry.as_dict(),\n            \"data\": coordinator.data,\n            \"Note\": \"Entities that show as null are not available for your Envoy\",\n            \"devices\": devices,\n        },\n        TO_REDACT,\n    )\n"
  },
  {
    "path": "custom_components/enphase_envoy_custom/envoy_reader.py",
    "content": "\"\"\"Module to read production and consumption values from an Enphase Envoy on the local network.\"\"\"\nimport argparse\nimport datetime\nimport logging\nimport time\nfrom json.decoder import JSONDecodeError\nimport json\nfrom ipaddress import IPv4Address, IPv6Address\nimport sys\nimport getpass\n\n#Modules not in standard Python Library - add to manifest requirements\nimport re\nimport jwt\nimport asyncio\nimport httpx\nimport xmltodict\nfrom envoy_utils.envoy_utils import EnvoyUtils\n\n#\n# Legacy parser is only used on ancient firmwares\n#\nPRODUCTION_REGEX = r\"<td>Currentl.*</td>\\s+<td>\\s*(\\d+|\\d+\\.\\d+)\\s*(W|kW|MW)</td>\"\nDAY_PRODUCTION_REGEX = r\"<td>Today</td>\\s+<td>\\s*(\\d+|\\d+\\.\\d+)\\s*(Wh|kWh|MWh)</td>\"\nWEEK_PRODUCTION_REGEX = (\n    r\"<td>Past Week</td>\\s+<td>\\s*(\\d+|\\d+\\.\\d+)\\s*(Wh|kWh|MWh)</td>\"\n)\nLIFE_PRODUCTION_REGEX = (\n    r\"<td>Since Installation</td>\\s+<td>\\s*(\\d+|\\d+\\.\\d+)\\s*(Wh|kWh|MWh)</td>\"\n)\nSERIAL_REGEX = re.compile(r\"Envoy\\s*Serial\\s*Number:\\s*([0-9]+)\")\nACTIVE_INVERTER_COUNT_REGEX = r\"<td>Number of Microinverters Online</td>\\s*<td>\\s*(\\d*)\\s*</td>\"\n\nENDPOINT_URL_PRODUCTION_JSON = \"http{}://{}/production.json?details=1\"\nENDPOINT_URL_PRODUCTION_V1 = \"http{}://{}/api/v1/production\"\nENDPOINT_URL_PRODUCTION_INVERTERS = \"http{}://{}/api/v1/production/inverters\"\nENDPOINT_URL_PRODUCTION = \"http{}://{}/production\"\nENDPOINT_URL_CHECK_JWT = \"https://{}/auth/check_jwt\"\nENDPOINT_URL_ENSEMBLE_INVENTORY = \"http{}://{}/ivp/ensemble/inventory\"\nENDPOINT_URL_HOME_JSON = \"http{}://{}/home.json\"\nENDPOINT_URL_HOME = \"http{}://{}/home\"\nENDPOINT_URL_INFO_XML = \"http{}://{}/info\"\nENDPOINT_URL_METERS = \"http{}://{}/ivp/meters\"\nENDPOINT_URL_METERS_REPORTS = \"http{}://{}/ivp/meters/reports\"\nENDPOINT_URL_METERS_READINGS = \"http{}://{}/ivp/meters/readings\"\n\n# pylint: disable=pointless-string-statement\n\nENVOY_MODEL_S = \"PC\"\nENVOY_MODEL_C = \"P\"\nENVOY_MODEL_LEGACY = \"P0\"\n\nLOGIN_URL = \"https://entrez.enphaseenergy.com/login_main_page\"\nTOKEN_URL = \"https://entrez.enphaseenergy.com/entrez_tokens\"\n\n# paths for the enlighten 1 year owner token\nENLIGHTEN_AUTH_URL = \"https://enlighten.enphaseenergy.com/login/login.json\"\nENLIGHTEN_TOKEN_URL = \"https://entrez.enphaseenergy.com/tokens\"\n\n_LOGGER = logging.getLogger(__name__)\n\n\ndef has_production_and_consumption(json):\n    \"\"\"Check if json has keys for both production and consumption.\"\"\"\n    return \"production\" in json and \"consumption\" in json\n\n\ndef has_metering_setup(json):\n    \"\"\"Check if Active Count of Production CTs (eim) installed is greater than one.\"\"\"\n    return json[\"production\"][1][\"activeCount\"] > 0\n\n\ndef has_production_metering_setup(json):\n    \"\"\"Check if Production CTs (eim) are installed.\"\"\"\n    return json[0][\"state\"] == \"enabled\"\n\n\ndef has_consumption_metering_setup(json):\n    \"\"\"Check if Consumption CTs (eim) are installed.\"\"\"\n    return json[1][\"state\"] == \"enabled\"\n\n\ndef has_net_consumption_meters_type(json):\n    \"\"\"Check if Consumption measurement type is net-consumption.\"\"\"\n    return json[1][\"measurementType\"] == \"net-consumption\"\n\n\ndef get_production_meters_phase_count(json):\n    \"\"\"Get Count of Production CTs (eim) installed.\"\"\"\n    return json[0][\"phaseCount\"]\n\n\ndef get_consumption_meters_phase_count(json):\n    \"\"\"Get Count of Consumption CTs (eim) installed.\"\"\"\n    return json[1][\"phaseCount\"]\n\n    \ndef is_ipv6_address(address: str) -> bool:\n    \"\"\"Check if a given string is an IPv6 address.\"\"\"\n    try:\n        IPv6Address(address)\n    except ValueError:\n        return False\n    return True\n\nclass SwitchToHTTPS(Exception):\n    pass\n\n\nclass EnvoyReader:  # pylint: disable=too-many-instance-attributes\n    \"\"\"Instance of EnvoyReader\"\"\"\n\n    # P0 for older Envoy model C, s/w < R3.9 no json pages\n    # P for production data only (ie. Envoy model C, s/w >= R3.9)\n    # PC for production and consumption data (ie. Envoy model S)\n\n    message_battery_not_available = (\n        \"Battery storage data not available for your Envoy device.\"\n    )\n\n    message_production_not_available = (\n        \"CTs production data not available for your Envoy device.\"\n    )\n\n    message_consumption_not_available = (\n        \"CTs consumption data not available for your Envoy device.\"\n    )\n\n    message_grid_status_not_available = (\n        \"Grid status not available for your Envoy device.\"\n    )\n\n    message_frequency_not_available = (\n        \"Frequency data not available for your Envoy device.\"\n    )\n\n    message_voltage_not_available = (\n        \"Voltage data not available for your Envoy device.\"\n    )\n    message_pf_not_available = (\n        \"Power Factor data not available for your Envoy device.\"\n    )\n    message_current_consumption_not_available = (\n        \"Amps consumption data not available for your Envoy device.\"\n    )\n    message_current_production_not_available = (\n        \"Amps production data not available for your Envoy device.\"\n    )\n\n    message_active_inverters_not_available = (\n        \"Active Inverter count not available for your Envoy device.\"\n    )\n\n\n    def __init__(  # pylint: disable=too-many-arguments\n        self,\n        host,\n        username=\"envoy\",\n        password=\"\",\n        inverters=False,\n        async_client=None,\n        enlighten_user=None,\n        enlighten_pass=None,\n        commissioned=False,\n        enlighten_site_id=None,\n        enlighten_serial_num=None,\n        https_flag=\"\",\n        use_enlighten_owner_token=False,\n        token_refresh_buffer_seconds=0,\n        store=None,\n        info_refresh_buffer_seconds=3600,\n        fetch_timeout_seconds=30,\n        fetch_holdoff_seconds=0,\n        fetch_retries=1,\n        do_not_use_production_json=False,\n    ):\n        \"\"\"Init the EnvoyReader.\"\"\"\n        self.host = host.lower().replace('[','').replace(']','')\n        # IPv6 addresses need to be enclosed in brackets\n        if is_ipv6_address(self.host):\n            self.host = f\"[{self.host}]\"\n        self.username = username\n        self.password = password\n        self.get_inverters = inverters\n        self.endpoint_type = None\n        self.has_grid_status = True\n        self.serial_number_last_six = None\n        self.endpoint_meters_reports_json_results = None\n        self.endpoint_meters_readings_json_results = None\n        self.endpoint_production_json_results = None\n        self.endpoint_production_v1_results = None\n        self.endpoint_production_inverters = None\n        self.endpoint_production_results = None\n        self.endpoint_ensemble_json_results = None\n        self.endpoint_home_json_results = None\n        self.endpoint_home_results = None\n        self.isProductionMeteringEnabled = False  # pylint: disable=invalid-name\n        self.isConsumptionMeteringEnabled = False  # pylint: disable=invalid-name\n        self.net_consumption_meters_type = False\n        self.production_meters_phase_count = 0\n        self.consumption_meters_phase_count = 0\n        self._async_client = async_client\n        self._authorization_header = None\n        self._cookies = None\n        self.enlighten_user = enlighten_user\n        self.enlighten_pass = enlighten_pass\n        self.commissioned = commissioned\n        self.enlighten_site_id = enlighten_site_id\n        self.enlighten_serial_num = enlighten_serial_num\n        self.https_flag = https_flag\n        self.use_enlighten_owner_token = use_enlighten_owner_token\n        self.token_refresh_buffer_seconds = token_refresh_buffer_seconds\n        self.endpoint_info_results = None\n        self.endpoint_meters_json_results = None\n        self.info_refresh_buffer_seconds = info_refresh_buffer_seconds\n        self.info_next_refresh_time = datetime.datetime.now()\n        self.meters_next_refresh_time = datetime.datetime.now()\n        self._store = store\n        self._store_data = {}\n        self._store_update_pending = False\n        self._fetch_timeout_seconds = fetch_timeout_seconds\n        self._fetch_holdoff_seconds = fetch_holdoff_seconds\n        self._fetch_retries = max(fetch_retries,1)\n        self._do_not_use_production_json=do_not_use_production_json\n\n    @property\n    def _token(self):\n        return self._store_data.get(\"token\", \"\")\n\n    @_token.setter\n    def _token(self, token_value):\n        self._store_data[\"token\"] = token_value\n        self._store_update_pending = True\n\n    async def _sync_store(self):\n        if self._store and not self._store_data:\n            self._store_data = await self._store.async_load() or {}\n\n        if self._store and self._store_update_pending:\n            self._store_update_pending = False\n            await self._store.async_save(self._store_data)\n\n    @property\n    def async_client(self):\n        \"\"\"Return the httpx client.\"\"\"\n        return self._async_client or httpx.AsyncClient(verify=False,\n                                                       headers=self._authorization_header,\n                                                       cookies=self._cookies)\n\n    @property\n    def non_local_async_client(self):\n        \"\"\"Return the httpx client for non-local usage.\"\"\"\n        return self._async_client or httpx.AsyncClient(verify=True,\n                                                       headers=self._authorization_header,\n                                                       cookies=self._cookies)\n\n    async def _update(self):\n        \"\"\"Update the data.\"\"\"\n        _LOGGER.debug(\"_update running\")\n        if self.endpoint_type == ENVOY_MODEL_S:\n            await self._update_meters_endpoint()\n            await self._update_from_pc_endpoint()\n        if self.endpoint_type == ENVOY_MODEL_C or (\n            self.endpoint_type == ENVOY_MODEL_S and not self.isProductionMeteringEnabled\n        ):\n            await self._update_from_p_endpoint()\n        if self.endpoint_type == ENVOY_MODEL_LEGACY:\n            await self._update_from_p0_endpoint()\n            \n        await self._update_info_endpoint()\n\n    async def _update_from_meters_reports_endpoint(self):\n        \"\"\"Update from ivp/meters endpoint.\"\"\"\n        if self.endpoint_type == ENVOY_MODEL_S:\n            #only touch meters reports if confirmed envoy model S, other type choke up on this request\n            await self._update_endpoint(\n                \"endpoint_meters_reports_json_results\", ENDPOINT_URL_METERS_REPORTS\n            )\n\n    async def _update_from_meters_readings_endpoint(self):\n        \"\"\"Update from ivp/meters/readings endpoint.\"\"\"\n        if self.endpoint_type == ENVOY_MODEL_S:\n            #only touch meters reports if confirmed envoy model S, other type choke up on this request\n            await self._update_endpoint(\n                \"endpoint_meters_readings_json_results\", ENDPOINT_URL_METERS_READINGS\n            )\n\n    async def _update_from_pc_endpoint(self,detectmode=False):\n        \"\"\"Update from PC endpoint.\"\"\"\n        if not self._do_not_use_production_json or detectmode:\n            await self._update_endpoint(\n                \"endpoint_production_json_results\", ENDPOINT_URL_PRODUCTION_JSON\n            )\n        await self._update_endpoint(\n            \"endpoint_ensemble_json_results\", ENDPOINT_URL_ENSEMBLE_INVENTORY\n        )\n        if self.has_grid_status:\n            await self._update_endpoint(\n                \"endpoint_home_json_results\", ENDPOINT_URL_HOME_JSON\n            )\n\n    async def _update_from_p_endpoint(self):\n        \"\"\"Update from P endpoint.\"\"\"\n        await self._update_endpoint(\n            \"endpoint_production_v1_results\", ENDPOINT_URL_PRODUCTION_V1\n        )\n\n    async def _update_from_p0_endpoint(self):\n        \"\"\"Update from P0 endpoint.\"\"\"\n        await self._update_endpoint(\n            \"endpoint_production_results\", ENDPOINT_URL_PRODUCTION\n        )\n        await self._update_endpoint(\n            \"endpoint_home_results\", ENDPOINT_URL_HOME\n        )\n\n    async def _update_info_endpoint(self):\n        \"\"\"Update from info endpoint if next time expired.\"\"\"\n        if self.info_next_refresh_time <= datetime.datetime.now():\n            await self._update_endpoint(\"endpoint_info_results\", ENDPOINT_URL_INFO_XML)\n            self.info_next_refresh_time = datetime.datetime.now() + datetime.timedelta(\n                seconds=self.info_refresh_buffer_seconds\n            )\n            _LOGGER.debug(\n                \"Info endpoint updated, set next update time: %s using interval: %s\",\n                self.info_next_refresh_time,\n                self.info_refresh_buffer_seconds,\n            )\n        else:\n            _LOGGER.debug(\n                \"Info endpoint next update time is: %s using interval: %s\",\n                self.info_next_refresh_time,\n                self.info_refresh_buffer_seconds,\n            )\n\n    async def _update_meters_endpoint(self):\n        \"\"\"Update from meters endpoint if next time expried.\"\"\"\n        if self.meters_next_refresh_time <= datetime.datetime.now():\n            await self._update_endpoint(\"endpoint_meters_json_results\", ENDPOINT_URL_METERS)\n\n            #some devices return [] for ivp/meters\n            if self.endpoint_meters_json_results and self.endpoint_meters_json_results.text != \"[]\":\n\n                self.isProductionMeteringEnabled = has_production_metering_setup(\n                    self.endpoint_meters_json_results.json()\n                )\n                self.isConsumptionMeteringEnabled = has_consumption_metering_setup(\n                    self.endpoint_meters_json_results.json()\n                )\n                self.net_consumption_meters_type = has_net_consumption_meters_type(\n                    self.endpoint_meters_json_results.json()\n                )\n                self.production_meters_phase_count = get_production_meters_phase_count(\n                    self.endpoint_meters_json_results.json()\n                )\n                self.consumption_meters_phase_count = get_consumption_meters_phase_count(\n                    self.endpoint_meters_json_results.json()\n                )\n                self.meters_next_refresh_time = datetime.datetime.now() + datetime.timedelta(\n                    seconds=self.info_refresh_buffer_seconds\n                )\n\n            _LOGGER.debug(\n                \"Meters endpoint updated, set next update time: %s using interval: %s\",\n                self.meters_next_refresh_time,\n                self.info_refresh_buffer_seconds,\n            )\n        else:\n            _LOGGER.debug(\n                \"Meters endpoint next update time is: %s using interval: %s\",\n                self.meters_next_refresh_time,\n                self.info_refresh_buffer_seconds,\n            )\n        await self._update_from_meters_reports_endpoint()\n        await self._update_from_meters_readings_endpoint()\n\n    async def _update_endpoint(self, attr, url):\n        \"\"\"Update a property from an endpoint.\"\"\"\n        formatted_url = url.format(self.https_flag, self.host)\n        response = await self._async_fetch_with_retry(\n            formatted_url, follow_redirects=False\n        )\n        setattr(self, attr, response)\n\n    async def _async_fetch_with_retry(self, url, **kwargs):\n        \"\"\"Retry 3 times to fetch the url if there is a transport error.\"\"\"\n        for attempt in range(self._fetch_retries + 1):\n            header = \" <Blank Header> \"\n            if self._authorization_header:\n                header = \" <Token hidden> \"\n            _LOGGER.debug(\n                \"HTTP GET Attempt #%s of %s: %s: use token: %s: Header:%s Timeout: %s Holdoff: %s\",\n                attempt + 1,\n                self._fetch_retries + 1,\n                url,\n                self.use_enlighten_owner_token,\n                header,\n                self._fetch_timeout_seconds,\n                self._fetch_holdoff_seconds,\n            )\n            async with self.async_client as client:\n                try:\n                    getstart = time.time()\n                    resp = await client.get(\n                        url, headers=self._authorization_header, timeout=self._fetch_timeout_seconds, **kwargs\n                    )\n                    getend = time.time()\n                    if resp.status_code == 401 and attempt < self._fetch_retries:\n                        if self.use_enlighten_owner_token:\n                            _LOGGER.debug(\n                                \"Received 401 from Envoy; refreshing cookies, in attempt %s of %s:\",\n                                attempt+1,\n                                self._fetch_retries + 1\n                             )\n                            could_refresh_cookies = await self._refresh_token_cookies()\n                            if not could_refresh_cookies:\n                                _LOGGER.debug(\n                                    \"cookie refresh failed, getting token, in attempt %s of %s:\",\n                                    attempt+1,\n                                    self._fetch_retries + 1\n                                )\n                                await self._getEnphaseToken()\n                            continue\n                        # don't try token and cookies refresh for legacy envoy\n                        else:\n                            _LOGGER.debug(\n                                \"Received 401 from Envoy; retrying, attempt %s of %s\",\n                                attempt+1,\n                                self._fetch_retries + 1\n                            )\n                            continue\n                    _LOGGER.debug(\"Fetched (%s of %s) in %s sec from %s: %s: %s\",\n                        attempt + 1,\n                        self._fetch_retries + 1,\n                        round(getend - getstart,1),\n                        url, \n                        resp, \n                        resp.text\n                    )\n                    if resp.status_code == 404:\n                        return None\n                    return resp\n                \n                except httpx.TimeoutException as exc:\n                    if attempt == self._fetch_retries:\n                        _LOGGER.warning(\"HTTP Timeout in fetch_with_retry, raising: %s\",exc)\n                        raise\n                    # Sleep a bit and try once more\n                    _LOGGER.warning(\"HTTP Timeout in fetch_with_retry, waiting %s sec: %s\",self._fetch_holdoff_seconds,exc)\n                    await asyncio.sleep(self._fetch_holdoff_seconds)\n                except Exception as exc:\n                    if attempt == self._fetch_retries:\n                        _LOGGER.warning(\"Error in fetch_with_retry, raising: %s\",exc)\n                        raise\n                    # Sleep a bit and try once more\n                    _LOGGER.warning(\"Error in fetch_with_retry, waiting %s sec: %s\",self._fetch_holdoff_seconds,exc)\n                    await asyncio.sleep(self._fetch_holdoff_seconds)\n\n    async def _async_post(self, url, data, cookies=None, client=None, **kwargs):\n        _LOGGER.debug(\"HTTP POST Attempt: %s\", url)\n        if client is None:\n            client = self.async_client\n        # _LOGGER.debug(\"HTTP POST Data: %s\", data)\n        try:\n            async with client:\n                resp = await client.post(\n                    url, cookies=cookies, data=data, timeout=30, **kwargs\n                )\n                _LOGGER.debug(\"HTTP POST %s: %s: %s\", url, resp, resp.text)\n                _LOGGER.debug(\"HTTP POST Cookie: %s\", resp.cookies)\n                return resp\n        except httpx.TransportError:  # pylint: disable=try-except-raise\n            raise\n\n    async def _fetch_owner_token_json(self) :\n        \"\"\"Try to fetch the owner token json from Enlighten API\"\"\"\n        async with self.non_local_async_client as client:\n            # login to the enlighten website\n            payload_login = {\n                'user[email]': self.enlighten_user,\n                'user[password]': self.enlighten_pass,\n            }\n            resp = await client.post(ENLIGHTEN_AUTH_URL, data=payload_login, timeout=30)\n            if resp.status_code >= 400:\n                raise RuntimeError(f\"Could not Authenticate with Enlighten, status: {resp.status_code}, {resp}\")\n\n            # now that we're in a logged in session, we can request the 1 year owner token via enlighten\n            login_data = resp.json()\n            payload_token = {\n                \"session_id\": login_data[\"session_id\"],\n                \"serial_num\": self.enlighten_serial_num,\n                \"username\": self.enlighten_user,\n            }\n            resp = await client.post(\n                ENLIGHTEN_TOKEN_URL, json=payload_token, timeout=30\n            )\n            if resp.status_code != 200:\n                raise RuntimeError(f\"Could not get enlighten token, status: {resp.status_code}, {resp}\")\n            return resp.text\n\n    async def _getEnphaseToken(self):\n        self._token = await self._fetch_owner_token_json()\n        _LOGGER.debug(\"Obtained Token\")\n\n        if self._is_enphase_token_expired(self._token):\n            raise RuntimeError(\"Just received token already expired\")\n\n        await self._refresh_token_cookies()\n\n    async def _refresh_token_cookies(self):\n        \"\"\"\n         Refresh the client's cookie with the token (if valid)\n         :returns True if cookie refreshed, False if it couldn't be\n        \"\"\"\n        # Create HTTP Header\n        self._authorization_header = {\"Authorization\": \"Bearer \" + self._token}\n\n        # Fetch the Enphase Token status from the local Envoy\n        token_validation = await self._async_fetch_with_retry(\n            ENDPOINT_URL_CHECK_JWT.format(self.host)\n        )\n\n        if token_validation.status_code == 200:\n            # set the cookies for future clients\n            self._cookies = token_validation.cookies\n            return True\n\n        # token not valid if we get here\n        return False\n\n\n    def _is_enphase_token_valid(self, response):\n        if response == \"Valid token.\":\n            _LOGGER.debug(\"Token is valid\")\n            return True\n        else:\n            _LOGGER.debug(\"Invalid token!\")\n            return False\n\n    def _is_enphase_token_expired(self, token):\n        decode = jwt.decode(\n            token, options={\"verify_signature\": False}, algorithms=\"ES256\"\n        )\n        exp_epoch = decode[\"exp\"]\n        # allow a buffer so we can try and grab it sooner\n        exp_epoch -= self.token_refresh_buffer_seconds\n        exp_time = datetime.datetime.fromtimestamp(exp_epoch)\n        if datetime.datetime.now() < exp_time:\n            _LOGGER.debug(\"Token expires at: %s\", exp_time)\n            return False\n        else:\n            _LOGGER.debug(\"Token expired on: %s\", exp_time)\n            return True\n\n    async def check_connection(self):\n        \"\"\"Check if the Envoy is reachable. Also check if HTTP or\"\"\"\n        \"\"\"HTTPS is needed.\"\"\"\n        _LOGGER.debug(\"Checking Host: %s\", self.host)\n        resp = await self._async_fetch_with_retry(\n            ENDPOINT_URL_PRODUCTION_V1.format(self.https_flag, self.host)\n        )\n        _LOGGER.debug(\"Check connection HTTP Code: %s\", resp.status_code)\n        if resp.status_code == 301:\n            raise SwitchToHTTPS\n\n    async def getData(self, getInverters=True):  # pylint: disable=invalid-name\n        \"\"\"Fetch data from the endpoint and if inverters selected default\"\"\"\n        \"\"\"to fetching inverter data.\"\"\"\n\n        # Check if the Secure flag is set\n        if self.https_flag == \"s\":\n            _LOGGER.debug(\n                \"Checking Token value: %s (Only first 10 characters shown)\",\n                self._token[1:10],\n            )\n            # Check if a token has already been retrieved\n            if self._token == \"\":\n                _LOGGER.debug(\"Found empty token: %s\", self._token)\n                await self._getEnphaseToken()\n            else:\n                _LOGGER.debug(\n                    \"Token is populated: %s (Only first 10 characters shown)\",\n                    self._token[1:10],\n                )\n                if self._is_enphase_token_expired(self._token):\n                    _LOGGER.debug(\"Found Expired token - Retrieving new token\")\n                    await self._getEnphaseToken()\n\n        if not self.endpoint_type:\n            await self.detect_model()\n        else:\n            await self._update()\n\n        _LOGGER.debug(\n            \"Using Model: %s (HTTP%s, Production Metering: %s phases: %s, Consumption Metering: %s phases: %s, Net consumption CT: %s, Get Inverters: %s)\",\n            self.endpoint_type, \n            self.https_flag,\n            self.isProductionMeteringEnabled,\n            self.production_meters_phase_count,\n            self.isConsumptionMeteringEnabled,\n            self.consumption_meters_phase_count,\n            self.net_consumption_meters_type,\n            self.get_inverters\n        )\n\n        if not self.get_inverters or not getInverters:\n            return\n\n        inverters_url = ENDPOINT_URL_PRODUCTION_INVERTERS.format(\n            self.https_flag, self.host\n        )\n        if self.use_enlighten_owner_token:\n            response = await self._async_fetch_with_retry(inverters_url)\n        else:\n            # Inverter page on envoy with old firmware requires username/password\n            inverters_auth = httpx.DigestAuth(self.username, self.password)\n            response = await self._async_fetch_with_retry(\n                inverters_url, auth=inverters_auth\n            )\n        if response.status_code in [401,404]:\n            if self.endpoint_type in [ENVOY_MODEL_C, ENVOY_MODEL_LEGACY]:\n                self.get_inverters = False\n                _LOGGER.debug(\"Error %s in Getdata for getting invertors, disabling inverters\",response.status_code)\n                return\n            response.raise_for_status()\n        self.endpoint_production_inverters = response\n        return\n\n    async def detect_model(self):\n        \"\"\"Method to determine if the Envoy supports consumption values or only production.\"\"\"\n        # If a password was not given as an argument when instantiating\n        # the EnvoyReader object than use the last six numbers of the serial\n        # number as the password.  Otherwise use the password argument value.\n        _LOGGER.debug(\"Detect Model running\")\n        if self.password == \"\" and not self.serial_number_last_six:\n            await self.get_serial_number()\n\n        try:\n            await self._update_from_pc_endpoint(detectmode=True)\n        except httpx.HTTPError:\n            pass\n\n        # If self.endpoint_production_json_results.status_code is set with\n        # 401 then we will give an error\n        if (\n            self.endpoint_production_json_results\n            and self.endpoint_production_json_results.status_code == 401\n        ):\n            raise RuntimeError(\n                \"Could not connect to Envoy model. \"\n                + \"Appears your Envoy is running firmware that requires secure communcation. \"\n                + \"Please enter in the needed Enlighten credentials during setup.\"\n            )\n\n        await self._update_info_endpoint()\n\n        if (\n            self.endpoint_production_json_results\n            and self.endpoint_production_json_results.status_code == 200\n            and has_production_and_consumption(\n                self.endpoint_production_json_results.json()\n            )\n        ):\n            _LOGGER.debug(\"Detect Model found production and consumption\")\n             #only access meters endpoint if envoy metered, other type may choke up\n            self.endpoint_type = ENVOY_MODEL_S\n            \n            await self._update_meters_endpoint()\n\n            if not self.isProductionMeteringEnabled:\n                await self._update_from_p_endpoint()\n            return\n\n        try:\n            await self._update_from_p_endpoint()\n        except httpx.HTTPError:\n            pass\n        if (\n            self.endpoint_production_v1_results\n            and self.endpoint_production_v1_results.status_code == 200\n        ):\n            self.endpoint_type = ENVOY_MODEL_C  # Envoy-C, production only\n            return\n\n        try:\n            await self._update_from_p0_endpoint()\n        except httpx.HTTPError:\n            pass\n        if (\n            self.endpoint_production_results\n            and self.endpoint_production_results.status_code == 200\n        ):\n            self.endpoint_type = ENVOY_MODEL_LEGACY  # older Envoy-C\n            self.get_inverters = False # don't get inverters for this model\n            return\n\n        raise RuntimeError(\n            \"Could not connect or determine Envoy model. \"\n            + \"Check that the device is up at 'http://\"\n            + self.host\n            + \"'.\"\n        )\n\n    async def get_serial_number(self):\n        \"\"\"Method to get last six digits of Envoy serial number for auth\"\"\"\n        full_serial = await self.get_full_serial_number()\n        if full_serial:\n            gen_passwd = EnvoyUtils.get_password(full_serial, self.username)\n            if self.username == \"envoy\" or self.username != \"installer\":\n                self.password = self.serial_number_last_six = full_serial[-6:]\n            else:\n                self.password = gen_passwd\n\n    async def get_full_serial_number(self):\n        \"\"\"Method to get the  Envoy serial number.\"\"\"\n        response = await self._async_fetch_with_retry(\n            f\"http{self.https_flag}://{self.host}/info.xml\",\n            follow_redirects=True,\n        )\n        if not response.text:\n            return None\n        if \"<sn>\" in response.text:\n            return response.text.split(\"<sn>\")[1].split(\"</sn>\")[0]\n        match = SERIAL_REGEX.search(response.text)\n        if match:\n            # if info.xml is in html format we're dealing with ENVOY R\n            _LOGGER.debug(\"Legacy model identified by info.xml being html. Disabling inverters\")\n            self.get_inverters = False\n            return match.group(1)\n\n    def create_connect_errormessage(self):\n        \"\"\"Create error message if unable to connect to Envoy\"\"\"\n        return (\n            \"Unable to connect to Envoy. \"\n            + \"Check that the device is up at 'http://\"\n            + self.host\n            + \"'.\"\n        )\n\n    def create_json_errormessage(self):\n        \"\"\"Create error message if unable to parse JSON response\"\"\"\n        return (\n            \"Got a response from '\"\n            + self.host\n            + \"', but metric could not be found. \"\n            + \"Maybe your model of Envoy doesn't \"\n            + \"support the requested metric.\"\n        )\n\n    async def _meters_readings_value(self,field,report=\"net-consumption\",phase=None):\n        \"\"\"Extract value from meters readings json\"\"\"\n        report_map = {\"production\": 0, \"net-consumption\": 1, \"total-consumption\": 1}\n\n        phase_map = {\"l1\": 0, \"l2\": 1, \"l3\": 2}\n        #meters readings is only available for ENVOY Metered with CT configured\n        if (self.endpoint_type == ENVOY_MODEL_S) and (\n            #net-consumption requires consumption CT installed is Solar power included mode\n            (report == \"net-consumption\" \n                and self.isConsumptionMeteringEnabled \n                and self.net_consumption_meters_type)\n            # production data requires production CT installed\n            or (report == \"production\" and self.isProductionMeteringEnabled )\n            #if at least consumption CT is installed total-consumption will be available even in Load only mode install\n            or (report == \"total-consumption\" \n                and self.isConsumptionMeteringEnabled\n                and not self.net_consumption_meters_type )\n        ):\n            if self.endpoint_meters_readings_json_results:\n                raw_json = self.endpoint_meters_readings_json_results.json()\n                if phase == None:\n                    try:\n                        jsondata = raw_json[report_map[report]][field]\n                        return jsondata\n                    except (KeyError, IndexError):\n                        return None\n                \n                #if production data requested and multiple phases are configured and requested phase is in count of configured phases return data or\n                #if consumption data requested and multiple phases are configured and requested phase is in count of configured phases return date\n                if ((self.production_meters_phase_count > 1 and phase_map[phase] < self.production_meters_phase_count and report==\"production\")\n                 or (self.consumption_meters_phase_count > 1 and phase_map[phase] < self.consumption_meters_phase_count and report!=\"production\")):\n                    try:\n                        jsondata = raw_json[report_map[report]][\"channels\"][phase_map[phase]][field]\n                        return jsondata\n                    except (KeyError, IndexError):\n                        return None\n        return None\n\n    async def _meters_report_value(self,field,report=\"production\",phase=None):\n        \"\"\"Extract value from meters reports json if consumption meter is available\"\"\"\n        report_map = {\"production\": 0, \"net-consumption\": 1, \"total-consumption\": 2}\n        phase_map = {\"l1\": 0, \"l2\": 1, \"l3\": 2}\n        #meters reports is only available for ENVOY Metered with CT configured\n        if (self.endpoint_type == ENVOY_MODEL_S) and (\n            #net-consumption requires consumption CT installed is Solar power included mode\n            (report == \"net-consumption\" and self.isConsumptionMeteringEnabled and self.net_consumption_meters_type)\n            # production data requires production CT installed\n            or (report == \"production\" and self.isProductionMeteringEnabled)\n            #if at least consumption CT is installed total-consumption will be available even in Load only mode install\n            or (report == \"total-consumption\" and self.isConsumptionMeteringEnabled)\n        ):\n            if self.endpoint_meters_reports_json_results:\n                raw_json = self.endpoint_meters_reports_json_results.json()\n                if phase == None:\n                    jsondata = raw_json[report_map[report]][\"cumulative\"][field]\n                    return jsondata\n                \n                #if production data requested and multiple phases are configured and requested phase is in count of configured phases return data or\n                #if consumption data requested and multiple phases are configured and requested phase is in count of configured phases return date\n                if ((self.production_meters_phase_count > 1 and phase_map[phase] < self.production_meters_phase_count and report==\"production\")\n                 or (self.consumption_meters_phase_count > 1 and phase_map[phase] < self.consumption_meters_phase_count and report!=\"production\")):\n                    try:\n                        jsondata = raw_json[report_map[report]][\"lines\"][phase_map[phase]][field]\n                        return jsondata\n                    except (KeyError, IndexError):\n                        return None\n        return None\n\n    async def production(self,phase=None):\n        \"\"\"Report System or Phase Power Production data from sources for various Envoy types\"\"\"\n        if phase is not None:\n            # if phase is specified return phase data rather then system data\n            return await self.production_phase(phase)\n        \n        if self.endpoint_type == ENVOY_MODEL_S:\n            if self.isProductionMeteringEnabled:\n                raw_json = self.endpoint_meters_reports_json_results.json()\n                production = raw_json[0][\"cumulative\"][\"currW\"]\n            else:\n                raw_json = self.endpoint_production_json_results.json()\n                production = raw_json[\"production\"][0][\"wNow\"]\n        elif self.endpoint_type == ENVOY_MODEL_C:\n            raw_json = self.endpoint_production_v1_results.json()\n            production = raw_json[\"wattsNow\"]\n        elif self.endpoint_type == ENVOY_MODEL_LEGACY:\n            text = self.endpoint_production_results.text\n            match = re.search(PRODUCTION_REGEX, text, re.MULTILINE)\n            if match:\n                if match.group(2) == \"kW\":\n                    production = float(match.group(1)) * 1000\n                else:\n                    if match.group(2) == \"mW\":\n                        production = float(match.group(1)) * 1000000\n                    else:\n                        production = float(match.group(1))\n            else:\n                raise RuntimeError(\"No match for production, check REGEX  \" + text)\n        return int(production)\n\n    async def production_phase(self, phase):\n        \"\"\"Report Phase Power Production data from meters report json\"\"\"\n        jsondata = await self._meters_report_value(\"currW\",report=\"production\",phase=phase)\n        if jsondata is None:\n            return self.message_consumption_not_available if phase is None else None\n        return int(jsondata)\n\n    async def consumption(self,phase=None):\n        \"\"\"Report cumulative or phase Power consumption (to house) from consumption CT meters report\"\"\"\n        jsondata = await self._meters_report_value(\"currW\",report=\"total-consumption\",phase=phase)\n        if jsondata is None:\n            return self.message_consumption_not_available if phase is None else None\n        return int(jsondata)\n\n    async def net_consumption(self,phase=None):\n        \"\"\"Report cumulative or phase Power consumption (to/from grid) from consumption CT meters report\"\"\"\n        jsondata = await self._meters_readings_value(\"instantaneousDemand\",report=\"net-consumption\",phase=phase)\n        if jsondata is None:\n            return self.message_consumption_not_available if phase is None else None\n        return int(jsondata)\n\n    async def daily_production(self,phase=None):\n        \"\"\"Report System or Phase Daily energy Production data from sources for various Envoy types\"\"\"\n        if phase is not None:\n            # if phase is specified return phase data rather then system data\n            return await self.daily_production_phase(phase)\n\n        if self.endpoint_type == ENVOY_MODEL_S and self.isProductionMeteringEnabled:\n            if self._do_not_use_production_json:\n                return self.message_production_not_available\n            raw_json = self.endpoint_production_json_results.json()\n            daily_production = raw_json[\"production\"][1][\"whToday\"]\n        elif self.endpoint_type == ENVOY_MODEL_C or (\n            self.endpoint_type == ENVOY_MODEL_S and not self.isProductionMeteringEnabled\n        ):\n            raw_json = self.endpoint_production_v1_results.json()\n            daily_production = raw_json[\"wattHoursToday\"]\n        elif self.endpoint_type == ENVOY_MODEL_LEGACY:\n            text = self.endpoint_production_results.text\n            match = re.search(DAY_PRODUCTION_REGEX, text, re.MULTILINE)\n            if match:\n                if match.group(2) == \"kWh\":\n                    daily_production = float(match.group(1)) * 1000\n                else:\n                    if match.group(2) == \"MWh\":\n                        daily_production = float(match.group(1)) * 1000000\n                    else:\n                        daily_production = float(match.group(1))\n            else:\n                raise RuntimeError(\n                    \"No match for Day production, \" \"check REGEX  \" + text\n                )\n        return int(daily_production)\n\n    async def daily_production_phase(self, phase):\n        \"\"\"Report Phase Daily energy Production data from production json\"\"\"\n        phase_map = {\"l1\": 0,\"l2\": 1,\"l3\": 2}\n\n        if (self.endpoint_type == ENVOY_MODEL_S and self.isProductionMeteringEnabled and\n            self.production_meters_phase_count > 1 and phase_map[phase] < self.production_meters_phase_count\n            and not self._do_not_use_production_json):\n            raw_json = self.endpoint_production_json_results.json()\n            try:\n                return int(\n                    raw_json[\"production\"][1][\"lines\"][phase_map[phase]][\"whToday\"]\n                )\n            except (KeyError, IndexError):\n                return None\n\n        return None\n\n    async def daily_consumption(self,phase=None):\n        \"\"\"Report System or Phase Daily energy Consumption data from production json\"\"\"\n        if phase is not None:\n            # if phase is specified return phase data rather then system data\n            return await self.daily_consumption_phase(phase)\n \n        \"\"\"Only return data if Envoy supports Consumption\"\"\"\n        if self.endpoint_type == ENVOY_MODEL_S and self.isConsumptionMeteringEnabled:\n            if self._do_not_use_production_json:\n                return self.message_consumption_not_available\n            raw_json = self.endpoint_production_json_results.json()\n            daily_consumption = raw_json[\"consumption\"][0][\"whToday\"]\n            return int(daily_consumption)\n\n        return self.message_consumption_not_available\n\n    async def daily_consumption_phase(self, phase):\n        \"\"\"Report Phase Daily energy Consumption data from production json\"\"\"\n        phase_map = {\"l1\": 0,\"l2\": 1,\"l3\": 2}\n\n        \"\"\"Only return data if Envoy supports Consumption\"\"\"\n        if (self.endpoint_type == ENVOY_MODEL_S and self.isConsumptionMeteringEnabled and\n            self.consumption_meters_phase_count > 1 and phase_map[phase] < self.consumption_meters_phase_count):\n            if self._do_not_use_production_json:\n                return None\n            raw_json = self.endpoint_production_json_results.json()\n            try:\n                return int(\n                    raw_json[\"consumption\"][0][\"lines\"][phase_map[phase]][\"whToday\"]\n                )\n            except (KeyError, IndexError):\n                return None\n\n        return None\n\n    async def seven_days_production(self):\n        \"\"\"Report Last seven day energy production data from production json\"\"\"\n\n        if self.endpoint_type == ENVOY_MODEL_S and self.isProductionMeteringEnabled:\n            if self._do_not_use_production_json:\n                return self.message_production_not_available\n            raw_json = self.endpoint_production_json_results.json()\n            seven_days_production = raw_json[\"production\"][1][\"whLastSevenDays\"]\n        elif self.endpoint_type == ENVOY_MODEL_C or (\n            self.endpoint_type == ENVOY_MODEL_S and not self.isProductionMeteringEnabled\n        ):\n            raw_json = self.endpoint_production_v1_results.json()\n            seven_days_production = raw_json[\"wattHoursSevenDays\"]\n        elif self.endpoint_type == ENVOY_MODEL_LEGACY:\n            text = self.endpoint_production_results.text\n            match = re.search(WEEK_PRODUCTION_REGEX, text, re.MULTILINE)\n            if match:\n                if match.group(2) == \"kWh\":\n                    seven_days_production = float(match.group(1)) * 1000\n                else:\n                    if match.group(2) == \"MWh\":\n                        seven_days_production = float(match.group(1)) * 1000000\n                    else:\n                        seven_days_production = float(match.group(1))\n            else:\n                raise RuntimeError(\n                    \"No match for 7 Day production, \" \"check REGEX \" + text\n                )\n        return int(seven_days_production)\n\n    async def seven_days_consumption(self):\n        \"\"\"Report Last seven day energy consumption data from production json\"\"\"\n\n        \"\"\"Only return data if Envoy supports Consumption\"\"\"\n        if self.endpoint_type == ENVOY_MODEL_S and self.isConsumptionMeteringEnabled:\n            if self._do_not_use_production_json:\n                return self.message_production_not_available    \n            raw_json = self.endpoint_production_json_results.json()\n            seven_days_consumption = raw_json[\"consumption\"][0][\"whLastSevenDays\"]\n            return int(seven_days_consumption)\n\n        return self.message_consumption_not_available\n\n    async def lifetime_production(self,phase=None):\n        \"\"\"Report system or Phase lifetime Energy production from sources for various Envoy types\"\"\"\n        if phase is not None:\n            # if phase is specified return phase data rather then system data\n            return await self.lifetime_production_phase(phase)\n\n        if self.endpoint_type == ENVOY_MODEL_S:\n            if self.isProductionMeteringEnabled:\n                raw_json = self.endpoint_meters_reports_json_results.json()\n                lifetime_production = raw_json[0][\"cumulative\"][\"whDlvdCum\"]\n            else:\n                raw_json = self.endpoint_production_json_results.json()\n                lifetime_production = raw_json[\"production\"][0][\"whLifetime\"]\n        elif self.endpoint_type == ENVOY_MODEL_C:\n            raw_json = self.endpoint_production_v1_results.json()\n            lifetime_production = raw_json[\"wattHoursLifetime\"]\n        elif self.endpoint_type == ENVOY_MODEL_LEGACY:\n            text = self.endpoint_production_results.text\n            match = re.search(LIFE_PRODUCTION_REGEX, text, re.MULTILINE)\n            if match:\n                if match.group(2) == \"kWh\":\n                    lifetime_production = float(match.group(1)) * 1000\n                else:\n                    if match.group(2) == \"MWh\":\n                        lifetime_production = float(match.group(1)) * 1000000\n                    else:\n                        lifetime_production = float(match.group(1))\n            else:\n                raise RuntimeError(\n                    \"No match for Lifetime production, \" \"check REGEX \" + text\n                )\n        return int(lifetime_production)\n\n    async def lifetime_production_phase(self, phase):\n        \"\"\"Report Phase lifetime Energy production from meters repors json\"\"\"\n        jsondata = await self._meters_report_value(\"whDlvdCum\",report=\"production\",phase=phase)\n        if jsondata is None:\n            return self.message_production_not_available if phase is None else None\n        return int(jsondata)\n\n    async def lifetime_net_production(self,phase=None):\n        \"\"\"Report cumulative or phase lifetime net production (exported to grid) from consumption CT meters report\"\"\"\n        jsondata = await self._meters_readings_value(\"actEnergyRcvd\",report=\"net-consumption\",phase=phase)\n        if jsondata is None:\n            return self.message_consumption_not_available if phase is None else None\n        return int(jsondata)\n        \n    async def lifetime_consumption(self,phase=None):\n        \"\"\"Report cumulative or phase lifetime total-consumption from consumption CT meters report\"\"\"\n        jsondata = await self._meters_report_value(\"whDlvdCum\",report=\"total-consumption\",phase=phase)\n        if jsondata is None:\n            return self.message_consumption_not_available if phase is None else None\n        return int(jsondata)\n        \n    async def lifetime_net_consumption(self,phase=None):\n        \"\"\"Report cumulative or phase lifetime net-consumption from consumption CT meters report\"\"\"\n        jsondata = await self._meters_readings_value(\"actEnergyDlvd\",report=\"net-consumption\",phase=phase)\n        if jsondata is None:\n            return self.message_consumption_not_available if phase is None else None\n        return int(jsondata)\n        \n    async def inverters_production(self):\n        \"\"\"Running getData() beforehand will set self.enpoint_type and self.isDataRetrieved\"\"\"\n        \"\"\"so that this method will only read data from stored variables\"\"\"\n\n        \"\"\"Only return data if Envoy supports retrieving Inverter data\"\"\"\n        if not self.get_inverters:\n            return None\n        \n        response_dict = {}\n        try:\n            for item in self.endpoint_production_inverters.json():\n                response_dict[item[\"serialNumber\"]] = [\n                    item[\"lastReportWatts\"],\n                    time.strftime(\n                        \"%Y-%m-%d %H:%M:%S\", time.localtime(item[\"lastReportDate\"])\n                    ),\n                ]\n        except (JSONDecodeError, KeyError, IndexError, TypeError, AttributeError):\n            return None\n\n        return response_dict\n\n    async def battery_storage(self):\n        \"\"\"Return battery data from Envoys that support and have batteries installed\"\"\"\n        if self.endpoint_type in [ENVOY_MODEL_C,ENVOY_MODEL_LEGACY]:\n            return self.message_battery_not_available\n\n        try:\n            raw_json = self.endpoint_production_json_results.json()\n        except JSONDecodeError:\n            return None\n\n        \"\"\"For Envoys that support batteries but do not have them installed the\"\"\"\n        \"\"\"percentFull will not be available in the JSON results. The API will\"\"\"\n        \"\"\"only return battery data if batteries are installed.\"\"\"\n        if \"percentFull\" not in raw_json[\"storage\"][0].keys():\n            # \"ENCHARGE\" batteries are part of the \"ENSEMBLE\" api instead\n            # Check to see if it's there. Enphase has too much fun with these names\n            if self.endpoint_ensemble_json_results is not None:\n                ensemble_json = self.endpoint_ensemble_json_results.json()\n                if len(ensemble_json) > 0 and \"devices\" in ensemble_json[0].keys():\n                    return ensemble_json[0][\"devices\"]\n            return self.message_battery_not_available\n\n        return raw_json[\"storage\"][0]\n\n    async def pf(self,phase=None):\n        \"\"\"Report cumulative or phase PowerFactor from consumption CT meters report\"\"\"\n        jsondata = await self._meters_report_value(\"pwrFactor\",report=\"net-consumption\",phase=phase)\n        if jsondata is None:\n            return self.message_pf_not_available if phase is None else None\n        return float(str(jsondata))\n        \n    async def voltage(self,phase=None):\n        \"\"\"Report cumulative or phase Voltage from consumption CT meters report\"\"\"\n        jsondata = await self._meters_report_value(\"rmsVoltage\",report=\"net-consumption\",phase=phase)\n        if jsondata is None:\n            return self.message_voltage_not_available if phase is None else None\n        return float(str(jsondata))\n        \n    async def frequency(self,phase=None):\n        \"\"\"Report cumulative or phase Frequency from consumption CT meters report\"\"\"\n        jsondata = await self._meters_report_value(\"freqHz\",report=\"net-consumption\",phase=phase)\n        if jsondata is None:\n            return self.message_frequency_not_available if phase is None else None\n        return float(str(jsondata))\n\n    async def consumption_Current(self,phase=None):\n        \"\"\"Report cumulative or phase rmsCurrent from consumption CT meters report\"\"\"\n        jsondata = await self._meters_report_value(\"rmsCurrent\",report=\"net-consumption\",phase=phase)\n        if jsondata is None:\n            return self.message_current_consumption_not_available if phase is None else None\n        return float(str(jsondata))\n        \n    async def production_Current(self,phase=None):\n        \"\"\"Report cumulative or phase rmsCurrent from production CT meters report\"\"\"\n        jsondata = await self._meters_report_value(\"rmsCurrent\",report=\"production\",phase=phase)\n        if jsondata is None:\n            return self.message_current_production_not_available if phase is None else None\n        return float(str(jsondata))\n        \n    async def grid_status(self):\n        \"\"\"Return grid status reported by Envoy\"\"\"\n        if self.has_grid_status and self.endpoint_home_json_results is not None:\n            if self.endpoint_home_json_results.status_code == 200:\n                home_json = self.endpoint_home_json_results.json()\n                if (\"enpower\" in home_json.keys() and \"grid_status\" in home_json[\"enpower\"].keys()):\n                    return home_json[\"enpower\"][\"grid_status\"]\n        self.has_grid_status = False\n        return None\n\n    async def active_inverter_count(self) -> int|str:\n        \"\"\"Return active inverter count from /home html for legacy envoy\"\"\"\n        if (self.endpoint_type == ENVOY_MODEL_LEGACY\n            and self.endpoint_home_results\n            and self.endpoint_home_results.status_code == 200):\n                \n            text = self.endpoint_home_results.text\n            match = re.search(ACTIVE_INVERTER_COUNT_REGEX, text, re.MULTILINE)\n            if match:\n                active_count = int(match.group(1))\n                return active_count\n\n        return self.message_active_inverters_not_available\n\n    async def envoy_info(self):\n        \"\"\"Return information reported by Envoy info.xml.\"\"\"\n        device_data = {}\n\n        if self.endpoint_info_results:\n            try:\n                data = xmltodict.parse(self.endpoint_info_results.text)\n                device_data[\"software\"] = data[\"envoy_info\"][\"device\"][\"software\"]\n                device_data[\"pn\"] = data[\"envoy_info\"][\"device\"][\"pn\"]\n                device_data[\"metered\"] = data[\"envoy_info\"][\"device\"][\"imeter\"]\n            except Exception:  # pylint: disable=broad-except\n                pass\n        # add internal key information for envoy class\n        device_data[\"Using-model\"] = self.endpoint_type\n        device_data[\"Using-httpsflag\"] = self.https_flag\n        device_data[\"Using-ProductionMeteringEnabled\"] = self.isProductionMeteringEnabled\n        device_data[\"Using-ConsumptionMeteringEnabled\"] = self.isConsumptionMeteringEnabled\n        device_data[\"Using-GetInverters\"] = self.get_inverters\n        device_data[\"Using-UseEnligthen\"] = self.use_enlighten_owner_token\n        device_data[\"Using-InfoUpdateInterval\"] = self.info_refresh_buffer_seconds\n        device_data[\"Using-hasgridstatus\"] = self.has_grid_status\n        device_data[\"Using-FetchRetryCount\"] = self._fetch_retries\n        device_data[\"Using-FetchTimeOut\"] = self._fetch_timeout_seconds\n        device_data[\"Using-FetchHoldoff\"] = self._fetch_holdoff_seconds\n\n        if self.endpoint_meters_json_results:\n            device_data[\"Endpoint-meters\"] = self.endpoint_meters_json_results.text\n        else:\n            device_data[\"Endpoint-meters\"] = self.endpoint_meters_json_results\n        if self.endpoint_meters_readings_json_results:\n            device_data[\"Endpoint-meters-readings\"] = self.endpoint_meters_readings_json_results.text\n        else:\n            device_data[\"Endpoint-meters-readings\"] = self.endpoint_meters_readings_json_results\n        if self.endpoint_meters_reports_json_results:\n            device_data[\"Endpoint-meters-reports\"] = self.endpoint_meters_reports_json_results.text\n        else:\n            device_data[\"Endpoint-meters-reports\"] = self.endpoint_meters_reports_json_results\n        if self.endpoint_production_json_results:\n            device_data[\n                \"Endpoint-production_json\"\n            ] = self.endpoint_production_json_results.text\n        else:\n            device_data[\n                \"Endpoint-production_json\"\n            ] = self.endpoint_production_json_results\n        if self.endpoint_production_v1_results:\n            device_data[\n                \"Endpoint-production_v1\"\n            ] = self.endpoint_production_v1_results.text\n        else:\n            device_data[\"Endpoint-production_v1\"] = self.endpoint_production_v1_results\n        if self.endpoint_production_results:\n            device_data[\"Endpoint-production\"] = self.endpoint_production_results.text\n        else:\n            device_data[\"Endpoint-production\"] = self.endpoint_production_results\n        if self.endpoint_production_inverters:\n            device_data[\n                \"Endpoint-production_inverters\"\n            ] = self.endpoint_production_inverters.text\n        else:\n            device_data[\n                \"Endpoint-production_inverters\"\n            ] = self.endpoint_production_inverters\n        if self.endpoint_ensemble_json_results:\n            device_data[\n                \"Endpoint-ensemble_json\"\n            ] = self.endpoint_ensemble_json_results.text\n        else:\n            device_data[\"Endpoint-ensemble_json\"] = self.endpoint_ensemble_json_results\n        if self.endpoint_home_json_results:\n            device_data[\"Endpoint-home\"] = self.endpoint_home_json_results.text\n        else:\n            device_data[\"Endpoint-home\"] = self.endpoint_home_json_results\n        if self.endpoint_info_results:\n            device_data[\"Endpoint-info\"] = self.endpoint_info_results.text\n        else:\n            device_data[\"Endpoint-info\"] = self.endpoint_info_results\n        if self.endpoint_home_results:\n            device_data[\"legacy-home\"] = self.endpoint_home_results.text\n        else:\n            device_data[\"legacy-home\"] = self.endpoint_home_results\n\n        return device_data\n\n    def run_in_console(self, dumpraw=False,loopcount=1,waittime=1):\n        \"\"\"If running this module directly, print all the values in the console.\"\"\"\n        loop = asyncio.get_event_loop()\n        for attempt in range(0,loopcount):\n            if attempt > 0:\n                print(\"Sleeping...\")\n                time.sleep(waittime)\n            print(\"Reading...\")\n            data_results = loop.run_until_complete(\n                asyncio.gather(self.getData(), return_exceptions=False)\n            )\n\n            loop = asyncio.get_event_loop()\n            results = loop.run_until_complete(\n                asyncio.gather(\n                    self.production(), #0\n                    self.consumption(),\n                    self.net_consumption(),\n                    self.daily_production(),\n                    self.daily_consumption(),\n                    self.seven_days_production(),\n                    self.seven_days_consumption(),\n                    self.lifetime_production(),\n                    self.lifetime_net_production(),\n                    self.lifetime_consumption(),\n                    self.lifetime_net_consumption(), #10\n                    self.battery_storage(),\n                    self.inverters_production(),\n                    self.envoy_info(),\n                    self.pf(),\n                    self.voltage(),\n                    self.frequency(),\n                    self.consumption_Current(),\n                    self.production_Current(),\n                    #get values for phase L2\n                    self.production_phase(\"l2\"),\n                    self.consumption(\"l2\"),  #20\n                    self.net_consumption(\"l2\"),\n                    self.daily_production_phase(\"l2\"),\n                    self.daily_consumption_phase(\"l2\"),\n                    self.lifetime_production_phase(\"l2\"),\n                    self.lifetime_net_production(\"l2\"),\n                    self.lifetime_consumption(\"l2\"),\n                    self.lifetime_net_consumption(\"l2\"),\n                    self.pf(\"l2\"),\n                    self.voltage(\"l2\"),\n                    self.frequency(\"l2\"), #30\n                    self.consumption_Current(\"l2\"),\n                    self.production_Current(\"l2\"),\n                    self.grid_status(),\n                    self.active_inverter_count(), #34\n                    return_exceptions=False,\n                )\n            )\n\n            print(\"--System values--\")\n            print(f\"production:               {results[0]}\")\n            print(f\"consumption:              {results[1]}\")\n            print(f\"net_consumption:          {results[2]}\")\n            print(f\"daily_production:         {results[3]}\")\n            print(f\"daily_consumption:        {results[4]}\")\n            print(f\"seven_days_production:    {results[5]}\")\n            print(f\"seven_days_consumption:   {results[6]}\")\n            print(f\"lifetime_production:      {results[7]}\")\n            print(f\"lifetime_net_production:  {results[8]}\")\n            print(f\"lifetime_consumption:     {results[9]}\")\n            print(f\"lifetime_net_consumption: {results[10]}\")\n            print(f\"battery_storage:          {results[11]}\")\n            print(f\"pf:                       {results[14]}\")\n            print(f\"voltage:                  {results[15]}\")\n            print(f\"frequency:                {results[16]}\")\n            print(f\"consumption_Current:      {results[17]}\")\n            print(f\"production_Current:       {results[18]}\")\n            print(\"--Phase L2 values--\")\n            print(f\"production:               {results[19]}\")\n            print(f\"consumption:              {results[20]}\")\n            print(f\"net_consumption:          {results[21]}\")\n            print(f\"daily_production:         {results[22]}\")\n            print(f\"daily_consumption:        {results[23]}\")\n            print(f\"lifetime_production:      {results[24]}\")\n            print(f\"lifetime_net_production:  {results[25]}\")\n            print(f\"lifetime_consumption:     {results[26]}\")\n            print(f\"lifetime_net_consumption: {results[27]}\")\n            print(f\"pf:                       {results[28]}\")\n            print(f\"voltage:                  {results[29]}\")\n            print(f\"frequency:                {results[30]}\")\n            print(f\"consumption_Current:      {results[31]}\")\n            print(f\"production_Current:       {results[32]}\")\n            print(f\"grid_status:              {results[33]}\")\n            print(f\"active_inverters:         {results[34]}\")\n            if \"401\" in str(data_results):\n                print(\n                    \"inverters_production:    Unable to retrieve inverter data - Authentication failure\"\n                )\n            elif results[12] is None:\n                print(\n                    \"inverters_production:    Inverter data not available for your Envoy device.\"\n                )\n            else:\n                print(f\"inverters_production:     {results[12]}\")\n            if dumpraw:\n                print(f\"envoy_info:              {json.dumps(results[13],indent=2)}\")\n\n\nif __name__ == \"__main__\":\n    SECURE = \"\"\n\n    parser = argparse.ArgumentParser(\n        description=\"Retrieve energy information from the Enphase Envoy device.\"\n    )\n    parser.add_argument(\n        \"-u\", \"--user\", dest=\"username\", help=\"Username (Envoy or Enphase)\"\n    )\n    parser.add_argument(\n        \"-p\", \"--pass\", dest=\"password\", help=\"Password (Envoy or Enphase)\"\n    )\n    parser.add_argument(\n        \"-o\",\n        \"--ownertoken\",\n        dest=\"ownertoken\",\n        help=\"Use Enphase owner token from enlighten\",\n        action='store_true'\n    )\n    parser.add_argument(\n        \"-s\",\n        \"--serialnum\",\n        dest=\"enlighten_serial_num\",\n        help=\"Envoy Serial Number. Needed to get Token from Enphase\",\n    )\n    parser.add_argument(\n        \"-i\",\n        \"--ipaddress\",\n        dest=\"host_ip\",\n        help=\"Envoy IP address.\",\n    )\n    parser.add_argument(\n        \"-r\",\n        \"--rawdump\",\n        dest=\"rawdump\",\n        help=\"Dump raw json content of envoy info\",\n        action='store_true'\n    )\n    parser.add_argument(\n        \"-d\",\n        \"--debuglog\",\n        dest=\"debuglog\",\n        help=\"Enable Debug log output\",\n        action='store_true'\n    )\n    parser.add_argument(\n        \"-n\",\n        \"--number\",\n        dest=\"loopcount\",\n        help=\"NUmber of loops to executet\",\n    )\n    parser.add_argument(\n        \"-w\",\n        \"--waittime\",\n        dest=\"waittime\",\n        help=\"TIme to wait between loops [sec]\",\n    )\n\n\n    args = parser.parse_args()\n\n    if args.debuglog:\n        _LOGGER.setLevel(logging.DEBUG)\n        _LOGGER.addHandler(logging.StreamHandler(sys.stdout))\n\n    if args.host_ip is None:\n        HOST = input(\n            \"Enter the Envoy IP address or host name, \"\n            + \"or press enter to use 'envoy' as default: \"\n        )\n    else:\n        HOST = args.host_ip \n\n    if args.username is None:\n        USERNAME = input(\n            \"Enter the Username for Enphase site or Envoy, \"\n            + \"or press enter to use 'envoy' as default: \"\n        )\n    else:\n        USERNAME = args.username\n\n    if args.password is None:\n        PASSWORD = getpass.getpass(\n            \"Enter the Password for Enphase site or Envoy, \"\n            + \"or press enter to use the default password: \"\n        )\n    else:\n        PASSWORD = args.password\n\n    if (\n        args.username is None\n        and args.password is None\n        and args.ownertoken == False\n        and USERNAME != \"\"\n        and PASSWORD != \"\"\n    ):\n        OWNERTOKEN = (input(\n            \"Use Token from Enphase to login to Envoy (Y/N):\"\n        ).lower()[0]==\"y\")\n    else:\n        OWNERTOKEN = args.ownertoken\n\n    if OWNERTOKEN and args.enlighten_serial_num is None:\n        SERIALNUM = input(\n            \"Enter the Envoy serialnumber: \"\n        )\n    else:\n        SERIALNUM = args.enlighten_serial_num\n\n    if OWNERTOKEN:\n        SECURE = \"s\"\n    else:\n        SECURE = \"\"\n\n    if HOST == \"\":\n        HOST = \"envoy\"\n\n    if USERNAME == \"\":\n        USERNAME = \"envoy\"\n\n    LOOPCOUNT = 1\n    if (args.loopcount is not None):\n        LOOPCOUNT = int(args.loopcount)\n    \n    WAITTIME = 1\n    if (args.waittime is not None):\n        WAITTIME = int(args.waittime)\n \n    _LOGGER.debug(\"Host %s\",HOST)\n    _LOGGER.debug(\"Username %s\",USERNAME)\n    _LOGGER.debug(\"Password specified %s\",PASSWORD!=\"\")\n    _LOGGER.debug(\"serialnum %s\",SERIALNUM)\n    _LOGGER.debug(\"Secure %s\",SECURE)\n    _LOGGER.debug(\"Loopcount %s\",LOOPCOUNT)\n    _LOGGER.debug(\"waittime %s\",WAITTIME)\n\n    TESTREADER = EnvoyReader(\n        HOST,\n        username=USERNAME,\n        password=PASSWORD,\n        enlighten_user=USERNAME,\n        enlighten_pass=PASSWORD,\n        inverters=True,\n        enlighten_serial_num=SERIALNUM,\n        https_flag=SECURE,\n        use_enlighten_owner_token=OWNERTOKEN,\n    )\n\n    TESTREADER.run_in_console(args.rawdump,LOOPCOUNT,WAITTIME)"
  },
  {
    "path": "custom_components/enphase_envoy_custom/manifest.json",
    "content": "{\n  \"domain\": \"enphase_envoy\",\n  \"name\": \"Enphase Envoy (DEV)\",\n  \"documentation\": \"https://github.com/briancmpbll/home_assistant_custom_envoy#readme\",\n  \"requirements\": [\n    \"pyjwt\",\n    \"xmltodict\",\n    \"httpx\",\n    \"envoy_utils\"\n  ],\n  \"codeowners\": [\"@briancmpbll\"],\n  \"config_flow\": true,\n  \"iot_class\": \"local_polling\",\n  \"version\": \"0.0.20\"\n}\n"
  },
  {
    "path": "custom_components/enphase_envoy_custom/sensor.py",
    "content": "\"\"\"Support for Enphase Envoy solar energy monitor.\"\"\"\nfrom __future__ import annotations\n\nimport datetime\n\nfrom time import strftime, localtime\n\nfrom homeassistant.components.sensor import SensorEntity\nfrom homeassistant.config_entries import ConfigEntry\nfrom homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN\nfrom homeassistant.core import HomeAssistant, callback\nfrom homeassistant.helpers.entity import DeviceInfo\nfrom homeassistant.helpers.entity_platform import AddEntitiesCallback\nfrom homeassistant.helpers.event import async_track_state_change_event\nfrom homeassistant.helpers.update_coordinator import CoordinatorEntity\n\nfrom .const import BATTERY_ENERGY_DISCHARGED_SENSOR, BATTERY_ENERGY_CHARGED_SENSOR, COORDINATOR, DOMAIN, NAME, SENSORS, ICON, PHASE_SENSORS\n\nasync def async_setup_entry(\n    hass: HomeAssistant,\n    config_entry: ConfigEntry,\n    async_add_entities: AddEntitiesCallback,\n) -> None:\n    \"\"\"Set up envoy sensor platform.\"\"\"\n    data = hass.data[DOMAIN][config_entry.entry_id]\n    coordinator = data[COORDINATOR]\n    name = data[NAME]\n\n    entities = []\n    for sensor_description in SENSORS:\n        if (sensor_description.key == \"inverters\"):\n            if (coordinator.data.get(\"inverters_production\") is not None):\n                for inverter in coordinator.data[\"inverters_production\"]:\n                    entity_name = f\"{name} {sensor_description.name} {inverter}\"\n                    split_name = entity_name.split(\" \")\n                    serial_number = split_name[-1]\n                    entities.append(\n                        EnvoyInverterEntity(\n                            sensor_description,\n                            entity_name,\n                            name,\n                            config_entry.unique_id,\n                            serial_number,\n                            coordinator,\n                        )\n                    )\n        elif (sensor_description.key == \"batteries\"):\n            if (coordinator.data.get(\"batteries\") is not None):\n                for battery in coordinator.data[\"batteries\"]:\n                    entity_name = f\"{name} {sensor_description.name} {battery}\"\n                    serial_number = battery\n                    entities.append(\n                        EnvoyBatteryEntity(\n                            sensor_description,\n                            entity_name,\n                            name,\n                            config_entry.unique_id,\n                            serial_number,\n                            coordinator\n                        )\n                    )\n\n        elif (sensor_description.key == \"current_battery_capacity\"):\n            if (coordinator.data.get(\"batteries\") is not None):\n                battery_capacity_entity = TotalBatteryCapacityEntity(\n                    sensor_description,\n                    f\"{name} {sensor_description.name}\",\n                    name,\n                    config_entry.unique_id,\n                    None,\n                    coordinator\n                )\n                entities.append(battery_capacity_entity)\n\n                entities.append(\n                    BatteryEnergyChangeEntity(\n                        BATTERY_ENERGY_CHARGED_SENSOR,\n                        f\"{name} {BATTERY_ENERGY_CHARGED_SENSOR.name}\",\n                        name,\n                        config_entry.unique_id,\n                        None,\n                        battery_capacity_entity,\n                        True\n                    )\n                )\n\n                entities.append(\n                    BatteryEnergyChangeEntity(\n                        BATTERY_ENERGY_DISCHARGED_SENSOR,\n                        f\"{name} {BATTERY_ENERGY_DISCHARGED_SENSOR.name}\",\n                        name,\n                        config_entry.unique_id,\n                        None,\n                        battery_capacity_entity,\n                        False\n                    )\n                )\n\n        elif (sensor_description.key == \"total_battery_percentage\"):\n            if (coordinator.data.get(\"batteries\") is not None):\n                entities.append(TotalBatteryPercentageEntity(\n                        sensor_description,\n                        f\"{name} {sensor_description.name}\",\n                        name,\n                        config_entry.unique_id,\n                        None,\n                        coordinator\n                    ))\n\n        else:\n            data = coordinator.data.get(sensor_description.key)\n            if isinstance(data, str) and \"not available\" in data:\n                continue\n\n            entity_name = f\"{name} {sensor_description.name}\"\n            entities.append(\n                CoordinatedEnvoyEntity(\n                    sensor_description,\n                    entity_name,\n                    name,\n                    config_entry.unique_id,\n                    None,\n                    coordinator,\n                )\n            )\n\n    for sensor_description in PHASE_SENSORS:\n        data = coordinator.data.get(sensor_description.key)\n        if data == None:\n            continue\n\n        entity_name = f\"{name} {sensor_description.name}\"\n        entities.append(\n            CoordinatedEnvoyEntity(\n                sensor_description,\n                entity_name,\n                name,\n                config_entry.unique_id,\n                None,\n                coordinator,\n            )\n        )\n\n    async_add_entities(entities)\n\nclass EnvoyEntity(SensorEntity):\n    \"\"\"Envoy entity\"\"\"\n\n    def __init__(\n        self,\n        description,\n        name,\n        device_name,\n        device_serial_number,\n        serial_number,\n    ):\n        \"\"\"Initialize Envoy entity.\"\"\"\n        self.entity_description = description\n        self._name = name\n        self._serial_number = serial_number\n        self._device_name = device_name\n        self._device_serial_number = device_serial_number\n\n    @property\n    def name(self):\n        \"\"\"Return the name of the sensor.\"\"\"\n        return self._name\n\n    @property\n    def unique_id(self):\n        \"\"\"Return the unique id of the sensor.\"\"\"\n        if self._serial_number:\n            return self._serial_number\n        if self._device_serial_number:\n            return f\"{self._device_serial_number}_{self.entity_description.key}\"\n\n    @property\n    def icon(self):\n        \"\"\"Icon to use in the frontend, if any.\"\"\"\n        return ICON\n\n    @property\n    def extra_state_attributes(self):\n        \"\"\"Return the state attributes.\"\"\"\n        return None\n\n    @property\n    def device_info(self) -> DeviceInfo | None:\n        \"\"\"Return the device_info of the device.\"\"\"\n        if not self._device_serial_number:\n            return None\n\n        sw_version = None\n        hw_version = None\n\n        if hasattr(self, 'coordinator'):\n            if self.coordinator.data.get(\"envoy_info\"):\n                sw_version = self.coordinator.data.get(\"envoy_info\").get(\"software\", None)\n                hw_version = self.coordinator.data.get(\"envoy_info\").get(\"pn\", None)\n\n        return DeviceInfo(\n            identifiers={(DOMAIN, str(self._device_serial_number))},\n            manufacturer=\"Enphase\",\n            model=\"Envoy\",\n            name=self._device_name,\n            sw_version=sw_version,\n            hw_version=hw_version,\n        )\n\nclass CoordinatedEnvoyEntity(EnvoyEntity, CoordinatorEntity):\n    def __init__(\n        self,\n        description,\n        name,\n        device_name,\n        device_serial_number,\n        serial_number,\n        coordinator,\n    ):\n        EnvoyEntity.__init__(self, description, name, device_name, device_serial_number, serial_number)\n        CoordinatorEntity.__init__(self, coordinator)\n\n    @property\n    def native_value(self):\n        \"\"\"Return the state of the sensor.\"\"\"\n        return self.coordinator.data.get(self.entity_description.key)\n\nclass EnvoyInverterEntity(CoordinatedEnvoyEntity):\n    \"\"\"Envoy inverter entity.\"\"\"\n\n    def __init__(\n        self,\n        description,\n        name,\n        device_name,\n        device_serial_number,\n        serial_number,\n        coordinator,\n    ):\n        super().__init__(\n            description=description,\n            name=name,\n            device_name=device_name,\n            device_serial_number=device_serial_number,\n            serial_number=serial_number,\n            coordinator=coordinator\n        )\n\n    @property\n    def native_value(self):\n        \"\"\"Return the state of the sensor.\"\"\"\n        if (\n            self.coordinator.data.get(\"inverters_production\") is not None\n        ):\n            return self.coordinator.data.get(\"inverters_production\").get(\n                self._serial_number\n            )[0]\n\n        return None\n\n    @property\n    def extra_state_attributes(self):\n        \"\"\"Return the state attributes.\"\"\"\n        if (\n            self.coordinator.data.get(\"inverters_production\") is not None\n        ):\n            value = self.coordinator.data.get(\"inverters_production\").get(\n                self._serial_number\n            )[1]\n            return {\"last_reported\": value}\n\n        return None\n\nclass EnvoyBatteryEntity(CoordinatedEnvoyEntity):\n    \"\"\"Envoy battery entity.\"\"\"\n\n    def __init__(\n        self,\n        description,\n        name,\n        device_name,\n        device_serial_number,\n        serial_number,\n        coordinator,\n    ):\n        super().__init__(\n            description=description,\n            name=name,\n            device_name=device_name,\n            device_serial_number=device_serial_number,\n            serial_number=serial_number,\n            coordinator=coordinator\n        )\n\n    @property\n    def native_value(self):\n        \"\"\"Return the state of the sensor.\"\"\"\n        if (\n            self.coordinator.data.get(\"batteries\") is not None\n        ):\n            return self.coordinator.data.get(\"batteries\").get(\n                self._serial_number\n            ).get(\"percentFull\")\n\n        return None\n\n    @property\n    def extra_state_attributes(self):\n        \"\"\"Return the state attributes.\"\"\"\n        if (\n            self.coordinator.data.get(\"batteries\") is not None\n        ):\n            battery = self.coordinator.data.get(\"batteries\").get(\n                self._serial_number\n            )\n            last_reported = strftime(\n                \"%Y-%m-%d %H:%M:%S\", localtime(battery.get(\"last_rpt_date\"))\n            )\n            return {\n                \"last_reported\": last_reported,\n                \"capacity\": battery.get(\"encharge_capacity\")\n            }\n\n        return None\n\nclass TotalBatteryCapacityEntity(CoordinatedEnvoyEntity):\n    def __init__(\n        self,\n        description,\n        name,\n        device_name,\n        device_serial_number,\n        serial_number,\n        coordinator,\n    ):\n        super().__init__(\n            description=description,\n            name=name,\n            device_name=device_name,\n            device_serial_number=device_serial_number,\n            serial_number=serial_number,\n            coordinator=coordinator\n        )\n\n    @property\n    def native_value(self):\n        \"\"\"Return the state of the sensor.\"\"\"\n        batteries = self.coordinator.data.get(\"batteries\")\n        if (\n            batteries is not None\n        ):\n            total = 0\n            for battery in batteries:\n                percentage = batteries.get(battery).get(\"percentFull\")\n                capacity = batteries.get(battery).get(\"encharge_capacity\")\n                total += round(capacity * (percentage / 100.0))\n\n            return total\n\n        return None\n\n\nclass TotalBatteryPercentageEntity(CoordinatedEnvoyEntity):\n    def __init__(\n        self,\n        description,\n        name,\n        device_name,\n        device_serial_number,\n        serial_number,\n        coordinator,\n    ):\n        super().__init__(\n            description=description,\n            name=name,\n            device_name=device_name,\n            device_serial_number=device_serial_number,\n            serial_number=serial_number,\n            coordinator=coordinator\n        )\n\n    @property\n    def native_value(self):\n        \"\"\"Return the state of the sensor.\"\"\"\n        batteries = self.coordinator.data.get(\"batteries\")\n        if (\n            batteries is not None\n        ):\n            battery_sum = 0\n            for battery in batteries:\n                battery_sum += batteries.get(battery).get(\"percentFull\", 0)\n\n            return round(battery_sum / len(batteries), 2)\n\n        return None\n\nclass BatteryEnergyChangeEntity(EnvoyEntity):\n    def __init__(\n        self,\n        description,\n        name,\n        device_name,\n        device_serial_number,\n        serial_number,\n        total_battery_capacity_entity,\n        positive: bool\n    ):\n        super().__init__(\n            description=description,\n            name=name,\n            device_name=device_name,\n            device_serial_number=device_serial_number,\n            serial_number=serial_number,\n        )\n\n        self._sensor_source = total_battery_capacity_entity\n        self._positive = positive\n        self._state = 0\n        self._attr_last_reset = datetime.datetime.now()\n\n    async def async_added_to_hass(self):\n        \"\"\"Handle entity which will be added.\"\"\"\n        await super().async_added_to_hass()\n\n        @callback\n        def calc_change(event):\n            \"\"\"Handle the sensor state changes.\"\"\"\n            old_state = event.data.get(\"old_state\")\n            new_state = event.data.get(\"new_state\")\n\n            if (\n                old_state is None\n                or old_state.state in (STATE_UNKNOWN, STATE_UNAVAILABLE)\n                or new_state.state in (STATE_UNKNOWN, STATE_UNAVAILABLE)\n            ):\n                self._state = 0\n\n            else:\n                old_state_value = int(old_state.state)\n                new_state_value = int(new_state.state)\n\n                if (self._positive):\n                    if (new_state_value > old_state_value):\n                        self._state = new_state_value - old_state_value\n                    else:\n                        self._state = 0\n\n                else:\n                    if (old_state_value > new_state_value):\n                        self._state = old_state_value - new_state_value\n                    else:\n                        self._state = 0\n\n            self._attr_last_reset = datetime.datetime.now()\n            self.async_write_ha_state()\n\n        self.async_on_remove(\n            async_track_state_change_event(\n                self.hass, self._sensor_source.entity_id, calc_change\n            )\n        )\n\n    @property\n    def native_value(self):\n        \"\"\"Return the state of the sensor.\"\"\"\n        return self._state\n"
  },
  {
    "path": "custom_components/enphase_envoy_custom/strings.json",
    "content": "{\n  \"config\": {\n    \"flow_title\": \"{serial} ({host})\",\n    \"step\": {\n      \"user\": {\n        \"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.\",\n        \"data\": {\n          \"host\": \"[%key:common::config_flow::data::host%]\",\n          \"username\": \"[%key:common::config_flow::data::username%]\",\n          \"password\": \"[%key:common::config_flow::data::password%]\",\n          \"serial\": \"envoy serial number\",\n          \"use_enlighten\": \"Use Enlighten\"\n        }\n      }\n    },\n    \"error\": {\n      \"cannot_connect\": \"[%key:common::config_flow::error::cannot_connect%]\",\n      \"invalid_auth\": \"[%key:common::config_flow::error::invalid_auth%]\",\n      \"unknown\": \"[%key:common::config_flow::error::unknown%]\"\n    },\n    \"abort\": {\n      \"already_configured\": \"[%key:common::config_flow::abort::already_configured_device%]\",\n      \"reauth_successful\": \"[%key:common::config_flow::abort::reauth_successful%]\"\n    }\n  },\n  \"options\": {\n    \"step\": {\n      \"user\": {\n        \"title\": \"Envoy options\",\n        \"data\": {\n          \"data_interval\": \"Time between entity updates [s].\",\n          \"data_fetch_timeout_seconds\": \"Timeout for getting single Envoy data page [s], minimum 5.\",\n          \"data_fetch_retry_count\": \"How many retries in getting single Envoy data page. minium 1.\",\n          \"data_fetch_holdoff_seconds\": \"Time between 2 retries to get single Envoy data page[s], minimum 0.\",\n          \"data_collection_timeout_seconds\": \"Overall Timeout on getting all Envoy data pages[s], minimum 30.\"\n        },\n        \"data_description\": {\n          \"data_interval\": \"Time between data updates, minimum 5 sec. After any change here or below reload the envoy.\",\n          \"data_collection_timeout_seconds\": \"If overall data collection takes more then this time it will be cancelled. Account for retries.\"\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "custom_components/enphase_envoy_custom/translations/ca.json",
    "content": "{\n    \"config\": {\n        \"abort\": {\n            \"already_configured\": \"El dispositiu ja est\\u00e0 configurat\",\n            \"reauth_successful\": \"Re-autenticaci\\u00f3 realitzada correctament\"\n        },\n        \"error\": {\n            \"cannot_connect\": \"Ha fallat la connexi\\u00f3\",\n            \"invalid_auth\": \"Autenticaci\\u00f3 inv\\u00e0lida\",\n            \"unknown\": \"Error inesperat\"\n        },\n        \"flow_title\": \"{serial} ({host})\",\n        \"step\": {\n            \"user\": {\n                \"data\": {\n                    \"host\": \"Amfitri\\u00f3\",\n                    \"password\": \"Contrasenya\",\n                    \"username\": \"Nom d'usuari\"\n                }\n            }\n        }\n    }\n}"
  },
  {
    "path": "custom_components/enphase_envoy_custom/translations/cs.json",
    "content": "{\n    \"config\": {\n        \"abort\": {\n            \"already_configured\": \"Za\\u0159\\u00edzen\\u00ed je ji\\u017e nastaveno\",\n            \"reauth_successful\": \"Op\\u011btovn\\u00e9 ov\\u011b\\u0159en\\u00ed bylo \\u00fasp\\u011b\\u0161n\\u00e9\"\n        },\n        \"error\": {\n            \"cannot_connect\": \"Nepoda\\u0159ilo se p\\u0159ipojit\",\n            \"invalid_auth\": \"Neplatn\\u00e9 ov\\u011b\\u0159en\\u00ed\",\n            \"unknown\": \"Neo\\u010dek\\u00e1van\\u00e1 chyba\"\n        },\n        \"flow_title\": \"Envoy {serial} ({host})\",\n        \"step\": {\n            \"user\": {\n                \"data\": {\n                    \"host\": \"Hostitel\",\n                    \"password\": \"Heslo\",\n                    \"username\": \"U\\u017eivatelsk\\u00e9 jm\\u00e9no\"\n                }\n            }\n        }\n    }\n}"
  },
  {
    "path": "custom_components/enphase_envoy_custom/translations/de.json",
    "content": "{\n    \"config\": {\n        \"abort\": {\n            \"already_configured\": \"Ger\\u00e4t ist bereits konfiguriert\",\n            \"reauth_successful\": \"Die erneute Authentifizierung war erfolgreich\"\n        },\n        \"error\": {\n            \"cannot_connect\": \"Verbindung fehlgeschlagen\",\n            \"invalid_auth\": \"Ung\\u00fcltige Authentifizierung\",\n            \"unknown\": \"Unerwarteter Fehler\"\n        },\n        \"flow_title\": \"{serial} ({host})\",\n        \"step\": {\n            \"user\": {\n                \"data\": {\n                    \"host\": \"Host\",\n                    \"password\": \"Passwort\",\n                    \"username\": \"Benutzername\"\n                }\n            }\n        }\n    }\n}"
  },
  {
    "path": "custom_components/enphase_envoy_custom/translations/el.json",
    "content": "{\n    \"config\": {\n        \"step\": {\n            \"user\": {\n            }\n        }\n    }\n}"
  },
  {
    "path": "custom_components/enphase_envoy_custom/translations/en.json",
    "content": "{\n  \"config\": {\n    \"abort\": {\n      \"already_configured\": \"Device is already configured\",\n      \"reauth_successful\": \"Re-authentication was successful\"\n    },\n    \"error\": {\n      \"cannot_connect\": \"Failed to connect\",\n      \"invalid_auth\": \"Invalid authentication\",\n      \"unknown\": \"Unexpected error\"\n    },\n    \"flow_title\": \"{serial} ({host})\",\n    \"step\": {\n      \"user\": {\n        \"data\": {\n          \"host\": \"Host\",\n          \"password\": \"Password\",\n          \"username\": \"Username\",\n          \"serial\": \"envoy serial number\",\n          \"use_enlighten\": \"Use Enlighten\"\n        },\n        \"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.\"\n      }\n    }\n  },\n  \"options\": {\n    \"step\": {\n      \"user\": {\n        \"title\": \"Envoy options\",\n        \"data\": {\n          \"data_interval\": \"Time between entity updates [s].\",\n          \"data_fetch_timeout_seconds\": \"Timeout for getting single Envoy data page [s], minimum 5.\",\n          \"data_fetch_retry_count\": \"How many retries in getting single Envoy data page. minimum 1.\",\n          \"data_fetch_holdoff_seconds\": \"Time between 2 retries to get single Envoy data page[s], minimum 0.\",\n          \"data_collection_timeout_seconds\": \"Overall Timeout on getting all Envoy data pages[s], minimum 30.\",\n          \"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)\"\n        },\n        \"data_description\": {\n          \"data_interval\": \"Time between data updates, minimum 5 sec. After any change here or below reload the envoy.\",\n          \"data_collection_timeout_seconds\": \"If overall data collection takes more then this time it will be cancelled. Account for retries.\"\n        }\n      }\n    }\n  }\n\n}\n"
  },
  {
    "path": "custom_components/enphase_envoy_custom/translations/es-419.json",
    "content": "{\n    \"config\": {\n        \"flow_title\": \"{serial} ({host})\"\n    }\n}"
  },
  {
    "path": "custom_components/enphase_envoy_custom/translations/es.json",
    "content": "{\n    \"config\": {\n        \"abort\": {\n            \"already_configured\": \"El dispositivo ya est\\u00e1 configurado\",\n            \"reauth_successful\": \"La reautenticaci\\u00f3n se realiz\\u00f3 correctamente\"\n        },\n        \"error\": {\n            \"cannot_connect\": \"No se pudo conectar\",\n            \"invalid_auth\": \"Autenticaci\\u00f3n no v\\u00e1lida\",\n            \"unknown\": \"Error inesperado\"\n        },\n        \"flow_title\": \"Envoy {serial} ({host})\",\n        \"step\": {\n            \"user\": {\n                \"data\": {\n                    \"host\": \"Host\",\n                    \"password\": \"Contrase\\u00f1a\",\n                    \"username\": \"Usuario\"\n                }\n            }\n        }\n    }\n}"
  },
  {
    "path": "custom_components/enphase_envoy_custom/translations/et.json",
    "content": "{\n    \"config\": {\n        \"abort\": {\n            \"already_configured\": \"Seade on juba h\\u00e4\\u00e4lestatud\",\n            \"reauth_successful\": \"Taastuvastamine \\u00f5nnestus\"\n        },\n        \"error\": {\n            \"cannot_connect\": \"\\u00dchendamine nurjus\",\n            \"invalid_auth\": \"Tuvastamise viga\",\n            \"unknown\": \"Tundmatu viga\"\n        },\n        \"flow_title\": \"{serial} ({host})\",\n        \"step\": {\n            \"user\": {\n                \"data\": {\n                    \"host\": \"Host\",\n                    \"password\": \"Salas\\u00f5na\",\n                    \"username\": \"Kasutajanimi\"\n                }\n            }\n        }\n    }\n}"
  },
  {
    "path": "custom_components/enphase_envoy_custom/translations/fr.json",
    "content": "{\n    \"config\": {\n        \"abort\": {\n            \"already_configured\": \"L'appareil est d\\u00e9j\\u00e0 configur\\u00e9\",\n            \"reauth_successful\": \"La r\\u00e9-authentification a r\\u00e9ussi\"\n        },\n        \"error\": {\n            \"cannot_connect\": \"\\u00c9chec de connexion\",\n            \"invalid_auth\": \"Authentification invalide\",\n            \"unknown\": \"Erreur inattendue\"\n        },\n        \"flow_title\": \"{serial} ({host})\",\n        \"step\": {\n            \"user\": {\n                \"data\": {\n                    \"host\": \"H\\u00f4te\",\n                    \"password\": \"Mot de passe\",\n                    \"username\": \"Nom d'utilisateur\",\n                    \"serial\": \"Num\\u00e9ro de s\\u00e9rie envoy\",\n                    \"use_enlighten\": \"R\\u00e9cup\\u00e9ration d'un Token\"\n\t\t},\n\t\t\"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.\"\n      }\n        }\n    },\n\t\"options\": {\n\t    \"step\": {\n\t        \"user\": {\n\t            \"title\": \"Options Envoy\",\n\t            \"data\": {\n\t                \"data_interval\": \"Temps entre 2 rafraichissement [s]\",\n\t\t\t\"data_fetch_timeout_seconds\": \"TimeOut pour obtenir une page de donn\\u00e9es Envoy [s], minimum 5.\",\n          \t\t\"data_fetch_retry_count\": \"Nombre de tentatives max pour obtenir une page de donn\\u00e9es Envoy [s]. au minimum 1.\",\n          \t\t\"data_fetch_holdoff_seconds\": \"D\\u00e9lai entre 2 tentatives pour obtenir une page de donn\\u00e9es Envoy [s], minimum 0.\",\n          \t\t\"data_collection_timeout_seconds\": \"TimeOut totale pour obtenir toutes les donn\\u00e9es Envoy [s], minimum 30.\"\n        \n\t            },\n\t            \"data_description\": {\n\t                \"data_interval\": \"Temps entre 2 rafraichissement, minimum 5 sec. Relancer apres un changement\",\n\t\t\t\"data_collection_timeout_seconds\": \"Si la récupération des donn\\u00e9es prend plus de temps que le TimeOut, elle sera annul\\u00e9e\"\n\n\t            }\n\t        }\n\t    }\n\t}\n}\n"
  },
  {
    "path": "custom_components/enphase_envoy_custom/translations/he.json",
    "content": "{\n    \"config\": {\n        \"abort\": {\n            \"already_configured\": \"\\u05ea\\u05e6\\u05d5\\u05e8\\u05ea \\u05d4\\u05d4\\u05ea\\u05e7\\u05df \\u05db\\u05d1\\u05e8 \\u05e0\\u05e7\\u05d1\\u05e2\\u05d4\",\n            \"reauth_successful\": \"\\u05d4\\u05d0\\u05d9\\u05de\\u05d5\\u05ea \\u05de\\u05d7\\u05d3\\u05e9 \\u05d4\\u05e6\\u05dc\\u05d9\\u05d7\"\n        },\n        \"error\": {\n            \"cannot_connect\": \"\\u05d4\\u05d4\\u05ea\\u05d7\\u05d1\\u05e8\\u05d5\\u05ea \\u05e0\\u05db\\u05e9\\u05dc\\u05d4\",\n            \"invalid_auth\": \"\\u05d0\\u05d9\\u05de\\u05d5\\u05ea \\u05dc\\u05d0 \\u05d7\\u05d5\\u05e7\\u05d9\",\n            \"unknown\": \"\\u05e9\\u05d2\\u05d9\\u05d0\\u05d4 \\u05d1\\u05dc\\u05ea\\u05d9 \\u05e6\\u05e4\\u05d5\\u05d9\\u05d4\"\n        },\n        \"flow_title\": \"{serial} ({host})\",\n        \"step\": {\n            \"user\": {\n                \"data\": {\n                    \"host\": \"\\u05de\\u05d0\\u05e8\\u05d7\",\n                    \"password\": \"\\u05e1\\u05d9\\u05e1\\u05de\\u05d4\",\n                    \"username\": \"\\u05e9\\u05dd \\u05de\\u05e9\\u05ea\\u05de\\u05e9\"\n                }\n            }\n        }\n    }\n}"
  },
  {
    "path": "custom_components/enphase_envoy_custom/translations/hu.json",
    "content": "{\n    \"config\": {\n        \"abort\": {\n            \"already_configured\": \"Az eszk\\u00f6z m\\u00e1r konfigur\\u00e1lva van\",\n            \"reauth_successful\": \"Az \\u00fajhiteles\\u00edt\\u00e9s sikeres volt\"\n        },\n        \"error\": {\n            \"cannot_connect\": \"Sikertelen csatlakoz\\u00e1s\",\n            \"invalid_auth\": \"\\u00c9rv\\u00e9nytelen hiteles\\u00edt\\u00e9s\",\n            \"unknown\": \"V\\u00e1ratlan hiba t\\u00f6rt\\u00e9nt\"\n        },\n        \"flow_title\": \"{serial} ({host})\",\n        \"step\": {\n            \"user\": {\n                \"data\": {\n                    \"host\": \"C\\u00edm\",\n                    \"password\": \"Jelsz\\u00f3\",\n                    \"username\": \"Felhaszn\\u00e1l\\u00f3n\\u00e9v\"\n                }\n            }\n        }\n    }\n}"
  },
  {
    "path": "custom_components/enphase_envoy_custom/translations/id.json",
    "content": "{\n    \"config\": {\n        \"abort\": {\n            \"already_configured\": \"Perangkat sudah dikonfigurasi\",\n            \"reauth_successful\": \"Autentikasi ulang berhasil\"\n        },\n        \"error\": {\n            \"cannot_connect\": \"Gagal terhubung\",\n            \"invalid_auth\": \"Autentikasi tidak valid\",\n            \"unknown\": \"Kesalahan yang tidak diharapkan\"\n        },\n        \"flow_title\": \"{serial} ({host})\",\n        \"step\": {\n            \"user\": {\n                \"data\": {\n                    \"host\": \"Host\",\n                    \"password\": \"Kata Sandi\",\n                    \"username\": \"Nama Pengguna\"\n                }\n            }\n        }\n    }\n}"
  },
  {
    "path": "custom_components/enphase_envoy_custom/translations/it.json",
    "content": "{\n    \"config\": {\n        \"abort\": {\n            \"already_configured\": \"Il dispositivo \\u00e8 gi\\u00e0 configurato\",\n            \"reauth_successful\": \"La nuova autenticazione \\u00e8 stata eseguita correttamente\"\n        },\n        \"error\": {\n            \"cannot_connect\": \"Impossibile connettersi\",\n            \"invalid_auth\": \"Autenticazione non valida\",\n            \"unknown\": \"Errore imprevisto\"\n        },\n        \"flow_title\": \"{serial} ({host})\",\n        \"step\": {\n            \"user\": {\n                \"data\": {\n                    \"host\": \"Host\",\n                    \"password\": \"Password\",\n                    \"username\": \"Nome utente\"\n                }\n            }\n        }\n    }\n}"
  },
  {
    "path": "custom_components/enphase_envoy_custom/translations/ja.json",
    "content": "{\n    \"config\": {\n        \"abort\": {\n            \"already_configured\": \"\\u30c7\\u30d0\\u30a4\\u30b9\\u306f\\u3059\\u3067\\u306b\\u8a2d\\u5b9a\\u3055\\u308c\\u3066\\u3044\\u307e\\u3059\",\n            \"reauth_successful\": \"\\u518d\\u8a8d\\u8a3c\\u306b\\u6210\\u529f\\u3057\\u307e\\u3057\\u305f\"\n        },\n        \"error\": {\n            \"cannot_connect\": \"\\u63a5\\u7d9a\\u306b\\u5931\\u6557\\u3057\\u307e\\u3057\\u305f\",\n            \"invalid_auth\": \"\\u7121\\u52b9\\u306a\\u8a8d\\u8a3c\",\n            \"unknown\": \"\\u4e88\\u671f\\u3057\\u306a\\u3044\\u30a8\\u30e9\\u30fc\"\n        },\n        \"flow_title\": \"{serial} ({host})\",\n        \"step\": {\n            \"user\": {\n                \"data\": {\n                    \"host\": \"\\u30db\\u30b9\\u30c8\",\n                    \"password\": \"\\u30d1\\u30b9\\u30ef\\u30fc\\u30c9\",\n                    \"username\": \"\\u30e6\\u30fc\\u30b6\\u30fc\\u540d\"\n                }\n            }\n        }\n    }\n}"
  },
  {
    "path": "custom_components/enphase_envoy_custom/translations/ko.json",
    "content": "{\n    \"config\": {\n        \"abort\": {\n            \"already_configured\": \"\\uae30\\uae30\\uac00 \\uc774\\ubbf8 \\uad6c\\uc131\\ub418\\uc5c8\\uc2b5\\ub2c8\\ub2e4\",\n            \"reauth_successful\": \"\\uc7ac\\uc778\\uc99d\\uc5d0 \\uc131\\uacf5\\ud588\\uc2b5\\ub2c8\\ub2e4\"\n        },\n        \"error\": {\n            \"cannot_connect\": \"\\uc5f0\\uacb0\\ud558\\uc9c0 \\ubabb\\ud588\\uc2b5\\ub2c8\\ub2e4\",\n            \"invalid_auth\": \"\\uc778\\uc99d\\uc774 \\uc798\\ubabb\\ub418\\uc5c8\\uc2b5\\ub2c8\\ub2e4\",\n            \"unknown\": \"\\uc608\\uc0c1\\uce58 \\ubabb\\ud55c \\uc624\\ub958\\uac00 \\ubc1c\\uc0dd\\ud588\\uc2b5\\ub2c8\\ub2e4\"\n        },\n        \"flow_title\": \"Envoy {serial} ({host})\",\n        \"step\": {\n            \"user\": {\n                \"data\": {\n                    \"host\": \"\\ud638\\uc2a4\\ud2b8\",\n                    \"password\": \"\\ube44\\ubc00\\ubc88\\ud638\",\n                    \"username\": \"\\uc0ac\\uc6a9\\uc790 \\uc774\\ub984\"\n                }\n            }\n        }\n    }\n}"
  },
  {
    "path": "custom_components/enphase_envoy_custom/translations/nl.json",
    "content": "{\n    \"config\": {\n        \"abort\": {\n            \"already_configured\": \"Apparaat is al geconfigureerd\",\n            \"reauth_successful\": \"Herauthenticatie was succesvol\"\n        },\n        \"error\": {\n            \"cannot_connect\": \"Kan geen verbinding maken\",\n            \"invalid_auth\": \"Ongeldige authenticatie\",\n            \"unknown\": \"Onverwachte fout\"\n        },\n        \"flow_title\": \"{serial} ({host})\",\n        \"step\": {\n            \"user\": {\n                \"data\": {\n                    \"host\": \"Host\",\n                    \"password\": \"Wachtwoord\",\n                    \"username\": \"Gebruikersnaam\",\n                    \"serial\": \"Envoy serienummer\",\n                    \"use_enlighten\": \"Gebruik Enlighten\"\n                },\n                \"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.\"\n            }\n        }\n    },\n    \"options\": {\n      \"step\": {\n        \"user\": {\n          \"title\": \"Envoy opties\",\n          \"data\": {\n            \"data_interval\": \"Tijd tussen entity updates [s].\",\n            \"data_fetch_timeout_seconds\": \"Timeout voor het lezen van Envoy gegevens [s], minimaal 5.\",\n            \"data_fetch_retry_count\": \"Aantal extra pogingen Envoy gegevens te lezen als timeout optreed, minimaal 1.\",\n            \"data_fetch_holdoff_seconds\": \"Wachttijd tussen 2 pogingen om Envoy gegevens te lezen [s], minimaal 0.\",\n            \"data_collection_timeout_seconds\": \"Maximale tijd voor het lezen van alle benodigde Envoy gegevens[s], minimaal 30.\",\n            \"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)\"\n          },\n          \"data_description\": {\n            \"data_interval\": \"Tijd tussen 2 gegevens verversingen, minimaal 5 sec. Bij wijziging van een instelling op deze pagina de Envoy opnieuw laden.\",\n            \"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.\"\n          }\n        }\n      }\n    }\n}\n"
  },
  {
    "path": "custom_components/enphase_envoy_custom/translations/no.json",
    "content": "{\n    \"config\": {\n        \"abort\": {\n            \"already_configured\": \"Enheten er allerede konfigurert\",\n            \"reauth_successful\": \"Godkjenning p\\u00e5 nytt var vellykket\"\n        },\n        \"error\": {\n            \"cannot_connect\": \"Tilkobling mislyktes\",\n            \"invalid_auth\": \"Ugyldig godkjenning\",\n            \"unknown\": \"Uventet feil\"\n        },\n        \"flow_title\": \"{serial} ({host})\",\n        \"step\": {\n            \"user\": {\n                \"data\": {\n                    \"host\": \"Vert\",\n                    \"password\": \"Passord\",\n                    \"username\": \"Brukernavn\"\n                }\n            }\n        }\n    }\n}"
  },
  {
    "path": "custom_components/enphase_envoy_custom/translations/pl.json",
    "content": "{\n    \"config\": {\n        \"abort\": {\n            \"already_configured\": \"Urz\\u0105dzenie jest ju\\u017c skonfigurowane\",\n            \"reauth_successful\": \"Ponowne uwierzytelnienie powiod\\u0142o si\\u0119\"\n        },\n        \"error\": {\n            \"cannot_connect\": \"Nie mo\\u017cna nawi\\u0105za\\u0107 po\\u0142\\u0105czenia\",\n            \"invalid_auth\": \"Niepoprawne uwierzytelnienie\",\n            \"unknown\": \"Nieoczekiwany b\\u0142\\u0105d\"\n        },\n        \"flow_title\": \"{serial} ({host})\",\n        \"step\": {\n            \"user\": {\n                \"data\": {\n                    \"host\": \"Nazwa hosta lub adres IP\",\n                    \"password\": \"Has\\u0142o\",\n                    \"username\": \"Nazwa u\\u017cytkownika\"\n                }\n            }\n        }\n    }\n}"
  },
  {
    "path": "custom_components/enphase_envoy_custom/translations/ru.json",
    "content": "{\n    \"config\": {\n        \"abort\": {\n            \"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.\",\n            \"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.\"\n        },\n        \"error\": {\n            \"cannot_connect\": \"\\u041d\\u0435 \\u0443\\u0434\\u0430\\u043b\\u043e\\u0441\\u044c \\u043f\\u043e\\u0434\\u043a\\u043b\\u044e\\u0447\\u0438\\u0442\\u044c\\u0441\\u044f.\",\n            \"invalid_auth\": \"\\u041e\\u0448\\u0438\\u0431\\u043a\\u0430 \\u0430\\u0443\\u0442\\u0435\\u043d\\u0442\\u0438\\u0444\\u0438\\u043a\\u0430\\u0446\\u0438\\u0438.\",\n            \"unknown\": \"\\u041d\\u0435\\u043f\\u0440\\u0435\\u0434\\u0432\\u0438\\u0434\\u0435\\u043d\\u043d\\u0430\\u044f \\u043e\\u0448\\u0438\\u0431\\u043a\\u0430.\"\n        },\n        \"flow_title\": \"{serial} ({host})\",\n        \"step\": {\n            \"user\": {\n                \"data\": {\n                    \"host\": \"\\u0425\\u043e\\u0441\\u0442\",\n                    \"password\": \"\\u041f\\u0430\\u0440\\u043e\\u043b\\u044c\",\n                    \"username\": \"\\u0418\\u043c\\u044f \\u043f\\u043e\\u043b\\u044c\\u0437\\u043e\\u0432\\u0430\\u0442\\u0435\\u043b\\u044f\"\n                }\n            }\n        }\n    }\n}"
  },
  {
    "path": "custom_components/enphase_envoy_custom/translations/sv.json",
    "content": "{\n    \"config\": {\n        \"abort\": {\n            \"already_configured\": \"Enheten \\u00e4r redan konfigurerad\"\n        },\n        \"error\": {\n            \"cannot_connect\": \"Kunde inte ansluta\",\n            \"unknown\": \"Ov\\u00e4ntat fel\"\n        },\n        \"step\": {\n            \"user\": {\n                \"data\": {\n                    \"password\": \"L\\u00f6senord\",\n                    \"username\": \"Anv\\u00e4ndarnamn\"\n                }\n            }\n        }\n    }\n}"
  },
  {
    "path": "custom_components/enphase_envoy_custom/translations/tr.json",
    "content": "{\n    \"config\": {\n        \"abort\": {\n            \"already_configured\": \"Cihaz zaten yap\\u0131land\\u0131r\\u0131lm\\u0131\\u015f\",\n            \"reauth_successful\": \"Yeniden kimlik do\\u011frulama ba\\u015far\\u0131l\\u0131 oldu\"\n        },\n        \"error\": {\n            \"cannot_connect\": \"Ba\\u011flanma hatas\\u0131\",\n            \"invalid_auth\": \"Ge\\u00e7ersiz kimlik do\\u011frulama\",\n            \"unknown\": \"Beklenmeyen hata\"\n        },\n        \"flow_title\": \"{serial} ( {host} )\",\n        \"step\": {\n            \"user\": {\n                \"data\": {\n                    \"host\": \"Sunucu\",\n                    \"password\": \"Parola\",\n                    \"username\": \"Kullan\\u0131c\\u0131 Ad\\u0131\"\n                }\n            }\n        }\n    }\n}"
  },
  {
    "path": "custom_components/enphase_envoy_custom/translations/zh-Hans.json",
    "content": "{\n    \"config\": {\n        \"step\": {\n            \"user\": {\n                \"data\": {\n                    \"password\": \"\\u5bc6\\u7801\"\n                }\n            }\n        }\n    }\n}"
  },
  {
    "path": "custom_components/enphase_envoy_custom/translations/zh-Hant.json",
    "content": "{\n    \"config\": {\n        \"abort\": {\n            \"already_configured\": \"\\u88dd\\u7f6e\\u5df2\\u7d93\\u8a2d\\u5b9a\\u5b8c\\u6210\",\n            \"reauth_successful\": \"\\u91cd\\u65b0\\u8a8d\\u8b49\\u6210\\u529f\"\n        },\n        \"error\": {\n            \"cannot_connect\": \"\\u9023\\u7dda\\u5931\\u6557\",\n            \"invalid_auth\": \"\\u9a57\\u8b49\\u78bc\\u7121\\u6548\",\n            \"unknown\": \"\\u672a\\u9810\\u671f\\u932f\\u8aa4\"\n        },\n        \"flow_title\": \"{serial} ({host})\",\n        \"step\": {\n            \"user\": {\n                \"data\": {\n                    \"host\": \"\\u4e3b\\u6a5f\\u7aef\",\n                    \"password\": \"\\u5bc6\\u78bc\",\n                    \"username\": \"\\u4f7f\\u7528\\u8005\\u540d\\u7a31\"\n                }\n            }\n        }\n    }\n}"
  },
  {
    "path": "hacs.json",
    "content": "{\n  \"name\": \"Enphase Envoy (DEV)\",\n  \"render_readme\": true,\n  \"content_in_root\": false\n}\n"
  }
]