Add optional test fixture collection to enphase_envoy diagnostic report (#116242)

* diagnostics_fixtures

* fix codespell errors

* fix merge order and typo

* remove pointless-string-statement
This commit is contained in:
Arie Catsman 2024-06-24 10:37:32 +02:00 committed by GitHub
parent deee10813c
commit 158c8b8400
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 8282 additions and 6 deletions

View File

@ -12,12 +12,22 @@ from pyenphase import AUTH_TOKEN_MIN_VERSION, Envoy, EnvoyError
import voluptuous as vol
from homeassistant.components import zeroconf
from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult
from homeassistant.config_entries import (
ConfigEntry,
ConfigFlow,
ConfigFlowResult,
OptionsFlowWithConfigEntry,
)
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.httpx_client import get_async_client
from .const import DOMAIN, INVALID_AUTH_ERRORS
from .const import (
DOMAIN,
INVALID_AUTH_ERRORS,
OPTION_DIAGNOSTICS_INCLUDE_FIXTURES,
OPTION_DIAGNOSTICS_INCLUDE_FIXTURES_DEFAULT_VALUE,
)
_LOGGER = logging.getLogger(__name__)
@ -50,6 +60,12 @@ class EnphaseConfigFlow(ConfigFlow, domain=DOMAIN):
self.protovers: str | None = None
self._reauth_entry: ConfigEntry | None = None
@staticmethod
@callback
def async_get_options_flow(config_entry: ConfigEntry) -> EnvoyOptionsFlowHandler:
"""Options flow handler for Enphase_Envoy."""
return EnvoyOptionsFlowHandler(config_entry)
@callback
def _async_generate_schema(self) -> vol.Schema:
"""Generate schema."""
@ -282,3 +298,33 @@ class EnphaseConfigFlow(ConfigFlow, domain=DOMAIN):
description_placeholders=description_placeholders,
errors=errors,
)
class EnvoyOptionsFlowHandler(OptionsFlowWithConfigEntry):
"""Envoy config flow options handler."""
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Manage the options."""
if user_input is not None:
return self.async_create_entry(title="", data=user_input)
return self.async_show_form(
step_id="init",
data_schema=vol.Schema(
{
vol.Required(
OPTION_DIAGNOSTICS_INCLUDE_FIXTURES,
default=self.config_entry.options.get(
OPTION_DIAGNOSTICS_INCLUDE_FIXTURES,
OPTION_DIAGNOSTICS_INCLUDE_FIXTURES_DEFAULT_VALUE,
),
): bool,
}
),
description_placeholders={
CONF_SERIAL: self.config_entry.unique_id,
CONF_HOST: self.config_entry.data.get("host"),
},
)

View File

@ -15,3 +15,6 @@ PLATFORMS = [
]
INVALID_AUTH_ERRORS = (EnvoyAuthenticationError, EnvoyAuthenticationRequired)
OPTION_DIAGNOSTICS_INCLUDE_FIXTURES = "diagnostics_include_fixtures"
OPTION_DIAGNOSTICS_INCLUDE_FIXTURES_DEFAULT_VALUE = False

View File

@ -6,6 +6,8 @@ import copy
from typing import TYPE_CHECKING, Any
from attr import asdict
from pyenphase.envoy import Envoy
from pyenphase.exceptions import EnvoyError
from homeassistant.components.diagnostics import async_redact_data
from homeassistant.config_entries import ConfigEntry
@ -21,7 +23,7 @@ from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.json import json_dumps
from homeassistant.util.json import json_loads
from .const import DOMAIN
from .const import DOMAIN, OPTION_DIAGNOSTICS_INCLUDE_FIXTURES
from .coordinator import EnphaseUpdateCoordinator
CONF_TITLE = "title"
@ -38,6 +40,46 @@ TO_REDACT = {
}
async def _get_fixture_collection(envoy: Envoy, serial: str) -> dict[str, Any]:
"""Collect Envoy endpoints to use for test fixture set."""
fixture_data: dict[str, Any] = {}
end_points = [
"/info",
"/api/v1/production",
"/api/v1/production/inverters",
"/production.json",
"/production.json?details=1",
"/production",
"/ivp/ensemble/power",
"/ivp/ensemble/inventory",
"/ivp/ensemble/dry_contacts",
"/ivp/ensemble/status",
"/ivp/ensemble/secctrl",
"/ivp/ss/dry_contact_settings",
"/admin/lib/tariff",
"/ivp/ss/gen_config",
"/ivp/ss/gen_schedule",
"/ivp/sc/pvlimit",
"/ivp/ss/pel_settings",
"/ivp/ensemble/generator",
"/ivp/meters",
"/ivp/meters/readings",
]
for end_point in end_points:
response = await envoy.request(end_point)
fixture_data[end_point] = response.text.replace("\n", "").replace(
serial, CLEAN_TEXT
)
fixture_data[f"{end_point}_log"] = json_dumps(
{
"headers": dict(response.headers.items()),
"code": response.status_code,
}
)
return fixture_data
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, entry: ConfigEntry
) -> dict[str, Any]:
@ -113,12 +155,20 @@ async def async_get_config_entry_diagnostics(
"ct_storage_meter": envoy.storage_meter_type,
}
fixture_data: dict[str, Any] = {}
if entry.options.get(OPTION_DIAGNOSTICS_INCLUDE_FIXTURES, False):
try:
fixture_data = await _get_fixture_collection(envoy=envoy, serial=old_serial)
except EnvoyError as err:
fixture_data["Error"] = repr(err)
diagnostic_data: dict[str, Any] = {
"config_entry": async_redact_data(entry.as_dict(), TO_REDACT),
"envoy_properties": envoy_properties,
"raw_data": json_loads(coordinator_data_cleaned),
"envoy_model_data": envoy_model,
"envoy_entities_by_device": json_loads(device_entities_cleaned),
"fixtures": fixture_data,
}
return diagnostic_data

View File

@ -36,6 +36,16 @@
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
}
},
"options": {
"step": {
"init": {
"title": "Envoy {serial} {host} options",
"data": {
"diagnostics_include_fixtures": "Include test fixture data in diagnostic report. Use when requested to provide test data for troubleshooting or development activies. With this option enabled the diagnostic report may take more time to download. When report is created best disable this option again."
}
}
}
},
"entity": {
"binary_sensor": {
"communicating": {

View File

@ -339,11 +339,22 @@ def mock_envoy_fixture(
raw={"varies_by": "firmware_version"},
)
mock_envoy.update = AsyncMock(return_value=mock_envoy.data)
response = Mock()
response.status_code = 200
response.text = "Testing request \nreplies."
response.headers = {"Hello": "World"}
mock_envoy.request = AsyncMock(return_value=response)
return mock_envoy
@pytest.fixture(name="setup_enphase_envoy")
async def setup_enphase_envoy_fixture(hass: HomeAssistant, config, mock_envoy):
async def setup_enphase_envoy_fixture(
hass: HomeAssistant,
config,
mock_envoy,
):
"""Define a fixture to set up Enphase Envoy."""
with (
patch(

File diff suppressed because it is too large Load Diff

View File

@ -10,7 +10,12 @@ from syrupy.assertion import SnapshotAssertion
from homeassistant import config_entries
from homeassistant.components import zeroconf
from homeassistant.components.enphase_envoy.const import DOMAIN, PLATFORMS
from homeassistant.components.enphase_envoy.const import (
DOMAIN,
OPTION_DIAGNOSTICS_INCLUDE_FIXTURES,
OPTION_DIAGNOSTICS_INCLUDE_FIXTURES_DEFAULT_VALUE,
PLATFORMS,
)
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
@ -656,6 +661,41 @@ async def test_reauth(hass: HomeAssistant, config_entry, setup_enphase_envoy) ->
assert result2["reason"] == "reauth_successful"
async def test_options_default(
hass: HomeAssistant, config_entry, setup_enphase_envoy
) -> None:
"""Test we can configure options."""
result = await hass.config_entries.options.async_init(config_entry.entry_id)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "init"
result = await hass.config_entries.options.async_configure(
result["flow_id"],
user_input={
OPTION_DIAGNOSTICS_INCLUDE_FIXTURES: OPTION_DIAGNOSTICS_INCLUDE_FIXTURES_DEFAULT_VALUE
},
)
assert result["type"] is FlowResultType.CREATE_ENTRY
assert config_entry.options == {
OPTION_DIAGNOSTICS_INCLUDE_FIXTURES: OPTION_DIAGNOSTICS_INCLUDE_FIXTURES_DEFAULT_VALUE
}
async def test_options_set(
hass: HomeAssistant, config_entry, setup_enphase_envoy
) -> None:
"""Test we can configure options."""
result = await hass.config_entries.options.async_init(config_entry.entry_id)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "init"
result = await hass.config_entries.options.async_configure(
result["flow_id"], user_input={OPTION_DIAGNOSTICS_INCLUDE_FIXTURES: True}
)
assert result["type"] is FlowResultType.CREATE_ENTRY
assert config_entry.options == {OPTION_DIAGNOSTICS_INCLUDE_FIXTURES: True}
async def test_reconfigure(
hass: HomeAssistant, config_entry, setup_enphase_envoy
) -> None:

View File

@ -1,10 +1,20 @@
"""Test Enphase Envoy diagnostics."""
from syrupy import SnapshotAssertion
from unittest.mock import AsyncMock, patch
from pyenphase.exceptions import EnvoyError
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.enphase_envoy.const import (
DOMAIN,
OPTION_DIAGNOSTICS_INCLUDE_FIXTURES,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component
from tests.common import MockConfigEntry
from tests.components.diagnostics import get_diagnostics_for_config_entry
from tests.typing import ClientSessionGenerator
@ -35,3 +45,76 @@ async def test_entry_diagnostics(
assert await get_diagnostics_for_config_entry(
hass, hass_client, config_entry
) == snapshot(exclude=limit_diagnostic_attrs)
@pytest.fixture(name="config_entry_options")
def config_entry_options_fixture(hass: HomeAssistant, config, serial_number):
"""Define a config entry fixture."""
entry = MockConfigEntry(
domain=DOMAIN,
entry_id="45a36e55aaddb2007c5f6602e0c38e72",
title=f"Envoy {serial_number}" if serial_number else "Envoy",
unique_id=serial_number,
data=config,
options={OPTION_DIAGNOSTICS_INCLUDE_FIXTURES: True},
)
entry.add_to_hass(hass)
return entry
async def test_entry_diagnostics_with_fixtures(
hass: HomeAssistant,
hass_client: ClientSessionGenerator,
config_entry_options: ConfigEntry,
setup_enphase_envoy,
snapshot: SnapshotAssertion,
) -> None:
"""Test config entry diagnostics."""
assert await get_diagnostics_for_config_entry(
hass, hass_client, config_entry_options
) == snapshot(exclude=limit_diagnostic_attrs)
@pytest.fixture(name="setup_enphase_envoy_options_error")
async def setup_enphase_envoy_options_error_fixture(
hass: HomeAssistant,
config,
mock_envoy_options_error,
):
"""Define a fixture to set up Enphase Envoy."""
with (
patch(
"homeassistant.components.enphase_envoy.config_flow.Envoy",
return_value=mock_envoy_options_error,
),
patch(
"homeassistant.components.enphase_envoy.Envoy",
return_value=mock_envoy_options_error,
),
):
assert await async_setup_component(hass, DOMAIN, config)
await hass.async_block_till_done()
yield
@pytest.fixture(name="mock_envoy_options_error")
def mock_envoy_options_fixture(
mock_envoy,
):
"""Mock envoy with error in request."""
mock_envoy_options = mock_envoy
mock_envoy_options.request.side_effect = AsyncMock(side_effect=EnvoyError("Test"))
return mock_envoy_options
async def test_entry_diagnostics_with_fixtures_with_error(
hass: HomeAssistant,
hass_client: ClientSessionGenerator,
config_entry_options: ConfigEntry,
setup_enphase_envoy_options_error,
snapshot: SnapshotAssertion,
) -> None:
"""Test config entry diagnostics."""
assert await get_diagnostics_for_config_entry(
hass, hass_client, config_entry_options
) == snapshot(exclude=limit_diagnostic_attrs)