diff --git a/homeassistant/EventBus.py b/homeassistant/EventBus.py deleted file mode 100644 index ad207a8a4d7..00000000000 --- a/homeassistant/EventBus.py +++ /dev/null @@ -1,71 +0,0 @@ -import logging - -from collections import defaultdict -from itertools import chain -from threading import Thread, RLock - -ALL_EVENTS = '*' - -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" - - # 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 - def run(): - self.lock.acquire() - - self.logger.info("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.logger.info("New listener for event {}. Total: {}".format(event_type, len(self.listeners[event_type]))) - - self.lock.release() - - -class Event(object): - """ An event to be sent over the eventbus. """ - - def __init__(self, event_type, data={}): - self.event_type = event_type - self.data = data - self.stop_propegating = False - self.remove_listener = False - - def __str__(self): - return str([self.event_type, self.data]) diff --git a/homeassistant/HomeAssistant.py b/homeassistant/HomeAssistant.py deleted file mode 100644 index 0792cbafa40..00000000000 --- a/homeassistant/HomeAssistant.py +++ /dev/null @@ -1,117 +0,0 @@ -import logging -from ConfigParser import SafeConfigParser -import time - -from homeassistant.common import EVENT_START, EVENT_SHUTDOWN - -from homeassistant.StateMachine import StateMachine -from homeassistant.EventBus import EventBus, Event -from homeassistant.HttpInterface import HttpInterface - -from homeassistant.observer.DeviceTracker import DeviceTracker -from homeassistant.observer.WeatherWatcher import WeatherWatcher -from homeassistant.observer.Timer import Timer - -from homeassistant.actor.LightTrigger import LightTrigger - -CONFIG_FILE = "home-assistant.conf" - -class HomeAssistant(object): - """ Class to tie all modules together and handle dependencies. """ - - def __init__(self): - self.logger = logging.getLogger(__name__) - - self.config = None - self.eventbus = None - self.statemachine = None - - self.timer = None - self.weatherwatcher = None - self.devicetracker = None - - self.lighttrigger = None - self.httpinterface = None - - - def get_config(self): - if self.config is None: - self.logger.info("Loading HomeAssistant config") - self.config = SafeConfigParser() - self.config.read(CONFIG_FILE) - - return self.config - - - def get_event_bus(self): - if self.eventbus is None: - self.logger.info("Setting up event bus") - self.eventbus = EventBus() - - return self.eventbus - - - def get_state_machine(self): - if self.statemachine is None: - self.logger.info("Setting up state machine") - self.statemachine = StateMachine(self.get_event_bus()) - - return self.statemachine - - - def setup_timer(self): - if self.timer is None: - self.logger.info("Setting up timer") - self.timer = Timer(self.get_event_bus()) - - return self.timer - - - def setup_weather_watcher(self): - if self.weatherwatcher is None: - self.logger.info("Setting up weather watcher") - self.weatherwatcher = WeatherWatcher(self.get_config(), self.get_event_bus(), self.get_state_machine()) - - return self.weatherwatcher - - - def setup_device_tracker(self, device_scanner): - if self.devicetracker is None: - self.logger.info("Setting up device tracker") - self.devicetracker = DeviceTracker(self.get_event_bus(), self.get_state_machine(), device_scanner) - - return self.devicetracker - - - def setup_light_trigger(self, light_control): - if self.lighttrigger is None: - self.logger.info("Setting up light trigger") - assert self.devicetracker is not None, "Cannot setup light trigger without a device tracker being setup" - - self.lighttrigger = LightTrigger(self.get_event_bus(), self.get_state_machine(), self.devicetracker, self.setup_weather_watcher(), light_control) - - return self.lighttrigger - - - def setup_http_interface(self): - if self.httpinterface is None: - self.logger.info("Setting up HTTP interface") - self.httpinterface = HttpInterface(self.get_event_bus(), self.get_state_machine()) - - return self.httpinterface - - - def start(self): - self.setup_timer() - - self.get_event_bus().fire(Event(EVENT_START)) - - while True: - try: - time.sleep(1) - - except KeyboardInterrupt: - print "" - self.eventbus.fire(Event(EVENT_SHUTDOWN)) - - break diff --git a/homeassistant/HttpInterface.py b/homeassistant/HttpInterface.py index 23fc601a15e..6394b4d670c 100644 --- a/homeassistant/HttpInterface.py +++ b/homeassistant/HttpInterface.py @@ -1,3 +1,11 @@ +""" +homeassistant.httpinterface +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This module provides an HTTP interface for debug purposes. + +""" + import threading import urlparse import logging @@ -5,12 +13,12 @@ from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer import requests -from homeassistant.common import EVENT_START, EVENT_SHUTDOWN +from .core import EVENT_START, EVENT_SHUTDOWN SERVER_HOST = '127.0.0.1' SERVER_PORT = 8080 -class HttpInterface(threading.Thread): +class HTTPInterface(threading.Thread): """ Provides an HTTP interface for Home Assistant. """ def __init__(self, eventbus, statemachine): diff --git a/homeassistant/StateMachine.py b/homeassistant/StateMachine.py deleted file mode 100644 index aab91080690..00000000000 --- a/homeassistant/StateMachine.py +++ /dev/null @@ -1,70 +0,0 @@ -from collections import namedtuple -from threading import RLock -from datetime import datetime - -from homeassistant.EventBus import Event -from homeassistant.common import ensure_list, matcher - -EVENT_STATE_CHANGED = "state_changed" - -State = namedtuple("State", ['state','last_changed']) - -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 add_category(self, category, initial_state): - """ Add a category which state we will keep track off. """ - self.states[category] = State(initial_state, datetime.now()) - - def set_state(self, category, new_state): - """ Set the state of a category. """ - self.lock.acquire() - - assert category in self.states, "Category does not exist: {}".format(category) - - 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. """ - assert category in self.states, "Category does not exist: {}".format(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. """ - assert category in self.states, "Category does not exist: {}".format(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 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): - 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) diff --git a/homeassistant/__init__.py b/homeassistant/__init__.py index e69de29bb2d..141f08d2be9 100644 --- a/homeassistant/__init__.py +++ b/homeassistant/__init__.py @@ -0,0 +1,75 @@ +""" +homeassistant +~~~~~~~~~~~~~ + +Module to control the lights based on devices at home and the state of the sun. + +""" + +import logging +import time + +from .core import EventBus, StateMachine, Event, EVENT_START, EVENT_SHUTDOWN +from .httpinterface import HTTPInterface +from .observers import DeviceTracker, WeatherWatcher, Timer +from .actors import LightTrigger + + + +class HomeAssistant(object): + """ Class to tie all modules together and handle dependencies. """ + + def __init__(self, latitude=None, longitude=None): + self.latitude = latitude + self.longitude = longitude + + self.logger = logging.getLogger(__name__) + + self.eventbus = EventBus() + self.statemachine = StateMachine(self.eventbus) + + self.httpinterface = None + self.weatherwatcher = None + + def setup_light_trigger(self, device_scanner, light_control): + """ Sets up the light trigger system. """ + self.logger.info("Setting up light trigger") + + devicetracker = DeviceTracker(self.eventbus, self.statemachine, device_scanner) + + LightTrigger(self.eventbus, self.statemachine, self._setup_weather_watcher(), devicetracker, light_control) + + + def setup_http_interface(self): + """ Sets up the HTTP interface. """ + if self.httpinterface is None: + self.logger.info("Setting up HTTP interface") + self.httpinterface = HTTPInterface(self.eventbus, self.statemachine) + + return self.httpinterface + + + def start(self): + """ Start home assistant. """ + Timer(self.eventbus) + + self.eventbus.fire(Event(EVENT_START)) + + while True: + try: + time.sleep(1) + + except KeyboardInterrupt: + print "" + self.eventbus.fire(Event(EVENT_SHUTDOWN)) + + 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) + + return self.weatherwatcher + + diff --git a/homeassistant/actor/HueLightControl.py b/homeassistant/actor/HueLightControl.py deleted file mode 100644 index c202ee86b98..00000000000 --- a/homeassistant/actor/HueLightControl.py +++ /dev/null @@ -1,52 +0,0 @@ -from phue import Bridge - -MAX_TRANSITION_TIME = 9000 - - -def process_transition_time(transition_seconds): - """ Transition time is in 1/10th seconds and cannot exceed MAX_TRANSITION_TIME. """ - return min(MAX_TRANSITION_TIME, transition_seconds * 10) - - -class HueLightControl(object): - """ Class to interface with the Hue light system. """ - - def __init__(self, config=None): - self.bridge = Bridge(config.get("hue","host") if config is not None and config.has_option("hue","host") else None) - self.lights = self.bridge.get_light_objects() - self.light_ids = [light.light_id for light in self.lights] - - - def is_light_on(self, light_id=None): - """ Returns if specified light is on. - - If light_id not specified will report on combined status of all lights. """ - if light_id is None: - return sum([1 for light in self.lights if light.on]) > 0 - - else: - return self.bridge.get_light(light_id, 'on') - - - def turn_light_on(self, light_id=None, transition_seconds=None): - if light_id is None: - light_id = self.light_ids - - command = {'on': True, 'xy': [0.5119, 0.4147], 'bri':164} - - if transition_seconds is not None: - command['transitiontime'] = process_transition_time(transition_seconds) - - self.bridge.set_light(light_id, command) - - - def turn_light_off(self, light_id=None, transition_seconds=None): - if light_id is None: - light_id = self.light_ids - - command = {'on': False} - - if transition_seconds is not None: - command['transitiontime'] = process_transition_time(transition_seconds) - - self.bridge.set_light(light_id, command) diff --git a/homeassistant/actor/__init__.py b/homeassistant/actor/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/homeassistant/actor/LightTrigger.py b/homeassistant/actors.py similarity index 51% rename from homeassistant/actor/LightTrigger.py rename to homeassistant/actors.py index eca02eccf29..11178b193f1 100644 --- a/homeassistant/actor/LightTrigger.py +++ b/homeassistant/actors.py @@ -1,17 +1,36 @@ -import logging -from datetime import datetime, timedelta +""" +homeassistant.actors +~~~~~~~~~~~~~~~~~~~~ -from homeassistant.observer.WeatherWatcher import STATE_CATEGORY_SUN, SUN_STATE_BELOW_HORIZON, SUN_STATE_ABOVE_HORIZON -from homeassistant.observer.DeviceTracker import STATE_CATEGORY_ALL_DEVICES, STATE_DEVICE_HOME, STATE_DEVICE_NOT_HOME -from homeassistant.StateMachine import track_state_change -from homeassistant.observer.Timer import track_time_change +This module provides actors that will react to events happening within homeassistant. + +""" + +import logging +from datetime import timedelta + +from phue import Bridge + +from .core 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, + track_time_change) LIGHT_TRANSITION_TIME = timedelta(minutes=15) +HUE_MAX_TRANSITION_TIME = 9000 + + +def _hue_process_transition_time(transition_seconds): + """ Transition time is in 1/10th seconds and cannot exceed MAX_TRANSITION_TIME. """ + return min(HUE_MAX_TRANSITION_TIME, transition_seconds * 10) + + class LightTrigger(object): """ Class to turn on lights based on available devices and state of the sun. """ - def __init__(self, eventbus, statemachine, device_tracker, weather, light_control): + def __init__(self, eventbus, statemachine, weather, device_tracker, light_control): self.eventbus = eventbus self.statemachine = statemachine self.weather = weather @@ -21,10 +40,10 @@ class LightTrigger(object): # Track home coming of each seperate device for category in device_tracker.device_state_categories(): - track_state_change(eventbus, category, STATE_DEVICE_NOT_HOME, STATE_DEVICE_HOME, self._handle_device_state_change) + track_state_change(eventbus, category, DEVICE_STATE_NOT_HOME, DEVICE_STATE_HOME, self._handle_device_state_change) # Track when all devices are gone to shut down lights - track_state_change(eventbus, STATE_CATEGORY_ALL_DEVICES, STATE_DEVICE_HOME, STATE_DEVICE_NOT_HOME, self._handle_device_state_change) + track_state_change(eventbus, STATE_CATEGORY_ALL_DEVICES, DEVICE_STATE_HOME, DEVICE_STATE_NOT_HOME, self._handle_device_state_change) # Track every time sun rises so we can schedule a time-based pre-sun set event track_state_change(eventbus, STATE_CATEGORY_SUN, SUN_STATE_BELOW_HORIZON, SUN_STATE_ABOVE_HORIZON, self._handle_sun_rising) @@ -41,10 +60,10 @@ class LightTrigger(object): start_point = self.weather.next_sun_setting() - LIGHT_TRANSITION_TIME * len(self.light_control.light_ids) - # Lambda can keep track of function parameters, not from local parameters - # If we put the lambda directly in the below statement only the last light - # would be turned on.. def turn_on(light_id): + """ Lambda can keep track of function parameters, not from local parameters + If we put the lambda directly in the below statement only the last light + would be turned on.. """ return lambda now: self._turn_light_on_before_sunset(light_id) for index, light_id in enumerate(self.light_control.light_ids): @@ -54,7 +73,7 @@ class LightTrigger(object): def _turn_light_on_before_sunset(self, light_id=None): """ Helper function to turn on lights slowly if there are devices home and the light is not on yet. """ - if self.statemachine.is_state(STATE_CATEGORY_ALL_DEVICES, STATE_DEVICE_HOME) and not self.light_control.is_light_on(light_id): + if self.statemachine.is_state(STATE_CATEGORY_ALL_DEVICES, DEVICE_STATE_HOME) and not self.light_control.is_light_on(light_id): self.light_control.turn_light_on(light_id, LIGHT_TRANSITION_TIME.seconds) @@ -65,11 +84,55 @@ class LightTrigger(object): light_needed = not lights_are_on and self.statemachine.is_state(STATE_CATEGORY_SUN, SUN_STATE_BELOW_HORIZON) # Specific device came home ? - if category != STATE_CATEGORY_ALL_DEVICES and new_state.state == STATE_DEVICE_HOME and light_needed: + if category != STATE_CATEGORY_ALL_DEVICES and new_state.state == DEVICE_STATE_HOME and light_needed: self.logger.info("Home coming event for {}. Turning lights on".format(category)) self.light_control.turn_light_on() # Did all devices leave the house? - elif category == STATE_CATEGORY_ALL_DEVICES and new_state.state == STATE_DEVICE_NOT_HOME and lights_are_on: + elif category == STATE_CATEGORY_ALL_DEVICES and new_state.state == DEVICE_STATE_NOT_HOME and lights_are_on: self.logger.info("Everyone has left but lights are on. Turning lights off") self.light_control.turn_light_off() + + +class HueLightControl(object): + """ Class to interface with the Hue light system. """ + + def __init__(self, host=None): + self.bridge = Bridge(host) + self.lights = self.bridge.get_light_objects() + self.light_ids = [light.light_id for light in self.lights] + + + def is_light_on(self, light_id=None): + """ Returns if specified or all light are on. """ + if light_id is None: + return sum([1 for light in self.lights if light.on]) > 0 + + else: + return self.bridge.get_light(light_id, 'on') + + + def turn_light_on(self, light_id=None, transition_seconds=None): + """ Turn the specified or all lights on. """ + if light_id is None: + light_id = self.light_ids + + command = {'on': True, 'xy': [0.5119, 0.4147], 'bri':164} + + if transition_seconds is not None: + command['transitiontime'] = _hue_process_transition_time(transition_seconds) + + self.bridge.set_light(light_id, command) + + + def turn_light_off(self, light_id=None, transition_seconds=None): + """ Turn the specified or all lights off. """ + if light_id is None: + light_id = self.light_ids + + command = {'on': False} + + if transition_seconds is not None: + command['transitiontime'] = _hue_process_transition_time(transition_seconds) + + self.bridge.set_light(light_id, command) diff --git a/homeassistant/common.py b/homeassistant/common.py deleted file mode 100644 index a77c0ca2d91..00000000000 --- a/homeassistant/common.py +++ /dev/null @@ -1,10 +0,0 @@ -EVENT_START = "start" -EVENT_SHUTDOWN = "shutdown" - -def ensure_list(parameter): - 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 diff --git a/homeassistant/core.py b/homeassistant/core.py new file mode 100644 index 00000000000..6c77994b802 --- /dev/null +++ b/homeassistant/core.py @@ -0,0 +1,151 @@ +""" +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 add_category(self, category, initial_state): + """ Add a category which state we will keep track off. """ + self.states[category] = State(initial_state, datetime.now()) + + def set_state(self, category, new_state): + """ Set the state of a category. """ + self.lock.acquire() + + assert category in self.states, "Category does not exist: {}".format(category) + + 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. """ + assert category in self.states, "Category does not exist: {}".format(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. """ + assert category in self.states, "Category does not exist: {}".format(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())] diff --git a/homeassistant/observer/DeviceTracker.py b/homeassistant/observer/DeviceTracker.py deleted file mode 100644 index adfbe50483c..00000000000 --- a/homeassistant/observer/DeviceTracker.py +++ /dev/null @@ -1,84 +0,0 @@ -from datetime import datetime, timedelta - -from homeassistant.observer.Timer import track_time_change - -STATE_DEVICE_NOT_HOME = 'device_not_home' -STATE_DEVICE_HOME = 'device_home' - - -# After how much time do we consider a device not home if -# it does not show up on scans -TIME_SPAN_FOR_ERROR_IN_SCANNING = timedelta(minutes=1) - -STATE_CATEGORY_ALL_DEVICES = 'device.alldevices' -STATE_CATEGORY_DEVICE_FORMAT = 'device.{}' - - -class DeviceTracker(object): - """ Class that tracks which devices are home and which are not. """ - - def __init__(self, eventbus, statemachine, device_scanner): - self.statemachine = statemachine - self.eventbus = eventbus - - temp_devices_to_track = device_scanner.get_devices_to_track() - - self.devices_to_track = { device: { 'name': temp_devices_to_track[device], - 'category': STATE_CATEGORY_DEVICE_FORMAT.format(temp_devices_to_track[device]) } - for device in temp_devices_to_track } - - # Add categories to state machine and update last_seen attribute - # If we don't update now a change event will be fired on boot. - initial_search = device_scanner.scan_devices() - - default_last_seen = datetime(1990, 1, 1) - - for device in self.devices_to_track: - if device in initial_search: - new_state = STATE_DEVICE_HOME - new_last_seen = datetime.now() - else: - new_state = STATE_DEVICE_NOT_HOME - new_last_seen = default_last_seen - - self.devices_to_track[device]['last_seen'] = new_last_seen - self.statemachine.add_category(self.devices_to_track[device]['category'], new_state) - - # Update all devices state - statemachine.add_category(STATE_CATEGORY_ALL_DEVICES, STATE_DEVICE_HOME if len(initial_search) > 0 else STATE_DEVICE_NOT_HOME) - - track_time_change(eventbus, lambda time: self.update_devices(device_scanner.scan_devices())) - - - def device_state_categories(self): - """ Returns a list of categories of devices that are being tracked by this class. """ - return [self.devices_to_track[device]['category'] for device in self.devices_to_track] - - - def update_devices(self, found_devices): - """ Keep track of devices that are home, all that are not will be marked not home. """ - - temp_tracking_devices = self.devices_to_track.keys() - - for device in found_devices: - # Are we tracking this device? - if device in temp_tracking_devices: - temp_tracking_devices.remove(device) - - self.devices_to_track[device]['last_seen'] = datetime.now() - self.statemachine.set_state(self.devices_to_track[device]['category'], STATE_DEVICE_HOME) - - # For all devices we did not find, set state to NH - # But only if they have been gone for longer then the error time span - # Because we do not want to have stuff happening when the device does - # not show up for 1 scan beacuse of reboot etc - for device in temp_tracking_devices: - if datetime.now() - self.devices_to_track[device]['last_seen'] > TIME_SPAN_FOR_ERROR_IN_SCANNING: - self.statemachine.set_state(self.devices_to_track[device]['category'], STATE_DEVICE_NOT_HOME) - - # Get the currently used statuses - states_of_devices = [self.statemachine.get_state(self.devices_to_track[device]['category']).state for device in self.devices_to_track] - - all_devices_state = STATE_DEVICE_HOME if STATE_DEVICE_HOME in states_of_devices else STATE_DEVICE_NOT_HOME - - self.statemachine.set_state(STATE_CATEGORY_ALL_DEVICES, all_devices_state) diff --git a/homeassistant/observer/Timer.py b/homeassistant/observer/Timer.py deleted file mode 100644 index a93d72f56b6..00000000000 --- a/homeassistant/observer/Timer.py +++ /dev/null @@ -1,74 +0,0 @@ -import logging -from datetime import datetime -import threading -import time - -from homeassistant.common import EVENT_START, EVENT_SHUTDOWN -from homeassistant.EventBus import Event -from homeassistant.common import ensure_list, matcher - -TIME_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 % TIME_INTERVAL == 0, "60 % TIME_INTERVAL should be 0!" - -EVENT_TIME_CHANGED = "time_changed" - -class Timer(threading.Thread): - """ Timer will sent out an event every TIME_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("Starting") - - now = datetime.now() - - while True: - while True: - time.sleep(1) - - now = datetime.now() - - if self._stop.isSet() or now.second % TIME_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): - 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): - 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) diff --git a/homeassistant/observer/TomatoDeviceScanner.py b/homeassistant/observer/TomatoDeviceScanner.py deleted file mode 100644 index 4472d908214..00000000000 --- a/homeassistant/observer/TomatoDeviceScanner.py +++ /dev/null @@ -1,108 +0,0 @@ -import logging -import csv -import os -from datetime import datetime, timedelta -from threading import Lock - -import requests - -MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5) - -KNOWN_DEVICES_FILE = "tomato_known_devices.csv" - -class TomatoDeviceScanner(object): - """ This class tracks devices connected to a wireless router running Tomato firmware. """ - - def __init__(self, config): - self.config = config - self.logger = logging.getLogger(__name__) - self.lock = Lock() - - # Read known devices - if os.path.isfile(KNOWN_DEVICES_FILE): - with open(KNOWN_DEVICES_FILE) as inp: - known_devices = { row['mac']: row for row in csv.DictReader(inp) } - - # Update known devices csv file for future use - with open(KNOWN_DEVICES_FILE, 'a') as outp: - writer = csv.writer(outp) - - # Query for new devices - exec(self._tomato_request("devlist")) - - for name, _, mac, _ in dhcpd_lease: - if mac not in known_devices: - writer.writerow((mac, name, 0)) - - self.last_results = [mac for iface, mac, rssi, tx, rx, quality, unknown_num in wldev] - self.date_updated = datetime.now() - - # Create a dict with ID: NAME of the devices to track - self.devices_to_track = dict() - - for mac in known_devices: - if known_devices[mac]['track'] == '1': - self.devices_to_track[mac] = known_devices[mac]['name'] - - # Quicker way of the previous statement but it doesn't go together with exec: - # unqualified exec is not allowed in function '__init__' it contains a nested function with free variables - # self.devices_to_track = {mac: known_devices[mac]['name'] for mac in known_devices if known_devices[mac]['track'] == '1'} - - if len(self.devices_to_track) == 0: - self.logger.warning("No devices to track. Please update {}.".format(KNOWN_DEVICES_FILE)) - - def get_devices_to_track(self): - """ Returns a ``dict`` with device_id: device_name values. """ - return self.devices_to_track - - def scan_devices(self): - """ Scans for new devices and returns a list containing device_ids. """ - self.lock.acquire() - - # We don't want to hammer the router. Only update if MIN_TIME_BETWEEN_SCANS has passed - if self.date_updated is None or datetime.now() - self.date_updated > MIN_TIME_BETWEEN_SCANS: - self.logger.info("Scanning") - - try: - # Query for new devices - exec(self._tomato_request("devlist")) - - self.last_results = [mac for iface, mac, rssi, tx, rx, quality, unknown_num in wldev] - self.date_updated = datetime.now() - - except Exception as e: - self.logger.exception("Scanning failed") - - - self.lock.release() - return self.last_results - - def _tomato_request(self, action): - # Get router info - req = requests.post('http://{}/update.cgi'.format(self.config.get('tomato','host')), - data={'_http_id':self.config.get('tomato','http_id'), 'exec':action}, - auth=requests.auth.HTTPBasicAuth(self.config.get('tomato','username'), self.config.get('tomato','password'))) - - return req.text - - - -""" -Tomato API: -for ip, mac, iface in arplist: - pass - -print wlnoise - -print dhcpd_static - -for iface, mac, rssi, tx, rx, quality, unknown_num in wldev: - print mac, quality - -for name, ip, mac, lease in dhcpd_lease: - if name: - print name, ip - - else: - print ip -""" diff --git a/homeassistant/observer/WeatherWatcher.py b/homeassistant/observer/WeatherWatcher.py deleted file mode 100644 index 102ae3cfb72..00000000000 --- a/homeassistant/observer/WeatherWatcher.py +++ /dev/null @@ -1,77 +0,0 @@ -import logging -from datetime import timedelta - -import ephem - -from homeassistant.observer.Timer import track_time_change - -STATE_CATEGORY_SUN = "weather.sun" - -SUN_STATE_ABOVE_HORIZON = "above_horizon" -SUN_STATE_BELOW_HORIZON = "below_horizon" - -class WeatherWatcher(object): - """ Class that keeps track of the state of the sun. """ - - def __init__(self, config, eventbus, statemachine): - self.logger = logging.getLogger(__name__) - self.config = config - self.eventbus = eventbus - self.statemachine = statemachine - - self.sun = ephem.Sun() - - statemachine.add_category(STATE_CATEGORY_SUN, SUN_STATE_BELOW_HORIZON) - - self._update_sun_state() - - - def next_sun_rising(self, observer=None): - """ Returns a datetime object that points at the next sun rising. """ - - if observer is None: - observer = self._get_observer() - - return ephem.localtime(observer.next_rising(self.sun)) - - - def next_sun_setting(self, observer=None): - """ Returns a datetime object that points at the next sun setting. """ - - if observer is None: - observer = self._get_observer() - - return ephem.localtime(observer.next_setting(self.sun)) - - - def _update_sun_state(self, now=None): - """ Updates the state of the sun and schedules when to check next. """ - - observer = self._get_observer() - - next_rising = self.next_sun_rising(observer) - next_setting = self.next_sun_setting(observer) - - if next_rising > next_setting: - new_state = SUN_STATE_ABOVE_HORIZON - next_change = next_setting - - else: - new_state = SUN_STATE_BELOW_HORIZON - next_change = next_rising - - self.logger.info("Sun:{}. Next change: {}".format(new_state, next_change.strftime("%H:%M"))) - - self.statemachine.set_state(STATE_CATEGORY_SUN, new_state) - - # +10 seconds to be sure that the change has occured - track_time_change(self.eventbus, self._update_sun_state, point_in_time=next_change + timedelta(seconds=10)) - - - def _get_observer(self): - """ Creates an observer representing the location from the config and the current time. """ - observer = ephem.Observer() - observer.lat = self.config.get('common','latitude') - observer.long = self.config.get('common','longitude') - - return observer diff --git a/homeassistant/observer/__init__.py b/homeassistant/observer/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/homeassistant/observers.py b/homeassistant/observers.py new file mode 100644 index 00000000000..435c2b1f628 --- /dev/null +++ b/homeassistant/observers.py @@ -0,0 +1,349 @@ +""" +homeassistant.observers +~~~~~~~~~~~~~~~~~~~~~~~ + +This module provides observers that can change the state or fire +events based on observations. + +""" + +import logging +import csv +import os +from datetime import datetime, timedelta +import threading +import time + +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" + + +STATE_CATEGORY_SUN = "weather.sun" +STATE_CATEGORY_ALL_DEVICES = 'device.alldevices' +STATE_CATEGORY_DEVICE_FORMAT = 'device.{}' + +SUN_STATE_ABOVE_HORIZON = "above_horizon" +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) +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. """ + + def __init__(self, eventbus, statemachine, latitude, longitude): + self.logger = logging.getLogger(__name__) + self.eventbus = eventbus + self.statemachine = statemachine + self.latitude = latitude + self.longitude = longitude + + self.sun = ephem.Sun() + + self._update_sun_state(create_state=True) + + + def next_sun_rising(self, observer=None): + """ Returns a datetime object that points at the next sun rising. """ + + if observer is None: + observer = self._get_observer() + + return ephem.localtime(observer.next_rising(self.sun)) + + + def next_sun_setting(self, observer=None): + """ Returns a datetime object that points at the next sun setting. """ + + if observer is None: + observer = self._get_observer() + + return ephem.localtime(observer.next_setting(self.sun)) + + + def _update_sun_state(self, now=None, create_state=False): + """ Updates the state of the sun and schedules when to check next. """ + + observer = self._get_observer() + + next_rising = self.next_sun_rising(observer) + next_setting = self.next_sun_setting(observer) + + if next_rising > next_setting: + new_state = SUN_STATE_ABOVE_HORIZON + next_change = next_setting + + else: + new_state = SUN_STATE_BELOW_HORIZON + next_change = next_rising + + self.logger.info("Sun:{}. Next change: {}".format(new_state, next_change.strftime("%H:%M"))) + + if create_state: + self.statemachine.add_category(STATE_CATEGORY_SUN, new_state) + + else: + self.statemachine.set_state(STATE_CATEGORY_SUN, new_state) + + # +10 seconds to be sure that the change has occured + track_time_change(self.eventbus, self._update_sun_state, point_in_time=next_change + timedelta(seconds=10)) + + + def _get_observer(self): + """ Creates an observer representing the location and the current time. """ + observer = ephem.Observer() + observer.lat = self.latitude + observer.long = self.longitude + + return observer + +class DeviceTracker(object): + """ Class that tracks which devices are home and which are not. """ + + def __init__(self, eventbus, statemachine, device_scanner): + self.statemachine = statemachine + self.eventbus = eventbus + + temp_devices_to_track = device_scanner.get_devices_to_track() + + self.devices_to_track = { device: { 'name': temp_devices_to_track[device], + 'category': STATE_CATEGORY_DEVICE_FORMAT.format(temp_devices_to_track[device]) } + for device in temp_devices_to_track } + + # Add categories to state machine and update last_seen attribute + # If we don't update now a change event will be fired on boot. + initial_search = device_scanner.scan_devices() + + default_last_seen = datetime(1990, 1, 1) + + for device in self.devices_to_track: + if device in initial_search: + new_state = DEVICE_STATE_HOME + new_last_seen = datetime.now() + else: + new_state = DEVICE_STATE_NOT_HOME + new_last_seen = default_last_seen + + self.devices_to_track[device]['last_seen'] = new_last_seen + self.statemachine.add_category(self.devices_to_track[device]['category'], new_state) + + # Update all devices state + statemachine.add_category(STATE_CATEGORY_ALL_DEVICES, DEVICE_STATE_HOME if len(initial_search) > 0 else DEVICE_STATE_NOT_HOME) + + track_time_change(eventbus, lambda time: self.update_devices(device_scanner.scan_devices())) + + + def device_state_categories(self): + """ Returns a list of categories of devices that are being tracked by this class. """ + return [self.devices_to_track[device]['category'] for device in self.devices_to_track] + + + def update_devices(self, found_devices): + """ Keep track of devices that are home, all that are not will be marked not home. """ + + temp_tracking_devices = self.devices_to_track.keys() + + for device in found_devices: + # Are we tracking this device? + if device in temp_tracking_devices: + temp_tracking_devices.remove(device) + + self.devices_to_track[device]['last_seen'] = datetime.now() + self.statemachine.set_state(self.devices_to_track[device]['category'], DEVICE_STATE_HOME) + + # For all devices we did not find, set state to NH + # But only if they have been gone for longer then the error time span + # Because we do not want to have stuff happening when the device does + # not show up for 1 scan beacuse of reboot etc + for device in temp_tracking_devices: + if datetime.now() - self.devices_to_track[device]['last_seen'] > TOMATO_TIME_SPAN_FOR_ERROR_IN_SCANNING: + self.statemachine.set_state(self.devices_to_track[device]['category'], DEVICE_STATE_NOT_HOME) + + # Get the currently used statuses + states_of_devices = [self.statemachine.get_state(self.devices_to_track[device]['category']).state for device in self.devices_to_track] + + all_devices_state = DEVICE_STATE_HOME if DEVICE_STATE_HOME in states_of_devices else DEVICE_STATE_NOT_HOME + + self.statemachine.set_state(STATE_CATEGORY_ALL_DEVICES, all_devices_state) + +class TomatoDeviceScanner(object): + """ This class tracks devices connected to a wireless router running Tomato firmware. """ + + def __init__(self, host, username, password, http_id): + self.host = host + self.username = username + self.password = password + self.http_id = http_id + + self.logger = logging.getLogger(__name__) + self.lock = threading.Lock() + + # Read known devices + if os.path.isfile(TOMATO_KNOWN_DEVICES_FILE): + with open(TOMATO_KNOWN_DEVICES_FILE) as inp: + known_devices = { row['mac']: row for row in csv.DictReader(inp) } + + # Update known devices csv file for future use + with open(TOMATO_KNOWN_DEVICES_FILE, 'a') as outp: + writer = csv.writer(outp) + + # Query for new devices + exec(self._tomato_request("devlist")) + + for name, _, mac, _ in dhcpd_lease: + if mac not in known_devices: + writer.writerow((mac, name, 0)) + + self.last_results = [mac for iface, mac, rssi, tx, rx, quality, unknown_num in wldev] + self.date_updated = datetime.now() + + # Create a dict with ID: NAME of the devices to track + self.devices_to_track = dict() + + for mac in known_devices: + if known_devices[mac]['track'] == '1': + self.devices_to_track[mac] = known_devices[mac]['name'] + + # Quicker way of the previous statement but it doesn't go together with exec: + # unqualified exec is not allowed in function '__init__' it contains a nested function with free variables + # self.devices_to_track = {mac: known_devices[mac]['name'] for mac in known_devices if known_devices[mac]['track'] == '1'} + + if len(self.devices_to_track) == 0: + self.logger.warning("No devices to track. Please update {}.".format(TOMATO_KNOWN_DEVICES_FILE)) + + def get_devices_to_track(self): + """ Returns a ``dict`` with device_id: device_name values. """ + return self.devices_to_track + + def scan_devices(self): + """ Scans for new devices and returns a list containing device_ids. """ + self.lock.acquire() + + # We don't want to hammer the router. Only update if TOMATO_MIN_TIME_BETWEEN_SCANS has passed + if self.date_updated is None or datetime.now() - self.date_updated > TOMATO_MIN_TIME_BETWEEN_SCANS: + self.logger.info("Tomato:Scanning") + + try: + # Query for new devices + exec(self._tomato_request("devlist")) + + self.last_results = [mac for iface, mac, rssi, tx, rx, quality, unknown_num in wldev] + self.date_updated = datetime.now() + + except Exception as e: + self.logger.exception("Scanning failed") + + + self.lock.release() + return self.last_results + + def _tomato_request(self, action): + """ Talk to the Tomato API. """ + # Get router info + req = requests.post('http://{}/update.cgi'.format(self.host), + data={'_http_id':self.http_id, 'exec':action}, + auth=requests.auth.HTTPBasicAuth(self.username, self.password)) + + return req.text + + + +""" +Tomato API: +for ip, mac, iface in arplist: + pass + +print wlnoise + +print dhcpd_static + +for iface, mac, rssi, tx, rx, quality, unknown_num in wldev: + print mac, quality + +for name, ip, mac, lease in dhcpd_lease: + if name: + print name, ip + + else: + print ip +""" diff --git a/start.py b/start.py index c14413f669b..79ff571cc1d 100644 --- a/start.py +++ b/start.py @@ -1,12 +1,21 @@ -from homeassistant.HomeAssistant import HomeAssistant +from ConfigParser import SafeConfigParser -from homeassistant.actor.HueLightControl import HueLightControl -from homeassistant.observer.TomatoDeviceScanner import TomatoDeviceScanner +from homeassistant import HomeAssistant -ha = HomeAssistant() +from homeassistant.actors import HueLightControl +from homeassistant.observers import TomatoDeviceScanner + +config = SafeConfigParser() +config.read("home-assistant.conf") + +tomato = TomatoDeviceScanner(config.get('tomato','host'), config.get('tomato','username'), + config.get('tomato','password'), config.get('tomato','http_id')) + + +ha = HomeAssistant(config.get("common","latitude"), config.get("common","longitude")) + +ha.setup_light_trigger(tomato, HueLightControl()) -ha.setup_device_tracker(TomatoDeviceScanner(ha.get_config())) -ha.setup_light_trigger(HueLightControl(ha.get_config())) ha.setup_http_interface() ha.start()