mirror of
https://github.com/home-assistant/core.git
synced 2025-07-24 13:47:35 +00:00
Implement import of consider_home in nmap_tracker to avoid breaking change (#55379)
This commit is contained in:
parent
be04d7b92e
commit
5549a925b8
@ -14,7 +14,11 @@ from getmac import get_mac_address
|
|||||||
from mac_vendor_lookup import AsyncMacLookup
|
from mac_vendor_lookup import AsyncMacLookup
|
||||||
from nmap import PortScanner, PortScannerError
|
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.config_entries import ConfigEntry
|
||||||
from homeassistant.const import CONF_EXCLUDE, CONF_HOSTS, EVENT_HOMEASSISTANT_STARTED
|
from homeassistant.const import CONF_EXCLUDE, CONF_HOSTS, EVENT_HOMEASSISTANT_STARTED
|
||||||
from homeassistant.core import CoreState, HomeAssistant, callback
|
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'
|
# 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"
|
NMAP_TRANSIENT_FAILURE: Final = "Assertion failed: htn.toclock_running == true"
|
||||||
MAX_SCAN_ATTEMPTS: Final = 16
|
MAX_SCAN_ATTEMPTS: Final = 16
|
||||||
OFFLINE_SCANS_TO_MARK_UNAVAILABLE: Final = 3
|
|
||||||
|
|
||||||
|
|
||||||
def short_hostname(hostname: str) -> str:
|
def short_hostname(hostname: str) -> str:
|
||||||
@ -65,7 +68,7 @@ class NmapDevice:
|
|||||||
manufacturer: str
|
manufacturer: str
|
||||||
reason: str
|
reason: str
|
||||||
last_update: datetime
|
last_update: datetime
|
||||||
offline_scans: int
|
first_offline: datetime | None
|
||||||
|
|
||||||
|
|
||||||
class NmapTrackedDevices:
|
class NmapTrackedDevices:
|
||||||
@ -137,6 +140,7 @@ class NmapDeviceScanner:
|
|||||||
"""Initialize the scanner."""
|
"""Initialize the scanner."""
|
||||||
self.devices = devices
|
self.devices = devices
|
||||||
self.home_interval = None
|
self.home_interval = None
|
||||||
|
self.consider_home = DEFAULT_CONSIDER_HOME
|
||||||
|
|
||||||
self._hass = hass
|
self._hass = hass
|
||||||
self._entry = entry
|
self._entry = entry
|
||||||
@ -170,6 +174,10 @@ class NmapDeviceScanner:
|
|||||||
self.home_interval = timedelta(
|
self.home_interval = timedelta(
|
||||||
minutes=cv.positive_int(config[CONF_HOME_INTERVAL])
|
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()
|
self._scan_lock = asyncio.Lock()
|
||||||
if self._hass.state == CoreState.running:
|
if self._hass.state == CoreState.running:
|
||||||
await self._async_start_scanner()
|
await self._async_start_scanner()
|
||||||
@ -320,16 +328,35 @@ class NmapDeviceScanner:
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
@callback
|
@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."""
|
"""Mark an IP offline."""
|
||||||
if not (formatted_mac := self.devices.ipv4_last_mac.get(ipv4)):
|
if not (formatted_mac := self.devices.ipv4_last_mac.get(ipv4)):
|
||||||
return
|
return
|
||||||
if not (device := self.devices.tracked.get(formatted_mac)):
|
if not (device := self.devices.tracked.get(formatted_mac)):
|
||||||
# Device was unloaded
|
# Device was unloaded
|
||||||
return
|
return
|
||||||
device.offline_scans += 1
|
if not device.first_offline:
|
||||||
if device.offline_scans < OFFLINE_SCANS_TO_MARK_UNAVAILABLE:
|
_LOGGER.debug(
|
||||||
|
"Setting first_offline for %s (%s) to: %s", ipv4, formatted_mac, now
|
||||||
|
)
|
||||||
|
device.first_offline = now
|
||||||
return
|
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
|
device.reason = reason
|
||||||
async_dispatcher_send(self._hass, signal_device_update(formatted_mac), False)
|
async_dispatcher_send(self._hass, signal_device_update(formatted_mac), False)
|
||||||
del self.devices.ipv4_last_mac[ipv4]
|
del self.devices.ipv4_last_mac[ipv4]
|
||||||
@ -347,7 +374,7 @@ class NmapDeviceScanner:
|
|||||||
status = info["status"]
|
status = info["status"]
|
||||||
reason = status["reason"]
|
reason = status["reason"]
|
||||||
if status["state"] != "up":
|
if status["state"] != "up":
|
||||||
self._async_increment_device_offline(ipv4, reason)
|
self._async_device_offline(ipv4, reason, now)
|
||||||
continue
|
continue
|
||||||
# Mac address only returned if nmap ran as root
|
# Mac address only returned if nmap ran as root
|
||||||
mac = info["addresses"].get(
|
mac = info["addresses"].get(
|
||||||
@ -356,12 +383,11 @@ class NmapDeviceScanner:
|
|||||||
partial(get_mac_address, ip=ipv4)
|
partial(get_mac_address, ip=ipv4)
|
||||||
)
|
)
|
||||||
if mac is None:
|
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)
|
_LOGGER.info("No MAC address found for %s", ipv4)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
formatted_mac = format_mac(mac)
|
formatted_mac = format_mac(mac)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
devices.config_entry_owner.setdefault(formatted_mac, entry_id)
|
devices.config_entry_owner.setdefault(formatted_mac, entry_id)
|
||||||
!= entry_id
|
!= entry_id
|
||||||
@ -372,7 +398,7 @@ class NmapDeviceScanner:
|
|||||||
vendor = info.get("vendor", {}).get(mac) or self._async_get_vendor(mac)
|
vendor = info.get("vendor", {}).get(mac) or self._async_get_vendor(mac)
|
||||||
name = human_readable_name(hostname, vendor, mac)
|
name = human_readable_name(hostname, vendor, mac)
|
||||||
device = NmapDevice(
|
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
|
new = formatted_mac not in devices.tracked
|
||||||
|
@ -8,7 +8,11 @@ import voluptuous as vol
|
|||||||
|
|
||||||
from homeassistant import config_entries
|
from homeassistant import config_entries
|
||||||
from homeassistant.components import network
|
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.components.network.const import MDNS_TARGET_IP
|
||||||
from homeassistant.config_entries import ConfigEntry, OptionsFlow
|
from homeassistant.config_entries import ConfigEntry, OptionsFlow
|
||||||
from homeassistant.const import CONF_EXCLUDE, CONF_HOSTS
|
from homeassistant.const import CONF_EXCLUDE, CONF_HOSTS
|
||||||
@ -24,6 +28,8 @@ from .const import (
|
|||||||
TRACKER_SCAN_INTERVAL,
|
TRACKER_SCAN_INTERVAL,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
MAX_SCAN_INTERVAL = 3600
|
||||||
|
MAX_CONSIDER_HOME = MAX_SCAN_INTERVAL * 6
|
||||||
DEFAULT_NETWORK_PREFIX = 24
|
DEFAULT_NETWORK_PREFIX = 24
|
||||||
|
|
||||||
|
|
||||||
@ -116,7 +122,12 @@ async def _async_build_schema_with_user_input(
|
|||||||
vol.Optional(
|
vol.Optional(
|
||||||
CONF_SCAN_INTERVAL,
|
CONF_SCAN_INTERVAL,
|
||||||
default=user_input.get(CONF_SCAN_INTERVAL, TRACKER_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)
|
return vol.Schema(schema)
|
||||||
|
@ -12,7 +12,11 @@ from homeassistant.components.device_tracker import (
|
|||||||
SOURCE_TYPE_ROUTER,
|
SOURCE_TYPE_ROUTER,
|
||||||
)
|
)
|
||||||
from homeassistant.components.device_tracker.config_entry import ScannerEntity
|
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.config_entries import SOURCE_IMPORT, ConfigEntry
|
||||||
from homeassistant.const import CONF_EXCLUDE, CONF_HOSTS
|
from homeassistant.const import CONF_EXCLUDE, CONF_HOSTS
|
||||||
from homeassistant.core import HomeAssistant, callback
|
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_HOSTS): cv.ensure_list,
|
||||||
vol.Required(CONF_HOME_INTERVAL, default=0): cv.positive_int,
|
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_EXCLUDE, default=[]): vol.All(cv.ensure_list, [cv.string]),
|
||||||
vol.Optional(CONF_OPTIONS, default=DEFAULT_OPTIONS): 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:
|
else:
|
||||||
scan_interval = TRACKER_SCAN_INTERVAL
|
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 = {
|
import_config = {
|
||||||
CONF_HOSTS: ",".join(validated_config[CONF_HOSTS]),
|
CONF_HOSTS: ",".join(validated_config[CONF_HOSTS]),
|
||||||
CONF_HOME_INTERVAL: validated_config[CONF_HOME_INTERVAL],
|
CONF_HOME_INTERVAL: validated_config[CONF_HOME_INTERVAL],
|
||||||
|
CONF_CONSIDER_HOME: consider_home,
|
||||||
CONF_EXCLUDE: ",".join(validated_config[CONF_EXCLUDE]),
|
CONF_EXCLUDE: ",".join(validated_config[CONF_EXCLUDE]),
|
||||||
CONF_OPTIONS: validated_config[CONF_OPTIONS],
|
CONF_OPTIONS: validated_config[CONF_OPTIONS],
|
||||||
CONF_SCAN_INTERVAL: scan_interval,
|
CONF_SCAN_INTERVAL: scan_interval,
|
||||||
|
@ -7,6 +7,7 @@
|
|||||||
"data": {
|
"data": {
|
||||||
"hosts": "[%key:component::nmap_tracker::config::step::user::data::hosts%]",
|
"hosts": "[%key:component::nmap_tracker::config::step::user::data::hosts%]",
|
||||||
"home_interval": "[%key:component::nmap_tracker::config::step::user::data::home_interval%]",
|
"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%]",
|
"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%]",
|
||||||
"interval_seconds": "Scan interval"
|
"interval_seconds": "Scan interval"
|
||||||
|
@ -25,12 +25,12 @@
|
|||||||
"step": {
|
"step": {
|
||||||
"init": {
|
"init": {
|
||||||
"data": {
|
"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",
|
"exclude": "Network addresses (comma seperated) to exclude from scanning",
|
||||||
"home_interval": "Minimum number of minutes between scans of active devices (preserve battery)",
|
"home_interval": "Minimum number of minutes between scans of active devices (preserve battery)",
|
||||||
"hosts": "Network addresses (comma seperated) to scan",
|
"hosts": "Network addresses (comma seperated) to scan",
|
||||||
"interval_seconds": "Scan interval",
|
"interval_seconds": "Scan interval",
|
||||||
"scan_options": "Raw configurable scan options for Nmap",
|
"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)."
|
"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
|
import pytest
|
||||||
|
|
||||||
from homeassistant import config_entries, data_entry_flow, setup
|
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 (
|
from homeassistant.components.nmap_tracker.const import (
|
||||||
CONF_HOME_INTERVAL,
|
CONF_HOME_INTERVAL,
|
||||||
CONF_OPTIONS,
|
CONF_OPTIONS,
|
||||||
@ -206,6 +209,7 @@ async def test_options_flow(hass: HomeAssistant) -> None:
|
|||||||
CONF_EXCLUDE: "4.4.4.4",
|
CONF_EXCLUDE: "4.4.4.4",
|
||||||
CONF_HOME_INTERVAL: 3,
|
CONF_HOME_INTERVAL: 3,
|
||||||
CONF_HOSTS: "192.168.1.0/24",
|
CONF_HOSTS: "192.168.1.0/24",
|
||||||
|
CONF_CONSIDER_HOME: 180,
|
||||||
CONF_SCAN_INTERVAL: 120,
|
CONF_SCAN_INTERVAL: 120,
|
||||||
CONF_OPTIONS: "-F -T4 --min-rate 10 --host-timeout 5s",
|
CONF_OPTIONS: "-F -T4 --min-rate 10 --host-timeout 5s",
|
||||||
}
|
}
|
||||||
@ -219,6 +223,7 @@ async def test_options_flow(hass: HomeAssistant) -> None:
|
|||||||
user_input={
|
user_input={
|
||||||
CONF_HOSTS: "192.168.1.0/24, 192.168.2.0/24",
|
CONF_HOSTS: "192.168.1.0/24, 192.168.2.0/24",
|
||||||
CONF_HOME_INTERVAL: 5,
|
CONF_HOME_INTERVAL: 5,
|
||||||
|
CONF_CONSIDER_HOME: 500,
|
||||||
CONF_OPTIONS: "-sn",
|
CONF_OPTIONS: "-sn",
|
||||||
CONF_EXCLUDE: "4.4.4.4, 5.5.5.5",
|
CONF_EXCLUDE: "4.4.4.4, 5.5.5.5",
|
||||||
CONF_SCAN_INTERVAL: 10,
|
CONF_SCAN_INTERVAL: 10,
|
||||||
@ -230,6 +235,7 @@ async def test_options_flow(hass: HomeAssistant) -> None:
|
|||||||
assert config_entry.options == {
|
assert config_entry.options == {
|
||||||
CONF_HOSTS: "192.168.1.0/24,192.168.2.0/24",
|
CONF_HOSTS: "192.168.1.0/24,192.168.2.0/24",
|
||||||
CONF_HOME_INTERVAL: 5,
|
CONF_HOME_INTERVAL: 5,
|
||||||
|
CONF_CONSIDER_HOME: 500,
|
||||||
CONF_OPTIONS: "-sn",
|
CONF_OPTIONS: "-sn",
|
||||||
CONF_EXCLUDE: "4.4.4.4,5.5.5.5",
|
CONF_EXCLUDE: "4.4.4.4,5.5.5.5",
|
||||||
CONF_SCAN_INTERVAL: 10,
|
CONF_SCAN_INTERVAL: 10,
|
||||||
@ -250,6 +256,7 @@ async def test_import(hass: HomeAssistant) -> None:
|
|||||||
data={
|
data={
|
||||||
CONF_HOSTS: "1.2.3.4/20",
|
CONF_HOSTS: "1.2.3.4/20",
|
||||||
CONF_HOME_INTERVAL: 3,
|
CONF_HOME_INTERVAL: 3,
|
||||||
|
CONF_CONSIDER_HOME: 500,
|
||||||
CONF_OPTIONS: DEFAULT_OPTIONS,
|
CONF_OPTIONS: DEFAULT_OPTIONS,
|
||||||
CONF_EXCLUDE: "4.4.4.4, 6.4.3.2",
|
CONF_EXCLUDE: "4.4.4.4, 6.4.3.2",
|
||||||
CONF_SCAN_INTERVAL: 2000,
|
CONF_SCAN_INTERVAL: 2000,
|
||||||
@ -263,6 +270,7 @@ async def test_import(hass: HomeAssistant) -> None:
|
|||||||
assert result["options"] == {
|
assert result["options"] == {
|
||||||
CONF_HOSTS: "1.2.3.4/20",
|
CONF_HOSTS: "1.2.3.4/20",
|
||||||
CONF_HOME_INTERVAL: 3,
|
CONF_HOME_INTERVAL: 3,
|
||||||
|
CONF_CONSIDER_HOME: 500,
|
||||||
CONF_OPTIONS: DEFAULT_OPTIONS,
|
CONF_OPTIONS: DEFAULT_OPTIONS,
|
||||||
CONF_EXCLUDE: "4.4.4.4,6.4.3.2",
|
CONF_EXCLUDE: "4.4.4.4,6.4.3.2",
|
||||||
CONF_SCAN_INTERVAL: 2000,
|
CONF_SCAN_INTERVAL: 2000,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user