mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 03:07:37 +00:00
Add scheduled envoy firmware checks to enphase_envoy coordinator (#136102)
* Add scheduled envoy firmware checks to enphase_envoy coordinator * Set firmware scantime to 4 hours and split test in 2
This commit is contained in:
parent
11d44e608b
commit
ba2c8646e9
@ -79,6 +79,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: EnphaseConfigEntry) ->
|
||||
"""Unload a config entry."""
|
||||
coordinator = entry.runtime_data
|
||||
coordinator.async_cancel_token_refresh()
|
||||
coordinator.async_cancel_firmware_refresh()
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
|
||||
|
@ -25,6 +25,7 @@ SCAN_INTERVAL = timedelta(seconds=60)
|
||||
TOKEN_REFRESH_CHECK_INTERVAL = timedelta(days=1)
|
||||
STALE_TOKEN_THRESHOLD = timedelta(days=30).total_seconds()
|
||||
NOTIFICATION_ID = "enphase_envoy_notification"
|
||||
FIRMWARE_REFRESH_INTERVAL = timedelta(hours=4)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -50,6 +51,7 @@ class EnphaseUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
self._setup_complete = False
|
||||
self.envoy_firmware = ""
|
||||
self._cancel_token_refresh: CALLBACK_TYPE | None = None
|
||||
self._cancel_firmware_refresh: CALLBACK_TYPE | None = None
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
@ -87,10 +89,48 @@ class EnphaseUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
return
|
||||
self._async_update_saved_token()
|
||||
|
||||
@callback
|
||||
def _async_refresh_firmware(self, now: datetime.datetime) -> None:
|
||||
"""Proactively check for firmware changes in Envoy."""
|
||||
self.hass.async_create_background_task(
|
||||
self._async_try_refresh_firmware(), "{name} firmware refresh"
|
||||
)
|
||||
|
||||
async def _async_try_refresh_firmware(self) -> None:
|
||||
"""Check firmware in Envoy and reload config entry if changed."""
|
||||
# envoy.setup just reads firmware, serial and partnumber from /info
|
||||
try:
|
||||
await self.envoy.setup()
|
||||
except EnvoyError as err:
|
||||
# just try again next time
|
||||
_LOGGER.debug("%s: Error reading firmware: %s", err, self.name)
|
||||
return
|
||||
if (current_firmware := self.envoy_firmware) and current_firmware != (
|
||||
new_firmware := self.envoy.firmware
|
||||
):
|
||||
self.envoy_firmware = new_firmware
|
||||
_LOGGER.warning(
|
||||
"Envoy firmware changed from: %s to: %s, reloading config entry %s",
|
||||
current_firmware,
|
||||
new_firmware,
|
||||
self.name,
|
||||
)
|
||||
# reload the integration to get all established again
|
||||
self.hass.async_create_task(
|
||||
self.hass.config_entries.async_reload(self.config_entry.entry_id)
|
||||
)
|
||||
|
||||
@callback
|
||||
def _async_mark_setup_complete(self) -> None:
|
||||
"""Mark setup as complete and setup token refresh if needed."""
|
||||
"""Mark setup as complete and setup firmware checks and token refresh if needed."""
|
||||
self._setup_complete = True
|
||||
self.async_cancel_firmware_refresh()
|
||||
self._cancel_firmware_refresh = async_track_time_interval(
|
||||
self.hass,
|
||||
self._async_refresh_firmware,
|
||||
FIRMWARE_REFRESH_INTERVAL,
|
||||
cancel_on_shutdown=True,
|
||||
)
|
||||
self.async_cancel_token_refresh()
|
||||
if not isinstance(self.envoy.auth, EnvoyTokenAuth):
|
||||
return
|
||||
@ -204,3 +244,10 @@ class EnphaseUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
if self._cancel_token_refresh:
|
||||
self._cancel_token_refresh()
|
||||
self._cancel_token_refresh = None
|
||||
|
||||
@callback
|
||||
def async_cancel_firmware_refresh(self) -> None:
|
||||
"""Cancel firmware refresh."""
|
||||
if self._cancel_firmware_refresh:
|
||||
self._cancel_firmware_refresh()
|
||||
self._cancel_firmware_refresh = None
|
||||
|
@ -1,6 +1,8 @@
|
||||
"""Test Enphase Envoy runtime."""
|
||||
|
||||
from unittest.mock import AsyncMock, patch
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
from jwt import encode
|
||||
@ -15,7 +17,10 @@ from homeassistant.components.enphase_envoy.const import (
|
||||
OPTION_DISABLE_KEEP_ALIVE,
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.components.enphase_envoy.coordinator import SCAN_INTERVAL
|
||||
from homeassistant.components.enphase_envoy.coordinator import (
|
||||
FIRMWARE_REFRESH_INTERVAL,
|
||||
SCAN_INTERVAL,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import (
|
||||
CONF_HOST,
|
||||
@ -377,3 +382,82 @@ async def test_option_change_reload(
|
||||
OPTION_DIAGNOSTICS_INCLUDE_FIXTURES: True,
|
||||
OPTION_DISABLE_KEEP_ALIVE: False,
|
||||
}
|
||||
|
||||
|
||||
def mock_envoy_setup(mock_envoy: AsyncMock):
|
||||
"""Mock envoy.setup."""
|
||||
mock_envoy.firmware = "9.9.9999"
|
||||
|
||||
|
||||
@patch(
|
||||
"homeassistant.components.enphase_envoy.coordinator.SCAN_INTERVAL",
|
||||
timedelta(days=1),
|
||||
)
|
||||
@respx.mock
|
||||
async def test_coordinator_firmware_refresh(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
mock_envoy: AsyncMock,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test coordinator scheduled firmware check."""
|
||||
await setup_integration(hass, config_entry)
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
# Move time to next firmware check moment
|
||||
# SCAN_INTERVAL is patched to 1 day to disable it's firmware detection
|
||||
mock_envoy.setup.reset_mock()
|
||||
freezer.tick(FIRMWARE_REFRESH_INTERVAL)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
mock_envoy.setup.assert_called_once_with()
|
||||
mock_envoy.setup.reset_mock()
|
||||
|
||||
envoy = config_entry.runtime_data.envoy
|
||||
assert envoy.firmware == "7.6.175"
|
||||
|
||||
caplog.set_level(logging.WARNING)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.enphase_envoy.Envoy.setup",
|
||||
MagicMock(return_value=mock_envoy_setup(mock_envoy)),
|
||||
):
|
||||
freezer.tick(FIRMWARE_REFRESH_INTERVAL)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
assert (
|
||||
"Envoy firmware changed from: 7.6.175 to: 9.9.9999, reloading config entry Envoy 1234"
|
||||
in caplog.text
|
||||
)
|
||||
envoy = config_entry.runtime_data.envoy
|
||||
assert envoy.firmware == "9.9.9999"
|
||||
|
||||
|
||||
@respx.mock
|
||||
async def test_coordinator_firmware_refresh_with_envoy_error(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
mock_envoy: AsyncMock,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test coordinator scheduled firmware check."""
|
||||
await setup_integration(hass, config_entry)
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
caplog.set_level(logging.DEBUG)
|
||||
logging.getLogger("homeassistant.components.enphase_envoy.coordinator").setLevel(
|
||||
logging.DEBUG
|
||||
)
|
||||
|
||||
mock_envoy.setup.side_effect = EnvoyError
|
||||
freezer.tick(FIRMWARE_REFRESH_INTERVAL)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
assert "Error reading firmware:" in caplog.text
|
||||
|
Loading…
x
Reference in New Issue
Block a user