Implement import of consider_home in nmap_tracker to avoid breaking change (#55379)
parent
be04d7b92e
commit
5549a925b8
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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)."
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue