|
|
|
@@ -1,52 +1,82 @@
|
|
|
|
|
"""
|
|
|
|
|
homeassistant.components.tracker
|
|
|
|
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
|
homeassistant.components.device_tracker
|
|
|
|
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
|
|
|
|
|
|
Provides functionality to keep track of devices.
|
|
|
|
|
|
|
|
|
|
device_tracker:
|
|
|
|
|
platform: netgear
|
|
|
|
|
|
|
|
|
|
# Optional
|
|
|
|
|
|
|
|
|
|
# How many seconds to wait after not seeing device to consider it not home
|
|
|
|
|
consider_home: 180
|
|
|
|
|
|
|
|
|
|
# Seconds between each scan
|
|
|
|
|
interval_seconds: 12
|
|
|
|
|
|
|
|
|
|
# New found devices auto found
|
|
|
|
|
track_new_devices: yes
|
|
|
|
|
"""
|
|
|
|
|
import logging
|
|
|
|
|
import threading
|
|
|
|
|
import os
|
|
|
|
|
import csv
|
|
|
|
|
from datetime import timedelta
|
|
|
|
|
import logging
|
|
|
|
|
import os
|
|
|
|
|
import threading
|
|
|
|
|
|
|
|
|
|
from homeassistant.helpers import validate_config
|
|
|
|
|
from homeassistant.helpers.entity import _OVERWRITE
|
|
|
|
|
from homeassistant.bootstrap import prepare_setup_platform
|
|
|
|
|
from homeassistant.components import discovery, group
|
|
|
|
|
from homeassistant.config import load_yaml_config_file
|
|
|
|
|
from homeassistant.exceptions import HomeAssistantError
|
|
|
|
|
from homeassistant.helpers import config_per_platform
|
|
|
|
|
from homeassistant.helpers.entity import Entity
|
|
|
|
|
import homeassistant.util as util
|
|
|
|
|
import homeassistant.util.dt as dt_util
|
|
|
|
|
from homeassistant.bootstrap import prepare_setup_platform
|
|
|
|
|
|
|
|
|
|
from homeassistant.helpers.event import track_utc_time_change
|
|
|
|
|
from homeassistant.const import (
|
|
|
|
|
STATE_HOME, STATE_NOT_HOME, ATTR_ENTITY_PICTURE, ATTR_FRIENDLY_NAME,
|
|
|
|
|
CONF_PLATFORM, DEVICE_DEFAULT_NAME)
|
|
|
|
|
from homeassistant.components import group
|
|
|
|
|
ATTR_ENTITY_PICTURE, DEVICE_DEFAULT_NAME, STATE_HOME, STATE_NOT_HOME)
|
|
|
|
|
|
|
|
|
|
DOMAIN = "device_tracker"
|
|
|
|
|
DEPENDENCIES = []
|
|
|
|
|
|
|
|
|
|
SERVICE_DEVICE_TRACKER_RELOAD = "reload_devices_csv"
|
|
|
|
|
|
|
|
|
|
GROUP_NAME_ALL_DEVICES = 'all devices'
|
|
|
|
|
ENTITY_ID_ALL_DEVICES = group.ENTITY_ID_FORMAT.format('all_devices')
|
|
|
|
|
|
|
|
|
|
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
|
|
|
|
|
|
|
|
|
# After how much time do we consider a device not home if
|
|
|
|
|
# it does not show up on scans
|
|
|
|
|
TIME_DEVICE_NOT_FOUND = timedelta(minutes=3)
|
|
|
|
|
CSV_DEVICES = "known_devices.csv"
|
|
|
|
|
YAML_DEVICES = 'known_devices.yaml'
|
|
|
|
|
|
|
|
|
|
# Filename to save known devices to
|
|
|
|
|
KNOWN_DEVICES_FILE = "known_devices.csv"
|
|
|
|
|
CONF_TRACK_NEW = "track_new_devices"
|
|
|
|
|
DEFAULT_CONF_TRACK_NEW = True
|
|
|
|
|
|
|
|
|
|
CONF_SECONDS = "interval_seconds"
|
|
|
|
|
CONF_CONSIDER_HOME = 'consider_home'
|
|
|
|
|
DEFAULT_CONF_CONSIDER_HOME = 180 # seconds
|
|
|
|
|
|
|
|
|
|
DEFAULT_CONF_SECONDS = 12
|
|
|
|
|
CONF_SCAN_INTERVAL = "interval_seconds"
|
|
|
|
|
DEFAULT_SCAN_INTERVAL = 12
|
|
|
|
|
|
|
|
|
|
TRACK_NEW_DEVICES = "track_new_devices"
|
|
|
|
|
CONF_AWAY_HIDE = 'hide_if_away'
|
|
|
|
|
DEFAULT_AWAY_HIDE = False
|
|
|
|
|
|
|
|
|
|
SERVICE_SEE = 'see'
|
|
|
|
|
|
|
|
|
|
ATTR_LATITUDE = 'latitude'
|
|
|
|
|
ATTR_LONGITUDE = 'longitude'
|
|
|
|
|
ATTR_MAC = 'mac'
|
|
|
|
|
ATTR_DEV_ID = 'dev_id'
|
|
|
|
|
ATTR_HOST_NAME = 'host_name'
|
|
|
|
|
ATTR_LOCATION_NAME = 'location_name'
|
|
|
|
|
ATTR_GPS = 'gps'
|
|
|
|
|
|
|
|
|
|
DISCOVERY_PLATFORMS = {
|
|
|
|
|
discovery.SERVICE_NETGEAR: 'netgear',
|
|
|
|
|
}
|
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
# pylint: disable=too-many-arguments
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def is_on(hass, entity_id=None):
|
|
|
|
|
""" Returns if any or specified device is home. """
|
|
|
|
@@ -55,293 +85,309 @@ def is_on(hass, entity_id=None):
|
|
|
|
|
return hass.states.is_state(entity, STATE_HOME)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def see(hass, mac=None, dev_id=None, host_name=None, location_name=None,
|
|
|
|
|
gps=None):
|
|
|
|
|
""" Call service to notify you see device. """
|
|
|
|
|
data = {key: value for key, value in
|
|
|
|
|
((ATTR_MAC, mac),
|
|
|
|
|
(ATTR_DEV_ID, dev_id),
|
|
|
|
|
(ATTR_HOST_NAME, host_name),
|
|
|
|
|
(ATTR_LOCATION_NAME, location_name),
|
|
|
|
|
(ATTR_GPS, gps)) if value is not None}
|
|
|
|
|
hass.services.call(DOMAIN, SERVICE_SEE, data)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def setup(hass, config):
|
|
|
|
|
""" Sets up the device tracker. """
|
|
|
|
|
""" Setup device tracker """
|
|
|
|
|
yaml_path = hass.config.path(YAML_DEVICES)
|
|
|
|
|
csv_path = hass.config.path(CSV_DEVICES)
|
|
|
|
|
if os.path.isfile(csv_path) and not os.path.isfile(yaml_path) and \
|
|
|
|
|
convert_csv_config(csv_path, yaml_path):
|
|
|
|
|
os.remove(csv_path)
|
|
|
|
|
|
|
|
|
|
if not validate_config(config, {DOMAIN: [CONF_PLATFORM]}, _LOGGER):
|
|
|
|
|
return False
|
|
|
|
|
conf = config.get(DOMAIN, {})
|
|
|
|
|
consider_home = util.convert(conf.get(CONF_CONSIDER_HOME), int,
|
|
|
|
|
DEFAULT_CONF_CONSIDER_HOME)
|
|
|
|
|
track_new = util.convert(conf.get(CONF_TRACK_NEW), bool,
|
|
|
|
|
DEFAULT_CONF_TRACK_NEW)
|
|
|
|
|
|
|
|
|
|
tracker_type = config[DOMAIN].get(CONF_PLATFORM)
|
|
|
|
|
devices = load_config(yaml_path, hass, timedelta(seconds=consider_home))
|
|
|
|
|
tracker = DeviceTracker(hass, consider_home, track_new, devices)
|
|
|
|
|
|
|
|
|
|
tracker_implementation = \
|
|
|
|
|
prepare_setup_platform(hass, config, DOMAIN, tracker_type)
|
|
|
|
|
|
|
|
|
|
if tracker_implementation is None:
|
|
|
|
|
_LOGGER.error("Unknown device_tracker type specified: %s.",
|
|
|
|
|
tracker_type)
|
|
|
|
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
device_scanner = tracker_implementation.get_scanner(hass, config)
|
|
|
|
|
|
|
|
|
|
if device_scanner is None:
|
|
|
|
|
_LOGGER.error("Failed to initialize device scanner: %s",
|
|
|
|
|
tracker_type)
|
|
|
|
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
seconds = util.convert(config[DOMAIN].get(CONF_SECONDS), int,
|
|
|
|
|
DEFAULT_CONF_SECONDS)
|
|
|
|
|
|
|
|
|
|
track_new_devices = config[DOMAIN].get(TRACK_NEW_DEVICES) or False
|
|
|
|
|
_LOGGER.info("Tracking new devices: %s", track_new_devices)
|
|
|
|
|
|
|
|
|
|
tracker = DeviceTracker(hass, device_scanner, seconds, track_new_devices)
|
|
|
|
|
|
|
|
|
|
# We only succeeded if we got to parse the known devices file
|
|
|
|
|
return not tracker.invalid_known_devices_file
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class DeviceTracker(object):
|
|
|
|
|
""" Class that tracks which devices are home and which are not. """
|
|
|
|
|
|
|
|
|
|
def __init__(self, hass, device_scanner, seconds, track_new_devices):
|
|
|
|
|
self.hass = hass
|
|
|
|
|
|
|
|
|
|
self.device_scanner = device_scanner
|
|
|
|
|
|
|
|
|
|
self.lock = threading.Lock()
|
|
|
|
|
|
|
|
|
|
# Do we track new devices by default?
|
|
|
|
|
self.track_new_devices = track_new_devices
|
|
|
|
|
|
|
|
|
|
# Dictionary to keep track of known devices and devices we track
|
|
|
|
|
self.tracked = {}
|
|
|
|
|
self.untracked_devices = set()
|
|
|
|
|
|
|
|
|
|
# Did we encounter an invalid known devices file
|
|
|
|
|
self.invalid_known_devices_file = False
|
|
|
|
|
|
|
|
|
|
# Wrap it in a func instead of lambda so it can be identified in
|
|
|
|
|
# the bus by its __name__ attribute.
|
|
|
|
|
def update_device_state(now):
|
|
|
|
|
""" Triggers update of the device states. """
|
|
|
|
|
self.update_devices(now)
|
|
|
|
|
|
|
|
|
|
dev_group = group.Group(
|
|
|
|
|
hass, GROUP_NAME_ALL_DEVICES, user_defined=False)
|
|
|
|
|
|
|
|
|
|
def reload_known_devices_service(service):
|
|
|
|
|
""" Reload known devices file. """
|
|
|
|
|
self._read_known_devices_file()
|
|
|
|
|
|
|
|
|
|
self.update_devices(dt_util.utcnow())
|
|
|
|
|
|
|
|
|
|
dev_group.update_tracked_entity_ids(self.device_entity_ids)
|
|
|
|
|
|
|
|
|
|
reload_known_devices_service(None)
|
|
|
|
|
|
|
|
|
|
if self.invalid_known_devices_file:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
seconds = range(0, 60, seconds)
|
|
|
|
|
|
|
|
|
|
_LOGGER.info("Device tracker interval second=%s", seconds)
|
|
|
|
|
track_utc_time_change(hass, update_device_state, second=seconds)
|
|
|
|
|
|
|
|
|
|
hass.services.register(DOMAIN,
|
|
|
|
|
SERVICE_DEVICE_TRACKER_RELOAD,
|
|
|
|
|
reload_known_devices_service)
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def device_entity_ids(self):
|
|
|
|
|
""" Returns a set containing all device entity ids
|
|
|
|
|
that are being tracked. """
|
|
|
|
|
return set(device['entity_id'] for device in self.tracked.values())
|
|
|
|
|
|
|
|
|
|
def _update_state(self, now, device, is_home):
|
|
|
|
|
""" Update the state of a device. """
|
|
|
|
|
dev_info = self.tracked[device]
|
|
|
|
|
|
|
|
|
|
if is_home:
|
|
|
|
|
# Update last seen if at home
|
|
|
|
|
dev_info['last_seen'] = now
|
|
|
|
|
else:
|
|
|
|
|
# State remains at home if it has been seen in the last
|
|
|
|
|
# TIME_DEVICE_NOT_FOUND
|
|
|
|
|
is_home = now - dev_info['last_seen'] < TIME_DEVICE_NOT_FOUND
|
|
|
|
|
|
|
|
|
|
state = STATE_HOME if is_home else STATE_NOT_HOME
|
|
|
|
|
|
|
|
|
|
# overwrite properties that have been set in the config file
|
|
|
|
|
attr = dict(dev_info['state_attr'])
|
|
|
|
|
attr.update(_OVERWRITE.get(dev_info['entity_id'], {}))
|
|
|
|
|
|
|
|
|
|
self.hass.states.set(
|
|
|
|
|
dev_info['entity_id'], state, attr)
|
|
|
|
|
|
|
|
|
|
def update_devices(self, now):
|
|
|
|
|
""" Update device states based on the found devices. """
|
|
|
|
|
if not self.lock.acquire(False):
|
|
|
|
|
def setup_platform(p_type, p_config, disc_info=None):
|
|
|
|
|
""" Setup a device tracker platform. """
|
|
|
|
|
platform = prepare_setup_platform(hass, config, DOMAIN, p_type)
|
|
|
|
|
if platform is None:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
found_devices = set(dev.upper() for dev in
|
|
|
|
|
self.device_scanner.scan_devices())
|
|
|
|
|
if hasattr(platform, 'get_scanner'):
|
|
|
|
|
scanner = platform.get_scanner(hass, {DOMAIN: p_config})
|
|
|
|
|
|
|
|
|
|
for device in self.tracked:
|
|
|
|
|
is_home = device in found_devices
|
|
|
|
|
if scanner is None:
|
|
|
|
|
_LOGGER.error('Error setting up platform %s', p_type)
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
self._update_state(now, device, is_home)
|
|
|
|
|
setup_scanner_platform(hass, p_config, scanner, tracker.see)
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
if is_home:
|
|
|
|
|
found_devices.remove(device)
|
|
|
|
|
if not platform.setup_scanner(hass, p_config, tracker.see):
|
|
|
|
|
_LOGGER.error('Error setting up platform %s', p_type)
|
|
|
|
|
except Exception: # pylint: disable=broad-except
|
|
|
|
|
_LOGGER.exception('Error setting up platform %s', p_type)
|
|
|
|
|
|
|
|
|
|
# Did we find any devices that we didn't know about yet?
|
|
|
|
|
new_devices = found_devices - self.untracked_devices
|
|
|
|
|
for p_type, p_config in \
|
|
|
|
|
config_per_platform(config, DOMAIN, _LOGGER):
|
|
|
|
|
setup_platform(p_type, p_config)
|
|
|
|
|
|
|
|
|
|
if new_devices:
|
|
|
|
|
if not self.track_new_devices:
|
|
|
|
|
self.untracked_devices.update(new_devices)
|
|
|
|
|
def device_tracker_discovered(service, info):
|
|
|
|
|
""" Called when a device tracker platform is discovered. """
|
|
|
|
|
setup_platform(DISCOVERY_PLATFORMS[service], {}, info)
|
|
|
|
|
|
|
|
|
|
self._update_known_devices_file(new_devices)
|
|
|
|
|
finally:
|
|
|
|
|
self.lock.release()
|
|
|
|
|
discovery.listen(hass, DISCOVERY_PLATFORMS.keys(),
|
|
|
|
|
device_tracker_discovered)
|
|
|
|
|
|
|
|
|
|
# pylint: disable=too-many-branches
|
|
|
|
|
def _read_known_devices_file(self):
|
|
|
|
|
""" Parse and process the known devices file. """
|
|
|
|
|
known_dev_path = self.hass.config.path(KNOWN_DEVICES_FILE)
|
|
|
|
|
def update_stale(now):
|
|
|
|
|
""" Clean up stale devices. """
|
|
|
|
|
tracker.update_stale(now)
|
|
|
|
|
track_utc_time_change(hass, update_stale, second=range(0, 60, 5))
|
|
|
|
|
|
|
|
|
|
# Return if no known devices file exists
|
|
|
|
|
if not os.path.isfile(known_dev_path):
|
|
|
|
|
tracker.setup_group()
|
|
|
|
|
|
|
|
|
|
def see_service(call):
|
|
|
|
|
""" Service to see a device. """
|
|
|
|
|
args = {key: value for key, value in call.data.items() if key in
|
|
|
|
|
(ATTR_MAC, ATTR_DEV_ID, ATTR_HOST_NAME, ATTR_LOCATION_NAME,
|
|
|
|
|
ATTR_GPS)}
|
|
|
|
|
tracker.see(**args)
|
|
|
|
|
|
|
|
|
|
hass.services.register(DOMAIN, SERVICE_SEE, see_service)
|
|
|
|
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class DeviceTracker(object):
|
|
|
|
|
""" Track devices """
|
|
|
|
|
def __init__(self, hass, consider_home, track_new, devices):
|
|
|
|
|
self.hass = hass
|
|
|
|
|
self.devices = {dev.dev_id: dev for dev in devices}
|
|
|
|
|
self.mac_to_dev = {dev.mac: dev for dev in devices if dev.mac}
|
|
|
|
|
self.consider_home = timedelta(seconds=consider_home)
|
|
|
|
|
self.track_new = track_new
|
|
|
|
|
self.lock = threading.Lock()
|
|
|
|
|
|
|
|
|
|
entity_ids = []
|
|
|
|
|
for device in devices:
|
|
|
|
|
if device.track:
|
|
|
|
|
entity_ids.append(device.entity_id)
|
|
|
|
|
device.update_ha_state()
|
|
|
|
|
|
|
|
|
|
self.group = None
|
|
|
|
|
|
|
|
|
|
def see(self, mac=None, dev_id=None, host_name=None, location_name=None,
|
|
|
|
|
gps=None):
|
|
|
|
|
""" Notify device tracker that you see a device. """
|
|
|
|
|
with self.lock:
|
|
|
|
|
if mac is None and dev_id is None:
|
|
|
|
|
raise HomeAssistantError('Neither mac or device id passed in')
|
|
|
|
|
elif mac is not None:
|
|
|
|
|
mac = mac.upper()
|
|
|
|
|
device = self.mac_to_dev.get(mac)
|
|
|
|
|
if not device:
|
|
|
|
|
dev_id = util.slugify(host_name or mac)
|
|
|
|
|
else:
|
|
|
|
|
dev_id = str(dev_id)
|
|
|
|
|
device = self.devices.get(dev_id)
|
|
|
|
|
|
|
|
|
|
if device:
|
|
|
|
|
device.seen(host_name, location_name, gps)
|
|
|
|
|
if device.track:
|
|
|
|
|
device.update_ha_state()
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
# If no device can be found, create it
|
|
|
|
|
device = Device(
|
|
|
|
|
self.hass, self.consider_home, self.track_new, dev_id, mac,
|
|
|
|
|
(host_name or dev_id).replace('_', ' '))
|
|
|
|
|
self.devices[dev_id] = device
|
|
|
|
|
if mac is not None:
|
|
|
|
|
self.mac_to_dev[mac] = device
|
|
|
|
|
|
|
|
|
|
device.seen(host_name, location_name, gps)
|
|
|
|
|
if device.track:
|
|
|
|
|
device.update_ha_state()
|
|
|
|
|
|
|
|
|
|
# During init, we ignore the group
|
|
|
|
|
if self.group is not None:
|
|
|
|
|
self.group.update_tracked_entity_ids(
|
|
|
|
|
list(self.group.tracking) + [device.entity_id])
|
|
|
|
|
update_config(self.hass.config.path(YAML_DEVICES), dev_id, device)
|
|
|
|
|
|
|
|
|
|
def setup_group(self):
|
|
|
|
|
""" Initializes group for all tracked devices. """
|
|
|
|
|
entity_ids = (dev.entity_id for dev in self.devices.values()
|
|
|
|
|
if dev.track)
|
|
|
|
|
self.group = group.setup_group(
|
|
|
|
|
self.hass, GROUP_NAME_ALL_DEVICES, entity_ids, False)
|
|
|
|
|
|
|
|
|
|
def update_stale(self, now):
|
|
|
|
|
""" Update stale devices. """
|
|
|
|
|
with self.lock:
|
|
|
|
|
for device in self.devices.values():
|
|
|
|
|
if device.last_update_home and device.stale(now):
|
|
|
|
|
device.update_ha_state(True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Device(Entity):
|
|
|
|
|
""" Tracked device. """
|
|
|
|
|
# pylint: disable=too-many-instance-attributes, too-many-arguments
|
|
|
|
|
|
|
|
|
|
host_name = None
|
|
|
|
|
location_name = None
|
|
|
|
|
gps = None
|
|
|
|
|
last_seen = None
|
|
|
|
|
|
|
|
|
|
# Track if the last update of this device was HOME
|
|
|
|
|
last_update_home = False
|
|
|
|
|
_state = STATE_NOT_HOME
|
|
|
|
|
|
|
|
|
|
def __init__(self, hass, consider_home, track, dev_id, mac, name=None,
|
|
|
|
|
picture=None, away_hide=False):
|
|
|
|
|
self.hass = hass
|
|
|
|
|
self.entity_id = ENTITY_ID_FORMAT.format(dev_id)
|
|
|
|
|
|
|
|
|
|
# Timedelta object how long we consider a device home if it is not
|
|
|
|
|
# detected anymore.
|
|
|
|
|
self.consider_home = consider_home
|
|
|
|
|
|
|
|
|
|
# Device ID
|
|
|
|
|
self.dev_id = dev_id
|
|
|
|
|
self.mac = mac
|
|
|
|
|
|
|
|
|
|
# If we should track this device
|
|
|
|
|
self.track = track
|
|
|
|
|
|
|
|
|
|
# Configured name
|
|
|
|
|
self.config_name = name
|
|
|
|
|
|
|
|
|
|
# Configured picture
|
|
|
|
|
self.config_picture = picture
|
|
|
|
|
self.away_hide = away_hide
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def name(self):
|
|
|
|
|
""" Returns the name of the entity. """
|
|
|
|
|
return self.config_name or self.host_name or DEVICE_DEFAULT_NAME
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def state(self):
|
|
|
|
|
""" State of the device. """
|
|
|
|
|
return self._state
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def state_attributes(self):
|
|
|
|
|
""" Device state attributes. """
|
|
|
|
|
attr = {}
|
|
|
|
|
|
|
|
|
|
if self.config_picture:
|
|
|
|
|
attr[ATTR_ENTITY_PICTURE] = self.config_picture
|
|
|
|
|
|
|
|
|
|
if self.gps:
|
|
|
|
|
attr[ATTR_LATITUDE] = self.gps[0],
|
|
|
|
|
attr[ATTR_LONGITUDE] = self.gps[1],
|
|
|
|
|
|
|
|
|
|
return attr
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def hidden(self):
|
|
|
|
|
""" If device should be hidden. """
|
|
|
|
|
return self.away_hide and self.state != STATE_HOME
|
|
|
|
|
|
|
|
|
|
def seen(self, host_name=None, location_name=None, gps=None):
|
|
|
|
|
""" Mark the device as seen. """
|
|
|
|
|
self.last_seen = dt_util.utcnow()
|
|
|
|
|
self.host_name = host_name
|
|
|
|
|
self.location_name = location_name
|
|
|
|
|
self.gps = gps
|
|
|
|
|
self.update()
|
|
|
|
|
|
|
|
|
|
def stale(self, now=None):
|
|
|
|
|
""" Return if device state is stale. """
|
|
|
|
|
return self.last_seen and \
|
|
|
|
|
(now or dt_util.utcnow()) - self.last_seen > self.consider_home
|
|
|
|
|
|
|
|
|
|
def update(self):
|
|
|
|
|
""" Update state of entity. """
|
|
|
|
|
if not self.last_seen:
|
|
|
|
|
return
|
|
|
|
|
elif self.location_name:
|
|
|
|
|
self._state = self.location_name
|
|
|
|
|
elif self.stale():
|
|
|
|
|
self._state = STATE_NOT_HOME
|
|
|
|
|
self.last_update_home = False
|
|
|
|
|
else:
|
|
|
|
|
self._state = STATE_HOME
|
|
|
|
|
self.last_update_home = True
|
|
|
|
|
|
|
|
|
|
self.lock.acquire()
|
|
|
|
|
|
|
|
|
|
self.untracked_devices.clear()
|
|
|
|
|
def convert_csv_config(csv_path, yaml_path):
|
|
|
|
|
""" Convert CSV config file format to YAML. """
|
|
|
|
|
used_ids = set()
|
|
|
|
|
with open(csv_path) as inp:
|
|
|
|
|
for row in csv.DictReader(inp):
|
|
|
|
|
dev_id = util.ensure_unique_string(util.slugify(row['name']),
|
|
|
|
|
used_ids)
|
|
|
|
|
used_ids.add(dev_id)
|
|
|
|
|
device = Device(None, None, row['track'] == '1', dev_id,
|
|
|
|
|
row['device'], row['name'], row['picture'])
|
|
|
|
|
update_config(yaml_path, dev_id, device)
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
with open(known_dev_path) as inp:
|
|
|
|
|
|
|
|
|
|
# To track which devices need an entity_id assigned
|
|
|
|
|
need_entity_id = []
|
|
|
|
|
def load_config(path, hass, consider_home):
|
|
|
|
|
""" Load devices from YAML config file. """
|
|
|
|
|
if not os.path.isfile(path):
|
|
|
|
|
return []
|
|
|
|
|
return [
|
|
|
|
|
Device(hass, consider_home, device.get('track', False),
|
|
|
|
|
str(dev_id), device.get('mac'), device.get('name'),
|
|
|
|
|
device.get('picture'), device.get(CONF_AWAY_HIDE, False))
|
|
|
|
|
for dev_id, device in load_yaml_config_file(path).items()]
|
|
|
|
|
|
|
|
|
|
# All devices that are still in this set after we read the CSV file
|
|
|
|
|
# have been removed from the file and thus need to be cleaned up.
|
|
|
|
|
removed_devices = set(self.tracked.keys())
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
for row in csv.DictReader(inp):
|
|
|
|
|
device = row['device'].upper()
|
|
|
|
|
def setup_scanner_platform(hass, config, scanner, see_device):
|
|
|
|
|
""" Helper method to connect scanner-based platform to device tracker. """
|
|
|
|
|
interval = util.convert(config.get(CONF_SCAN_INTERVAL), int,
|
|
|
|
|
DEFAULT_SCAN_INTERVAL)
|
|
|
|
|
|
|
|
|
|
if row['track'] == '1':
|
|
|
|
|
if device in self.tracked:
|
|
|
|
|
# Device exists
|
|
|
|
|
removed_devices.remove(device)
|
|
|
|
|
else:
|
|
|
|
|
# We found a new device
|
|
|
|
|
need_entity_id.append(device)
|
|
|
|
|
# Initial scan of each mac we also tell about host name for config
|
|
|
|
|
seen = set()
|
|
|
|
|
|
|
|
|
|
self._track_device(device, row['name'])
|
|
|
|
|
def device_tracker_scan(now):
|
|
|
|
|
""" Called when interval matches. """
|
|
|
|
|
for mac in scanner.scan_devices():
|
|
|
|
|
if mac in seen:
|
|
|
|
|
host_name = None
|
|
|
|
|
else:
|
|
|
|
|
host_name = scanner.get_device_name(mac)
|
|
|
|
|
seen.add(mac)
|
|
|
|
|
see_device(mac=mac, host_name=host_name)
|
|
|
|
|
|
|
|
|
|
# Update state_attr with latest from file
|
|
|
|
|
state_attr = {
|
|
|
|
|
ATTR_FRIENDLY_NAME: row['name']
|
|
|
|
|
}
|
|
|
|
|
track_utc_time_change(hass, device_tracker_scan, second=range(0, 60,
|
|
|
|
|
interval))
|
|
|
|
|
|
|
|
|
|
if row['picture']:
|
|
|
|
|
state_attr[ATTR_ENTITY_PICTURE] = row['picture']
|
|
|
|
|
device_tracker_scan(None)
|
|
|
|
|
|
|
|
|
|
self.tracked[device]['state_attr'] = state_attr
|
|
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
self.untracked_devices.add(device)
|
|
|
|
|
def update_config(path, dev_id, device):
|
|
|
|
|
""" Add device to YAML config file. """
|
|
|
|
|
with open(path, 'a') as out:
|
|
|
|
|
out.write('\n')
|
|
|
|
|
out.write('{}:\n'.format(device.dev_id))
|
|
|
|
|
|
|
|
|
|
# Remove existing devices that we no longer track
|
|
|
|
|
for device in removed_devices:
|
|
|
|
|
entity_id = self.tracked[device]['entity_id']
|
|
|
|
|
|
|
|
|
|
_LOGGER.info("Removing entity %s", entity_id)
|
|
|
|
|
|
|
|
|
|
self.hass.states.remove(entity_id)
|
|
|
|
|
|
|
|
|
|
self.tracked.pop(device)
|
|
|
|
|
|
|
|
|
|
self._generate_entity_ids(need_entity_id)
|
|
|
|
|
|
|
|
|
|
if not self.tracked:
|
|
|
|
|
_LOGGER.warning(
|
|
|
|
|
"No devices to track. Please update %s.",
|
|
|
|
|
known_dev_path)
|
|
|
|
|
|
|
|
|
|
_LOGGER.info("Loaded devices from %s", known_dev_path)
|
|
|
|
|
|
|
|
|
|
except KeyError:
|
|
|
|
|
self.invalid_known_devices_file = True
|
|
|
|
|
|
|
|
|
|
_LOGGER.warning(
|
|
|
|
|
("Invalid known devices file: %s. "
|
|
|
|
|
"We won't update it with new found devices."),
|
|
|
|
|
known_dev_path)
|
|
|
|
|
|
|
|
|
|
finally:
|
|
|
|
|
self.lock.release()
|
|
|
|
|
|
|
|
|
|
def _update_known_devices_file(self, new_devices):
|
|
|
|
|
""" Add new devices to known devices file. """
|
|
|
|
|
if not self.invalid_known_devices_file:
|
|
|
|
|
known_dev_path = self.hass.config.path(KNOWN_DEVICES_FILE)
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
# If file does not exist we will write the header too
|
|
|
|
|
is_new_file = not os.path.isfile(known_dev_path)
|
|
|
|
|
|
|
|
|
|
with open(known_dev_path, 'a') as outp:
|
|
|
|
|
_LOGGER.info("Found %d new devices, updating %s",
|
|
|
|
|
len(new_devices), known_dev_path)
|
|
|
|
|
|
|
|
|
|
writer = csv.writer(outp)
|
|
|
|
|
|
|
|
|
|
if is_new_file:
|
|
|
|
|
writer.writerow(("device", "name", "track", "picture"))
|
|
|
|
|
|
|
|
|
|
for device in new_devices:
|
|
|
|
|
# See if the device scanner knows the name
|
|
|
|
|
# else defaults to unknown device
|
|
|
|
|
name = self.device_scanner.get_device_name(device) or \
|
|
|
|
|
DEVICE_DEFAULT_NAME
|
|
|
|
|
|
|
|
|
|
track = 0
|
|
|
|
|
if self.track_new_devices:
|
|
|
|
|
self._track_device(device, name)
|
|
|
|
|
track = 1
|
|
|
|
|
|
|
|
|
|
writer.writerow((device, name, track, ""))
|
|
|
|
|
|
|
|
|
|
if self.track_new_devices:
|
|
|
|
|
self._generate_entity_ids(new_devices)
|
|
|
|
|
|
|
|
|
|
except IOError:
|
|
|
|
|
_LOGGER.exception("Error updating %s with %d new devices",
|
|
|
|
|
known_dev_path, len(new_devices))
|
|
|
|
|
|
|
|
|
|
def _track_device(self, device, name):
|
|
|
|
|
"""
|
|
|
|
|
Add a device to the list of tracked devices.
|
|
|
|
|
Does not generate the entity id yet.
|
|
|
|
|
"""
|
|
|
|
|
default_last_seen = dt_util.utcnow().replace(year=1990)
|
|
|
|
|
|
|
|
|
|
self.tracked[device] = {
|
|
|
|
|
'name': name,
|
|
|
|
|
'last_seen': default_last_seen,
|
|
|
|
|
'state_attr': {ATTR_FRIENDLY_NAME: name}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
def _generate_entity_ids(self, need_entity_id):
|
|
|
|
|
""" Generate entity ids for a list of devices. """
|
|
|
|
|
# Setup entity_ids for the new devices
|
|
|
|
|
used_entity_ids = [info['entity_id'] for device, info
|
|
|
|
|
in self.tracked.items()
|
|
|
|
|
if device not in need_entity_id]
|
|
|
|
|
|
|
|
|
|
for device in need_entity_id:
|
|
|
|
|
name = self.tracked[device]['name']
|
|
|
|
|
|
|
|
|
|
entity_id = util.ensure_unique_string(
|
|
|
|
|
ENTITY_ID_FORMAT.format(util.slugify(name)),
|
|
|
|
|
used_entity_ids)
|
|
|
|
|
|
|
|
|
|
used_entity_ids.append(entity_id)
|
|
|
|
|
|
|
|
|
|
self.tracked[device]['entity_id'] = entity_id
|
|
|
|
|
for key, value in (('name', device.name), ('mac', device.mac),
|
|
|
|
|
('picture', device.config_picture),
|
|
|
|
|
('track', 'yes' if device.track else 'no'),
|
|
|
|
|
(CONF_AWAY_HIDE,
|
|
|
|
|
'yes' if device.away_hide else 'no')):
|
|
|
|
|
out.write(' {}: {}\n'.format(key, '' if value is None else value))
|
|
|
|
|