2019-03-14 14:00:19 +00:00
|
|
|
"""Support for scanning a network with nmap."""
|
2021-08-21 19:25:28 +00:00
|
|
|
from __future__ import annotations
|
2021-08-16 20:19:32 +00:00
|
|
|
|
2019-12-03 23:46:38 +00:00
|
|
|
import logging
|
2021-08-21 19:25:28 +00:00
|
|
|
from typing import Any, Callable
|
2014-12-15 05:29:36 +00:00
|
|
|
|
2016-09-18 06:44:15 +00:00
|
|
|
import voluptuous as vol
|
|
|
|
|
2017-01-02 19:50:42 +00:00
|
|
|
from homeassistant.components.device_tracker import (
|
2021-08-16 20:19:32 +00:00
|
|
|
DOMAIN as DEVICE_TRACKER_DOMAIN,
|
2021-08-21 19:25:28 +00:00
|
|
|
PLATFORM_SCHEMA as DEVICE_TRACKER_PLATFORM_SCHEMA,
|
2021-08-16 20:19:32 +00:00
|
|
|
SOURCE_TYPE_ROUTER,
|
2021-07-02 15:24:43 +00:00
|
|
|
)
|
2021-08-16 20:19:32 +00:00
|
|
|
from homeassistant.components.device_tracker.config_entry import ScannerEntity
|
2021-08-30 03:38:41 +00:00
|
|
|
from homeassistant.components.device_tracker.const import (
|
|
|
|
CONF_CONSIDER_HOME,
|
|
|
|
CONF_SCAN_INTERVAL,
|
|
|
|
DEFAULT_CONSIDER_HOME,
|
|
|
|
)
|
2021-08-16 20:19:32 +00:00
|
|
|
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
2021-02-11 16:45:26 +00:00
|
|
|
from homeassistant.const import CONF_EXCLUDE, CONF_HOSTS
|
2021-08-16 20:19:32 +00:00
|
|
|
from homeassistant.core import HomeAssistant, callback
|
2019-12-03 23:46:38 +00:00
|
|
|
import homeassistant.helpers.config_validation as cv
|
2021-08-16 20:19:32 +00:00
|
|
|
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
|
|
|
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
2021-08-21 19:25:28 +00:00
|
|
|
from homeassistant.helpers.entity import DeviceInfo
|
|
|
|
from homeassistant.helpers.typing import ConfigType
|
2021-08-16 20:19:32 +00:00
|
|
|
|
2021-08-21 19:25:28 +00:00
|
|
|
from . import NmapDevice, NmapDeviceScanner, short_hostname, signal_device_update
|
2021-08-16 20:19:32 +00:00
|
|
|
from .const import (
|
|
|
|
CONF_HOME_INTERVAL,
|
|
|
|
CONF_OPTIONS,
|
|
|
|
DEFAULT_OPTIONS,
|
|
|
|
DOMAIN,
|
|
|
|
TRACKER_SCAN_INTERVAL,
|
|
|
|
)
|
2015-03-07 20:52:54 +00:00
|
|
|
|
2021-06-28 10:49:14 +00:00
|
|
|
_LOGGER = logging.getLogger(__name__)
|
2015-08-09 04:22:34 +00:00
|
|
|
|
2021-08-21 19:25:28 +00:00
|
|
|
|
|
|
|
PLATFORM_SCHEMA = DEVICE_TRACKER_PLATFORM_SCHEMA.extend(
|
2019-07-31 19:25:30 +00:00
|
|
|
{
|
|
|
|
vol.Required(CONF_HOSTS): cv.ensure_list,
|
|
|
|
vol.Required(CONF_HOME_INTERVAL, default=0): cv.positive_int,
|
2021-08-30 03:38:41 +00:00
|
|
|
vol.Required(
|
|
|
|
CONF_CONSIDER_HOME, default=DEFAULT_CONSIDER_HOME.total_seconds()
|
|
|
|
): cv.time_period,
|
2019-07-31 19:25:30 +00:00
|
|
|
vol.Optional(CONF_EXCLUDE, default=[]): vol.All(cv.ensure_list, [cv.string]),
|
|
|
|
vol.Optional(CONF_OPTIONS, default=DEFAULT_OPTIONS): cv.string,
|
|
|
|
}
|
|
|
|
)
|
2016-09-18 06:44:15 +00:00
|
|
|
|
2014-12-15 05:29:36 +00:00
|
|
|
|
2021-08-21 19:25:28 +00:00
|
|
|
async def async_get_scanner(hass: HomeAssistant, config: ConfigType) -> None:
|
2016-03-07 20:18:53 +00:00
|
|
|
"""Validate the configuration and return a Nmap scanner."""
|
2021-08-16 20:19:32 +00:00
|
|
|
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
|
|
|
|
|
2021-08-30 03:38:41 +00:00
|
|
|
if CONF_CONSIDER_HOME in validated_config:
|
|
|
|
consider_home = validated_config[CONF_CONSIDER_HOME].total_seconds()
|
|
|
|
else:
|
|
|
|
consider_home = DEFAULT_CONSIDER_HOME.total_seconds()
|
|
|
|
|
2021-08-16 20:19:32 +00:00
|
|
|
import_config = {
|
|
|
|
CONF_HOSTS: ",".join(validated_config[CONF_HOSTS]),
|
|
|
|
CONF_HOME_INTERVAL: validated_config[CONF_HOME_INTERVAL],
|
2021-08-30 03:38:41 +00:00
|
|
|
CONF_CONSIDER_HOME: consider_home,
|
2021-08-16 20:19:32 +00:00
|
|
|
CONF_EXCLUDE: ",".join(validated_config[CONF_EXCLUDE]),
|
|
|
|
CONF_OPTIONS: validated_config[CONF_OPTIONS],
|
|
|
|
CONF_SCAN_INTERVAL: scan_interval,
|
|
|
|
}
|
2021-07-06 16:28:23 +00:00
|
|
|
|
2021-08-16 20:19:32 +00:00
|
|
|
hass.async_create_task(
|
|
|
|
hass.config_entries.flow.async_init(
|
|
|
|
DOMAIN,
|
|
|
|
context={"source": SOURCE_IMPORT},
|
|
|
|
data=import_config,
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
_LOGGER.warning(
|
|
|
|
"Your Nmap Tracker configuration has been imported into the UI, "
|
|
|
|
"please remove it from configuration.yaml. "
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
async def async_setup_entry(
|
|
|
|
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: Callable
|
|
|
|
) -> None:
|
|
|
|
"""Set up device tracker for Nmap Tracker component."""
|
|
|
|
nmap_tracker = hass.data[DOMAIN][entry.entry_id]
|
|
|
|
|
|
|
|
@callback
|
|
|
|
def device_new(mac_address):
|
|
|
|
"""Signal a new device."""
|
|
|
|
async_add_entities([NmapTrackerEntity(nmap_tracker, mac_address, True)])
|
|
|
|
|
|
|
|
@callback
|
|
|
|
def device_missing(mac_address):
|
|
|
|
"""Signal a missing device."""
|
|
|
|
async_add_entities([NmapTrackerEntity(nmap_tracker, mac_address, False)])
|
|
|
|
|
|
|
|
entry.async_on_unload(
|
|
|
|
async_dispatcher_connect(hass, nmap_tracker.signal_device_new, device_new)
|
|
|
|
)
|
|
|
|
entry.async_on_unload(
|
|
|
|
async_dispatcher_connect(
|
|
|
|
hass, nmap_tracker.signal_device_missing, device_missing
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
class NmapTrackerEntity(ScannerEntity):
|
|
|
|
"""An Nmap Tracker entity."""
|
|
|
|
|
|
|
|
def __init__(
|
|
|
|
self, nmap_tracker: NmapDeviceScanner, mac_address: str, active: bool
|
|
|
|
) -> None:
|
|
|
|
"""Initialize an nmap tracker entity."""
|
|
|
|
self._mac_address = mac_address
|
|
|
|
self._nmap_tracker = nmap_tracker
|
|
|
|
self._tracked = self._nmap_tracker.devices.tracked
|
|
|
|
self._active = active
|
|
|
|
|
|
|
|
@property
|
2021-08-21 19:25:28 +00:00
|
|
|
def _device(self) -> NmapDevice:
|
2021-08-16 20:19:32 +00:00
|
|
|
"""Get latest device state."""
|
|
|
|
return self._tracked[self._mac_address]
|
|
|
|
|
|
|
|
@property
|
|
|
|
def is_connected(self) -> bool:
|
|
|
|
"""Return device status."""
|
|
|
|
return self._active
|
|
|
|
|
|
|
|
@property
|
|
|
|
def name(self) -> str:
|
|
|
|
"""Return device name."""
|
|
|
|
return self._device.name
|
|
|
|
|
|
|
|
@property
|
|
|
|
def unique_id(self) -> str:
|
|
|
|
"""Return device unique id."""
|
|
|
|
return self._mac_address
|
|
|
|
|
|
|
|
@property
|
|
|
|
def ip_address(self) -> str:
|
|
|
|
"""Return the primary ip address of the device."""
|
|
|
|
return self._device.ipv4
|
|
|
|
|
|
|
|
@property
|
|
|
|
def mac_address(self) -> str:
|
|
|
|
"""Return the mac address of the device."""
|
|
|
|
return self._mac_address
|
|
|
|
|
|
|
|
@property
|
2021-08-21 19:25:28 +00:00
|
|
|
def hostname(self) -> str | None:
|
2021-08-16 20:19:32 +00:00
|
|
|
"""Return hostname of the device."""
|
2021-08-21 19:25:28 +00:00
|
|
|
if not self._device.hostname:
|
|
|
|
return None
|
2021-08-16 20:19:32 +00:00
|
|
|
return short_hostname(self._device.hostname)
|
|
|
|
|
|
|
|
@property
|
|
|
|
def source_type(self) -> str:
|
|
|
|
"""Return tracker source type."""
|
|
|
|
return SOURCE_TYPE_ROUTER
|
|
|
|
|
|
|
|
@property
|
2021-08-21 19:25:28 +00:00
|
|
|
def device_info(self) -> DeviceInfo:
|
2021-08-16 20:19:32 +00:00
|
|
|
"""Return the device information."""
|
|
|
|
return {
|
|
|
|
"connections": {(CONNECTION_NETWORK_MAC, self._mac_address)},
|
|
|
|
"default_manufacturer": self._device.manufacturer,
|
|
|
|
"default_name": self.name,
|
|
|
|
}
|
|
|
|
|
|
|
|
@property
|
|
|
|
def should_poll(self) -> bool:
|
|
|
|
"""No polling needed."""
|
|
|
|
return False
|
|
|
|
|
|
|
|
@property
|
2021-08-21 19:25:28 +00:00
|
|
|
def icon(self) -> str:
|
2021-08-16 20:19:32 +00:00
|
|
|
"""Return device icon."""
|
|
|
|
return "mdi:lan-connect" if self._active else "mdi:lan-disconnect"
|
|
|
|
|
|
|
|
@callback
|
|
|
|
def async_process_update(self, online: bool) -> None:
|
|
|
|
"""Update device."""
|
|
|
|
self._active = online
|
|
|
|
|
|
|
|
@property
|
2021-08-21 19:25:28 +00:00
|
|
|
def extra_state_attributes(self) -> dict[str, Any]:
|
2021-08-16 20:19:32 +00:00
|
|
|
"""Return the attributes."""
|
|
|
|
return {
|
|
|
|
"last_time_reachable": self._device.last_update.isoformat(
|
|
|
|
timespec="seconds"
|
|
|
|
),
|
|
|
|
"reason": self._device.reason,
|
|
|
|
}
|
|
|
|
|
|
|
|
@callback
|
2021-08-21 19:25:28 +00:00
|
|
|
def async_on_demand_update(self, online: bool) -> None:
|
2021-08-16 20:19:32 +00:00
|
|
|
"""Update state."""
|
|
|
|
self.async_process_update(online)
|
|
|
|
self.async_write_ha_state()
|
|
|
|
|
2021-08-21 19:25:28 +00:00
|
|
|
async def async_added_to_hass(self) -> None:
|
2021-08-16 20:19:32 +00:00
|
|
|
"""Register state update callback."""
|
|
|
|
self.async_on_remove(
|
|
|
|
async_dispatcher_connect(
|
|
|
|
self.hass,
|
|
|
|
signal_device_update(self._mac_address),
|
|
|
|
self.async_on_demand_update,
|
|
|
|
)
|
2019-07-31 19:25:30 +00:00
|
|
|
)
|