diff --git a/homeassistant/__init__.py b/homeassistant/__init__.py index afc4ea8375f..e69de29bb2d 100644 --- a/homeassistant/__init__.py +++ b/homeassistant/__init__.py @@ -1,802 +0,0 @@ -""" -homeassistant -~~~~~~~~~~~~~ - -Home Assistant is a Home Automation framework for observing the state -of entities and react to changes. -""" - -import os -import time -import logging -import threading -import enum -import re -import functools as ft -from collections import namedtuple - -from homeassistant.const import ( - EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, - SERVICE_HOMEASSISTANT_STOP, EVENT_TIME_CHANGED, EVENT_STATE_CHANGED, - EVENT_CALL_SERVICE, ATTR_NOW, ATTR_DOMAIN, ATTR_SERVICE, MATCH_ALL, - EVENT_SERVICE_EXECUTED, ATTR_SERVICE_CALL_ID, EVENT_SERVICE_REGISTERED, - TEMP_CELCIUS, TEMP_FAHRENHEIT, ATTR_FRIENDLY_NAME) -import homeassistant.util as util -import homeassistant.util.dt as date_util - -DOMAIN = "homeassistant" - -# How often time_changed event should fire -TIMER_INTERVAL = 1 # seconds - -# How long we wait for the result of a service call -SERVICE_CALL_LIMIT = 10 # seconds - -# Define number of MINIMUM worker threads. -# During bootstrap of HA (see bootstrap._setup_component()) worker threads -# will be added for each component that polls devices. -MIN_WORKER_THREAD = 2 - -# Pattern for validating entity IDs (format: .) -ENTITY_ID_PATTERN = re.compile(r"^(?P\w+)\.(?P\w+)$") - -_LOGGER = logging.getLogger(__name__) - -# Temporary to support deprecated methods -_MockHA = namedtuple("MockHomeAssistant", ['bus']) - - -class HomeAssistant(object): - """ Core class to route all communication to right components. """ - - def __init__(self): - self.pool = pool = create_worker_pool() - self.bus = EventBus(pool) - self.services = ServiceRegistry(self.bus, pool) - self.states = StateMachine(self.bus) - self.config = Config() - - def start(self): - """ Start home assistant. """ - _LOGGER.info( - "Starting Home Assistant (%d threads)", self.pool.worker_count) - - create_timer(self) - self.bus.fire(EVENT_HOMEASSISTANT_START) - - def block_till_stopped(self): - """ Will register service homeassistant/stop and - will block until called. """ - request_shutdown = threading.Event() - - def stop_homeassistant(service): - """ Stops Home Assistant. """ - request_shutdown.set() - - self.services.register( - DOMAIN, SERVICE_HOMEASSISTANT_STOP, stop_homeassistant) - - while not request_shutdown.isSet(): - try: - time.sleep(1) - except KeyboardInterrupt: - break - - self.stop() - - def stop(self): - """ Stops Home Assistant and shuts down all threads. """ - _LOGGER.info("Stopping") - - self.bus.fire(EVENT_HOMEASSISTANT_STOP) - - # Wait till all responses to homeassistant_stop are done - self.pool.block_till_done() - - self.pool.stop() - - def track_point_in_time(self, action, point_in_time): - """Deprecated method as of 8/4/2015 to track point in time.""" - _LOGGER.warning( - 'hass.track_point_in_time is deprecated. ' - 'Please use homeassistant.helpers.event.track_point_in_time') - import homeassistant.helpers.event as helper - helper.track_point_in_time(self, action, point_in_time) - - def track_point_in_utc_time(self, action, point_in_time): - """Deprecated method as of 8/4/2015 to track point in UTC time.""" - _LOGGER.warning( - 'hass.track_point_in_utc_time is deprecated. ' - 'Please use homeassistant.helpers.event.track_point_in_utc_time') - import homeassistant.helpers.event as helper - helper.track_point_in_utc_time(self, action, point_in_time) - - def track_utc_time_change(self, action, - year=None, month=None, day=None, - hour=None, minute=None, second=None): - """Deprecated method as of 8/4/2015 to track UTC time change.""" - # pylint: disable=too-many-arguments - _LOGGER.warning( - 'hass.track_utc_time_change is deprecated. ' - 'Please use homeassistant.helpers.event.track_utc_time_change') - import homeassistant.helpers.event as helper - helper.track_utc_time_change(self, action, year, month, day, hour, - minute, second) - - def track_time_change(self, action, - year=None, month=None, day=None, - hour=None, minute=None, second=None, utc=False): - """Deprecated method as of 8/4/2015 to track time change.""" - # pylint: disable=too-many-arguments - _LOGGER.warning( - 'hass.track_time_change is deprecated. ' - 'Please use homeassistant.helpers.event.track_time_change') - import homeassistant.helpers.event as helper - helper.track_time_change(self, action, year, month, day, hour, - minute, second) - - -class JobPriority(util.OrderedEnum): - """ Provides priorities for bus events. """ - # pylint: disable=no-init,too-few-public-methods - - EVENT_CALLBACK = 0 - EVENT_SERVICE = 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 JobPriority.EVENT_TIME - elif event_type == EVENT_STATE_CHANGED: - return JobPriority.EVENT_STATE - elif event_type == EVENT_CALL_SERVICE: - return JobPriority.EVENT_SERVICE - elif event_type == EVENT_SERVICE_EXECUTED: - return JobPriority.EVENT_CALLBACK - else: - return JobPriority.EVENT_DEFAULT - - -class EventOrigin(enum.Enum): - """ Distinguish between origin of event. """ - # pylint: disable=no-init,too-few-public-methods - - local = "LOCAL" - remote = "REMOTE" - - def __str__(self): - return self.value - - -# pylint: disable=too-few-public-methods -class Event(object): - """ Represents an event within the Bus. """ - - __slots__ = ['event_type', 'data', 'origin', 'time_fired'] - - def __init__(self, event_type, data=None, origin=EventOrigin.local, - time_fired=None): - self.event_type = event_type - self.data = data or {} - self.origin = origin - self.time_fired = date_util.strip_microseconds( - time_fired or date_util.utcnow()) - - def as_dict(self): - """ Returns a dict representation of this Event. """ - return { - 'event_type': self.event_type, - 'data': dict(self.data), - 'origin': str(self.origin), - 'time_fired': date_util.datetime_to_str(self.time_fired), - } - - def __repr__(self): - # pylint: disable=maybe-no-member - if self.data: - return "".format( - self.event_type, str(self.origin)[0], - util.repr_helper(self.data)) - else: - return "".format(self.event_type, - str(self.origin)[0]) - - def __eq__(self, other): - return (self.__class__ == other.__class__ and - self.event_type == other.event_type and - self.data == other.data and - self.origin == other.origin and - self.time_fired == other.time_fired) - - -class EventBus(object): - """ Class that allows different components to communicate via services - and events. - """ - - def __init__(self, pool=None): - self._listeners = {} - self._lock = threading.Lock() - self._pool = pool or create_worker_pool() - - @property - def listeners(self): - """ Dict with events that is being listened for and the number - of listeners. - """ - with self._lock: - return {key: len(self._listeners[key]) - for key in self._listeners} - - def fire(self, event_type, event_data=None, origin=EventOrigin.local): - """ Fire an event. """ - if not self._pool.running: - raise HomeAssistantError('Home Assistant has shut down.') - - 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._listeners.get - listeners = get(MATCH_ALL, []) + get(event_type, []) - - event = Event(event_type, event_data, origin) - - if event_type != EVENT_TIME_CHANGED: - _LOGGER.info("Bus:Handling %s", event) - - if not listeners: - return - - job_priority = JobPriority.from_event_type(event_type) - - for func in listeners: - self._pool.add_job(job_priority, (func, event)) - - 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._lock: - if event_type in self._listeners: - self._listeners[event_type].append(listener) - else: - self._listeners[event_type] = [listener] - - def listen_once(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. - - Returns registered listener that can be used with remove_listener. - """ - @ft.wraps(listener) - def onetime_listener(event): - """ Removes listener from eventbus and then fires listener. """ - if hasattr(onetime_listener, 'run'): - return - # 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.remove_listener(event_type, onetime_listener) - - listener(event) - - self.listen(event_type, onetime_listener) - - return onetime_listener - - def remove_listener(self, event_type, listener): - """ Removes a listener of a specific event_type. """ - with self._lock: - try: - self._listeners[event_type].remove(listener) - - # delete event_type list if empty - if not self._listeners[event_type]: - self._listeners.pop(event_type) - - except (KeyError, ValueError): - # KeyError is key event_type listener did not exist - # ValueError if listener did not exist within event_type - pass - - -class State(object): - """ - Object to represent a state within the state machine. - - entity_id: the entity that is represented. - state: the state of the entity - attributes: extra information on entity and state - last_changed: last time the state was changed, not the attributes. - last_updated: last time this object was updated. - """ - - __slots__ = ['entity_id', 'state', 'attributes', - 'last_changed', 'last_updated'] - - # pylint: disable=too-many-arguments - def __init__(self, entity_id, state, attributes=None, last_changed=None, - last_updated=None): - if not ENTITY_ID_PATTERN.match(entity_id): - raise InvalidEntityFormatError(( - "Invalid entity id encountered: {}. " - "Format should be .").format(entity_id)) - - self.entity_id = entity_id.lower() - self.state = state - self.attributes = attributes or {} - self.last_updated = date_util.strip_microseconds( - last_updated or date_util.utcnow()) - - # Strip microsecond from last_changed else we cannot guarantee - # state == State.from_dict(state.as_dict()) - # This behavior occurs because to_dict uses datetime_to_str - # which does not preserve microseconds - self.last_changed = date_util.strip_microseconds( - last_changed or self.last_updated) - - @property - def domain(self): - """ Returns domain of this state. """ - return util.split_entity_id(self.entity_id)[0] - - @property - def object_id(self): - """ Returns object_id of this state. """ - return util.split_entity_id(self.entity_id)[1] - - @property - def name(self): - """ Name to represent this state. """ - return ( - self.attributes.get(ATTR_FRIENDLY_NAME) or - self.object_id.replace('_', ' ')) - - def copy(self): - """ Creates a copy of itself. """ - return State(self.entity_id, self.state, - dict(self.attributes), self.last_changed) - - def as_dict(self): - """ Converts State to a dict to be used within JSON. - Ensures: state == State.from_dict(state.as_dict()) """ - - return {'entity_id': self.entity_id, - 'state': self.state, - 'attributes': self.attributes, - 'last_changed': date_util.datetime_to_str(self.last_changed), - 'last_updated': date_util.datetime_to_str(self.last_updated)} - - @classmethod - def from_dict(cls, json_dict): - """ Static method to create a state from a dict. - Ensures: state == State.from_json_dict(state.to_json_dict()) """ - - if not (json_dict and - 'entity_id' in json_dict and - 'state' in json_dict): - return None - - last_changed = json_dict.get('last_changed') - - if last_changed: - last_changed = date_util.str_to_datetime(last_changed) - - last_updated = json_dict.get('last_updated') - - if last_updated: - last_updated = date_util.str_to_datetime(last_updated) - - return cls(json_dict['entity_id'], json_dict['state'], - json_dict.get('attributes'), last_changed, last_updated) - - def __eq__(self, other): - return (self.__class__ == other.__class__ and - self.entity_id == other.entity_id and - self.state == other.state and - self.attributes == other.attributes) - - def __repr__(self): - attr = "; {}".format(util.repr_helper(self.attributes)) \ - if self.attributes else "" - - return "".format( - self.entity_id, self.state, attr, - date_util.datetime_to_local_str(self.last_changed)) - - -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() - - def entity_ids(self, domain_filter=None): - """ List of entity ids that are being tracked. """ - if domain_filter is None: - return list(self._states.keys()) - - domain_filter = domain_filter.lower() - - return [state.entity_id for key, state - in self._states.items() - if util.split_entity_id(key)[0] == domain_filter] - - def all(self): - """ Returns a list of all states. """ - with self._lock: - return [state.copy() for state in self._states.values()] - - def get(self, entity_id): - """ Returns the state of the specified entity. """ - state = self._states.get(entity_id.lower()) - - # 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. """ - entity_id = entity_id.lower() - - return (entity_id in self._states and - self._states[entity_id].state == state) - - def remove(self, entity_id): - """ Removes an entity from the state machine. - - Returns boolean to indicate if an entity was removed. """ - entity_id = entity_id.lower() - - with self._lock: - return self._states.pop(entity_id, None) is not 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. - - If you just update the attributes and not the state, last changed will - not be affected. - """ - entity_id = entity_id.lower() - new_state = str(new_state) - attributes = attributes or {} - - with self._lock: - old_state = self._states.get(entity_id) - - is_existing = old_state is not None - same_state = is_existing and old_state.state == new_state - same_attr = is_existing and old_state.attributes == attributes - - if same_state and same_attr: - return - - # If state did not exist or is different, set it - last_changed = old_state.last_changed if same_state else None - - state = State(entity_id, new_state, attributes, last_changed) - self._states[entity_id] = state - - event_data = {'entity_id': entity_id, 'new_state': state} - - if old_state: - event_data['old_state'] = old_state - - self._bus.fire(EVENT_STATE_CHANGED, event_data) - - def track_change(self, entity_ids, action, from_state=None, to_state=None): - """ - DEPRECATED AS OF 8/4/2015 - """ - _LOGGER.warning( - 'hass.states.track_change is deprecated. ' - 'Use homeassistant.helpers.event.track_state_change instead.') - import homeassistant.helpers.event as helper - helper.track_state_change(_MockHA(self._bus), entity_ids, action, - from_state, to_state) - - -# 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() - self._bus = bus - self._cur_id = 0 - 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} - - self._bus.fire( - EVENT_SERVICE_REGISTERED, - {ATTR_DOMAIN: domain, ATTR_SERVICE: service}) - - def call(self, domain, service, service_data=None, blocking=False): - """ - Calls specified service. - Specify blocking=True to wait till service is executed. - Waits a maximum of SERVICE_CALL_LIMIT. - - If blocking = True, will return boolean if service executed - succesfully within SERVICE_CALL_LIMIT. - - This method will fire an event to call the service. - This event will be picked up by this ServiceRegistry and any - other ServiceRegistry that is listening on the EventBus. - - Because the service is sent as an event you are not allowed to use - the keys ATTR_DOMAIN and ATTR_SERVICE in your service_data. - """ - call_id = self._generate_unique_id() - event_data = service_data or {} - event_data[ATTR_DOMAIN] = domain - event_data[ATTR_SERVICE] = service - event_data[ATTR_SERVICE_CALL_ID] = call_id - - if blocking: - executed_event = threading.Event() - - def service_executed(call): - """ - Called when a service is executed. - Will set the event if matches our service call. - """ - if call.data[ATTR_SERVICE_CALL_ID] == call_id: - executed_event.set() - - self._bus.listen(EVENT_SERVICE_EXECUTED, service_executed) - - self._bus.fire(EVENT_CALL_SERVICE, event_data) - - if blocking: - success = executed_event.wait(SERVICE_CALL_LIMIT) - self._bus.remove_listener( - EVENT_SERVICE_EXECUTED, service_executed) - return success - - 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) - - if not self.has_service(domain, service): - return - - service_handler = self._services[domain][service] - service_call = ServiceCall(domain, service, service_data) - - # Add a job to the pool that calls _execute_service - self._pool.add_job(JobPriority.EVENT_SERVICE, - (self._execute_service, - (service_handler, service_call))) - - def _execute_service(self, service_and_call): - """ Executes a service and fires a SERVICE_EXECUTED event. """ - service, call = service_and_call - service(call) - - if ATTR_SERVICE_CALL_ID in call.data: - self._bus.fire( - EVENT_SERVICE_EXECUTED, - {ATTR_SERVICE_CALL_ID: call.data[ATTR_SERVICE_CALL_ID]}) - - def _generate_unique_id(self): - """ Generates a unique service call id. """ - self._cur_id += 1 - return "{}-{}".format(id(self), self._cur_id) - - -class Config(object): - """ Configuration settings for Home Assistant. """ - - # pylint: disable=too-many-instance-attributes - def __init__(self): - self.latitude = None - self.longitude = None - self.temperature_unit = None - self.location_name = None - self.time_zone = None - - # List of loaded components - self.components = [] - - # Remote.API object pointing at local API - self.api = None - - # Directory that holds the configuration - self.config_dir = os.path.join(os.getcwd(), 'config') - - def path(self, *path): - """ Returns path to the file within the config dir. """ - return os.path.join(self.config_dir, *path) - - def temperature(self, value, unit): - """ Converts temperature to user preferred unit if set. """ - if not (unit in (TEMP_CELCIUS, TEMP_FAHRENHEIT) and - self.temperature_unit and unit != self.temperature_unit): - return value, unit - - try: - if unit == TEMP_CELCIUS: - # Convert C to F - return round(float(value) * 1.8 + 32.0, 1), TEMP_FAHRENHEIT - - # Convert F to C - return round((float(value)-32.0)/1.8, 1), TEMP_CELCIUS - - except ValueError: - # Could not convert value to float - return value, unit - - def as_dict(self): - """ Converts config to a dictionary. """ - time_zone = self.time_zone or date_util.UTC - - return { - 'latitude': self.latitude, - 'longitude': self.longitude, - 'temperature_unit': self.temperature_unit, - 'location_name': self.location_name, - 'time_zone': time_zone.zone, - 'components': self.components, - } - - -class HomeAssistantError(Exception): - """ General Home Assistant exception occured. """ - pass - - -class InvalidEntityFormatError(HomeAssistantError): - """ When an invalid formatted entity is encountered. """ - pass - - -class NoEntitySpecifiedError(HomeAssistantError): - """ When no entity is specified. """ - pass - - -def create_timer(hass, interval=TIMER_INTERVAL): - """ Creates a timer. Timer will start on HOMEASSISTANT_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 % interval == 0, "60 % TIMER_INTERVAL should be 0!" - - def timer(): - """Send an EVENT_TIME_CHANGED on interval.""" - stop_event = threading.Event() - - def stop_timer(event): - """Stop the timer.""" - stop_event.set() - - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_timer) - - _LOGGER.info("Timer:starting") - - last_fired_on_second = -1 - - calc_now = date_util.utcnow - - while not stop_event.isSet(): - now = calc_now() - - # 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 % interval or \ - now.second == last_fired_on_second: - - # Sleep till it is the next time that we have to fire an event. - # Aim for halfway through the second that fits TIMER_INTERVAL. - # 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 = interval - now.second % interval + \ - .5 - now.microsecond/1000000.0 - - time.sleep(slp_seconds) - - now = calc_now() - - last_fired_on_second = now.second - - # Event might have been set while sleeping - if not stop_event.isSet(): - try: - hass.bus.fire(EVENT_TIME_CHANGED, {ATTR_NOW: now}) - except HomeAssistantError: - # HA raises error if firing event after it has shut down - break - - def start_timer(event): - """Start the timer.""" - thread = threading.Thread(target=timer) - thread.daemon = True - thread.start() - - hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_timer) - - -def create_worker_pool(worker_count=MIN_WORKER_THREAD): - """ Creates a worker pool to be used. """ - - def job_handler(job): - """ Called whenever a job is available to do. """ - try: - func, arg = job - func(arg) - except Exception: # pylint: disable=broad-except - # Catch any exception our service/event_listener might throw - # We do not want to crash our ThreadPool - _LOGGER.exception("BusHandler:Exception doing job") - - def busy_callback(worker_count, current_jobs, pending_jobs_count): - """ Callback to be called when the pool queue gets too big. """ - - _LOGGER.warning( - "WorkerPool:All %d threads are busy and %d jobs pending", - worker_count, pending_jobs_count) - - for start, job in current_jobs: - _LOGGER.warning("WorkerPool:Current job from %s: %s", - date_util.datetime_to_local_str(start), job) - - return util.ThreadPool(job_handler, worker_count, busy_callback) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 514c1adce57..e5f6d2b9672 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -13,7 +13,7 @@ import os import logging from collections import defaultdict -import homeassistant +import homeassistant.core as core import homeassistant.util.dt as date_util import homeassistant.util.package as pkg_util import homeassistant.util.location as loc_util @@ -152,9 +152,9 @@ def from_config_dict(config, hass=None): Dynamically loads required components and its dependencies. """ if hass is None: - hass = homeassistant.HomeAssistant() + hass = core.HomeAssistant() - process_ha_core_config(hass, config.get(homeassistant.DOMAIN, {})) + process_ha_core_config(hass, config.get(core.DOMAIN, {})) enable_logging(hass) @@ -168,7 +168,7 @@ def from_config_dict(config, hass=None): # Filter out the repeating and common config section [homeassistant] components = (key for key in config.keys() - if ' ' not in key and key != homeassistant.DOMAIN) + if ' ' not in key and key != core.DOMAIN) if not core_components.setup(hass, config): _LOGGER.error('Home Assistant core failed to initialize. ' @@ -192,7 +192,7 @@ def from_config_file(config_path, hass=None): instantiates a new Home Assistant object if 'hass' is not given. """ if hass is None: - hass = homeassistant.HomeAssistant() + hass = core.HomeAssistant() # Set config dir to directory holding config file hass.config.config_dir = os.path.abspath(os.path.dirname(config_path)) @@ -222,7 +222,8 @@ def enable_logging(hass): } )) except ImportError: - _LOGGER.warn("Colorlog package not found, console coloring disabled") + _LOGGER.warning( + "Colorlog package not found, console coloring disabled") # Log errors to a file if we have write access to file or config dir err_log_path = hass.config.path('home-assistant.log') diff --git a/homeassistant/components/__init__.py b/homeassistant/components/__init__.py index 0b757766bc0..e5e917c5250 100644 --- a/homeassistant/components/__init__.py +++ b/homeassistant/components/__init__.py @@ -17,7 +17,7 @@ Each component should publish services only under its own domain. import itertools as it import logging -import homeassistant as ha +import homeassistant.core as ha import homeassistant.util as util from homeassistant.helpers import extract_entity_ids from homeassistant.loader import get_component diff --git a/homeassistant/components/api.py b/homeassistant/components/api.py index b0d6faa2f49..108cc88741b 100644 --- a/homeassistant/components/api.py +++ b/homeassistant/components/api.py @@ -9,7 +9,7 @@ import logging import threading import json -import homeassistant as ha +import homeassistant.core as ha from homeassistant.helpers.state import TrackStates import homeassistant.remote as rem from homeassistant.const import ( diff --git a/homeassistant/components/conversation.py b/homeassistant/components/conversation.py index bf78e13a094..2d439a7ac4a 100644 --- a/homeassistant/components/conversation.py +++ b/homeassistant/components/conversation.py @@ -8,7 +8,7 @@ This is more a proof of concept. import logging import re -import homeassistant +from homeassistant import core from homeassistant.const import ( ATTR_FRIENDLY_NAME, ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF) @@ -52,16 +52,14 @@ def setup(hass, config): return if command == 'on': - hass.services.call( - homeassistant.DOMAIN, SERVICE_TURN_ON, { - ATTR_ENTITY_ID: entity_ids, - }, blocking=True) + hass.services.call(core.DOMAIN, SERVICE_TURN_ON, { + ATTR_ENTITY_ID: entity_ids, + }, blocking=True) elif command == 'off': - hass.services.call( - homeassistant.DOMAIN, SERVICE_TURN_OFF, { - ATTR_ENTITY_ID: entity_ids, - }, blocking=True) + hass.services.call(core.DOMAIN, SERVICE_TURN_OFF, { + ATTR_ENTITY_ID: entity_ids, + }, blocking=True) else: logger.error( diff --git a/homeassistant/components/demo.py b/homeassistant/components/demo.py index d0b8b155b4a..17d20571f62 100644 --- a/homeassistant/components/demo.py +++ b/homeassistant/components/demo.py @@ -6,7 +6,7 @@ Sets up a demo environment that mimics interaction with devices. """ import time -import homeassistant as ha +import homeassistant.core as ha import homeassistant.bootstrap as bootstrap import homeassistant.loader as loader from homeassistant.const import ( diff --git a/homeassistant/components/group.py b/homeassistant/components/group.py index 6975db4c46c..1d307baaca9 100644 --- a/homeassistant/components/group.py +++ b/homeassistant/components/group.py @@ -5,7 +5,7 @@ homeassistant.components.group Provides functionality to group devices that can be turned on or off. """ -import homeassistant as ha +import homeassistant.core as ha from homeassistant.helpers import generate_entity_id from homeassistant.helpers.event import track_state_change from homeassistant.helpers.entity import Entity diff --git a/homeassistant/components/http.py b/homeassistant/components/http.py index fcd17356885..a28def8e7ba 100644 --- a/homeassistant/components/http.py +++ b/homeassistant/components/http.py @@ -86,7 +86,7 @@ from http import cookies from socketserver import ThreadingMixIn from urllib.parse import urlparse, parse_qs -import homeassistant as ha +import homeassistant.core as ha from homeassistant.const import ( SERVER_PORT, CONTENT_TYPE_JSON, HTTP_HEADER_HA_AUTH, HTTP_HEADER_CONTENT_TYPE, HTTP_HEADER_ACCEPT_ENCODING, diff --git a/homeassistant/components/logbook.py b/homeassistant/components/logbook.py index acc9a36e494..c7a403f12ec 100644 --- a/homeassistant/components/logbook.py +++ b/homeassistant/components/logbook.py @@ -8,7 +8,7 @@ from datetime import timedelta from itertools import groupby import re -from homeassistant import State, DOMAIN as HA_DOMAIN +from homeassistant.core import State, DOMAIN as HA_DOMAIN from homeassistant.const import ( EVENT_STATE_CHANGED, STATE_HOME, STATE_ON, STATE_OFF, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, HTTP_BAD_REQUEST) diff --git a/homeassistant/components/mqtt.py b/homeassistant/components/mqtt.py index a892cc5015c..aa1a3167029 100644 --- a/homeassistant/components/mqtt.py +++ b/homeassistant/components/mqtt.py @@ -46,7 +46,7 @@ The keep alive in seconds for this client. Default is 60. import logging import socket -from homeassistant import HomeAssistantError +from homeassistant.core import HomeAssistantError import homeassistant.util as util from homeassistant.helpers import validate_config from homeassistant.const import ( diff --git a/homeassistant/components/recorder.py b/homeassistant/components/recorder.py index 5dca3bddde4..73487163425 100644 --- a/homeassistant/components/recorder.py +++ b/homeassistant/components/recorder.py @@ -13,7 +13,7 @@ from datetime import datetime, date import json import atexit -from homeassistant import Event, EventOrigin, State +from homeassistant.core import Event, EventOrigin, State import homeassistant.util.dt as date_util from homeassistant.remote import JSONEncoder from homeassistant.const import ( diff --git a/homeassistant/components/scene.py b/homeassistant/components/scene.py index a748e17ec5d..579ce1f20fb 100644 --- a/homeassistant/components/scene.py +++ b/homeassistant/components/scene.py @@ -18,7 +18,7 @@ old state will not be restored when it is being deactivated. import logging from collections import namedtuple -from homeassistant import State +from homeassistant.core import State from homeassistant.helpers.event import track_state_change from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.entity_component import EntityComponent diff --git a/homeassistant/config.py b/homeassistant/config.py index 3b2dd1ce740..6ae40e9e7c7 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -7,7 +7,7 @@ Module to help with parsing and generating configuration files. import logging import os -from homeassistant import HomeAssistantError +from homeassistant.core import HomeAssistantError from homeassistant.const import ( CONF_LATITUDE, CONF_LONGITUDE, CONF_TEMPERATURE_UNIT, CONF_NAME, CONF_TIME_ZONE) diff --git a/homeassistant/core.py b/homeassistant/core.py new file mode 100644 index 00000000000..afc4ea8375f --- /dev/null +++ b/homeassistant/core.py @@ -0,0 +1,802 @@ +""" +homeassistant +~~~~~~~~~~~~~ + +Home Assistant is a Home Automation framework for observing the state +of entities and react to changes. +""" + +import os +import time +import logging +import threading +import enum +import re +import functools as ft +from collections import namedtuple + +from homeassistant.const import ( + EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, + SERVICE_HOMEASSISTANT_STOP, EVENT_TIME_CHANGED, EVENT_STATE_CHANGED, + EVENT_CALL_SERVICE, ATTR_NOW, ATTR_DOMAIN, ATTR_SERVICE, MATCH_ALL, + EVENT_SERVICE_EXECUTED, ATTR_SERVICE_CALL_ID, EVENT_SERVICE_REGISTERED, + TEMP_CELCIUS, TEMP_FAHRENHEIT, ATTR_FRIENDLY_NAME) +import homeassistant.util as util +import homeassistant.util.dt as date_util + +DOMAIN = "homeassistant" + +# How often time_changed event should fire +TIMER_INTERVAL = 1 # seconds + +# How long we wait for the result of a service call +SERVICE_CALL_LIMIT = 10 # seconds + +# Define number of MINIMUM worker threads. +# During bootstrap of HA (see bootstrap._setup_component()) worker threads +# will be added for each component that polls devices. +MIN_WORKER_THREAD = 2 + +# Pattern for validating entity IDs (format: .) +ENTITY_ID_PATTERN = re.compile(r"^(?P\w+)\.(?P\w+)$") + +_LOGGER = logging.getLogger(__name__) + +# Temporary to support deprecated methods +_MockHA = namedtuple("MockHomeAssistant", ['bus']) + + +class HomeAssistant(object): + """ Core class to route all communication to right components. """ + + def __init__(self): + self.pool = pool = create_worker_pool() + self.bus = EventBus(pool) + self.services = ServiceRegistry(self.bus, pool) + self.states = StateMachine(self.bus) + self.config = Config() + + def start(self): + """ Start home assistant. """ + _LOGGER.info( + "Starting Home Assistant (%d threads)", self.pool.worker_count) + + create_timer(self) + self.bus.fire(EVENT_HOMEASSISTANT_START) + + def block_till_stopped(self): + """ Will register service homeassistant/stop and + will block until called. """ + request_shutdown = threading.Event() + + def stop_homeassistant(service): + """ Stops Home Assistant. """ + request_shutdown.set() + + self.services.register( + DOMAIN, SERVICE_HOMEASSISTANT_STOP, stop_homeassistant) + + while not request_shutdown.isSet(): + try: + time.sleep(1) + except KeyboardInterrupt: + break + + self.stop() + + def stop(self): + """ Stops Home Assistant and shuts down all threads. """ + _LOGGER.info("Stopping") + + self.bus.fire(EVENT_HOMEASSISTANT_STOP) + + # Wait till all responses to homeassistant_stop are done + self.pool.block_till_done() + + self.pool.stop() + + def track_point_in_time(self, action, point_in_time): + """Deprecated method as of 8/4/2015 to track point in time.""" + _LOGGER.warning( + 'hass.track_point_in_time is deprecated. ' + 'Please use homeassistant.helpers.event.track_point_in_time') + import homeassistant.helpers.event as helper + helper.track_point_in_time(self, action, point_in_time) + + def track_point_in_utc_time(self, action, point_in_time): + """Deprecated method as of 8/4/2015 to track point in UTC time.""" + _LOGGER.warning( + 'hass.track_point_in_utc_time is deprecated. ' + 'Please use homeassistant.helpers.event.track_point_in_utc_time') + import homeassistant.helpers.event as helper + helper.track_point_in_utc_time(self, action, point_in_time) + + def track_utc_time_change(self, action, + year=None, month=None, day=None, + hour=None, minute=None, second=None): + """Deprecated method as of 8/4/2015 to track UTC time change.""" + # pylint: disable=too-many-arguments + _LOGGER.warning( + 'hass.track_utc_time_change is deprecated. ' + 'Please use homeassistant.helpers.event.track_utc_time_change') + import homeassistant.helpers.event as helper + helper.track_utc_time_change(self, action, year, month, day, hour, + minute, second) + + def track_time_change(self, action, + year=None, month=None, day=None, + hour=None, minute=None, second=None, utc=False): + """Deprecated method as of 8/4/2015 to track time change.""" + # pylint: disable=too-many-arguments + _LOGGER.warning( + 'hass.track_time_change is deprecated. ' + 'Please use homeassistant.helpers.event.track_time_change') + import homeassistant.helpers.event as helper + helper.track_time_change(self, action, year, month, day, hour, + minute, second) + + +class JobPriority(util.OrderedEnum): + """ Provides priorities for bus events. """ + # pylint: disable=no-init,too-few-public-methods + + EVENT_CALLBACK = 0 + EVENT_SERVICE = 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 JobPriority.EVENT_TIME + elif event_type == EVENT_STATE_CHANGED: + return JobPriority.EVENT_STATE + elif event_type == EVENT_CALL_SERVICE: + return JobPriority.EVENT_SERVICE + elif event_type == EVENT_SERVICE_EXECUTED: + return JobPriority.EVENT_CALLBACK + else: + return JobPriority.EVENT_DEFAULT + + +class EventOrigin(enum.Enum): + """ Distinguish between origin of event. """ + # pylint: disable=no-init,too-few-public-methods + + local = "LOCAL" + remote = "REMOTE" + + def __str__(self): + return self.value + + +# pylint: disable=too-few-public-methods +class Event(object): + """ Represents an event within the Bus. """ + + __slots__ = ['event_type', 'data', 'origin', 'time_fired'] + + def __init__(self, event_type, data=None, origin=EventOrigin.local, + time_fired=None): + self.event_type = event_type + self.data = data or {} + self.origin = origin + self.time_fired = date_util.strip_microseconds( + time_fired or date_util.utcnow()) + + def as_dict(self): + """ Returns a dict representation of this Event. """ + return { + 'event_type': self.event_type, + 'data': dict(self.data), + 'origin': str(self.origin), + 'time_fired': date_util.datetime_to_str(self.time_fired), + } + + def __repr__(self): + # pylint: disable=maybe-no-member + if self.data: + return "".format( + self.event_type, str(self.origin)[0], + util.repr_helper(self.data)) + else: + return "".format(self.event_type, + str(self.origin)[0]) + + def __eq__(self, other): + return (self.__class__ == other.__class__ and + self.event_type == other.event_type and + self.data == other.data and + self.origin == other.origin and + self.time_fired == other.time_fired) + + +class EventBus(object): + """ Class that allows different components to communicate via services + and events. + """ + + def __init__(self, pool=None): + self._listeners = {} + self._lock = threading.Lock() + self._pool = pool or create_worker_pool() + + @property + def listeners(self): + """ Dict with events that is being listened for and the number + of listeners. + """ + with self._lock: + return {key: len(self._listeners[key]) + for key in self._listeners} + + def fire(self, event_type, event_data=None, origin=EventOrigin.local): + """ Fire an event. """ + if not self._pool.running: + raise HomeAssistantError('Home Assistant has shut down.') + + 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._listeners.get + listeners = get(MATCH_ALL, []) + get(event_type, []) + + event = Event(event_type, event_data, origin) + + if event_type != EVENT_TIME_CHANGED: + _LOGGER.info("Bus:Handling %s", event) + + if not listeners: + return + + job_priority = JobPriority.from_event_type(event_type) + + for func in listeners: + self._pool.add_job(job_priority, (func, event)) + + 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._lock: + if event_type in self._listeners: + self._listeners[event_type].append(listener) + else: + self._listeners[event_type] = [listener] + + def listen_once(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. + + Returns registered listener that can be used with remove_listener. + """ + @ft.wraps(listener) + def onetime_listener(event): + """ Removes listener from eventbus and then fires listener. """ + if hasattr(onetime_listener, 'run'): + return + # 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.remove_listener(event_type, onetime_listener) + + listener(event) + + self.listen(event_type, onetime_listener) + + return onetime_listener + + def remove_listener(self, event_type, listener): + """ Removes a listener of a specific event_type. """ + with self._lock: + try: + self._listeners[event_type].remove(listener) + + # delete event_type list if empty + if not self._listeners[event_type]: + self._listeners.pop(event_type) + + except (KeyError, ValueError): + # KeyError is key event_type listener did not exist + # ValueError if listener did not exist within event_type + pass + + +class State(object): + """ + Object to represent a state within the state machine. + + entity_id: the entity that is represented. + state: the state of the entity + attributes: extra information on entity and state + last_changed: last time the state was changed, not the attributes. + last_updated: last time this object was updated. + """ + + __slots__ = ['entity_id', 'state', 'attributes', + 'last_changed', 'last_updated'] + + # pylint: disable=too-many-arguments + def __init__(self, entity_id, state, attributes=None, last_changed=None, + last_updated=None): + if not ENTITY_ID_PATTERN.match(entity_id): + raise InvalidEntityFormatError(( + "Invalid entity id encountered: {}. " + "Format should be .").format(entity_id)) + + self.entity_id = entity_id.lower() + self.state = state + self.attributes = attributes or {} + self.last_updated = date_util.strip_microseconds( + last_updated or date_util.utcnow()) + + # Strip microsecond from last_changed else we cannot guarantee + # state == State.from_dict(state.as_dict()) + # This behavior occurs because to_dict uses datetime_to_str + # which does not preserve microseconds + self.last_changed = date_util.strip_microseconds( + last_changed or self.last_updated) + + @property + def domain(self): + """ Returns domain of this state. """ + return util.split_entity_id(self.entity_id)[0] + + @property + def object_id(self): + """ Returns object_id of this state. """ + return util.split_entity_id(self.entity_id)[1] + + @property + def name(self): + """ Name to represent this state. """ + return ( + self.attributes.get(ATTR_FRIENDLY_NAME) or + self.object_id.replace('_', ' ')) + + def copy(self): + """ Creates a copy of itself. """ + return State(self.entity_id, self.state, + dict(self.attributes), self.last_changed) + + def as_dict(self): + """ Converts State to a dict to be used within JSON. + Ensures: state == State.from_dict(state.as_dict()) """ + + return {'entity_id': self.entity_id, + 'state': self.state, + 'attributes': self.attributes, + 'last_changed': date_util.datetime_to_str(self.last_changed), + 'last_updated': date_util.datetime_to_str(self.last_updated)} + + @classmethod + def from_dict(cls, json_dict): + """ Static method to create a state from a dict. + Ensures: state == State.from_json_dict(state.to_json_dict()) """ + + if not (json_dict and + 'entity_id' in json_dict and + 'state' in json_dict): + return None + + last_changed = json_dict.get('last_changed') + + if last_changed: + last_changed = date_util.str_to_datetime(last_changed) + + last_updated = json_dict.get('last_updated') + + if last_updated: + last_updated = date_util.str_to_datetime(last_updated) + + return cls(json_dict['entity_id'], json_dict['state'], + json_dict.get('attributes'), last_changed, last_updated) + + def __eq__(self, other): + return (self.__class__ == other.__class__ and + self.entity_id == other.entity_id and + self.state == other.state and + self.attributes == other.attributes) + + def __repr__(self): + attr = "; {}".format(util.repr_helper(self.attributes)) \ + if self.attributes else "" + + return "".format( + self.entity_id, self.state, attr, + date_util.datetime_to_local_str(self.last_changed)) + + +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() + + def entity_ids(self, domain_filter=None): + """ List of entity ids that are being tracked. """ + if domain_filter is None: + return list(self._states.keys()) + + domain_filter = domain_filter.lower() + + return [state.entity_id for key, state + in self._states.items() + if util.split_entity_id(key)[0] == domain_filter] + + def all(self): + """ Returns a list of all states. """ + with self._lock: + return [state.copy() for state in self._states.values()] + + def get(self, entity_id): + """ Returns the state of the specified entity. """ + state = self._states.get(entity_id.lower()) + + # 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. """ + entity_id = entity_id.lower() + + return (entity_id in self._states and + self._states[entity_id].state == state) + + def remove(self, entity_id): + """ Removes an entity from the state machine. + + Returns boolean to indicate if an entity was removed. """ + entity_id = entity_id.lower() + + with self._lock: + return self._states.pop(entity_id, None) is not 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. + + If you just update the attributes and not the state, last changed will + not be affected. + """ + entity_id = entity_id.lower() + new_state = str(new_state) + attributes = attributes or {} + + with self._lock: + old_state = self._states.get(entity_id) + + is_existing = old_state is not None + same_state = is_existing and old_state.state == new_state + same_attr = is_existing and old_state.attributes == attributes + + if same_state and same_attr: + return + + # If state did not exist or is different, set it + last_changed = old_state.last_changed if same_state else None + + state = State(entity_id, new_state, attributes, last_changed) + self._states[entity_id] = state + + event_data = {'entity_id': entity_id, 'new_state': state} + + if old_state: + event_data['old_state'] = old_state + + self._bus.fire(EVENT_STATE_CHANGED, event_data) + + def track_change(self, entity_ids, action, from_state=None, to_state=None): + """ + DEPRECATED AS OF 8/4/2015 + """ + _LOGGER.warning( + 'hass.states.track_change is deprecated. ' + 'Use homeassistant.helpers.event.track_state_change instead.') + import homeassistant.helpers.event as helper + helper.track_state_change(_MockHA(self._bus), entity_ids, action, + from_state, to_state) + + +# 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() + self._bus = bus + self._cur_id = 0 + 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} + + self._bus.fire( + EVENT_SERVICE_REGISTERED, + {ATTR_DOMAIN: domain, ATTR_SERVICE: service}) + + def call(self, domain, service, service_data=None, blocking=False): + """ + Calls specified service. + Specify blocking=True to wait till service is executed. + Waits a maximum of SERVICE_CALL_LIMIT. + + If blocking = True, will return boolean if service executed + succesfully within SERVICE_CALL_LIMIT. + + This method will fire an event to call the service. + This event will be picked up by this ServiceRegistry and any + other ServiceRegistry that is listening on the EventBus. + + Because the service is sent as an event you are not allowed to use + the keys ATTR_DOMAIN and ATTR_SERVICE in your service_data. + """ + call_id = self._generate_unique_id() + event_data = service_data or {} + event_data[ATTR_DOMAIN] = domain + event_data[ATTR_SERVICE] = service + event_data[ATTR_SERVICE_CALL_ID] = call_id + + if blocking: + executed_event = threading.Event() + + def service_executed(call): + """ + Called when a service is executed. + Will set the event if matches our service call. + """ + if call.data[ATTR_SERVICE_CALL_ID] == call_id: + executed_event.set() + + self._bus.listen(EVENT_SERVICE_EXECUTED, service_executed) + + self._bus.fire(EVENT_CALL_SERVICE, event_data) + + if blocking: + success = executed_event.wait(SERVICE_CALL_LIMIT) + self._bus.remove_listener( + EVENT_SERVICE_EXECUTED, service_executed) + return success + + 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) + + if not self.has_service(domain, service): + return + + service_handler = self._services[domain][service] + service_call = ServiceCall(domain, service, service_data) + + # Add a job to the pool that calls _execute_service + self._pool.add_job(JobPriority.EVENT_SERVICE, + (self._execute_service, + (service_handler, service_call))) + + def _execute_service(self, service_and_call): + """ Executes a service and fires a SERVICE_EXECUTED event. """ + service, call = service_and_call + service(call) + + if ATTR_SERVICE_CALL_ID in call.data: + self._bus.fire( + EVENT_SERVICE_EXECUTED, + {ATTR_SERVICE_CALL_ID: call.data[ATTR_SERVICE_CALL_ID]}) + + def _generate_unique_id(self): + """ Generates a unique service call id. """ + self._cur_id += 1 + return "{}-{}".format(id(self), self._cur_id) + + +class Config(object): + """ Configuration settings for Home Assistant. """ + + # pylint: disable=too-many-instance-attributes + def __init__(self): + self.latitude = None + self.longitude = None + self.temperature_unit = None + self.location_name = None + self.time_zone = None + + # List of loaded components + self.components = [] + + # Remote.API object pointing at local API + self.api = None + + # Directory that holds the configuration + self.config_dir = os.path.join(os.getcwd(), 'config') + + def path(self, *path): + """ Returns path to the file within the config dir. """ + return os.path.join(self.config_dir, *path) + + def temperature(self, value, unit): + """ Converts temperature to user preferred unit if set. """ + if not (unit in (TEMP_CELCIUS, TEMP_FAHRENHEIT) and + self.temperature_unit and unit != self.temperature_unit): + return value, unit + + try: + if unit == TEMP_CELCIUS: + # Convert C to F + return round(float(value) * 1.8 + 32.0, 1), TEMP_FAHRENHEIT + + # Convert F to C + return round((float(value)-32.0)/1.8, 1), TEMP_CELCIUS + + except ValueError: + # Could not convert value to float + return value, unit + + def as_dict(self): + """ Converts config to a dictionary. """ + time_zone = self.time_zone or date_util.UTC + + return { + 'latitude': self.latitude, + 'longitude': self.longitude, + 'temperature_unit': self.temperature_unit, + 'location_name': self.location_name, + 'time_zone': time_zone.zone, + 'components': self.components, + } + + +class HomeAssistantError(Exception): + """ General Home Assistant exception occured. """ + pass + + +class InvalidEntityFormatError(HomeAssistantError): + """ When an invalid formatted entity is encountered. """ + pass + + +class NoEntitySpecifiedError(HomeAssistantError): + """ When no entity is specified. """ + pass + + +def create_timer(hass, interval=TIMER_INTERVAL): + """ Creates a timer. Timer will start on HOMEASSISTANT_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 % interval == 0, "60 % TIMER_INTERVAL should be 0!" + + def timer(): + """Send an EVENT_TIME_CHANGED on interval.""" + stop_event = threading.Event() + + def stop_timer(event): + """Stop the timer.""" + stop_event.set() + + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_timer) + + _LOGGER.info("Timer:starting") + + last_fired_on_second = -1 + + calc_now = date_util.utcnow + + while not stop_event.isSet(): + now = calc_now() + + # 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 % interval or \ + now.second == last_fired_on_second: + + # Sleep till it is the next time that we have to fire an event. + # Aim for halfway through the second that fits TIMER_INTERVAL. + # 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 = interval - now.second % interval + \ + .5 - now.microsecond/1000000.0 + + time.sleep(slp_seconds) + + now = calc_now() + + last_fired_on_second = now.second + + # Event might have been set while sleeping + if not stop_event.isSet(): + try: + hass.bus.fire(EVENT_TIME_CHANGED, {ATTR_NOW: now}) + except HomeAssistantError: + # HA raises error if firing event after it has shut down + break + + def start_timer(event): + """Start the timer.""" + thread = threading.Thread(target=timer) + thread.daemon = True + thread.start() + + hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_timer) + + +def create_worker_pool(worker_count=MIN_WORKER_THREAD): + """ Creates a worker pool to be used. """ + + def job_handler(job): + """ Called whenever a job is available to do. """ + try: + func, arg = job + func(arg) + except Exception: # pylint: disable=broad-except + # Catch any exception our service/event_listener might throw + # We do not want to crash our ThreadPool + _LOGGER.exception("BusHandler:Exception doing job") + + def busy_callback(worker_count, current_jobs, pending_jobs_count): + """ Callback to be called when the pool queue gets too big. """ + + _LOGGER.warning( + "WorkerPool:All %d threads are busy and %d jobs pending", + worker_count, pending_jobs_count) + + for start, job in current_jobs: + _LOGGER.warning("WorkerPool:Current job from %s: %s", + date_util.datetime_to_local_str(start), job) + + return util.ThreadPool(job_handler, worker_count, busy_callback) diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index f9751ffc14c..0ca63856c27 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -7,7 +7,7 @@ Provides ABC for entities in HA. from collections import defaultdict -from homeassistant import NoEntitySpecifiedError +from homeassistant.core import NoEntitySpecifiedError from homeassistant.const import ( ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT, ATTR_HIDDEN, diff --git a/homeassistant/helpers/state.py b/homeassistant/helpers/state.py index 66e9a448d8e..d87ee48930c 100644 --- a/homeassistant/helpers/state.py +++ b/homeassistant/helpers/state.py @@ -6,7 +6,7 @@ Helpers that help with state related things. """ import logging -from homeassistant import State +from homeassistant.core import State import homeassistant.util.dt as dt_util from homeassistant.const import ( STATE_ON, STATE_OFF, SERVICE_TURN_ON, SERVICE_TURN_OFF, ATTR_ENTITY_ID) diff --git a/homeassistant/remote.py b/homeassistant/remote.py index 5a0a828bb21..2488f0a9c46 100644 --- a/homeassistant/remote.py +++ b/homeassistant/remote.py @@ -17,7 +17,7 @@ import urllib.parse import requests -import homeassistant as ha +import homeassistant.core as ha import homeassistant.bootstrap as bootstrap from homeassistant.const import ( diff --git a/tests/common.py b/tests/common.py index d6dd6cd3279..be6aa623a25 100644 --- a/tests/common.py +++ b/tests/common.py @@ -8,7 +8,7 @@ import os from datetime import timedelta from unittest import mock -import homeassistant as ha +import homeassistant.core as ha import homeassistant.util.location as location_util import homeassistant.util.dt as dt_util from homeassistant.helpers.entity import ToggleEntity diff --git a/tests/components/automation/test_event.py b/tests/components/automation/test_event.py index dc685fa944d..a2c36283c9a 100644 --- a/tests/components/automation/test_event.py +++ b/tests/components/automation/test_event.py @@ -6,7 +6,7 @@ Tests demo component. """ import unittest -import homeassistant as ha +import homeassistant.core as ha import homeassistant.components.automation as automation import homeassistant.components.automation.event as event from homeassistant.const import CONF_PLATFORM diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index 4bd0f4b96f3..2af17ea405c 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -6,7 +6,7 @@ Tests demo component. """ import unittest -import homeassistant as ha +import homeassistant.core as ha import homeassistant.components.automation as automation import homeassistant.components.automation.event as event from homeassistant.const import CONF_PLATFORM, ATTR_ENTITY_ID diff --git a/tests/components/automation/test_mqtt.py b/tests/components/automation/test_mqtt.py index 1fdc74bf3ce..9402b5300b6 100644 --- a/tests/components/automation/test_mqtt.py +++ b/tests/components/automation/test_mqtt.py @@ -6,7 +6,7 @@ Tests demo component. """ import unittest -import homeassistant as ha +import homeassistant.core as ha import homeassistant.components.automation as automation import homeassistant.components.automation.mqtt as mqtt from homeassistant.const import CONF_PLATFORM diff --git a/tests/components/automation/test_state.py b/tests/components/automation/test_state.py index 5966946b278..47d612cbb02 100644 --- a/tests/components/automation/test_state.py +++ b/tests/components/automation/test_state.py @@ -6,7 +6,7 @@ Tests demo component. """ import unittest -import homeassistant as ha +import homeassistant.core as ha import homeassistant.components.automation as automation import homeassistant.components.automation.state as state from homeassistant.const import CONF_PLATFORM diff --git a/tests/components/automation/test_time.py b/tests/components/automation/test_time.py index 5581cc01348..0f11a2a67c5 100644 --- a/tests/components/automation/test_time.py +++ b/tests/components/automation/test_time.py @@ -6,7 +6,7 @@ Tests demo component. """ import unittest -import homeassistant as ha +import homeassistant.core as ha import homeassistant.loader as loader import homeassistant.util.dt as dt_util import homeassistant.components.automation as automation diff --git a/tests/components/test_api.py b/tests/components/test_api.py index ff25b476d32..93b1cd06abe 100644 --- a/tests/components/test_api.py +++ b/tests/components/test_api.py @@ -10,7 +10,7 @@ import json import requests -import homeassistant as ha +import homeassistant.core as ha import homeassistant.bootstrap as bootstrap import homeassistant.remote as remote import homeassistant.components.http as http diff --git a/tests/components/test_configurator.py b/tests/components/test_configurator.py index c64fc39e50a..f41a5319ffd 100644 --- a/tests/components/test_configurator.py +++ b/tests/components/test_configurator.py @@ -7,7 +7,7 @@ Tests Configurator component. # pylint: disable=too-many-public-methods,protected-access import unittest -import homeassistant as ha +import homeassistant.core as ha import homeassistant.components.configurator as configurator from homeassistant.const import EVENT_TIME_CHANGED diff --git a/tests/components/test_demo.py b/tests/components/test_demo.py index 9e697fb0c74..0abd546e4c4 100644 --- a/tests/components/test_demo.py +++ b/tests/components/test_demo.py @@ -6,7 +6,7 @@ Tests demo component. """ import unittest -import homeassistant as ha +import homeassistant.core as ha import homeassistant.components.demo as demo from tests.common import mock_http_component diff --git a/tests/components/test_device_tracker.py b/tests/components/test_device_tracker.py index bbc647987c9..66fd97c4730 100644 --- a/tests/components/test_device_tracker.py +++ b/tests/components/test_device_tracker.py @@ -10,7 +10,7 @@ from datetime import timedelta import logging import os -import homeassistant as ha +import homeassistant.core as ha import homeassistant.loader as loader import homeassistant.util.dt as dt_util from homeassistant.const import ( diff --git a/tests/components/test_frontend.py b/tests/components/test_frontend.py index d6431f5f5df..65fcb5b6091 100644 --- a/tests/components/test_frontend.py +++ b/tests/components/test_frontend.py @@ -10,7 +10,7 @@ import unittest import requests -import homeassistant as ha +import homeassistant.core as ha import homeassistant.bootstrap as bootstrap import homeassistant.components.http as http from homeassistant.const import HTTP_HEADER_HA_AUTH diff --git a/tests/components/test_group.py b/tests/components/test_group.py index 22256057d7a..d66a24606a3 100644 --- a/tests/components/test_group.py +++ b/tests/components/test_group.py @@ -8,7 +8,7 @@ Tests the group compoments. import unittest import logging -import homeassistant as ha +import homeassistant.core as ha from homeassistant.const import STATE_ON, STATE_OFF, STATE_HOME, STATE_UNKNOWN import homeassistant.components.group as group diff --git a/tests/components/test_history.py b/tests/components/test_history.py index 675e2d022d9..12d10c52744 100644 --- a/tests/components/test_history.py +++ b/tests/components/test_history.py @@ -9,7 +9,7 @@ import time import os import unittest -import homeassistant as ha +import homeassistant.core as ha import homeassistant.util.dt as dt_util from homeassistant.components import history, recorder diff --git a/tests/components/test_init.py b/tests/components/test_init.py index 8c00616bbb4..0074b75e148 100644 --- a/tests/components/test_init.py +++ b/tests/components/test_init.py @@ -7,7 +7,7 @@ Tests core compoments. # pylint: disable=protected-access,too-many-public-methods import unittest -import homeassistant as ha +import homeassistant.core as ha import homeassistant.loader as loader from homeassistant.const import ( STATE_ON, STATE_OFF, SERVICE_TURN_ON, SERVICE_TURN_OFF) diff --git a/tests/components/test_logbook.py b/tests/components/test_logbook.py index cd415590d2d..16f6ba8aa33 100644 --- a/tests/components/test_logbook.py +++ b/tests/components/test_logbook.py @@ -8,7 +8,7 @@ Tests the logbook component. import unittest from datetime import timedelta -import homeassistant as ha +import homeassistant.core as ha from homeassistant.const import ( EVENT_STATE_CHANGED, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) import homeassistant.util.dt as dt_util diff --git a/tests/components/test_media_player.py b/tests/components/test_media_player.py index f3f0128af3e..1fd406dc026 100644 --- a/tests/components/test_media_player.py +++ b/tests/components/test_media_player.py @@ -8,7 +8,7 @@ Tests media_player component. import logging import unittest -import homeassistant as ha +import homeassistant.core as ha from homeassistant.const import ( STATE_OFF, SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_VOLUME_UP, SERVICE_VOLUME_DOWN, diff --git a/tests/components/test_sun.py b/tests/components/test_sun.py index 705caadcd3a..9d2ae38fdd6 100644 --- a/tests/components/test_sun.py +++ b/tests/components/test_sun.py @@ -10,7 +10,7 @@ from datetime import timedelta from astral import Astral -import homeassistant as ha +import homeassistant.core as ha import homeassistant.util.dt as dt_util import homeassistant.components.sun as sun diff --git a/tests/helpers/test_entity.py b/tests/helpers/test_entity.py index 14559ded39a..b8823f23a5a 100644 --- a/tests/helpers/test_entity.py +++ b/tests/helpers/test_entity.py @@ -7,7 +7,7 @@ Tests the entity helper. # pylint: disable=protected-access,too-many-public-methods import unittest -import homeassistant as ha +import homeassistant.core as ha import homeassistant.helpers.entity as entity from homeassistant.const import ATTR_HIDDEN diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py index 69338cf431b..89711e2584e 100644 --- a/tests/helpers/test_event.py +++ b/tests/helpers/test_event.py @@ -9,7 +9,7 @@ Tests event helpers. import unittest from datetime import datetime -import homeassistant as ha +import homeassistant.core as ha from homeassistant.helpers.event import * diff --git a/tests/helpers/test_init.py b/tests/helpers/test_init.py index 9257dd634ef..c1af6ba8ccc 100644 --- a/tests/helpers/test_init.py +++ b/tests/helpers/test_init.py @@ -9,7 +9,7 @@ import unittest from common import get_test_home_assistant -import homeassistant as ha +import homeassistant.core as ha import homeassistant.loader as loader from homeassistant.const import STATE_ON, STATE_OFF, ATTR_ENTITY_ID from homeassistant.helpers import extract_entity_ids diff --git a/tests/test_config.py b/tests/test_config.py index b8d050d14ae..f683fac890c 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -9,7 +9,7 @@ import unittest import unittest.mock as mock import os -from homeassistant import DOMAIN, HomeAssistantError +from homeassistant.core import DOMAIN, HomeAssistantError import homeassistant.config as config_util from homeassistant.const import ( CONF_LATITUDE, CONF_LONGITUDE, CONF_TEMPERATURE_UNIT, CONF_NAME, diff --git a/tests/test_init.py b/tests/test_core.py similarity index 99% rename from tests/test_init.py rename to tests/test_core.py index 4d484be580e..6e7b52795b2 100644 --- a/tests/test_init.py +++ b/tests/test_core.py @@ -15,7 +15,7 @@ from datetime import datetime import pytz -import homeassistant as ha +import homeassistant.core as ha import homeassistant.util.dt as dt_util from homeassistant.helpers.event import track_state_change from homeassistant.const import ( diff --git a/tests/test_remote.py b/tests/test_remote.py index 7c00cbfd526..0f45091d598 100644 --- a/tests/test_remote.py +++ b/tests/test_remote.py @@ -9,7 +9,7 @@ Uses port 8125 as a port that nothing runs on # pylint: disable=protected-access,too-many-public-methods import unittest -import homeassistant as ha +import homeassistant.core as ha import homeassistant.bootstrap as bootstrap import homeassistant.remote as remote import homeassistant.components.http as http