Implement import of consider_home in nmap_tracker to avoid breaking change (#55379)

pull/55434/head
J. Nick Koston 2021-08-29 22:38:41 -05:00 committed by GitHub
parent be04d7b92e
commit 5549a925b8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 75 additions and 16 deletions

View File

@ -14,7 +14,11 @@ from getmac import get_mac_address
from mac_vendor_lookup import AsyncMacLookup
from nmap import PortScanner, PortScannerError
from homeassistant.components.device_tracker.const import CONF_SCAN_INTERVAL
from homeassistant.components.device_tracker.const import (
CONF_CONSIDER_HOME,
CONF_SCAN_INTERVAL,
DEFAULT_CONSIDER_HOME,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_EXCLUDE, CONF_HOSTS, EVENT_HOMEASSISTANT_STARTED
from homeassistant.core import CoreState, HomeAssistant, callback
@ -37,7 +41,6 @@ from .const import (
# Some version of nmap will fail with 'Assertion failed: htn.toclock_running == true (Target.cc: stopTimeOutClock: 503)\n'
NMAP_TRANSIENT_FAILURE: Final = "Assertion failed: htn.toclock_running == true"
MAX_SCAN_ATTEMPTS: Final = 16
OFFLINE_SCANS_TO_MARK_UNAVAILABLE: Final = 3
def short_hostname(hostname: str) -> str:
@ -65,7 +68,7 @@ class NmapDevice:
manufacturer: str
reason: str
last_update: datetime
offline_scans: int
first_offline: datetime | None
class NmapTrackedDevices:
@ -137,6 +140,7 @@ class NmapDeviceScanner:
"""Initialize the scanner."""
self.devices = devices
self.home_interval = None
self.consider_home = DEFAULT_CONSIDER_HOME
self._hass = hass
self._entry = entry
@ -170,6 +174,10 @@ class NmapDeviceScanner:
self.home_interval = timedelta(
minutes=cv.positive_int(config[CONF_HOME_INTERVAL])
)
if config.get(CONF_CONSIDER_HOME):
self.consider_home = timedelta(
seconds=cv.positive_float(config[CONF_CONSIDER_HOME])
)
self._scan_lock = asyncio.Lock()
if self._hass.state == CoreState.running:
await self._async_start_scanner()
@ -320,16 +328,35 @@ class NmapDeviceScanner:
return result
@callback
def _async_increment_device_offline(self, ipv4, reason):
def _async_device_offline(self, ipv4: str, reason: str, now: datetime) -> None:
"""Mark an IP offline."""
if not (formatted_mac := self.devices.ipv4_last_mac.get(ipv4)):
return
if not (device := self.devices.tracked.get(formatted_mac)):
# Device was unloaded
return
device.offline_scans += 1
if device.offline_scans < OFFLINE_SCANS_TO_MARK_UNAVAILABLE:
if not device.first_offline:
_LOGGER.debug(
"Setting first_offline for %s (%s) to: %s", ipv4, formatted_mac, now
)
device.first_offline = now
return
if device.first_offline + self.consider_home > now:
_LOGGER.debug(
"Device %s (%s) has NOT been offline (first offline at: %s) long enough to be considered not home: %s",
ipv4,
formatted_mac,
device.first_offline,
self.consider_home,
)
return
_LOGGER.debug(
"Device %s (%s) has been offline (first offline at: %s) long enough to be considered not home: %s",
ipv4,
formatted_mac,
device.first_offline,
self.consider_home,
)
device.reason = reason
async_dispatcher_send(self._hass, signal_device_update(formatted_mac), False)
del self.devices.ipv4_last_mac[ipv4]
@ -347,7 +374,7 @@ class NmapDeviceScanner:
status = info["status"]
reason = status["reason"]
if status["state"] != "up":
self._async_increment_device_offline(ipv4, reason)
self._async_device_offline(ipv4, reason, now)
continue
# Mac address only returned if nmap ran as root
mac = info["addresses"].get(
@ -356,12 +383,11 @@ class NmapDeviceScanner:
partial(get_mac_address, ip=ipv4)
)
if mac is None:
self._async_increment_device_offline(ipv4, "No MAC address found")
self._async_device_offline(ipv4, "No MAC address found", now)
_LOGGER.info("No MAC address found for %s", ipv4)
continue
formatted_mac = format_mac(mac)
if (
devices.config_entry_owner.setdefault(formatted_mac, entry_id)
!= entry_id
@ -372,7 +398,7 @@ class NmapDeviceScanner:
vendor = info.get("vendor", {}).get(mac) or self._async_get_vendor(mac)
name = human_readable_name(hostname, vendor, mac)
device = NmapDevice(
formatted_mac, hostname, name, ipv4, vendor, reason, now, 0
formatted_mac, hostname, name, ipv4, vendor, reason, now, None
)
new = formatted_mac not in devices.tracked

View File

@ -8,7 +8,11 @@ import voluptuous as vol
from homeassistant import config_entries
from homeassistant.components import network
from homeassistant.components.device_tracker.const import CONF_SCAN_INTERVAL
from homeassistant.components.device_tracker.const import (
CONF_CONSIDER_HOME,
CONF_SCAN_INTERVAL,
DEFAULT_CONSIDER_HOME,
)
from homeassistant.components.network.const import MDNS_TARGET_IP
from homeassistant.config_entries import ConfigEntry, OptionsFlow
from homeassistant.const import CONF_EXCLUDE, CONF_HOSTS
@ -24,6 +28,8 @@ from .const import (
TRACKER_SCAN_INTERVAL,
)
MAX_SCAN_INTERVAL = 3600
MAX_CONSIDER_HOME = MAX_SCAN_INTERVAL * 6
DEFAULT_NETWORK_PREFIX = 24
@ -116,7 +122,12 @@ async def _async_build_schema_with_user_input(
vol.Optional(
CONF_SCAN_INTERVAL,
default=user_input.get(CONF_SCAN_INTERVAL, TRACKER_SCAN_INTERVAL),
): vol.All(vol.Coerce(int), vol.Range(min=10, max=3600)),
): vol.All(vol.Coerce(int), vol.Range(min=10, max=MAX_SCAN_INTERVAL)),
vol.Optional(
CONF_CONSIDER_HOME,
default=user_input.get(CONF_CONSIDER_HOME)
or DEFAULT_CONSIDER_HOME.total_seconds(),
): vol.All(vol.Coerce(int), vol.Range(min=1, max=MAX_CONSIDER_HOME)),
}
)
return vol.Schema(schema)

View File

@ -12,7 +12,11 @@ from homeassistant.components.device_tracker import (
SOURCE_TYPE_ROUTER,
)
from homeassistant.components.device_tracker.config_entry import ScannerEntity
from homeassistant.components.device_tracker.const import CONF_SCAN_INTERVAL
from homeassistant.components.device_tracker.const import (
CONF_CONSIDER_HOME,
CONF_SCAN_INTERVAL,
DEFAULT_CONSIDER_HOME,
)
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import CONF_EXCLUDE, CONF_HOSTS
from homeassistant.core import HomeAssistant, callback
@ -38,6 +42,9 @@ PLATFORM_SCHEMA = DEVICE_TRACKER_PLATFORM_SCHEMA.extend(
{
vol.Required(CONF_HOSTS): cv.ensure_list,
vol.Required(CONF_HOME_INTERVAL, default=0): cv.positive_int,
vol.Required(
CONF_CONSIDER_HOME, default=DEFAULT_CONSIDER_HOME.total_seconds()
): cv.time_period,
vol.Optional(CONF_EXCLUDE, default=[]): vol.All(cv.ensure_list, [cv.string]),
vol.Optional(CONF_OPTIONS, default=DEFAULT_OPTIONS): cv.string,
}
@ -53,9 +60,15 @@ async def async_get_scanner(hass: HomeAssistant, config: ConfigType) -> None:
else:
scan_interval = TRACKER_SCAN_INTERVAL
if CONF_CONSIDER_HOME in validated_config:
consider_home = validated_config[CONF_CONSIDER_HOME].total_seconds()
else:
consider_home = DEFAULT_CONSIDER_HOME.total_seconds()
import_config = {
CONF_HOSTS: ",".join(validated_config[CONF_HOSTS]),
CONF_HOME_INTERVAL: validated_config[CONF_HOME_INTERVAL],
CONF_CONSIDER_HOME: consider_home,
CONF_EXCLUDE: ",".join(validated_config[CONF_EXCLUDE]),
CONF_OPTIONS: validated_config[CONF_OPTIONS],
CONF_SCAN_INTERVAL: scan_interval,

View File

@ -7,6 +7,7 @@
"data": {
"hosts": "[%key:component::nmap_tracker::config::step::user::data::hosts%]",
"home_interval": "[%key:component::nmap_tracker::config::step::user::data::home_interval%]",
"consider_home": "Seconds to wait till marking a device tracker as not home after not being seen.",
"exclude": "[%key:component::nmap_tracker::config::step::user::data::exclude%]",
"scan_options": "[%key:component::nmap_tracker::config::step::user::data::scan_options%]",
"interval_seconds": "Scan interval"

View File

@ -25,12 +25,12 @@
"step": {
"init": {
"data": {
"consider_home": "Seconds to wait till marking a device tracker as not home after not being seen.",
"exclude": "Network addresses (comma seperated) to exclude from scanning",
"home_interval": "Minimum number of minutes between scans of active devices (preserve battery)",
"hosts": "Network addresses (comma seperated) to scan",
"interval_seconds": "Scan interval",
"scan_options": "Raw configurable scan options for Nmap",
"track_new_devices": "Track new devices"
"scan_options": "Raw configurable scan options for Nmap"
},
"description": "Configure hosts to be scanned by Nmap. Network address and excludes can be IP Addresses (192.168.1.1), IP Networks (192.168.0.0/24) or IP Ranges (192.168.1.0-32)."
}

View File

@ -4,7 +4,10 @@ from unittest.mock import patch
import pytest
from homeassistant import config_entries, data_entry_flow, setup
from homeassistant.components.device_tracker.const import CONF_SCAN_INTERVAL
from homeassistant.components.device_tracker.const import (
CONF_CONSIDER_HOME,
CONF_SCAN_INTERVAL,
)
from homeassistant.components.nmap_tracker.const import (
CONF_HOME_INTERVAL,
CONF_OPTIONS,
@ -206,6 +209,7 @@ async def test_options_flow(hass: HomeAssistant) -> None:
CONF_EXCLUDE: "4.4.4.4",
CONF_HOME_INTERVAL: 3,
CONF_HOSTS: "192.168.1.0/24",
CONF_CONSIDER_HOME: 180,
CONF_SCAN_INTERVAL: 120,
CONF_OPTIONS: "-F -T4 --min-rate 10 --host-timeout 5s",
}
@ -219,6 +223,7 @@ async def test_options_flow(hass: HomeAssistant) -> None:
user_input={
CONF_HOSTS: "192.168.1.0/24, 192.168.2.0/24",
CONF_HOME_INTERVAL: 5,
CONF_CONSIDER_HOME: 500,
CONF_OPTIONS: "-sn",
CONF_EXCLUDE: "4.4.4.4, 5.5.5.5",
CONF_SCAN_INTERVAL: 10,
@ -230,6 +235,7 @@ async def test_options_flow(hass: HomeAssistant) -> None:
assert config_entry.options == {
CONF_HOSTS: "192.168.1.0/24,192.168.2.0/24",
CONF_HOME_INTERVAL: 5,
CONF_CONSIDER_HOME: 500,
CONF_OPTIONS: "-sn",
CONF_EXCLUDE: "4.4.4.4,5.5.5.5",
CONF_SCAN_INTERVAL: 10,
@ -250,6 +256,7 @@ async def test_import(hass: HomeAssistant) -> None:
data={
CONF_HOSTS: "1.2.3.4/20",
CONF_HOME_INTERVAL: 3,
CONF_CONSIDER_HOME: 500,
CONF_OPTIONS: DEFAULT_OPTIONS,
CONF_EXCLUDE: "4.4.4.4, 6.4.3.2",
CONF_SCAN_INTERVAL: 2000,
@ -263,6 +270,7 @@ async def test_import(hass: HomeAssistant) -> None:
assert result["options"] == {
CONF_HOSTS: "1.2.3.4/20",
CONF_HOME_INTERVAL: 3,
CONF_CONSIDER_HOME: 500,
CONF_OPTIONS: DEFAULT_OPTIONS,
CONF_EXCLUDE: "4.4.4.4,6.4.3.2",
CONF_SCAN_INTERVAL: 2000,