Allow Shelly CoAP to honour default network adapter (#110997)

* Allow Shelly CoAP to honor default network adapter

* apply review comment

* 1 more debug log line

* adapt code to library changes

* test

* improve test

* one more test
This commit is contained in:
Simone Chemelli 2024-03-11 09:52:15 +01:00 committed by GitHub
parent 30c3174498
commit 4095de0566
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 73 additions and 7 deletions

View File

@ -7,7 +7,7 @@ from typing import Any, Final
from aioshelly.block_device import BlockDevice, BlockUpdateType from aioshelly.block_device import BlockDevice, BlockUpdateType
from aioshelly.common import ConnectionOptions from aioshelly.common import ConnectionOptions
from aioshelly.const import RPC_GENERATIONS from aioshelly.const import DEFAULT_COAP_PORT, RPC_GENERATIONS
from aioshelly.exceptions import ( from aioshelly.exceptions import (
DeviceConnectionError, DeviceConnectionError,
FirmwareUnsupported, FirmwareUnsupported,
@ -37,7 +37,6 @@ from .const import (
CONF_COAP_PORT, CONF_COAP_PORT,
CONF_SLEEP_PERIOD, CONF_SLEEP_PERIOD,
DATA_CONFIG_ENTRY, DATA_CONFIG_ENTRY,
DEFAULT_COAP_PORT,
DOMAIN, DOMAIN,
FIRMWARE_UNSUPPORTED_ISSUE_ID, FIRMWARE_UNSUPPORTED_ISSUE_ID,
LOGGER, LOGGER,

View File

@ -33,7 +33,6 @@ LOGGER: Logger = getLogger(__package__)
DATA_CONFIG_ENTRY: Final = "config_entry" DATA_CONFIG_ENTRY: Final = "config_entry"
CONF_COAP_PORT: Final = "coap_port" CONF_COAP_PORT: Final = "coap_port"
DEFAULT_COAP_PORT: Final = 5683
FIRMWARE_PATTERN: Final = re.compile(r"^(\d{8})") FIRMWARE_PATTERN: Final = re.compile(r"^(\d{8})")
# max light transition time in milliseconds # max light transition time in milliseconds

View File

@ -3,7 +3,7 @@
"name": "Shelly", "name": "Shelly",
"codeowners": ["@balloob", "@bieniu", "@thecode", "@chemelli74", "@bdraco"], "codeowners": ["@balloob", "@bieniu", "@thecode", "@chemelli74", "@bdraco"],
"config_flow": true, "config_flow": true,
"dependencies": ["bluetooth", "http"], "dependencies": ["bluetooth", "http", "network"],
"documentation": "https://www.home-assistant.io/integrations/shelly", "documentation": "https://www.home-assistant.io/integrations/shelly",
"integration_type": "device", "integration_type": "device",
"iot_class": "local_push", "iot_class": "local_push",

View File

@ -3,12 +3,14 @@
from __future__ import annotations from __future__ import annotations
from datetime import datetime, timedelta from datetime import datetime, timedelta
from ipaddress import IPv4Address
from typing import Any, cast from typing import Any, cast
from aiohttp.web import Request, WebSocketResponse from aiohttp.web import Request, WebSocketResponse
from aioshelly.block_device import COAP, Block, BlockDevice from aioshelly.block_device import COAP, Block, BlockDevice
from aioshelly.const import ( from aioshelly.const import (
BLOCK_GENERATIONS, BLOCK_GENERATIONS,
DEFAULT_COAP_PORT,
MODEL_1L, MODEL_1L,
MODEL_DIMMER, MODEL_DIMMER,
MODEL_DIMMER_2, MODEL_DIMMER_2,
@ -19,6 +21,7 @@ from aioshelly.const import (
) )
from aioshelly.rpc_device import RpcDevice, WsServer from aioshelly.rpc_device import RpcDevice, WsServer
from homeassistant.components import network
from homeassistant.components.http import HomeAssistantView 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
@ -36,7 +39,6 @@ from .const import (
BASIC_INPUTS_EVENTS_TYPES, BASIC_INPUTS_EVENTS_TYPES,
CONF_COAP_PORT, CONF_COAP_PORT,
CONF_GEN, CONF_GEN,
DEFAULT_COAP_PORT,
DEVICES_WITHOUT_FIRMWARE_CHANGELOG, DEVICES_WITHOUT_FIRMWARE_CHANGELOG,
DOMAIN, DOMAIN,
FIRMWARE_UNSUPPORTED_ISSUE_ID, FIRMWARE_UNSUPPORTED_ISSUE_ID,
@ -221,12 +223,27 @@ def get_shbtn_input_triggers() -> list[tuple[str, str]]:
async def get_coap_context(hass: HomeAssistant) -> COAP: async def get_coap_context(hass: HomeAssistant) -> COAP:
"""Get CoAP context to be used in all Shelly Gen1 devices.""" """Get CoAP context to be used in all Shelly Gen1 devices."""
context = COAP() context = COAP()
adapters = await network.async_get_adapters(hass)
LOGGER.debug("Network adapters: %s", adapters)
ipv4: list[IPv4Address] = []
if not network.async_only_default_interface_enabled(adapters):
for address in await network.async_get_enabled_source_ips(hass):
if address.version == 4 and not (
address.is_link_local
or address.is_loopback
or address.is_multicast
or address.is_unspecified
):
ipv4.append(address)
LOGGER.debug("Network IPv4 addresses: %s", ipv4)
if DOMAIN in hass.data: if DOMAIN in hass.data:
port = hass.data[DOMAIN].get(CONF_COAP_PORT, DEFAULT_COAP_PORT) port = hass.data[DOMAIN].get(CONF_COAP_PORT, DEFAULT_COAP_PORT)
else: else:
port = DEFAULT_COAP_PORT port = DEFAULT_COAP_PORT
LOGGER.info("Starting CoAP context with UDP port %s", port) LOGGER.info("Starting CoAP context with UDP port %s", port)
await context.initialize(port) await context.initialize(port, ipv4)
@callback @callback
def shutdown_listener(ev: Event) -> None: def shutdown_listener(ev: Event) -> None:

View File

@ -1,7 +1,9 @@
"""Test cases for the Shelly component.""" """Test cases for the Shelly component."""
from unittest.mock import AsyncMock, Mock, patch from ipaddress import IPv4Address
from unittest.mock import AsyncMock, Mock, call, patch
from aioshelly.block_device import COAP
from aioshelly.exceptions import ( from aioshelly.exceptions import (
DeviceConnectionError, DeviceConnectionError,
FirmwareUnsupported, FirmwareUnsupported,
@ -49,6 +51,55 @@ async def test_custom_coap_port(
assert "Starting CoAP context with UDP port 7632" in caplog.text assert "Starting CoAP context with UDP port 7632" in caplog.text
async def test_ip_address_with_only_default_interface(
hass: HomeAssistant, mock_block_device: Mock, caplog: pytest.LogCaptureFixture
) -> None:
"""Test more local ip addresses with only the default interface.."""
with patch(
"homeassistant.components.network.async_only_default_interface_enabled",
return_value=True,
), patch(
"homeassistant.components.network.async_get_enabled_source_ips",
return_value=[IPv4Address("192.168.1.10"), IPv4Address("10.10.10.10")],
), patch(
"homeassistant.components.shelly.utils.COAP",
autospec=COAP,
) as mock_coap_init:
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
# Make sure COAP.initialize is called with an empty list
# when async_only_default_interface_enabled is True even if
# async_get_enabled_source_ips returns more than one address
assert mock_coap_init.mock_calls[1] == call().initialize(7632, [])
async def test_ip_address_without_only_default_interface(
hass: HomeAssistant, mock_block_device: Mock, caplog: pytest.LogCaptureFixture
) -> None:
"""Test more local ip addresses without only the default interface.."""
with patch(
"homeassistant.components.network.async_only_default_interface_enabled",
return_value=False,
), patch(
"homeassistant.components.network.async_get_enabled_source_ips",
return_value=[IPv4Address("192.168.1.10"), IPv4Address("10.10.10.10")],
), patch(
"homeassistant.components.shelly.utils.COAP",
autospec=COAP,
) as mock_coap_init:
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
assert mock_coap_init.mock_calls[1] == call().initialize(
7632, [IPv4Address("192.168.1.10"), IPv4Address("10.10.10.10")]
)
@pytest.mark.parametrize("gen", [1, 2, 3]) @pytest.mark.parametrize("gen", [1, 2, 3])
async def test_shared_device_mac( async def test_shared_device_mac(
hass: HomeAssistant, hass: HomeAssistant,