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"] [submodule "homeassistant/external/pywemo"]
path = homeassistant/external/pywemo path = homeassistant/external/pywemo
url = https://github.com/balloob/pywemo.git 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: script:
- flake8 homeassistant --exclude bower_components,external - flake8 homeassistant --exclude bower_components,external
- pylint homeassistant - pylint homeassistant
- coverage run --source=homeassistant -m unittest discover ha_test - coverage run --source=homeassistant -m unittest discover tests
after_success: after_success:
- coveralls - coveralls

View File

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

View File

@ -18,19 +18,8 @@ except ImportError:
from homeassistant import bootstrap from homeassistant import bootstrap
def main(): def validate_dependencies():
""" Starts Home Assistant. Will create demo config if no config found. """ """ Validate all dependencies that HA uses. """
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
import_fail = False import_fail = False
for module in ['requests']: for module in ['requests']:
@ -44,11 +33,14 @@ def main():
if import_fail: if import_fail:
print(("Install dependencies by running: " print(("Install dependencies by running: "
"pip3 install -r requirements.txt")) "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 # Test if configuration directory exists
config_dir = os.path.join(os.getcwd(), args.config)
if not os.path.isdir(config_dir): if not os.path.isdir(config_dir):
print(('Fatal Error: Unable to find specified configuration ' print(('Fatal Error: Unable to find specified configuration '
'directory {} ').format(config_dir)) 'directory {} ').format(config_dir))
@ -68,6 +60,27 @@ def main():
'to write a default one to {}').format(config_path)) 'to write a default one to {}').format(config_path))
sys.exit() 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 = bootstrap.from_config_file(config_path)
hass.start() hass.start()
hass.block_till_stopped() hass.block_till_stopped()

View File

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

View File

@ -6,13 +6,19 @@ Provides functionality to interact with Chromecasts.
""" """
import logging 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 import homeassistant.util as util
from homeassistant.helpers import extract_entity_ids from homeassistant.helpers import extract_entity_ids
from homeassistant.const import ( from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, SERVICE_TURN_OFF, SERVICE_VOLUME_UP, ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, SERVICE_TURN_OFF, SERVICE_VOLUME_UP,
SERVICE_VOLUME_DOWN, SERVICE_MEDIA_PLAY_PAUSE, SERVICE_MEDIA_PLAY, SERVICE_VOLUME_DOWN, SERVICE_MEDIA_PLAY_PAUSE, SERVICE_MEDIA_PLAY,
SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PREV_TRACK, SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PREV_TRACK)
CONF_HOSTS)
DOMAIN = 'chromecast' DOMAIN = 'chromecast'
@ -105,12 +111,35 @@ def media_prev_track(hass, entity_id=None):
hass.services.call(DOMAIN, SERVICE_MEDIA_PREV_TRACK, data) hass.services.call(DOMAIN, SERVICE_MEDIA_PREV_TRACK, data)
# pylint: disable=too-many-locals, too-many-branches def setup_chromecast(casts, host):
def setup(hass, config): """ Tries to convert host to Chromecast object and set it up. """
""" Listen for chromecast events. """
logger = logging.getLogger(__name__) # Check if already setup
if any(cast.host == host for cast in casts.values()):
return
try: try:
cast = pychromecast.PyChromecast(host)
entity_id = util.ensure_unique_string(
ENTITY_ID_FORMAT.format(
util.slugify(cast.device.friendly_name)),
casts.keys())
casts[entity_id] = cast
except pychromecast.ChromecastConnectionError:
pass
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 import pychromecast
except ImportError: except ImportError:
logger.exception(("Failed to import pychromecast. " logger.exception(("Failed to import pychromecast. "
@ -119,33 +148,24 @@ def setup(hass, config):
return False return False
if CONF_HOSTS in config[DOMAIN]: casts = {}
hosts = config[DOMAIN][CONF_HOSTS].split(",")
# If no hosts given, scan for chromecasts # If discovery component not loaded, scan ourselves
else: if discovery.DOMAIN not in hass.components:
logger.info("Scanning for Chromecasts") logger.info("Scanning for Chromecasts")
hosts = pychromecast.discover_chromecasts() hosts = pychromecast.discover_chromecasts()
casts = {} for host in hosts:
setup_chromecast(casts, host)
for host in hosts: # pylint: disable=unused-argument
try: def chromecast_discovered(service, info):
cast = pychromecast.PyChromecast(host) """ Called when a Chromecast has been discovered. """
logger.info("New Chromecast discovered: %s", info[0])
setup_chromecast(casts, info[0])
entity_id = util.ensure_unique_string( discovery.listen(
ENTITY_ID_FORMAT.format( hass, discovery.services.GOOGLE_CAST, chromecast_discovered)
util.slugify(cast.device.friendly_name)),
casts.keys())
casts[entity_id] = cast
except pychromecast.ChromecastConnectionError:
pass
if not casts:
logger.error("Could not find Chromecasts")
return False
def update_chromecast_state(entity_id, chromecast): def update_chromecast_state(entity_id, chromecast):
""" Retrieve state of Chromecast and update statemachine. """ """ Retrieve state of Chromecast and update statemachine. """
@ -194,10 +214,11 @@ def setup(hass, config):
def update_chromecast_states(time): # pylint: disable=unused-argument def update_chromecast_states(time): # pylint: disable=unused-argument
""" Updates all chromecast states. """ """ Updates all chromecast states. """
logger.info("Updating Chromecast status") if casts:
logger.info("Updating Chromecast status")
for entity_id, cast in casts.items(): for entity_id, cast in casts.items():
update_chromecast_state(entity_id, cast) update_chromecast_state(entity_id, cast)
def _service_to_entities(service): def _service_to_entities(service):
""" Helper method to get entities from service. """ """ Helper method to get entities from service. """

View File

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

View File

@ -101,7 +101,12 @@ class LuciDeviceScanner(object):
result = _req_json_rpc(url, 'net.arptable', result = _req_json_rpc(url, 'net.arptable',
params={'auth': self.token}) params={'auth': self.token})
if result: 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 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. Provides functionality to group devices that can be turned on or off.
""" """
import logging
import homeassistant as ha import homeassistant as ha
import homeassistant.util as util import homeassistant.util as util
from homeassistant.const import ( 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" DOMAIN = "group"
DEPENDENCIES = [] DEPENDENCIES = []
@ -22,8 +21,6 @@ ATTR_AUTO = "auto"
# List of ON/OFF state tuples for groupable states # List of ON/OFF state tuples for groupable states
_GROUP_TYPES = [(STATE_ON, STATE_OFF), (STATE_HOME, STATE_NOT_HOME)] _GROUP_TYPES = [(STATE_ON, STATE_OFF), (STATE_HOME, STATE_NOT_HOME)]
_GROUPS = {}
def _get_group_on_off(state): def _get_group_on_off(state):
""" Determine the group on/off states based on a 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): def setup(hass, config):
""" Sets up all groups found definded in the configuration. """ """ Sets up all groups found definded in the configuration. """
for name, entity_ids in config.get(DOMAIN, {}).items(): for name, entity_ids in config.get(DOMAIN, {}).items():
entity_ids = entity_ids.split(",") setup_group(hass, name, entity_ids.split(","))
setup_group(hass, name, entity_ids)
return True return True
def setup_group(hass, name, entity_ids, user_defined=True): class Group(object):
""" Sets up a group state that is the combined state of """ Tracks a group of entity ids. """
several states. Supports ON/OFF and DEVICE_HOME/DEVICE_NOT_HOME. """ def __init__(self, hass, name, entity_ids=None, user_defined=True):
logger = logging.getLogger(__name__) self.hass = hass
self.name = name
self.user_defined = user_defined
# In case an iterable is passed in self.entity_id = util.ensure_unique_string(
entity_ids = list(entity_ids) ENTITY_ID_FORMAT.format(util.slugify(name)),
hass.states.entity_ids(DOMAIN))
if not entity_ids: self.tracking = []
logger.error( self.group_on, self.group_off = None, None
'Error setting up group %s: no entities passed in to track', name)
return False if entity_ids is not None:
self.update_tracked_entity_ids(entity_ids)
# 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
else: else:
group_ids.append(entity_id) self.force_update()
# Keep track of the group state to init later on @property
group_state = group_state or state.state == group_on 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 @property
if not group_ids: def state_attr(self):
logger.error('Unable to find any entities to track for group %s', name) """ 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: self.force_update()
logger.warning(
'Warnings during setting up group %s: %s',
name, ", ".join(warnings))
group_entity_id = ENTITY_ID_FORMAT.format(util.slugify(name)) self.start()
state = group_on if group_state else group_off
state_attr = {ATTR_ENTITY_ID: group_ids, ATTR_AUTO: not user_defined} 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 # 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 """ Updates the group state based on a state change by
a tracked entity. """ 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 = OFF and new_state = ON: set ON
# if cur_gr_state = ON and new_state = OFF: research # 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: 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 # Check if any of the other states is still on
if not any([hass.states.is_state(ent_id, group_on) if not any(self.hass.states.is_state(ent_id, group_on)
for ent_id in group_ids for ent_id in self.tracking if entity_id != ent_id):
if entity_id != ent_id]): self.hass.states.set(
hass.states.set(group_entity_id, group_off, state_attr) self.entity_id, group_off, self.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
def remove_group(hass, name): def setup_group(hass, name, entity_ids, user_defined=True):
""" Remove a group and its state listener from Home Assistant. """ """ Sets up a group state that is the combined state of
group_entity_id = ENTITY_ID_FORMAT.format(util.slugify(name)) several states. Supports ON/OFF and DEVICE_HOME/DEVICE_NOT_HOME. """
if hass.states.get(group_entity_id) is not None: return Group(hass, name, entity_ids, user_defined)
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))

View File

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

View File

@ -77,9 +77,36 @@ class HueLight(ToggleDevice):
self.bridge = bridge self.bridge = bridge
self.update_lights = update_lights 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. """ """ 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): def turn_on(self, **kwargs):
""" Turn the specified or all lights on. """ """ Turn the specified or all lights on. """
@ -118,24 +145,6 @@ class HueLight(ToggleDevice):
self.bridge.set_light(self.light_id, command) 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): def update(self):
""" Synchronize state with bridge. """ """ Synchronize state with bridge. """
self.update_lights(no_throttle=True) self.update_lights(no_throttle=True)

View File

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

View File

@ -22,16 +22,22 @@ def get_service(hass, config):
try: try:
# pylint: disable=unused-variable # pylint: disable=unused-variable
from pushbullet import PushBullet # noqa from pushbullet import PushBullet, InvalidKeyError # noqa
except ImportError: except ImportError:
_LOGGER.exception( _LOGGER.exception(
"Unable to import pushbullet. " "Unable to import pushbullet. "
"Did you maybe not install the 'pushbullet' package?") "Did you maybe not install the 'pushbullet.py' package?")
return None return None
return PushBulletNotificationService(config[DOMAIN][CONF_API_KEY]) 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 # pylint: disable=too-few-public-methods

View File

@ -6,12 +6,13 @@ Component to interface with various switches that can be controlled remotely.
import logging import logging
from datetime import timedelta from datetime import timedelta
from homeassistant.loader import get_component
import homeassistant.util as util import homeassistant.util as util
from homeassistant.const import ( from homeassistant.const import (
STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, ATTR_ENTITY_ID) STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, ATTR_ENTITY_ID)
from homeassistant.helpers import ( from homeassistant.helpers import (
extract_entity_ids, platform_devices_from_config) extract_entity_ids, platform_devices_from_config)
from homeassistant.components import group from homeassistant.components import group, discovery
DOMAIN = 'switch' DOMAIN = 'switch'
DEPENDENCIES = [] DEPENDENCIES = []
@ -27,6 +28,11 @@ ATTR_CURRENT_POWER_MWH = "current_power_mwh"
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
# Maps discovered services to their platforms
DISCOVERY = {
discovery.services.BELKIN_WEMO: 'wemo'
}
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -58,21 +64,41 @@ def setup(hass, config):
switches = platform_devices_from_config( switches = platform_devices_from_config(
config, DOMAIN, hass, ENTITY_ID_FORMAT, logger) config, DOMAIN, hass, ENTITY_ID_FORMAT, logger)
if not switches:
return False
# pylint: disable=unused-argument # pylint: disable=unused-argument
@util.Throttle(MIN_TIME_BETWEEN_SCANS) @util.Throttle(MIN_TIME_BETWEEN_SCANS)
def update_states(now): def update_states(now):
""" Update states of all switches. """ """ Update states of all switches. """
if switches:
logger.info("Updating switch states")
logger.info("Updating switch states") for switch in switches.values():
switch.update_ha_state(hass)
for switch in switches.values():
switch.update_ha_state(hass)
update_states(None) 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): def handle_switch_service(service):
""" Handles calls to the switch services. """ """ Handles calls to the switch services. """
target_switches = [switches[entity_id] for entity_id target_switches = [switches[entity_id] for entity_id
@ -90,10 +116,6 @@ def setup(hass, config):
switch.update_ha_state(hass) 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 # Update state every 30 seconds
hass.track_time_change(update_states, second=[0, 30]) hass.track_time_change(update_states, second=[0, 30])

View File

@ -36,10 +36,24 @@ class TellstickSwitch(ToggleDevice):
self.tellstick = tellstick self.tellstick = tellstick
self.state_attr = {ATTR_FRIENDLY_NAME: tellstick.name} self.state_attr = {ATTR_FRIENDLY_NAME: tellstick.name}
def get_name(self): @property
def name(self):
""" Returns the name of the switch if any. """ """ Returns the name of the switch if any. """
return self.tellstick.name 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 # pylint: disable=unused-argument
def turn_on(self, **kwargs): def turn_on(self, **kwargs):
""" Turns the switch on. """ """ Turns the switch on. """
@ -49,14 +63,3 @@ class TellstickSwitch(ToggleDevice):
def turn_off(self, **kwargs): def turn_off(self, **kwargs):
""" Turns the switch off. """ """ Turns the switch off. """
self.tellstick.turn_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): def get_devices(hass, config):
""" Find and return WeMo switches. """ """ Find and return WeMo switches. """
try: pywemo, _ = get_pywemo()
# 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`?"))
if pywemo is None:
return [] return []
logging.getLogger(__name__).info("Scanning for WeMo devices") logging.getLogger(__name__).info("Scanning for WeMo devices")
@ -31,28 +24,53 @@ def get_devices(hass, config):
if isinstance(switch, pywemo.Switch)] 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): class WemoSwitch(ToggleDevice):
""" represents a WeMo switch within home assistant. """ """ represents a WeMo switch within home assistant. """
def __init__(self, wemo): def __init__(self, wemo):
self.wemo = 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. """ """ Returns the name of the switch if any. """
return self.wemo.name return self.wemo.name
def turn_on(self, **kwargs): @property
""" Turns the switch on. """ def state_attributes(self):
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. """ """ Returns optional state attributes. """
if self.wemo.model.startswith('Belkin Insight'): if self.wemo.model.startswith('Belkin Insight'):
cur_info = self.wemo.insight_params cur_info = self.wemo.insight_params
@ -64,3 +82,16 @@ class WemoSwitch(ToggleDevice):
} }
else: else:
return {ATTR_FRIENDLY_NAME: self.wemo.name} 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. # Can be used to specify a catch all when registering state or event listeners.
MATCH_ALL = '*' MATCH_ALL = '*'
# If no name is specified
DEVICE_DEFAULT_NAME = "Unnamed Device"
# #### CONFIG #### # #### CONFIG ####
CONF_LATITUDE = "latitude" CONF_LATITUDE = "latitude"
CONF_LONGITUDE = "longitude" CONF_LONGITUDE = "longitude"
@ -29,6 +32,7 @@ STATE_ON = 'on'
STATE_OFF = 'off' STATE_OFF = 'off'
STATE_HOME = 'home' STATE_HOME = 'home'
STATE_NOT_HOME = 'not_home' STATE_NOT_HOME = 'not_home'
STATE_UNKNOWN = "unknown"
# #### STATE AND EVENT ATTRIBUTES #### # #### STATE AND EVENT ATTRIBUTES ####
# Contains current time for a TIME_CHANGED event # 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.loader import get_component
from homeassistant.const import ( 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 from homeassistant.util import ensure_unique_string, slugify
@ -146,20 +147,17 @@ def platform_devices_from_config(config, domain, hass,
devices.extend(p_devices) devices.extend(p_devices)
if len(devices) == 0:
logger.error("No devices found for %s", domain)
# Setup entity IDs for each device # Setup entity IDs for each device
no_name_count = 1
device_dict = {} device_dict = {}
for device in devices: no_name_count = 0
name = device.get_name()
if name is None: for device in devices:
name = "{} #{}".format(domain, no_name_count) name = device.name
if name == DEVICE_DEFAULT_NAME:
no_name_count += 1 no_name_count += 1
name = "{} #{}".format(domain, no_name_count)
entity_id = ensure_unique_string( entity_id = ensure_unique_string(
entity_id_format.format(slugify(name)), entity_id_format.format(slugify(name)),
@ -177,9 +175,34 @@ class Device(object):
entity_id = None 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): def get_name(self):
""" Returns the name of the device if any. """ """ Returns the name of the device if any. """
return None return DEVICE_DEFAULT_NAME
def get_state(self): def get_state(self):
""" Returns state of the device. """ """ Returns state of the device. """
@ -200,22 +223,32 @@ class Device(object):
""" """
if self.entity_id is None: if self.entity_id is None:
raise NoEntitySpecifiedError( raise NoEntitySpecifiedError(
"No entity specified for device {}".format(self.get_name())) "No entity specified for device {}".format(self.name))
if force_refresh: if force_refresh:
self.update() self.update()
return hass.states.set(self.entity_id, self.get_state(), return hass.states.set(self.entity_id, self.state,
self.get_state_attributes()) self.state_attributes)
def __eq__(self, other):
return (isinstance(other, Device) and
other.unique_id == self.unique_id)
class ToggleDevice(Device): class ToggleDevice(Device):
""" ABC for devices that can be turned on and off. """ """ ABC for devices that can be turned on and off. """
# pylint: disable=no-self-use # pylint: disable=no-self-use
def get_state(self): @property
def state(self):
""" Returns the state. """ """ 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): def turn_on(self, **kwargs):
""" Turn the device on. """ """ Turn the device on. """
@ -224,7 +257,3 @@ class ToggleDevice(Device):
def turn_off(self, **kwargs): def turn_off(self, **kwargs):
""" Turn the device off. """ """ Turn the device off. """
pass 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 # optional, needed for specific components
# discovery
zeroconf>=0.16.0
# sun # sun
pyephem>=3.7 pyephem>=3.7
@ -18,8 +21,8 @@ pyuserinput>=0.1.9
# switch.tellstick, tellstick_sensor # switch.tellstick, tellstick_sensor
tellcore-py>=1.0.4 tellcore-py>=1.0.4
# namp_tracker plugin # device_tracker.nmap
python-libnmap python-libnmap
# pushbullet # notify.pushbullet
pushbullet.py>=0.5.0 pushbullet.py>=0.7.1

View File

@ -2,4 +2,4 @@
pylint homeassistant pylint homeassistant
flake8 homeassistant --exclude bower_components,external 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. Call init before using it in your tests to ensure clean test data.
""" """
from homeassistant.const import STATE_ON, STATE_OFF from homeassistant.const import STATE_ON, STATE_OFF
from ha_test.helpers import MockToggleDevice from tests.helpers import MockToggleDevice
DEVICES = [] 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. Call init before using it in your tests to ensure clean test data.
""" """
from homeassistant.const import STATE_ON, STATE_OFF from homeassistant.const import STATE_ON, STATE_OFF
from ha_test.helpers import MockToggleDevice from tests.helpers import MockToggleDevice
DEVICES = [] DEVICES = []

View File

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

View File

@ -1,5 +1,5 @@
""" """
ha_test.test_component_chromecast tests.test_component_chromecast
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Tests Chromecast component. Tests Chromecast component.
@ -79,12 +79,3 @@ class TestChromecast(unittest.TestCase):
self.assertEqual(service_name, call.service) self.assertEqual(service_name, call.service)
self.assertEqual(self.test_entity, self.assertEqual(self.test_entity,
call.data.get(ATTR_ENTITY_ID)) 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. Tests core compoments.

View File

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

View File

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

View File

@ -1,5 +1,5 @@
""" """
ha_test.test_component_group tests.test_component_group
~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Tests the group compoments. Tests the group compoments.
@ -9,7 +9,7 @@ import unittest
import logging import logging
import homeassistant as ha 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 import homeassistant.components.group as group
@ -40,38 +40,41 @@ class TestComponentsGroup(unittest.TestCase):
""" Stop down stuff we started. """ """ Stop down stuff we started. """
self.hass.stop() self.hass.stop()
def test_setup_group(self): def test_setup_group_with_mixed_groupable_states(self):
""" Test setup_group method. """ """ Try to setup a group with mixed groupable states """
# Try to setup a group with mixed groupable states
self.hass.states.set('device_tracker.Paulus', STATE_HOME) self.hass.states.set('device_tracker.Paulus', STATE_HOME)
self.assertTrue(group.setup_group( group.setup_group(
self.hass, 'person_and_light', self.hass, 'person_and_light',
['light.Bowl', 'device_tracker.Paulus'])) ['light.Bowl', 'device_tracker.Paulus'])
self.assertEqual( self.assertEqual(
STATE_ON, STATE_ON,
self.hass.states.get( self.hass.states.get(
group.ENTITY_ID_FORMAT.format('person_and_light')).state) group.ENTITY_ID_FORMAT.format('person_and_light')).state)
# Try to setup a group with a non existing state def test_setup_group_with_a_non_existing_state(self):
self.assertNotIn('non.existing', self.hass.states.entity_ids()) """ Try to setup a group with a non existing state """
self.assertTrue(group.setup_group( grp = group.setup_group(
self.hass, 'light_and_nothing', self.hass, 'light_and_nothing',
['light.Bowl', 'non.existing'])) ['light.Bowl', 'non.existing'])
self.assertEqual(
STATE_ON,
self.hass.states.get(
group.ENTITY_ID_FORMAT.format('light_and_nothing')).state)
# 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.living_room', "Plex")
self.hass.states.set('cast.bedroom', "Netflix") 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 grp = group.setup_group(
self.assertFalse(group.setup_group(self.hass, 'nothing', [])) 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): def test_monitor_group(self):
""" Test if the group keeps track of states. """ """ Test if the group keeps track of states. """
@ -159,3 +162,10 @@ class TestComponentsGroup(unittest.TestCase):
self.assertEqual(STATE_ON, group_state.state) self.assertEqual(STATE_ON, group_state.state)
self.assertFalse(group_state.attributes[group.ATTR_AUTO]) 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. 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. Tests switch component.

View File

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

View File

@ -1,5 +1,5 @@
""" """
ha_test.test_component_switch tests.test_component_switch
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Tests switch component. Tests switch component.
@ -7,7 +7,6 @@ Tests switch component.
# pylint: disable=too-many-public-methods,protected-access # pylint: disable=too-many-public-methods,protected-access
import unittest import unittest
import homeassistant as ha
import homeassistant.loader as loader import homeassistant.loader as loader
from homeassistant.const import STATE_ON, STATE_OFF, CONF_PLATFORM from homeassistant.const import STATE_ON, STATE_OFF, CONF_PLATFORM
import homeassistant.components.switch as switch 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_2.entity_id))
self.assertTrue(switch.is_on(self.hass, self.switch_3.entity_id)) self.assertTrue(switch.is_on(self.hass, self.switch_3.entity_id))
def test_setup(self): def test_setup_two_platforms(self):
# Bogus config """ Test with bad 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'}}
))
# Test if switch component returns 0 switches # Test if switch component returns 0 switches
test_platform = loader.get_component('switch.test') test_platform = loader.get_component('switch.test')
test_platform.init(True) 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) loader.set_component('switch.test2', test_platform)
test_platform.init(False) 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. 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. 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. 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. 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. Tests Home Assistant util methods.