diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index 8f08aab8d30..e5e90bf19af 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -14,6 +14,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, Platform from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady +from homeassistant.helpers import issue_registry as ir from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import ( @@ -30,6 +31,7 @@ from .const import ( DEFAULT_COAP_PORT, DOMAIN, LOGGER, + PUSH_UPDATE_ISSUE_ID, ) from .coordinator import ( ShellyBlockCoordinator, @@ -323,6 +325,14 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return unload_ok + # delete push update issue if it exists + LOGGER.debug( + "Deleting issue %s", PUSH_UPDATE_ISSUE_ID.format(unique=entry.unique_id) + ) + ir.async_delete_issue( + hass, DOMAIN, PUSH_UPDATE_ISSUE_ID.format(unique=entry.unique_id) + ) + platforms = BLOCK_SLEEPING_PLATFORMS if not entry.data.get(CONF_SLEEP_PERIOD): diff --git a/homeassistant/components/shelly/const.py b/homeassistant/components/shelly/const.py index 7aa86af1e9a..e678f92c480 100644 --- a/homeassistant/components/shelly/const.py +++ b/homeassistant/components/shelly/const.py @@ -174,3 +174,7 @@ class BLEScannerMode(StrEnum): DISABLED = "disabled" ACTIVE = "active" PASSIVE = "passive" + + +MAX_PUSH_UPDATE_FAILURES = 5 +PUSH_UPDATE_ISSUE_ID = "push_update_{unique}" diff --git a/homeassistant/components/shelly/coordinator.py b/homeassistant/components/shelly/coordinator.py index 6d7b3496880..0d4a091b729 100644 --- a/homeassistant/components/shelly/coordinator.py +++ b/homeassistant/components/shelly/coordinator.py @@ -17,6 +17,7 @@ from awesomeversion import AwesomeVersion from homeassistant.config_entries import ConfigEntry, ConfigEntryState from homeassistant.const import ATTR_DEVICE_ID, CONF_HOST, EVENT_HOMEASSISTANT_STOP from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback +from homeassistant.helpers import issue_registry as ir from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.device_registry import ( CONNECTION_NETWORK_MAC, @@ -41,7 +42,9 @@ from .const import ( EVENT_SHELLY_CLICK, INPUTS_EVENTS_DICT, LOGGER, + MAX_PUSH_UPDATE_FAILURES, MODELS_SUPPORTING_LIGHT_EFFECTS, + PUSH_UPDATE_ISSUE_ID, REST_SENSORS_UPDATE_INTERVAL, RPC_INPUTS_EVENTS_TYPES, RPC_RECONNECT_INTERVAL, @@ -162,6 +165,7 @@ class ShellyBlockCoordinator(ShellyCoordinatorBase[BlockDevice]): self._last_effect: int | None = None self._last_input_events_count: dict = {} self._last_target_temp: float | None = None + self._push_update_failures: int = 0 entry.async_on_unload( self.async_add_listener(self._async_device_updates_handler) @@ -270,6 +274,25 @@ class ShellyBlockCoordinator(ShellyCoordinatorBase[BlockDevice]): except InvalidAuthError: self.entry.async_start_reauth(self.hass) else: + self._push_update_failures += 1 + if self._push_update_failures > MAX_PUSH_UPDATE_FAILURES: + LOGGER.debug( + "Creating issue %s", PUSH_UPDATE_ISSUE_ID.format(unique=self.mac) + ) + ir.async_create_issue( + self.hass, + DOMAIN, + PUSH_UPDATE_ISSUE_ID.format(unique=self.mac), + is_fixable=False, + is_persistent=False, + severity=ir.IssueSeverity.ERROR, + learn_more_url="https://www.home-assistant.io/integrations/shelly/#shelly-device-configuration-generation-1", + translation_key="push_update_failure", + translation_placeholders={ + "device_name": self.entry.title, + "ip_address": self.device.ip_address, + }, + ) device_update_info(self.hass, self.device, self.entry) def async_setup(self) -> None: diff --git a/homeassistant/components/shelly/strings.json b/homeassistant/components/shelly/strings.json index eeb2c3d3224..7c3f6033d07 100644 --- a/homeassistant/components/shelly/strings.json +++ b/homeassistant/components/shelly/strings.json @@ -118,5 +118,11 @@ } } } + }, + "issues": { + "push_update_failure": { + "title": "Shelly device {device_name} push update failure", + "description": "Home Assistant is not receiving push updates from the Shelly device {device_name} with IP address {ip_address}. Check the CoIoT configuration in the web panel of the device and your network configuration." + } } } diff --git a/tests/components/shelly/test_coordinator.py b/tests/components/shelly/test_coordinator.py index 9039893999d..8536c3d72e6 100644 --- a/tests/components/shelly/test_coordinator.py +++ b/tests/components/shelly/test_coordinator.py @@ -13,6 +13,7 @@ from homeassistant.components.shelly.const import ( ATTR_GENERATION, DOMAIN, ENTRY_RELOAD_COOLDOWN, + MAX_PUSH_UPDATE_FAILURES, RPC_RECONNECT_INTERVAL, SLEEP_PERIOD_MULTIPLIER, UPDATE_PERIOD_MULTIPLIER, @@ -24,15 +25,18 @@ from homeassistant.helpers.device_registry import ( async_entries_for_config_entry, async_get as async_get_dev_reg, ) +import homeassistant.helpers.issue_registry as ir from homeassistant.util import dt as dt_util from . import ( + MOCK_MAC, init_integration, inject_rpc_device_event, mock_polling_rpc_update, mock_rest_update, register_entity, ) +from .conftest import MOCK_BLOCKS from tests.common import async_fire_time_changed @@ -249,6 +253,31 @@ async def test_block_sleeping_device_no_periodic_updates( assert hass.states.get(entity_id).state == STATE_UNAVAILABLE +async def test_block_device_push_updates_failure( + hass: HomeAssistant, mock_block_device, monkeypatch +) -> None: + """Test block device with push updates failure.""" + issue_registry: ir.IssueRegistry = ir.async_get(hass) + + monkeypatch.setattr( + mock_block_device, + "update", + AsyncMock(return_value=MOCK_BLOCKS), + ) + await init_integration(hass, 1) + + # Move time to force polling + for _ in range(MAX_PUSH_UPDATE_FAILURES + 1): + async_fire_time_changed( + hass, dt_util.utcnow() + timedelta(seconds=UPDATE_PERIOD_MULTIPLIER * 15) + ) + await hass.async_block_till_done() + + assert issue_registry.async_get_issue( + domain=DOMAIN, issue_id=f"push_update_{MOCK_MAC}" + ) + + async def test_block_button_click_event( hass: HomeAssistant, mock_block_device, events, monkeypatch ) -> None: