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:
parent
e1576d5998
commit
d4c91bd0b7
6 changed files with 101 additions and 6 deletions
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"],
|
||||
},
|
||||
)
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue