Import track_new_devices and scan_interval from yaml for nmap_tracker (#52409)

* Import track_new_devices and scan_interval from yaml for nmap_tracker

* Import track_new_devices and scan_interval from yaml for nmap_tracker

* Import track_new_devices and scan_interval from yaml for nmap_tracker

* tests

* translate

* tweak

* adjust

* save indent

* pylint

* There are two CONF_SCAN_INTERVAL constants

* adjust name -- there are TWO CONF_SCAN_INTERVAL constants

* remove CONF_SCAN_INTERVAL/CONF_TRACK_NEW from user flow

* assert it does not appear in the user step
pull/52443/head
J. Nick Koston 2021-07-02 10:24:43 -05:00 committed by GitHub
parent 98fdb00bc7
commit a3f1489785
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 146 additions and 48 deletions

View File

@ -12,6 +12,10 @@ 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,
CONF_TRACK_NEW,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_EXCLUDE, CONF_HOSTS, EVENT_HOMEASSISTANT_STARTED
from homeassistant.core import CoreState, HomeAssistant, callback
@ -25,6 +29,7 @@ import homeassistant.util.dt as dt_util
from .const import (
CONF_HOME_INTERVAL,
CONF_OPTIONS,
DEFAULT_TRACK_NEW_DEVICES,
DOMAIN,
NMAP_TRACKED_DEVICES,
PLATFORMS,
@ -146,7 +151,10 @@ class NmapDeviceScanner:
self._hosts = None
self._options = None
self._exclude = None
self._scan_interval = None
self._track_new_devices = None
self._known_mac_addresses = {}
self._finished_first_scan = False
self._last_results = []
self._mac_vendor_lookup = None
@ -154,6 +162,10 @@ class NmapDeviceScanner:
async def async_setup(self):
"""Set up the tracker."""
config = self._entry.options
self._track_new_devices = config.get(CONF_TRACK_NEW, DEFAULT_TRACK_NEW_DEVICES)
self._scan_interval = timedelta(
seconds=config.get(CONF_SCAN_INTERVAL, TRACKER_SCAN_INTERVAL)
)
self._hosts = cv.ensure_list_csv(config[CONF_HOSTS])
self._exclude = cv.ensure_list_csv(config[CONF_EXCLUDE])
self._options = config[CONF_OPTIONS]
@ -170,6 +182,12 @@ class NmapDeviceScanner:
EVENT_HOMEASSISTANT_STARTED, self._async_start_scanner
)
)
registry = er.async_get(self._hass)
self._known_mac_addresses = {
entry.unique_id: entry.original_name
for entry in registry.entities.values()
if entry.config_entry_id == self._entry_id
}
@property
def signal_device_new(self) -> str:
@ -199,7 +217,7 @@ class NmapDeviceScanner:
async_track_time_interval(
self._hass,
self._async_scan_devices,
timedelta(seconds=TRACKER_SCAN_INTERVAL),
self._scan_interval,
)
)
self._mac_vendor_lookup = AsyncMacLookup()
@ -258,26 +276,22 @@ class NmapDeviceScanner:
# After all config entries have finished their first
# scan we mark devices that were not found as not_home
# from unavailable
registry = er.async_get(self._hass)
now = dt_util.now()
for entry in registry.entities.values():
if entry.config_entry_id != self._entry_id:
for mac_address, original_name in self._known_mac_addresses.items():
if mac_address in self.devices.tracked:
continue
if entry.unique_id not in self.devices.tracked:
self.devices.config_entry_owner[entry.unique_id] = self._entry_id
self.devices.tracked[entry.unique_id] = NmapDevice(
entry.unique_id,
None,
entry.original_name,
None,
self._async_get_vendor(entry.unique_id),
"Device not found in initial scan",
now,
1,
)
async_dispatcher_send(
self._hass, self.signal_device_missing, entry.unique_id
)
self.devices.config_entry_owner[mac_address] = self._entry_id
self.devices.tracked[mac_address] = NmapDevice(
mac_address,
None,
original_name,
None,
self._async_get_vendor(mac_address),
"Device not found in initial scan",
now,
1,
)
async_dispatcher_send(self._hass, self.signal_device_missing, mac_address)
def _run_nmap_scan(self):
"""Run nmap and return the result."""
@ -344,21 +358,28 @@ class NmapDeviceScanner:
_LOGGER.info("No MAC address found for %s", ipv4)
continue
hostname = info["hostnames"][0]["name"] if info["hostnames"] else ipv4
formatted_mac = format_mac(mac)
new = formatted_mac not in devices.tracked
if (
new
and not self._track_new_devices
and formatted_mac not in devices.tracked
and formatted_mac not in self._known_mac_addresses
):
continue
if (
devices.config_entry_owner.setdefault(formatted_mac, entry_id)
!= entry_id
):
continue
hostname = info["hostnames"][0]["name"] if info["hostnames"] else ipv4
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
)
new = formatted_mac not in devices.tracked
devices.tracked[formatted_mac] = device
devices.ipv4_last_mac[ipv4] = formatted_mac

View File

@ -8,13 +8,24 @@ import ifaddr
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.components.device_tracker.const import (
CONF_SCAN_INTERVAL,
CONF_TRACK_NEW,
)
from homeassistant.const import CONF_EXCLUDE, CONF_HOSTS
from homeassistant.core import callback
from homeassistant.data_entry_flow import FlowResult
import homeassistant.helpers.config_validation as cv
from homeassistant.util import get_local_ip
from .const import CONF_HOME_INTERVAL, CONF_OPTIONS, DEFAULT_OPTIONS, DOMAIN
from .const import (
CONF_HOME_INTERVAL,
CONF_OPTIONS,
DEFAULT_OPTIONS,
DEFAULT_TRACK_NEW_DEVICES,
DOMAIN,
TRACKER_SCAN_INTERVAL,
)
DEFAULT_NETWORK_PREFIX = 24
@ -92,23 +103,35 @@ def normalize_input(user_input):
return errors
async def _async_build_schema_with_user_input(hass, user_input):
async def _async_build_schema_with_user_input(hass, user_input, include_options):
hosts = user_input.get(CONF_HOSTS, await hass.async_add_executor_job(get_network))
exclude = user_input.get(
CONF_EXCLUDE, await hass.async_add_executor_job(get_local_ip)
)
return vol.Schema(
{
vol.Required(CONF_HOSTS, default=hosts): str,
vol.Required(
CONF_HOME_INTERVAL, default=user_input.get(CONF_HOME_INTERVAL, 0)
): int,
vol.Optional(CONF_EXCLUDE, default=exclude): str,
vol.Optional(
CONF_OPTIONS, default=user_input.get(CONF_OPTIONS, DEFAULT_OPTIONS)
): str,
}
)
schema = {
vol.Required(CONF_HOSTS, default=hosts): str,
vol.Required(
CONF_HOME_INTERVAL, default=user_input.get(CONF_HOME_INTERVAL, 0)
): int,
vol.Optional(CONF_EXCLUDE, default=exclude): str,
vol.Optional(
CONF_OPTIONS, default=user_input.get(CONF_OPTIONS, DEFAULT_OPTIONS)
): str,
}
if include_options:
schema.update(
{
vol.Optional(
CONF_TRACK_NEW,
default=user_input.get(CONF_TRACK_NEW, DEFAULT_TRACK_NEW_DEVICES),
): bool,
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)),
}
)
return vol.Schema(schema)
class OptionsFlowHandler(config_entries.OptionsFlow):
@ -133,7 +156,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
return self.async_show_form(
step_id="init",
data_schema=await _async_build_schema_with_user_input(
self.hass, self.options
self.hass, self.options, True
),
errors=errors,
)
@ -170,7 +193,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
return self.async_show_form(
step_id="user",
data_schema=await _async_build_schema_with_user_input(
self.hass, self.options
self.hass, self.options, False
),
errors=errors,
)

View File

@ -12,3 +12,5 @@ CONF_OPTIONS = "scan_options"
DEFAULT_OPTIONS = "-F --host-timeout 5s"
TRACKER_SCAN_INTERVAL = 120
DEFAULT_TRACK_NEW_DEVICES = True

View File

@ -11,6 +11,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_NEW_DEVICE_DEFAULTS,
CONF_SCAN_INTERVAL,
CONF_TRACK_NEW,
)
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import CONF_EXCLUDE, CONF_HOSTS
from homeassistant.core import HomeAssistant, callback
@ -19,7 +24,14 @@ from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from . import NmapDeviceScanner, short_hostname, signal_device_update
from .const import CONF_HOME_INTERVAL, CONF_OPTIONS, DEFAULT_OPTIONS, DOMAIN
from .const import (
CONF_HOME_INTERVAL,
CONF_OPTIONS,
DEFAULT_OPTIONS,
DEFAULT_TRACK_NEW_DEVICES,
DOMAIN,
TRACKER_SCAN_INTERVAL,
)
_LOGGER = logging.getLogger(__name__)
@ -37,16 +49,27 @@ async def async_get_scanner(hass, config):
"""Validate the configuration and return a Nmap scanner."""
validated_config = config[DEVICE_TRACKER_DOMAIN]
if CONF_SCAN_INTERVAL in validated_config:
scan_interval = validated_config[CONF_SCAN_INTERVAL].total_seconds()
else:
scan_interval = TRACKER_SCAN_INTERVAL
import_config = {
CONF_HOSTS: ",".join(validated_config[CONF_HOSTS]),
CONF_HOME_INTERVAL: validated_config[CONF_HOME_INTERVAL],
CONF_EXCLUDE: ",".join(validated_config[CONF_EXCLUDE]),
CONF_OPTIONS: validated_config[CONF_OPTIONS],
CONF_SCAN_INTERVAL: scan_interval,
CONF_TRACK_NEW: validated_config.get(CONF_NEW_DEVICE_DEFAULTS, {}).get(
CONF_TRACK_NEW, DEFAULT_TRACK_NEW_DEVICES
),
}
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_IMPORT},
data={
CONF_HOSTS: ",".join(validated_config[CONF_HOSTS]),
CONF_HOME_INTERVAL: validated_config[CONF_HOME_INTERVAL],
CONF_EXCLUDE: ",".join(validated_config[CONF_EXCLUDE]),
CONF_OPTIONS: validated_config[CONF_OPTIONS],
},
data=import_config,
)
)

View File

@ -8,8 +8,10 @@
"hosts": "[%key:component::nmap_tracker::config::step::user::data::hosts%]",
"home_interval": "[%key:component::nmap_tracker::config::step::user::data::home_interval%]",
"exclude": "[%key:component::nmap_tracker::config::step::user::data::exclude%]",
"scan_options": "[%key:component::nmap_tracker::config::step::user::data::scan_options%]"
}
"scan_options": "[%key:component::nmap_tracker::config::step::user::data::scan_options%]",
"track_new_devices": "Track new devices",
"interval_seconds": "Scan interval"
}
}
},
"error": {

View File

@ -28,7 +28,9 @@
"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",
"scan_options": "Raw configurable scan options for Nmap"
"interval_seconds": "Scan interval",
"scan_options": "Raw configurable scan options for Nmap",
"track_new_devices": "Track new devices"
},
"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,6 +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,
CONF_TRACK_NEW,
)
from homeassistant.components.nmap_tracker.const import (
CONF_HOME_INTERVAL,
CONF_OPTIONS,
@ -28,6 +32,10 @@ async def test_form(hass: HomeAssistant, hosts: str) -> None:
assert result["type"] == "form"
assert result["errors"] == {}
schema_defaults = result["data_schema"]({})
assert CONF_TRACK_NEW not in schema_defaults
assert CONF_SCAN_INTERVAL not in schema_defaults
with patch(
"homeassistant.components.nmap_tracker.async_setup_entry",
return_value=True,
@ -198,6 +206,15 @@ async def test_options_flow(hass: HomeAssistant) -> None:
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "init"
assert result["data_schema"]({}) == {
CONF_EXCLUDE: "4.4.4.4",
CONF_HOME_INTERVAL: 3,
CONF_HOSTS: "192.168.1.0/24",
CONF_SCAN_INTERVAL: 120,
CONF_OPTIONS: "-F --host-timeout 5s",
CONF_TRACK_NEW: True,
}
with patch(
"homeassistant.components.nmap_tracker.async_setup_entry",
return_value=True,
@ -209,6 +226,8 @@ async def test_options_flow(hass: HomeAssistant) -> None:
CONF_HOME_INTERVAL: 5,
CONF_OPTIONS: "-sn",
CONF_EXCLUDE: "4.4.4.4, 5.5.5.5",
CONF_SCAN_INTERVAL: 10,
CONF_TRACK_NEW: False,
},
)
await hass.async_block_till_done()
@ -219,6 +238,8 @@ async def test_options_flow(hass: HomeAssistant) -> None:
CONF_HOME_INTERVAL: 5,
CONF_OPTIONS: "-sn",
CONF_EXCLUDE: "4.4.4.4,5.5.5.5",
CONF_SCAN_INTERVAL: 10,
CONF_TRACK_NEW: False,
}
assert len(mock_setup_entry.mock_calls) == 1
@ -238,6 +259,8 @@ async def test_import(hass: HomeAssistant) -> None:
CONF_HOME_INTERVAL: 3,
CONF_OPTIONS: DEFAULT_OPTIONS,
CONF_EXCLUDE: "4.4.4.4, 6.4.3.2",
CONF_SCAN_INTERVAL: 2000,
CONF_TRACK_NEW: False,
},
)
await hass.async_block_till_done()
@ -250,6 +273,8 @@ async def test_import(hass: HomeAssistant) -> None:
CONF_HOME_INTERVAL: 3,
CONF_OPTIONS: DEFAULT_OPTIONS,
CONF_EXCLUDE: "4.4.4.4,6.4.3.2",
CONF_SCAN_INTERVAL: 2000,
CONF_TRACK_NEW: False,
}
assert len(mock_setup_entry.mock_calls) == 1