mirror of
https://github.com/home-assistant/core.git
synced 2025-07-17 10:17:09 +00:00
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
@ -9,6 +9,7 @@ from aioshelly.common import ConnectionOptions
|
|||||||
from aioshelly.const import RPC_GENERATIONS
|
from aioshelly.const import RPC_GENERATIONS
|
||||||
from aioshelly.exceptions import (
|
from aioshelly.exceptions import (
|
||||||
DeviceConnectionError,
|
DeviceConnectionError,
|
||||||
|
FirmwareUnsupported,
|
||||||
InvalidAuthError,
|
InvalidAuthError,
|
||||||
MacAddressMismatchError,
|
MacAddressMismatchError,
|
||||||
)
|
)
|
||||||
@ -37,6 +38,7 @@ from .const import (
|
|||||||
DATA_CONFIG_ENTRY,
|
DATA_CONFIG_ENTRY,
|
||||||
DEFAULT_COAP_PORT,
|
DEFAULT_COAP_PORT,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
|
FIRMWARE_UNSUPPORTED_ISSUE_ID,
|
||||||
LOGGER,
|
LOGGER,
|
||||||
MODELS_WITH_WRONG_SLEEP_PERIOD,
|
MODELS_WITH_WRONG_SLEEP_PERIOD,
|
||||||
PUSH_UPDATE_ISSUE_ID,
|
PUSH_UPDATE_ISSUE_ID,
|
||||||
@ -50,6 +52,7 @@ from .coordinator import (
|
|||||||
get_entry_data,
|
get_entry_data,
|
||||||
)
|
)
|
||||||
from .utils import (
|
from .utils import (
|
||||||
|
async_create_issue_unsupported_firmware,
|
||||||
get_block_device_sleep_period,
|
get_block_device_sleep_period,
|
||||||
get_coap_context,
|
get_coap_context,
|
||||||
get_device_entry_gen,
|
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
|
raise ConfigEntryNotReady(repr(err)) from err
|
||||||
except InvalidAuthError as err:
|
except InvalidAuthError as err:
|
||||||
raise ConfigEntryAuthFailed(repr(err)) from 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()
|
await _async_block_device_setup()
|
||||||
elif sleep_period is None or device_entry is None:
|
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)
|
LOGGER.debug("Setting up offline block device %s", entry.title)
|
||||||
await _async_block_device_setup()
|
await _async_block_device_setup()
|
||||||
|
|
||||||
|
ir.async_delete_issue(
|
||||||
|
hass, DOMAIN, FIRMWARE_UNSUPPORTED_ISSUE_ID.format(unique=entry.unique_id)
|
||||||
|
)
|
||||||
return True
|
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)
|
LOGGER.debug("Setting up online RPC device %s", entry.title)
|
||||||
try:
|
try:
|
||||||
await device.initialize()
|
await device.initialize()
|
||||||
|
except FirmwareUnsupported as err:
|
||||||
|
async_create_issue_unsupported_firmware(hass, entry)
|
||||||
|
raise ConfigEntryNotReady from err
|
||||||
except (DeviceConnectionError, MacAddressMismatchError) as err:
|
except (DeviceConnectionError, MacAddressMismatchError) as err:
|
||||||
raise ConfigEntryNotReady(repr(err)) from err
|
raise ConfigEntryNotReady(repr(err)) from err
|
||||||
except InvalidAuthError as 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)
|
LOGGER.debug("Setting up offline block device %s", entry.title)
|
||||||
await _async_rpc_device_setup()
|
await _async_rpc_device_setup()
|
||||||
|
|
||||||
|
ir.async_delete_issue(
|
||||||
|
hass, DOMAIN, FIRMWARE_UNSUPPORTED_ISSUE_ID.format(unique=entry.unique_id)
|
||||||
|
)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
@ -212,6 +212,8 @@ PUSH_UPDATE_ISSUE_ID = "push_update_{unique}"
|
|||||||
|
|
||||||
NOT_CALIBRATED_ISSUE_ID = "not_calibrated_{unique}"
|
NOT_CALIBRATED_ISSUE_ID = "not_calibrated_{unique}"
|
||||||
|
|
||||||
|
FIRMWARE_UNSUPPORTED_ISSUE_ID = "firmware_unsupported_{unique}"
|
||||||
|
|
||||||
GAS_VALVE_OPEN_STATES = ("opening", "opened")
|
GAS_VALVE_OPEN_STATES = ("opening", "opened")
|
||||||
|
|
||||||
OTA_BEGIN = "ota_begin"
|
OTA_BEGIN = "ota_begin"
|
||||||
|
@ -168,6 +168,10 @@
|
|||||||
"deprecated_valve_switch_entity": {
|
"deprecated_valve_switch_entity": {
|
||||||
"title": "Deprecated switch entity for Shelly Gas Valve detected in {info}",
|
"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."
|
"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.config_entries import ConfigEntry
|
||||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||||
from homeassistant.core import Event, HomeAssistant, callback
|
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 (
|
from homeassistant.helpers.device_registry import (
|
||||||
CONNECTION_NETWORK_MAC,
|
CONNECTION_NETWORK_MAC,
|
||||||
async_get as dr_async_get,
|
async_get as dr_async_get,
|
||||||
@ -38,6 +38,7 @@ from .const import (
|
|||||||
DEFAULT_COAP_PORT,
|
DEFAULT_COAP_PORT,
|
||||||
DEVICES_WITHOUT_FIRMWARE_CHANGELOG,
|
DEVICES_WITHOUT_FIRMWARE_CHANGELOG,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
|
FIRMWARE_UNSUPPORTED_ISSUE_ID,
|
||||||
GEN1_RELEASE_URL,
|
GEN1_RELEASE_URL,
|
||||||
GEN2_RELEASE_URL,
|
GEN2_RELEASE_URL,
|
||||||
LOGGER,
|
LOGGER,
|
||||||
@ -426,3 +427,23 @@ def get_release_url(gen: int, model: str, beta: bool) -> str | None:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
return GEN1_RELEASE_URL if gen in BLOCK_GENERATIONS else GEN2_RELEASE_URL
|
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 unittest.mock import AsyncMock, patch
|
||||||
|
|
||||||
from aioshelly.const import MODEL_BULB, MODEL_BUTTON1
|
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 freezegun.api import FrozenDateTimeFactory
|
||||||
|
|
||||||
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
|
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
|
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(
|
async def test_block_polling_connection_error(
|
||||||
hass: HomeAssistant, freezer: FrozenDateTimeFactory, mock_block_device, monkeypatch
|
hass: HomeAssistant, freezer: FrozenDateTimeFactory, mock_block_device, monkeypatch
|
||||||
) -> None:
|
) -> None:
|
||||||
@ -504,7 +529,28 @@ async def test_rpc_sleeping_device_no_periodic_updates(
|
|||||||
async_fire_time_changed(hass)
|
async_fire_time_changed(hass)
|
||||||
await hass.async_block_till_done()
|
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(
|
async def test_rpc_reconnect_auth_error(
|
||||||
|
@ -5,6 +5,7 @@ from unittest.mock import AsyncMock, patch
|
|||||||
|
|
||||||
from aioshelly.exceptions import (
|
from aioshelly.exceptions import (
|
||||||
DeviceConnectionError,
|
DeviceConnectionError,
|
||||||
|
FirmwareUnsupported,
|
||||||
InvalidAuthError,
|
InvalidAuthError,
|
||||||
MacAddressMismatchError,
|
MacAddressMismatchError,
|
||||||
)
|
)
|
||||||
@ -79,15 +80,21 @@ async def test_setup_entry_not_shelly(
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("gen", [1, 2, 3])
|
@pytest.mark.parametrize("gen", [1, 2, 3])
|
||||||
|
@pytest.mark.parametrize("side_effect", [DeviceConnectionError, FirmwareUnsupported])
|
||||||
async def test_device_connection_error(
|
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:
|
) -> None:
|
||||||
"""Test device connection error."""
|
"""Test device connection error."""
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(
|
||||||
mock_block_device, "initialize", AsyncMock(side_effect=DeviceConnectionError)
|
mock_block_device, "initialize", AsyncMock(side_effect=side_effect)
|
||||||
)
|
)
|
||||||
monkeypatch.setattr(
|
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)
|
entry = await init_integration(hass, gen)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user