diff --git a/.gitmodules b/.gitmodules index 8ea8376a6a4..5cfe7de0098 100644 --- a/.gitmodules +++ b/.gitmodules @@ -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 diff --git a/.travis.yml b/.travis.yml index 61ed87bf6b5..e1cfad31623 100644 --- a/.travis.yml +++ b/.travis.yml @@ -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 diff --git a/homeassistant/__init__.py b/homeassistant/__init__.py index 777164b3283..e889282fae4 100644 --- a/homeassistant/__init__.py +++ b/homeassistant/__init__.py @@ -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] diff --git a/homeassistant/__main__.py b/homeassistant/__main__.py index c8d50151cea..edda63b72c0 100644 --- a/homeassistant/__main__.py +++ b/homeassistant/__main__.py @@ -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() diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 1b2a8ee7312..07dc46692d1 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -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,22 +82,11 @@ def from_config_dict(config, hass=None): add_worker = True 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 component.setup(hass, config): - 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) + if add_worker: + hass.pool.add_worker() 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 diff --git a/homeassistant/components/chromecast.py b/homeassistant/components/chromecast.py index fc5f7e73dc3..1736f0d2444 100644 --- a/homeassistant/components/chromecast.py +++ b/homeassistant/components/chromecast.py @@ -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,12 +111,35 @@ 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. """ + + # Check if already setup + if any(cast.host == host for cast in casts.values()): + return 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 except ImportError: logger.exception(("Failed to import pychromecast. " @@ -119,33 +148,24 @@ def setup(hass, config): return False - if CONF_HOSTS in config[DOMAIN]: - hosts = config[DOMAIN][CONF_HOSTS].split(",") + casts = {} - # If no hosts given, scan for chromecasts - else: + # If discovery component not loaded, scan ourselves + if discovery.DOMAIN not in hass.components: logger.info("Scanning for Chromecasts") hosts = pychromecast.discover_chromecasts() - casts = {} + for host in hosts: + setup_chromecast(casts, host) - for host in hosts: - try: - cast = pychromecast.PyChromecast(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]) - 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 - - if not casts: - logger.error("Could not find Chromecasts") - return False + discovery.listen( + hass, discovery.services.GOOGLE_CAST, chromecast_discovered) def update_chromecast_state(entity_id, chromecast): """ Retrieve state of Chromecast and update statemachine. """ @@ -194,10 +214,11 @@ def setup(hass, config): def update_chromecast_states(time): # pylint: disable=unused-argument """ Updates all chromecast states. """ - logger.info("Updating Chromecast status") + if casts: + logger.info("Updating Chromecast status") - for entity_id, cast in casts.items(): - update_chromecast_state(entity_id, cast) + for entity_id, cast in casts.items(): + update_chromecast_state(entity_id, cast) def _service_to_entities(service): """ Helper method to get entities from service. """ diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index c478e118036..3da9054be8d 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -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) diff --git a/homeassistant/components/device_tracker/luci.py b/homeassistant/components/device_tracker/luci.py index 9ed73f21375..9dbf503fc94 100644 --- a/homeassistant/components/device_tracker/luci.py +++ b/homeassistant/components/device_tracker/luci.py @@ -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 diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery.py new file mode 100644 index 00000000000..99c13c63794 --- /dev/null +++ b/homeassistant/components/discovery.py @@ -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 diff --git a/homeassistant/components/group.py b/homeassistant/components/group.py index eac63ee845b..05c69b6e230 100644 --- a/homeassistant/components/group.py +++ b/homeassistant/components/group.py @@ -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) diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index 352cc500a9b..6a8b97f363e 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -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. """ diff --git a/homeassistant/components/light/hue.py b/homeassistant/components/light/hue.py index bf8dae839c5..04291217eff 100644 --- a/homeassistant/components/light/hue.py +++ b/homeassistant/components/light/hue.py @@ -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) diff --git a/homeassistant/components/notify/__init__.py b/homeassistant/components/notify/__init__.py index d0c73045f63..0728a979588 100644 --- a/homeassistant/components/notify/__init__.py +++ b/homeassistant/components/notify/__init__.py @@ -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 diff --git a/homeassistant/components/notify/pushbullet.py b/homeassistant/components/notify/pushbullet.py index 0f22d7d2970..953fa874458 100644 --- a/homeassistant/components/notify/pushbullet.py +++ b/homeassistant/components/notify/pushbullet.py @@ -22,16 +22,22 @@ 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 - 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 diff --git a/homeassistant/components/switch/__init__.py b/homeassistant/components/switch/__init__.py index 4c687b31ef6..c601fad92f7 100644 --- a/homeassistant/components/switch/__init__.py +++ b/homeassistant/components/switch/__init__.py @@ -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,21 +64,41 @@ 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") - 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) + # 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]) diff --git a/homeassistant/components/switch/tellstick.py b/homeassistant/components/switch/tellstick.py index 54b85b3ecb4..67336481c0a 100644 --- a/homeassistant/components/switch/tellstick.py +++ b/homeassistant/components/switch/tellstick.py @@ -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 diff --git a/homeassistant/components/switch/wemo.py b/homeassistant/components/switch/wemo.py index e75d1832ea1..3c3db895713 100644 --- a/homeassistant/components/switch/wemo.py +++ b/homeassistant/components/switch/wemo.py @@ -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() diff --git a/homeassistant/const.py b/homeassistant/const.py index c5f897fb63e..f1a14b35de7 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -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 diff --git a/homeassistant/external/netdisco b/homeassistant/external/netdisco new file mode 160000 index 00000000000..27026d1f4a1 --- /dev/null +++ b/homeassistant/external/netdisco @@ -0,0 +1 @@ +Subproject commit 27026d1f4a13afceb794a176f01cad9c1b37dc3b diff --git a/homeassistant/external/pywemo b/homeassistant/external/pywemo index 687fc493096..7f6c383ded7 160000 --- a/homeassistant/external/pywemo +++ b/homeassistant/external/pywemo @@ -1 +1 @@ -Subproject commit 687fc4930967da6b2aa258a0e6bb0c4026a1907c +Subproject commit 7f6c383ded75f1273cbca28e858b8a8c96da66d4 diff --git a/homeassistant/helpers.py b/homeassistant/helpers.py index 8bd69d0b1a0..70a085056a2 100644 --- a/homeassistant/helpers.py +++ b/homeassistant/helpers.py @@ -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 diff --git a/requirements.txt b/requirements.txt index f0fb8e89f12..e0e003dd5eb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 diff --git a/run_tests.sh b/run_tests.sh index c5b93d4440d..899c7f8cba3 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -2,4 +2,4 @@ pylint homeassistant flake8 homeassistant --exclude bower_components,external -python3 -m unittest discover ha_test +python3 -m unittest discover tests diff --git a/ha_test/config/custom_components/device_tracker/test.py b/tests/config/custom_components/device_tracker/test.py similarity index 100% rename from ha_test/config/custom_components/device_tracker/test.py rename to tests/config/custom_components/device_tracker/test.py diff --git a/ha_test/config/custom_components/light/test.py b/tests/config/custom_components/light/test.py similarity index 92% rename from ha_test/config/custom_components/light/test.py rename to tests/config/custom_components/light/test.py index 0ed04a21717..9b4ebcac2b1 100644 --- a/ha_test/config/custom_components/light/test.py +++ b/tests/config/custom_components/light/test.py @@ -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 = [] diff --git a/ha_test/config/custom_components/switch/test.py b/tests/config/custom_components/switch/test.py similarity index 92% rename from ha_test/config/custom_components/switch/test.py rename to tests/config/custom_components/switch/test.py index 682c27f695f..35e544fa0cb 100644 --- a/ha_test/config/custom_components/switch/test.py +++ b/tests/config/custom_components/switch/test.py @@ -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 = [] diff --git a/ha_test/helpers.py b/tests/helpers.py similarity index 76% rename from ha_test/helpers.py rename to tests/helpers.py index f04dac72553..48808361410 100644 --- a/ha_test/helpers.py +++ b/tests/helpers.py @@ -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: diff --git a/ha_test/test_component_chromecast.py b/tests/test_component_chromecast.py similarity index 88% rename from ha_test/test_component_chromecast.py rename to tests/test_component_chromecast.py index 75ac9765c63..962afcf982a 100644 --- a/ha_test/test_component_chromecast.py +++ b/tests/test_component_chromecast.py @@ -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'}})) diff --git a/ha_test/test_component_core.py b/tests/test_component_core.py similarity index 98% rename from ha_test/test_component_core.py rename to tests/test_component_core.py index 2c53d578277..8c00616bbb4 100644 --- a/ha_test/test_component_core.py +++ b/tests/test_component_core.py @@ -1,5 +1,5 @@ """ -ha_test.test_component_core +tests.test_component_core ~~~~~~~~~~~~~~~~~~~~~~~~~~~ Tests core compoments. diff --git a/ha_test/test_component_demo.py b/tests/test_component_demo.py similarity index 98% rename from ha_test/test_component_demo.py rename to tests/test_component_demo.py index b687653a0bc..d92b292f312 100644 --- a/ha_test/test_component_demo.py +++ b/tests/test_component_demo.py @@ -1,5 +1,5 @@ """ -ha_test.test_component_demo +tests.test_component_demo ~~~~~~~~~~~~~~~~~~~~~~~~~~~ Tests demo component. diff --git a/ha_test/test_component_device_scanner.py b/tests/test_component_device_scanner.py similarity index 98% rename from ha_test/test_component_device_scanner.py rename to tests/test_component_device_scanner.py index 3c6385bc42f..1accc1c5ff8 100644 --- a/ha_test/test_component_device_scanner.py +++ b/tests/test_component_device_scanner.py @@ -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) diff --git a/ha_test/test_component_group.py b/tests/test_component_group.py similarity index 80% rename from ha_test/test_component_group.py rename to tests/test_component_group.py index 4e6307aa2b5..2be58b5acc3 100644 --- a/ha_test/test_component_group.py +++ b/tests/test_component_group.py @@ -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) diff --git a/ha_test/test_component_http.py b/tests/test_component_http.py similarity index 99% rename from ha_test/test_component_http.py rename to tests/test_component_http.py index 98b976cf099..ba547e2bbe4 100644 --- a/ha_test/test_component_http.py +++ b/tests/test_component_http.py @@ -1,5 +1,5 @@ """ -ha_test.test_component_http +tests.test_component_http ~~~~~~~~~~~~~~~~~~~~~~~~~~~ Tests Home Assistant HTTP component does what it should do. diff --git a/ha_test/test_component_light.py b/tests/test_component_light.py similarity index 99% rename from ha_test/test_component_light.py rename to tests/test_component_light.py index e9cb219d07b..84fb07d6427 100644 --- a/ha_test/test_component_light.py +++ b/tests/test_component_light.py @@ -1,5 +1,5 @@ """ -ha_test.test_component_switch +tests.test_component_switch ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Tests switch component. diff --git a/ha_test/test_component_sun.py b/tests/test_component_sun.py similarity index 99% rename from ha_test/test_component_sun.py rename to tests/test_component_sun.py index a587f60bff5..33570cdcc4e 100644 --- a/ha_test/test_component_sun.py +++ b/tests/test_component_sun.py @@ -1,5 +1,5 @@ """ -ha_test.test_component_sun +tests.test_component_sun ~~~~~~~~~~~~~~~~~~~~~~~~~~ Tests Sun component. diff --git a/ha_test/test_component_switch.py b/tests/test_component_switch.py similarity index 83% rename from ha_test/test_component_switch.py rename to tests/test_component_switch.py index 687df62ed5f..9c2624e0ce6 100644 --- a/ha_test/test_component_switch.py +++ b/tests/test_component_switch.py @@ -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) diff --git a/ha_test/test_core.py b/tests/test_core.py similarity index 99% rename from ha_test/test_core.py rename to tests/test_core.py index 2c86cbab1b7..d3d97e0b8d4 100644 --- a/ha_test/test_core.py +++ b/tests/test_core.py @@ -1,5 +1,5 @@ """ -ha_test.test_core +tests.test_core ~~~~~~~~~~~~~~~~~ Provides tests to verify that Home Assistant core works. diff --git a/ha_test/test_helpers.py b/tests/test_helpers.py similarity index 98% rename from ha_test/test_helpers.py rename to tests/test_helpers.py index f61204c837f..48e73536c03 100644 --- a/ha_test/test_helpers.py +++ b/tests/test_helpers.py @@ -1,5 +1,5 @@ """ -ha_test.test_helpers +tests.test_helpers ~~~~~~~~~~~~~~~~~~~~ Tests component helpers. diff --git a/ha_test/test_loader.py b/tests/test_loader.py similarity index 99% rename from ha_test/test_loader.py rename to tests/test_loader.py index b7ae75c0e2a..dd80587b247 100644 --- a/ha_test/test_loader.py +++ b/tests/test_loader.py @@ -1,5 +1,5 @@ """ -ha_ha_test.test_loader +ha_tests.test_loader ~~~~~~~~~~~~~~~~~~~~~~ Provides tests to verify that we can load components. diff --git a/ha_test/test_remote.py b/tests/test_remote.py similarity index 99% rename from ha_test/test_remote.py rename to tests/test_remote.py index f6de538e54a..e22eca3e49f 100644 --- a/ha_test/test_remote.py +++ b/tests/test_remote.py @@ -1,5 +1,5 @@ """ -ha_test.remote +tests.remote ~~~~~~~~~~~~~~ Tests Home Assistant remote methods and classes. diff --git a/ha_test/test_util.py b/tests/test_util.py similarity index 99% rename from ha_test/test_util.py rename to tests/test_util.py index 0f606fb45f2..038db227e1a 100644 --- a/ha_test/test_util.py +++ b/tests/test_util.py @@ -1,5 +1,5 @@ """ -ha_test.test_util +tests.test_util ~~~~~~~~~~~~~~~~~ Tests Home Assistant util methods.