Add a repair issue for Shelly devices with unsupported firmware (#109076)

Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
Simone Chemelli 2024-01-30 21:59:33 +01:00 committed by GitHub
parent e1576d5998
commit d4c91bd0b7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 101 additions and 6 deletions

View file

@ -9,6 +9,7 @@ from aioshelly.common import ConnectionOptions
from aioshelly.const import RPC_GENERATIONS
from aioshelly.exceptions import (
DeviceConnectionError,
FirmwareUnsupported,
InvalidAuthError,
MacAddressMismatchError,
)
@ -37,6 +38,7 @@ from .const import (
DATA_CONFIG_ENTRY,
DEFAULT_COAP_PORT,
DOMAIN,
FIRMWARE_UNSUPPORTED_ISSUE_ID,
LOGGER,
MODELS_WITH_WRONG_SLEEP_PERIOD,
PUSH_UPDATE_ISSUE_ID,
@ -50,6 +52,7 @@ from .coordinator import (
get_entry_data,
)
from .utils import (
async_create_issue_unsupported_firmware,
get_block_device_sleep_period,
get_coap_context,
get_device_entry_gen,
@ -216,6 +219,9 @@ async def _async_setup_block_entry(hass: HomeAssistant, entry: ConfigEntry) -> b
raise ConfigEntryNotReady(repr(err)) from err
except InvalidAuthError as err:
raise ConfigEntryAuthFailed(repr(err)) from err
except FirmwareUnsupported as err:
async_create_issue_unsupported_firmware(hass, entry)
raise ConfigEntryNotReady from err
await _async_block_device_setup()
elif sleep_period is None or device_entry is None:
@ -230,6 +236,9 @@ async def _async_setup_block_entry(hass: HomeAssistant, entry: ConfigEntry) -> b
LOGGER.debug("Setting up offline block device %s", entry.title)
await _async_block_device_setup()
ir.async_delete_issue(
hass, DOMAIN, FIRMWARE_UNSUPPORTED_ISSUE_ID.format(unique=entry.unique_id)
)
return True
@ -296,6 +305,9 @@ async def _async_setup_rpc_entry(hass: HomeAssistant, entry: ConfigEntry) -> boo
LOGGER.debug("Setting up online RPC device %s", entry.title)
try:
await device.initialize()
except FirmwareUnsupported as err:
async_create_issue_unsupported_firmware(hass, entry)
raise ConfigEntryNotReady from err
except (DeviceConnectionError, MacAddressMismatchError) as err:
raise ConfigEntryNotReady(repr(err)) from err
except InvalidAuthError as err:
@ -314,6 +326,9 @@ async def _async_setup_rpc_entry(hass: HomeAssistant, entry: ConfigEntry) -> boo
LOGGER.debug("Setting up offline block device %s", entry.title)
await _async_rpc_device_setup()
ir.async_delete_issue(
hass, DOMAIN, FIRMWARE_UNSUPPORTED_ISSUE_ID.format(unique=entry.unique_id)
)
return True

View file

@ -212,6 +212,8 @@ PUSH_UPDATE_ISSUE_ID = "push_update_{unique}"
NOT_CALIBRATED_ISSUE_ID = "not_calibrated_{unique}"
FIRMWARE_UNSUPPORTED_ISSUE_ID = "firmware_unsupported_{unique}"
GAS_VALVE_OPEN_STATES = ("opening", "opened")
OTA_BEGIN = "ota_begin"

View file

@ -168,6 +168,10 @@
"deprecated_valve_switch_entity": {
"title": "Deprecated switch entity for Shelly Gas Valve detected in {info}",
"description": "Your Shelly Gas Valve entity `{entity}` is being used in `{info}`. A valve entity is available and should be used going forward.\n\nPlease adjust `{info}` to fix this issue."
},
"unsupported_firmware": {
"title": "Unsupported firmware for device {device_name}",
"description": "Your Shelly device {device_name} with IP address {ip_address} is running an unsupported firmware. Please update the firmware.\n\nIf the device does not offer an update, check internet connectivity (gateway, DNS, time) and restart the device."
}
}
}

View file

@ -22,7 +22,7 @@ from homeassistant.components.http import HomeAssistantView
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.core import Event, HomeAssistant, callback
from homeassistant.helpers import singleton
from homeassistant.helpers import issue_registry as ir, singleton
from homeassistant.helpers.device_registry import (
CONNECTION_NETWORK_MAC,
async_get as dr_async_get,
@ -38,6 +38,7 @@ from .const import (
DEFAULT_COAP_PORT,
DEVICES_WITHOUT_FIRMWARE_CHANGELOG,
DOMAIN,
FIRMWARE_UNSUPPORTED_ISSUE_ID,
GEN1_RELEASE_URL,
GEN2_RELEASE_URL,
LOGGER,
@ -426,3 +427,23 @@ def get_release_url(gen: int, model: str, beta: bool) -> str | None:
return None
return GEN1_RELEASE_URL if gen in BLOCK_GENERATIONS else GEN2_RELEASE_URL
@callback
def async_create_issue_unsupported_firmware(
hass: HomeAssistant, entry: ConfigEntry
) -> None:
"""Create a repair issue if the device runs an unsupported firmware."""
ir.async_create_issue(
hass,
DOMAIN,
FIRMWARE_UNSUPPORTED_ISSUE_ID.format(unique=entry.unique_id),
is_fixable=False,
is_persistent=False,
severity=ir.IssueSeverity.ERROR,
translation_key="unsupported_firmware",
translation_placeholders={
"device_name": entry.title,
"ip_address": entry.data["host"],
},
)

View file

@ -3,7 +3,11 @@ from datetime import timedelta
from unittest.mock import AsyncMock, patch
from aioshelly.const import MODEL_BULB, MODEL_BUTTON1
from aioshelly.exceptions import DeviceConnectionError, InvalidAuthError
from aioshelly.exceptions import (
DeviceConnectionError,
FirmwareUnsupported,
InvalidAuthError,
)
from freezegun.api import FrozenDateTimeFactory
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
@ -186,6 +190,27 @@ async def test_block_rest_update_auth_error(
assert flow["context"].get("entry_id") == entry.entry_id
async def test_block_firmware_unsupported(
hass: HomeAssistant, freezer: FrozenDateTimeFactory, mock_block_device, monkeypatch
) -> None:
"""Test block device polling authentication error."""
monkeypatch.setattr(
mock_block_device,
"update",
AsyncMock(side_effect=FirmwareUnsupported),
)
entry = await init_integration(hass, 1)
assert entry.state is ConfigEntryState.LOADED
# Move time to generate polling
freezer.tick(timedelta(seconds=UPDATE_PERIOD_MULTIPLIER * 15))
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert entry.state is ConfigEntryState.LOADED
async def test_block_polling_connection_error(
hass: HomeAssistant, freezer: FrozenDateTimeFactory, mock_block_device, monkeypatch
) -> None:
@ -504,7 +529,28 @@ async def test_rpc_sleeping_device_no_periodic_updates(
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert get_entity_state(hass, entity_id) == STATE_UNAVAILABLE
assert get_entity_state(hass, entity_id) is STATE_UNAVAILABLE
async def test_rpc_firmware_unsupported(
hass: HomeAssistant, freezer: FrozenDateTimeFactory, mock_rpc_device, monkeypatch
) -> None:
"""Test RPC update entry unsupported firmware."""
entry = await init_integration(hass, 2)
register_entity(
hass,
SENSOR_DOMAIN,
"test_name_temperature",
"temperature:0-temperature_0",
entry,
)
# Move time to generate sleep period update
freezer.tick(timedelta(seconds=600 * SLEEP_PERIOD_MULTIPLIER))
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert entry.state is ConfigEntryState.LOADED
async def test_rpc_reconnect_auth_error(

View file

@ -5,6 +5,7 @@ from unittest.mock import AsyncMock, patch
from aioshelly.exceptions import (
DeviceConnectionError,
FirmwareUnsupported,
InvalidAuthError,
MacAddressMismatchError,
)
@ -79,15 +80,21 @@ async def test_setup_entry_not_shelly(
@pytest.mark.parametrize("gen", [1, 2, 3])
@pytest.mark.parametrize("side_effect", [DeviceConnectionError, FirmwareUnsupported])
async def test_device_connection_error(
hass: HomeAssistant, gen, mock_block_device, mock_rpc_device, monkeypatch
hass: HomeAssistant,
gen,
side_effect,
mock_block_device,
mock_rpc_device,
monkeypatch,
) -> None:
"""Test device connection error."""
monkeypatch.setattr(
mock_block_device, "initialize", AsyncMock(side_effect=DeviceConnectionError)
mock_block_device, "initialize", AsyncMock(side_effect=side_effect)
)
monkeypatch.setattr(
mock_rpc_device, "initialize", AsyncMock(side_effect=DeviceConnectionError)
mock_rpc_device, "initialize", AsyncMock(side_effect=side_effect)
)
entry = await init_integration(hass, gen)