Add consider_home option to Fritz device_tracker (#50741)

Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
Simone Chemelli 2021-05-24 16:54:57 +02:00 committed by GitHub
parent 2ae91bf0ea
commit 987e8ed5ed
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 140 additions and 19 deletions

View File

@ -34,7 +34,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
try: try:
await fritz_tools.async_setup() await fritz_tools.async_setup()
await fritz_tools.async_start() await fritz_tools.async_start(entry.options)
except FritzSecurityError as ex: except FritzSecurityError as ex:
raise ConfigEntryAuthFailed from ex raise ConfigEntryAuthFailed from ex
except FritzConnectionException as ex: except FritzConnectionException as ex:
@ -53,6 +53,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
entry.async_on_unload( entry.async_on_unload(
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_unload) hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_unload)
) )
entry.async_on_unload(entry.add_update_listener(update_listener))
# Load the other platforms like switch # Load the other platforms like switch
hass.config_entries.async_setup_platforms(entry, PLATFORMS) hass.config_entries.async_setup_platforms(entry, PLATFORMS)
@ -79,3 +81,9 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
await async_unload_services(hass) await async_unload_services(hass)
return unload_ok return unload_ok
async def update_listener(hass: HomeAssistant, entry: ConfigEntry):
"""Update when config_entry options update."""
if entry.options:
await hass.config_entries.async_reload(entry.entry_id)

View File

@ -16,6 +16,7 @@ from fritzconnection.core.exceptions import (
from fritzconnection.lib.fritzhosts import FritzHosts from fritzconnection.lib.fritzhosts import FritzHosts
from fritzconnection.lib.fritzstatus import FritzStatus from fritzconnection.lib.fritzstatus import FritzStatus
from homeassistant.components.device_tracker.const import CONF_CONSIDER_HOME
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
@ -59,6 +60,7 @@ class FritzBoxTools:
"""Initialize FritzboxTools class.""" """Initialize FritzboxTools class."""
self._cancel_scan = None self._cancel_scan = None
self._devices: dict[str, Any] = {} self._devices: dict[str, Any] = {}
self._options = None
self._unique_id = None self._unique_id = None
self.connection = None self.connection = None
self.fritz_hosts = None self.fritz_hosts = None
@ -95,10 +97,10 @@ class FritzBoxTools:
self.sw_version = info.get("NewSoftwareVersion") self.sw_version = info.get("NewSoftwareVersion")
self.mac = self.unique_id self.mac = self.unique_id
async def async_start(self): async def async_start(self, options):
"""Start FritzHosts connection.""" """Start FritzHosts connection."""
self.fritz_hosts = FritzHosts(fc=self.connection) self.fritz_hosts = FritzHosts(fc=self.connection)
self._options = options
await self.hass.async_add_executor_job(self.scan_devices) await self.hass.async_add_executor_job(self.scan_devices)
self._cancel_scan = async_track_time_interval( self._cancel_scan = async_track_time_interval(
@ -141,6 +143,8 @@ class FritzBoxTools:
"""Scan for new devices and return a list of found device ids.""" """Scan for new devices and return a list of found device ids."""
_LOGGER.debug("Checking devices for FRITZ!Box router %s", self.host) _LOGGER.debug("Checking devices for FRITZ!Box router %s", self.host)
consider_home = self._options[CONF_CONSIDER_HOME]
new_device = False new_device = False
for known_host in self._update_info(): for known_host in self._update_info():
if not known_host.get("mac"): if not known_host.get("mac"):
@ -154,10 +158,10 @@ class FritzBoxTools:
dev_info = Device(dev_mac, dev_ip, dev_name) dev_info = Device(dev_mac, dev_ip, dev_name)
if dev_mac in self._devices: if dev_mac in self._devices:
self._devices[dev_mac].update(dev_info, dev_home) self._devices[dev_mac].update(dev_info, dev_home, consider_home)
else: else:
device = FritzDevice(dev_mac) device = FritzDevice(dev_mac)
device.update(dev_info, dev_home) device.update(dev_info, dev_home, consider_home)
self._devices[dev_mac] = device self._devices[dev_mac] = device
new_device = True new_device = True
@ -204,19 +208,25 @@ class FritzDevice:
self._last_activity = None self._last_activity = None
self._connected = False self._connected = False
def update(self, dev_info, dev_home): def update(self, dev_info, dev_home, consider_home):
"""Update device info.""" """Update device info."""
utc_point_in_time = dt_util.utcnow() utc_point_in_time = dt_util.utcnow()
if not self._name: if not self._name:
self._name = dev_info.name or self._mac.replace(":", "_") self._name = dev_info.name or self._mac.replace(":", "_")
self._connected = dev_home
if not self._connected: if not dev_home and self._last_activity:
self._connected = (
utc_point_in_time - self._last_activity
).total_seconds() < consider_home
else:
self._connected = dev_home
if self._connected:
self._ip_address = dev_info.ip_address
self._last_activity = utc_point_in_time
else:
self._ip_address = None self._ip_address = None
return
self._last_activity = utc_point_in_time
self._ip_address = dev_info.ip_address
@property @property
def is_connected(self): def is_connected(self):

View File

@ -2,19 +2,25 @@
from __future__ import annotations from __future__ import annotations
import logging import logging
from typing import Any
from urllib.parse import urlparse from urllib.parse import urlparse
from fritzconnection.core.exceptions import FritzConnectionException, FritzSecurityError from fritzconnection.core.exceptions import FritzConnectionException, FritzSecurityError
import voluptuous as vol import voluptuous as vol
from homeassistant.components.device_tracker.const import (
CONF_CONSIDER_HOME,
DEFAULT_CONSIDER_HOME,
)
from homeassistant.components.ssdp import ( from homeassistant.components.ssdp import (
ATTR_SSDP_LOCATION, ATTR_SSDP_LOCATION,
ATTR_UPNP_FRIENDLY_NAME, ATTR_UPNP_FRIENDLY_NAME,
ATTR_UPNP_UDN, ATTR_UPNP_UDN,
) )
from homeassistant.config_entries import ConfigEntry, ConfigFlow from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.data_entry_flow import FlowResult
from .common import FritzBoxTools from .common import FritzBoxTools
from .const import ( from .const import (
@ -34,6 +40,12 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN):
VERSION = 1 VERSION = 1
@staticmethod
@callback
def async_get_options_flow(config_entry):
"""Get the options flow for this handler."""
return FritzBoxToolsOptionsFlowHandler(config_entry)
def __init__(self): def __init__(self):
"""Initialize FRITZ!Box Tools flow.""" """Initialize FRITZ!Box Tools flow."""
self._host = None self._host = None
@ -85,6 +97,9 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN):
CONF_PORT: self.fritz_tools.port, CONF_PORT: self.fritz_tools.port,
CONF_USERNAME: self.fritz_tools.username, CONF_USERNAME: self.fritz_tools.username,
}, },
options={
CONF_CONSIDER_HOME: DEFAULT_CONSIDER_HOME.total_seconds(),
},
) )
async def async_step_ssdp(self, discovery_info): async def async_step_ssdp(self, discovery_info):
@ -244,3 +259,31 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN):
CONF_PORT: import_config.get(CONF_PORT, DEFAULT_PORT), CONF_PORT: import_config.get(CONF_PORT, DEFAULT_PORT),
} }
) )
class FritzBoxToolsOptionsFlowHandler(OptionsFlow):
"""Handle a option flow."""
def __init__(self, config_entry: ConfigEntry) -> None:
"""Initialize options flow."""
self.config_entry = config_entry
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle options flow."""
if user_input is not None:
return self.async_create_entry(title="", data=user_input)
data_schema = vol.Schema(
{
vol.Optional(
CONF_CONSIDER_HOME,
default=self.config_entry.options.get(
CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME.total_seconds()
),
): vol.All(vol.Coerce(int), vol.Clamp(min=0, max=900)),
}
)
return self.async_show_form(step_id="init", data_schema=data_schema)

View File

@ -116,6 +116,7 @@ class FritzBoxTracker(ScannerEntity):
self._router = router self._router = router
self._mac = device.mac_address self._mac = device.mac_address
self._name = device.hostname or DEFAULT_DEVICE_NAME self._name = device.hostname or DEFAULT_DEVICE_NAME
self._last_activity = device.last_activity
self._active = False self._active = False
self._attrs: dict = {} self._attrs: dict = {}
@ -186,16 +187,22 @@ class FritzBoxTracker(ScannerEntity):
"""Return if the entity should be enabled when first added to the entity registry.""" """Return if the entity should be enabled when first added to the entity registry."""
return False return False
@property
def extra_state_attributes(self) -> dict[str, str]:
"""Return the attributes."""
attrs: dict[str, str] = {}
if self._last_activity is not None:
attrs["last_time_reachable"] = self._last_activity.isoformat(
timespec="seconds"
)
return attrs
@callback @callback
def async_process_update(self) -> None: def async_process_update(self) -> None:
"""Update device.""" """Update device."""
device = self._router.devices[self._mac] device: FritzDevice = self._router.devices[self._mac]
self._active = device.is_connected self._active = device.is_connected
self._last_activity = device.last_activity
if device.last_activity:
self._attrs["last_time_reachable"] = device.last_activity.isoformat(
timespec="seconds"
)
@callback @callback
def async_on_demand_update(self): def async_on_demand_update(self):

View File

@ -40,5 +40,14 @@
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]"
} }
},
"options": {
"step": {
"init": {
"data": {
"consider_home": "Seconds to consider a device at 'home'"
}
}
}
} }
} }

View File

@ -51,5 +51,14 @@
"title": "Setup FRITZ!Box Tools" "title": "Setup FRITZ!Box Tools"
} }
} }
},
"options": {
"step": {
"init": {
"data": {
"consider_home": "Seconds to consider a device at 'home'"
}
}
}
} }
} }

View File

@ -4,6 +4,10 @@ from unittest.mock import patch
from fritzconnection.core.exceptions import FritzConnectionException, FritzSecurityError from fritzconnection.core.exceptions import FritzConnectionException, FritzSecurityError
import pytest import pytest
from homeassistant.components.device_tracker.const import (
CONF_CONSIDER_HOME,
DEFAULT_CONSIDER_HOME,
)
from homeassistant.components.fritz.const import ( from homeassistant.components.fritz.const import (
DOMAIN, DOMAIN,
ERROR_AUTH_INVALID, ERROR_AUTH_INVALID,
@ -83,6 +87,10 @@ async def test_user(hass: HomeAssistant, fc_class_mock):
assert result["data"][CONF_HOST] == "fake_host" assert result["data"][CONF_HOST] == "fake_host"
assert result["data"][CONF_PASSWORD] == "fake_pass" assert result["data"][CONF_PASSWORD] == "fake_pass"
assert result["data"][CONF_USERNAME] == "fake_user" assert result["data"][CONF_USERNAME] == "fake_user"
assert (
result["options"][CONF_CONSIDER_HOME]
== DEFAULT_CONSIDER_HOME.total_seconds()
)
assert not result["result"].unique_id assert not result["result"].unique_id
await hass.async_block_till_done() await hass.async_block_till_done()
@ -416,3 +424,30 @@ async def test_import(hass: HomeAssistant, fc_class_mock):
await hass.async_block_till_done() await hass.async_block_till_done()
assert mock_setup_entry.called assert mock_setup_entry.called
async def test_options_flow(hass: HomeAssistant, fc_class_mock):
"""Test options flow."""
mock_config = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA)
mock_config.add_to_hass(hass)
with patch(
"homeassistant.components.fritz.common.FritzConnection",
side_effect=fc_class_mock,
), patch("homeassistant.components.fritz.common.FritzStatus"), patch(
"homeassistant.components.fritz.common.FritzBoxTools"
):
result = await hass.config_entries.options.async_init(mock_config.entry_id)
assert result["type"] == RESULT_TYPE_FORM
assert result["step_id"] == "init"
result = await hass.config_entries.options.async_init(mock_config.entry_id)
result = await hass.config_entries.options.async_configure(
result["flow_id"],
user_input={
CONF_CONSIDER_HOME: 37,
},
)
assert result["type"] == RESULT_TYPE_CREATE_ENTRY
assert mock_config.options[CONF_CONSIDER_HOME] == 37