From 2e10d7223a935c296cbc51fbb70acd6618eadd0f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 24 Apr 2014 00:40:45 -0700 Subject: [PATCH] Re-organized core for better reusability --- home-assistant.conf.default | 6 +- homeassistant/__init__.py | 611 +++++++++--------- homeassistant/bootstrap.py | 77 +-- homeassistant/components/__init__.py | 60 +- homeassistant/components/browser.py | 12 +- homeassistant/components/chromecast.py | 117 ++-- .../components/device_sun_light_trigger.py | 54 +- homeassistant/components/device_tracker.py | 33 +- homeassistant/components/downloader.py | 8 +- homeassistant/components/group.py | 40 +- .../components/httpinterface/__init__.py | 40 +- homeassistant/components/keyboard.py | 62 +- homeassistant/components/light/__init__.py | 40 +- homeassistant/components/sun.py | 21 +- homeassistant/components/wemo.py | 50 +- homeassistant/remote.py | 157 ++--- homeassistant/test.py | 105 ++- homeassistant/util.py | 30 +- start.py | 3 +- 19 files changed, 772 insertions(+), 754 deletions(-) diff --git a/home-assistant.conf.default b/home-assistant.conf.default index 159a483d7e60..b26ccfa2f101 100644 --- a/home-assistant.conf.default +++ b/home-assistant.conf.default @@ -24,6 +24,8 @@ password=PASSWORD # hosts=192.168.1.9,192.168.1.12 [wemo] +# Optional: hard code the hosts to find WeMos instead of scanning the network +# hosts=192.168.1.9,192.168.1.12 [downloader] download_dir=downloads @@ -34,8 +36,8 @@ download_dir=downloads # Example how you can specify which light profile to use when turning lights on # light_profile=relax -# A comma seperated list of states that have to be tracked -# As a single group +# A comma seperated list of states that have to be tracked as a single group +# Grouped states should share the same states (ON/OFF or HOME/NOT_HOME) [group] living_room=light.Bowl,light.Ceiling,light.TV_back_light bedroom=light.Bed_light diff --git a/homeassistant/__init__.py b/homeassistant/__init__.py index 82ac3f9c2927..75204036e17a 100644 --- a/homeassistant/__init__.py +++ b/homeassistant/__init__.py @@ -23,35 +23,176 @@ SERVICE_HOMEASSISTANT_STOP = "stop" EVENT_HOMEASSISTANT_START = "homeassistant_start" EVENT_STATE_CHANGED = "state_changed" EVENT_TIME_CHANGED = "time_changed" +EVENT_CALL_SERVICE = "call_service" +ATTR_NOW = "now" +ATTR_DOMAIN = "domain" +ATTR_SERVICE = "service" + +# How often time_changed event should fire TIMER_INTERVAL = 10 # seconds -# We want to be able to fire every time a minute starts (seconds=0). -# We want this so other modules can use that to make sure they fire -# every minute. -assert 60 % TIMER_INTERVAL == 0, "60 % TIMER_INTERVAL should be 0!" - -BUS_NUM_THREAD = 4 -BUS_REPORT_BUSY_TIMEOUT = dt.timedelta(minutes=1) +# Number of worker threads +POOL_NUM_THREAD = 4 -def start_home_assistant(bus): - """ Start home assistant. """ - request_shutdown = threading.Event() +class HomeAssistant(object): + """ Core class to route all communication to right components. """ - bus.register_service(DOMAIN, SERVICE_HOMEASSISTANT_STOP, - lambda service: request_shutdown.set()) + def __init__(self): + self._pool = pool = _create_worker_pool() - Timer(bus) + self.bus = EventBus(pool) + self.states = StateMachine(self.bus) + self.services = ServiceRegistry(self.bus, pool) - bus.fire_event(EVENT_HOMEASSISTANT_START) + def start(self, non_blocking=False): + """ Start home assistant. + Set non_blocking to True if you don't want this method to block + as long as Home Assistant is running. """ - while not request_shutdown.isSet(): - try: - time.sleep(1) + Timer(self) - except KeyboardInterrupt: - break + self.bus.fire(EVENT_HOMEASSISTANT_START) + + if non_blocking: + return + + request_shutdown = threading.Event() + + self.services.register(DOMAIN, SERVICE_HOMEASSISTANT_STOP, + lambda service: request_shutdown.set()) + + while not request_shutdown.isSet(): + try: + time.sleep(1) + + except KeyboardInterrupt: + break + + def call_service(self, domain, service, service_data=None): + """ Fires event to call specified service. """ + event_data = service_data or {} + event_data[ATTR_DOMAIN] = domain + event_data[ATTR_SERVICE] = service + + self.bus.fire(EVENT_CALL_SERVICE, event_data) + + def get_entity_ids(self, domain_filter=None): + """ Returns known entity ids. """ + if domain_filter: + return [entity_id for entity_id in self.states.entity_ids + if entity_id.startswith(domain_filter)] + else: + return self.states.entity_ids + + def track_state_change(self, entity_id, action, + from_state=None, to_state=None): + """ Track specific state changes. """ + from_state = _process_match_param(from_state) + to_state = _process_match_param(to_state) + + @ft.wraps(action) + def state_listener(event): + """ The listener that listens for specific state changes. """ + if entity_id == event.data['entity_id'] and \ + _matcher(event.data['old_state'].state, from_state) and \ + _matcher(event.data['new_state'].state, to_state): + + action(event.data['entity_id'], + event.data['old_state'], + event.data['new_state']) + + self.bus.listen(EVENT_STATE_CHANGED, state_listener) + + def track_point_in_time(self, action, point_in_time): + """ Adds a listener that fires once after a spefic point in time. """ + + @ft.wraps(action) + def point_in_time_listener(event): + """ Listens for matching time_changed events. """ + now = event.data[ATTR_NOW] + + if now > point_in_time and \ + not hasattr(point_in_time_listener, 'run'): + + # Set variable so that we will never run twice. + # Because the event bus might have to wait till a thread comes + # available to execute this listener it might occur that the + # listener gets lined up twice to be executed. This will make + # sure the second time it does nothing. + point_in_time_listener.run = True + + self.bus.remove_listener(EVENT_TIME_CHANGED, + point_in_time_listener) + + action(now) + + self.bus.listen(EVENT_TIME_CHANGED, point_in_time_listener) + + # pylint: disable=too-many-arguments + def track_time_change(self, action, + year=None, month=None, day=None, + hour=None, minute=None, second=None): + """ Adds a listener that will fire if time matches a pattern. """ + + # We do not have to wrap the function with time pattern matching logic + # if no pattern given + if any((val is not None for val in + (year, month, day, hour, minute, second))): + + pmp = _process_match_param + year, month, day = pmp(year), pmp(month), pmp(day) + hour, minute, second = pmp(hour), pmp(minute), pmp(second) + + @ft.wraps(action) + def time_listener(event): + """ Listens for matching time_changed events. """ + now = event.data[ATTR_NOW] + + mat = _matcher + + if mat(now.year, year) and \ + mat(now.month, month) and \ + mat(now.day, day) and \ + mat(now.hour, hour) and \ + mat(now.minute, minute) and \ + mat(now.second, second): + + action(now) + + else: + @ft.wraps(action) + def time_listener(event): + """ Fires every time event that comes in. """ + action(event.data[ATTR_NOW]) + + self.bus.listen(EVENT_TIME_CHANGED, time_listener) + + def listen_once_event(self, event_type, listener): + """ Listen once for event of a specific type. + + To listen to all events specify the constant ``MATCH_ALL`` + as event_type. + + Note: at the moment it is impossible to remove a one time listener. + """ + @ft.wraps(listener) + def onetime_listener(event): + """ Removes listener from eventbus and then fires listener. """ + if not hasattr(onetime_listener, 'run'): + # Set variable so that we will never run twice. + # Because the event bus might have to wait till a thread comes + # available to execute this listener it might occur that the + # listener gets lined up twice to be executed. + # This will make sure the second time it does nothing. + onetime_listener.run = True + + self.bus.remove_listener(event_type, onetime_listener) + + listener(event) + + self.bus.listen(event_type, onetime_listener) def _process_match_param(parameter): @@ -72,118 +213,31 @@ def _matcher(subject, pattern): return MATCH_ALL == pattern or subject in pattern -def track_state_change(bus, entity_id, action, from_state=None, to_state=None): - """ Helper method to track specific state changes. """ - from_state = _process_match_param(from_state) - to_state = _process_match_param(to_state) +class JobPriority(util.OrderedEnum): + """ Provides priorities for bus events. """ + # pylint: disable=no-init - @ft.wraps(action) - def state_listener(event): - """ State change listener that listens for specific state changes. """ - if entity_id == event.data['entity_id'] and \ - _matcher(event.data['old_state'].state, from_state) and \ - _matcher(event.data['new_state'].state, to_state): + EVENT_SERVICE = 1 + EVENT_STATE = 2 + EVENT_TIME = 3 + EVENT_DEFAULT = 4 - action(event.data['entity_id'], - event.data['old_state'], - event.data['new_state']) - - bus.listen_event(EVENT_STATE_CHANGED, state_listener) + @staticmethod + def from_event_type(event_type): + """ Returns a priority based on event type. """ + if event_type == EVENT_TIME_CHANGED: + return JobPriority.EVENT_TIME + elif event_type == EVENT_STATE_CHANGED: + return JobPriority.EVENT_STATE + elif event_type == EVENT_CALL_SERVICE: + return JobPriority.EVENT_SERVICE + else: + return JobPriority.EVENT_DEFAULT -def track_point_in_time(bus, action, point_in_time): - """ Adds a listener that will fire once after a spefic point in time. """ - - @ft.wraps(action) - def point_in_time_listener(event): - """ Listens for matching time_changed events. """ - now = event.data['now'] - - if now > point_in_time and not hasattr(point_in_time_listener, 'run'): - - # Set variable so that we will never run twice. - # Because the event bus might have to wait till a thread comes - # available to execute this listener it might occur that the - # listener gets lined up twice to be executed. This will make sure - # the second time it does nothing. - point_in_time_listener.run = True - - bus.remove_event_listener(EVENT_TIME_CHANGED, - point_in_time_listener) - - action(now) - - bus.listen_event(EVENT_TIME_CHANGED, point_in_time_listener) - - -# pylint: disable=too-many-arguments -def track_time_change(bus, action, - year=None, month=None, day=None, - hour=None, minute=None, second=None): - """ Adds a listener that will fire if time matches a pattern. """ - - # We do not have to wrap the function with time pattern matching logic if - # no pattern given - if any((val is not None for val in - (year, month, day, hour, minute, second))): - - pmp = _process_match_param - year, month, day = pmp(year), pmp(month), pmp(day) - hour, minute, second = pmp(hour), pmp(minute), pmp(second) - - @ft.wraps(action) - def time_listener(event): - """ Listens for matching time_changed events. """ - now = event.data['now'] - - mat = _matcher - - if mat(now.year, year) and \ - mat(now.month, month) and \ - mat(now.day, day) and \ - mat(now.hour, hour) and \ - mat(now.minute, minute) and \ - mat(now.second, second): - - action(now) - - else: - @ft.wraps(action) - def time_listener(event): - """ Fires every time event that comes in. """ - action(event.data['now']) - - bus.listen_event(EVENT_TIME_CHANGED, time_listener) - - -def listen_once_event(bus, event_type, listener): - """ Listen once for event of a specific type. - - To listen to all events specify the constant ``MATCH_ALL`` - as event_type. - - Note: at the moment it is impossible to remove a one time listener. - """ - @ft.wraps(listener) - def onetime_listener(event): - """ Removes listener from eventbus and then fires listener. """ - if not hasattr(onetime_listener, 'run'): - # Set variable so that we will never run twice. - # Because the event bus might have to wait till a thread comes - # available to execute this listener it might occur that the - # listener gets lined up twice to be executed. - # This will make sure the second time it does nothing. - onetime_listener.run = True - - bus.remove_event_listener(event_type, onetime_listener) - - listener(event) - - bus.listen_event(event_type, onetime_listener) - - -def create_bus_job_handler(logger): - """ Creates a job handler that logs errors to supplied `logger`. """ +def _create_worker_pool(thread_count=POOL_NUM_THREAD): + """ Creates a worker pool to be used. """ + logger = logging.getLogger(__name__) def job_handler(job): """ Called whenever a job is available to do. """ @@ -195,46 +249,19 @@ def create_bus_job_handler(logger): # We do not want to crash our ThreadPool logger.exception("BusHandler:Exception doing job") - return job_handler + def busy_callback(current_jobs, pending_jobs_count): + """ Callback to be called when the pool queue gets too big. """ + log_error = logger.error + log_error( + "WorkerPool:All {} threads are busy and {} jobs pending".format( + thread_count, pending_jobs_count)) -class BusPriority(util.OrderedEnum): - """ Provides priorities for bus events. """ - # pylint: disable=no-init + for start, job in current_jobs: + log_error("WorkerPool:Current job from {}: {}".format( + util.datetime_to_str(start), job)) - SERVICE_DEFAULT = 1 - EVENT_STATE = 2 - EVENT_TIME = 3 - EVENT_DEFAULT = 4 - - @staticmethod - def from_event_type(event_type): - """ Returns a priority based on event type. """ - if event_type == EVENT_TIME_CHANGED: - return BusPriority.EVENT_TIME - elif event_type == EVENT_STATE_CHANGED: - return BusPriority.EVENT_STATE - else: - return BusPriority.EVENT_DEFAULT - - -# pylint: disable=too-few-public-methods -class ServiceCall(object): - """ Represents a call to a service. """ - - __slots__ = ['domain', 'service', 'data'] - - def __init__(self, domain, service, data=None): - self.domain = domain - self.service = service - self.data = data or {} - - def __repr__(self): - if self.data: - return "".format( - self.domain, self.service, util.repr_helper(self.data)) - else: - return "".format(self.domain, self.service) + return util.ThreadPool(thread_count, job_handler, busy_callback) # pylint: disable=too-few-public-methods @@ -255,137 +282,73 @@ class Event(object): return "".format(self.event_type) -class Bus(object): +class EventBus(object): """ Class that allows different components to communicate via services and events. """ - # pylint: disable=too-many-instance-attributes - def __init__(self, thread_count=None): - self.thread_count = thread_count or BUS_NUM_THREAD - self._event_listeners = {} - self._services = {} - self.logger = logging.getLogger(__name__) - self.event_lock = threading.Lock() - self.service_lock = threading.Lock() - self.last_busy_notice = dt.datetime.now() - - self.pool = util.ThreadPool(self.thread_count, - create_bus_job_handler(self.logger)) + def __init__(self, pool=None): + self._listeners = {} + self._logger = logging.getLogger(__name__) + self._lock = threading.Lock() + self._pool = pool or _create_worker_pool() @property - def services(self): - """ Dict with per domain a list of available services. """ - with self.service_lock: - return {domain: list(self._services[domain].keys()) - for domain in self._services} - - @property - def event_listeners(self): + def listeners(self): """ Dict with events that is being listened for and the number of listeners. """ - with self.event_lock: - return {key: len(self._event_listeners[key]) - for key in self._event_listeners} + with self._lock: + return {key: len(self._listeners[key]) + for key in self._listeners} - def has_service(self, domain, service): - """ Returns True if specified service exists. """ - return service in self._services.get(domain, []) - - def call_service(self, domain, service, service_data=None): - """ Calls a service. """ - service_call = ServiceCall(domain, service, service_data) - - with self.service_lock: - try: - self.pool.add_job(BusPriority.SERVICE_DEFAULT, - (self._services[domain][service], - service_call)) - - self._check_busy() - - except KeyError: # if key domain or service does not exist - raise ServiceDoesNotExistError( - "Service does not exist: {}/{}".format(domain, service)) - - def register_service(self, domain, service, service_func): - """ Register a service. """ - with self.service_lock: - if domain in self._services: - self._services[domain][service] = service_func - else: - self._services[domain] = {service: service_func} - - def fire_event(self, event_type, event_data=None): + def fire(self, event_type, event_data=None): """ Fire an event. """ - with self.event_lock: + with self._lock: # Copy the list of the current listeners because some listeners # remove themselves as a listener while being executed which # causes the iterator to be confused. - get = self._event_listeners.get + get = self._listeners.get listeners = get(MATCH_ALL, []) + get(event_type, []) event = Event(event_type, event_data) - self.logger.info("Bus:Handling {}".format(event)) + self._logger.info("Bus:Handling {}".format(event)) if not listeners: return for func in listeners: - self.pool.add_job(BusPriority.from_event_type(event_type), + self._pool.add_job(JobPriority.from_event_type(event_type), (func, event)) - self._check_busy() - - def listen_event(self, event_type, listener): + def listen(self, event_type, listener): """ Listen for all events or events of a specific type. To listen to all events specify the constant ``MATCH_ALL`` as event_type. """ - with self.event_lock: - if event_type in self._event_listeners: - self._event_listeners[event_type].append(listener) + with self._lock: + if event_type in self._listeners: + self._listeners[event_type].append(listener) else: - self._event_listeners[event_type] = [listener] + self._listeners[event_type] = [listener] - def remove_event_listener(self, event_type, listener): + def remove_listener(self, event_type, listener): """ Removes a listener of a specific event_type. """ - with self.event_lock: + with self._lock: try: - self._event_listeners[event_type].remove(listener) + self._listeners[event_type].remove(listener) # delete event_type list if empty - if not self._event_listeners[event_type]: - self._event_listeners.pop(event_type) + if not self._listeners[event_type]: + self._listeners.pop(event_type) except (KeyError, AttributeError): # KeyError is key event_type listener did not exist # AttributeError if listener did not exist within event_type pass - def _check_busy(self): - """ Complain if we have more than twice as many jobs queued as threads - and if we didn't complain about it recently. """ - if self.pool.work_queue.qsize() / self.thread_count >= 2 and \ - dt.datetime.now()-self.last_busy_notice > BUS_REPORT_BUSY_TIMEOUT: - - self.last_busy_notice = dt.datetime.now() - - log_error = self.logger.error - - log_error( - "Bus:All {} threads are busy and {} jobs pending".format( - self.thread_count, self.pool.work_queue.qsize())) - - jobs = self.pool.current_jobs - - for start, job in jobs: - log_error("Bus:Current job from {}: {}".format( - util.datetime_to_str(start), job)) - class State(object): """ Object to represent a state within the state machine. """ @@ -422,11 +385,6 @@ class State(object): 'attributes': self.attributes, 'last_changed': util.datetime_to_str(self.last_changed)} - def __eq__(self, other): - return (self.__class__ == other.__class__ and - self.state == other.state and - self.attributes == other.attributes) - @classmethod def from_dict(cls, json_dict): """ Static method to create a state from a dict. @@ -443,6 +401,11 @@ class State(object): return cls(json_dict['entity_id'], json_dict['state'], json_dict.get('attributes'), last_changed) + def __eq__(self, other): + return (self.__class__ == other.__class__ and + self.state == other.state and + self.attributes == other.attributes) + def __repr__(self): if self.attributes: return "".format( @@ -457,73 +420,141 @@ class StateMachine(object): """ Helper class that tracks the state of different entities. """ def __init__(self, bus): - self.states = {} - self.bus = bus - self.lock = threading.Lock() + self._states = {} + self._bus = bus + self._lock = threading.Lock() @property def entity_ids(self): """ List of entity ids that are being tracked. """ - return self.states.keys() + return list(self._states.keys()) - def get_state(self, entity_id): + def get(self, entity_id): """ Returns the state of the specified entity. """ - state = self.states.get(entity_id) + state = self._states.get(entity_id) # Make a copy so people won't mutate the state return state.copy() if state else None def is_state(self, entity_id, state): """ Returns True if entity exists and is specified state. """ - return (entity_id in self.states and - self.states[entity_id].state == state) + return (entity_id in self._states and + self._states[entity_id].state == state) - def remove_entity(self, entity_id): + def remove(self, entity_id): """ Removes a entity from the state machine. Returns boolean to indicate if a entity was removed. """ - with self.lock: - return self.states.pop(entity_id, None) is not None + with self._lock: + return self._states.pop(entity_id, None) is not None - def set_state(self, entity_id, new_state, attributes=None): + def set(self, entity_id, new_state, attributes=None): """ Set the state of an entity, add entity if it does not exist. Attributes is an optional dict to specify attributes of this state. """ attributes = attributes or {} - with self.lock: - if entity_id in self.states: - old_state = self.states[entity_id] + with self._lock: + if entity_id in self._states: + old_state = self._states[entity_id] if old_state.state != new_state or \ old_state.attributes != attributes: - state = self.states[entity_id] = \ + state = self._states[entity_id] = \ State(entity_id, new_state, attributes) - self.bus.fire_event(EVENT_STATE_CHANGED, - {'entity_id': entity_id, - 'old_state': old_state, - 'new_state': state}) + self._bus.fire(EVENT_STATE_CHANGED, + {'entity_id': entity_id, + 'old_state': old_state, + 'new_state': state}) else: # If state did not exist yet - self.states[entity_id] = State(entity_id, new_state, - attributes) + self._states[entity_id] = State(entity_id, new_state, + attributes) + + +# pylint: disable=too-few-public-methods +class ServiceCall(object): + """ Represents a call to a service. """ + + __slots__ = ['domain', 'service', 'data'] + + def __init__(self, domain, service, data=None): + self.domain = domain + self.service = service + self.data = data or {} + + def __repr__(self): + if self.data: + return "".format( + self.domain, self.service, util.repr_helper(self.data)) + else: + return "".format(self.domain, self.service) + + +class ServiceRegistry(object): + """ Offers services over the eventbus. """ + + def __init__(self, bus, pool=None): + self._services = {} + self._lock = threading.Lock() + self._pool = pool or _create_worker_pool() + bus.listen(EVENT_CALL_SERVICE, self._event_to_service_call) + + @property + def services(self): + """ Dict with per domain a list of available services. """ + with self._lock: + return {domain: list(self._services[domain].keys()) + for domain in self._services} + + def has_service(self, domain, service): + """ Returns True if specified service exists. """ + return service in self._services.get(domain, []) + + def register(self, domain, service, service_func): + """ Register a service. """ + with self._lock: + if domain in self._services: + self._services[domain][service] = service_func + else: + self._services[domain] = {service: service_func} + + def _event_to_service_call(self, event): + """ Calls a service from an event. """ + service_data = dict(event.data) + domain = service_data.pop(ATTR_DOMAIN, None) + service = service_data.pop(ATTR_SERVICE, None) + + with self._lock: + if domain in self._services and service in self._services[domain]: + service_call = ServiceCall(domain, service, service_data) + + self._pool.add_job(JobPriority.EVENT_SERVICE, + (self._services[domain][service], + service_call)) class Timer(threading.Thread): """ Timer will sent out an event every TIMER_INTERVAL seconds. """ - def __init__(self, bus): + def __init__(self, hass, interval=None): threading.Thread.__init__(self) self.daemon = True - self.bus = bus + self._bus = hass.bus + self.interval = interval or TIMER_INTERVAL - listen_once_event(bus, EVENT_HOMEASSISTANT_START, - lambda event: self.start()) + # We want to be able to fire every time a minute starts (seconds=0). + # We want this so other modules can use that to make sure they fire + # every minute. + assert 60 % self.interval == 0, "60 % TIMER_INTERVAL should be 0!" + + hass.listen_once_event(EVENT_HOMEASSISTANT_START, + lambda event: self.start()) def run(self): """ Start the timer. """ @@ -533,6 +564,7 @@ class Timer(threading.Thread): last_fired_on_second = -1 calc_now = dt.datetime.now + interval = self.interval while True: now = calc_now() @@ -540,7 +572,7 @@ class Timer(threading.Thread): # First check checks if we are not on a second matching the # timer interval. Second check checks if we did not already fire # this interval. - if now.second % TIMER_INTERVAL or \ + if now.second % interval or \ now.second == last_fired_on_second: # Sleep till it is the next time that we have to fire an event. @@ -548,7 +580,7 @@ class Timer(threading.Thread): # If TIMER_INTERVAL is 10 fire at .5, 10.5, 20.5, etc seconds. # This will yield the best results because time.sleep() is not # 100% accurate because of non-realtime OS's - slp_seconds = TIMER_INTERVAL - now.second % TIMER_INTERVAL + \ + slp_seconds = interval - now.second % interval + \ .5 - now.microsecond/1000000.0 time.sleep(slp_seconds) @@ -557,13 +589,8 @@ class Timer(threading.Thread): last_fired_on_second = now.second - self.bus.fire_event(EVENT_TIME_CHANGED, - {'now': now}) + self._bus.fire(EVENT_TIME_CHANGED, {ATTR_NOW: now}) class HomeAssistantError(Exception): """ General Home Assistant exception occured. """ - - -class ServiceDoesNotExistError(HomeAssistantError): - """ A service has been referenced that deos not exist. """ diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index f09b9d97ac2d..5a8a4327f9eb 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -1,31 +1,38 @@ """ Provides methods to bootstrap a home assistant instance. + +Each method will return a tuple (bus, statemachine). + +After bootstrapping you can add your own components or +start by calling homeassistant.start_home_assistant(bus) """ import importlib import configparser import logging -import homeassistant as ha +import homeassistant import homeassistant.components as components # pylint: disable=too-many-branches,too-many-locals,too-many-statements -def from_config_file(config_path): +def from_config_file(config_path, enable_logging=True): """ Starts home assistant with all possible functionality - based on a config file. """ + based on a config file. + Will return a tuple (bus, statemachine). """ - # Setup the logging for home assistant. - logging.basicConfig(level=logging.INFO) + if enable_logging: + # Setup the logging for home assistant. + logging.basicConfig(level=logging.INFO) - # Log errors to a file - err_handler = logging.FileHandler("home-assistant.log", - mode='w', delay=True) - err_handler.setLevel(logging.ERROR) - err_handler.setFormatter( - logging.Formatter('%(asctime)s %(name)s: %(message)s', - datefmt='%H:%M %d-%m-%y')) - logging.getLogger('').addHandler(err_handler) + # Log errors to a file + err_handler = logging.FileHandler("home-assistant.log", + mode='w', delay=True) + err_handler.setLevel(logging.ERROR) + err_handler.setFormatter( + logging.Formatter('%(asctime)s %(name)s: %(message)s', + datefmt='%H:%M %d-%m-%y')) + logging.getLogger('').addHandler(err_handler) # Start the actual bootstrapping logger = logging.getLogger(__name__) @@ -37,8 +44,7 @@ def from_config_file(config_path): config.read(config_path) # Init core - bus = ha.Bus() - statemachine = ha.StateMachine(bus) + hass = homeassistant.HomeAssistant() has_opt = config.has_option get_opt = config.get @@ -107,7 +113,7 @@ def from_config_file(config_path): # Device Tracker if dev_scan: - device_tracker.DeviceTracker(bus, statemachine, dev_scan) + device_tracker.DeviceTracker(hass, dev_scan) add_status("Device Tracker", True) @@ -117,11 +123,10 @@ def from_config_file(config_path): sun = load_module('sun') - add_status("Weather - Ephem", - sun.setup( - bus, statemachine, - get_opt("common", "latitude"), - get_opt("common", "longitude"))) + add_status("Sun", + sun.setup(hass, + get_opt("common", "latitude"), + get_opt("common", "longitude"))) else: sun = None @@ -131,17 +136,15 @@ def from_config_file(config_path): hosts = get_hosts("chromecast") - chromecast_started = chromecast.setup(bus, statemachine, hosts) - - add_status("Chromecast", chromecast_started) - else: - chromecast_started = False + add_status("Chromecast", chromecast.setup(hass, hosts)) # WeMo if has_section("wemo"): wemo = load_module('wemo') - add_status("WeMo", wemo.setup(bus, statemachine)) + hosts = get_hosts("wemo") + + add_status("WeMo", wemo.setup(hass, hosts)) # Light control if has_section("light.hue"): @@ -152,7 +155,7 @@ def from_config_file(config_path): add_status("Light - Hue", light_control.success_init) if light_control.success_init: - light.setup(bus, statemachine, light_control) + light.setup(hass, light_control) else: light_control = None @@ -163,23 +166,22 @@ def from_config_file(config_path): downloader = load_module('downloader') add_status("Downloader", downloader.setup( - bus, get_opt("downloader", "download_dir"))) + hass, get_opt("downloader", "download_dir"))) - add_status("Core components", components.setup(bus, statemachine)) + add_status("Core components", components.setup(hass)) if has_section('browser'): - add_status("Browser", load_module('browser').setup(bus)) + add_status("Browser", load_module('browser').setup(hass)) if has_section('keyboard'): - add_status("Keyboard", load_module('keyboard').setup(bus)) + add_status("Keyboard", load_module('keyboard').setup(hass)) # Init HTTP interface if has_opt("httpinterface", "api_password"): httpinterface = load_module('httpinterface') httpinterface.HTTPInterface( - bus, statemachine, - get_opt("httpinterface", "api_password")) + hass, get_opt("httpinterface", "api_password")) add_status("HTTPInterface", True) @@ -189,8 +191,7 @@ def from_config_file(config_path): for name, entity_ids in config.items("group"): add_status("Group - {}".format(name), - group.setup(bus, statemachine, name, - entity_ids.split(","))) + group.setup(hass, name, entity_ids.split(","))) # Light trigger if light_control and sun: @@ -201,7 +202,7 @@ def from_config_file(config_path): "light_profile") add_status("Device Sun Light Trigger", - device_sun_light_trigger.setup(bus, statemachine, + device_sun_light_trigger.setup(hass, light_group, light_profile)) for component, success_init in statusses: @@ -209,4 +210,4 @@ def from_config_file(config_path): logger.info("{}: {}".format(component, status)) - ha.start_home_assistant(bus) + return hass diff --git a/homeassistant/components/__init__.py b/homeassistant/components/__init__.py index 7fa4b5d12993..bb1f78a2d75c 100644 --- a/homeassistant/components/__init__.py +++ b/homeassistant/components/__init__.py @@ -28,8 +28,8 @@ ATTR_FRIENDLY_NAME = "friendly_name" STATE_ON = 'on' STATE_OFF = 'off' -STATE_NOT_HOME = 'not_home' STATE_HOME = 'home' +STATE_NOT_HOME = 'not_home' SERVICE_TURN_ON = 'turn_on' SERVICE_TURN_OFF = 'turn_off' @@ -38,35 +38,25 @@ SERVICE_VOLUME_UP = "volume_up" SERVICE_VOLUME_DOWN = "volume_down" SERVICE_VOLUME_MUTE = "volume_mute" SERVICE_MEDIA_PLAY_PAUSE = "media_play_pause" +SERVICE_MEDIA_PLAY = "media_play" +SERVICE_MEDIA_PAUSE = "media_pause" SERVICE_MEDIA_NEXT_TRACK = "media_next_track" SERVICE_MEDIA_PREV_TRACK = "media_prev_track" -_LOADED_COMP = {} - def _get_component(component): - """ Returns requested component. Imports it if necessary. """ + """ Returns requested component. """ - comps = _LOADED_COMP - - # See if we have the module locally cached, else import it try: - return comps[component] + return importlib.import_module( + 'homeassistant.components.{}'.format(component)) - except KeyError: - # If comps[component] does not exist, import module - try: - comps[component] = importlib.import_module( - 'homeassistant.components.'+component) - - except ImportError: - # If we got a bogus component the input will fail - comps[component] = None - - return comps[component] + except ImportError: + # If we got a bogus component the input will fail + return None -def is_on(statemachine, entity_id=None): +def is_on(hass, entity_id=None): """ Loads up the module to call the is_on method. If there is no entity id given we will check all. """ if entity_id: @@ -74,7 +64,7 @@ def is_on(statemachine, entity_id=None): entity_ids = group.expand_entity_ids([entity_id]) else: - entity_ids = statemachine.entity_ids + entity_ids = hass.states.entity_ids for entity_id in entity_ids: domain = util.split_entity_id(entity_id)[0] @@ -82,7 +72,7 @@ def is_on(statemachine, entity_id=None): module = _get_component(domain) try: - if module.is_on(statemachine, entity_id): + if module.is_on(hass, entity_id): return True except AttributeError: @@ -92,17 +82,17 @@ def is_on(statemachine, entity_id=None): return False -def turn_on(bus, **service_data): +def turn_on(hass, **service_data): """ Turns specified entity on if possible. """ - bus.call_service(ha.DOMAIN, SERVICE_TURN_ON, service_data) + hass.call_service(ha.DOMAIN, SERVICE_TURN_ON, service_data) -def turn_off(bus, **service_data): +def turn_off(hass, **service_data): """ Turns specified entity off. """ - bus.call_service(ha.DOMAIN, SERVICE_TURN_OFF, service_data) + hass.call_service(ha.DOMAIN, SERVICE_TURN_OFF, service_data) -def extract_entity_ids(statemachine, service): +def extract_entity_ids(hass, service): """ Helper method to extract a list of entity ids from a service call. Will convert group entity ids to the entity ids it represents. @@ -121,19 +111,19 @@ def extract_entity_ids(statemachine, service): entity_ids.extend( ent_id for ent_id - in group.expand_entity_ids(statemachine, ent_ids) + in group.expand_entity_ids(hass, ent_ids) if ent_id not in entity_ids) return entity_ids -def setup(bus, statemachine): +def setup(hass): """ Setup general services related to homeassistant. """ def handle_turn_service(service): """ Method to handle calls to homeassistant.turn_on/off. """ - entity_ids = extract_entity_ids(statemachine, service) + entity_ids = extract_entity_ids(hass, service) # Generic turn on/off method requires entity id if not entity_ids: @@ -150,13 +140,9 @@ def setup(bus, statemachine): # ent_ids is a generator, convert it to a list. data[ATTR_ENTITY_ID] = list(ent_ids) - try: - bus.call_service(domain, service.service, data) - except ha.ServiceDoesNotExistError: - # turn_on service does not exist - pass + hass.call_service(domain, service.service, data) - bus.register_service(ha.DOMAIN, SERVICE_TURN_OFF, handle_turn_service) - bus.register_service(ha.DOMAIN, SERVICE_TURN_ON, handle_turn_service) + hass.services.register(ha.DOMAIN, SERVICE_TURN_OFF, handle_turn_service) + hass.services.register(ha.DOMAIN, SERVICE_TURN_ON, handle_turn_service) return True diff --git a/homeassistant/components/browser.py b/homeassistant/components/browser.py index c94fde789573..d1d4272a6a29 100644 --- a/homeassistant/components/browser.py +++ b/homeassistant/components/browser.py @@ -10,16 +10,16 @@ DOMAIN = "browser" SERVICE_BROWSE_URL = "browse_url" -def setup(bus): +def setup(hass): """ Listen for browse_url events and open the url in the default webbrowser. """ import webbrowser - bus.register_service(DOMAIN, SERVICE_BROWSE_URL, - lambda service: - webbrowser.open( - service.data.get('url', - 'https://www.google.com'))) + hass.services.register(DOMAIN, SERVICE_BROWSE_URL, + lambda service: + webbrowser.open( + service.data.get('url', + 'https://www.google.com'))) return True diff --git a/homeassistant/components/chromecast.py b/homeassistant/components/chromecast.py index 2150a639f6d6..e60f05961793 100644 --- a/homeassistant/components/chromecast.py +++ b/homeassistant/components/chromecast.py @@ -6,7 +6,6 @@ Provides functionality to interact with Chromecasts. """ import logging -import homeassistant as ha import homeassistant.util as util import homeassistant.components as components @@ -34,61 +33,74 @@ MEDIA_STATE_PLAYING = 'playing' MEDIA_STATE_STOPPED = 'stopped' -def is_on(statemachine, entity_id=None): +def is_on(hass, entity_id=None): """ Returns true if specified ChromeCast entity_id is on. Will check all chromecasts if no entity_id specified. """ - entity_ids = [entity_id] if entity_id \ - else util.filter_entity_ids(statemachine.entity_ids, DOMAIN) + entity_ids = [entity_id] if entity_id else hass.get_entity_ids(DOMAIN) - return any(not statemachine.is_state(entity_id, STATE_NO_APP) + return any(not hass.states.is_state(entity_id, STATE_NO_APP) for entity_id in entity_ids) -def turn_off(bus, entity_id=None): +def turn_off(hass, entity_id=None): """ Will turn off specified Chromecast or all. """ data = {components.ATTR_ENTITY_ID: entity_id} if entity_id else {} - bus.call_service(DOMAIN, components.SERVICE_TURN_OFF, data) + hass.call_service(DOMAIN, components.SERVICE_TURN_OFF, data) -def volume_up(bus, entity_id=None): +def volume_up(hass, entity_id=None): """ Send the chromecast the command for volume up. """ data = {components.ATTR_ENTITY_ID: entity_id} if entity_id else {} - bus.call_service(DOMAIN, components.SERVICE_VOLUME_UP, data) + hass.call_service(DOMAIN, components.SERVICE_VOLUME_UP, data) -def volume_down(bus, entity_id=None): +def volume_down(hass, entity_id=None): """ Send the chromecast the command for volume down. """ data = {components.ATTR_ENTITY_ID: entity_id} if entity_id else {} - bus.call_service(DOMAIN, components.SERVICE_VOLUME_DOWN, data) + hass.call_service(DOMAIN, components.SERVICE_VOLUME_DOWN, data) -def media_play_pause(bus, entity_id=None): +def media_play_pause(hass, entity_id=None): """ Send the chromecast the command for play/pause. """ data = {components.ATTR_ENTITY_ID: entity_id} if entity_id else {} - bus.call_service(DOMAIN, components.SERVICE_MEDIA_PLAY_PAUSE, data) + hass.call_service(DOMAIN, components.SERVICE_MEDIA_PLAY_PAUSE, data) -def media_next_track(bus, entity_id=None): +def media_play(hass, entity_id=None): + """ Send the chromecast the command for play/pause. """ + data = {components.ATTR_ENTITY_ID: entity_id} if entity_id else {} + + hass.call_service(DOMAIN, components.SERVICE_MEDIA_PLAY, data) + + +def media_pause(hass, entity_id=None): + """ Send the chromecast the command for play/pause. """ + data = {components.ATTR_ENTITY_ID: entity_id} if entity_id else {} + + hass.call_service(DOMAIN, components.SERVICE_MEDIA_PAUSE, data) + + +def media_next_track(hass, entity_id=None): """ Send the chromecast the command for next track. """ data = {components.ATTR_ENTITY_ID: entity_id} if entity_id else {} - bus.call_service(DOMAIN, components.SERVICE_MEDIA_NEXT_TRACK, data) + hass.call_service(DOMAIN, components.SERVICE_MEDIA_NEXT_TRACK, data) -def media_prev_track(bus, entity_id=None): +def media_prev_track(hass, entity_id=None): """ Send the chromecast the command for prev track. """ data = {components.ATTR_ENTITY_ID: entity_id} if entity_id else {} - bus.call_service(DOMAIN, components.SERVICE_MEDIA_PREV_TRACK, data) + hass.call_service(DOMAIN, components.SERVICE_MEDIA_PREV_TRACK, data) # pylint: disable=too-many-locals, too-many-branches -def setup(bus, statemachine, hosts=None): +def setup(hass, hosts=None): """ Listen for chromecast events. """ logger = logging.getLogger(__name__) @@ -170,7 +182,7 @@ def setup(bus, statemachine, hosts=None): else: state = STATE_NO_APP - statemachine.set_state(entity_id, state, state_attr) + hass.states.set(entity_id, state, state_attr) def update_chromecast_states(time): # pylint: disable=unused-argument """ Updates all chromecast states. """ @@ -181,7 +193,7 @@ def setup(bus, statemachine, hosts=None): def _service_to_entities(service): """ Helper method to get entities from service. """ - entity_ids = components.extract_entity_ids(statemachine, service) + entity_ids = components.extract_entity_ids(hass, service) if entity_ids: for entity_id in entity_ids: @@ -225,6 +237,22 @@ def setup(bus, statemachine, hosts=None): if ramp: ramp.playpause() + def media_play_service(service): + """ Service to send the chromecast the command for play/pause. """ + for _, cast in _service_to_entities(service): + ramp = cast.get_protocol(pychromecast.PROTOCOL_RAMP) + + if ramp and ramp.state == pychromecast.RAMP_STATE_STOPPED: + ramp.playpause() + + def media_pause_service(service): + """ Service to send the chromecast the command for play/pause. """ + for _, cast in _service_to_entities(service): + ramp = cast.get_protocol(pychromecast.PROTOCOL_RAMP) + + if ramp and ramp.state == pychromecast.RAMP_STATE_PLAYING: + ramp.playpause() + def media_next_track_service(service): """ Service to send the chromecast the command for next track. """ for entity_id, cast in _service_to_entities(service): @@ -241,35 +269,42 @@ def setup(bus, statemachine, hosts=None): pychromecast.play_youtube_video(video_id, cast.host) update_chromecast_state(entity_id, cast) - ha.track_time_change(bus, update_chromecast_states) + hass.track_time_change(update_chromecast_states) - bus.register_service(DOMAIN, components.SERVICE_TURN_OFF, - turn_off_service) + hass.services.register(DOMAIN, components.SERVICE_TURN_OFF, + turn_off_service) - bus.register_service(DOMAIN, components.SERVICE_VOLUME_UP, - volume_up_service) + hass.services.register(DOMAIN, components.SERVICE_VOLUME_UP, + volume_up_service) - bus.register_service(DOMAIN, components.SERVICE_VOLUME_DOWN, - volume_down_service) + hass.services.register(DOMAIN, components.SERVICE_VOLUME_DOWN, + volume_down_service) - bus.register_service(DOMAIN, components.SERVICE_MEDIA_PLAY_PAUSE, - media_play_pause_service) + hass.services.register(DOMAIN, components.SERVICE_MEDIA_PLAY_PAUSE, + media_play_pause_service) - bus.register_service(DOMAIN, components.SERVICE_MEDIA_NEXT_TRACK, - media_next_track_service) + hass.services.register(DOMAIN, components.SERVICE_MEDIA_PLAY, + media_play_service) - bus.register_service(DOMAIN, "start_fireplace", - lambda service: - play_youtube_video_service(service, "eyU3bRy2x44")) + hass.services.register(DOMAIN, components.SERVICE_MEDIA_PAUSE, + media_pause_service) - bus.register_service(DOMAIN, "start_epic_sax", - lambda service: - play_youtube_video_service(service, "kxopViU98Xo")) + hass.services.register(DOMAIN, components.SERVICE_MEDIA_NEXT_TRACK, + media_next_track_service) - bus.register_service(DOMAIN, SERVICE_YOUTUBE_VIDEO, - lambda service: - play_youtube_video_service(service, - service.data.get('video'))) + hass.services.register(DOMAIN, "start_fireplace", + lambda service: + play_youtube_video_service(service, "eyU3bRy2x44")) + + hass.services.register(DOMAIN, "start_epic_sax", + lambda service: + play_youtube_video_service(service, "kxopViU98Xo")) + + hass.services.register(DOMAIN, SERVICE_YOUTUBE_VIDEO, + lambda service: + play_youtube_video_service(service, + service.data.get( + 'video'))) update_chromecast_states(None) diff --git a/homeassistant/components/device_sun_light_trigger.py b/homeassistant/components/device_sun_light_trigger.py index a396e72f8a80..c7e3cf0172c0 100644 --- a/homeassistant/components/device_sun_light_trigger.py +++ b/homeassistant/components/device_sun_light_trigger.py @@ -8,8 +8,6 @@ the state of the sun and devices. import logging from datetime import datetime, timedelta -import homeassistant as ha -import homeassistant.util as util import homeassistant.components as components from . import light, sun, device_tracker, group @@ -21,7 +19,7 @@ LIGHT_PROFILE = 'relax' # pylint: disable=too-many-branches -def setup(bus, statemachine, light_group=None, light_profile=None): +def setup(hass, light_group=None, light_profile=None): """ Triggers to turn lights on or off based on device precense. """ light_group = light_group or light.GROUP_NAME_ALL_LIGHTS @@ -29,8 +27,7 @@ def setup(bus, statemachine, light_group=None, light_profile=None): logger = logging.getLogger(__name__) - device_entity_ids = util.filter_entity_ids(statemachine.entity_ids, - device_tracker.DOMAIN) + device_entity_ids = hass.get_entity_ids(device_tracker.DOMAIN) if not device_entity_ids: logger.error("No devices found to track") @@ -38,8 +35,7 @@ def setup(bus, statemachine, light_group=None, light_profile=None): return False # Get the light IDs from the specified group - light_ids = util.filter_entity_ids( - group.get_entity_ids(statemachine, light_group), light.DOMAIN) + light_ids = group.get_entity_ids(hass, light_group, light.DOMAIN) if not light_ids: logger.error("No lights found to turn on ") @@ -49,7 +45,7 @@ def setup(bus, statemachine, light_group=None, light_profile=None): def calc_time_for_light_when_sunset(): """ Calculates the time when to start fading lights in when sun sets. Returns None if no next_setting data available. """ - next_setting = sun.next_setting(statemachine) + next_setting = sun.next_setting(hass) if next_setting: return next_setting - LIGHT_TRANSITION_TIME * len(light_ids) @@ -65,10 +61,10 @@ def setup(bus, statemachine, light_group=None, light_profile=None): def turn_light_on_before_sunset(light_id): """ Helper function to turn on lights slowly if there are devices home and the light is not on yet. """ - if (device_tracker.is_on(statemachine) and - not light.is_on(statemachine, light_id)): + if (device_tracker.is_on(hass) and + not light.is_on(hass, light_id)): - light.turn_on(bus, light_id, + light.turn_on(hass, light_id, transition=LIGHT_TRANSITION_TIME.seconds, profile=light_profile) @@ -82,26 +78,25 @@ def setup(bus, statemachine, light_group=None, light_profile=None): if start_point: for index, light_id in enumerate(light_ids): - ha.track_point_in_time(bus, turn_on(light_id), - (start_point + - index * LIGHT_TRANSITION_TIME)) + hass.track_point_in_time(turn_on(light_id), + (start_point + + index * LIGHT_TRANSITION_TIME)) # Track every time sun rises so we can schedule a time-based # pre-sun set event - ha.track_state_change(bus, sun.ENTITY_ID, - schedule_light_on_sun_rise, - sun.STATE_BELOW_HORIZON, sun.STATE_ABOVE_HORIZON) + hass.track_state_change(sun.ENTITY_ID, schedule_light_on_sun_rise, + sun.STATE_BELOW_HORIZON, sun.STATE_ABOVE_HORIZON) # If the sun is already above horizon # schedule the time-based pre-sun set event - if sun.is_on(statemachine): + if sun.is_on(hass): schedule_light_on_sun_rise(None, None, None) def check_light_on_dev_state_change(entity, old_state, new_state): """ Function to handle tracked device state changes. """ - lights_are_on = group.is_on(statemachine, light_group) + lights_are_on = group.is_on(hass, light_group) - light_needed = not (lights_are_on or sun.is_on(statemachine)) + light_needed = not (lights_are_on or sun.is_on(hass)) # Specific device came home ? if (entity != device_tracker.ENTITY_ID_ALL_DEVICES and @@ -118,7 +113,7 @@ def setup(bus, statemachine, light_group=None, light_profile=None): "Home coming event for {}. Turning lights on". format(entity)) - light.turn_on(bus, light_ids, + light.turn_on(hass, light_ids, profile=light_profile) # Are we in the time span were we would turn on the lights @@ -126,14 +121,14 @@ def setup(bus, statemachine, light_group=None, light_profile=None): # Check this by seeing if current time is later then the point # in time when we would start putting the lights on. elif (start_point and - start_point < now < sun.next_setting(statemachine)): + start_point < now < sun.next_setting(hass)): # Check for every light if it would be on if someone was home # when the fading in started and turn it on if so for index, light_id in enumerate(light_ids): if now > start_point + index * LIGHT_TRANSITION_TIME: - light.turn_on(bus, light_id) + light.turn_on(hass, light_id) else: # If this light didn't happen to be turned on yet so @@ -147,16 +142,17 @@ def setup(bus, statemachine, light_group=None, light_profile=None): logger.info( "Everyone has left but there are devices on. Turning them off") - light.turn_off(bus) + light.turn_off(hass) # Track home coming of each seperate device for entity in device_entity_ids: - ha.track_state_change(bus, entity, check_light_on_dev_state_change, - components.STATE_NOT_HOME, components.STATE_HOME) + hass.track_state_change(entity, check_light_on_dev_state_change, + components.STATE_NOT_HOME, + components.STATE_HOME) # Track when all devices are gone to shut down lights - ha.track_state_change(bus, device_tracker.ENTITY_ID_ALL_DEVICES, - check_light_on_dev_state_change, - components.STATE_HOME, components.STATE_NOT_HOME) + hass.track_state_change(device_tracker.ENTITY_ID_ALL_DEVICES, + check_light_on_dev_state_change, + components.STATE_HOME, components.STATE_NOT_HOME) return True diff --git a/homeassistant/components/device_tracker.py b/homeassistant/components/device_tracker.py index 3c57b85f2c15..7f00277be399 100644 --- a/homeassistant/components/device_tracker.py +++ b/homeassistant/components/device_tracker.py @@ -14,7 +14,6 @@ from datetime import datetime, timedelta import requests -import homeassistant as ha import homeassistant.util as util import homeassistant.components as components @@ -41,20 +40,20 @@ MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5) KNOWN_DEVICES_FILE = "known_devices.csv" -def is_on(statemachine, entity_id=None): +def is_on(hass, entity_id=None): """ Returns if any or specified device is home. """ entity = entity_id or ENTITY_ID_ALL_DEVICES - return statemachine.is_state(entity, components.STATE_HOME) + return hass.states.is_state(entity, components.STATE_HOME) # pylint: disable=too-many-instance-attributes class DeviceTracker(object): """ Class that tracks which devices are home and which are not. """ - def __init__(self, bus, statemachine, device_scanner, error_scanning=None): - self.statemachine = statemachine - self.bus = bus + def __init__(self, hass, device_scanner, error_scanning=None): + self.states = hass.states + self.device_scanner = device_scanner self.error_scanning = error_scanning or TIME_SPAN_FOR_ERROR_IN_SCANNING @@ -77,16 +76,15 @@ class DeviceTracker(object): """ Triggers update of the device states. """ self.update_devices() - ha.track_time_change(bus, update_device_state) + hass.track_time_change(update_device_state) - bus.register_service(DOMAIN, - SERVICE_DEVICE_TRACKER_RELOAD, - lambda service: self._read_known_devices_file()) + hass.services.register(DOMAIN, + SERVICE_DEVICE_TRACKER_RELOAD, + lambda service: self._read_known_devices_file()) self.update_devices() - group.setup(bus, statemachine, GROUP_NAME_ALL_DEVICES, - list(self.device_entity_ids)) + group.setup(hass, GROUP_NAME_ALL_DEVICES, self.device_entity_ids) @property def device_entity_ids(self): @@ -116,7 +114,7 @@ class DeviceTracker(object): known_dev[device]['last_seen'] = now - self.statemachine.set_state( + self.states.set( known_dev[device]['entity_id'], components.STATE_HOME) # For all devices we did not find, set state to NH @@ -126,8 +124,8 @@ class DeviceTracker(object): for device in temp_tracking_devices: if now - known_dev[device]['last_seen'] > self.error_scanning: - self.statemachine.set_state(known_dev[device]['entity_id'], - components.STATE_NOT_HOME) + self.states.set(known_dev[device]['entity_id'], + components.STATE_NOT_HOME) # If we come along any unknown devices we will write them to the # known devices file but only if we did not encounter an invalid @@ -234,7 +232,7 @@ class DeviceTracker(object): self.logger.info( "DeviceTracker:Removing entity {}".format( entity_id)) - self.statemachine.remove_entity(entity_id) + self.states.remove(entity_id) # File parsed, warnings given if necessary # entities cleaned up, make it available @@ -392,7 +390,8 @@ class NetgearDeviceScanner(object): except ImportError: self.logger.exception( ("Netgear:Failed to import pynetgear. " - "Did you maybe not cloned the git submodules?")) + "Did you maybe not run `git submodule init` " + "and `git submodule update`?")) self.success_init = False diff --git a/homeassistant/components/downloader.py b/homeassistant/components/downloader.py index 34c21d6c3a87..1254d7db61c6 100644 --- a/homeassistant/components/downloader.py +++ b/homeassistant/components/downloader.py @@ -20,7 +20,7 @@ ATTR_SUBDIR = "subdir" # pylint: disable=too-many-branches -def setup(bus, download_path): +def setup(hass, download_path): """ Listens for download events to download files. """ logger = logging.getLogger(__name__) @@ -36,7 +36,7 @@ def setup(bus, download_path): if not os.path.isdir(download_path): logger.error( - ("Download path {} does not exist. File Downloader not active."). + "Download path {} does not exist. File Downloader not active.". format(download_path)) return False @@ -126,7 +126,7 @@ def setup(bus, download_path): threading.Thread(target=do_download).start() - bus.register_service(DOMAIN, SERVICE_DOWNLOAD_FILE, - download_file) + hass.services.register(DOMAIN, SERVICE_DOWNLOAD_FILE, + download_file) return True diff --git a/homeassistant/components/group.py b/homeassistant/components/group.py index b50508037f1e..9b8d989b78bd 100644 --- a/homeassistant/components/group.py +++ b/homeassistant/components/group.py @@ -7,7 +7,6 @@ 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.components import (STATE_ON, STATE_OFF, STATE_HOME, STATE_NOT_HOME, @@ -32,9 +31,9 @@ def _get_group_type(state): return None -def is_on(statemachine, entity_id): +def is_on(hass, entity_id): """ Returns if the group state is in its ON-state. """ - state = statemachine.get_state(entity_id) + state = hass.states.get(entity_id) if state: group_type = _get_group_type(state.state) @@ -45,7 +44,7 @@ def is_on(statemachine, entity_id): return False -def expand_entity_ids(statemachine, entity_ids): +def expand_entity_ids(hass, entity_ids): """ Returns the given list of entity ids and expands group ids into the entity ids it represents if found. """ found_ids = [] @@ -58,7 +57,7 @@ def expand_entity_ids(statemachine, entity_ids): if domain == DOMAIN: found_ids.extend( ent_id for ent_id - in get_entity_ids(statemachine, entity_id) + in get_entity_ids(hass, entity_id) if ent_id not in found_ids) else: @@ -72,11 +71,17 @@ def expand_entity_ids(statemachine, entity_ids): return found_ids -def get_entity_ids(statemachine, entity_id): +def get_entity_ids(hass, entity_id, domain_filter=None): """ Get the entity ids that make up this group. """ try: - return \ - statemachine.get_state(entity_id).attributes[ATTR_ENTITY_ID] + entity_ids = hass.states.get(entity_id).attributes[ATTR_ENTITY_ID] + + if domain_filter: + return [entity_id for entity_id in entity_ids + if entity_id.startswith(domain_filter)] + else: + return entity_ids + except (AttributeError, KeyError): # AttributeError if state did not exist # KeyError if key did not exist in attributes @@ -84,10 +89,13 @@ def get_entity_ids(statemachine, entity_id): # pylint: disable=too-many-branches, too-many-locals -def setup(bus, statemachine, name, entity_ids): +def setup(hass, name, entity_ids): """ Sets up a group state that is the combined state of several states. Supports ON/OFF and DEVICE_HOME/DEVICE_NOT_HOME. """ + # Convert entity_ids to a list incase it is an iterable + entity_ids = list(entity_ids) + logger = logging.getLogger(__name__) # Loop over the given entities to: @@ -98,7 +106,7 @@ def setup(bus, statemachine, name, entity_ids): group_type, group_on, group_off, group_state = None, None, None, None for entity_id in entity_ids: - state = statemachine.get_state(entity_id) + state = hass.states.get(entity_id) # Try to determine group type if we didn't yet if not group_type and state: @@ -143,7 +151,7 @@ def setup(bus, statemachine, name, entity_ids): """ Updates the group state based on a state change by a tracked entity. """ - cur_group_state = statemachine.get_state(group_entity_id).state + cur_group_state = hass.states.get(group_entity_id).state # if cur_group_state = OFF and new_state = ON: set ON # if cur_group_state = ON and new_state = OFF: research @@ -151,18 +159,18 @@ def setup(bus, statemachine, name, entity_ids): if cur_group_state == group_off and new_state.state == group_on: - statemachine.set_state(group_entity_id, group_on, state_attr) + hass.states.set(group_entity_id, group_on, state_attr) elif cur_group_state == group_on and new_state.state == group_off: # Check if any of the other states is still on - if not any([statemachine.is_state(ent_id, group_on) + if not any([hass.states.is_state(ent_id, group_on) for ent_id in entity_ids if entity_id != ent_id]): - statemachine.set_state(group_entity_id, group_off, state_attr) + hass.states.set(group_entity_id, group_off, state_attr) for entity_id in entity_ids: - ha.track_state_change(bus, entity_id, update_group_state) + hass.track_state_change(entity_id, update_group_state) - statemachine.set_state(group_entity_id, group_state, state_attr) + hass.states.set(group_entity_id, group_state, state_attr) return True diff --git a/homeassistant/components/httpinterface/__init__.py b/homeassistant/components/httpinterface/__init__.py index ec406bed100a..be20e5be228c 100644 --- a/homeassistant/components/httpinterface/__init__.py +++ b/homeassistant/components/httpinterface/__init__.py @@ -107,8 +107,7 @@ class HTTPInterface(threading.Thread): """ Provides an HTTP interface for Home Assistant. """ # pylint: disable=too-many-arguments - def __init__(self, bus, statemachine, api_password, - server_port=None, server_host=None): + def __init__(self, hass, api_password, server_port=None, server_host=None): threading.Thread.__init__(self) self.daemon = True @@ -122,12 +121,11 @@ class HTTPInterface(threading.Thread): self.server.flash_message = None self.server.logger = logging.getLogger(__name__) - self.server.bus = bus - self.server.statemachine = statemachine + self.server.hass = hass self.server.api_password = api_password - ha.listen_once_event(bus, ha.EVENT_HOMEASSISTANT_START, - lambda event: self.start()) + hass.listen_once_event(ha.EVENT_HOMEASSISTANT_START, + lambda event: self.start()) def run(self): """ Start the HTTP interface. """ @@ -330,10 +328,10 @@ class RequestHandler(BaseHTTPRequestHandler): "").format(self.server.api_password)) for entity_id in \ - sorted(self.server.statemachine.entity_ids, + sorted(self.server.hass.states.entity_ids, key=lambda key: key.lower()): - state = self.server.statemachine.get_state(entity_id) + state = self.server.hass.states.get(entity_id) attributes = "
".join( ["{}: {}".format(attr, state.attributes[attr]) @@ -372,7 +370,7 @@ class RequestHandler(BaseHTTPRequestHandler): "DomainService")) for domain, services in sorted( - self.server.bus.services.items()): + self.server.hass.services.services.items()): write("{}{}".format( domain, ", ".join(services))) @@ -435,7 +433,7 @@ class RequestHandler(BaseHTTPRequestHandler): "EventListeners")) for event, listener_count in sorted( - self.server.bus.event_listeners.items()): + self.server.hass.bus.listeners.items()): write("{}{}".format( event, listener_count)) @@ -505,13 +503,11 @@ class RequestHandler(BaseHTTPRequestHandler): attributes = None # Write state - self.server.statemachine.set_state(entity_id, - new_state, - attributes) + self.server.hass.states.set(entity_id, new_state, attributes) # Return state if json, else redirect to main page if self.use_json: - state = self.server.statemachine.get_state(entity_id) + state = self.server.hass.states.get(entity_id) self._write_json(state.as_dict(), status_code=HTTP_CREATED, @@ -552,7 +548,7 @@ class RequestHandler(BaseHTTPRequestHandler): # Happens if key 'event_data' does not exist event_data = None - self.server.bus.fire_event(event_type, event_data) + self.server.hass.bus.fire(event_type, event_data) self._message("Event {} fired.".format(event_type)) @@ -587,14 +583,10 @@ class RequestHandler(BaseHTTPRequestHandler): # Happens if key 'service_data' does not exist service_data = None - self.server.bus.call_service(domain, service, service_data) + self.server.hass.call_service(domain, service, service_data) self._message("Service {}/{} called.".format(domain, service)) - except ha.ServiceDoesNotExistError: - # If the service does not exist - self._message('Service does not exist', HTTP_BAD_REQUEST) - except KeyError: # Occurs if domain or service does not exist in data self._message("No domain or service received.", HTTP_BAD_REQUEST) @@ -608,14 +600,14 @@ class RequestHandler(BaseHTTPRequestHandler): def _handle_get_api_states(self, path_match, data): """ Returns the entitie ids which state are being tracked. """ self._write_json( - {'entity_ids': list(self.server.statemachine.entity_ids)}) + {'entity_ids': list(self.server.hass.states.entity_ids)}) # pylint: disable=unused-argument def _handle_get_api_states_entity(self, path_match, data): """ Returns the state of a specific entity. """ entity_id = path_match.group('entity_id') - state = self.server.statemachine.get_state(entity_id) + state = self.server.hass.states.get(entity_id) try: self._write_json(state.as_dict()) @@ -625,11 +617,11 @@ class RequestHandler(BaseHTTPRequestHandler): def _handle_get_api_events(self, path_match, data): """ Handles getting overview of event listeners. """ - self._write_json({'event_listeners': self.server.bus.event_listeners}) + self._write_json({'event_listeners': self.server.hass.bus.listeners}) def _handle_get_api_services(self, path_match, data): """ Handles getting overview of services. """ - self._write_json({'services': self.server.bus.services}) + self._write_json({'services': self.server.hass.services.services}) def _handle_get_static(self, path_match, data): """ Returns a static file. """ diff --git a/homeassistant/components/keyboard.py b/homeassistant/components/keyboard.py index 095f39efe712..2b261a4fc568 100644 --- a/homeassistant/components/keyboard.py +++ b/homeassistant/components/keyboard.py @@ -11,37 +11,37 @@ import homeassistant.components as components DOMAIN = "keyboard" -def volume_up(bus): +def volume_up(hass): """ Press the keyboard button for volume up. """ - bus.call_service(DOMAIN, components.SERVICE_VOLUME_UP) + hass.call_service(DOMAIN, components.SERVICE_VOLUME_UP) -def volume_down(bus): +def volume_down(hass): """ Press the keyboard button for volume down. """ - bus.call_service(DOMAIN, components.SERVICE_VOLUME_DOWN) + hass.call_service(DOMAIN, components.SERVICE_VOLUME_DOWN) -def volume_mute(bus): +def volume_mute(hass): """ Press the keyboard button for muting volume. """ - bus.call_service(DOMAIN, components.SERVICE_VOLUME_MUTE) + hass.call_service(DOMAIN, components.SERVICE_VOLUME_MUTE) -def media_play_pause(bus): +def media_play_pause(hass): """ Press the keyboard button for play/pause. """ - bus.call_service(DOMAIN, components.SERVICE_MEDIA_PLAY_PAUSE) + hass.call_service(DOMAIN, components.SERVICE_MEDIA_PLAY_PAUSE) -def media_next_track(bus): +def media_next_track(hass): """ Press the keyboard button for next track. """ - bus.call_service(DOMAIN, components.SERVICE_MEDIA_NEXT_TRACK) + hass.call_service(DOMAIN, components.SERVICE_MEDIA_NEXT_TRACK) -def media_prev_track(bus): +def media_prev_track(hass): """ Press the keyboard button for prev track. """ - bus.call_service(DOMAIN, components.SERVICE_MEDIA_PREV_TRACK) + hass.call_service(DOMAIN, components.SERVICE_MEDIA_PREV_TRACK) -def setup(bus): +def setup(hass): """ Listen for keyboard events. """ try: import pykeyboard @@ -54,28 +54,28 @@ def setup(bus): keyboard = pykeyboard.PyKeyboard() keyboard.special_key_assignment() - bus.register_service(DOMAIN, components.SERVICE_VOLUME_UP, - lambda service: - keyboard.tap_key(keyboard.volume_up_key)) + hass.services.register(DOMAIN, components.SERVICE_VOLUME_UP, + lambda service: + keyboard.tap_key(keyboard.volume_up_key)) - bus.register_service(DOMAIN, components.SERVICE_VOLUME_DOWN, - lambda service: - keyboard.tap_key(keyboard.volume_down_key)) + hass.services.register(DOMAIN, components.SERVICE_VOLUME_DOWN, + lambda service: + keyboard.tap_key(keyboard.volume_down_key)) - bus.register_service(DOMAIN, components.SERVICE_VOLUME_MUTE, - lambda service: - keyboard.tap_key(keyboard.volume_mute_key)) + hass.services.register(DOMAIN, components.SERVICE_VOLUME_MUTE, + lambda service: + keyboard.tap_key(keyboard.volume_mute_key)) - bus.register_service(DOMAIN, components.SERVICE_MEDIA_PLAY_PAUSE, - lambda service: - keyboard.tap_key(keyboard.media_play_pause_key)) + hass.services.register(DOMAIN, components.SERVICE_MEDIA_PLAY_PAUSE, + lambda service: + keyboard.tap_key(keyboard.media_play_pause_key)) - bus.register_service(DOMAIN, components.SERVICE_MEDIA_NEXT_TRACK, - lambda service: - keyboard.tap_key(keyboard.media_next_track_key)) + hass.services.register(DOMAIN, components.SERVICE_MEDIA_NEXT_TRACK, + lambda service: + keyboard.tap_key(keyboard.media_next_track_key)) - bus.register_service(DOMAIN, components.SERVICE_MEDIA_PREV_TRACK, - lambda service: - keyboard.tap_key(keyboard.media_prev_track_key)) + hass.services.register(DOMAIN, components.SERVICE_MEDIA_PREV_TRACK, + lambda service: + keyboard.tap_key(keyboard.media_prev_track_key)) return True diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index 2db359bb0f01..c15116c4d681 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -55,7 +55,6 @@ from collections import namedtuple import os import csv -import homeassistant as ha import homeassistant.util as util from homeassistant.components import (group, extract_entity_ids, STATE_ON, STATE_OFF, @@ -89,15 +88,15 @@ ATTR_PROFILE = "profile" LIGHT_PROFILES_FILE = "light_profiles.csv" -def is_on(statemachine, entity_id=None): +def is_on(hass, entity_id=None): """ Returns if the lights are on based on the statemachine. """ entity_id = entity_id or ENTITY_ID_ALL_LIGHTS - return statemachine.is_state(entity_id, STATE_ON) + return hass.states.is_state(entity_id, STATE_ON) # pylint: disable=too-many-arguments -def turn_on(bus, entity_id=None, transition=None, brightness=None, +def turn_on(hass, entity_id=None, transition=None, brightness=None, rgb_color=None, xy_color=None, profile=None): """ Turns all or specified light on. """ data = {} @@ -120,10 +119,10 @@ def turn_on(bus, entity_id=None, transition=None, brightness=None, if xy_color: data[ATTR_XY_COLOR] = xy_color - bus.call_service(DOMAIN, SERVICE_TURN_ON, data) + hass.call_service(DOMAIN, SERVICE_TURN_ON, data) -def turn_off(bus, entity_id=None, transition=None): +def turn_off(hass, entity_id=None, transition=None): """ Turns all or specified light off. """ data = {} @@ -133,11 +132,11 @@ def turn_off(bus, entity_id=None, transition=None): if transition is not None: data[ATTR_TRANSITION] = transition - bus.call_service(DOMAIN, SERVICE_TURN_OFF, data) + hass.call_service(DOMAIN, SERVICE_TURN_OFF, data) # pylint: disable=too-many-branches, too-many-locals -def setup(bus, statemachine, light_control): +def setup(hass, light_control): """ Exposes light control via statemachine and services. """ logger = logging.getLogger(__name__) @@ -178,11 +177,11 @@ def setup(bus, statemachine, light_control): else: state = STATE_OFF - statemachine.set_state(entity_id, state, state_attr) + hass.states.set(entity_id, state, state_attr) def update_light_state(light_id): """ Update the state of specified light. """ - _update_light_state(light_id, light_control.get_state(light_id)) + _update_light_state(light_id, light_control.get(light_id)) # pylint: disable=unused-argument def update_lights_state(time, force_reload=False): @@ -196,7 +195,7 @@ def setup(bus, statemachine, light_control): logger.info("Updating light status") update_lights_state.last_updated = datetime.now() - for light_id, light_state in light_control.get_states().items(): + for light_id, light_state in light_control.gets().items(): _update_light_state(light_id, light_state) # Update light state and discover lights for tracking the group @@ -207,8 +206,7 @@ def setup(bus, statemachine, light_control): return False # Track all lights in a group - group.setup(bus, statemachine, - GROUP_NAME_ALL_LIGHTS, light_to_ent.values()) + group.setup(hass, GROUP_NAME_ALL_LIGHTS, light_to_ent.values()) # Load built-in profiles and custom profiles profile_paths = [os.path.dirname(__file__), os.getcwd()] @@ -245,7 +243,7 @@ def setup(bus, statemachine, light_control): # Convert the entity ids to valid light ids light_ids = [ent_to_light[entity_id] for entity_id - in extract_entity_ids(statemachine, service) + in extract_entity_ids(hass, service) if entity_id in ent_to_light] if not light_ids: @@ -310,14 +308,14 @@ def setup(bus, statemachine, light_control): update_lights_state(None, True) # Update light state every 30 seconds - ha.track_time_change(bus, update_lights_state, second=[0, 30]) + hass.track_time_change(update_lights_state, second=[0, 30]) # Listen for light on and light off service calls - bus.register_service(DOMAIN, SERVICE_TURN_ON, - handle_light_service) + hass.services.register(DOMAIN, SERVICE_TURN_ON, + handle_light_service) - bus.register_service(DOMAIN, SERVICE_TURN_OFF, - handle_light_service) + hass.services.register(DOMAIN, SERVICE_TURN_OFF, + handle_light_service) return True @@ -392,7 +390,7 @@ class HueLightControl(object): return self._lights.get(light_id) - def get_state(self, light_id): + def get(self, light_id): """ Return a LightState representing light light_id. """ try: info = self._bridge.get_light(light_id) @@ -403,7 +401,7 @@ class HueLightControl(object): # socket.error when we cannot reach Hue return None - def get_states(self): + def gets(self): """ Return a dict with id mapped to LightState objects. """ states = {} diff --git a/homeassistant/components/sun.py b/homeassistant/components/sun.py index 9009e9a726fa..c728ce39d818 100644 --- a/homeassistant/components/sun.py +++ b/homeassistant/components/sun.py @@ -7,7 +7,6 @@ Provides functionality to keep track of the sun. import logging from datetime import timedelta -import homeassistant as ha import homeassistant.util as util ENTITY_ID = "sun.sun" @@ -19,16 +18,16 @@ STATE_ATTR_NEXT_RISING = "next_rising" STATE_ATTR_NEXT_SETTING = "next_setting" -def is_on(statemachine, entity_id=None): +def is_on(hass, entity_id=None): """ Returns if the sun is currently up based on the statemachine. """ entity_id = entity_id or ENTITY_ID - return statemachine.is_state(entity_id, STATE_ABOVE_HORIZON) + return hass.states.is_state(entity_id, STATE_ABOVE_HORIZON) -def next_setting(statemachine): +def next_setting(hass): """ Returns the datetime object representing the next sun setting. """ - state = statemachine.get_state(ENTITY_ID) + state = hass.states.get(ENTITY_ID) try: return util.str_to_datetime(state.attributes[STATE_ATTR_NEXT_SETTING]) @@ -38,9 +37,9 @@ def next_setting(statemachine): return None -def next_rising(statemachine): +def next_rising(hass): """ Returns the datetime object representing the next sun rising. """ - state = statemachine.get_state(ENTITY_ID) + state = hass.states.get(ENTITY_ID) try: return util.str_to_datetime(state.attributes[STATE_ATTR_NEXT_RISING]) @@ -50,7 +49,7 @@ def next_rising(statemachine): return None -def setup(bus, statemachine, latitude, longitude): +def setup(hass, latitude, longitude): """ Tracks the state of the sun. """ logger = logging.getLogger(__name__) @@ -89,11 +88,11 @@ def setup(bus, statemachine, latitude, longitude): STATE_ATTR_NEXT_SETTING: util.datetime_to_str(next_setting_dt) } - statemachine.set_state(ENTITY_ID, new_state, state_attributes) + hass.states.set(ENTITY_ID, new_state, state_attributes) # +10 seconds to be sure that the change has occured - ha.track_point_in_time(bus, update_sun_state, - next_change + timedelta(seconds=10)) + hass.track_point_in_time(update_sun_state, + next_change + timedelta(seconds=10)) update_sun_state(None) diff --git a/homeassistant/components/wemo.py b/homeassistant/components/wemo.py index 914d48cb8e2f..6e5983cd739b 100644 --- a/homeassistant/components/wemo.py +++ b/homeassistant/components/wemo.py @@ -2,10 +2,8 @@ Component to interface with WeMo devices on the network. """ import logging -import socket from datetime import datetime, timedelta -import homeassistant as ha import homeassistant.util as util from homeassistant.components import (group, extract_entity_ids, STATE_ON, STATE_OFF, @@ -27,36 +25,39 @@ ATTR_TODAY_STANDBY_TIME = "today_standby_time" MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) -def is_on(statemachine, entity_id=None): +def is_on(hass, entity_id=None): """ Returns if the wemo is on based on the statemachine. """ entity_id = entity_id or ENTITY_ID_ALL_WEMOS - return statemachine.is_state(entity_id, STATE_ON) + return hass.states.is_state(entity_id, STATE_ON) -def turn_on(bus, entity_id=None): +def turn_on(hass, entity_id=None): """ Turns all or specified wemo on. """ data = {ATTR_ENTITY_ID: entity_id} if entity_id else None - bus.call_service(DOMAIN, SERVICE_TURN_ON, data) + hass.call_service(DOMAIN, SERVICE_TURN_ON, data) -def turn_off(bus, entity_id=None): +def turn_off(hass, entity_id=None): """ Turns all or specified wemo off. """ data = {ATTR_ENTITY_ID: entity_id} if entity_id else None - bus.call_service(DOMAIN, SERVICE_TURN_OFF, data) + hass.call_service(DOMAIN, SERVICE_TURN_OFF, data) # pylint: disable=too-many-branches -def setup(bus, statemachine): +def setup(hass, hosts=None): """ Track states and offer events for WeMo switches. """ logger = logging.getLogger(__name__) try: import homeassistant.external.pywemo.pywemo as pywemo except ImportError: - pass + logger.exception(( + "Failed to import pywemo. " + "Did you maybe not run `git submodule init` " + "and `git submodule update`?")) return False @@ -76,7 +77,7 @@ def setup(bus, statemachine): # Dict mapping entity IDs to devices ent_to_dev = {} - def _update_wemo_state(device): + def update_wemo_state(device): """ Update the state of specified WeMo device. """ # We currently only support switches @@ -107,33 +108,32 @@ def setup(bus, statemachine): #state_attr[ATTR_TODAY_ON_TIME] = device.today_on_time #state_attr[ATTR_TODAY_STANDBY_TIME] = device.today_standby_time - statemachine.set_state(entity_id, state, state_attr) + hass.states.set(entity_id, state, state_attr) # pylint: disable=unused-argument - def _update_wemos_state(time, force_reload=False): + def update_wemos_state(time, force_reload=False): """ Update states of all WeMo devices. """ # First time this method gets called, force_reload should be True if (force_reload or - datetime.now() - _update_wemos_state.last_updated > + datetime.now() - update_wemos_state.last_updated > MIN_TIME_BETWEEN_SCANS): logger.info("Updating WeMo status") - _update_wemos_state.last_updated = datetime.now() + update_wemos_state.last_updated = datetime.now() for device in switches: - _update_wemo_state(device) + update_wemo_state(device) - _update_wemos_state(None, True) + update_wemos_state(None, True) # Track all lights in a group - group.setup(bus, statemachine, - GROUP_NAME_ALL_WEMOS, list(sno_to_ent.values())) + group.setup(hass, GROUP_NAME_ALL_WEMOS, sno_to_ent.values()) - def _handle_wemo_service(service): + def handle_wemo_service(service): """ Handles calls to the WeMo service. """ devices = [ent_to_dev[entity_id] for entity_id - in extract_entity_ids(statemachine, service) + in extract_entity_ids(hass, service) if entity_id in ent_to_dev] if not devices: @@ -145,13 +145,13 @@ def setup(bus, statemachine): else: device.off() - _update_wemo_state(device) + update_wemo_state(device) # Update WeMo state every 30 seconds - ha.track_time_change(bus, _update_wemos_state, second=[0, 30]) + hass.track_time_change(update_wemos_state, second=[0, 30]) - bus.register_service(DOMAIN, SERVICE_TURN_OFF, _handle_wemo_service) + hass.services.register(DOMAIN, SERVICE_TURN_OFF, handle_wemo_service) - bus.register_service(DOMAIN, SERVICE_TURN_ON, _handle_wemo_service) + hass.services.register(DOMAIN, SERVICE_TURN_ON, handle_wemo_service) return True diff --git a/homeassistant/remote.py b/homeassistant/remote.py index ef1444c314ac..26641b113c99 100644 --- a/homeassistant/remote.py +++ b/homeassistant/remote.py @@ -61,10 +61,8 @@ class JSONEncoder(json.JSONEncoder): return json.JSONEncoder.default(self, obj) -class Bus(object): - """ Drop-in replacement for a normal bus that will forward interaction to - a remote bus. - """ +class EventBus(object): + """ Allows to interface with a Home Assistant EventBus via the API. """ def __init__(self, host, api_password, port=None): self.logger = logging.getLogger(__name__) @@ -72,32 +70,7 @@ class Bus(object): self._call_api = _setup_call_api(host, port, api_password) @property - def services(self): - """ List the available services. """ - try: - req = self._call_api(METHOD_GET, hah.URL_API_SERVICES) - - if req.status_code == 200: - data = req.json() - - return data['services'] - - else: - raise ha.HomeAssistantError( - "Got unexpected result (3): {}.".format(req.text)) - - except ValueError: # If req.json() can't parse the json - self.logger.exception("Bus:Got unexpected result") - raise ha.HomeAssistantError( - "Got unexpected result: {}".format(req.text)) - - except KeyError: # If not all expected keys are in the returned JSON - self.logger.exception("Bus:Got unexpected result (2)") - raise ha.HomeAssistantError( - "Got unexpected result (2): {}".format(req.text)) - - @property - def event_listeners(self): + def listeners(self): """ List of events that is being listened for. """ try: req = self._call_api(METHOD_GET, hah.URL_API_EVENTS) @@ -121,37 +94,7 @@ class Bus(object): raise ha.HomeAssistantError( "Got unexpected result (2): {}".format(req.text)) - def call_service(self, domain, service, service_data=None): - """ Calls a service. """ - - if service_data: - data = {'service_data': json.dumps(service_data)} - else: - data = None - - req = self._call_api(METHOD_POST, - hah.URL_API_SERVICES_SERVICE.format( - domain, service), - data) - - if req.status_code != 200: - error = "Error calling service: {} - {}".format( - req.status_code, req.text) - - self.logger.error("Bus:{}".format(error)) - - if req.status_code == 400: - raise ha.ServiceDoesNotExistError(error) - else: - raise ha.HomeAssistantError(error) - - def register_service(self, domain, service, service_callback): - """ Not implemented for remote bus. - - Will throw NotImplementedError. """ - raise NotImplementedError - - def fire_event(self, event_type, event_data=None): + def fire(self, event_type, event_data=None): """ Fire an event. """ if event_data: @@ -170,30 +113,9 @@ class Bus(object): self.logger.error("Bus:{}".format(error)) raise ha.HomeAssistantError(error) - def has_service(self, domain, service): - """ Not implemented for remote bus. - - Will throw NotImplementedError. """ - raise NotImplementedError - - def listen_event(self, event_type, listener): - """ Not implemented for remote bus. - - Will throw NotImplementedError. """ - raise NotImplementedError - - def remove_event_listener(self, event_type, listener): - """ Not implemented for remote bus. - - Will throw NotImplementedError. """ - - raise NotImplementedError - class StateMachine(object): - """ Drop-in replacement for a normal statemachine that communicates with a - remote statemachine. - """ + """ Allows to interface with a Home Assistant StateMachine via the API. """ def __init__(self, host, api_password, port=None): self._call_api = _setup_call_api(host, port, api_password) @@ -222,14 +144,7 @@ class StateMachine(object): self.logger.exception("StateMachine:Got unexpected result (2)") return [] - def remove_entity(self, entity_id): - """ This method is not implemented for remote statemachine. - - Throws NotImplementedError. """ - - raise NotImplementedError - - def set_state(self, entity_id, new_state, attributes=None): + def set(self, entity_id, new_state, attributes=None): """ Set the state of a entity, add entity if it does not exist. Attributes is an optional dict to specify attributes of this state. """ @@ -260,7 +175,7 @@ class StateMachine(object): finally: self.lock.release() - def get_state(self, entity_id): + def get(self, entity_id): """ Returns the state of the specified entity. """ try: @@ -297,7 +212,61 @@ class StateMachine(object): def is_state(self, entity_id, state): """ Returns True if entity exists and is specified state. """ try: - return self.get_state(entity_id).state == state + return self.get(entity_id).state == state except AttributeError: - # get_state returned None + # get returned None return False + + +class ServiceRegistry(object): + """ Allows to interface with a Home Assistant ServiceRegistry + via the API. """ + + def __init__(self, host, api_password, port=None): + self.logger = logging.getLogger(__name__) + + self._call_api = _setup_call_api(host, port, api_password) + + @property + def services(self): + """ List the available services. """ + try: + req = self._call_api(METHOD_GET, hah.URL_API_SERVICES) + + if req.status_code == 200: + data = req.json() + + return data['services'] + + else: + raise ha.HomeAssistantError( + "Got unexpected result (3): {}.".format(req.text)) + + except ValueError: # If req.json() can't parse the json + self.logger.exception("ServiceRegistry:Got unexpected result") + raise ha.HomeAssistantError( + "Got unexpected result: {}".format(req.text)) + + except KeyError: # If not all expected keys are in the returned JSON + self.logger.exception("ServiceRegistry:Got unexpected result (2)") + raise ha.HomeAssistantError( + "Got unexpected result (2): {}".format(req.text)) + + def call_service(self, domain, service, service_data=None): + """ Calls a service. """ + + if service_data: + data = {'service_data': json.dumps(service_data)} + else: + data = None + + req = self._call_api(METHOD_POST, + hah.URL_API_SERVICES_SERVICE.format( + domain, service), + data) + + if req.status_code != 200: + error = "Error calling service: {} - {}".format( + req.status_code, req.text) + + self.logger.error("ServiceRegistry:{}".format(error)) diff --git a/homeassistant/test.py b/homeassistant/test.py index 2ec10a785690..c193e2df7b25 100644 --- a/homeassistant/test.py +++ b/homeassistant/test.py @@ -27,30 +27,28 @@ def _url(path=""): class HAHelper(object): # pylint: disable=too-few-public-methods """ Helper class to keep track of current running HA instance. """ - core = None + hass = None def ensure_homeassistant_started(): """ Ensures home assistant is started. """ - if not HAHelper.core: - core = {'bus': ha.Bus()} - core['statemachine'] = ha.StateMachine(core['bus']) + if not HAHelper.hass: + hass = ha.HomeAssistant() - core['bus'].listen_event('test_event', len) - core['statemachine'].set_state('test', 'a_state') + hass.bus.listen('test_event', len) + hass.states.set('test', 'a_state') - hah.HTTPInterface(core['bus'], core['statemachine'], - API_PASSWORD) + hah.HTTPInterface(hass, API_PASSWORD) - core['bus'].fire_event(ha.EVENT_HOMEASSISTANT_START) + hass.bus.fire(ha.EVENT_HOMEASSISTANT_START) # Give objects time to startup time.sleep(1) - HAHelper.core = core + HAHelper.hass = hass - return HAHelper.core['bus'], HAHelper.core['statemachine'] + return HAHelper.hass # pylint: disable=too-many-public-methods @@ -60,7 +58,7 @@ class TestHTTPInterface(unittest.TestCase): @classmethod def setUpClass(cls): # pylint: disable=invalid-name """ things to be run when tests are started. """ - cls.bus, cls.statemachine = ensure_homeassistant_started() + cls.hass = ensure_homeassistant_started() def test_debug_interface(self): """ Test if we can login by comparing not logged in screen to @@ -89,14 +87,14 @@ class TestHTTPInterface(unittest.TestCase): def test_debug_change_state(self): """ Test if we can change a state from the debug interface. """ - self.statemachine.set_state("test.test", "not_to_be_set_state") + self.hass.states.set("test.test", "not_to_be_set") requests.post(_url(hah.URL_CHANGE_STATE), data={"entity_id": "test.test", "new_state": "debug_state_change2", "api_password": API_PASSWORD}) - self.assertEqual(self.statemachine.get_state("test.test").state, + self.assertEqual(self.hass.states.get("test.test").state, "debug_state_change2") def test_debug_fire_event(self): @@ -109,7 +107,7 @@ class TestHTTPInterface(unittest.TestCase): if "test" in event.data: test_value.append(1) - ha.listen_once_event(self.bus, "test_event_with_data", listener) + self.hass.listen_once_event("test_event_with_data", listener) requests.post( _url(hah.URL_FIRE_EVENT), @@ -129,10 +127,10 @@ class TestHTTPInterface(unittest.TestCase): data = req.json() - self.assertEqual(list(self.statemachine.entity_ids), + self.assertEqual(list(self.hass.states.entity_ids), data['entity_ids']) - def test_api_get_state(self): + def test_api_get(self): """ Test if the debug interface allows us to get a state. """ req = requests.get( _url(hah.URL_API_STATES_ENTITY.format("test")), @@ -140,7 +138,7 @@ class TestHTTPInterface(unittest.TestCase): data = ha.State.from_dict(req.json()) - state = self.statemachine.get_state("test") + state = self.hass.states.get("test") self.assertEqual(data.state, state.state) self.assertEqual(data.last_changed, state.last_changed) @@ -157,13 +155,13 @@ class TestHTTPInterface(unittest.TestCase): def test_api_state_change(self): """ Test if we can change the state of an entity that exists. """ - self.statemachine.set_state("test.test", "not_to_be_set_state") + self.hass.states.set("test.test", "not_to_be_set") requests.post(_url(hah.URL_API_STATES_ENTITY.format("test.test")), data={"new_state": "debug_state_change2", "api_password": API_PASSWORD}) - self.assertEqual(self.statemachine.get_state("test.test").state, + self.assertEqual(self.hass.states.get("test.test").state, "debug_state_change2") # pylint: disable=invalid-name @@ -179,8 +177,8 @@ class TestHTTPInterface(unittest.TestCase): data={"new_state": new_state, "api_password": API_PASSWORD}) - cur_state = (self.statemachine. - get_state("test_entity_that_does_not_exist").state) + cur_state = (self.hass.states. + get("test_entity_that_does_not_exist").state) self.assertEqual(req.status_code, 201) self.assertEqual(cur_state, new_state) @@ -194,7 +192,7 @@ class TestHTTPInterface(unittest.TestCase): """ Helper method that will verify our event got called. """ test_value.append(1) - ha.listen_once_event(self.bus, "test.event_no_data", listener) + self.hass.listen_once_event("test.event_no_data", listener) requests.post( _url(hah.URL_API_EVENTS_EVENT.format("test.event_no_data")), @@ -216,7 +214,7 @@ class TestHTTPInterface(unittest.TestCase): if "test" in event.data: test_value.append(1) - ha.listen_once_event(self.bus, "test_event_with_data", listener) + self.hass.listen_once_event("test_event_with_data", listener) requests.post( _url(hah.URL_API_EVENTS_EVENT.format("test_event_with_data")), @@ -237,7 +235,7 @@ class TestHTTPInterface(unittest.TestCase): """ Helper method that will verify our event got called. """ test_value.append(1) - ha.listen_once_event(self.bus, "test_event_with_bad_data", listener) + self.hass.listen_once_event("test_event_with_bad_data", listener) req = requests.post( _url(hah.URL_API_EVENTS_EVENT.format("test_event")), @@ -257,7 +255,7 @@ class TestHTTPInterface(unittest.TestCase): data = req.json() - self.assertEqual(data['event_listeners'], self.bus.event_listeners) + self.assertEqual(data['event_listeners'], self.hass.bus.listeners) def test_api_get_services(self): """ Test if we can get a dict describing current services. """ @@ -266,7 +264,7 @@ class TestHTTPInterface(unittest.TestCase): data = req.json() - self.assertEqual(data['services'], self.bus.services) + self.assertEqual(data['services'], self.hass.services.services) def test_api_call_service_no_data(self): """ Test if the API allows us to call a service. """ @@ -276,7 +274,7 @@ class TestHTTPInterface(unittest.TestCase): """ Helper method that will verify that our service got called. """ test_value.append(1) - self.bus.register_service("test_domain", "test_service", listener) + self.hass.services.register("test_domain", "test_service", listener) requests.post( _url(hah.URL_API_SERVICES_SERVICE.format( @@ -298,7 +296,7 @@ class TestHTTPInterface(unittest.TestCase): if "test" in service_call.data: test_value.append(1) - self.bus.register_service("test_domain", "test_service", listener) + self.hass.services.register("test_domain", "test_service", listener) requests.post( _url(hah.URL_API_SERVICES_SERVICE.format( @@ -318,25 +316,26 @@ class TestRemote(unittest.TestCase): @classmethod def setUpClass(cls): # pylint: disable=invalid-name """ things to be run when tests are started. """ - cls.bus, cls.statemachine = ensure_homeassistant_started() + cls.hass = ensure_homeassistant_started() cls.remote_sm = remote.StateMachine("127.0.0.1", API_PASSWORD) - cls.remote_eb = remote.Bus("127.0.0.1", API_PASSWORD) + cls.remote_eb = remote.EventBus("127.0.0.1", API_PASSWORD) + cls.remote_sr = remote.ServiceRegistry("127.0.0.1", API_PASSWORD) cls.sm_with_remote_eb = ha.StateMachine(cls.remote_eb) - cls.sm_with_remote_eb.set_state("test", "a_state") + cls.sm_with_remote_eb.set("test", "a_state") # pylint: disable=invalid-name def test_remote_sm_list_state_entities(self): """ Test if the debug interface allows us to list state entity ids. """ - self.assertEqual(list(self.statemachine.entity_ids), + self.assertEqual(list(self.hass.states.entity_ids), self.remote_sm.entity_ids) - def test_remote_sm_get_state(self): + def test_remote_sm_get(self): """ Test if debug interface allows us to get state of an entity. """ - remote_state = self.remote_sm.get_state("test") + remote_state = self.remote_sm.get("test") - state = self.statemachine.get_state("test") + state = self.hass.states.get("test") self.assertEqual(remote_state.state, state.state) self.assertEqual(remote_state.last_changed, state.last_changed) @@ -344,22 +343,22 @@ class TestRemote(unittest.TestCase): def test_remote_sm_get_non_existing_state(self): """ Test remote state machine to get state of non existing entity. """ - self.assertEqual(self.remote_sm.get_state("test_does_not_exist"), None) + self.assertEqual(self.remote_sm.get("test_does_not_exist"), None) def test_remote_sm_state_change(self): """ Test if we can change the state of an existing entity. """ - self.remote_sm.set_state("test", "set_remotely", {"test": 1}) + self.remote_sm.set("test", "set_remotely", {"test": 1}) - state = self.statemachine.get_state("test") + state = self.hass.states.get("test") self.assertEqual(state.state, "set_remotely") self.assertEqual(state.attributes['test'], 1) def test_remote_eb_listening_for_same(self): """ Test if remote EB correctly reports listener overview. """ - self.assertEqual(self.bus.event_listeners, - self.remote_eb.event_listeners) + self.assertEqual(self.hass.bus.listeners, + self.remote_eb.listeners) # pylint: disable=invalid-name def test_remote_eb_fire_event_with_no_data(self): @@ -370,9 +369,9 @@ class TestRemote(unittest.TestCase): """ Helper method that will verify our event got called. """ test_value.append(1) - ha.listen_once_event(self.bus, "test_event_no_data", listener) + self.hass.listen_once_event("test_event_no_data", listener) - self.remote_eb.fire_event("test_event_no_data") + self.remote_eb.fire("test_event_no_data") # Allow the event to take place time.sleep(1) @@ -389,9 +388,9 @@ class TestRemote(unittest.TestCase): if event.data["test"] == 1: test_value.append(1) - ha.listen_once_event(self.bus, "test_event_with_data", listener) + self.hass.listen_once_event("test_event_with_data", listener) - self.remote_eb.fire_event("test_event_with_data", {"test": 1}) + self.remote_eb.fire("test_event_with_data", {"test": 1}) # Allow the event to take place time.sleep(1) @@ -399,7 +398,7 @@ class TestRemote(unittest.TestCase): self.assertEqual(len(test_value), 1) # pylint: disable=invalid-name - def test_remote_eb_call_service_with_no_data(self): + def test_remote_sr_call_service_with_no_data(self): """ Test if the remote bus allows us to fire a service. """ test_value = [] @@ -407,9 +406,9 @@ class TestRemote(unittest.TestCase): """ Helper method that will verify our service got called. """ test_value.append(1) - self.bus.register_service("test_domain", "test_service", listener) + self.hass.services.register("test_domain", "test_service", listener) - self.remote_eb.call_service("test_domain", "test_service") + self.remote_sr.call_service("test_domain", "test_service") # Allow the service call to take place time.sleep(1) @@ -417,7 +416,7 @@ class TestRemote(unittest.TestCase): self.assertEqual(len(test_value), 1) # pylint: disable=invalid-name - def test_remote_eb_call_service_with_data(self): + def test_remote_sr_call_service_with_data(self): """ Test if the remote bus allows us to fire an event. """ test_value = [] @@ -426,9 +425,9 @@ class TestRemote(unittest.TestCase): if service_call.data["test"] == 1: test_value.append(1) - self.bus.register_service("test_domain", "test_service", listener) + self.hass.services.register("test_domain", "test_service", listener) - self.remote_eb.call_service("test_domain", "test_service", {"test": 1}) + self.remote_sr.call_service("test_domain", "test_service", {"test": 1}) # Allow the event to take place time.sleep(1) @@ -444,9 +443,9 @@ class TestRemote(unittest.TestCase): """ Helper method that will verify our event got called. """ test_value.append(1) - ha.listen_once_event(self.bus, ha.EVENT_STATE_CHANGED, listener) + self.hass.listen_once_event(ha.EVENT_STATE_CHANGED, listener) - self.sm_with_remote_eb.set_state("test", "local sm with remote eb") + self.sm_with_remote_eb.set("test", "local sm with remote eb") # Allow the event to take place time.sleep(1) diff --git a/homeassistant/util.py b/homeassistant/util.py index 14fe4b66aacd..ac551609e932 100644 --- a/homeassistant/util.py +++ b/homeassistant/util.py @@ -52,16 +52,6 @@ def split_entity_id(entity_id): return entity_id.split(".", 1) -def filter_entity_ids(entity_ids, domain_filter=None, strip_domain=False): - """ Filter a list of entities based on domain. Setting strip_domain - will only return the object_ids. """ - return [ - split_entity_id(entity_id)[1] if strip_domain else entity_id - for entity_id in entity_ids if - not domain_filter or entity_id.startswith(domain_filter) - ] - - def repr_helper(inp): """ Helps creating a more readable string representation of objects. """ if isinstance(inp, dict): @@ -78,7 +68,7 @@ def repr_helper(inp): # License: Code is given as is. Use at your own risk and discretion. # pylint: disable=invalid-name def color_RGB_to_xy(R, G, B): - ''' Convert from RGB color to XY color. ''' + """ Convert from RGB color to XY color. """ var_R = (R / 255.) var_G = (G / 255.) var_B = (B / 255.) @@ -176,9 +166,17 @@ class ThreadPool(object): Will initiate it's workers using worker(queue).start() """ # pylint: disable=too-few-public-methods - def __init__(self, worker_count, job_handler): + def __init__(self, worker_count, job_handler, busy_callback=None): + """ + worker_count: number of threads to run that handle jobs + job_handler: method to be called from worker thread to handle job + busy_callback: method to be called when queue gets too big. + Parameters: list_of_current_jobs, number_pending_jobs + """ work_queue = self.work_queue = queue.PriorityQueue() current_jobs = self.current_jobs = [] + self.busy_callback = busy_callback + self.busy_warning_limit = worker_count**2 for _ in range(worker_count): worker = threading.Thread(target=_threadpool_worker, @@ -191,6 +189,14 @@ class ThreadPool(object): """ Add a job to be sent to the workers. """ self.work_queue.put(PriorityQueueItem(priority, job)) + # check if our queue is getting too big + if self.work_queue.qsize() > self.busy_warning_limit: + + # Increase limit we will issue next warning + self.busy_warning_limit *= 2 + + self.busy_callback(self.current_jobs, self.work_queue.qsize()) + class PriorityQueueItem(object): """ Holds a priority and a value. Used within PriorityQueue. """ diff --git a/start.py b/start.py index 16a26c780a58..eeb29a27a12f 100644 --- a/start.py +++ b/start.py @@ -1,5 +1,6 @@ """ Starts home assistant with all possible functionality. """ +import homeassistant import homeassistant.bootstrap -homeassistant.bootstrap.from_config_file("home-assistant.conf") +homeassistant.bootstrap.from_config_file("home-assistant.conf").start()