Fix Shelly entry unload and add tests for init (#80760)

This commit is contained in:
Shay Levy 2022-10-22 10:00:35 +03:00 committed by GitHub
parent f35af09429
commit 228d491216
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 218 additions and 28 deletions

View File

@ -1106,7 +1106,6 @@ omit =
homeassistant/components/sesame/lock.py homeassistant/components/sesame/lock.py
homeassistant/components/seven_segments/image_processing.py homeassistant/components/seven_segments/image_processing.py
homeassistant/components/seventeentrack/sensor.py homeassistant/components/seventeentrack/sensor.py
homeassistant/components/shelly/__init__.py
homeassistant/components/shelly/binary_sensor.py homeassistant/components/shelly/binary_sensor.py
homeassistant/components/shelly/climate.py homeassistant/components/shelly/climate.py
homeassistant/components/shelly/coordinator.py homeassistant/components/shelly/coordinator.py

View File

@ -143,6 +143,7 @@ async def _async_setup_block_entry(hass: HomeAssistant, entry: ConfigEntry) -> b
) )
}, },
) )
# https://github.com/home-assistant/core/pull/48076
if device_entry and entry.entry_id not in device_entry.config_entries: if device_entry and entry.entry_id not in device_entry.config_entries:
device_entry = None device_entry = None
@ -231,6 +232,7 @@ async def _async_setup_rpc_entry(hass: HomeAssistant, entry: ConfigEntry) -> boo
) )
}, },
) )
# https://github.com/home-assistant/core/pull/48076
if device_entry and entry.entry_id not in device_entry.config_entries: if device_entry and entry.entry_id not in device_entry.config_entries:
device_entry = None device_entry = None
@ -295,9 +297,13 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry.""" """Unload a config entry."""
shelly_entry_data = get_entry_data(hass)[entry.entry_id] shelly_entry_data = get_entry_data(hass)[entry.entry_id]
if shelly_entry_data.device is not None: # If device is present, block/rpc coordinator is not setup yet
# If device is present, block/rpc coordinator is not setup yet device = shelly_entry_data.device
shelly_entry_data.device.shutdown() if isinstance(device, RpcDevice):
await device.shutdown()
return True
if isinstance(device, BlockDevice):
device.shutdown()
return True return True
platforms = RPC_SLEEPING_PLATFORMS platforms = RPC_SLEEPING_PLATFORMS

View File

@ -5,19 +5,21 @@ from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry from tests.common import MockConfigEntry
MOCK_MAC = "123456789ABC"
async def init_integration( async def init_integration(
hass: HomeAssistant, gen: int, model="SHSW-25" hass: HomeAssistant, gen: int, model="SHSW-25", sleep_period=0
) -> MockConfigEntry: ) -> MockConfigEntry:
"""Set up the Shelly integration in Home Assistant.""" """Set up the Shelly integration in Home Assistant."""
data = { data = {
CONF_HOST: "192.168.1.37", CONF_HOST: "192.168.1.37",
CONF_SLEEP_PERIOD: 0, CONF_SLEEP_PERIOD: sleep_period,
"model": model, "model": model,
"gen": gen, "gen": gen,
} }
entry = MockConfigEntry(domain=DOMAIN, data=data, unique_id=DOMAIN) entry = MockConfigEntry(domain=DOMAIN, data=data, unique_id=MOCK_MAC)
entry.add_to_hass(hass) entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id) await hass.config_entries.async_setup(entry.entry_id)

View File

@ -1,6 +1,8 @@
"""Test configuration for Shelly.""" """Test configuration for Shelly."""
from unittest.mock import AsyncMock, Mock, patch from unittest.mock import AsyncMock, Mock, patch
from aioshelly.block_device import BlockDevice
from aioshelly.rpc_device import RpcDevice
import pytest import pytest
from homeassistant.components.shelly.const import ( from homeassistant.components.shelly.const import (
@ -8,13 +10,15 @@ from homeassistant.components.shelly.const import (
REST_SENSORS_UPDATE_INTERVAL, REST_SENSORS_UPDATE_INTERVAL,
) )
from . import MOCK_MAC
from tests.common import async_capture_events, async_mock_service, mock_device_registry from tests.common import async_capture_events, async_mock_service, mock_device_registry
MOCK_SETTINGS = { MOCK_SETTINGS = {
"name": "Test name", "name": "Test name",
"mode": "relay", "mode": "relay",
"device": { "device": {
"mac": "test-mac", "mac": MOCK_MAC,
"hostname": "test-host", "hostname": "test-host",
"type": "SHSW-25", "type": "SHSW-25",
"num_outputs": 2, "num_outputs": 2,
@ -95,7 +99,7 @@ MOCK_CONFIG = {
} }
MOCK_SHELLY_COAP = { MOCK_SHELLY_COAP = {
"mac": "test-mac", "mac": MOCK_MAC,
"auth": False, "auth": False,
"fw": "20201124-092854/v1.9.0@57ac4ad8", "fw": "20201124-092854/v1.9.0@57ac4ad8",
"num_outputs": 2, "num_outputs": 2,
@ -104,7 +108,7 @@ MOCK_SHELLY_COAP = {
MOCK_SHELLY_RPC = { MOCK_SHELLY_RPC = {
"name": "Test Gen2", "name": "Test Gen2",
"id": "shellyplus2pm-123456789abc", "id": "shellyplus2pm-123456789abc",
"mac": "123456789ABC", "mac": MOCK_MAC,
"model": "SNSW-002P16EU", "model": "SNSW-002P16EU",
"gen": 2, "gen": 2,
"fw_id": "20220830-130540/0.11.0-gfa1bc37", "fw_id": "20220830-130540/0.11.0-gfa1bc37",
@ -142,7 +146,13 @@ MOCK_STATUS_RPC = {
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def mock_coap(): def mock_coap():
"""Mock out coap.""" """Mock out coap."""
with patch("homeassistant.components.shelly.utils.get_coap_context"): with patch(
"homeassistant.components.shelly.utils.COAP",
return_value=Mock(
initialize=AsyncMock(),
close=Mock(),
),
):
yield yield
@ -174,24 +184,18 @@ def events(hass):
@pytest.fixture @pytest.fixture
async def mock_block_device(): async def mock_block_device():
"""Mock block (Gen1, CoAP) device.""" """Mock block (Gen1, CoAP) device."""
with patch("homeassistant.components.shelly.utils.COAP", autospec=True), patch( with patch("aioshelly.block_device.BlockDevice.create") as block_device_mock:
"aioshelly.block_device.BlockDevice.create"
) as block_device_mock:
def update(): def update():
block_device_mock.return_value.subscribe_updates.call_args[0][0]({}) block_device_mock.return_value.subscribe_updates.call_args[0][0]({})
device = Mock( device = Mock(
spec=BlockDevice,
blocks=MOCK_BLOCKS, blocks=MOCK_BLOCKS,
settings=MOCK_SETTINGS, settings=MOCK_SETTINGS,
shelly=MOCK_SHELLY_COAP, shelly=MOCK_SHELLY_COAP,
status=MOCK_STATUS_COAP, status=MOCK_STATUS_COAP,
firmware_version="some fw string", firmware_version="some fw string",
update=AsyncMock(),
update_status=AsyncMock(),
trigger_ota_update=AsyncMock(),
trigger_reboot=AsyncMock(),
initialize=AsyncMock(),
initialized=True, initialized=True,
) )
block_device_mock.return_value = device block_device_mock.return_value = device
@ -209,18 +213,13 @@ async def mock_rpc_device():
rpc_device_mock.return_value.subscribe_updates.call_args[0][0]({}) rpc_device_mock.return_value.subscribe_updates.call_args[0][0]({})
device = Mock( device = Mock(
call_rpc=AsyncMock(), spec=RpcDevice,
config=MOCK_CONFIG, config=MOCK_CONFIG,
event={}, event={},
shelly=MOCK_SHELLY_RPC, shelly=MOCK_SHELLY_RPC,
status=MOCK_STATUS_RPC, status=MOCK_STATUS_RPC,
firmware_version="some fw string", firmware_version="some fw string",
update=AsyncMock(),
trigger_ota_update=AsyncMock(),
trigger_reboot=AsyncMock(),
initialize=AsyncMock(),
initialized=True, initialized=True,
shutdown=AsyncMock(),
) )
rpc_device_mock.return_value = device rpc_device_mock.return_value = device

View File

@ -0,0 +1,184 @@
"""Test cases for the Shelly component."""
from unittest.mock import AsyncMock
from aioshelly.exceptions import DeviceConnectionError, InvalidAuthError
import pytest
from homeassistant.components.shelly.const import DOMAIN
from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState
from homeassistant.const import STATE_ON, STATE_UNAVAILABLE
from homeassistant.helpers import device_registry
from homeassistant.setup import async_setup_component
from . import MOCK_MAC, init_integration
from tests.common import MockConfigEntry
async def test_custom_coap_port(hass, mock_block_device, caplog):
"""Test custom coap port."""
assert await async_setup_component(
hass,
DOMAIN,
{DOMAIN: {"coap_port": 7632}},
)
await hass.async_block_till_done()
await init_integration(hass, 1)
assert "Starting CoAP context with UDP port 7632" in caplog.text
@pytest.mark.parametrize("gen", [1, 2])
async def test_shared_device_mac(
hass, gen, mock_block_device, mock_rpc_device, device_reg, caplog
):
"""Test first time shared device with another domain."""
config_entry = MockConfigEntry(domain="test", data={}, unique_id="some_id")
config_entry.add_to_hass(hass)
device_reg.async_get_or_create(
config_entry_id=config_entry.entry_id,
connections={
(
device_registry.CONNECTION_NETWORK_MAC,
device_registry.format_mac(MOCK_MAC),
)
},
)
await init_integration(hass, gen, sleep_period=1000)
assert "will resume when device is online" in caplog.text
async def test_setup_entry_not_shelly(hass, caplog):
"""Test not Shelly entry."""
entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN)
entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(entry.entry_id) is False
await hass.async_block_till_done()
assert "probably comes from a custom integration" in caplog.text
@pytest.mark.parametrize("gen", [1, 2])
async def test_device_connection_error(
hass, gen, mock_block_device, mock_rpc_device, monkeypatch
):
"""Test device connection error."""
monkeypatch.setattr(
mock_block_device, "initialize", AsyncMock(side_effect=DeviceConnectionError)
)
monkeypatch.setattr(
mock_rpc_device, "initialize", AsyncMock(side_effect=DeviceConnectionError)
)
entry = await init_integration(hass, gen)
assert entry.state == ConfigEntryState.SETUP_RETRY
@pytest.mark.parametrize("gen", [1, 2])
async def test_device_auth_error(
hass, gen, mock_block_device, mock_rpc_device, monkeypatch
):
"""Test device authentication error."""
monkeypatch.setattr(
mock_block_device, "initialize", AsyncMock(side_effect=InvalidAuthError)
)
monkeypatch.setattr(
mock_rpc_device, "initialize", AsyncMock(side_effect=InvalidAuthError)
)
entry = await init_integration(hass, gen)
assert entry.state == ConfigEntryState.SETUP_ERROR
flows = hass.config_entries.flow.async_progress()
assert len(flows) == 1
flow = flows[0]
assert flow.get("step_id") == "reauth_confirm"
assert flow.get("handler") == DOMAIN
assert "context" in flow
assert flow["context"].get("source") == SOURCE_REAUTH
assert flow["context"].get("entry_id") == entry.entry_id
@pytest.mark.parametrize("entry_sleep, device_sleep", [(None, 0), (1000, 1000)])
async def test_sleeping_block_device_online(
hass, entry_sleep, device_sleep, mock_block_device, device_reg, caplog
):
"""Test sleeping block device online."""
config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id="shelly")
config_entry.add_to_hass(hass)
device_reg.async_get_or_create(
config_entry_id=config_entry.entry_id,
connections={
(
device_registry.CONNECTION_NETWORK_MAC,
device_registry.format_mac(MOCK_MAC),
)
},
)
entry = await init_integration(hass, 1, sleep_period=entry_sleep)
assert "will resume when device is online" in caplog.text
mock_block_device.mock_update()
assert "online, resuming setup" in caplog.text
assert entry.data["sleep_period"] == device_sleep
@pytest.mark.parametrize("entry_sleep, device_sleep", [(None, 0), (1000, 1000)])
async def test_sleeping_rpc_device_online(
hass, entry_sleep, device_sleep, mock_rpc_device, caplog
):
"""Test sleeping RPC device online."""
entry = await init_integration(hass, 2, sleep_period=entry_sleep)
assert "will resume when device is online" in caplog.text
mock_rpc_device.mock_update()
assert "online, resuming setup" in caplog.text
assert entry.data["sleep_period"] == device_sleep
@pytest.mark.parametrize(
"gen, entity_id",
[
(1, "switch.test_name_channel_1"),
(2, "switch.test_switch_0"),
],
)
async def test_entry_unload(hass, gen, entity_id, mock_block_device, mock_rpc_device):
"""Test entry unload."""
entry = await init_integration(hass, gen)
assert entry.state is ConfigEntryState.LOADED
assert hass.states.get(entity_id).state is STATE_ON
await hass.config_entries.async_unload(entry.entry_id)
await hass.async_block_till_done()
assert entry.state is ConfigEntryState.NOT_LOADED
assert hass.states.get(entity_id).state is STATE_UNAVAILABLE
@pytest.mark.parametrize(
"gen, entity_id",
[
(1, "switch.test_name_channel_1"),
(2, "switch.test_switch_0"),
],
)
async def test_entry_unload_device_not_ready(
hass, gen, entity_id, mock_block_device, mock_rpc_device
):
"""Test entry unload when device is not ready."""
entry = await init_integration(hass, gen, sleep_period=1000)
assert entry.state is ConfigEntryState.LOADED
assert hass.states.get(entity_id) is None
await hass.config_entries.async_unload(entry.entry_id)
await hass.async_block_till_done()
assert entry.state is ConfigEntryState.NOT_LOADED

View File

@ -6,7 +6,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_component import async_update_entity from homeassistant.helpers.entity_component import async_update_entity
from homeassistant.helpers.entity_registry import async_get from homeassistant.helpers.entity_registry import async_get
from . import init_integration from . import MOCK_MAC, init_integration
async def test_block_update(hass: HomeAssistant, mock_block_device, monkeypatch): async def test_block_update(hass: HomeAssistant, mock_block_device, monkeypatch):
@ -15,7 +15,7 @@ async def test_block_update(hass: HomeAssistant, mock_block_device, monkeypatch)
entity_registry.async_get_or_create( entity_registry.async_get_or_create(
UPDATE_DOMAIN, UPDATE_DOMAIN,
DOMAIN, DOMAIN,
"test-mac-fwupdate", f"{MOCK_MAC}-fwupdate",
suggested_object_id="test_name_firmware_update", suggested_object_id="test_name_firmware_update",
disabled_by=None, disabled_by=None,
) )
@ -46,7 +46,7 @@ async def test_rpc_update(hass: HomeAssistant, mock_rpc_device, monkeypatch):
entity_registry.async_get_or_create( entity_registry.async_get_or_create(
UPDATE_DOMAIN, UPDATE_DOMAIN,
DOMAIN, DOMAIN,
"shelly-sys-fwupdate", f"{MOCK_MAC}-sys-fwupdate",
suggested_object_id="test_name_firmware_update", suggested_object_id="test_name_firmware_update",
disabled_by=None, disabled_by=None,
) )