All platforms supported by components have their own file - you can can have custom platforms

This commit is contained in:
Paulus Schoutsen 2014-11-11 21:39:17 -08:00
parent 8c6e6e464e
commit 9f9b926011
15 changed files with 9888 additions and 7765 deletions

View File

@ -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

View 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()

View 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)

View 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

View 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

View File

@ -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

View File

@ -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)

View 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)

View File

@ -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

View 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

View 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

View File

@ -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)

View File

@ -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

View File

@ -1,7 +1,7 @@
[MASTER]
ignore=external
disable=locally-disabled
disable=locally-disabled,duplicate-code
[EXCEPTIONS]
overgeneral-exceptions=Exception,HomeAssistantError