async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Enphase Envoy from a config entry.""" config = entry.data name = config[CONF_NAME] envoy_reader = EnvoyReader( config[CONF_HOST], config[CONF_USERNAME], config[CONF_PASSWORD], inverters=True, async_client=get_async_client(hass), ) async def async_update_data(): """Fetch data from API endpoint.""" data = {} async with async_timeout.timeout(30): try: await envoy_reader.getData() except httpx.HTTPStatusError as err: raise ConfigEntryAuthFailed from err except httpx.HTTPError as err: raise UpdateFailed( f"Error communicating with API: {err}") from err for condition in SENSORS: if condition != "inverters": data[condition] = await getattr(envoy_reader, condition)() else: data[ "inverters_production"] = await envoy_reader.inverters_production( ) _LOGGER.debug("Retrieved data from API: %s", data) return data coordinator = DataUpdateCoordinator( hass, _LOGGER, name=f"envoy {name}", update_method=async_update_data, update_interval=SCAN_INTERVAL, ) try: await coordinator.async_config_entry_first_refresh() except ConfigEntryAuthFailed: envoy_reader.get_inverters = False await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {})[entry.entry_id] = { COORDINATOR: coordinator, NAME: name, } hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True
async def test_with_4_2_27_firmware(): """Verify with 4.2.27 firmware.""" version = "4.2.27" respx.get("/info.xml").mock(return_value=Response(200, text="")) respx.get("/production.json").mock( return_value=Response(200, json=_load_json_fixture(version, "production.json")) ) respx.get("/api/v1/production").mock( return_value=Response( 200, json=_load_json_fixture(version, "api_v1_production") ) ) reader = EnvoyReader("127.0.0.1", inverters=False) await reader.getData() assert await reader.consumption() == 5811 assert await reader.production() == 5891 assert await reader.daily_consumption() == 0 assert await reader.daily_production() == 17920 assert await reader.seven_days_consumption() == 0 assert await reader.seven_days_production() == 276614 assert await reader.lifetime_consumption() == 0 assert await reader.lifetime_production() == 10279087 assert await reader.inverters_production() is None
async def async_update(self): """Get the energy production data from the Enphase Envoy.""" if self._type != "inverters": _state = await getattr(EnvoyReader(self._ip_address), self._type)() if isinstance(_state, int): self._state = _state else: _LOGGER.error(_state) self._state = None elif self._type == "inverters": inverters = await (EnvoyReader( self._ip_address).inverters_production()) if isinstance(inverters, dict): serial_number = self._name.split(" ")[2] self._state = inverters[serial_number] else: self._state = None
async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, Any]: """Validate the user input allows us to connect.""" envoy_reader = EnvoyReader( data[CONF_HOST], data[CONF_USERNAME], data[CONF_PASSWORD], inverters=False, async_client=get_async_client(hass), ) try: await envoy_reader.getData() except httpx.HTTPStatusError as err: raise InvalidAuth from err except (RuntimeError, httpx.HTTPError) as err: raise CannotConnect from err
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Enphase Envoy sensor.""" ip_address = config[CONF_IP_ADDRESS] monitored_conditions = config[CONF_MONITORED_CONDITIONS] name = config[CONF_NAME] username = config[CONF_USERNAME] password = config[CONF_PASSWORD] envoy_reader = EnvoyReader(ip_address, username, password) entities = [] # Iterate through the list of sensors for condition in monitored_conditions: if condition == "inverters": try: inverters = await envoy_reader.inverters_production() except requests.exceptions.HTTPError: _LOGGER.warning( "Authentication for Inverter data failed during setup: %s", ip_address, ) continue if isinstance(inverters, dict): for inverter in inverters: entities.append( Envoy( envoy_reader, condition, f"{name}{SENSORS[condition][0]} {inverter}", SENSORS[condition][1], ) ) else: entities.append( Envoy( envoy_reader, condition, f"{name}{SENSORS[condition][0]}", SENSORS[condition][1], ) ) async_add_entities(entities)
async def test_with_5_0_49_firmware(): """Verify with 5.0.49 firmware.""" version = "5.0.49" respx.get("/info.xml").mock(return_value=Response(200, text="")) respx.get("/production.json").mock( return_value=Response(200, json=_load_json_fixture(version, "production.json")) ) respx.get("/api/v1/production").mock( return_value=Response( 200, json=_load_json_fixture(version, "api_v1_production") ) ) respx.get("/api/v1/production/inverters").mock( return_value=Response( 200, json=_load_json_fixture(version, "api_v1_production_inverters") ) ) reader = EnvoyReader("127.0.0.1", inverters=True) await reader.getData() assert ( await reader.consumption() == "Consumption data not available for your Envoy device." ) assert await reader.production() == 4859 assert ( await reader.daily_consumption() == "Consumption data not available for your Envoy device." ) assert await reader.daily_production() == 5046 assert ( await reader.seven_days_consumption() == "Consumption data not available for your Envoy device." ) assert await reader.seven_days_production() == 445686 assert ( await reader.lifetime_consumption() == "Consumption data not available for your Envoy device." ) assert await reader.lifetime_production() == 88742152 assert isinstance(await reader.inverters_production(), dict)
async def test_with_3_9_36_firmware(): """Verify with 3.9.36 firmware.""" version = "3.9.36" respx.get("/info.xml").mock(return_value=Response(200, text="")) respx.get("/production.json").mock(return_value=Response(404)) respx.get("/api/v1/production").mock( return_value=Response( 200, json=_load_json_fixture(version, "api_v1_production") ) ) respx.get("/api/v1/production/inverters").mock( return_value=Response( 200, json=_load_json_fixture(version, "api_v1_production_inverters") ) ) reader = EnvoyReader("127.0.0.1", inverters=True) await reader.getData() assert ( await reader.consumption() == "Consumption data not available for your Envoy device." ) assert await reader.production() == 1271 assert ( await reader.daily_consumption() == "Consumption data not available for your Envoy device." ) assert await reader.daily_production() == 1460 assert ( await reader.seven_days_consumption() == "Consumption data not available for your Envoy device." ) assert await reader.seven_days_production() == 130349 assert ( await reader.lifetime_consumption() == "Consumption data not available for your Envoy device." ) assert await reader.lifetime_production() == 6012540 assert isinstance(await reader.inverters_production(), dict)
async def async_setup_platform(homeassistant, config, async_add_entities, discovery_info=None): """Set up the Enphase Envoy sensor.""" ip_address = config[CONF_IP_ADDRESS] monitored_conditions = config[CONF_MONITORED_CONDITIONS] name = config[CONF_NAME] username = config[CONF_USERNAME] password = config[CONF_PASSWORD] if "inverters" in monitored_conditions: envoy_reader = EnvoyReader(ip_address, username, password, inverters=True) else: envoy_reader = EnvoyReader(ip_address, username, password) try: await envoy_reader.getData() except httpx.HTTPStatusError as err: _LOGGER.error("Authentication failure during setup: %s", err) return except httpx.HTTPError as err: raise PlatformNotReady from err async def async_update_data(): """Fetch data from API endpoint.""" data = {} async with async_timeout.timeout(30): try: await envoy_reader.getData() except httpx.HTTPError as err: raise UpdateFailed( f"Error communicating with API: {err}") from err for condition in monitored_conditions: if condition != "inverters": data[condition] = await getattr(envoy_reader, condition)() else: data["inverters_production"] = await getattr( envoy_reader, "inverters_production")() _LOGGER.debug("Retrieved data from API: %s", data) return data coordinator = DataUpdateCoordinator( homeassistant, _LOGGER, name="sensor", update_method=async_update_data, update_interval=SCAN_INTERVAL, ) await coordinator.async_refresh() if coordinator.data is None: raise PlatformNotReady entities = [] for condition in monitored_conditions: entity_name = "" if (condition == "inverters" and coordinator.data.get("inverters_production") is not None): for inverter in coordinator.data["inverters_production"]: entity_name = f"{name}{SENSORS[condition][0]} {inverter}" split_name = entity_name.split(" ") serial_number = split_name[-1] entities.append( Envoy( condition, entity_name, serial_number, SENSORS[condition][1], coordinator, )) elif condition != "inverters": entity_name = f"{name}{SENSORS[condition][0]}" entities.append( Envoy( condition, entity_name, None, SENSORS[condition][1], coordinator, )) async_add_entities(entities)
def main(): metric_prefix = "envoy" port = 9433 parser = argparse.ArgumentParser(description="envoy-logger") parser.add_argument('-v', "--verbose", action="count", help="Increase verbosity of outut", default=0) args = parser.parse_args() if args.verbose: print(args) # As long as we have httpx 0.19 installed and use the explicit username/password, things seems to work reader = EnvoyReader("envoy", "envoy", "058800", inverters=True) # Defaults to using envoy as the username and the last six of the # serial number as the password #reader=EnvoyReader("envoy", inverters=True) production = Gauge(f"{metric_prefix}_production", "Current system power production (W)") daily_production = Gauge(f"{metric_prefix}_daily_production", "Current day's energy production (Wh)") seven_days_production = Gauge(f"{metric_prefix}_seven_day_production", "Current seven day energy production (Wh)") lifetime_production = Gauge(f"{metric_prefix}_lifetime_production", "Lifetime energy production (Wh)") inverter = Gauge(f"{metric_prefix}_inverter", f"Inverter current power production (W)", ['serial_number']) start_http_server(port) while True: loop = asyncio.get_event_loop() data_results = loop.run_until_complete( asyncio.gather(reader.getData(), return_exceptions=True)) if args.verbose > 1: print(f"data_results:{data_results}") loop = asyncio.get_event_loop() results = loop.run_until_complete( asyncio.gather(reader.production(), reader.daily_production(), reader.seven_days_production(), reader.lifetime_production(), reader.inverters_production(), return_exceptions=True)) if args.verbose > 1: print(f"results:{results}") # results: # [13764, # 28698, # 642343, # 4162674, # {'202028033658': [120, '2021-03-11 11:30:02'], # '202029005102': [116, '2021-03-11 11:30:10'], # '202028035455': [120, '2021-03-11 11:30:18'], # '202029001566': [117, '2021-03-11 11:30:24'], # ... try: production.set(results[0]) daily_production.set(results[1]) seven_days_production.set(results[2]) lifetime_production.set(results[3]) if "401" in str(data_results): print( "inverters_production: Unable to retrieve inverter data - Authentication failure" ) elif results[4] is None: print( "inverters_production: Inverter data not available from Envoy." ) else: for sn, status in results[4].items(): inverter.labels([sn]).set(status[0]) except: print("Error parsing results") print(results) time.sleep(60)
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Enphase Envoy from a config entry.""" config = entry.data name = config[CONF_NAME] envoy_reader = EnvoyReader( config[CONF_HOST], config[CONF_USERNAME], config[CONF_PASSWORD], inverters=True, async_client=get_async_client(hass), ) async def async_update_data(): """Fetch data from API endpoint.""" async with async_timeout.timeout(30): try: await envoy_reader.getData() except httpx.HTTPStatusError as err: raise ConfigEntryAuthFailed from err except httpx.HTTPError as err: raise UpdateFailed(f"Error communicating with API: {err}") from err data = { description.key: await getattr(envoy_reader, description.key)() for description in SENSORS } data["inverters_production"] = await envoy_reader.inverters_production() _LOGGER.debug("Retrieved data from API: %s", data) return data coordinator = DataUpdateCoordinator( hass, _LOGGER, name=f"envoy {name}", update_method=async_update_data, update_interval=SCAN_INTERVAL, ) try: await coordinator.async_config_entry_first_refresh() except ConfigEntryAuthFailed: envoy_reader.get_inverters = False await coordinator.async_config_entry_first_refresh() if not entry.unique_id: try: serial = await envoy_reader.get_full_serial_number() except httpx.HTTPError as ex: raise ConfigEntryNotReady( f"Could not obtain serial number from envoy: {ex}" ) from ex else: hass.config_entries.async_update_entry(entry, unique_id=serial) hass.data.setdefault(DOMAIN, {})[entry.entry_id] = { COORDINATOR: coordinator, NAME: name, } await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True