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 1 commit
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
10 changes: 10 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 @@ -50,6 +51,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 +218,10 @@ 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:
error = repr(err)
async_create_issue_unsupported_firmware(hass, entry)
raise ConfigEntryNotReady(error) from err
chemelli74 marked this conversation as resolved.
Show resolved Hide resolved

await _async_block_device_setup()
elif sleep_period is None or device_entry is None:
Expand Down Expand Up @@ -296,6 +302,10 @@ 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:
error = repr(err)
async_create_issue_unsupported_firmware(hass, entry)
raise ConfigEntryNotReady(error) from err
chemelli74 marked this conversation as resolved.
Show resolved Hide resolved
except (DeviceConnectionError, MacAddressMismatchError) as err:
raise ConfigEntryNotReady(repr(err)) from err
except InvalidAuthError as err:
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
17 changes: 13 additions & 4 deletions homeassistant/components/shelly/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,21 @@
from datetime import timedelta
from typing import Any, Generic, TypeVar, cast

import aioshelly
from aioshelly.ble import async_ensure_ble_enabled, async_stop_scanner
from aioshelly.block_device import BlockDevice, BlockUpdateType
from aioshelly.const import MODEL_VALVE
from aioshelly.exceptions import DeviceConnectionError, InvalidAuthError, RpcCallError
from aioshelly.const import MODEL_NAMES, MODEL_VALVE
from aioshelly.exceptions import (
DeviceConnectionError,
FirmwareUnsupported,
InvalidAuthError,
RpcCallError,
)
bdraco marked this conversation as resolved.
Show resolved Hide resolved
from aioshelly.rpc_device import RpcDevice, RpcUpdateType

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.exceptions import ConfigEntryNotReady
bdraco marked this conversation as resolved.
Show resolved Hide resolved
from homeassistant.helpers import issue_registry as ir
from homeassistant.helpers.debounce import Debouncer
from homeassistant.helpers.device_registry import (
Expand Down Expand Up @@ -137,7 +142,7 @@
name=self.name,
connections={(CONNECTION_NETWORK_MAC, self.mac)},
manufacturer="Shelly",
model=aioshelly.const.MODEL_NAMES.get(self.model, self.model),
model=MODEL_NAMES.get(self.model, self.model),
sw_version=self.sw_version,
hw_version=f"gen{get_device_entry_gen(self.entry)} ({self.model})",
configuration_url=f"http://{self.entry.data[CONF_HOST]}",
Expand Down Expand Up @@ -298,6 +303,8 @@
raise UpdateFailed(f"Error fetching data: {repr(err)}") from err
except InvalidAuthError:
self.entry.async_start_reauth(self.hass)
except FirmwareUnsupported as err:
raise ConfigEntryNotReady(repr(err)) from err
bdraco marked this conversation as resolved.
Show resolved Hide resolved

@callback
def _async_handle_update(
Expand Down Expand Up @@ -536,6 +543,8 @@
raise UpdateFailed(f"Device disconnected: {repr(err)}") from err
except InvalidAuthError:
self.entry.async_start_reauth(self.hass)
except FirmwareUnsupported as err:
bdraco marked this conversation as resolved.
Show resolved Hide resolved
raise ConfigEntryNotReady(repr(err)) from err

Check warning on line 547 in homeassistant/components/shelly/coordinator.py

View check run for this annotation

Codecov / codecov/patch

homeassistant/components/shelly/coordinator.py#L546-L547

Added lines #L546 - L547 were not covered by tests
chemelli74 marked this conversation as resolved.
Show resolved Hide resolved

async def _async_disconnected(self) -> None:
"""Handle device disconnected."""
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"],
},
)
48 changes: 47 additions & 1 deletion 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 == ConfigEntryState.LOADED
chemelli74 marked this conversation as resolved.
Show resolved Hide resolved

# 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 == ConfigEntryState.LOADED
chemelli74 marked this conversation as resolved.
Show resolved Hide resolved


async def test_block_polling_connection_error(
hass: HomeAssistant, freezer: FrozenDateTimeFactory, mock_block_device, monkeypatch
) -> None:
Expand Down Expand Up @@ -507,6 +532,27 @@ async def test_rpc_sleeping_device_no_periodic_updates(
assert get_entity_state(hass, entity_id) == 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 == ConfigEntryState.LOADED
chemelli74 marked this conversation as resolved.
Show resolved Hide resolved


async def test_rpc_reconnect_auth_error(
hass: HomeAssistant, freezer: FrozenDateTimeFactory, mock_rpc_device, monkeypatch
) -> None:
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