Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions homeassistant/components/shelly/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from aioshelly.const import RPC_GENERATIONS
from aioshelly.exceptions import (
DeviceConnectionError,
FirmwareUnsupported,
InvalidAuthError,
MacAddressMismatchError,
)
Expand Down Expand Up @@ -37,6 +38,7 @@
DATA_CONFIG_ENTRY,
DEFAULT_COAP_PORT,
DOMAIN,
FIRMWARE_UNSUPPORTED_ISSUE_ID,
LOGGER,
MODELS_WITH_WRONG_SLEEP_PERIOD,
PUSH_UPDATE_ISSUE_ID,
Expand All @@ -50,6 +52,7 @@
get_entry_data,
)
from .utils import (
async_create_issue_unsupported_firmware,
get_block_device_sleep_period,
get_coap_context,
get_device_entry_gen,
Expand Down Expand Up @@ -216,6 +219,9 @@ def _async_device_online(_: Any, update_type: BlockUpdateType) -> None:
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:
Expand All @@ -230,6 +236,9 @@ def _async_device_online(_: Any, update_type: BlockUpdateType) -> None:
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


Expand Down Expand Up @@ -296,6 +305,9 @@ def _async_device_online(_: Any, update_type: RpcUpdateType) -> None:
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:
Expand All @@ -314,6 +326,9 @@ def _async_device_online(_: Any, update_type: RpcUpdateType) -> None:
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


Expand Down
2 changes: 2 additions & 0 deletions homeassistant/components/shelly/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,8 @@ class BLEScannerMode(StrEnum):

NOT_CALIBRATED_ISSUE_ID = "not_calibrated_{unique}"

FIRMWARE_UNSUPPORTED_ISSUE_ID = "firmware_unsupported_{unique}"

GAS_VALVE_OPEN_STATES = ("opening", "opened")

OTA_BEGIN = "ota_begin"
Expand Down
4 changes: 4 additions & 0 deletions homeassistant/components/shelly/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -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."
}
}
}
23 changes: 22 additions & 1 deletion homeassistant/components/shelly/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
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,
Expand All @@ -38,6 +38,7 @@
DEFAULT_COAP_PORT,
DEVICES_WITHOUT_FIRMWARE_CHANGELOG,
DOMAIN,
FIRMWARE_UNSUPPORTED_ISSUE_ID,
GEN1_RELEASE_URL,
GEN2_RELEASE_URL,
LOGGER,
Expand Down Expand Up @@ -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"],
},
)
50 changes: 48 additions & 2 deletions tests/components/shelly/test_coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@
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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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(
Expand Down
13 changes: 10 additions & 3 deletions tests/components/shelly/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

from aioshelly.exceptions import (
DeviceConnectionError,
FirmwareUnsupported,
InvalidAuthError,
MacAddressMismatchError,
)
Expand Down Expand Up @@ -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)
Expand Down
Loading