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 steppull/52443/head
parent
98fdb00bc7
commit
a3f1489785
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
|
|
@ -12,3 +12,5 @@ CONF_OPTIONS = "scan_options"
|
|||
DEFAULT_OPTIONS = "-F --host-timeout 5s"
|
||||
|
||||
TRACKER_SCAN_INTERVAL = 120
|
||||
|
||||
DEFAULT_TRACK_NEW_DEVICES = True
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
)
|
||||
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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)."
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
Loading…
Reference in New Issue