1
mirror of https://github.com/home-assistant/core synced 2024-07-12 07:21:24 +02:00

Re-organized core for better reusability

This commit is contained in:
Paulus Schoutsen 2014-04-24 00:40:45 -07:00
parent 3757ddf9df
commit 2e10d7223a
19 changed files with 772 additions and 754 deletions

View File

@ -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

View File

@ -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 "<ServiceCall {}.{}: {}>".format(
self.domain, self.service, util.repr_helper(self.data))
else:
return "<ServiceCall {}.{}>".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 "<Event {}>".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 "<state {}:{} @ {}>".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 "<ServiceCall {}.{}: {}>".format(
self.domain, self.service, util.repr_helper(self.data))
else:
return "<ServiceCall {}.{}>".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. """

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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):
"</tr>").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 = "<br>".join(
["{}: {}".format(attr, state.attributes[attr])
@ -372,7 +370,7 @@ class RequestHandler(BaseHTTPRequestHandler):
"<tr><th>Domain</th><th>Service</th></tr>"))
for domain, services in sorted(
self.server.bus.services.items()):
self.server.hass.services.services.items()):
write("<tr><td>{}</td><td>{}</td></tr>".format(
domain, ", ".join(services)))
@ -435,7 +433,7 @@ class RequestHandler(BaseHTTPRequestHandler):
"<tr><th>Event</th><th>Listeners</th></tr>"))
for event, listener_count in sorted(
self.server.bus.event_listeners.items()):
self.server.hass.bus.listeners.items()):
write("<tr><td>{}</td><td>{}</td></tr>".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. """

View File

@ -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

View File

@ -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 = {}

View File

@ -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)

View File

@ -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

View File

@ -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))

View File

@ -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)

View File

@ -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. """

View File

@ -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()