mirror of
https://github.com/home-assistant/core.git
synced 2025-04-23 16:57:53 +00:00
All platforms supported by components have their own file - you can can have custom platforms
This commit is contained in:
parent
8c6e6e464e
commit
9f9b926011
@ -1,652 +0,0 @@
|
||||
"""
|
||||
homeassistant.components.tracker
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Provides functionality to keep track of devices.
|
||||
"""
|
||||
import logging
|
||||
import threading
|
||||
import os
|
||||
import csv
|
||||
import re
|
||||
import json
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import requests
|
||||
|
||||
import homeassistant as ha
|
||||
import homeassistant.util as util
|
||||
import homeassistant.components as components
|
||||
|
||||
from homeassistant.components import group
|
||||
|
||||
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(
|
||||
GROUP_NAME_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_SPAN_FOR_ERROR_IN_SCANNING = timedelta(minutes=3)
|
||||
|
||||
# Return cached results if last scan was less then this time ago
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
|
||||
|
||||
# Filename to save known devices to
|
||||
KNOWN_DEVICES_FILE = "known_devices.csv"
|
||||
|
||||
CONF_HTTP_ID = "http_id"
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def is_on(hass, entity_id=None):
|
||||
""" Returns if any or specified device is home. """
|
||||
entity = entity_id or ENTITY_ID_ALL_DEVICES
|
||||
|
||||
return hass.states.is_state(entity, components.STATE_HOME)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
""" Sets up the device tracker. """
|
||||
|
||||
# We have flexible requirements for device tracker so
|
||||
# we cannot use util.validate_config
|
||||
|
||||
conf = config[DOMAIN]
|
||||
|
||||
if ha.CONF_TYPE not in conf:
|
||||
_LOGGER.error(
|
||||
'Missing required configuration item in %s: %s',
|
||||
DOMAIN, ha.CONF_TYPE)
|
||||
|
||||
return False
|
||||
|
||||
fields = [ha.CONF_HOST, ha.CONF_USERNAME, ha.CONF_PASSWORD]
|
||||
|
||||
router_type = conf[ha.CONF_TYPE]
|
||||
|
||||
if router_type == 'tomato':
|
||||
fields.append(CONF_HTTP_ID)
|
||||
|
||||
scanner = TomatoDeviceScanner
|
||||
|
||||
elif router_type == 'netgear':
|
||||
scanner = NetgearDeviceScanner
|
||||
|
||||
elif router_type == 'luci':
|
||||
scanner = LuciDeviceScanner
|
||||
|
||||
else:
|
||||
_LOGGER.error('Found unknown router type %s', router_type)
|
||||
|
||||
return False
|
||||
|
||||
if not util.validate_config(config, {DOMAIN: fields}, _LOGGER):
|
||||
return False
|
||||
|
||||
device_scanner = scanner(conf)
|
||||
|
||||
if not device_scanner.success_init:
|
||||
_LOGGER.error("Failed to initialize device scanner for %s",
|
||||
router_type)
|
||||
|
||||
return False
|
||||
|
||||
DeviceTracker(hass, device_scanner)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
class DeviceTracker(object):
|
||||
""" Class that tracks which devices are home and which are not. """
|
||||
|
||||
def __init__(self, hass, device_scanner):
|
||||
self.states = hass.states
|
||||
|
||||
self.device_scanner = device_scanner
|
||||
|
||||
self.error_scanning = TIME_SPAN_FOR_ERROR_IN_SCANNING
|
||||
|
||||
self.lock = threading.Lock()
|
||||
|
||||
self.path_known_devices_file = hass.get_config_path(KNOWN_DEVICES_FILE)
|
||||
|
||||
# Dictionary to keep track of known devices and devices we track
|
||||
self.known_devices = {}
|
||||
|
||||
# Did we encounter an invalid known devices file
|
||||
self.invalid_known_devices_file = False
|
||||
|
||||
self._read_known_devices_file()
|
||||
|
||||
# Wrap it in a func instead of lambda so it can be identified in
|
||||
# the bus by its __name__ attribute.
|
||||
def update_device_state(time): # pylint: disable=unused-argument
|
||||
""" Triggers update of the device states. """
|
||||
self.update_devices()
|
||||
|
||||
hass.track_time_change(update_device_state)
|
||||
|
||||
hass.services.register(DOMAIN,
|
||||
SERVICE_DEVICE_TRACKER_RELOAD,
|
||||
lambda service: self._read_known_devices_file())
|
||||
|
||||
self.update_devices()
|
||||
|
||||
group.setup_group(
|
||||
hass, GROUP_NAME_ALL_DEVICES, self.device_entity_ids, False)
|
||||
|
||||
@property
|
||||
def device_entity_ids(self):
|
||||
""" Returns a set containing all device entity ids
|
||||
that are being tracked. """
|
||||
return set([self.known_devices[device]['entity_id'] for device
|
||||
in self.known_devices
|
||||
if self.known_devices[device]['track']])
|
||||
|
||||
def update_devices(self, found_devices=None):
|
||||
""" Update device states based on the found devices. """
|
||||
self.lock.acquire()
|
||||
|
||||
found_devices = found_devices or self.device_scanner.scan_devices()
|
||||
|
||||
now = datetime.now()
|
||||
|
||||
known_dev = self.known_devices
|
||||
|
||||
temp_tracking_devices = [device for device in known_dev
|
||||
if known_dev[device]['track']]
|
||||
|
||||
for device in found_devices:
|
||||
# Are we tracking this device?
|
||||
if device in temp_tracking_devices:
|
||||
temp_tracking_devices.remove(device)
|
||||
|
||||
known_dev[device]['last_seen'] = now
|
||||
|
||||
self.states.set(
|
||||
known_dev[device]['entity_id'], components.STATE_HOME,
|
||||
known_dev[device]['default_state_attr'])
|
||||
|
||||
# For all devices we did not find, set state to NH
|
||||
# But only if they have been gone for longer then the error time span
|
||||
# Because we do not want to have stuff happening when the device does
|
||||
# not show up for 1 scan beacuse of reboot etc
|
||||
for device in temp_tracking_devices:
|
||||
if now - known_dev[device]['last_seen'] > self.error_scanning:
|
||||
|
||||
self.states.set(known_dev[device]['entity_id'],
|
||||
components.STATE_NOT_HOME,
|
||||
known_dev[device]['default_state_attr'])
|
||||
|
||||
# If we come along any unknown devices we will write them to the
|
||||
# known devices file but only if we did not encounter an invalid
|
||||
# known devices file
|
||||
if not self.invalid_known_devices_file:
|
||||
|
||||
known_dev_path = self.path_known_devices_file
|
||||
|
||||
unknown_devices = [device for device in found_devices
|
||||
if device not in known_dev]
|
||||
|
||||
if unknown_devices:
|
||||
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 {} new devices,"
|
||||
" updating {}").format(len(unknown_devices),
|
||||
known_dev_path))
|
||||
|
||||
writer = csv.writer(outp)
|
||||
|
||||
if is_new_file:
|
||||
writer.writerow((
|
||||
"device", "name", "track", "picture"))
|
||||
|
||||
for device in unknown_devices:
|
||||
# See if the device scanner knows the name
|
||||
# else defaults to unknown device
|
||||
name = (self.device_scanner.get_device_name(device)
|
||||
or "unknown_device")
|
||||
|
||||
writer.writerow((device, name, 0, ""))
|
||||
known_dev[device] = {'name': name,
|
||||
'track': False,
|
||||
'picture': ""}
|
||||
|
||||
except IOError:
|
||||
_LOGGER.exception((
|
||||
"Error updating {}"
|
||||
"with {} new devices").format(known_dev_path,
|
||||
len(unknown_devices)))
|
||||
|
||||
self.lock.release()
|
||||
|
||||
def _read_known_devices_file(self):
|
||||
""" Parse and process the known devices file. """
|
||||
|
||||
# Read known devices if file exists
|
||||
if os.path.isfile(self.path_known_devices_file):
|
||||
self.lock.acquire()
|
||||
|
||||
known_devices = {}
|
||||
|
||||
with open(self.path_known_devices_file) as inp:
|
||||
default_last_seen = datetime(1990, 1, 1)
|
||||
|
||||
# Temp variable to keep track of which entity ids we use
|
||||
# so we can ensure we have unique entity ids.
|
||||
used_entity_ids = []
|
||||
|
||||
try:
|
||||
for row in csv.DictReader(inp):
|
||||
device = row['device']
|
||||
|
||||
row['track'] = True if row['track'] == '1' else False
|
||||
|
||||
if row['picture']:
|
||||
row['default_state_attr'] = {
|
||||
components.ATTR_ENTITY_PICTURE: row['picture']}
|
||||
|
||||
else:
|
||||
row['default_state_attr'] = None
|
||||
|
||||
# If we track this device setup tracking variables
|
||||
if row['track']:
|
||||
row['last_seen'] = default_last_seen
|
||||
|
||||
# Make sure that each device is mapped
|
||||
# to a unique entity_id name
|
||||
name = util.slugify(row['name']) if row['name'] \
|
||||
else "unnamed_device"
|
||||
|
||||
entity_id = ENTITY_ID_FORMAT.format(name)
|
||||
tries = 1
|
||||
|
||||
while entity_id in used_entity_ids:
|
||||
tries += 1
|
||||
|
||||
suffix = "_{}".format(tries)
|
||||
|
||||
entity_id = ENTITY_ID_FORMAT.format(
|
||||
name + suffix)
|
||||
|
||||
row['entity_id'] = entity_id
|
||||
used_entity_ids.append(entity_id)
|
||||
|
||||
row['picture'] = row['picture']
|
||||
|
||||
known_devices[device] = row
|
||||
|
||||
if not known_devices:
|
||||
_LOGGER.warning(
|
||||
"No devices to track. Please update %s.",
|
||||
self.path_known_devices_file)
|
||||
|
||||
# Remove entities that are no longer maintained
|
||||
new_entity_ids = set([known_devices[device]['entity_id']
|
||||
for device in known_devices
|
||||
if known_devices[device]['track']])
|
||||
|
||||
for entity_id in \
|
||||
self.device_entity_ids - new_entity_ids:
|
||||
|
||||
_LOGGER.info("Removing entity %s", entity_id)
|
||||
self.states.remove(entity_id)
|
||||
|
||||
# File parsed, warnings given if necessary
|
||||
# entities cleaned up, make it available
|
||||
self.known_devices = known_devices
|
||||
|
||||
_LOGGER.info("Loaded devices from %s",
|
||||
self.path_known_devices_file)
|
||||
|
||||
except KeyError:
|
||||
self.invalid_known_devices_file = True
|
||||
_LOGGER.warning(
|
||||
("Invalid known devices file: %s. "
|
||||
"We won't update it with new found devices."),
|
||||
self.path_known_devices_file)
|
||||
|
||||
finally:
|
||||
self.lock.release()
|
||||
|
||||
|
||||
class TomatoDeviceScanner(object):
|
||||
""" This class queries a wireless router running Tomato firmware
|
||||
for connected devices.
|
||||
|
||||
A description of the Tomato API can be found on
|
||||
http://paulusschoutsen.nl/blog/2013/10/tomato-api-documentation/
|
||||
"""
|
||||
|
||||
def __init__(self, config):
|
||||
host, http_id = config['host'], config['http_id']
|
||||
username, password = config['username'], config['password']
|
||||
|
||||
self.req = requests.Request('POST',
|
||||
'http://{}/update.cgi'.format(host),
|
||||
data={'_http_id': http_id,
|
||||
'exec': 'devlist'},
|
||||
auth=requests.auth.HTTPBasicAuth(
|
||||
username, password)).prepare()
|
||||
|
||||
self.parse_api_pattern = re.compile(r"(?P<param>\w*) = (?P<value>.*);")
|
||||
|
||||
self.logger = logging.getLogger("{}.{}".format(__name__, "Tomato"))
|
||||
self.lock = threading.Lock()
|
||||
|
||||
self.date_updated = None
|
||||
self.last_results = {"wldev": [], "dhcpd_lease": []}
|
||||
|
||||
self.success_init = self._update_tomato_info()
|
||||
|
||||
def scan_devices(self):
|
||||
""" Scans for new devices and return a
|
||||
list containing found device ids. """
|
||||
|
||||
self._update_tomato_info()
|
||||
|
||||
return [item[1] for item in self.last_results['wldev']]
|
||||
|
||||
def get_device_name(self, device):
|
||||
""" Returns the name of the given device or None if we don't know. """
|
||||
|
||||
# Make sure there are results
|
||||
if not self.date_updated:
|
||||
self._update_tomato_info()
|
||||
|
||||
filter_named = [item[0] for item in self.last_results['dhcpd_lease']
|
||||
if item[2] == device]
|
||||
|
||||
if not filter_named or not filter_named[0]:
|
||||
return None
|
||||
else:
|
||||
return filter_named[0]
|
||||
|
||||
def _update_tomato_info(self):
|
||||
""" Ensures the information from the Tomato router is up to date.
|
||||
Returns boolean if scanning successful. """
|
||||
|
||||
self.lock.acquire()
|
||||
|
||||
# if date_updated is None or the date is too old we scan for new data
|
||||
if not self.date_updated or \
|
||||
datetime.now() - self.date_updated > MIN_TIME_BETWEEN_SCANS:
|
||||
|
||||
self.logger.info("Scanning")
|
||||
|
||||
try:
|
||||
response = requests.Session().send(self.req, timeout=3)
|
||||
|
||||
# Calling and parsing the Tomato api here. We only need the
|
||||
# wldev and dhcpd_lease values. For API description see:
|
||||
# http://paulusschoutsen.nl/
|
||||
# blog/2013/10/tomato-api-documentation/
|
||||
if response.status_code == 200:
|
||||
|
||||
for param, value in \
|
||||
self.parse_api_pattern.findall(response.text):
|
||||
|
||||
if param == 'wldev' or param == 'dhcpd_lease':
|
||||
self.last_results[param] = \
|
||||
json.loads(value.replace("'", '"'))
|
||||
|
||||
self.date_updated = datetime.now()
|
||||
|
||||
return True
|
||||
|
||||
elif response.status_code == 401:
|
||||
# Authentication error
|
||||
self.logger.exception((
|
||||
"Failed to authenticate, "
|
||||
"please check your username and password"))
|
||||
|
||||
return False
|
||||
|
||||
except requests.exceptions.ConnectionError:
|
||||
# We get this if we could not connect to the router or
|
||||
# an invalid http_id was supplied
|
||||
self.logger.exception((
|
||||
"Failed to connect to the router"
|
||||
" or invalid http_id supplied"))
|
||||
|
||||
return False
|
||||
|
||||
except requests.exceptions.Timeout:
|
||||
# We get this if we could not connect to the router or
|
||||
# an invalid http_id was supplied
|
||||
self.logger.exception(
|
||||
"Connection to the router timed out")
|
||||
|
||||
return False
|
||||
|
||||
except ValueError:
|
||||
# If json decoder could not parse the response
|
||||
self.logger.exception(
|
||||
"Failed to parse response from router")
|
||||
|
||||
return False
|
||||
|
||||
finally:
|
||||
self.lock.release()
|
||||
|
||||
else:
|
||||
# We acquired the lock before the IF check,
|
||||
# release it before we return True
|
||||
self.lock.release()
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class NetgearDeviceScanner(object):
|
||||
""" This class queries a Netgear wireless router using the SOAP-api. """
|
||||
|
||||
def __init__(self, config):
|
||||
host = config['host']
|
||||
username, password = config['username'], config['password']
|
||||
|
||||
self.logger = logging.getLogger("{}.{}".format(__name__, "Netgear"))
|
||||
self.date_updated = None
|
||||
self.last_results = []
|
||||
|
||||
try:
|
||||
# Pylint does not play nice if not every folders has an __init__.py
|
||||
# pylint: disable=no-name-in-module, import-error
|
||||
import homeassistant.external.pynetgear.pynetgear as pynetgear
|
||||
except ImportError:
|
||||
self.logger.exception(
|
||||
("Failed to import pynetgear. "
|
||||
"Did you maybe not run `git submodule init` "
|
||||
"and `git submodule update`?"))
|
||||
|
||||
self.success_init = False
|
||||
|
||||
return
|
||||
|
||||
self._api = pynetgear.Netgear(host, username, password)
|
||||
self.lock = threading.Lock()
|
||||
|
||||
self.logger.info("Logging in")
|
||||
if self._api.login():
|
||||
self.success_init = True
|
||||
self._update_info()
|
||||
|
||||
else:
|
||||
self.logger.error("Netgear:Failed to Login")
|
||||
|
||||
self.success_init = False
|
||||
|
||||
def scan_devices(self):
|
||||
""" Scans for new devices and return a
|
||||
list containing found device ids. """
|
||||
|
||||
self._update_info()
|
||||
|
||||
return [device.mac for device in self.last_results]
|
||||
|
||||
def get_device_name(self, mac):
|
||||
""" Returns the name of the given device or None if we don't know. """
|
||||
|
||||
# Make sure there are results
|
||||
if not self.date_updated:
|
||||
self._update_info()
|
||||
|
||||
filter_named = [device.name for device in self.last_results
|
||||
if device.mac == mac]
|
||||
|
||||
if filter_named:
|
||||
return filter_named[0]
|
||||
else:
|
||||
return None
|
||||
|
||||
def _update_info(self):
|
||||
""" Retrieves latest information from the Netgear router.
|
||||
Returns boolean if scanning successful. """
|
||||
if not self.success_init:
|
||||
return
|
||||
|
||||
with self.lock:
|
||||
# if date_updated is None or the date is too old we scan for
|
||||
# new data
|
||||
if not self.date_updated or \
|
||||
datetime.now() - self.date_updated > MIN_TIME_BETWEEN_SCANS:
|
||||
|
||||
self.logger.info("Scanning")
|
||||
|
||||
self.last_results = self._api.get_attached_devices()
|
||||
|
||||
self.date_updated = datetime.now()
|
||||
|
||||
return
|
||||
|
||||
else:
|
||||
return
|
||||
|
||||
|
||||
class LuciDeviceScanner(object):
|
||||
""" This class queries a wireless router running OpenWrt firmware
|
||||
for connected devices. Adapted from Tomato scanner.
|
||||
|
||||
# opkg install luci-mod-rpc
|
||||
for this to work on the router.
|
||||
|
||||
The API is described here:
|
||||
http://luci.subsignal.org/trac/wiki/Documentation/JsonRpcHowTo
|
||||
|
||||
(Currently, we do only wifi iwscan, and no DHCP lease access.)
|
||||
"""
|
||||
|
||||
def __init__(self, config):
|
||||
host = config['host']
|
||||
username, password = config['username'], config['password']
|
||||
|
||||
self.parse_api_pattern = re.compile(r"(?P<param>\w*) = (?P<value>.*);")
|
||||
|
||||
self.logger = logging.getLogger("{}.{}".format(__name__, "Luci"))
|
||||
self.lock = threading.Lock()
|
||||
|
||||
self.date_updated = None
|
||||
self.last_results = {}
|
||||
|
||||
self.token = self.get_token(host, username, password)
|
||||
self.host = host
|
||||
|
||||
self.mac2name = None
|
||||
self.success_init = self.token
|
||||
|
||||
def _req_json_rpc(self, url, method, *args, **kwargs):
|
||||
""" Perform one JSON RPC operation. """
|
||||
data = json.dumps({'method': method, 'params': args})
|
||||
try:
|
||||
res = requests.post(url, data=data, timeout=5, **kwargs)
|
||||
except requests.exceptions.Timeout:
|
||||
self.logger.exception("Connection to the router timed out")
|
||||
return
|
||||
if res.status_code == 200:
|
||||
try:
|
||||
result = res.json()
|
||||
except ValueError:
|
||||
# If json decoder could not parse the response
|
||||
self.logger.exception("Failed to parse response from luci")
|
||||
return
|
||||
try:
|
||||
return result['result']
|
||||
except KeyError:
|
||||
self.logger.exception("No result in response from luci")
|
||||
return
|
||||
elif res.status_code == 401:
|
||||
# Authentication error
|
||||
self.logger.exception(
|
||||
"Failed to authenticate, "
|
||||
"please check your username and password")
|
||||
return
|
||||
else:
|
||||
self.logger.error("Invalid response from luci: %s", res)
|
||||
|
||||
def get_token(self, host, username, password):
|
||||
""" Get authentication token for the given host+username+password """
|
||||
url = 'http://{}/cgi-bin/luci/rpc/auth'.format(host)
|
||||
return self._req_json_rpc(url, 'login', username, password)
|
||||
|
||||
def scan_devices(self):
|
||||
""" Scans for new devices and return a
|
||||
list containing found device ids. """
|
||||
|
||||
self._update_info()
|
||||
|
||||
return self.last_results
|
||||
|
||||
def get_device_name(self, device):
|
||||
""" Returns the name of the given device or None if we don't know. """
|
||||
|
||||
with self.lock:
|
||||
if self.mac2name is None:
|
||||
url = 'http://{}/cgi-bin/luci/rpc/uci'.format(self.host)
|
||||
result = self._req_json_rpc(url, 'get_all', 'dhcp',
|
||||
params={'auth': self.token})
|
||||
if result:
|
||||
hosts = [x for x in result.values()
|
||||
if x['.type'] == 'host' and
|
||||
'mac' in x and 'name' in x]
|
||||
mac2name_list = [(x['mac'], x['name']) for x in hosts]
|
||||
self.mac2name = dict(mac2name_list)
|
||||
else:
|
||||
# Error, handled in the _req_json_rpc
|
||||
return
|
||||
return self.mac2name.get(device, None)
|
||||
|
||||
def _update_info(self):
|
||||
""" Ensures the information from the Luci router is up to date.
|
||||
Returns boolean if scanning successful. """
|
||||
if not self.success_init:
|
||||
return False
|
||||
with self.lock:
|
||||
# if date_updated is None or the date is too old we scan
|
||||
# for new data
|
||||
if not self.date_updated or \
|
||||
datetime.now() - self.date_updated > MIN_TIME_BETWEEN_SCANS:
|
||||
|
||||
self.logger.info("Checking ARP")
|
||||
|
||||
url = 'http://{}/cgi-bin/luci/rpc/sys'.format(self.host)
|
||||
result = self._req_json_rpc(url, 'net.arptable',
|
||||
params={'auth': self.token})
|
||||
if result:
|
||||
self.last_results = [x['HW address'] for x in result]
|
||||
self.date_updated = datetime.now()
|
||||
return True
|
||||
return False
|
||||
|
||||
return True
|
296
homeassistant/components/device_tracker/__init__.py
Normal file
296
homeassistant/components/device_tracker/__init__.py
Normal file
@ -0,0 +1,296 @@
|
||||
"""
|
||||
homeassistant.components.tracker
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Provides functionality to keep track of devices.
|
||||
"""
|
||||
import logging
|
||||
import threading
|
||||
import os
|
||||
import csv
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import requests
|
||||
|
||||
import homeassistant as ha
|
||||
from homeassistant.loader import get_component
|
||||
import homeassistant.util as util
|
||||
import homeassistant.components as components
|
||||
|
||||
from homeassistant.components import group
|
||||
|
||||
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(
|
||||
GROUP_NAME_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_SPAN_FOR_ERROR_IN_SCANNING = timedelta(minutes=3)
|
||||
|
||||
# Filename to save known devices to
|
||||
KNOWN_DEVICES_FILE = "known_devices.csv"
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def is_on(hass, entity_id=None):
|
||||
""" Returns if any or specified device is home. """
|
||||
entity = entity_id or ENTITY_ID_ALL_DEVICES
|
||||
|
||||
return hass.states.is_state(entity, components.STATE_HOME)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
""" Sets up the device tracker. """
|
||||
|
||||
if not util.validate_config(config, {DOMAIN: [ha.CONF_TYPE]}, _LOGGER):
|
||||
return False
|
||||
|
||||
tracker_type = config[DOMAIN][ha.CONF_TYPE]
|
||||
|
||||
tracker_implementation = get_component(
|
||||
'device_tracker.{}'.format(tracker_type))
|
||||
|
||||
if tracker_implementation is None:
|
||||
_LOGGER.error("Unknown device_tracker type specified.")
|
||||
|
||||
return False
|
||||
|
||||
device_scanner = tracker_implementation.get_scanner(hass, config)
|
||||
|
||||
if device_scanner is None:
|
||||
_LOGGER.error("Failed to initialize device scanner for %s",
|
||||
tracker_type)
|
||||
|
||||
return False
|
||||
|
||||
DeviceTracker(hass, device_scanner)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
class DeviceTracker(object):
|
||||
""" Class that tracks which devices are home and which are not. """
|
||||
|
||||
def __init__(self, hass, device_scanner):
|
||||
self.states = hass.states
|
||||
|
||||
self.device_scanner = device_scanner
|
||||
|
||||
self.error_scanning = TIME_SPAN_FOR_ERROR_IN_SCANNING
|
||||
|
||||
self.lock = threading.Lock()
|
||||
|
||||
self.path_known_devices_file = hass.get_config_path(KNOWN_DEVICES_FILE)
|
||||
|
||||
# Dictionary to keep track of known devices and devices we track
|
||||
self.known_devices = {}
|
||||
|
||||
# Did we encounter an invalid known devices file
|
||||
self.invalid_known_devices_file = False
|
||||
|
||||
self._read_known_devices_file()
|
||||
|
||||
# Wrap it in a func instead of lambda so it can be identified in
|
||||
# the bus by its __name__ attribute.
|
||||
def update_device_state(time): # pylint: disable=unused-argument
|
||||
""" Triggers update of the device states. """
|
||||
self.update_devices()
|
||||
|
||||
hass.track_time_change(update_device_state)
|
||||
|
||||
hass.services.register(DOMAIN,
|
||||
SERVICE_DEVICE_TRACKER_RELOAD,
|
||||
lambda service: self._read_known_devices_file())
|
||||
|
||||
self.update_devices()
|
||||
|
||||
group.setup_group(
|
||||
hass, GROUP_NAME_ALL_DEVICES, self.device_entity_ids, False)
|
||||
|
||||
@property
|
||||
def device_entity_ids(self):
|
||||
""" Returns a set containing all device entity ids
|
||||
that are being tracked. """
|
||||
return set([self.known_devices[device]['entity_id'] for device
|
||||
in self.known_devices
|
||||
if self.known_devices[device]['track']])
|
||||
|
||||
def update_devices(self, found_devices=None):
|
||||
""" Update device states based on the found devices. """
|
||||
self.lock.acquire()
|
||||
|
||||
found_devices = found_devices or self.device_scanner.scan_devices()
|
||||
|
||||
now = datetime.now()
|
||||
|
||||
known_dev = self.known_devices
|
||||
|
||||
temp_tracking_devices = [device for device in known_dev
|
||||
if known_dev[device]['track']]
|
||||
|
||||
for device in found_devices:
|
||||
# Are we tracking this device?
|
||||
if device in temp_tracking_devices:
|
||||
temp_tracking_devices.remove(device)
|
||||
|
||||
known_dev[device]['last_seen'] = now
|
||||
|
||||
self.states.set(
|
||||
known_dev[device]['entity_id'], components.STATE_HOME,
|
||||
known_dev[device]['default_state_attr'])
|
||||
|
||||
# For all devices we did not find, set state to NH
|
||||
# But only if they have been gone for longer then the error time span
|
||||
# Because we do not want to have stuff happening when the device does
|
||||
# not show up for 1 scan beacuse of reboot etc
|
||||
for device in temp_tracking_devices:
|
||||
if now - known_dev[device]['last_seen'] > self.error_scanning:
|
||||
|
||||
self.states.set(known_dev[device]['entity_id'],
|
||||
components.STATE_NOT_HOME,
|
||||
known_dev[device]['default_state_attr'])
|
||||
|
||||
# If we come along any unknown devices we will write them to the
|
||||
# known devices file but only if we did not encounter an invalid
|
||||
# known devices file
|
||||
if not self.invalid_known_devices_file:
|
||||
|
||||
known_dev_path = self.path_known_devices_file
|
||||
|
||||
unknown_devices = [device for device in found_devices
|
||||
if device not in known_dev]
|
||||
|
||||
if unknown_devices:
|
||||
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 {} new devices,"
|
||||
" updating {}").format(len(unknown_devices),
|
||||
known_dev_path))
|
||||
|
||||
writer = csv.writer(outp)
|
||||
|
||||
if is_new_file:
|
||||
writer.writerow((
|
||||
"device", "name", "track", "picture"))
|
||||
|
||||
for device in unknown_devices:
|
||||
# See if the device scanner knows the name
|
||||
# else defaults to unknown device
|
||||
name = (self.device_scanner.get_device_name(device)
|
||||
or "unknown_device")
|
||||
|
||||
writer.writerow((device, name, 0, ""))
|
||||
known_dev[device] = {'name': name,
|
||||
'track': False,
|
||||
'picture': ""}
|
||||
|
||||
except IOError:
|
||||
_LOGGER.exception((
|
||||
"Error updating {}"
|
||||
"with {} new devices").format(known_dev_path,
|
||||
len(unknown_devices)))
|
||||
|
||||
self.lock.release()
|
||||
|
||||
def _read_known_devices_file(self):
|
||||
""" Parse and process the known devices file. """
|
||||
|
||||
# Read known devices if file exists
|
||||
if os.path.isfile(self.path_known_devices_file):
|
||||
self.lock.acquire()
|
||||
|
||||
known_devices = {}
|
||||
|
||||
with open(self.path_known_devices_file) as inp:
|
||||
default_last_seen = datetime(1990, 1, 1)
|
||||
|
||||
# Temp variable to keep track of which entity ids we use
|
||||
# so we can ensure we have unique entity ids.
|
||||
used_entity_ids = []
|
||||
|
||||
try:
|
||||
for row in csv.DictReader(inp):
|
||||
device = row['device']
|
||||
|
||||
row['track'] = True if row['track'] == '1' else False
|
||||
|
||||
if row['picture']:
|
||||
row['default_state_attr'] = {
|
||||
components.ATTR_ENTITY_PICTURE: row['picture']}
|
||||
|
||||
else:
|
||||
row['default_state_attr'] = None
|
||||
|
||||
# If we track this device setup tracking variables
|
||||
if row['track']:
|
||||
row['last_seen'] = default_last_seen
|
||||
|
||||
# Make sure that each device is mapped
|
||||
# to a unique entity_id name
|
||||
name = util.slugify(row['name']) if row['name'] \
|
||||
else "unnamed_device"
|
||||
|
||||
entity_id = ENTITY_ID_FORMAT.format(name)
|
||||
tries = 1
|
||||
|
||||
while entity_id in used_entity_ids:
|
||||
tries += 1
|
||||
|
||||
suffix = "_{}".format(tries)
|
||||
|
||||
entity_id = ENTITY_ID_FORMAT.format(
|
||||
name + suffix)
|
||||
|
||||
row['entity_id'] = entity_id
|
||||
used_entity_ids.append(entity_id)
|
||||
|
||||
row['picture'] = row['picture']
|
||||
|
||||
known_devices[device] = row
|
||||
|
||||
if not known_devices:
|
||||
_LOGGER.warning(
|
||||
"No devices to track. Please update %s.",
|
||||
self.path_known_devices_file)
|
||||
|
||||
# Remove entities that are no longer maintained
|
||||
new_entity_ids = set([known_devices[device]['entity_id']
|
||||
for device in known_devices
|
||||
if known_devices[device]['track']])
|
||||
|
||||
for entity_id in \
|
||||
self.device_entity_ids - new_entity_ids:
|
||||
|
||||
_LOGGER.info("Removing entity %s", entity_id)
|
||||
self.states.remove(entity_id)
|
||||
|
||||
# File parsed, warnings given if necessary
|
||||
# entities cleaned up, make it available
|
||||
self.known_devices = known_devices
|
||||
|
||||
_LOGGER.info("Loaded devices from %s",
|
||||
self.path_known_devices_file)
|
||||
|
||||
except KeyError:
|
||||
self.invalid_known_devices_file = True
|
||||
_LOGGER.warning(
|
||||
("Invalid known devices file: %s. "
|
||||
"We won't update it with new found devices."),
|
||||
self.path_known_devices_file)
|
||||
|
||||
finally:
|
||||
self.lock.release()
|
149
homeassistant/components/device_tracker/luci.py
Normal file
149
homeassistant/components/device_tracker/luci.py
Normal file
@ -0,0 +1,149 @@
|
||||
""" Supports scanning a OpenWRT router. """
|
||||
import logging
|
||||
import json
|
||||
from datetime import datetime, timedelta
|
||||
import re
|
||||
import threading
|
||||
import requests
|
||||
|
||||
import homeassistant as ha
|
||||
import homeassistant.util as util
|
||||
from homeassistant.components.device_tracker import DOMAIN
|
||||
|
||||
# Return cached results if last scan was less then this time ago
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def get_scanner(hass, config):
|
||||
""" Validates config and returns a Luci scanner. """
|
||||
if not util.validate_config(config,
|
||||
{DOMAIN: [ha.CONF_HOST, ha.CONF_USERNAME,
|
||||
ha.CONF_PASSWORD]},
|
||||
_LOGGER):
|
||||
return None
|
||||
|
||||
scanner = LuciDeviceScanner(config[DOMAIN])
|
||||
|
||||
return scanner if scanner.success_init else None
|
||||
|
||||
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
class LuciDeviceScanner(object):
|
||||
""" This class queries a wireless router running OpenWrt firmware
|
||||
for connected devices. Adapted from Tomato scanner.
|
||||
|
||||
# opkg install luci-mod-rpc
|
||||
for this to work on the router.
|
||||
|
||||
The API is described here:
|
||||
http://luci.subsignal.org/trac/wiki/Documentation/JsonRpcHowTo
|
||||
|
||||
(Currently, we do only wifi iwscan, and no DHCP lease access.)
|
||||
"""
|
||||
|
||||
def __init__(self, config):
|
||||
host = config[ha.CONF_HOST]
|
||||
username, password = config[ha.CONF_USERNAME], config[ha.CONF_PASSWORD]
|
||||
|
||||
self.parse_api_pattern = re.compile(r"(?P<param>\w*) = (?P<value>.*);")
|
||||
|
||||
self.lock = threading.Lock()
|
||||
|
||||
self.date_updated = None
|
||||
self.last_results = {}
|
||||
|
||||
self.token = _get_token(host, username, password)
|
||||
self.host = host
|
||||
|
||||
self.mac2name = None
|
||||
self.success_init = self.token is not None
|
||||
|
||||
def scan_devices(self):
|
||||
""" Scans for new devices and return a
|
||||
list containing found device ids. """
|
||||
|
||||
self._update_info()
|
||||
|
||||
return self.last_results
|
||||
|
||||
def get_device_name(self, device):
|
||||
""" Returns the name of the given device or None if we don't know. """
|
||||
|
||||
with self.lock:
|
||||
if self.mac2name is None:
|
||||
url = 'http://{}/cgi-bin/luci/rpc/uci'.format(self.host)
|
||||
result = _req_json_rpc(url, 'get_all', 'dhcp',
|
||||
params={'auth': self.token})
|
||||
if result:
|
||||
hosts = [x for x in result.values()
|
||||
if x['.type'] == 'host' and
|
||||
'mac' in x and 'name' in x]
|
||||
mac2name_list = [(x['mac'], x['name']) for x in hosts]
|
||||
self.mac2name = dict(mac2name_list)
|
||||
else:
|
||||
# Error, handled in the _req_json_rpc
|
||||
return
|
||||
return self.mac2name.get(device, None)
|
||||
|
||||
def _update_info(self):
|
||||
""" Ensures the information from the Luci router is up to date.
|
||||
Returns boolean if scanning successful. """
|
||||
if not self.success_init:
|
||||
return False
|
||||
with self.lock:
|
||||
# if date_updated is None or the date is too old we scan
|
||||
# for new data
|
||||
if not self.date_updated or \
|
||||
datetime.now() - self.date_updated > MIN_TIME_BETWEEN_SCANS:
|
||||
|
||||
_LOGGER.info("Checking ARP")
|
||||
|
||||
url = 'http://{}/cgi-bin/luci/rpc/sys'.format(self.host)
|
||||
result = _req_json_rpc(url, 'net.arptable',
|
||||
params={'auth': self.token})
|
||||
if result:
|
||||
self.last_results = [x['HW address'] for x in result]
|
||||
self.date_updated = datetime.now()
|
||||
return True
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def _req_json_rpc(url, method, *args, **kwargs):
|
||||
""" Perform one JSON RPC operation. """
|
||||
data = json.dumps({'method': method, 'params': args})
|
||||
try:
|
||||
res = requests.post(url, data=data, timeout=5, **kwargs)
|
||||
except requests.exceptions.Timeout:
|
||||
_LOGGER.exception("Connection to the router timed out")
|
||||
return
|
||||
if res.status_code == 200:
|
||||
try:
|
||||
result = res.json()
|
||||
except ValueError:
|
||||
# If json decoder could not parse the response
|
||||
_LOGGER.exception("Failed to parse response from luci")
|
||||
return
|
||||
try:
|
||||
return result['result']
|
||||
except KeyError:
|
||||
_LOGGER.exception("No result in response from luci")
|
||||
return
|
||||
elif res.status_code == 401:
|
||||
# Authentication error
|
||||
_LOGGER.exception(
|
||||
"Failed to authenticate, "
|
||||
"please check your username and password")
|
||||
return
|
||||
else:
|
||||
_LOGGER.error("Invalid response from luci: %s", res)
|
||||
|
||||
|
||||
def _get_token(host, username, password):
|
||||
""" Get authentication token for the given host+username+password """
|
||||
url = 'http://{}/cgi-bin/luci/rpc/auth'.format(host)
|
||||
return _req_json_rpc(url, 'login', username, password)
|
111
homeassistant/components/device_tracker/netgear.py
Normal file
111
homeassistant/components/device_tracker/netgear.py
Normal file
@ -0,0 +1,111 @@
|
||||
""" Supports scanning a Netgear router. """
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
import threading
|
||||
|
||||
import homeassistant as ha
|
||||
import homeassistant.util as util
|
||||
from homeassistant.components.device_tracker import DOMAIN
|
||||
|
||||
# Return cached results if last scan was less then this time ago
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def get_scanner(hass, config):
|
||||
""" Validates config and returns a Netgear scanner. """
|
||||
if not util.validate_config(config,
|
||||
{DOMAIN: [ha.CONF_HOST, ha.CONF_USERNAME,
|
||||
ha.CONF_PASSWORD]},
|
||||
_LOGGER):
|
||||
return None
|
||||
|
||||
scanner = NetgearDeviceScanner(config[DOMAIN])
|
||||
|
||||
return scanner if scanner.success_init else None
|
||||
|
||||
|
||||
class NetgearDeviceScanner(object):
|
||||
""" This class queries a Netgear wireless router using the SOAP-api. """
|
||||
|
||||
def __init__(self, config):
|
||||
host = config[ha.CONF_HOST]
|
||||
username, password = config[ha.CONF_USERNAME], config[ha.CONF_PASSWORD]
|
||||
|
||||
self.date_updated = None
|
||||
self.last_results = []
|
||||
|
||||
try:
|
||||
# Pylint does not play nice if not every folders has an __init__.py
|
||||
# pylint: disable=no-name-in-module, import-error
|
||||
import homeassistant.external.pynetgear.pynetgear as pynetgear
|
||||
except ImportError:
|
||||
_LOGGER.exception(
|
||||
("Failed to import pynetgear. "
|
||||
"Did you maybe not run `git submodule init` "
|
||||
"and `git submodule update`?"))
|
||||
|
||||
self.success_init = False
|
||||
|
||||
return
|
||||
|
||||
self._api = pynetgear.Netgear(host, username, password)
|
||||
self.lock = threading.Lock()
|
||||
|
||||
_LOGGER.info("Logging in")
|
||||
if self._api.login():
|
||||
self.success_init = True
|
||||
self._update_info()
|
||||
|
||||
else:
|
||||
_LOGGER.error("Failed to Login")
|
||||
|
||||
self.success_init = False
|
||||
|
||||
def scan_devices(self):
|
||||
""" Scans for new devices and return a
|
||||
list containing found device ids. """
|
||||
|
||||
self._update_info()
|
||||
|
||||
return [device.mac for device in self.last_results]
|
||||
|
||||
def get_device_name(self, mac):
|
||||
""" Returns the name of the given device or None if we don't know. """
|
||||
|
||||
# Make sure there are results
|
||||
if not self.date_updated:
|
||||
self._update_info()
|
||||
|
||||
filter_named = [device.name for device in self.last_results
|
||||
if device.mac == mac]
|
||||
|
||||
if filter_named:
|
||||
return filter_named[0]
|
||||
else:
|
||||
return None
|
||||
|
||||
def _update_info(self):
|
||||
""" Retrieves latest information from the Netgear router.
|
||||
Returns boolean if scanning successful. """
|
||||
if not self.success_init:
|
||||
return
|
||||
|
||||
with self.lock:
|
||||
# if date_updated is None or the date is too old we scan for
|
||||
# new data
|
||||
if not self.date_updated or \
|
||||
datetime.now() - self.date_updated > MIN_TIME_BETWEEN_SCANS:
|
||||
|
||||
_LOGGER.info("Scanning")
|
||||
|
||||
self.last_results = self._api.get_attached_devices()
|
||||
|
||||
self.date_updated = datetime.now()
|
||||
|
||||
return
|
||||
|
||||
else:
|
||||
return
|
158
homeassistant/components/device_tracker/tomato.py
Normal file
158
homeassistant/components/device_tracker/tomato.py
Normal file
@ -0,0 +1,158 @@
|
||||
""" Supports scanning a Tomato router. """
|
||||
import logging
|
||||
import json
|
||||
from datetime import datetime, timedelta
|
||||
import re
|
||||
import threading
|
||||
|
||||
import requests
|
||||
|
||||
import homeassistant as ha
|
||||
import homeassistant.util as util
|
||||
from homeassistant.components.device_tracker import DOMAIN
|
||||
|
||||
# Return cached results if last scan was less then this time ago
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
|
||||
|
||||
CONF_HTTP_ID = "http_id"
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def get_scanner(hass, config):
|
||||
""" Validates config and returns a Tomato scanner. """
|
||||
if not util.validate_config(config,
|
||||
{DOMAIN: [ha.CONF_HOST, ha.CONF_USERNAME,
|
||||
ha.CONF_PASSWORD, CONF_HTTP_ID]},
|
||||
_LOGGER):
|
||||
return None
|
||||
|
||||
return TomatoDeviceScanner(config[DOMAIN])
|
||||
|
||||
|
||||
class TomatoDeviceScanner(object):
|
||||
""" This class queries a wireless router running Tomato firmware
|
||||
for connected devices.
|
||||
|
||||
A description of the Tomato API can be found on
|
||||
http://paulusschoutsen.nl/blog/2013/10/tomato-api-documentation/
|
||||
"""
|
||||
|
||||
def __init__(self, config):
|
||||
host, http_id = config[ha.CONF_HOST], config[CONF_HTTP_ID]
|
||||
username, password = config[ha.CONF_USERNAME], config[ha.CONF_PASSWORD]
|
||||
|
||||
self.req = requests.Request('POST',
|
||||
'http://{}/update.cgi'.format(host),
|
||||
data={'_http_id': http_id,
|
||||
'exec': 'devlist'},
|
||||
auth=requests.auth.HTTPBasicAuth(
|
||||
username, password)).prepare()
|
||||
|
||||
self.parse_api_pattern = re.compile(r"(?P<param>\w*) = (?P<value>.*);")
|
||||
|
||||
self.logger = logging.getLogger("{}.{}".format(__name__, "Tomato"))
|
||||
self.lock = threading.Lock()
|
||||
|
||||
self.date_updated = None
|
||||
self.last_results = {"wldev": [], "dhcpd_lease": []}
|
||||
|
||||
self.success_init = self._update_tomato_info()
|
||||
|
||||
def scan_devices(self):
|
||||
""" Scans for new devices and return a
|
||||
list containing found device ids. """
|
||||
|
||||
self._update_tomato_info()
|
||||
|
||||
return [item[1] for item in self.last_results['wldev']]
|
||||
|
||||
def get_device_name(self, device):
|
||||
""" Returns the name of the given device or None if we don't know. """
|
||||
|
||||
# Make sure there are results
|
||||
if not self.date_updated:
|
||||
self._update_tomato_info()
|
||||
|
||||
filter_named = [item[0] for item in self.last_results['dhcpd_lease']
|
||||
if item[2] == device]
|
||||
|
||||
if not filter_named or not filter_named[0]:
|
||||
return None
|
||||
else:
|
||||
return filter_named[0]
|
||||
|
||||
def _update_tomato_info(self):
|
||||
""" Ensures the information from the Tomato router is up to date.
|
||||
Returns boolean if scanning successful. """
|
||||
|
||||
self.lock.acquire()
|
||||
|
||||
# if date_updated is None or the date is too old we scan for new data
|
||||
if not self.date_updated or \
|
||||
datetime.now() - self.date_updated > MIN_TIME_BETWEEN_SCANS:
|
||||
|
||||
self.logger.info("Scanning")
|
||||
|
||||
try:
|
||||
response = requests.Session().send(self.req, timeout=3)
|
||||
|
||||
# Calling and parsing the Tomato api here. We only need the
|
||||
# wldev and dhcpd_lease values. For API description see:
|
||||
# http://paulusschoutsen.nl/
|
||||
# blog/2013/10/tomato-api-documentation/
|
||||
if response.status_code == 200:
|
||||
|
||||
for param, value in \
|
||||
self.parse_api_pattern.findall(response.text):
|
||||
|
||||
if param == 'wldev' or param == 'dhcpd_lease':
|
||||
self.last_results[param] = \
|
||||
json.loads(value.replace("'", '"'))
|
||||
|
||||
self.date_updated = datetime.now()
|
||||
|
||||
return True
|
||||
|
||||
elif response.status_code == 401:
|
||||
# Authentication error
|
||||
self.logger.exception((
|
||||
"Failed to authenticate, "
|
||||
"please check your username and password"))
|
||||
|
||||
return False
|
||||
|
||||
except requests.exceptions.ConnectionError:
|
||||
# We get this if we could not connect to the router or
|
||||
# an invalid http_id was supplied
|
||||
self.logger.exception((
|
||||
"Failed to connect to the router"
|
||||
" or invalid http_id supplied"))
|
||||
|
||||
return False
|
||||
|
||||
except requests.exceptions.Timeout:
|
||||
# We get this if we could not connect to the router or
|
||||
# an invalid http_id was supplied
|
||||
self.logger.exception(
|
||||
"Connection to the router timed out")
|
||||
|
||||
return False
|
||||
|
||||
except ValueError:
|
||||
# If json decoder could not parse the response
|
||||
self.logger.exception(
|
||||
"Failed to parse response from router")
|
||||
|
||||
return False
|
||||
|
||||
finally:
|
||||
self.lock.release()
|
||||
|
||||
else:
|
||||
# We acquired the lock before the IF check,
|
||||
# release it before we return True
|
||||
self.lock.release()
|
||||
|
||||
return True
|
@ -1,2 +1,2 @@
|
||||
""" DO NOT MODIFY. Auto-generated by build_frontend script """
|
||||
VERSION = "feab16c797a25155a29f805b01fdd29b"
|
||||
VERSION = "655f75099496ad5e46673b838a21df2a"
|
||||
|
File diff suppressed because one or more lines are too long
@ -49,17 +49,15 @@ Supports following parameters:
|
||||
"""
|
||||
|
||||
import logging
|
||||
import socket
|
||||
from datetime import datetime, timedelta
|
||||
from collections import namedtuple
|
||||
import os
|
||||
import csv
|
||||
|
||||
import homeassistant as ha
|
||||
from homeassistant.loader import get_component
|
||||
import homeassistant.util as util
|
||||
from homeassistant.components import (
|
||||
ToggleDevice, group, extract_entity_ids, STATE_ON,
|
||||
SERVICE_TURN_ON, SERVICE_TURN_OFF, ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME)
|
||||
group, extract_entity_ids, STATE_ON,
|
||||
SERVICE_TURN_ON, SERVICE_TURN_OFF, ATTR_ENTITY_ID)
|
||||
|
||||
|
||||
DOMAIN = "light"
|
||||
@ -71,8 +69,6 @@ ENTITY_ID_ALL_LIGHTS = group.ENTITY_ID_FORMAT.format(
|
||||
|
||||
ENTITY_ID_FORMAT = DOMAIN + ".{}"
|
||||
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
|
||||
|
||||
# integer that represents transition time in seconds to make change
|
||||
ATTR_TRANSITION = "transition"
|
||||
|
||||
@ -86,7 +82,6 @@ ATTR_BRIGHTNESS = "brightness"
|
||||
# String representing a profile (built-in ones or external defined)
|
||||
ATTR_PROFILE = "profile"
|
||||
|
||||
PHUE_CONFIG_FILE = "phue.conf"
|
||||
LIGHT_PROFILES_FILE = "light_profiles.csv"
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@ -148,15 +143,14 @@ def setup(hass, config):
|
||||
|
||||
light_type = config[DOMAIN][ha.CONF_TYPE]
|
||||
|
||||
if light_type == 'hue':
|
||||
light_init = get_hue_lights
|
||||
light_init = get_component('light.{}'.format(light_type))
|
||||
|
||||
else:
|
||||
if light_init is None:
|
||||
_LOGGER.error("Unknown light type specified: %s", light_type)
|
||||
|
||||
return False
|
||||
|
||||
lights = light_init(hass, config[DOMAIN])
|
||||
lights = light_init.get_lights(hass, config[DOMAIN])
|
||||
|
||||
if len(lights) == 0:
|
||||
_LOGGER.error("No lights found")
|
||||
@ -301,136 +295,3 @@ def setup(hass, config):
|
||||
handle_light_service)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def get_hue_lights(hass, config):
|
||||
""" Gets the Hue lights. """
|
||||
host = config.get(ha.CONF_HOST, None)
|
||||
|
||||
try:
|
||||
# Pylint does not play nice if not every folders has an __init__.py
|
||||
# pylint: disable=no-name-in-module, import-error
|
||||
import homeassistant.external.phue.phue as phue
|
||||
except ImportError:
|
||||
_LOGGER.exception("Hue:Error while importing dependency phue.")
|
||||
|
||||
return []
|
||||
|
||||
try:
|
||||
bridge = phue.Bridge(
|
||||
host, config_file_path=hass.get_config_path(PHUE_CONFIG_FILE))
|
||||
except socket.error: # Error connecting using Phue
|
||||
_LOGGER.exception((
|
||||
"Hue:Error while connecting to the bridge. "
|
||||
"Did you follow the instructions to set it up?"))
|
||||
|
||||
return []
|
||||
|
||||
lights = {}
|
||||
|
||||
def update_lights(force_reload=False):
|
||||
""" Updates the light states. """
|
||||
now = datetime.now()
|
||||
|
||||
try:
|
||||
time_scans = now - update_lights.last_updated
|
||||
|
||||
# force_reload == True, return if updated in last second
|
||||
# force_reload == False, return if last update was less then
|
||||
# MIN_TIME_BETWEEN_SCANS ago
|
||||
if force_reload and time_scans.seconds < 1 or \
|
||||
not force_reload and time_scans < MIN_TIME_BETWEEN_SCANS:
|
||||
return
|
||||
except AttributeError:
|
||||
# First time we run last_updated is not set, continue as usual
|
||||
pass
|
||||
|
||||
update_lights.last_updated = now
|
||||
|
||||
try:
|
||||
api = bridge.get_api()
|
||||
except socket.error:
|
||||
# socket.error when we cannot reach Hue
|
||||
_LOGGER.exception("Hue:Cannot reach the bridge")
|
||||
return
|
||||
|
||||
api_states = api.get('lights')
|
||||
|
||||
if not isinstance(api_states, dict):
|
||||
_LOGGER.error("Hue:Got unexpected result from Hue API")
|
||||
return
|
||||
|
||||
for light_id, info in api_states.items():
|
||||
if light_id not in lights:
|
||||
lights[light_id] = HueLight(int(light_id), info,
|
||||
bridge, update_lights)
|
||||
else:
|
||||
lights[light_id].info = info
|
||||
|
||||
update_lights()
|
||||
|
||||
return list(lights.values())
|
||||
|
||||
|
||||
class HueLight(ToggleDevice):
|
||||
""" Represents a Hue light """
|
||||
|
||||
def __init__(self, light_id, info, bridge, update_lights):
|
||||
self.light_id = light_id
|
||||
self.info = info
|
||||
self.bridge = bridge
|
||||
self.update_lights = update_lights
|
||||
|
||||
def get_name(self):
|
||||
""" Get the mame of the Hue light. """
|
||||
return self.info['name']
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
""" Turn the specified or all lights on. """
|
||||
command = {'on': True}
|
||||
|
||||
if kwargs.get('transition') is not None:
|
||||
# Transition time is in 1/10th seconds and cannot exceed
|
||||
# 900 seconds.
|
||||
command['transitiontime'] = min(9000, kwargs['transition'] * 10)
|
||||
|
||||
if kwargs.get('brightness') is not None:
|
||||
command['bri'] = kwargs['brightness']
|
||||
|
||||
if kwargs.get('xy_color') is not None:
|
||||
command['xy'] = kwargs['xy_color']
|
||||
|
||||
self.bridge.set_light(self.light_id, command)
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
""" Turn the specified or all lights off. """
|
||||
command = {'on': False}
|
||||
|
||||
if kwargs.get('transition') is not None:
|
||||
# Transition time is in 1/10th seconds and cannot exceed
|
||||
# 900 seconds.
|
||||
command['transitiontime'] = min(9000, kwargs['transition'] * 10)
|
||||
|
||||
self.bridge.set_light(self.light_id, command)
|
||||
|
||||
def is_on(self):
|
||||
""" True if device is on. """
|
||||
self.update_lights()
|
||||
|
||||
return self.info['state']['reachable'] and self.info['state']['on']
|
||||
|
||||
def get_state_attributes(self):
|
||||
""" Returns optional state attributes. """
|
||||
attr = {
|
||||
ATTR_FRIENDLY_NAME: self.get_name()
|
||||
}
|
||||
|
||||
if self.is_on():
|
||||
attr[ATTR_BRIGHTNESS] = self.info['state']['bri']
|
||||
attr[ATTR_XY_COLOR] = self.info['state']['xy']
|
||||
|
||||
return attr
|
||||
|
||||
def update(self):
|
||||
""" Synchronize state with bridge. """
|
||||
self.update_lights(True)
|
||||
|
147
homeassistant/components/light/hue.py
Normal file
147
homeassistant/components/light/hue.py
Normal file
@ -0,0 +1,147 @@
|
||||
""" Support for Hue lights. """
|
||||
import logging
|
||||
import socket
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import homeassistant as ha
|
||||
from homeassistant.components import ToggleDevice, ATTR_FRIENDLY_NAME
|
||||
from homeassistant.components.light import (
|
||||
ATTR_BRIGHTNESS, ATTR_XY_COLOR, ATTR_TRANSITION)
|
||||
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
|
||||
|
||||
PHUE_CONFIG_FILE = "phue.conf"
|
||||
|
||||
|
||||
def get_lights(hass, config):
|
||||
""" Gets the Hue lights. """
|
||||
logger = logging.getLogger(__name__)
|
||||
try:
|
||||
# Pylint does not play nice if not every folders has an __init__.py
|
||||
# pylint: disable=no-name-in-module, import-error
|
||||
import homeassistant.external.phue.phue as phue
|
||||
except ImportError:
|
||||
logger.exception("Error while importing dependency phue.")
|
||||
|
||||
return []
|
||||
|
||||
host = config.get(ha.CONF_HOST, None)
|
||||
|
||||
try:
|
||||
bridge = phue.Bridge(
|
||||
host, config_file_path=hass.get_config_path(PHUE_CONFIG_FILE))
|
||||
except socket.error: # Error connecting using Phue
|
||||
logger.exception((
|
||||
"Error while connecting to the bridge. "
|
||||
"Did you follow the instructions to set it up?"))
|
||||
|
||||
return []
|
||||
|
||||
lights = {}
|
||||
|
||||
def update_lights(force_reload=False):
|
||||
""" Updates the light states. """
|
||||
now = datetime.now()
|
||||
|
||||
try:
|
||||
time_scans = now - update_lights.last_updated
|
||||
|
||||
# force_reload == True, return if updated in last second
|
||||
# force_reload == False, return if last update was less then
|
||||
# MIN_TIME_BETWEEN_SCANS ago
|
||||
if force_reload and time_scans.seconds < 1 or \
|
||||
not force_reload and time_scans < MIN_TIME_BETWEEN_SCANS:
|
||||
return
|
||||
except AttributeError:
|
||||
# First time we run last_updated is not set, continue as usual
|
||||
pass
|
||||
|
||||
update_lights.last_updated = now
|
||||
|
||||
try:
|
||||
api = bridge.get_api()
|
||||
except socket.error:
|
||||
# socket.error when we cannot reach Hue
|
||||
logger.exception("Cannot reach the bridge")
|
||||
return
|
||||
|
||||
api_states = api.get('lights')
|
||||
|
||||
if not isinstance(api_states, dict):
|
||||
logger.error("Got unexpected result from Hue API")
|
||||
return
|
||||
|
||||
for light_id, info in api_states.items():
|
||||
if light_id not in lights:
|
||||
lights[light_id] = HueLight(int(light_id), info,
|
||||
bridge, update_lights)
|
||||
else:
|
||||
lights[light_id].info = info
|
||||
|
||||
update_lights()
|
||||
|
||||
return list(lights.values())
|
||||
|
||||
|
||||
class HueLight(ToggleDevice):
|
||||
""" Represents a Hue light """
|
||||
|
||||
def __init__(self, light_id, info, bridge, update_lights):
|
||||
self.light_id = light_id
|
||||
self.info = info
|
||||
self.bridge = bridge
|
||||
self.update_lights = update_lights
|
||||
|
||||
def get_name(self):
|
||||
""" Get the mame of the Hue light. """
|
||||
return self.info['name']
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
""" Turn the specified or all lights on. """
|
||||
command = {'on': True}
|
||||
|
||||
if kwargs.get(ATTR_TRANSITION) is not None:
|
||||
# Transition time is in 1/10th seconds and cannot exceed
|
||||
# 900 seconds.
|
||||
command['transitiontime'] = min(9000, kwargs['transition'] * 10)
|
||||
|
||||
if kwargs.get(ATTR_BRIGHTNESS) is not None:
|
||||
command['bri'] = kwargs['brightness']
|
||||
|
||||
if kwargs.get(ATTR_XY_COLOR) is not None:
|
||||
command['xy'] = kwargs['xy_color']
|
||||
|
||||
self.bridge.set_light(self.light_id, command)
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
""" Turn the specified or all lights off. """
|
||||
command = {'on': False}
|
||||
|
||||
if kwargs.get('transition') is not None:
|
||||
# Transition time is in 1/10th seconds and cannot exceed
|
||||
# 900 seconds.
|
||||
command['transitiontime'] = min(9000, kwargs['transition'] * 10)
|
||||
|
||||
self.bridge.set_light(self.light_id, command)
|
||||
|
||||
def is_on(self):
|
||||
""" True if device is on. """
|
||||
self.update_lights()
|
||||
|
||||
return self.info['state']['reachable'] and self.info['state']['on']
|
||||
|
||||
def get_state_attributes(self):
|
||||
""" Returns optional state attributes. """
|
||||
attr = {
|
||||
ATTR_FRIENDLY_NAME: self.get_name()
|
||||
}
|
||||
|
||||
if self.is_on():
|
||||
attr[ATTR_BRIGHTNESS] = self.info['state']['bri']
|
||||
attr[ATTR_XY_COLOR] = self.info['state']['xy']
|
||||
|
||||
return attr
|
||||
|
||||
def update(self):
|
||||
""" Synchronize state with bridge. """
|
||||
self.update_lights(True)
|
@ -8,8 +8,9 @@ from datetime import datetime, timedelta
|
||||
|
||||
import homeassistant as ha
|
||||
import homeassistant.util as util
|
||||
from homeassistant.loader import get_component
|
||||
from homeassistant.components import (
|
||||
ToggleDevice, group, extract_entity_ids, STATE_ON,
|
||||
group, extract_entity_ids, STATE_ON,
|
||||
SERVICE_TURN_ON, SERVICE_TURN_OFF, ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME)
|
||||
|
||||
DOMAIN = 'switch'
|
||||
@ -62,18 +63,14 @@ def setup(hass, config):
|
||||
|
||||
switch_type = config[DOMAIN][ha.CONF_TYPE]
|
||||
|
||||
if switch_type == 'wemo':
|
||||
switch_init = get_wemo_switches
|
||||
switch_init = get_component('switch.{}'.format(switch_type))
|
||||
|
||||
elif switch_type == 'tellstick':
|
||||
switch_init = get_tellstick_switches
|
||||
|
||||
else:
|
||||
logger.error("Unknown switch type specified: %s", switch_type)
|
||||
if switch_init is None:
|
||||
logger.error("Error loading switch component %s", switch_type)
|
||||
|
||||
return False
|
||||
|
||||
switches = switch_init(config[DOMAIN])
|
||||
switches = switch_init.get_switches(hass, config[DOMAIN])
|
||||
|
||||
if len(switches) == 0:
|
||||
logger.error("No switches found")
|
||||
@ -144,114 +141,3 @@ def setup(hass, config):
|
||||
hass.services.register(DOMAIN, SERVICE_TURN_ON, handle_switch_service)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def get_wemo_switches(config):
|
||||
""" Find and return WeMo switches. """
|
||||
|
||||
try:
|
||||
# Pylint does not play nice if not every folders has an __init__.py
|
||||
# pylint: disable=no-name-in-module, import-error
|
||||
import homeassistant.external.pywemo.pywemo as pywemo
|
||||
except ImportError:
|
||||
_LOGGER.exception((
|
||||
"Wemo:Failed to import pywemo. "
|
||||
"Did you maybe not run `git submodule init` "
|
||||
"and `git submodule update`?"))
|
||||
|
||||
return []
|
||||
|
||||
if ha.CONF_HOSTS in config:
|
||||
switches = (pywemo.device_from_host(host) for host
|
||||
in config[ha.CONF_HOSTS].split(","))
|
||||
|
||||
else:
|
||||
_LOGGER.info("Scanning for WeMo devices")
|
||||
switches = pywemo.discover_devices()
|
||||
|
||||
# Filter out the switches and wrap in WemoSwitch object
|
||||
return [WemoSwitch(switch) for switch in switches
|
||||
if isinstance(switch, pywemo.Switch)]
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def get_tellstick_switches(config):
|
||||
""" Find and return Tellstick switches. """
|
||||
try:
|
||||
import tellcore.telldus as telldus
|
||||
except ImportError:
|
||||
_LOGGER.exception(
|
||||
"Failed to import tellcore")
|
||||
return []
|
||||
|
||||
core = telldus.TelldusCore()
|
||||
switches = core.devices()
|
||||
|
||||
return [TellstickSwitch(switch) for switch in switches]
|
||||
|
||||
|
||||
class WemoSwitch(ToggleDevice):
|
||||
""" represents a WeMo switch within home assistant. """
|
||||
def __init__(self, wemo):
|
||||
self.wemo = wemo
|
||||
self.state_attr = {ATTR_FRIENDLY_NAME: wemo.name}
|
||||
|
||||
def get_name(self):
|
||||
""" Returns the name of the switch if any. """
|
||||
return self.wemo.name
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
""" Turns the switch on. """
|
||||
self.wemo.on()
|
||||
|
||||
def turn_off(self):
|
||||
""" Turns the switch off. """
|
||||
self.wemo.off()
|
||||
|
||||
def is_on(self):
|
||||
""" True if switch is on. """
|
||||
return self.wemo.get_state(True)
|
||||
|
||||
def get_state_attributes(self):
|
||||
""" Returns optional state attributes. """
|
||||
return self.state_attr
|
||||
|
||||
|
||||
class TellstickSwitch(ToggleDevice):
|
||||
""" represents a Tellstick switch within home assistant. """
|
||||
def __init__(self, tellstick):
|
||||
self.tellstick = tellstick
|
||||
self.state_attr = {ATTR_FRIENDLY_NAME: tellstick.name}
|
||||
|
||||
def get_name(self):
|
||||
""" Returns the name of the switch if any. """
|
||||
return self.tellstick.name
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def turn_on(self, **kwargs):
|
||||
""" Turns the switch on. """
|
||||
self.tellstick.turn_on()
|
||||
|
||||
def turn_off(self):
|
||||
""" Turns the switch off. """
|
||||
self.tellstick.turn_off()
|
||||
|
||||
def is_on(self):
|
||||
""" True if switch is on. """
|
||||
|
||||
try:
|
||||
import tellcore.constants as tellcore_constants
|
||||
except ImportError:
|
||||
_LOGGER.exception(
|
||||
"Failed to import tellcore")
|
||||
return False
|
||||
|
||||
last_sent_command_mask = tellcore_constants.TELLSTICK_TURNON | \
|
||||
tellcore_constants.TELLSTICK_TURNOFF
|
||||
|
||||
return self.tellstick.last_sent_command(last_sent_command_mask) == \
|
||||
tellcore_constants.TELLSTICK_TURNON
|
||||
|
||||
def get_state_attributes(self):
|
||||
""" Returns optional state attributes. """
|
||||
return self.state_attr
|
61
homeassistant/components/switch/tellstick.py
Normal file
61
homeassistant/components/switch/tellstick.py
Normal file
@ -0,0 +1,61 @@
|
||||
""" Support for Tellstick switches. """
|
||||
import logging
|
||||
|
||||
from homeassistant.components import ToggleDevice, ATTR_FRIENDLY_NAME
|
||||
|
||||
try:
|
||||
import tellcore.constants as tc_constants
|
||||
except ImportError:
|
||||
# Don't care for now. Warning will come when get_switches is called.
|
||||
pass
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def get_switches(hass, config):
|
||||
""" Find and return Tellstick switches. """
|
||||
try:
|
||||
import tellcore.telldus as telldus
|
||||
except ImportError:
|
||||
logging.getLogger(__name__).exception(
|
||||
"Failed to import tellcore")
|
||||
return []
|
||||
|
||||
core = telldus.TelldusCore()
|
||||
switches = core.devices()
|
||||
|
||||
return [TellstickSwitch(switch) for switch in switches]
|
||||
|
||||
|
||||
class TellstickSwitch(ToggleDevice):
|
||||
""" represents a Tellstick switch within home assistant. """
|
||||
last_sent_command_mask = (tc_constants.TELLSTICK_TURNON |
|
||||
tc_constants.TELLSTICK_TURNOFF)
|
||||
|
||||
def __init__(self, tellstick):
|
||||
self.tellstick = tellstick
|
||||
self.state_attr = {ATTR_FRIENDLY_NAME: tellstick.name}
|
||||
|
||||
def get_name(self):
|
||||
""" Returns the name of the switch if any. """
|
||||
return self.tellstick.name
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def turn_on(self, **kwargs):
|
||||
""" Turns the switch on. """
|
||||
self.tellstick.turn_on()
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def turn_off(self, **kwargs):
|
||||
""" Turns the switch off. """
|
||||
self.tellstick.turn_off()
|
||||
|
||||
def is_on(self):
|
||||
""" True if switch is on. """
|
||||
last_command = self.tellstick.last_sent_command(
|
||||
self.last_sent_command_mask)
|
||||
|
||||
return last_command == tc_constants.TELLSTICK_TURNON
|
||||
|
||||
def get_state_attributes(self):
|
||||
""" Returns optional state attributes. """
|
||||
return self.state_attr
|
61
homeassistant/components/switch/wemo.py
Normal file
61
homeassistant/components/switch/wemo.py
Normal file
@ -0,0 +1,61 @@
|
||||
""" Support for WeMo switchces. """
|
||||
import logging
|
||||
|
||||
import homeassistant as ha
|
||||
from homeassistant.components import ToggleDevice, ATTR_FRIENDLY_NAME
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def get_switches(hass, config):
|
||||
""" Find and return WeMo switches. """
|
||||
|
||||
try:
|
||||
# Pylint does not play nice if not every folders has an __init__.py
|
||||
# pylint: disable=no-name-in-module, import-error
|
||||
import homeassistant.external.pywemo.pywemo as pywemo
|
||||
except ImportError:
|
||||
logging.getLogger(__name__).exception((
|
||||
"Failed to import pywemo. "
|
||||
"Did you maybe not run `git submodule init` "
|
||||
"and `git submodule update`?"))
|
||||
|
||||
return []
|
||||
|
||||
if ha.CONF_HOSTS in config:
|
||||
switches = (pywemo.device_from_host(host) for host
|
||||
in config[ha.CONF_HOSTS].split(","))
|
||||
|
||||
else:
|
||||
logging.getLogger(__name__).info("Scanning for WeMo devices")
|
||||
switches = pywemo.discover_devices()
|
||||
|
||||
# Filter out the switches and wrap in WemoSwitch object
|
||||
return [WemoSwitch(switch) for switch in switches
|
||||
if isinstance(switch, pywemo.Switch)]
|
||||
|
||||
|
||||
class WemoSwitch(ToggleDevice):
|
||||
""" represents a WeMo switch within home assistant. """
|
||||
def __init__(self, wemo):
|
||||
self.wemo = wemo
|
||||
self.state_attr = {ATTR_FRIENDLY_NAME: wemo.name}
|
||||
|
||||
def get_name(self):
|
||||
""" Returns the name of the switch if any. """
|
||||
return self.wemo.name
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
""" Turns the switch on. """
|
||||
self.wemo.on()
|
||||
|
||||
def turn_off(self):
|
||||
""" Turns the switch off. """
|
||||
self.wemo.off()
|
||||
|
||||
def is_on(self):
|
||||
""" True if switch is on. """
|
||||
return self.wemo.get_state(True)
|
||||
|
||||
def get_state_attributes(self):
|
||||
""" Returns optional state attributes. """
|
||||
return self.state_attr
|
@ -65,20 +65,26 @@ def setup(hass, config):
|
||||
|
||||
sensor_value_descriptions = {
|
||||
tellcore_constants.TELLSTICK_TEMPERATURE:
|
||||
DatatypeDescription('temperature',
|
||||
config[DOMAIN]['temperature_scale']),
|
||||
DatatypeDescription(
|
||||
'temperature', config[DOMAIN]['temperature_scale']),
|
||||
|
||||
tellcore_constants.TELLSTICK_HUMIDITY:
|
||||
DatatypeDescription('humidity', ' %'),
|
||||
DatatypeDescription('humidity', ' %'),
|
||||
|
||||
tellcore_constants.TELLSTICK_RAINRATE:
|
||||
DatatypeDescription('rain rate', ''),
|
||||
DatatypeDescription('rain rate', ''),
|
||||
|
||||
tellcore_constants.TELLSTICK_RAINTOTAL:
|
||||
DatatypeDescription('rain total', ''),
|
||||
DatatypeDescription('rain total', ''),
|
||||
|
||||
tellcore_constants.TELLSTICK_WINDDIRECTION:
|
||||
DatatypeDescription('wind direction', ''),
|
||||
DatatypeDescription('wind direction', ''),
|
||||
|
||||
tellcore_constants.TELLSTICK_WINDAVERAGE:
|
||||
DatatypeDescription('wind average', ''),
|
||||
DatatypeDescription('wind average', ''),
|
||||
|
||||
tellcore_constants.TELLSTICK_WINDGUST:
|
||||
DatatypeDescription('wind gust', '')
|
||||
DatatypeDescription('wind gust', '')
|
||||
}
|
||||
|
||||
def update_sensor_value_state(sensor_name, sensor_value):
|
||||
@ -95,8 +101,7 @@ def setup(hass, config):
|
||||
|
||||
state_attr = {
|
||||
ATTR_FRIENDLY_NAME: sensor_value_name,
|
||||
ATTR_UNIT_OF_MEASUREMENT:
|
||||
sensor_value_description.unit
|
||||
ATTR_UNIT_OF_MEASUREMENT: sensor_value_description.unit
|
||||
}
|
||||
|
||||
hass.states.set(entity_id, state, state_attr)
|
||||
|
@ -56,62 +56,37 @@ def get_component(comp_name):
|
||||
if comp_name in _COMPONENT_CACHE:
|
||||
return _COMPONENT_CACHE[comp_name]
|
||||
|
||||
# If we ie. try to load custom_components.switch.wemo but the parent
|
||||
# custom_components.switch does not exist, importing it will trigger
|
||||
# an exception because it will try to import the parent.
|
||||
# Because of this behavior, we will approach loading sub components
|
||||
# with caution: only load it if we can verify that the parent exists.
|
||||
|
||||
# First check config dir, then built-in
|
||||
potential_paths = [path for path in
|
||||
['custom_components.{}'.format(comp_name),
|
||||
'homeassistant.components.{}'.format(comp_name)]
|
||||
if path in AVAILABLE_COMPONENTS]
|
||||
|
||||
if not potential_paths:
|
||||
_LOGGER.error("Failed to find component %s", comp_name)
|
||||
|
||||
return None
|
||||
potential_paths = ['custom_components.{}'.format(comp_name),
|
||||
'homeassistant.components.{}'.format(comp_name)]
|
||||
|
||||
for path in potential_paths:
|
||||
comp = _get_component(path)
|
||||
# Validate here that root component exists
|
||||
# If path contains a '.' we are specifying a sub-component
|
||||
# Using rsplit we get the parent component from sub-component
|
||||
root_comp = path.rsplit(".", 1)[0] if '.' in comp_name else path
|
||||
|
||||
if comp is not None:
|
||||
_LOGGER.info("Loaded component %s from %s", comp_name, path)
|
||||
if root_comp not in AVAILABLE_COMPONENTS:
|
||||
continue
|
||||
|
||||
_COMPONENT_CACHE[comp_name] = comp
|
||||
try:
|
||||
_COMPONENT_CACHE[comp_name] = importlib.import_module(path)
|
||||
|
||||
return comp
|
||||
_LOGGER.info("Loaded %s from %s", comp_name, path)
|
||||
|
||||
return _COMPONENT_CACHE[comp_name]
|
||||
except ImportError:
|
||||
_LOGGER.exception(
|
||||
("Error loading %s. Make sure all "
|
||||
"dependencies are installed"), path)
|
||||
|
||||
# We did find components but were unable to load them
|
||||
_LOGGER.error("Unable to load component %s", comp_name)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _get_component(module):
|
||||
""" Tries to load specified component.
|
||||
Only returns it if also found to be valid."""
|
||||
try:
|
||||
comp = importlib.import_module(module)
|
||||
|
||||
except ImportError:
|
||||
_LOGGER.exception(("Error loading %s. Make sure all "
|
||||
"dependencies are installed"), module)
|
||||
|
||||
return None
|
||||
|
||||
# Validation if component has required methods and attributes
|
||||
errors = []
|
||||
|
||||
if not hasattr(comp, 'DOMAIN'):
|
||||
errors.append("missing DOMAIN attribute")
|
||||
|
||||
if not hasattr(comp, 'DEPENDENCIES'):
|
||||
errors.append("missing DEPENDENCIES attribute")
|
||||
|
||||
if not hasattr(comp, 'setup'):
|
||||
errors.append("missing setup method")
|
||||
|
||||
if errors:
|
||||
_LOGGER.error("Found invalid component %s: %s",
|
||||
module, ", ".join(errors))
|
||||
|
||||
return None
|
||||
|
||||
else:
|
||||
return comp
|
||||
|
Loading…
x
Reference in New Issue
Block a user