Make upnp update interval configurable (#35298)

* Simplification of upnp component

* Make update interval configurable

* Description

* Require minimal value of 30

* Black

* Linting

Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
This commit is contained in:
Steven Looman 2020-05-10 04:52:08 +02:00 committed by GitHub
parent 8994931ec4
commit a97460d1ab
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 130 additions and 17 deletions

View file

@ -5,10 +5,13 @@ import voluptuous as vol
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.components import ssdp from homeassistant.components import ssdp
from homeassistant.const import CONF_SCAN_INTERVAL
from .const import ( # pylint: disable=unused-import from .const import ( # pylint: disable=unused-import
CONFIG_ENTRY_SCAN_INTERVAL,
CONFIG_ENTRY_ST, CONFIG_ENTRY_ST,
CONFIG_ENTRY_UDN, CONFIG_ENTRY_UDN,
DEFAULT_SCAN_INTERVAL,
DISCOVERY_LOCATION, DISCOVERY_LOCATION,
DISCOVERY_NAME, DISCOVERY_NAME,
DISCOVERY_ST, DISCOVERY_ST,
@ -54,7 +57,9 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
await self.async_set_unique_id( await self.async_set_unique_id(
discovery[DISCOVERY_USN], raise_on_progress=False discovery[DISCOVERY_USN], raise_on_progress=False
) )
return await self._async_create_entry_from_data(discovery) return await self._async_create_entry_from_discovery(
discovery, user_input[CONF_SCAN_INTERVAL]
)
# Discover devices. # Discover devices.
discoveries = await Device.async_discover(self.hass) discoveries = await Device.async_discover(self.hass)
@ -82,6 +87,9 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
for discovery in self._discoveries for discovery in self._discoveries
} }
), ),
vol.Optional(
CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL,
): vol.All(vol.Coerce(int), vol.Range(min=30)),
} }
) )
return self.async_show_form(step_id="user", data_schema=data_schema,) return self.async_show_form(step_id="user", data_schema=data_schema,)
@ -119,7 +127,9 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
return self.async_abort(reason="no_devices_found") return self.async_abort(reason="no_devices_found")
discovery = self._discoveries[0] discovery = self._discoveries[0]
return await self._async_create_entry_from_data(discovery) return await self._async_create_entry_from_discovery(
discovery, DEFAULT_SCAN_INTERVAL
)
async def async_step_ssdp(self, discovery_info: Mapping): async def async_step_ssdp(self, discovery_info: Mapping):
"""Handle a discovered UPnP/IGD device. """Handle a discovered UPnP/IGD device.
@ -160,11 +170,19 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
return self.async_show_form(step_id="ssdp_confirm") return self.async_show_form(step_id="ssdp_confirm")
discovery = self._discoveries[0] discovery = self._discoveries[0]
return await self._async_create_entry_from_data(discovery) return await self._async_create_entry_from_discovery(
discovery, DEFAULT_SCAN_INTERVAL
)
async def _async_create_entry_from_data(self, discovery: Mapping): async def _async_create_entry_from_discovery(
"""Create an entry from own _data.""" self, discovery: Mapping, scan_interval
_LOGGER.debug("_async_create_entry_from_data: discovery: %s", discovery) ):
"""Create an entry from discovery."""
_LOGGER.debug(
"_async_create_entry_from_data: discovery: %s, scan_interval: %s",
discovery,
scan_interval,
)
# Get name from device, if not found already. # Get name from device, if not found already.
if DISCOVERY_NAME not in discovery and DISCOVERY_LOCATION in discovery: if DISCOVERY_NAME not in discovery and DISCOVERY_LOCATION in discovery:
discovery[DISCOVERY_NAME] = await self._async_get_name_for_discovery( discovery[DISCOVERY_NAME] = await self._async_get_name_for_discovery(
@ -175,6 +193,7 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
data = { data = {
CONFIG_ENTRY_UDN: discovery[DISCOVERY_UDN], CONFIG_ENTRY_UDN: discovery[DISCOVERY_UDN],
CONFIG_ENTRY_ST: discovery[DISCOVERY_ST], CONFIG_ENTRY_ST: discovery[DISCOVERY_ST],
CONFIG_ENTRY_SCAN_INTERVAL: scan_interval,
} }
return self.async_create_entry(title=title, data=data) return self.async_create_entry(title=title, data=data)

View file

@ -23,3 +23,5 @@ DISCOVERY_UDN = "udn"
DISCOVERY_USN = "usn" DISCOVERY_USN = "usn"
CONFIG_ENTRY_UDN = "udn" CONFIG_ENTRY_UDN = "udn"
CONFIG_ENTRY_ST = "st" CONFIG_ENTRY_ST = "st"
CONFIG_ENTRY_SCAN_INTERVAL = "scan_interval"
DEFAULT_SCAN_INTERVAL = timedelta(seconds=30).seconds

View file

@ -12,15 +12,17 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from .const import ( from .const import (
BYTES_RECEIVED, BYTES_RECEIVED,
BYTES_SENT, BYTES_SENT,
CONFIG_ENTRY_SCAN_INTERVAL,
CONFIG_ENTRY_UDN,
DATA_PACKETS, DATA_PACKETS,
DATA_RATE_PACKETS_PER_SECOND, DATA_RATE_PACKETS_PER_SECOND,
DEFAULT_SCAN_INTERVAL,
DOMAIN, DOMAIN,
KIBIBYTE, KIBIBYTE,
LOGGER as _LOGGER, LOGGER as _LOGGER,
PACKETS_RECEIVED, PACKETS_RECEIVED,
PACKETS_SENT, PACKETS_SENT,
TIMESTAMP, TIMESTAMP,
UPDATE_INTERVAL,
) )
from .device import Device from .device import Device
@ -78,21 +80,24 @@ async def async_setup_entry(
) -> None: ) -> None:
"""Set up the UPnP/IGD sensors.""" """Set up the UPnP/IGD sensors."""
data = config_entry.data data = config_entry.data
if "udn" in data: if CONFIG_ENTRY_UDN in data:
udn = data["udn"] udn = data[CONFIG_ENTRY_UDN]
else: else:
# any device will do # any device will do
udn = list(hass.data[DOMAIN]["devices"].keys())[0] udn = list(hass.data[DOMAIN]["devices"].keys())[0]
device: Device = hass.data[DOMAIN]["devices"][udn] device: Device = hass.data[DOMAIN]["devices"][udn]
update_interval_sec = data.get(CONFIG_ENTRY_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
update_interval = timedelta(seconds=update_interval_sec)
_LOGGER.debug("update_interval: %s", update_interval)
_LOGGER.debug("Adding sensors") _LOGGER.debug("Adding sensors")
coordinator = DataUpdateCoordinator( coordinator = DataUpdateCoordinator(
hass, hass,
_LOGGER, _LOGGER,
name=device.name, name=device.name,
update_method=device.async_get_traffic_data, update_method=device.async_get_traffic_data,
update_interval=timedelta(seconds=UPDATE_INTERVAL.seconds), update_interval=update_interval,
) )
await coordinator.async_refresh() await coordinator.async_refresh()
@ -117,11 +122,14 @@ class UpnpSensor(Entity):
coordinator: DataUpdateCoordinator, coordinator: DataUpdateCoordinator,
device: Device, device: Device,
sensor_type: Mapping[str, str], sensor_type: Mapping[str, str],
update_multiplier: int = 2,
) -> None: ) -> None:
"""Initialize the base sensor.""" """Initialize the base sensor."""
self._coordinator = coordinator self._coordinator = coordinator
self._device = device self._device = device
self._sensor_type = sensor_type self._sensor_type = sensor_type
self._update_counter_max = update_multiplier
self._update_counter = 0
@property @property
def should_poll(self) -> bool: def should_poll(self) -> bool:

View file

@ -9,7 +9,8 @@
}, },
"user": { "user": {
"data": { "data": {
"usn": "Device" "usn": "Device",
"scan_interval": "Update interval (seconds, minimal 30)"
} }
} }
}, },

View file

@ -1,8 +1,15 @@
"""Test UPnP/IGD config flow.""" """Test UPnP/IGD config flow."""
import pytest
import voluptuous as vol
from homeassistant import config_entries, data_entry_flow from homeassistant import config_entries, data_entry_flow
from homeassistant.components import ssdp from homeassistant.components import ssdp
from homeassistant.components.upnp.const import ( from homeassistant.components.upnp.const import (
CONFIG_ENTRY_SCAN_INTERVAL,
CONFIG_ENTRY_ST,
CONFIG_ENTRY_UDN,
DEFAULT_SCAN_INTERVAL,
DISCOVERY_LOCATION, DISCOVERY_LOCATION,
DISCOVERY_ST, DISCOVERY_ST,
DISCOVERY_UDN, DISCOVERY_UDN,
@ -52,8 +59,9 @@ async def test_flow_ssdp_discovery(hass: HomeAssistantType):
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result["title"] == mock_device.name assert result["title"] == mock_device.name
assert result["data"] == { assert result["data"] == {
"st": mock_device.device_type, CONFIG_ENTRY_ST: mock_device.device_type,
"udn": mock_device.udn, CONFIG_ENTRY_UDN: mock_device.udn,
CONFIG_ENTRY_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL,
} }
@ -89,11 +97,85 @@ async def test_flow_user(hass: HomeAssistantType):
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result["title"] == mock_device.name assert result["title"] == mock_device.name
assert result["data"] == { assert result["data"] == {
"st": mock_device.device_type, CONFIG_ENTRY_ST: mock_device.device_type,
"udn": mock_device.udn, CONFIG_ENTRY_UDN: mock_device.udn,
CONFIG_ENTRY_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL,
} }
async def test_flow_user_update_interval(hass: HomeAssistantType):
"""Test config flow: discovered + configured through user with non-default scan_interval."""
udn = "uuid:device_1"
mock_device = MockDevice(udn)
usn = f"{mock_device.udn}::{mock_device.device_type}"
scan_interval = 60
discovery_infos = [
{
DISCOVERY_USN: usn,
DISCOVERY_ST: mock_device.device_type,
DISCOVERY_UDN: mock_device.udn,
DISCOVERY_LOCATION: "dummy",
}
]
with patch.object(
Device, "async_create_device", AsyncMock(return_value=mock_device)
), patch.object(Device, "async_discover", AsyncMock(return_value=discovery_infos)):
# Discovered via step user.
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "user"
# Confirmed via step user.
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={"usn": usn, CONFIG_ENTRY_SCAN_INTERVAL: scan_interval},
)
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result["title"] == mock_device.name
assert result["data"] == {
CONFIG_ENTRY_ST: mock_device.device_type,
CONFIG_ENTRY_UDN: mock_device.udn,
CONFIG_ENTRY_SCAN_INTERVAL: scan_interval,
}
async def test_flow_user_update_interval_min_30(hass: HomeAssistantType):
"""Test config flow: discovered + configured through user with non-default scan_interval."""
udn = "uuid:device_1"
mock_device = MockDevice(udn)
usn = f"{mock_device.udn}::{mock_device.device_type}"
scan_interval = 15
discovery_infos = [
{
DISCOVERY_USN: usn,
DISCOVERY_ST: mock_device.device_type,
DISCOVERY_UDN: mock_device.udn,
DISCOVERY_LOCATION: "dummy",
}
]
with patch.object(
Device, "async_create_device", AsyncMock(return_value=mock_device)
), patch.object(Device, "async_discover", AsyncMock(return_value=discovery_infos)):
# Discovered via step user.
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "user"
# Confirmed via step user.
with pytest.raises(vol.error.MultipleInvalid):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={"usn": usn, CONFIG_ENTRY_SCAN_INTERVAL: scan_interval},
)
async def test_flow_config(hass: HomeAssistantType): async def test_flow_config(hass: HomeAssistantType):
"""Test config flow: discovered + configured through configuration.yaml.""" """Test config flow: discovered + configured through configuration.yaml."""
udn = "uuid:device_1" udn = "uuid:device_1"
@ -119,6 +201,7 @@ async def test_flow_config(hass: HomeAssistantType):
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result["title"] == mock_device.name assert result["title"] == mock_device.name
assert result["data"] == { assert result["data"] == {
"st": mock_device.device_type, CONFIG_ENTRY_ST: mock_device.device_type,
"udn": mock_device.udn, CONFIG_ENTRY_UDN: mock_device.udn,
CONFIG_ENTRY_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL,
} }