1
mirror of https://github.com/home-assistant/core synced 2024-09-28 03:04:04 +02:00

Cache envoy auth tokens to ensure integration works if cloud is offline (#97872)

This commit is contained in:
J. Nick Koston 2023-08-05 14:51:19 -10:00 committed by GitHub
parent 6a65a97715
commit 00e78fbf19
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 65 additions and 34 deletions

View File

@ -4,7 +4,7 @@ from __future__ import annotations
from pyenphase import Envoy
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME
from homeassistant.const import CONF_HOST
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.httpx_client import get_async_client
@ -16,15 +16,9 @@ from .coordinator import EnphaseUpdateCoordinator
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]
host = config[CONF_HOST]
username = config[CONF_USERNAME]
password = config[CONF_PASSWORD]
host = entry.data[CONF_HOST]
envoy = Envoy(host, get_async_client(hass, verify_ssl=False))
coordinator = EnphaseUpdateCoordinator(hass, envoy, name, username, password)
coordinator = EnphaseUpdateCoordinator(hass, envoy, entry)
await coordinator.async_config_entry_first_refresh()
if not entry.unique_id:

View File

@ -9,8 +9,6 @@ from awesomeversion import AwesomeVersion
from pyenphase import (
AUTH_TOKEN_MIN_VERSION,
Envoy,
EnvoyAuthenticationError,
EnvoyAuthenticationRequired,
EnvoyError,
)
import voluptuous as vol
@ -23,7 +21,7 @@ from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers.httpx_client import get_async_client
from homeassistant.util.network import is_ipv4_address
from .const import DOMAIN
from .const import DOMAIN, INVALID_AUTH_ERRORS
_LOGGER = logging.getLogger(__name__)
@ -31,8 +29,6 @@ ENVOY = "Envoy"
CONF_SERIAL = "serial"
INVALID_AUTH_ERRORS = (EnvoyAuthenticationError, EnvoyAuthenticationRequired)
INSTALLER_AUTH_USERNAME = "installer"

View File

@ -1,6 +1,15 @@
"""The enphase_envoy component."""
from pyenphase import (
EnvoyAuthenticationError,
EnvoyAuthenticationRequired,
)
from homeassistant.const import Platform
DOMAIN = "enphase_envoy"
PLATFORMS = [Platform.SENSOR]
CONF_TOKEN = "token"
INVALID_AUTH_ERRORS = (EnvoyAuthenticationError, EnvoyAuthenticationRequired)

View File

@ -7,15 +7,18 @@ from typing import Any
from pyenphase import (
Envoy,
EnvoyAuthenticationError,
EnvoyAuthenticationRequired,
EnvoyError,
EnvoyTokenAuth,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_NAME, CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import CONF_TOKEN, INVALID_AUTH_ERRORS
SCAN_INTERVAL = timedelta(seconds=60)
_LOGGER = logging.getLogger(__name__)
@ -25,24 +28,18 @@ class EnphaseUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
envoy_serial_number: str
def __init__(
self,
hass: HomeAssistant,
envoy: Envoy,
name: str,
username: str,
password: str,
) -> None:
def __init__(self, hass: HomeAssistant, envoy: Envoy, entry: ConfigEntry) -> None:
"""Initialize DataUpdateCoordinator for the envoy."""
self.envoy = envoy
self.username = username
self.password = password
self.name = name
entry_data = entry.data
self.entry = entry
self.username = entry_data[CONF_USERNAME]
self.password = entry_data[CONF_PASSWORD]
self._setup_complete = False
super().__init__(
hass,
_LOGGER,
name=name,
name=entry_data[CONF_NAME],
update_interval=SCAN_INTERVAL,
always_update=False,
)
@ -53,7 +50,32 @@ class EnphaseUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
await envoy.setup()
assert envoy.serial_number is not None
self.envoy_serial_number = envoy.serial_number
if token := self.entry.data.get(CONF_TOKEN):
try:
await envoy.authenticate(token=token)
except INVALID_AUTH_ERRORS:
# token likely expired or firmware changed
# so we fall through to authenticate with username/password
pass
else:
self._setup_complete = True
return
await envoy.authenticate(username=self.username, password=self.password)
assert envoy.auth is not None
if isinstance(envoy.auth, EnvoyTokenAuth):
# update token in config entry so we can
# startup without hitting the Cloud API
# as long as the token is valid
self.hass.config_entries.async_update_entry(
self.entry,
data={
**self.entry.data,
CONF_TOKEN: envoy.auth.token,
},
)
self._setup_complete = True
async def _async_update_data(self) -> dict[str, Any]:
@ -64,7 +86,7 @@ class EnphaseUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
if not self._setup_complete:
await self._async_setup_and_authenticate()
return (await envoy.update()).raw
except (EnvoyAuthenticationError, EnvoyAuthenticationRequired) as err:
except INVALID_AUTH_ERRORS as err:
if self._setup_complete and tries == 0:
# token likely expired or firmware changed, try to re-authenticate
self._setup_complete = False

View File

@ -8,7 +8,7 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_NAME, CONF_PASSWORD, CONF_UNIQUE_ID, CONF_USERNAME
from homeassistant.core import HomeAssistant
from .const import DOMAIN
from .const import CONF_TOKEN, DOMAIN
from .coordinator import EnphaseUpdateCoordinator
CONF_TITLE = "title"
@ -20,6 +20,7 @@ TO_REDACT = {
CONF_TITLE,
CONF_UNIQUE_ID,
CONF_USERNAME,
CONF_TOKEN,
}

View File

@ -6,7 +6,7 @@
"documentation": "https://www.home-assistant.io/integrations/enphase_envoy",
"iot_class": "local_polling",
"loggers": ["pyenphase"],
"requirements": ["pyenphase==0.8.0"],
"requirements": ["pyenphase==0.9.0"],
"zeroconf": [
{
"type": "_enphase-envoy._tcp.local."

View File

@ -1662,7 +1662,7 @@ pyedimax==0.2.1
pyefergy==22.1.1
# homeassistant.components.enphase_envoy
pyenphase==0.8.0
pyenphase==0.9.0
# homeassistant.components.envisalink
pyenvisalink==4.6

View File

@ -1229,7 +1229,7 @@ pyeconet==0.1.20
pyefergy==22.1.1
# homeassistant.components.enphase_envoy
pyenphase==0.8.0
pyenphase==0.9.0
# homeassistant.components.everlights
pyeverlights==0.1.0

View File

@ -7,6 +7,7 @@ from pyenphase import (
EnvoyInverter,
EnvoySystemConsumption,
EnvoySystemProduction,
EnvoyTokenAuth,
)
import pytest
@ -43,12 +44,13 @@ def config_fixture():
@pytest.fixture(name="mock_envoy")
def mock_envoy_fixture(serial_number, mock_authenticate, mock_setup):
def mock_envoy_fixture(serial_number, mock_authenticate, mock_setup, mock_auth):
"""Define a mocked Envoy fixture."""
mock_envoy = Mock(spec=Envoy)
mock_envoy.serial_number = serial_number
mock_envoy.authenticate = mock_authenticate
mock_envoy.setup = mock_setup
mock_envoy.auth = mock_auth
mock_envoy.data = EnvoyData(
system_consumption=EnvoySystemConsumption(
watt_hours_last_7_days=1234,
@ -99,6 +101,12 @@ def mock_authenticate():
return AsyncMock()
@pytest.fixture(name="mock_auth")
def mock_auth(serial_number):
"""Define a mocked EnvoyAuth fixture."""
return EnvoyTokenAuth("127.0.0.1", token="abc", envoy_serial=serial_number)
@pytest.fixture(name="mock_setup")
def mock_setup():
"""Define a mocked Envoy.setup fixture."""

View File

@ -24,6 +24,7 @@ async def test_entry_diagnostics(
"name": REDACTED,
"username": REDACTED,
"password": REDACTED,
"token": REDACTED,
},
"options": {},
"pref_disable_new_entities": False,