Merge branch 'dev' into component-thermostat

* dev:
  Use tuples instead of lists internally
  Use properties instead of getters for Device class
  Upgrade pushbullet.py to 0.7.1
  Prevent devices from being discovered twice
  Update netdisco to latest version
  Update netdisco to latest version
  Updated requirements.txt for the discovery component
  Automatic discovery and setting up of devices
  Ensure groups always have unique entity id
  Rename ha_test folder to tests
  Make group component more flexible
  Reorganized the main to be more modular
  Updated PyWemo to latest version
  Fix warnings from flake8 and pylint
  Check flags in ARP table for NUD_REACHABLE before assuming a device is online. Fixes #18.
  Pull in PyWemo bugfixes
This commit is contained in:
Paulus Schoutsen 2015-01-11 10:00:25 -08:00
commit cac1f56b2d
41 changed files with 584 additions and 349 deletions

3
.gitmodules vendored
View File

@ -4,3 +4,6 @@
[submodule "homeassistant/external/pywemo"]
path = homeassistant/external/pywemo
url = https://github.com/balloob/pywemo.git
[submodule "homeassistant/external/netdisco"]
path = homeassistant/external/netdisco
url = https://github.com/balloob/netdisco.git

View File

@ -7,6 +7,6 @@ install:
script:
- flake8 homeassistant --exclude bower_components,external
- pylint homeassistant
- coverage run --source=homeassistant -m unittest discover ha_test
- coverage run --source=homeassistant -m unittest discover tests
after_success:
- coveralls

View File

@ -51,6 +51,7 @@ class HomeAssistant(object):
self.bus = EventBus(pool)
self.services = ServiceRegistry(self.bus, pool)
self.states = StateMachine(self.bus)
self.components = []
self.config_dir = os.path.join(os.getcwd(), 'config')
@ -222,7 +223,7 @@ def _process_match_param(parameter):
elif isinstance(parameter, list):
return parameter
else:
return [parameter]
return (parameter,)
def _matcher(subject, pattern):
@ -588,7 +589,7 @@ class StateMachine(object):
# Ensure it is a lowercase list with entity ids we want to match on
if isinstance(entity_ids, str):
entity_ids = [entity_ids.lower()]
entity_ids = (entity_ids.lower(),)
else:
entity_ids = [entity_id.lower() for entity_id in entity_ids]

View File

@ -18,19 +18,8 @@ except ImportError:
from homeassistant import bootstrap
def main():
""" Starts Home Assistant. Will create demo config if no config found. """
parser = argparse.ArgumentParser()
parser.add_argument(
'-c', '--config',
metavar='path_to_config_dir',
default="config",
help="Directory that contains the Home Assistant configuration")
args = parser.parse_args()
# Validate that all core dependencies are installed
def validate_dependencies():
""" Validate all dependencies that HA uses. """
import_fail = False
for module in ['requests']:
@ -44,11 +33,14 @@ def main():
if import_fail:
print(("Install dependencies by running: "
"pip3 install -r requirements.txt"))
exit()
sys.exit()
def ensure_config_path(config_dir):
""" Gets the path to the configuration file.
Creates one if it not exists. """
# Test if configuration directory exists
config_dir = os.path.join(os.getcwd(), args.config)
if not os.path.isdir(config_dir):
print(('Fatal Error: Unable to find specified configuration '
'directory {} ').format(config_dir))
@ -68,6 +60,27 @@ def main():
'to write a default one to {}').format(config_path))
sys.exit()
return config_path
def main():
""" Starts Home Assistant. Will create demo config if no config found. """
parser = argparse.ArgumentParser()
parser.add_argument(
'-c', '--config',
metavar='path_to_config_dir',
default="config",
help="Directory that contains the Home Assistant configuration")
args = parser.parse_args()
validate_dependencies()
config_dir = os.path.join(os.getcwd(), args.config)
config_path = ensure_config_path(config_dir)
hass = bootstrap.from_config_file(config_path)
hass.start()
hass.block_till_stopped()

View File

@ -19,6 +19,33 @@ import homeassistant.loader as loader
import homeassistant.components as core_components
_LOGGER = logging.getLogger(__name__)
def setup_component(hass, domain, config=None):
""" Setup a component for Home Assistant. """
if config is None:
config = defaultdict(dict)
component = loader.get_component(domain)
try:
if component.setup(hass, config):
hass.components.append(component.DOMAIN)
_LOGGER.info("component %s initialized", domain)
return True
else:
_LOGGER.error("component %s failed to initialize", domain)
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Error during setup of component %s", domain)
return False
# pylint: disable=too-many-branches, too-many-statements
def from_config_dict(config, hass=None):
"""
@ -29,8 +56,6 @@ def from_config_dict(config, hass=None):
if hass is None:
hass = homeassistant.HomeAssistant()
logger = logging.getLogger(__name__)
loader.prepare(hass)
# Make a copy because we are mutating it.
@ -42,12 +67,12 @@ def from_config_dict(config, hass=None):
if ' ' not in key and key != homeassistant.DOMAIN)
if not core_components.setup(hass, config):
logger.error(("Home Assistant core failed to initialize. "
"Further initialization aborted."))
_LOGGER.error("Home Assistant core failed to initialize. "
"Further initialization aborted.")
return hass
logger.info("Home Assistant core initialized")
_LOGGER.info("Home Assistant core initialized")
# Setup the components
@ -57,23 +82,12 @@ def from_config_dict(config, hass=None):
add_worker = True
for domain in loader.load_order_components(components):
component = loader.get_component(domain)
try:
if component.setup(hass, config):
logger.info("component %s initialized", domain)
if setup_component(hass, domain, config):
add_worker = add_worker and domain != "group"
if add_worker:
hass.pool.add_worker()
else:
logger.error("component %s failed to initialize", domain)
except Exception: # pylint: disable=broad-except
logger.exception("Error during setup of component %s", domain)
return hass
@ -112,7 +126,7 @@ def from_config_file(config_path, hass=None, enable_logging=True):
logging.getLogger('').addHandler(err_handler)
else:
logging.getLogger(__name__).error(
_LOGGER.error(
"Unable to setup error log %s (access denied)", err_log_path)
# Read config

View File

@ -6,13 +6,19 @@ Provides functionality to interact with Chromecasts.
"""
import logging
try:
import pychromecast
except ImportError:
# Ignore, we will raise appropriate error later
pass
from homeassistant.loader import get_component
import homeassistant.util as util
from homeassistant.helpers import extract_entity_ids
from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, SERVICE_TURN_OFF, SERVICE_VOLUME_UP,
SERVICE_VOLUME_DOWN, SERVICE_MEDIA_PLAY_PAUSE, SERVICE_MEDIA_PLAY,
SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PREV_TRACK,
CONF_HOSTS)
SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PREV_TRACK)
DOMAIN = 'chromecast'
@ -105,31 +111,13 @@ def media_prev_track(hass, entity_id=None):
hass.services.call(DOMAIN, SERVICE_MEDIA_PREV_TRACK, data)
# pylint: disable=too-many-locals, too-many-branches
def setup(hass, config):
""" Listen for chromecast events. """
logger = logging.getLogger(__name__)
def setup_chromecast(casts, host):
""" Tries to convert host to Chromecast object and set it up. """
try:
import pychromecast
except ImportError:
logger.exception(("Failed to import pychromecast. "
"Did you maybe not install the 'pychromecast' "
"dependency?"))
# Check if already setup
if any(cast.host == host for cast in casts.values()):
return
return False
if CONF_HOSTS in config[DOMAIN]:
hosts = config[DOMAIN][CONF_HOSTS].split(",")
# If no hosts given, scan for chromecasts
else:
logger.info("Scanning for Chromecasts")
hosts = pychromecast.discover_chromecasts()
casts = {}
for host in hosts:
try:
cast = pychromecast.PyChromecast(host)
@ -143,10 +131,42 @@ def setup(hass, config):
except pychromecast.ChromecastConnectionError:
pass
if not casts:
logger.error("Could not find Chromecasts")
def setup(hass, config):
# pylint: disable=unused-argument,too-many-locals
""" Listen for chromecast events. """
logger = logging.getLogger(__name__)
discovery = get_component('discovery')
try:
# pylint: disable=redefined-outer-name
import pychromecast
except ImportError:
logger.exception(("Failed to import pychromecast. "
"Did you maybe not install the 'pychromecast' "
"dependency?"))
return False
casts = {}
# If discovery component not loaded, scan ourselves
if discovery.DOMAIN not in hass.components:
logger.info("Scanning for Chromecasts")
hosts = pychromecast.discover_chromecasts()
for host in hosts:
setup_chromecast(casts, host)
# pylint: disable=unused-argument
def chromecast_discovered(service, info):
""" Called when a Chromecast has been discovered. """
logger.info("New Chromecast discovered: %s", info[0])
setup_chromecast(casts, info[0])
discovery.listen(
hass, discovery.services.GOOGLE_CAST, chromecast_discovered)
def update_chromecast_state(entity_id, chromecast):
""" Retrieve state of Chromecast and update statemachine. """
chromecast.refresh()
@ -194,6 +214,7 @@ def setup(hass, config):
def update_chromecast_states(time): # pylint: disable=unused-argument
""" Updates all chromecast states. """
if casts:
logger.info("Updating Chromecast status")
for entity_id, cast in casts.items():

View File

@ -111,19 +111,16 @@ class DeviceTracker(object):
""" Triggers update of the device states. """
self.update_devices(now)
dev_group = group.Group(hass, GROUP_NAME_ALL_DEVICES)
# pylint: disable=unused-argument
def reload_known_devices_service(service):
""" Reload known devices file. """
group.remove_group(self.hass, GROUP_NAME_ALL_DEVICES)
self._read_known_devices_file()
self.update_devices(datetime.now())
if self.tracked:
group.setup_group(
self.hass, GROUP_NAME_ALL_DEVICES,
self.device_entity_ids, False)
dev_group.update_tracked_entity_ids(self.device_entity_ids)
reload_known_devices_service(None)

View File

@ -101,7 +101,12 @@ class LuciDeviceScanner(object):
result = _req_json_rpc(url, 'net.arptable',
params={'auth': self.token})
if result:
self.last_results = [x['HW address'] for x in result]
self.last_results = []
for device_entry in result:
# Check if the Flags for each device contain
# NUD_REACHABLE and if so, add it to last_results
if int(device_entry['Flags'], 16) & 0x2:
self.last_results.append(device_entry['HW address'])
return True

View File

@ -0,0 +1,88 @@
"""
Starts a service to scan in intervals for new devices.
Will emit EVENT_SERVICE_DISCOVERED whenever a new service has been discovered.
Knows which components handle certain types, will make sure they are
loaded before the EVENT_SERVICE_DISCOVERED is fired.
"""
import logging
import threading
# pylint: disable=no-name-in-module, import-error
from homeassistant.external.netdisco.netdisco import DiscoveryService
import homeassistant.external.netdisco.netdisco.const as services
from homeassistant import bootstrap
from homeassistant.const import EVENT_HOMEASSISTANT_START, ATTR_SERVICE
DOMAIN = "discovery"
DEPENDENCIES = []
EVENT_SERVICE_DISCOVERED = "service_discovered"
ATTR_DISCOVERED = "discovered"
SCAN_INTERVAL = 300 # seconds
SERVICE_HANDLERS = {
services.BELKIN_WEMO: "switch",
services.GOOGLE_CAST: "chromecast",
services.PHILIPS_HUE: "light",
}
def listen(hass, service, callback):
"""
Setup listener for discovery of specific service.
Service can be a string or a list/tuple.
"""
if not isinstance(service, str):
service = (service,)
def discovery_event_listener(event):
""" Listens for discovery events. """
if event.data[ATTR_SERVICE] in service:
callback(event.data[ATTR_SERVICE], event.data[ATTR_DISCOVERED])
hass.bus.listen(EVENT_SERVICE_DISCOVERED, discovery_event_listener)
def setup(hass, config):
""" Starts a discovery service. """
# Disable zeroconf logging, it spams
logging.getLogger('zeroconf').setLevel(logging.CRITICAL)
logger = logging.getLogger(__name__)
lock = threading.Lock()
def new_service_listener(service, info):
""" Called when a new service is found. """
with lock:
component = SERVICE_HANDLERS.get(service)
logger.info("Found new service: %s %s", service, info)
if component and component not in hass.components:
if bootstrap.setup_component(hass, component, config):
hass.pool.add_worker()
hass.bus.fire(EVENT_SERVICE_DISCOVERED, {
ATTR_SERVICE: service,
ATTR_DISCOVERED: info
})
# pylint: disable=unused-argument
def start_discovery(event):
""" Start discovering. """
netdisco = DiscoveryService(SCAN_INTERVAL)
netdisco.add_listener(new_service_listener)
netdisco.start()
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_discovery)
return True

View File

@ -5,12 +5,11 @@ homeassistant.components.groups
Provides functionality to group devices that can be turned on or off.
"""
import logging
import homeassistant as ha
import homeassistant.util as util
from homeassistant.const import (
ATTR_ENTITY_ID, STATE_ON, STATE_OFF, STATE_HOME, STATE_NOT_HOME)
ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, STATE_ON, STATE_OFF,
STATE_HOME, STATE_NOT_HOME, STATE_UNKNOWN)
DOMAIN = "group"
DEPENDENCIES = []
@ -22,8 +21,6 @@ ATTR_AUTO = "auto"
# List of ON/OFF state tuples for groupable states
_GROUP_TYPES = [(STATE_ON, STATE_OFF), (STATE_HOME, STATE_NOT_HOME)]
_GROUPS = {}
def _get_group_on_off(state):
""" Determine the group on/off states based on a state. """
@ -94,89 +91,98 @@ def get_entity_ids(hass, entity_id, domain_filter=None):
def setup(hass, config):
""" Sets up all groups found definded in the configuration. """
for name, entity_ids in config.get(DOMAIN, {}).items():
entity_ids = entity_ids.split(",")
setup_group(hass, name, entity_ids)
setup_group(hass, name, entity_ids.split(","))
return True
def setup_group(hass, name, entity_ids, user_defined=True):
""" Sets up a group state that is the combined state of
several states. Supports ON/OFF and DEVICE_HOME/DEVICE_NOT_HOME. """
logger = logging.getLogger(__name__)
class Group(object):
""" Tracks a group of entity ids. """
def __init__(self, hass, name, entity_ids=None, user_defined=True):
self.hass = hass
self.name = name
self.user_defined = user_defined
# In case an iterable is passed in
entity_ids = list(entity_ids)
self.entity_id = util.ensure_unique_string(
ENTITY_ID_FORMAT.format(util.slugify(name)),
hass.states.entity_ids(DOMAIN))
if not entity_ids:
logger.error(
'Error setting up group %s: no entities passed in to track', name)
self.tracking = []
self.group_on, self.group_off = None, None
return False
# Loop over the given entities to:
# - determine which group type this is (on_off, device_home)
# - determine which states exist and have groupable states
# - determine the current state of the group
warnings = []
group_ids = []
group_on, group_off = None, None
group_state = False
for entity_id in entity_ids:
state = hass.states.get(entity_id)
# Try to determine group type if we didn't yet
if group_on is None and state:
group_on, group_off = _get_group_on_off(state.state)
if group_on is None:
# We did not find a matching group_type
warnings.append(
"Entity {} has ungroupable state '{}'".format(
name, state.state))
continue
# Check if entity exists
if not state:
warnings.append("Entity {} does not exist".format(entity_id))
# Check if entity is invalid state
elif state.state != group_off and state.state != group_on:
warnings.append("State of {} is {} (expected: {} or {})".format(
entity_id, state.state, group_off, group_on))
# We have a valid group state
if entity_ids is not None:
self.update_tracked_entity_ids(entity_ids)
else:
group_ids.append(entity_id)
self.force_update()
# Keep track of the group state to init later on
group_state = group_state or state.state == group_on
@property
def state(self):
""" Return the current state from the group. """
return self.hass.states.get(self.entity_id)
# If none of the entities could be found during setup
if not group_ids:
logger.error('Unable to find any entities to track for group %s', name)
@property
def state_attr(self):
""" State attributes of this group. """
return {
ATTR_ENTITY_ID: self.tracking,
ATTR_AUTO: not self.user_defined,
ATTR_FRIENDLY_NAME: self.name
}
return False
def update_tracked_entity_ids(self, entity_ids):
""" Update the tracked entity IDs. """
self.stop()
self.tracking = tuple(entity_ids)
self.group_on, self.group_off = None, None
elif warnings:
logger.warning(
'Warnings during setting up group %s: %s',
name, ", ".join(warnings))
self.force_update()
group_entity_id = ENTITY_ID_FORMAT.format(util.slugify(name))
state = group_on if group_state else group_off
state_attr = {ATTR_ENTITY_ID: group_ids, ATTR_AUTO: not user_defined}
self.start()
def force_update(self):
""" Query all the tracked states and update group state. """
for entity_id in self.tracking:
state = self.hass.states.get(entity_id)
if state is not None:
self._update_group_state(state.entity_id, None, state)
# If parsing the entitys did not result in a state, set UNKNOWN
if self.state is None:
self.hass.states.set(
self.entity_id, STATE_UNKNOWN, self.state_attr)
def start(self):
""" Starts the tracking. """
self.hass.states.track_change(self.tracking, self._update_group_state)
def stop(self):
""" Unregisters the group from Home Assistant. """
self.hass.states.remove(self.entity_id)
self.hass.bus.remove_listener(
ha.EVENT_STATE_CHANGED, self._update_group_state)
# pylint: disable=unused-argument
def update_group_state(entity_id, old_state, new_state):
def _update_group_state(self, entity_id, old_state, new_state):
""" Updates the group state based on a state change by
a tracked entity. """
cur_gr_state = hass.states.get(group_entity_id).state
# We have not determined type of group yet
if self.group_on is None:
self.group_on, self.group_off = _get_group_on_off(new_state.state)
if self.group_on is not None:
# New state of the group is going to be based on the first
# state that we can recognize
self.hass.states.set(
self.entity_id, new_state.state, self.state_attr)
return
# There is already a group state
cur_gr_state = self.hass.states.get(self.entity_id).state
group_on, group_off = self.group_on, self.group_off
# if cur_gr_state = OFF and new_state = ON: set ON
# if cur_gr_state = ON and new_state = OFF: research
@ -184,31 +190,21 @@ def setup_group(hass, name, entity_ids, user_defined=True):
if cur_gr_state == group_off and new_state.state == group_on:
hass.states.set(group_entity_id, group_on, state_attr)
self.hass.states.set(
self.entity_id, group_on, self.state_attr)
elif cur_gr_state == group_on and new_state.state == group_off:
elif (cur_gr_state == group_on and
new_state.state == group_off):
# Check if any of the other states is still on
if not any([hass.states.is_state(ent_id, group_on)
for ent_id in group_ids
if entity_id != ent_id]):
hass.states.set(group_entity_id, group_off, state_attr)
_GROUPS[group_entity_id] = hass.states.track_change(
group_ids, update_group_state)
hass.states.set(group_entity_id, state, state_attr)
return True
if not any(self.hass.states.is_state(ent_id, group_on)
for ent_id in self.tracking if entity_id != ent_id):
self.hass.states.set(
self.entity_id, group_off, self.state_attr)
def remove_group(hass, name):
""" Remove a group and its state listener from Home Assistant. """
group_entity_id = ENTITY_ID_FORMAT.format(util.slugify(name))
def setup_group(hass, name, entity_ids, user_defined=True):
""" Sets up a group state that is the combined state of
several states. Supports ON/OFF and DEVICE_HOME/DEVICE_NOT_HOME. """
if hass.states.get(group_entity_id) is not None:
hass.states.remove(group_entity_id)
if group_entity_id in _GROUPS:
hass.bus.remove_listener(
ha.EVENT_STATE_CHANGED, _GROUPS.pop(group_entity_id))
return Group(hass, name, entity_ids, user_defined)

View File

@ -178,8 +178,7 @@ def setup(hass, config):
update_lights_state(None)
# Track all lights in a group
group.setup_group(
hass, GROUP_NAME_ALL_LIGHTS, lights.keys(), False)
group.Group(hass, GROUP_NAME_ALL_LIGHTS, lights.keys(), False)
def handle_light_service(service):
""" Hande a turn light on or off service call. """

View File

@ -77,9 +77,36 @@ class HueLight(ToggleDevice):
self.bridge = bridge
self.update_lights = update_lights
def get_name(self):
@property
def unique_id(self):
""" Returns the id of this Hue light """
return "{}.{}".format(
self.__class__, self.info.get('uniqueid', self.name))
@property
def name(self):
""" Get the mame of the Hue light. """
return self.info['name']
return self.info.get('name', 'No name')
@property
def state_attributes(self):
""" Returns optional state attributes. """
attr = {
ATTR_FRIENDLY_NAME: self.name
}
if self.is_on:
attr[ATTR_BRIGHTNESS] = self.info['state']['bri']
attr[ATTR_XY_COLOR] = self.info['state']['xy']
return attr
@property
def is_on(self):
""" True if device is on. """
self.update_lights()
return self.info['state']['reachable'] and self.info['state']['on']
def turn_on(self, **kwargs):
""" Turn the specified or all lights on. """
@ -118,24 +145,6 @@ class HueLight(ToggleDevice):
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(no_throttle=True)

View File

@ -50,7 +50,7 @@ def setup(hass, config):
notify_service = notify_implementation.get_service(hass, config)
if notify_service is None:
_LOGGER.error("Failed to initialize notificatino service %s",
_LOGGER.error("Failed to initialize notification service %s",
platform)
return False

View File

@ -22,17 +22,23 @@ def get_service(hass, config):
try:
# pylint: disable=unused-variable
from pushbullet import PushBullet # noqa
from pushbullet import PushBullet, InvalidKeyError # noqa
except ImportError:
_LOGGER.exception(
"Unable to import pushbullet. "
"Did you maybe not install the 'pushbullet' package?")
"Did you maybe not install the 'pushbullet.py' package?")
return None
try:
return PushBulletNotificationService(config[DOMAIN][CONF_API_KEY])
except InvalidKeyError:
_LOGGER.error(
"Wrong API key supplied. "
"Get it at https://www.pushbullet.com/account")
# pylint: disable=too-few-public-methods
class PushBulletNotificationService(BaseNotificationService):

View File

@ -6,12 +6,13 @@ Component to interface with various switches that can be controlled remotely.
import logging
from datetime import timedelta
from homeassistant.loader import get_component
import homeassistant.util as util
from homeassistant.const import (
STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, ATTR_ENTITY_ID)
from homeassistant.helpers import (
extract_entity_ids, platform_devices_from_config)
from homeassistant.components import group
from homeassistant.components import group, discovery
DOMAIN = 'switch'
DEPENDENCIES = []
@ -27,6 +28,11 @@ ATTR_CURRENT_POWER_MWH = "current_power_mwh"
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
# Maps discovered services to their platforms
DISCOVERY = {
discovery.services.BELKIN_WEMO: 'wemo'
}
_LOGGER = logging.getLogger(__name__)
@ -58,14 +64,11 @@ def setup(hass, config):
switches = platform_devices_from_config(
config, DOMAIN, hass, ENTITY_ID_FORMAT, logger)
if not switches:
return False
# pylint: disable=unused-argument
@util.Throttle(MIN_TIME_BETWEEN_SCANS)
def update_states(now):
""" Update states of all switches. """
if switches:
logger.info("Updating switch states")
for switch in switches.values():
@ -73,6 +76,29 @@ def setup(hass, config):
update_states(None)
# Track all switches in a group
switch_group = group.Group(
hass, GROUP_NAME_ALL_SWITCHES, switches.keys(), False)
def switch_discovered(service, info):
""" Called when a switch is discovered. """
platform = get_component("{}.{}".format(DOMAIN, DISCOVERY[service]))
switch = platform.device_discovered(hass, config, info)
if switch is not None and switch not in switches.values():
switch.entity_id = util.ensure_unique_string(
ENTITY_ID_FORMAT.format(util.slugify(switch.name)),
switches.keys())
switches[switch.entity_id] = switch
switch.update_ha_state(hass)
switch_group.update_tracked_entity_ids(switches.keys())
discovery.listen(hass, discovery.services.BELKIN_WEMO, switch_discovered)
def handle_switch_service(service):
""" Handles calls to the switch services. """
target_switches = [switches[entity_id] for entity_id
@ -90,10 +116,6 @@ def setup(hass, config):
switch.update_ha_state(hass)
# Track all switches in a group
group.setup_group(hass, GROUP_NAME_ALL_SWITCHES,
switches.keys(), False)
# Update state every 30 seconds
hass.track_time_change(update_states, second=[0, 30])

View File

@ -36,10 +36,24 @@ class TellstickSwitch(ToggleDevice):
self.tellstick = tellstick
self.state_attr = {ATTR_FRIENDLY_NAME: tellstick.name}
def get_name(self):
@property
def name(self):
""" Returns the name of the switch if any. """
return self.tellstick.name
@property
def state_attributes(self):
""" Returns optional state attributes. """
return self.state_attr
@property
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
# pylint: disable=unused-argument
def turn_on(self, **kwargs):
""" Turns the switch on. """
@ -49,14 +63,3 @@ class TellstickSwitch(ToggleDevice):
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

@ -11,16 +11,9 @@ from homeassistant.components.switch import (
def get_devices(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`?"))
pywemo, _ = get_pywemo()
if pywemo is None:
return []
logging.getLogger(__name__).info("Scanning for WeMo devices")
@ -31,28 +24,53 @@ def get_devices(hass, config):
if isinstance(switch, pywemo.Switch)]
def device_discovered(hass, config, info):
""" Called when a device is discovered. """
_, discovery = get_pywemo()
if discovery is None:
return
device = discovery.device_from_description(info)
return None if device is None else WemoSwitch(device)
def get_pywemo():
""" Tries to import PyWemo. """
try:
# pylint: disable=no-name-in-module, import-error
import homeassistant.external.pywemo.pywemo as pywemo
import homeassistant.external.pywemo.pywemo.discovery as discovery
return pywemo, discovery
except ImportError:
logging.getLogger(__name__).exception((
"Failed to import pywemo. "
"Did you maybe not run `git submodule init` "
"and `git submodule update`?"))
return None, None
class WemoSwitch(ToggleDevice):
""" represents a WeMo switch within home assistant. """
def __init__(self, wemo):
self.wemo = wemo
def get_name(self):
@property
def unique_id(self):
""" Returns the id of this WeMo switch """
return "{}.{}".format(self.__class__, self.wemo.serialnumber)
@property
def 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):
@property
def state_attributes(self):
""" Returns optional state attributes. """
if self.wemo.model.startswith('Belkin Insight'):
cur_info = self.wemo.insight_params
@ -64,3 +82,16 @@ class WemoSwitch(ToggleDevice):
}
else:
return {ATTR_FRIENDLY_NAME: self.wemo.name}
@property
def is_on(self):
""" True if switch is on. """
return self.wemo.get_state(True)
def turn_on(self, **kwargs):
""" Turns the switch on. """
self.wemo.on()
def turn_off(self):
""" Turns the switch off. """
self.wemo.off()

View File

@ -2,6 +2,9 @@
# Can be used to specify a catch all when registering state or event listeners.
MATCH_ALL = '*'
# If no name is specified
DEVICE_DEFAULT_NAME = "Unnamed Device"
# #### CONFIG ####
CONF_LATITUDE = "latitude"
CONF_LONGITUDE = "longitude"
@ -29,6 +32,7 @@ STATE_ON = 'on'
STATE_OFF = 'off'
STATE_HOME = 'home'
STATE_NOT_HOME = 'not_home'
STATE_UNKNOWN = "unknown"
# #### STATE AND EVENT ATTRIBUTES ####
# Contains current time for a TIME_CHANGED event

1
homeassistant/external/netdisco vendored Submodule

@ -0,0 +1 @@
Subproject commit 27026d1f4a13afceb794a176f01cad9c1b37dc3b

@ -1 +1 @@
Subproject commit 687fc4930967da6b2aa258a0e6bb0c4026a1907c
Subproject commit 7f6c383ded75f1273cbca28e858b8a8c96da66d4

View File

@ -7,7 +7,8 @@ from homeassistant import NoEntitySpecifiedError
from homeassistant.loader import get_component
from homeassistant.const import (
ATTR_ENTITY_ID, STATE_ON, STATE_OFF, CONF_PLATFORM, CONF_TYPE)
ATTR_ENTITY_ID, STATE_ON, STATE_OFF, CONF_PLATFORM, CONF_TYPE,
DEVICE_DEFAULT_NAME)
from homeassistant.util import ensure_unique_string, slugify
@ -146,20 +147,17 @@ def platform_devices_from_config(config, domain, hass,
devices.extend(p_devices)
if len(devices) == 0:
logger.error("No devices found for %s", domain)
# Setup entity IDs for each device
no_name_count = 1
device_dict = {}
for device in devices:
name = device.get_name()
no_name_count = 0
if name is None:
name = "{} #{}".format(domain, no_name_count)
for device in devices:
name = device.name
if name == DEVICE_DEFAULT_NAME:
no_name_count += 1
name = "{} #{}".format(domain, no_name_count)
entity_id = ensure_unique_string(
entity_id_format.format(slugify(name)),
@ -177,9 +175,34 @@ class Device(object):
entity_id = None
@property
def unique_id(self):
""" Returns a unique id. """
return "{}.{}".format(self.__class__, id(self))
@property
def name(self):
""" Returns the name of the device. """
return self.get_name()
@property
def state(self):
""" Returns the state of the device. """
return self.get_state()
@property
def state_attributes(self):
""" Returns the state attributes. """
return {}
# DEPRECATION NOTICE:
# Device is moving from getters to properties.
# For now the new properties will call the old functions
# This will be removed in the future.
def get_name(self):
""" Returns the name of the device if any. """
return None
return DEVICE_DEFAULT_NAME
def get_state(self):
""" Returns state of the device. """
@ -200,22 +223,32 @@ class Device(object):
"""
if self.entity_id is None:
raise NoEntitySpecifiedError(
"No entity specified for device {}".format(self.get_name()))
"No entity specified for device {}".format(self.name))
if force_refresh:
self.update()
return hass.states.set(self.entity_id, self.get_state(),
self.get_state_attributes())
return hass.states.set(self.entity_id, self.state,
self.state_attributes)
def __eq__(self, other):
return (isinstance(other, Device) and
other.unique_id == self.unique_id)
class ToggleDevice(Device):
""" ABC for devices that can be turned on and off. """
# pylint: disable=no-self-use
def get_state(self):
@property
def state(self):
""" Returns the state. """
return STATE_ON if self.is_on() else STATE_OFF
return STATE_ON if self.is_on else STATE_OFF
@property
def is_on(self):
""" True if device is on. """
return False
def turn_on(self, **kwargs):
""" Turn the device on. """
@ -224,7 +257,3 @@ class ToggleDevice(Device):
def turn_off(self, **kwargs):
""" Turn the device off. """
pass
def is_on(self):
""" True if device is on. """
return False

View File

@ -3,6 +3,9 @@ requests>=2.0
# optional, needed for specific components
# discovery
zeroconf>=0.16.0
# sun
pyephem>=3.7
@ -18,8 +21,8 @@ pyuserinput>=0.1.9
# switch.tellstick, tellstick_sensor
tellcore-py>=1.0.4
# namp_tracker plugin
# device_tracker.nmap
python-libnmap
# pushbullet
pushbullet.py>=0.5.0
# notify.pushbullet
pushbullet.py>=0.7.1

View File

@ -2,4 +2,4 @@
pylint homeassistant
flake8 homeassistant --exclude bower_components,external
python3 -m unittest discover ha_test
python3 -m unittest discover tests

View File

@ -7,7 +7,7 @@ Provides a mock switch platform.
Call init before using it in your tests to ensure clean test data.
"""
from homeassistant.const import STATE_ON, STATE_OFF
from ha_test.helpers import MockToggleDevice
from tests.helpers import MockToggleDevice
DEVICES = []

View File

@ -7,7 +7,7 @@ Provides a mock switch platform.
Call init before using it in your tests to ensure clean test data.
"""
from homeassistant.const import STATE_ON, STATE_OFF
from ha_test.helpers import MockToggleDevice
from tests.helpers import MockToggleDevice
DEVICES = []

View File

@ -1,5 +1,5 @@
"""
ha_test.helper
tests.helper
~~~~~~~~~~~~~
Helper method for writing tests.
@ -8,7 +8,7 @@ import os
import homeassistant as ha
from homeassistant.helpers import ToggleDevice
from homeassistant.const import STATE_ON, STATE_OFF
from homeassistant.const import STATE_ON, STATE_OFF, DEVICE_DEFAULT_NAME
def get_test_home_assistant():
@ -45,29 +45,37 @@ class MockModule(object):
class MockToggleDevice(ToggleDevice):
""" Provides a mock toggle device. """
def __init__(self, name, state):
self.name = name
self.state = state
self._name = name or DEVICE_DEFAULT_NAME
self._state = state
self.calls = []
def get_name(self):
@property
def name(self):
""" Returns the name of the device if any. """
self.calls.append(('get_name', {}))
return self.name
self.calls.append(('name', {}))
return self._name
@property
def state(self):
""" Returns the name of the device if any. """
self.calls.append(('state', {}))
return self._state
@property
def is_on(self):
""" True if device is on. """
self.calls.append(('is_on', {}))
return self._state == STATE_ON
def turn_on(self, **kwargs):
""" Turn the device on. """
self.calls.append(('turn_on', kwargs))
self.state = STATE_ON
self._state = STATE_ON
def turn_off(self, **kwargs):
""" Turn the device off. """
self.calls.append(('turn_off', kwargs))
self.state = STATE_OFF
def is_on(self):
""" True if device is on. """
self.calls.append(('is_on', {}))
return self.state == STATE_ON
self._state = STATE_OFF
def last_call(self, method=None):
if method is None:

View File

@ -1,5 +1,5 @@
"""
ha_test.test_component_chromecast
tests.test_component_chromecast
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Tests Chromecast component.
@ -79,12 +79,3 @@ class TestChromecast(unittest.TestCase):
self.assertEqual(service_name, call.service)
self.assertEqual(self.test_entity,
call.data.get(ATTR_ENTITY_ID))
def test_setup(self):
"""
Test Chromecast setup.
We do not have access to a Chromecast while testing so test errors.
In an ideal world we would create a mock pychromecast API..
"""
self.assertFalse(chromecast.setup(
self.hass, {chromecast.DOMAIN: {CONF_HOSTS: '127.0.0.1'}}))

View File

@ -1,5 +1,5 @@
"""
ha_test.test_component_core
tests.test_component_core
~~~~~~~~~~~~~~~~~~~~~~~~~~~
Tests core compoments.

View File

@ -1,5 +1,5 @@
"""
ha_test.test_component_demo
tests.test_component_demo
~~~~~~~~~~~~~~~~~~~~~~~~~~~
Tests demo component.

View File

@ -1,5 +1,5 @@
"""
ha_test.test_component_group
tests.test_component_group
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Tests the group compoments.
@ -75,7 +75,7 @@ class TestComponentsDeviceTracker(unittest.TestCase):
device_tracker.DOMAIN: {CONF_PLATFORM: 'test'}
}))
def test_device_tracker(self):
def test_writing_known_devices_file(self):
""" Test the device tracker class. """
scanner = loader.get_component(
'device_tracker.test').get_scanner(None, None)
@ -117,7 +117,6 @@ class TestComponentsDeviceTracker(unittest.TestCase):
dev3 = device_tracker.ENTITY_ID_FORMAT.format('DEV3')
now = datetime.now()
nowNext = now + timedelta(seconds=ha.TIMER_INTERVAL)
nowAlmostMinGone = (now + device_tracker.TIME_DEVICE_NOT_FOUND -
timedelta(seconds=1))
nowMinGone = nowAlmostMinGone + timedelta(seconds=2)

View File

@ -1,5 +1,5 @@
"""
ha_test.test_component_group
tests.test_component_group
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Tests the group compoments.
@ -9,7 +9,7 @@ import unittest
import logging
import homeassistant as ha
from homeassistant.const import STATE_ON, STATE_OFF, STATE_HOME, STATE_NOT_HOME
from homeassistant.const import STATE_ON, STATE_OFF, STATE_HOME, STATE_UNKNOWN
import homeassistant.components.group as group
@ -40,38 +40,41 @@ class TestComponentsGroup(unittest.TestCase):
""" Stop down stuff we started. """
self.hass.stop()
def test_setup_group(self):
""" Test setup_group method. """
# Try to setup a group with mixed groupable states
def test_setup_group_with_mixed_groupable_states(self):
""" Try to setup a group with mixed groupable states """
self.hass.states.set('device_tracker.Paulus', STATE_HOME)
self.assertTrue(group.setup_group(
group.setup_group(
self.hass, 'person_and_light',
['light.Bowl', 'device_tracker.Paulus']))
['light.Bowl', 'device_tracker.Paulus'])
self.assertEqual(
STATE_ON,
self.hass.states.get(
group.ENTITY_ID_FORMAT.format('person_and_light')).state)
# Try to setup a group with a non existing state
self.assertNotIn('non.existing', self.hass.states.entity_ids())
self.assertTrue(group.setup_group(
def test_setup_group_with_a_non_existing_state(self):
""" Try to setup a group with a non existing state """
grp = group.setup_group(
self.hass, 'light_and_nothing',
['light.Bowl', 'non.existing']))
self.assertEqual(
STATE_ON,
self.hass.states.get(
group.ENTITY_ID_FORMAT.format('light_and_nothing')).state)
['light.Bowl', 'non.existing'])
# Try to setup a group with non groupable states
self.assertEqual(STATE_ON, grp.state.state)
def test_setup_group_with_non_groupable_states(self):
self.hass.states.set('cast.living_room', "Plex")
self.hass.states.set('cast.bedroom', "Netflix")
self.assertFalse(
group.setup_group(
self.hass, 'chromecasts',
['cast.living_room', 'cast.bedroom']))
# Try to setup an empty group
self.assertFalse(group.setup_group(self.hass, 'nothing', []))
grp = group.setup_group(
self.hass, 'chromecasts',
['cast.living_room', 'cast.bedroom'])
self.assertEqual(STATE_UNKNOWN, grp.state.state)
def test_setup_empty_group(self):
""" Try to setup an empty group. """
grp = group.setup_group(self.hass, 'nothing', [])
self.assertEqual(STATE_UNKNOWN, grp.state.state)
def test_monitor_group(self):
""" Test if the group keeps track of states. """
@ -159,3 +162,10 @@ class TestComponentsGroup(unittest.TestCase):
self.assertEqual(STATE_ON, group_state.state)
self.assertFalse(group_state.attributes[group.ATTR_AUTO])
def test_groups_get_unique_names(self):
""" Two groups with same name should both have a unique entity id. """
grp1 = group.Group(self.hass, 'Je suis Charlie')
grp2 = group.Group(self.hass, 'Je suis Charlie')
self.assertNotEqual(grp1.entity_id, grp2.entity_id)

View File

@ -1,5 +1,5 @@
"""
ha_test.test_component_http
tests.test_component_http
~~~~~~~~~~~~~~~~~~~~~~~~~~~
Tests Home Assistant HTTP component does what it should do.

View File

@ -1,5 +1,5 @@
"""
ha_test.test_component_switch
tests.test_component_switch
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Tests switch component.

View File

@ -1,5 +1,5 @@
"""
ha_test.test_component_sun
tests.test_component_sun
~~~~~~~~~~~~~~~~~~~~~~~~~~
Tests Sun component.

View File

@ -1,5 +1,5 @@
"""
ha_test.test_component_switch
tests.test_component_switch
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Tests switch component.
@ -7,7 +7,6 @@ Tests switch component.
# pylint: disable=too-many-public-methods,protected-access
import unittest
import homeassistant as ha
import homeassistant.loader as loader
from homeassistant.const import STATE_ON, STATE_OFF, CONF_PLATFORM
import homeassistant.components.switch as switch
@ -82,29 +81,12 @@ class TestSwitch(unittest.TestCase):
self.assertTrue(switch.is_on(self.hass, self.switch_2.entity_id))
self.assertTrue(switch.is_on(self.hass, self.switch_3.entity_id))
def test_setup(self):
# Bogus config
self.assertFalse(switch.setup(self.hass, {}))
self.assertFalse(switch.setup(self.hass, {switch.DOMAIN: {}}))
# Test with non-existing component
self.assertFalse(switch.setup(
self.hass, {switch.DOMAIN: {CONF_PLATFORM: 'nonexisting'}}
))
def test_setup_two_platforms(self):
""" Test with bad config. """
# Test if switch component returns 0 switches
test_platform = loader.get_component('switch.test')
test_platform.init(True)
self.assertEqual(
[], test_platform.get_switches(None, None))
self.assertFalse(switch.setup(
self.hass, {switch.DOMAIN: {CONF_PLATFORM: 'test'}}
))
# Test if we can load 2 platforms
loader.set_component('switch.test2', test_platform)
test_platform.init(False)

View File

@ -1,5 +1,5 @@
"""
ha_test.test_core
tests.test_core
~~~~~~~~~~~~~~~~~
Provides tests to verify that Home Assistant core works.

View File

@ -1,5 +1,5 @@
"""
ha_test.test_helpers
tests.test_helpers
~~~~~~~~~~~~~~~~~~~~
Tests component helpers.

View File

@ -1,5 +1,5 @@
"""
ha_ha_test.test_loader
ha_tests.test_loader
~~~~~~~~~~~~~~~~~~~~~~
Provides tests to verify that we can load components.

View File

@ -1,5 +1,5 @@
"""
ha_test.remote
tests.remote
~~~~~~~~~~~~~~
Tests Home Assistant remote methods and classes.

View File

@ -1,5 +1,5 @@
"""
ha_test.test_util
tests.test_util
~~~~~~~~~~~~~~~~~
Tests Home Assistant util methods.