Integrated core.py into module-init

This commit is contained in:
Paulus Schoutsen 2013-09-30 00:20:27 -07:00
parent a75f396242
commit a491df761f
7 changed files with 246 additions and 297 deletions

View File

@ -6,70 +6,242 @@ Module to control the lights based on devices at home and the state of the sun.
"""
import logging
import time
import logging
import threading
from collections import defaultdict, namedtuple
from itertools import chain
from datetime import datetime
from .core import EventBus, StateMachine, Event, EVENT_START, EVENT_SHUTDOWN
from .httpinterface import HTTPInterface
from .observers import DeviceTracker, WeatherWatcher, Timer
from .actors import LightTrigger
ALL_EVENTS = '*'
EVENT_START = "start"
EVENT_SHUTDOWN = "shutdown"
EVENT_STATE_CHANGED = "state_changed"
EVENT_TIME_CHANGED = "time_changed"
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!"
class HomeAssistant(object):
""" Class to tie all modules together and handle dependencies. """
State = namedtuple("State", ['state','last_changed'])
def __init__(self, latitude=None, longitude=None):
self.latitude = latitude
self.longitude = longitude
def start_home_assistant(eventbus):
""" Start home assistant. """
Timer(eventbus)
eventbus.fire(Event(EVENT_START))
while True:
try:
time.sleep(1)
except KeyboardInterrupt:
print ""
eventbus.fire(Event(EVENT_SHUTDOWN))
break
def ensure_list(parameter):
""" Wraps parameter in a list if it is not one and returns it. """
return parameter if isinstance(parameter, list) else [parameter]
def matcher(subject, pattern):
""" Returns True if subject matches the pattern.
Pattern is either a list of allowed subjects or a '*'. """
return '*' in pattern or subject in pattern
def track_state_change(eventbus, category, from_state, to_state, action):
""" Helper method to track specific state changes. """
from_state = ensure_list(from_state)
to_state = ensure_list(to_state)
def listener(event):
""" State change listener that listens for specific state changes. """
assert isinstance(event, Event), "event needs to be of Event type"
if category == event.data['category'] and \
matcher(event.data['old_state'].state, from_state) and \
matcher(event.data['new_state'].state, to_state):
action(event.data['category'], event.data['old_state'], event.data['new_state'])
eventbus.listen(EVENT_STATE_CHANGED, listener)
def track_time_change(eventbus, action, year='*', month='*', day='*', hour='*', minute='*', second='*', point_in_time=None, listen_once=False):
""" Adds a listener that will listen for a specified or matching time. """
year, month, day = ensure_list(year), ensure_list(month), ensure_list(day)
hour, minute, second = ensure_list(hour), ensure_list(minute), ensure_list(second)
def listener(event):
""" Listens for matching time_changed events. """
assert isinstance(event, Event), "event needs to be of Event type"
if (point_in_time is not None and event.data['now'] > point_in_time) or \
(point_in_time is None and \
matcher(event.data['now'].year, year) and \
matcher(event.data['now'].month, month) and \
matcher(event.data['now'].day, day) and \
matcher(event.data['now'].hour, hour) and \
matcher(event.data['now'].minute, minute) and \
matcher(event.data['now'].second, second)):
# point_in_time are exact points in time so we always remove it after fire
event.remove_listener = listen_once or point_in_time is not None
action(event.data['now'])
eventbus.listen(EVENT_TIME_CHANGED, listener)
class EventBus(object):
""" Class provides an eventbus. Allows code to listen for events and fire them. """
def __init__(self):
self.listeners = defaultdict(list)
self.lock = threading.RLock()
self.logger = logging.getLogger(__name__)
self.eventbus = EventBus()
self.statemachine = StateMachine(self.eventbus)
def fire(self, event):
""" Fire an event. """
assert isinstance(event, Event), "event needs to be an instance of Event"
self.httpinterface = None
self.weatherwatcher = None
def run():
""" We dont want the eventbus to be blocking,
We dont want the eventbus to crash when one of its listeners throws an Exception
So run in a thread. """
self.lock.acquire()
def setup_light_trigger(self, device_scanner, light_control):
""" Sets up the light trigger system. """
self.logger.info("Setting up light trigger")
self.logger.info("EventBus:Event {}: {}".format(event.event_type, event.data))
devicetracker = DeviceTracker(self.eventbus, self.statemachine, device_scanner)
for callback in chain(self.listeners[ALL_EVENTS], self.listeners[event.event_type]):
callback(event)
LightTrigger(self.eventbus, self.statemachine, self._setup_weather_watcher(), devicetracker, light_control)
if event.remove_listener:
if callback in self.listeners[ALL_EVENTS]:
self.listeners[ALL_EVENTS].remove(callback)
if callback in self.listeners[event.event_type]:
self.listeners[event.event_type].remove(callback)
def setup_http_interface(self, api_password):
""" Sets up the HTTP interface. """
if self.httpinterface is None:
self.logger.info("Setting up HTTP interface")
self.httpinterface = HTTPInterface(self.eventbus, self.statemachine, api_password)
event.remove_listener = False
return self.httpinterface
if event.stop_propegating:
break
self.lock.release()
def start(self):
""" Start home assistant. """
Timer(self.eventbus)
threading.Thread(target=run).start()
self.eventbus.fire(Event(EVENT_START))
def listen(self, event_type, callback):
""" Listen for all events or events of a specific type.
To listen to all events specify the constant ``ALL_EVENTS`` as event_type. """
self.lock.acquire()
self.listeners[event_type].append(callback)
self.lock.release()
class Event(object):
""" An event to be sent over the eventbus. """
def __init__(self, event_type, data=None):
self.event_type = event_type
self.data = {} if data is None else data
self.stop_propegating = False
self.remove_listener = False
def __str__(self):
return str([self.event_type, self.data])
class StateMachine(object):
""" Helper class that tracks the state of different objects. """
def __init__(self, eventbus):
self.states = dict()
self.eventbus = eventbus
self.lock = threading.RLock()
def set_state(self, category, new_state):
""" Set the state of a category, add category is it does not exist. """
self.lock.acquire()
# Add category is it does not exist
if category not in self.states:
self.states[category] = State(new_state, datetime.now())
# Change state and fire listeners
else:
old_state = self.states[category]
if old_state.state != new_state:
self.states[category] = State(new_state, datetime.now())
self.eventbus.fire(Event(EVENT_STATE_CHANGED, {'category':category, 'old_state':old_state, 'new_state':self.states[category]}))
self.lock.release()
def is_state(self, category, state):
""" Returns True if category is specified state. """
self._validate_category(category)
return self.get_state(category).state == state
def get_state(self, category):
""" Returns a tuple (state,last_changed) describing the state of the specified category. """
self._validate_category(category)
return self.states[category]
def get_states(self):
""" Returns a list of tuples (category, state, last_changed) sorted by category. """
return [(category, self.states[category].state, self.states[category].last_changed) for category in sorted(self.states.keys())]
def _validate_category(self, category):
""" Helper function to throw an exception when the category does not exist. """
if category not in self.states:
raise CategoryDoesNotExistException("Category {} does not exist.".format(category))
class Timer(threading.Thread):
""" Timer will sent out an event every TIMER_INTERVAL seconds. """
def __init__(self, eventbus):
threading.Thread.__init__(self)
self.eventbus = eventbus
self._stop = threading.Event()
eventbus.listen(EVENT_START, lambda event: self.start())
eventbus.listen(EVENT_SHUTDOWN, lambda event: self._stop.set())
def run(self):
""" Start the timer. """
logging.getLogger(__name__).info("Timer:starting")
now = datetime.now()
while True:
try:
while True:
time.sleep(1)
except KeyboardInterrupt:
print ""
self.eventbus.fire(Event(EVENT_SHUTDOWN))
now = datetime.now()
if self._stop.isSet() or now.second % TIMER_INTERVAL == 0:
break
if self._stop.isSet():
break
def _setup_weather_watcher(self):
""" Sets up the weather watcher. """
if self.weatherwatcher is None:
self.weatherwatcher = WeatherWatcher(self.eventbus, self.statemachine, self.latitude, self.longitude)
self.eventbus.fire(Event(EVENT_TIME_CHANGED, {'now':now}))
return self.weatherwatcher
class HomeAssistantException(Exception):
""" General Home Assistant exception occured. """
class CategoryDoesNotExistException(HomeAssistantException):
""" Specified category does not exist within the state machine. """

View File

@ -11,7 +11,7 @@ from datetime import timedelta
from phue import Bridge
from .core import track_state_change
from . import track_state_change
from .observers import (STATE_CATEGORY_SUN, SUN_STATE_BELOW_HORIZON, SUN_STATE_ABOVE_HORIZON,
STATE_CATEGORY_ALL_DEVICES, DEVICE_STATE_HOME, DEVICE_STATE_NOT_HOME,

View File

@ -1,164 +0,0 @@
"""
homeassistant.common
~~~~~~~~~~~~~~~~~~~~~~~~~~~
This module provides the core components of homeassistant.
"""
import logging
from collections import defaultdict, namedtuple
from itertools import chain
from threading import Thread, RLock
from datetime import datetime
ALL_EVENTS = '*'
EVENT_STATE_CHANGED = "state_changed"
EVENT_START = "start"
EVENT_SHUTDOWN = "shutdown"
State = namedtuple("State", ['state','last_changed'])
def ensure_list(parameter):
""" Wraps parameter in a list if it is not one and returns it. """
return parameter if isinstance(parameter, list) else [parameter]
def matcher(subject, pattern):
""" Returns True if subject matches the pattern.
Pattern is either a list of allowed subjects or a '*'. """
return '*' in pattern or subject in pattern
def track_state_change(eventbus, category, from_state, to_state, action):
""" Helper method to track specific state changes. """
from_state = ensure_list(from_state)
to_state = ensure_list(to_state)
def listener(event):
""" State change listener that listens for specific state changes. """
assert isinstance(event, Event), "event needs to be of Event type"
if category == event.data['category'] and \
matcher(event.data['old_state'].state, from_state) and \
matcher(event.data['new_state'].state, to_state):
action(event.data['category'], event.data['old_state'], event.data['new_state'])
eventbus.listen(EVENT_STATE_CHANGED, listener)
class EventBus(object):
""" Class provides an eventbus. Allows code to listen for events and fire them. """
def __init__(self):
self.listeners = defaultdict(list)
self.lock = RLock()
self.logger = logging.getLogger(__name__)
def fire(self, event):
""" Fire an event. """
assert isinstance(event, Event), "event needs to be an instance of Event"
def run():
""" We dont want the eventbus to be blocking,
We dont want the eventbus to crash when one of its listeners throws an Exception
So run in a thread. """
self.lock.acquire()
self.logger.info("EventBus:Event {}: {}".format(event.event_type, event.data))
for callback in chain(self.listeners[ALL_EVENTS], self.listeners[event.event_type]):
callback(event)
if event.remove_listener:
if callback in self.listeners[ALL_EVENTS]:
self.listeners[ALL_EVENTS].remove(callback)
if callback in self.listeners[event.event_type]:
self.listeners[event.event_type].remove(callback)
event.remove_listener = False
if event.stop_propegating:
break
self.lock.release()
Thread(target=run).start()
def listen(self, event_type, callback):
""" Listen for all events or events of a specific type.
To listen to all events specify the constant ``ALL_EVENTS`` as event_type. """
self.lock.acquire()
self.listeners[event_type].append(callback)
self.lock.release()
class Event(object):
""" An event to be sent over the eventbus. """
def __init__(self, event_type, data=None):
self.event_type = event_type
self.data = {} if data is None else data
self.stop_propegating = False
self.remove_listener = False
def __str__(self):
return str([self.event_type, self.data])
class StateMachine(object):
""" Helper class that tracks the state of different objects. """
def __init__(self, eventbus):
self.states = dict()
self.eventbus = eventbus
self.lock = RLock()
def set_state(self, category, new_state):
""" Set the state of a category, add category is it does not exist. """
self.lock.acquire()
# Add category is it does not exist
if category not in self.states:
self.states[category] = State(new_state, datetime.now())
# Change state and fire listeners
else:
old_state = self.states[category]
if old_state.state != new_state:
self.states[category] = State(new_state, datetime.now())
self.eventbus.fire(Event(EVENT_STATE_CHANGED, {'category':category, 'old_state':old_state, 'new_state':self.states[category]}))
self.lock.release()
def is_state(self, category, state):
""" Returns True if category is specified state. """
self._validate_category(category)
return self.get_state(category).state == state
def get_state(self, category):
""" Returns a tuple (state,last_changed) describing the state of the specified category. """
self._validate_category(category)
return self.states[category]
def get_states(self):
""" Returns a list of tuples (category, state, last_changed) sorted by category. """
return [(category, self.states[category].state, self.states[category].last_changed) for category in sorted(self.states.keys())]
def _validate_category(self, category):
""" Helper function to throw an exception when the category does not exist. """
if category not in self.states:
raise CategoryDoesNotExistException("Category {} does not exist.".format(category))
class HomeAssistantException(Exception):
""" General Home Assistant exception occured. """
class CategoryDoesNotExistException(HomeAssistantException):
""" Specified category does not exist within the state machine. """

View File

@ -30,7 +30,7 @@ from urlparse import urlparse, parse_qs
import requests
from .core import EVENT_START, EVENT_SHUTDOWN, Event, CategoryDoesNotExistException
from . import EVENT_START, EVENT_SHUTDOWN, Event, CategoryDoesNotExistException
SERVER_PORT = 8080
@ -168,9 +168,6 @@ class RequestHandler(BaseHTTPRequestHandler):
action = self.path[1:]
use_json = False
self.server.logger.info(post_data)
self.server.logger.info(action)
given_api_password = post_data.get("api_password", [''])[0]
# Action to change the state
@ -206,11 +203,12 @@ class RequestHandler(BaseHTTPRequestHandler):
def _verify_api_password(self, api_password, use_json):
""" Helper method to verify the API password and take action if incorrect. """
if api_password == self.server.api_password:
return True
elif use_json:
self._message(True, "API password missing or incorrect.", MESSAGE_STATUS_UNAUTHORIZED)
self._message(True, "API password missing or incorrect.", MESSAGE_STATUS_UNAUTHORIZED)
else:
self.send_response(200)
@ -218,7 +216,7 @@ class RequestHandler(BaseHTTPRequestHandler):
self.end_headers()
write = lambda txt: self.wfile.write(txt+"\n")
write("<html>")
write("<form action='/' method='GET'>")
write("API password: <input name='api_password' />")

View File

@ -12,25 +12,13 @@ import csv
import os
from datetime import datetime, timedelta
import threading
import time
import re
import json
import requests
import ephem
from .core import ensure_list, matcher, Event, EVENT_START, EVENT_SHUTDOWN
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!"
EVENT_TIME_CHANGED = "time_changed"
from . import track_time_change
STATE_CATEGORY_SUN = "weather.sun"
STATE_CATEGORY_ALL_DEVICES = 'device.alldevices'
@ -42,7 +30,6 @@ SUN_STATE_BELOW_HORIZON = "below_horizon"
DEVICE_STATE_NOT_HOME = 'device_not_home'
DEVICE_STATE_HOME = 'device_home'
# After how much time do we consider a device not home if
# it does not show up on scans
TOMATO_TIME_SPAN_FOR_ERROR_IN_SCANNING = timedelta(minutes=1)
@ -50,66 +37,6 @@ TOMATO_MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
TOMATO_KNOWN_DEVICES_FILE = "tomato_known_devices.csv"
class Timer(threading.Thread):
""" Timer will sent out an event every TIMER_INTERVAL seconds. """
def __init__(self, eventbus):
threading.Thread.__init__(self)
self.eventbus = eventbus
self._stop = threading.Event()
eventbus.listen(EVENT_START, lambda event: self.start())
eventbus.listen(EVENT_SHUTDOWN, lambda event: self._stop.set())
def run(self):
""" Start the timer. """
logging.getLogger(__name__).info("Timer:starting")
now = datetime.now()
while True:
while True:
time.sleep(1)
now = datetime.now()
if self._stop.isSet() or now.second % TIMER_INTERVAL == 0:
break
if self._stop.isSet():
break
self.eventbus.fire(Event(EVENT_TIME_CHANGED, {'now':now}))
def track_time_change(eventbus, action, year='*', month='*', day='*', hour='*', minute='*', second='*', point_in_time=None, listen_once=False):
""" Adds a listener that will listen for a specified or matching time. """
year, month, day = ensure_list(year), ensure_list(month), ensure_list(day)
hour, minute, second = ensure_list(hour), ensure_list(minute), ensure_list(second)
def listener(event):
""" Listens for matching time_changed events. """
assert isinstance(event, Event), "event needs to be of Event type"
if (point_in_time is not None and event.data['now'] > point_in_time) or \
(point_in_time is None and \
matcher(event.data['now'].year, year) and \
matcher(event.data['now'].month, month) and \
matcher(event.data['now'].day, day) and \
matcher(event.data['now'].hour, hour) and \
matcher(event.data['now'].minute, minute) and \
matcher(event.data['now'].second, second)):
# point_in_time are exact points in time so we always remove it after fire
event.remove_listener = listen_once or point_in_time is not None
action(event.data['now'])
eventbus.listen(EVENT_TIME_CHANGED, listener)
class WeatherWatcher(object):
""" Class that keeps track of the state of the sun. """

View File

@ -11,7 +11,7 @@ import time
import requests
from .core import EventBus, StateMachine, Event, EVENT_START, EVENT_SHUTDOWN
from . import EventBus, StateMachine, Event, EVENT_START, EVENT_SHUTDOWN
from .httpinterface import HTTPInterface, SERVER_PORT
@ -29,6 +29,14 @@ class HomeAssistantTestCase(unittest.TestCase):
cls.statemachine = StateMachine(cls.eventbus)
cls.init_ha = False
def start_ha(self):
cls.eventbus.fire(Event(EVENT_START))
# Give objects time to startup
time.sleep(1)
cls.start_ha = start_ha
@classmethod
def tearDownClass(cls):
cls.eventbus.fire(Event(EVENT_SHUTDOWN))
@ -50,10 +58,7 @@ class TestHTTPInterface(HomeAssistantTestCase):
self.statemachine.set_state("test", "INIT_STATE")
self.eventbus.fire(Event(EVENT_START))
# Give objects time to start up
time.sleep(1)
self.start_ha()
def test_debug_interface(self):

View File

@ -1,21 +1,32 @@
from ConfigParser import SafeConfigParser
from homeassistant import HomeAssistant
from homeassistant.actors import HueLightControl
from homeassistant.observers import TomatoDeviceScanner
from homeassistant import StateMachine, EventBus, start_home_assistant
from homeassistant.observers import TomatoDeviceScanner, DeviceTracker, WeatherWatcher
from homeassistant.actors import HueLightControl, LightTrigger
from homeassistant.httpinterface import HTTPInterface
# Read config
config = SafeConfigParser()
config.read("home-assistant.conf")
tomato = TomatoDeviceScanner(config.get('tomato','host'), config.get('tomato','username'),
# Init core
eventbus = EventBus()
statemachine = StateMachine(eventbus)
# Init observers
tomato = TomatoDeviceScanner(config.get('tomato','host'), config.get('tomato','username'),
config.get('tomato','password'), config.get('tomato','http_id'))
devicetracker = DeviceTracker(eventbus, statemachine, tomato)
ha = HomeAssistant(config.get("common","latitude"), config.get("common","longitude"))
weatherwatcher = WeatherWatcher(eventbus, statemachine,
config.get("common","latitude"),
config.get("common","longitude"))
ha.setup_light_trigger(tomato, HueLightControl())
# Init actors
LightTrigger(eventbus, statemachine, weatherwatcher, devicetracker, HueLightControl())
ha.setup_http_interface(config.get("common","api_password"))
# Init HTTP interface
HTTPInterface(eventbus, statemachine, config.get("common","api_password"))
ha.start()
start_home_assistant(eventbus)