mirror of
https://github.com/home-assistant/core.git
synced 2025-07-26 06:37:52 +00:00
Add upnp binary sensor for connectivity status (#54489)
* New binary sensor for connectivity * Add binary_sensor * New binary sensor for connectivity * Add binary_sensor * Handle values returned as None * Small text update for Uptime * Update homeassistant/components/upnp/binary_sensor.py Co-authored-by: Joakim Sørensen <hi@ludeeus.dev> * Update homeassistant/components/upnp/binary_sensor.py Co-authored-by: Joakim Sørensen <hi@ludeeus.dev> * Update homeassistant/components/upnp/binary_sensor.py Co-authored-by: Joakim Sørensen <hi@ludeeus.dev> * Update homeassistant/components/upnp/binary_sensor.py Co-authored-by: Joakim Sørensen <hi@ludeeus.dev> * Update homeassistant/components/upnp/binary_sensor.py Co-authored-by: Joakim Sørensen <hi@ludeeus.dev> * Update homeassistant/components/upnp/binary_sensor.py Co-authored-by: Joakim Sørensen <hi@ludeeus.dev> * Update homeassistant/components/upnp/binary_sensor.py Co-authored-by: Joakim Sørensen <hi@ludeeus.dev> * Update homeassistant/components/upnp/binary_sensor.py Co-authored-by: Joakim Sørensen <hi@ludeeus.dev> * Updates based on review * Update homeassistant/components/upnp/binary_sensor.py Co-authored-by: Joakim Sørensen <hi@ludeeus.dev> * Further updates based on review * Set device_class as a class atribute * Create 1 combined data coordinator and UpnpEntity class * Updates on coordinator * Update comment * Fix in async_step_init for coordinator * Add async_get_status to mocked device and set times polled for each call seperately * Updated to get device through coordinator Check polling for each status call seperately * Use collections.abc instead of Typing for Mapping * Remove adding device to hass.data as coordinator is now saved * Removed setting _coordinator * Added myself as codeowner * Update type in __init__ * Removed attributes from binary sensor * Fix async_unload_entry * Add expected return value to is_on Co-authored-by: Joakim Sørensen <hi@ludeeus.dev>
This commit is contained in:
parent
5b75c8254b
commit
8bf79d61ee
@ -545,7 +545,7 @@ homeassistant/components/upb/* @gwww
|
|||||||
homeassistant/components/upc_connect/* @pvizeli @fabaff
|
homeassistant/components/upc_connect/* @pvizeli @fabaff
|
||||||
homeassistant/components/upcloud/* @scop
|
homeassistant/components/upcloud/* @scop
|
||||||
homeassistant/components/updater/* @home-assistant/core
|
homeassistant/components/updater/* @home-assistant/core
|
||||||
homeassistant/components/upnp/* @StevenLooman
|
homeassistant/components/upnp/* @StevenLooman @ehendrix23
|
||||||
homeassistant/components/uptimerobot/* @ludeeus
|
homeassistant/components/uptimerobot/* @ludeeus
|
||||||
homeassistant/components/usgs_earthquakes_feed/* @exxamalte
|
homeassistant/components/usgs_earthquakes_feed/* @exxamalte
|
||||||
homeassistant/components/utility_meter/* @dgomes
|
homeassistant/components/utility_meter/* @dgomes
|
||||||
|
@ -3,6 +3,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from collections.abc import Mapping
|
from collections.abc import Mapping
|
||||||
|
from datetime import timedelta
|
||||||
from ipaddress import ip_address
|
from ipaddress import ip_address
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
@ -17,24 +18,30 @@ from homeassistant.core import HomeAssistant, callback
|
|||||||
from homeassistant.exceptions import ConfigEntryNotReady
|
from homeassistant.exceptions import ConfigEntryNotReady
|
||||||
from homeassistant.helpers import config_validation as cv, device_registry as dr
|
from homeassistant.helpers import config_validation as cv, device_registry as dr
|
||||||
from homeassistant.helpers.typing import ConfigType
|
from homeassistant.helpers.typing import ConfigType
|
||||||
|
from homeassistant.helpers.update_coordinator import (
|
||||||
|
CoordinatorEntity,
|
||||||
|
DataUpdateCoordinator,
|
||||||
|
)
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
CONF_LOCAL_IP,
|
CONF_LOCAL_IP,
|
||||||
CONFIG_ENTRY_HOSTNAME,
|
CONFIG_ENTRY_HOSTNAME,
|
||||||
|
CONFIG_ENTRY_SCAN_INTERVAL,
|
||||||
CONFIG_ENTRY_ST,
|
CONFIG_ENTRY_ST,
|
||||||
CONFIG_ENTRY_UDN,
|
CONFIG_ENTRY_UDN,
|
||||||
|
DEFAULT_SCAN_INTERVAL,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
DOMAIN_CONFIG,
|
DOMAIN_CONFIG,
|
||||||
DOMAIN_DEVICES,
|
DOMAIN_DEVICES,
|
||||||
DOMAIN_LOCAL_IP,
|
DOMAIN_LOCAL_IP,
|
||||||
LOGGER as _LOGGER,
|
LOGGER,
|
||||||
)
|
)
|
||||||
from .device import Device
|
from .device import Device
|
||||||
|
|
||||||
NOTIFICATION_ID = "upnp_notification"
|
NOTIFICATION_ID = "upnp_notification"
|
||||||
NOTIFICATION_TITLE = "UPnP/IGD Setup"
|
NOTIFICATION_TITLE = "UPnP/IGD Setup"
|
||||||
|
|
||||||
PLATFORMS = ["sensor"]
|
PLATFORMS = ["binary_sensor", "sensor"]
|
||||||
|
|
||||||
CONFIG_SCHEMA = vol.Schema(
|
CONFIG_SCHEMA = vol.Schema(
|
||||||
{
|
{
|
||||||
@ -50,7 +57,7 @@ CONFIG_SCHEMA = vol.Schema(
|
|||||||
|
|
||||||
async def async_setup(hass: HomeAssistant, config: ConfigType):
|
async def async_setup(hass: HomeAssistant, config: ConfigType):
|
||||||
"""Set up UPnP component."""
|
"""Set up UPnP component."""
|
||||||
_LOGGER.debug("async_setup, config: %s", config)
|
LOGGER.debug("async_setup, config: %s", config)
|
||||||
conf_default = CONFIG_SCHEMA({DOMAIN: {}})[DOMAIN]
|
conf_default = CONFIG_SCHEMA({DOMAIN: {}})[DOMAIN]
|
||||||
conf = config.get(DOMAIN, conf_default)
|
conf = config.get(DOMAIN, conf_default)
|
||||||
local_ip = await async_get_source_ip(hass, PUBLIC_TARGET_IP)
|
local_ip = await async_get_source_ip(hass, PUBLIC_TARGET_IP)
|
||||||
@ -73,7 +80,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType):
|
|||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
"""Set up UPnP/IGD device from a config entry."""
|
"""Set up UPnP/IGD device from a config entry."""
|
||||||
_LOGGER.debug("Setting up config entry: %s", entry.unique_id)
|
LOGGER.debug("Setting up config entry: %s", entry.unique_id)
|
||||||
|
|
||||||
udn = entry.data[CONFIG_ENTRY_UDN]
|
udn = entry.data[CONFIG_ENTRY_UDN]
|
||||||
st = entry.data[CONFIG_ENTRY_ST] # pylint: disable=invalid-name
|
st = entry.data[CONFIG_ENTRY_ST] # pylint: disable=invalid-name
|
||||||
@ -86,7 +93,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
@callback
|
@callback
|
||||||
def device_discovered(info: Mapping[str, Any]) -> None:
|
def device_discovered(info: Mapping[str, Any]) -> None:
|
||||||
nonlocal discovery_info
|
nonlocal discovery_info
|
||||||
_LOGGER.debug(
|
LOGGER.debug(
|
||||||
"Device discovered: %s, at: %s", usn, info[ssdp.ATTR_SSDP_LOCATION]
|
"Device discovered: %s, at: %s", usn, info[ssdp.ATTR_SSDP_LOCATION]
|
||||||
)
|
)
|
||||||
discovery_info = info
|
discovery_info = info
|
||||||
@ -103,7 +110,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
try:
|
try:
|
||||||
await asyncio.wait_for(device_discovered_event.wait(), timeout=10)
|
await asyncio.wait_for(device_discovered_event.wait(), timeout=10)
|
||||||
except asyncio.TimeoutError as err:
|
except asyncio.TimeoutError as err:
|
||||||
_LOGGER.debug("Device not discovered: %s", usn)
|
LOGGER.debug("Device not discovered: %s", usn)
|
||||||
raise ConfigEntryNotReady from err
|
raise ConfigEntryNotReady from err
|
||||||
finally:
|
finally:
|
||||||
cancel_discovered_callback()
|
cancel_discovered_callback()
|
||||||
@ -114,12 +121,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
]
|
]
|
||||||
device = await Device.async_create_device(hass, location)
|
device = await Device.async_create_device(hass, location)
|
||||||
|
|
||||||
# Save device.
|
|
||||||
hass.data[DOMAIN][DOMAIN_DEVICES][udn] = device
|
|
||||||
|
|
||||||
# Ensure entry has a unique_id.
|
# Ensure entry has a unique_id.
|
||||||
if not entry.unique_id:
|
if not entry.unique_id:
|
||||||
_LOGGER.debug(
|
LOGGER.debug(
|
||||||
"Setting unique_id: %s, for config_entry: %s",
|
"Setting unique_id: %s, for config_entry: %s",
|
||||||
device.unique_id,
|
device.unique_id,
|
||||||
entry,
|
entry,
|
||||||
@ -150,8 +154,24 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
model=device.model_name,
|
model=device.model_name,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
update_interval_sec = entry.options.get(
|
||||||
|
CONFIG_ENTRY_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL
|
||||||
|
)
|
||||||
|
update_interval = timedelta(seconds=update_interval_sec)
|
||||||
|
LOGGER.debug("update_interval: %s", update_interval)
|
||||||
|
coordinator = UpnpDataUpdateCoordinator(
|
||||||
|
hass,
|
||||||
|
device=device,
|
||||||
|
update_interval=update_interval,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Save coordinator.
|
||||||
|
hass.data[DOMAIN][entry.entry_id] = coordinator
|
||||||
|
|
||||||
|
await coordinator.async_config_entry_first_refresh()
|
||||||
|
|
||||||
# Create sensors.
|
# Create sensors.
|
||||||
_LOGGER.debug("Enabling sensors")
|
LOGGER.debug("Enabling sensors")
|
||||||
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
|
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
|
||||||
|
|
||||||
# Start device updater.
|
# Start device updater.
|
||||||
@ -162,14 +182,53 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
|
|
||||||
async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
|
async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
|
||||||
"""Unload a UPnP/IGD device from a config entry."""
|
"""Unload a UPnP/IGD device from a config entry."""
|
||||||
_LOGGER.debug("Unloading config entry: %s", config_entry.unique_id)
|
LOGGER.debug("Unloading config entry: %s", config_entry.unique_id)
|
||||||
|
|
||||||
udn = config_entry.data.get(CONFIG_ENTRY_UDN)
|
if coordinator := hass.data[DOMAIN].pop(config_entry.entry_id, None):
|
||||||
if udn in hass.data[DOMAIN][DOMAIN_DEVICES]:
|
await coordinator.device.async_stop()
|
||||||
device = hass.data[DOMAIN][DOMAIN_DEVICES][udn]
|
|
||||||
await device.async_stop()
|
|
||||||
|
|
||||||
del hass.data[DOMAIN][DOMAIN_DEVICES][udn]
|
LOGGER.debug("Deleting sensors")
|
||||||
|
|
||||||
_LOGGER.debug("Deleting sensors")
|
|
||||||
return await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS)
|
return await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS)
|
||||||
|
|
||||||
|
|
||||||
|
class UpnpDataUpdateCoordinator(DataUpdateCoordinator):
|
||||||
|
"""Define an object to update data from UPNP device."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, hass: HomeAssistant, device: Device, update_interval: timedelta
|
||||||
|
) -> None:
|
||||||
|
"""Initialize."""
|
||||||
|
self.device = device
|
||||||
|
|
||||||
|
super().__init__(
|
||||||
|
hass, LOGGER, name=device.name, update_interval=update_interval
|
||||||
|
)
|
||||||
|
|
||||||
|
async def _async_update_data(self) -> Mapping[str, Any]:
|
||||||
|
"""Update data."""
|
||||||
|
update_values = await asyncio.gather(
|
||||||
|
self.device.async_get_traffic_data(),
|
||||||
|
self.device.async_get_status(),
|
||||||
|
)
|
||||||
|
|
||||||
|
data = dict(update_values[0])
|
||||||
|
data.update(update_values[1])
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
class UpnpEntity(CoordinatorEntity):
|
||||||
|
"""Base class for UPnP/IGD entities."""
|
||||||
|
|
||||||
|
coordinator: UpnpDataUpdateCoordinator
|
||||||
|
|
||||||
|
def __init__(self, coordinator: UpnpDataUpdateCoordinator) -> None:
|
||||||
|
"""Initialize the base entities."""
|
||||||
|
super().__init__(coordinator)
|
||||||
|
self._device = coordinator.device
|
||||||
|
self._attr_device_info = {
|
||||||
|
"connections": {(dr.CONNECTION_UPNP, coordinator.device.udn)},
|
||||||
|
"name": coordinator.device.name,
|
||||||
|
"manufacturer": coordinator.device.manufacturer,
|
||||||
|
"model": coordinator.device.model_name,
|
||||||
|
}
|
||||||
|
54
homeassistant/components/upnp/binary_sensor.py
Normal file
54
homeassistant/components/upnp/binary_sensor.py
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
"""Support for UPnP/IGD Binary Sensors."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from homeassistant.components.binary_sensor import (
|
||||||
|
DEVICE_CLASS_CONNECTIVITY,
|
||||||
|
BinarySensorEntity,
|
||||||
|
)
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
|
from . import UpnpDataUpdateCoordinator, UpnpEntity
|
||||||
|
from .const import DOMAIN, LOGGER, WANSTATUS
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config_entry: ConfigEntry,
|
||||||
|
async_add_entities: AddEntitiesCallback,
|
||||||
|
) -> None:
|
||||||
|
"""Set up the UPnP/IGD sensors."""
|
||||||
|
coordinator = hass.data[DOMAIN][config_entry.entry_id]
|
||||||
|
|
||||||
|
LOGGER.debug("Adding binary sensor")
|
||||||
|
|
||||||
|
sensors = [
|
||||||
|
UpnpStatusBinarySensor(coordinator),
|
||||||
|
]
|
||||||
|
async_add_entities(sensors)
|
||||||
|
|
||||||
|
|
||||||
|
class UpnpStatusBinarySensor(UpnpEntity, BinarySensorEntity):
|
||||||
|
"""Class for UPnP/IGD binary sensors."""
|
||||||
|
|
||||||
|
_attr_device_class = DEVICE_CLASS_CONNECTIVITY
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
coordinator: UpnpDataUpdateCoordinator,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize the base sensor."""
|
||||||
|
super().__init__(coordinator)
|
||||||
|
self._attr_name = f"{coordinator.device.name} wan status"
|
||||||
|
self._attr_unique_id = f"{coordinator.device.udn}_wanstatus"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def available(self) -> bool:
|
||||||
|
"""Return if entity is available."""
|
||||||
|
return super().available and self.coordinator.data.get(WANSTATUS)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_on(self) -> bool:
|
||||||
|
"""Return true if the binary sensor is on."""
|
||||||
|
return self.coordinator.data[WANSTATUS] == "Connected"
|
@ -20,8 +20,7 @@ from .const import (
|
|||||||
CONFIG_ENTRY_UDN,
|
CONFIG_ENTRY_UDN,
|
||||||
DEFAULT_SCAN_INTERVAL,
|
DEFAULT_SCAN_INTERVAL,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
DOMAIN_DEVICES,
|
LOGGER,
|
||||||
LOGGER as _LOGGER,
|
|
||||||
SSDP_SEARCH_TIMEOUT,
|
SSDP_SEARCH_TIMEOUT,
|
||||||
ST_IGD_V1,
|
ST_IGD_V1,
|
||||||
ST_IGD_V2,
|
ST_IGD_V2,
|
||||||
@ -43,7 +42,7 @@ async def _async_wait_for_discoveries(hass: HomeAssistant) -> bool:
|
|||||||
|
|
||||||
@callback
|
@callback
|
||||||
def device_discovered(info: Mapping[str, Any]) -> None:
|
def device_discovered(info: Mapping[str, Any]) -> None:
|
||||||
_LOGGER.info(
|
LOGGER.info(
|
||||||
"Device discovered: %s, at: %s",
|
"Device discovered: %s, at: %s",
|
||||||
info[ssdp.ATTR_SSDP_USN],
|
info[ssdp.ATTR_SSDP_USN],
|
||||||
info[ssdp.ATTR_SSDP_LOCATION],
|
info[ssdp.ATTR_SSDP_LOCATION],
|
||||||
@ -103,7 +102,7 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
self, user_input: Mapping | None = None
|
self, user_input: Mapping | None = None
|
||||||
) -> Mapping[str, Any]:
|
) -> Mapping[str, Any]:
|
||||||
"""Handle a flow start."""
|
"""Handle a flow start."""
|
||||||
_LOGGER.debug("async_step_user: user_input: %s", user_input)
|
LOGGER.debug("async_step_user: user_input: %s", user_input)
|
||||||
|
|
||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
# Ensure wanted device was discovered.
|
# Ensure wanted device was discovered.
|
||||||
@ -162,12 +161,12 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
configured before, find any device and create a config_entry for it.
|
configured before, find any device and create a config_entry for it.
|
||||||
Otherwise, do nothing.
|
Otherwise, do nothing.
|
||||||
"""
|
"""
|
||||||
_LOGGER.debug("async_step_import: import_info: %s", import_info)
|
LOGGER.debug("async_step_import: import_info: %s", import_info)
|
||||||
|
|
||||||
# Landed here via configuration.yaml entry.
|
# Landed here via configuration.yaml entry.
|
||||||
# Any device already added, then abort.
|
# Any device already added, then abort.
|
||||||
if self._async_current_entries():
|
if self._async_current_entries():
|
||||||
_LOGGER.debug("Already configured, aborting")
|
LOGGER.debug("Already configured, aborting")
|
||||||
return self.async_abort(reason="already_configured")
|
return self.async_abort(reason="already_configured")
|
||||||
|
|
||||||
# Discover devices.
|
# Discover devices.
|
||||||
@ -176,7 +175,7 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
|
|
||||||
# Ensure anything to add. If not, silently abort.
|
# Ensure anything to add. If not, silently abort.
|
||||||
if not discoveries:
|
if not discoveries:
|
||||||
_LOGGER.info("No UPnP devices discovered, aborting")
|
LOGGER.info("No UPnP devices discovered, aborting")
|
||||||
return self.async_abort(reason="no_devices_found")
|
return self.async_abort(reason="no_devices_found")
|
||||||
|
|
||||||
# Ensure complete discovery.
|
# Ensure complete discovery.
|
||||||
@ -187,7 +186,7 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
or ssdp.ATTR_SSDP_LOCATION not in discovery
|
or ssdp.ATTR_SSDP_LOCATION not in discovery
|
||||||
or ssdp.ATTR_SSDP_USN not in discovery
|
or ssdp.ATTR_SSDP_USN not in discovery
|
||||||
):
|
):
|
||||||
_LOGGER.debug("Incomplete discovery, ignoring")
|
LOGGER.debug("Incomplete discovery, ignoring")
|
||||||
return self.async_abort(reason="incomplete_discovery")
|
return self.async_abort(reason="incomplete_discovery")
|
||||||
|
|
||||||
# Ensure not already configuring/configured.
|
# Ensure not already configuring/configured.
|
||||||
@ -202,7 +201,7 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
This flow is triggered by the SSDP component. It will check if the
|
This flow is triggered by the SSDP component. It will check if the
|
||||||
host is already configured and delegate to the import step if not.
|
host is already configured and delegate to the import step if not.
|
||||||
"""
|
"""
|
||||||
_LOGGER.debug("async_step_ssdp: discovery_info: %s", discovery_info)
|
LOGGER.debug("async_step_ssdp: discovery_info: %s", discovery_info)
|
||||||
|
|
||||||
# Ensure complete discovery.
|
# Ensure complete discovery.
|
||||||
if (
|
if (
|
||||||
@ -211,7 +210,7 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
or ssdp.ATTR_SSDP_LOCATION not in discovery_info
|
or ssdp.ATTR_SSDP_LOCATION not in discovery_info
|
||||||
or ssdp.ATTR_SSDP_USN not in discovery_info
|
or ssdp.ATTR_SSDP_USN not in discovery_info
|
||||||
):
|
):
|
||||||
_LOGGER.debug("Incomplete discovery, ignoring")
|
LOGGER.debug("Incomplete discovery, ignoring")
|
||||||
return self.async_abort(reason="incomplete_discovery")
|
return self.async_abort(reason="incomplete_discovery")
|
||||||
|
|
||||||
# Ensure not already configuring/configured.
|
# Ensure not already configuring/configured.
|
||||||
@ -225,7 +224,7 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
for config_entry in existing_entries:
|
for config_entry in existing_entries:
|
||||||
entry_hostname = config_entry.data.get(CONFIG_ENTRY_HOSTNAME)
|
entry_hostname = config_entry.data.get(CONFIG_ENTRY_HOSTNAME)
|
||||||
if entry_hostname == hostname:
|
if entry_hostname == hostname:
|
||||||
_LOGGER.debug(
|
LOGGER.debug(
|
||||||
"Found existing config_entry with same hostname, discovery ignored"
|
"Found existing config_entry with same hostname, discovery ignored"
|
||||||
)
|
)
|
||||||
return self.async_abort(reason="discovery_ignored")
|
return self.async_abort(reason="discovery_ignored")
|
||||||
@ -244,7 +243,7 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
self, user_input: Mapping | None = None
|
self, user_input: Mapping | None = None
|
||||||
) -> Mapping[str, Any]:
|
) -> Mapping[str, Any]:
|
||||||
"""Confirm integration via SSDP."""
|
"""Confirm integration via SSDP."""
|
||||||
_LOGGER.debug("async_step_ssdp_confirm: user_input: %s", user_input)
|
LOGGER.debug("async_step_ssdp_confirm: user_input: %s", user_input)
|
||||||
if user_input is None:
|
if user_input is None:
|
||||||
return self.async_show_form(step_id="ssdp_confirm")
|
return self.async_show_form(step_id="ssdp_confirm")
|
||||||
|
|
||||||
@ -264,7 +263,7 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
discovery: Mapping,
|
discovery: Mapping,
|
||||||
) -> Mapping[str, Any]:
|
) -> Mapping[str, Any]:
|
||||||
"""Create an entry from discovery."""
|
"""Create an entry from discovery."""
|
||||||
_LOGGER.debug(
|
LOGGER.debug(
|
||||||
"_async_create_entry_from_discovery: discovery: %s",
|
"_async_create_entry_from_discovery: discovery: %s",
|
||||||
discovery,
|
discovery,
|
||||||
)
|
)
|
||||||
@ -288,13 +287,12 @@ class UpnpOptionsFlowHandler(config_entries.OptionsFlow):
|
|||||||
async def async_step_init(self, user_input: Mapping = None) -> None:
|
async def async_step_init(self, user_input: Mapping = None) -> None:
|
||||||
"""Manage the options."""
|
"""Manage the options."""
|
||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
udn = self.config_entry.data[CONFIG_ENTRY_UDN]
|
coordinator = self.hass.data[DOMAIN][self.config_entry.entry_id]
|
||||||
coordinator = self.hass.data[DOMAIN][DOMAIN_DEVICES][udn].coordinator
|
|
||||||
update_interval_sec = user_input.get(
|
update_interval_sec = user_input.get(
|
||||||
CONFIG_ENTRY_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL
|
CONFIG_ENTRY_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL
|
||||||
)
|
)
|
||||||
update_interval = timedelta(seconds=update_interval_sec)
|
update_interval = timedelta(seconds=update_interval_sec)
|
||||||
_LOGGER.debug("Updating coordinator, update_interval: %s", update_interval)
|
LOGGER.debug("Updating coordinator, update_interval: %s", update_interval)
|
||||||
coordinator.update_interval = update_interval
|
coordinator.update_interval = update_interval
|
||||||
return self.async_create_entry(title="", data=user_input)
|
return self.async_create_entry(title="", data=user_input)
|
||||||
|
|
||||||
|
@ -18,6 +18,9 @@ PACKETS_SENT = "packets_sent"
|
|||||||
TIMESTAMP = "timestamp"
|
TIMESTAMP = "timestamp"
|
||||||
DATA_PACKETS = "packets"
|
DATA_PACKETS = "packets"
|
||||||
DATA_RATE_PACKETS_PER_SECOND = f"{DATA_PACKETS}/{TIME_SECONDS}"
|
DATA_RATE_PACKETS_PER_SECOND = f"{DATA_PACKETS}/{TIME_SECONDS}"
|
||||||
|
WANSTATUS = "wan_status"
|
||||||
|
WANIP = "wan_ip"
|
||||||
|
UPTIME = "uptime"
|
||||||
KIBIBYTE = 1024
|
KIBIBYTE = 1024
|
||||||
UPDATE_INTERVAL = timedelta(seconds=30)
|
UPDATE_INTERVAL = timedelta(seconds=30)
|
||||||
CONFIG_ENTRY_SCAN_INTERVAL = "scan_interval"
|
CONFIG_ENTRY_SCAN_INTERVAL = "scan_interval"
|
||||||
|
@ -27,6 +27,9 @@ from .const import (
|
|||||||
PACKETS_RECEIVED,
|
PACKETS_RECEIVED,
|
||||||
PACKETS_SENT,
|
PACKETS_SENT,
|
||||||
TIMESTAMP,
|
TIMESTAMP,
|
||||||
|
UPTIME,
|
||||||
|
WANIP,
|
||||||
|
WANSTATUS,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -154,3 +157,18 @@ class Device:
|
|||||||
PACKETS_RECEIVED: values[2],
|
PACKETS_RECEIVED: values[2],
|
||||||
PACKETS_SENT: values[3],
|
PACKETS_SENT: values[3],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async def async_get_status(self) -> Mapping[str, Any]:
|
||||||
|
"""Get connection status, uptime, and external IP."""
|
||||||
|
_LOGGER.debug("Getting status for device: %s", self)
|
||||||
|
|
||||||
|
values = await asyncio.gather(
|
||||||
|
self._igd_device.async_get_status_info(),
|
||||||
|
self._igd_device.async_get_external_ip_address(),
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
WANSTATUS: values[0][0] if values[0] is not None else None,
|
||||||
|
UPTIME: values[0][2] if values[0] is not None else None,
|
||||||
|
WANIP: values[1],
|
||||||
|
}
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/upnp",
|
"documentation": "https://www.home-assistant.io/integrations/upnp",
|
||||||
"requirements": ["async-upnp-client==0.19.2"],
|
"requirements": ["async-upnp-client==0.19.2"],
|
||||||
"dependencies": ["network", "ssdp"],
|
"dependencies": ["network", "ssdp"],
|
||||||
"codeowners": ["@StevenLooman"],
|
"codeowners": ["@StevenLooman","@ehendrix23"],
|
||||||
"ssdp": [
|
"ssdp": [
|
||||||
{
|
{
|
||||||
"st": "urn:schemas-upnp-org:device:InternetGatewayDevice:1"
|
"st": "urn:schemas-upnp-org:device:InternetGatewayDevice:1"
|
||||||
|
@ -1,38 +1,25 @@
|
|||||||
"""Support for UPnP/IGD Sensors."""
|
"""Support for UPnP/IGD Sensors."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import timedelta
|
|
||||||
from typing import Any, Mapping
|
|
||||||
|
|
||||||
from homeassistant.components.sensor import SensorEntity
|
from homeassistant.components.sensor import SensorEntity
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import DATA_BYTES, DATA_RATE_KIBIBYTES_PER_SECOND
|
from homeassistant.const import DATA_BYTES, DATA_RATE_KIBIBYTES_PER_SECOND
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers import device_registry as dr
|
|
||||||
from homeassistant.helpers.entity import DeviceInfo
|
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.helpers.update_coordinator import (
|
|
||||||
CoordinatorEntity,
|
|
||||||
DataUpdateCoordinator,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
from . import UpnpDataUpdateCoordinator, UpnpEntity
|
||||||
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,
|
||||||
DOMAIN_DEVICES,
|
|
||||||
KIBIBYTE,
|
KIBIBYTE,
|
||||||
LOGGER as _LOGGER,
|
LOGGER,
|
||||||
PACKETS_RECEIVED,
|
PACKETS_RECEIVED,
|
||||||
PACKETS_SENT,
|
PACKETS_SENT,
|
||||||
TIMESTAMP,
|
TIMESTAMP,
|
||||||
)
|
)
|
||||||
from .device import Device
|
|
||||||
|
|
||||||
SENSOR_TYPES = {
|
SENSOR_TYPES = {
|
||||||
BYTES_RECEIVED: {
|
BYTES_RECEIVED: {
|
||||||
@ -78,7 +65,7 @@ async def async_setup_platform(
|
|||||||
hass: HomeAssistant, config, async_add_entities, discovery_info=None
|
hass: HomeAssistant, config, async_add_entities, discovery_info=None
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Old way of setting up UPnP/IGD sensors."""
|
"""Old way of setting up UPnP/IGD sensors."""
|
||||||
_LOGGER.debug(
|
LOGGER.debug(
|
||||||
"async_setup_platform: config: %s, discovery: %s", config, discovery_info
|
"async_setup_platform: config: %s, discovery: %s", config, discovery_info
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -89,52 +76,36 @@ async def async_setup_entry(
|
|||||||
async_add_entities: AddEntitiesCallback,
|
async_add_entities: AddEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up the UPnP/IGD sensors."""
|
"""Set up the UPnP/IGD sensors."""
|
||||||
udn = config_entry.data[CONFIG_ENTRY_UDN]
|
coordinator = hass.data[DOMAIN][config_entry.entry_id]
|
||||||
device: Device = hass.data[DOMAIN][DOMAIN_DEVICES][udn]
|
|
||||||
|
|
||||||
update_interval_sec = config_entry.options.get(
|
LOGGER.debug("Adding sensors")
|
||||||
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")
|
|
||||||
coordinator = DataUpdateCoordinator[Mapping[str, Any]](
|
|
||||||
hass,
|
|
||||||
_LOGGER,
|
|
||||||
name=device.name,
|
|
||||||
update_method=device.async_get_traffic_data,
|
|
||||||
update_interval=update_interval,
|
|
||||||
)
|
|
||||||
device.coordinator = coordinator
|
|
||||||
|
|
||||||
await coordinator.async_refresh()
|
|
||||||
|
|
||||||
sensors = [
|
sensors = [
|
||||||
RawUpnpSensor(coordinator, device, SENSOR_TYPES[BYTES_RECEIVED]),
|
RawUpnpSensor(coordinator, SENSOR_TYPES[BYTES_RECEIVED]),
|
||||||
RawUpnpSensor(coordinator, device, SENSOR_TYPES[BYTES_SENT]),
|
RawUpnpSensor(coordinator, SENSOR_TYPES[BYTES_SENT]),
|
||||||
RawUpnpSensor(coordinator, device, SENSOR_TYPES[PACKETS_RECEIVED]),
|
RawUpnpSensor(coordinator, SENSOR_TYPES[PACKETS_RECEIVED]),
|
||||||
RawUpnpSensor(coordinator, device, SENSOR_TYPES[PACKETS_SENT]),
|
RawUpnpSensor(coordinator, SENSOR_TYPES[PACKETS_SENT]),
|
||||||
DerivedUpnpSensor(coordinator, device, SENSOR_TYPES[BYTES_RECEIVED]),
|
DerivedUpnpSensor(coordinator, SENSOR_TYPES[BYTES_RECEIVED]),
|
||||||
DerivedUpnpSensor(coordinator, device, SENSOR_TYPES[BYTES_SENT]),
|
DerivedUpnpSensor(coordinator, SENSOR_TYPES[BYTES_SENT]),
|
||||||
DerivedUpnpSensor(coordinator, device, SENSOR_TYPES[PACKETS_RECEIVED]),
|
DerivedUpnpSensor(coordinator, SENSOR_TYPES[PACKETS_RECEIVED]),
|
||||||
DerivedUpnpSensor(coordinator, device, SENSOR_TYPES[PACKETS_SENT]),
|
DerivedUpnpSensor(coordinator, SENSOR_TYPES[PACKETS_SENT]),
|
||||||
]
|
]
|
||||||
async_add_entities(sensors, True)
|
async_add_entities(sensors)
|
||||||
|
|
||||||
|
|
||||||
class UpnpSensor(CoordinatorEntity, SensorEntity):
|
class UpnpSensor(UpnpEntity, SensorEntity):
|
||||||
"""Base class for UPnP/IGD sensors."""
|
"""Base class for UPnP/IGD sensors."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
coordinator: DataUpdateCoordinator[Mapping[str, Any]],
|
coordinator: UpnpDataUpdateCoordinator,
|
||||||
device: Device,
|
sensor_type: dict[str, str],
|
||||||
sensor_type: Mapping[str, str],
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize the base sensor."""
|
"""Initialize the base sensor."""
|
||||||
super().__init__(coordinator)
|
super().__init__(coordinator)
|
||||||
self._device = device
|
|
||||||
self._sensor_type = sensor_type
|
self._sensor_type = sensor_type
|
||||||
|
self._attr_name = f"{coordinator.device.name} {sensor_type['name']}"
|
||||||
|
self._attr_unique_id = f"{coordinator.device.udn}_{sensor_type['unique_id']}"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def icon(self) -> str:
|
def icon(self) -> str:
|
||||||
@ -144,37 +115,15 @@ class UpnpSensor(CoordinatorEntity, SensorEntity):
|
|||||||
@property
|
@property
|
||||||
def available(self) -> bool:
|
def available(self) -> bool:
|
||||||
"""Return if entity is available."""
|
"""Return if entity is available."""
|
||||||
device_value_key = self._sensor_type["device_value_key"]
|
return super().available and self.coordinator.data.get(
|
||||||
return (
|
self._sensor_type["device_value_key"]
|
||||||
self.coordinator.last_update_success
|
|
||||||
and device_value_key in self.coordinator.data
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
|
||||||
def name(self) -> str:
|
|
||||||
"""Return the name of the sensor."""
|
|
||||||
return f"{self._device.name} {self._sensor_type['name']}"
|
|
||||||
|
|
||||||
@property
|
|
||||||
def unique_id(self) -> str:
|
|
||||||
"""Return an unique ID."""
|
|
||||||
return f"{self._device.udn}_{self._sensor_type['unique_id']}"
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def native_unit_of_measurement(self) -> str:
|
def native_unit_of_measurement(self) -> str:
|
||||||
"""Return the unit of measurement of this entity, if any."""
|
"""Return the unit of measurement of this entity, if any."""
|
||||||
return self._sensor_type["unit"]
|
return self._sensor_type["unit"]
|
||||||
|
|
||||||
@property
|
|
||||||
def device_info(self) -> DeviceInfo:
|
|
||||||
"""Get device info."""
|
|
||||||
return {
|
|
||||||
"connections": {(dr.CONNECTION_UPNP, self._device.udn)},
|
|
||||||
"name": self._device.name,
|
|
||||||
"manufacturer": self._device.manufacturer,
|
|
||||||
"model": self._device.model_name,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class RawUpnpSensor(UpnpSensor):
|
class RawUpnpSensor(UpnpSensor):
|
||||||
"""Representation of a UPnP/IGD sensor."""
|
"""Representation of a UPnP/IGD sensor."""
|
||||||
@ -192,21 +141,15 @@ class RawUpnpSensor(UpnpSensor):
|
|||||||
class DerivedUpnpSensor(UpnpSensor):
|
class DerivedUpnpSensor(UpnpSensor):
|
||||||
"""Representation of a UNIT Sent/Received per second sensor."""
|
"""Representation of a UNIT Sent/Received per second sensor."""
|
||||||
|
|
||||||
def __init__(self, coordinator, device, sensor_type) -> None:
|
def __init__(self, coordinator: UpnpDataUpdateCoordinator, sensor_type) -> None:
|
||||||
"""Initialize sensor."""
|
"""Initialize sensor."""
|
||||||
super().__init__(coordinator, device, sensor_type)
|
super().__init__(coordinator, sensor_type)
|
||||||
self._last_value = None
|
self._last_value = None
|
||||||
self._last_timestamp = None
|
self._last_timestamp = None
|
||||||
|
self._attr_name = f"{coordinator.device.name} {sensor_type['derived_name']}"
|
||||||
@property
|
self._attr_unique_id = (
|
||||||
def name(self) -> str:
|
f"{coordinator.device.udn}_{sensor_type['derived_unique_id']}"
|
||||||
"""Return the name of the sensor."""
|
)
|
||||||
return f"{self._device.name} {self._sensor_type['derived_name']}"
|
|
||||||
|
|
||||||
@property
|
|
||||||
def unique_id(self) -> str:
|
|
||||||
"""Return an unique ID."""
|
|
||||||
return f"{self._device.udn}_{self._sensor_type['derived_unique_id']}"
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def native_unit_of_measurement(self) -> str:
|
def native_unit_of_measurement(self) -> str:
|
||||||
|
@ -11,6 +11,9 @@ from homeassistant.components.upnp.const import (
|
|||||||
PACKETS_RECEIVED,
|
PACKETS_RECEIVED,
|
||||||
PACKETS_SENT,
|
PACKETS_SENT,
|
||||||
TIMESTAMP,
|
TIMESTAMP,
|
||||||
|
UPTIME,
|
||||||
|
WANIP,
|
||||||
|
WANSTATUS,
|
||||||
)
|
)
|
||||||
from homeassistant.components.upnp.device import Device
|
from homeassistant.components.upnp.device import Device
|
||||||
from homeassistant.util import dt
|
from homeassistant.util import dt
|
||||||
@ -27,7 +30,8 @@ class MockDevice(Device):
|
|||||||
mock_device_updater = AsyncMock()
|
mock_device_updater = AsyncMock()
|
||||||
super().__init__(igd_device, mock_device_updater)
|
super().__init__(igd_device, mock_device_updater)
|
||||||
self._udn = udn
|
self._udn = udn
|
||||||
self.times_polled = 0
|
self.traffic_times_polled = 0
|
||||||
|
self.status_times_polled = 0
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def async_create_device(cls, hass, ssdp_location) -> "MockDevice":
|
async def async_create_device(cls, hass, ssdp_location) -> "MockDevice":
|
||||||
@ -66,7 +70,7 @@ class MockDevice(Device):
|
|||||||
|
|
||||||
async def async_get_traffic_data(self) -> Mapping[str, Any]:
|
async def async_get_traffic_data(self) -> Mapping[str, Any]:
|
||||||
"""Get traffic data."""
|
"""Get traffic data."""
|
||||||
self.times_polled += 1
|
self.traffic_times_polled += 1
|
||||||
return {
|
return {
|
||||||
TIMESTAMP: dt.utcnow(),
|
TIMESTAMP: dt.utcnow(),
|
||||||
BYTES_RECEIVED: 0,
|
BYTES_RECEIVED: 0,
|
||||||
@ -75,6 +79,15 @@ class MockDevice(Device):
|
|||||||
PACKETS_SENT: 0,
|
PACKETS_SENT: 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async def async_get_status(self) -> Mapping[str, Any]:
|
||||||
|
"""Get connection status, uptime, and external IP."""
|
||||||
|
self.status_times_polled += 1
|
||||||
|
return {
|
||||||
|
WANSTATUS: "Connected",
|
||||||
|
UPTIME: 0,
|
||||||
|
WANIP: "192.168.0.1",
|
||||||
|
}
|
||||||
|
|
||||||
async def async_start(self) -> None:
|
async def async_start(self) -> None:
|
||||||
"""Start the device updater."""
|
"""Start the device updater."""
|
||||||
|
|
||||||
|
@ -14,7 +14,6 @@ from homeassistant.components.upnp.const import (
|
|||||||
CONFIG_ENTRY_UDN,
|
CONFIG_ENTRY_UDN,
|
||||||
DEFAULT_SCAN_INTERVAL,
|
DEFAULT_SCAN_INTERVAL,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
DOMAIN_DEVICES,
|
|
||||||
)
|
)
|
||||||
from homeassistant.core import CoreState, HomeAssistant
|
from homeassistant.core import CoreState, HomeAssistant
|
||||||
from homeassistant.setup import async_setup_component
|
from homeassistant.setup import async_setup_component
|
||||||
@ -238,15 +237,17 @@ async def test_options_flow(hass: HomeAssistant):
|
|||||||
config_entry.add_to_hass(hass)
|
config_entry.add_to_hass(hass)
|
||||||
assert await hass.config_entries.async_setup(config_entry.entry_id) is True
|
assert await hass.config_entries.async_setup(config_entry.entry_id) is True
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
mock_device = hass.data[DOMAIN][DOMAIN_DEVICES][TEST_UDN]
|
mock_device = hass.data[DOMAIN][config_entry.entry_id].device
|
||||||
|
|
||||||
# Reset.
|
# Reset.
|
||||||
mock_device.times_polled = 0
|
mock_device.traffic_times_polled = 0
|
||||||
|
mock_device.status_times_polled = 0
|
||||||
|
|
||||||
# Forward time, ensure single poll after 30 (default) seconds.
|
# Forward time, ensure single poll after 30 (default) seconds.
|
||||||
async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=31))
|
async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=31))
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
assert mock_device.times_polled == 1
|
assert mock_device.traffic_times_polled == 1
|
||||||
|
assert mock_device.status_times_polled == 1
|
||||||
|
|
||||||
# Options flow with no input results in form.
|
# Options flow with no input results in form.
|
||||||
result = await hass.config_entries.options.async_init(
|
result = await hass.config_entries.options.async_init(
|
||||||
@ -267,15 +268,18 @@ async def test_options_flow(hass: HomeAssistant):
|
|||||||
# Forward time, ensure single poll after 60 seconds, still from original setting.
|
# Forward time, ensure single poll after 60 seconds, still from original setting.
|
||||||
async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=61))
|
async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=61))
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
assert mock_device.times_polled == 2
|
assert mock_device.traffic_times_polled == 2
|
||||||
|
assert mock_device.status_times_polled == 2
|
||||||
|
|
||||||
# Now the updated interval takes effect.
|
# Now the updated interval takes effect.
|
||||||
# Forward time, ensure single poll after 120 seconds.
|
# Forward time, ensure single poll after 120 seconds.
|
||||||
async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=121))
|
async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=121))
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
assert mock_device.times_polled == 3
|
assert mock_device.traffic_times_polled == 3
|
||||||
|
assert mock_device.status_times_polled == 3
|
||||||
|
|
||||||
# Forward time, ensure single poll after 180 seconds.
|
# Forward time, ensure single poll after 180 seconds.
|
||||||
async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=181))
|
async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=181))
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
assert mock_device.times_polled == 4
|
assert mock_device.traffic_times_polled == 4
|
||||||
|
assert mock_device.status_times_polled == 4
|
||||||
|
Loading…
x
Reference in New Issue
Block a user