From 81e0a244f8701086aa8a73e48f59c8c226e2607b Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Sat, 5 Sep 2026 16:19:29 +1000 Subject: [PATCH] Fix Teslemetry energy-site setup crash on encrypted RSA key (#181348) Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../components/teslemetry/__init__.py | 10 +++- .../components/teslemetry/config_flow.py | 6 +- .../components/teslemetry/test_config_flow.py | 6 ++ tests/components/teslemetry/test_init.py | 59 +++++++++++++++++++ 4 files changed, 77 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/teslemetry/__init__.py b/homeassistant/components/teslemetry/__init__.py index 1e045e5bc8a7..01321782525a 100644 --- a/homeassistant/components/teslemetry/__init__.py +++ b/homeassistant/components/teslemetry/__init__.py @@ -333,9 +333,13 @@ async def _async_get_rsa_key_pem(hass: HomeAssistant) -> bytes: pem: bytes | None = hass.data.get(RSA_PARENT_KEY) if pem is None: path = hass.config.path(POWERWALL_KEY_FILE) - await Teslemetry( - session=async_get_clientsession(hass), access_token="" - ).get_rsa_private_key(path) + try: + await Teslemetry( + session=async_get_clientsession(hass), access_token="" + ).get_rsa_private_key(path) + except TypeError as err: + # An encrypted PEM surfaces as TypeError from the cryptography loader. + raise ValueError("RSA private key file is encrypted") from err pem = await hass.async_add_executor_job(Path(path).read_bytes) hass.data[RSA_PARENT_KEY] = pem return pem diff --git a/homeassistant/components/teslemetry/config_flow.py b/homeassistant/components/teslemetry/config_flow.py index 5d7becdf5559..c82e882f8d81 100644 --- a/homeassistant/components/teslemetry/config_flow.py +++ b/homeassistant/components/teslemetry/config_flow.py @@ -273,7 +273,11 @@ class EnergySiteSubentryFlowHandler(ConfigSubentryFlow): session=async_get_clientsession(self.hass), access_token="" ) try: - await keyholder.get_rsa_private_key(path) + try: + await keyholder.get_rsa_private_key(path) + except TypeError as err: + # An encrypted PEM surfaces as TypeError from the cryptography loader. + raise ValueError("RSA private key file is encrypted") from err self._key_pem = await self.hass.async_add_executor_job( Path(path).read_bytes ) diff --git a/tests/components/teslemetry/test_config_flow.py b/tests/components/teslemetry/test_config_flow.py index dbe8a54b1471..8092128f9559 100644 --- a/tests/components/teslemetry/test_config_flow.py +++ b/tests/components/teslemetry/test_config_flow.py @@ -1378,6 +1378,12 @@ async def test_pair_step_second_lookup_errors( ValueError, id="key_read_valueerror", ), + # An encrypted key PEM surfaces as TypeError from the cryptography loader. + pytest.param( + "homeassistant.components.teslemetry.config_flow.Teslemetry.get_rsa_private_key", + TypeError, + id="key_fetch_typeerror", + ), ], ) async def test_rsa_key_load_failure_aborts( diff --git a/tests/components/teslemetry/test_init.py b/tests/components/teslemetry/test_init.py index 8c7482035705..9160faaff4cf 100644 --- a/tests/components/teslemetry/test_init.py +++ b/tests/components/teslemetry/test_init.py @@ -1418,6 +1418,65 @@ async def test_local_control_failure_falls_back_to_cloud( ) +async def test_local_control_encrypted_key_falls_back_to_cloud( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, +) -> None: + """Fall back to cloud control when RSA key loading reports an encrypted PEM.""" + entry = _entry_with_powerwall() + entry.add_to_hass(hass) + + with ( + patch( + "homeassistant.components.teslemetry.Teslemetry.get_rsa_private_key", + side_effect=TypeError( + "Password was not given but private key is encrypted" + ), + ), + patch("homeassistant.components.teslemetry.PLATFORMS", []), + caplog.at_level(logging.WARNING), + ): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state is ConfigEntryState.LOADED + energysite = entry.runtime_data.energysites[0] + assert isinstance(energysite.api, EnergySite) + assert not isinstance(energysite.api, EnergySiteRouter) + assert energysite.can_local_control + assert "falling back to cloud control" in caplog.text + + +async def test_local_control_unexpected_typeerror_is_not_swallowed( + hass: HomeAssistant, +) -> None: + """A TypeError outside the key load is a real bug and must not degrade silently. + + ``_LOCAL_CONTROL_ERRORS`` deliberately excludes TypeError: only the key + loader's encrypted-PEM TypeError is converted to ValueError. A TypeError + from anywhere else in the resolve path (here, client construction) must + fail setup rather than silently falling back to cloud control. + """ + entry = _entry_with_powerwall() + entry.add_to_hass(hass) + + with ( + patch( + "homeassistant.components.teslemetry._async_get_rsa_key_pem", + return_value=_TEST_RSA_KEY_PEM, + ), + patch( + "homeassistant.components.teslemetry.PowerwallClient", + side_effect=TypeError("unexpected argument"), + ), + patch("homeassistant.components.teslemetry.PLATFORMS", []), + ): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state is ConfigEntryState.SETUP_ERROR + + async def test_get_rsa_key_pem_generates_and_caches(hass: HomeAssistant) -> None: """The RSA key is generated/read once, then served from the hass.data cache.""" with (