From a68704750bca61dead921b9b1cd39e4dc8e9362e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 21 Sep 2013 17:59:31 -0700 Subject: [PATCH] Code cleanup and reorg --- app/EventBus.py | 12 +++- app/HomeAssistant.py | 42 ++++++++--- app/HttpInterface.py | 59 ++++++++------- app/StateMachine.py | 13 +++- app/actor/HueLightControl.py | 52 ++++++++++++++ app/actor/HueTrigger.py | 107 ---------------------------- app/actor/LightTrigger.py | 75 +++++++++++++++++++ app/{ => observer}/DeviceTracker.py | 16 ++--- app/observer/Timer.py | 19 ++++- app/observer/TomatoDeviceScanner.py | 35 ++++----- app/observer/WeatherWatcher.py | 16 +++-- app/util.py | 5 +- start.py | 3 +- 13 files changed, 269 insertions(+), 185 deletions(-) create mode 100644 app/actor/HueLightControl.py delete mode 100644 app/actor/HueTrigger.py create mode 100644 app/actor/LightTrigger.py rename app/{ => observer}/DeviceTracker.py (83%) diff --git a/app/EventBus.py b/app/EventBus.py index dd10c2f63c0..b523221ca0a 100644 --- a/app/EventBus.py +++ b/app/EventBus.py @@ -7,12 +7,15 @@ 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, @@ -21,7 +24,7 @@ class EventBus(object): def run(): self.lock.acquire() - self.logger.info("{} event received: {}".format(event.event_type, event.data)) + 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) @@ -43,16 +46,21 @@ class EventBus(object): 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 added for event {}. Total: {}".format(event_type, len(self.listeners[event_type]))) + 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 diff --git a/app/HomeAssistant.py b/app/HomeAssistant.py index 342a7e59bee..d61ac98b8cf 100644 --- a/app/HomeAssistant.py +++ b/app/HomeAssistant.py @@ -1,19 +1,25 @@ +import logging from ConfigParser import SafeConfigParser import time from app.StateMachine import StateMachine from app.EventBus import EventBus -from app.DeviceTracker import DeviceTracker from app.HttpInterface import HttpInterface +from app.observer.DeviceTracker import DeviceTracker from app.observer.WeatherWatcher import WeatherWatcher from app.observer.Timer import Timer -from app.actor.HueTrigger import HueTrigger +from app.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 @@ -22,19 +28,22 @@ class HomeAssistant(object): self.weatherwatcher = None self.devicetracker = None - self.huetrigger = 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("home-assistant.conf") + 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 @@ -42,6 +51,7 @@ class HomeAssistant(object): 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 @@ -49,12 +59,15 @@ class HomeAssistant(object): 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 @@ -62,29 +75,36 @@ class HomeAssistant(object): 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_hue_trigger(self): - if self.huetrigger is None: - assert self.devicetracker is not None, "Cannot setup Hue Trigger without a device tracker being setup" + 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.huetrigger = HueTrigger(self.get_config(), self.get_event_bus(), self.get_state_machine(), self.devicetracker, self.setup_weather_watcher()) + self.lighttrigger = LightTrigger(self.get_event_bus(), self.get_state_machine(), self.devicetracker, self.setup_weather_watcher(), light_control) - return self.huetrigger + return self.lighttrigger def setup_http_interface(self): - self.httpinterface = HttpInterface(self.get_event_bus(), self.get_state_machine()) - self.httpinterface.start() + 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().start() + if self.httpinterface is not None: + self.httpinterface.start() + while True: try: time.sleep(1) diff --git a/app/HttpInterface.py b/app/HttpInterface.py index bbd31d4a27a..401f20f05bc 100644 --- a/app/HttpInterface.py +++ b/app/HttpInterface.py @@ -1,5 +1,6 @@ import threading import urlparse +import logging import requests @@ -8,10 +9,42 @@ from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer SERVER_HOST = '127.0.0.1' SERVER_PORT = 8080 +class HttpInterface(threading.Thread): + """ Provides an HTTP interface for Home Assistant. """ + + def __init__(self, eventbus, statemachine): + threading.Thread.__init__(self) + + self.server = HTTPServer((SERVER_HOST, SERVER_PORT), RequestHandler) + + self.server.eventbus = eventbus + self.server.statemachine = statemachine + + self._stop = threading.Event() + + + def run(self): + """ Start the HTTP interface. """ + logging.getLogger(__name__).info("Starting") + + while not self._stop.is_set(): + self.server.handle_request() + + + def stop(self): + """ Stop the HTTP interface. """ + self._stop.set() + + # Trigger a fake request to get the server to quit + requests.get("http://{}:{}".format(SERVER_HOST, SERVER_PORT)) + class RequestHandler(BaseHTTPRequestHandler): + """ Handles incoming HTTP requests """ #Handler for the GET requests def do_GET(self): + """ Handle incoming GET requests. """ + if self.path == "/": self.send_response(200) self.send_header('Content-type','text/html') @@ -51,6 +84,8 @@ class RequestHandler(BaseHTTPRequestHandler): def do_POST(self): + """ Handle incoming POST requests. """ + length = int(self.headers['Content-Length']) post_data = urlparse.parse_qs(self.rfile.read(length)) @@ -63,27 +98,3 @@ class RequestHandler(BaseHTTPRequestHandler): else: self.send_response(404) - - -class HttpInterface(threading.Thread): - - def __init__(self, eventbus, statemachine): - threading.Thread.__init__(self) - - self.server = HTTPServer((SERVER_HOST, SERVER_PORT), RequestHandler) - - self.server.eventbus = eventbus - self.server.statemachine = statemachine - - self._stop = threading.Event() - - - def run(self): - while not self._stop.is_set(): - self.server.handle_request() - - def stop(self): - self._stop.set() - - # Trigger a fake request to get the server to quit - requests.get("http://{}:{}".format(SERVER_HOST, SERVER_PORT)) diff --git a/app/StateMachine.py b/app/StateMachine.py index 5c59c9b89ed..175b784d823 100644 --- a/app/StateMachine.py +++ b/app/StateMachine.py @@ -3,13 +3,14 @@ from threading import RLock from datetime import datetime from app.EventBus import Event -from app.util import ensure_list, matcher +from app.util import 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() @@ -17,9 +18,11 @@ class StateMachine(object): 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) @@ -34,22 +37,26 @@ class StateMachine(object): 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): - from_state = ensure_list(from_state) - to_state = ensure_list(to_state) + """ Helper method to track specific state changes. """ + from_state = list(from_state) + to_state = list(to_state) def listener(event): assert isinstance(event, Event), "event needs to be of Event type" diff --git a/app/actor/HueLightControl.py b/app/actor/HueLightControl.py new file mode 100644 index 00000000000..c202ee86b98 --- /dev/null +++ b/app/actor/HueLightControl.py @@ -0,0 +1,52 @@ +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/app/actor/HueTrigger.py b/app/actor/HueTrigger.py deleted file mode 100644 index f9e36b5a994..00000000000 --- a/app/actor/HueTrigger.py +++ /dev/null @@ -1,107 +0,0 @@ -import logging -from datetime import timedelta - -from phue import Bridge - -from app.observer.WeatherWatcher import STATE_CATEGORY_SUN, SUN_STATE_BELOW_HORIZON, SUN_STATE_ABOVE_HORIZON -from app.StateMachine import track_state_change -from app.DeviceTracker import STATE_CATEGORY_ALL_DEVICES, STATE_DEVICE_HOME, STATE_DEVICE_NOT_HOME -from app.observer.Timer import track_time_change - -LIGHTS_TURNING_ON_BEFORE_SUN_SET_PERIOD = timedelta(minutes=15) - -LIGHT_TRANSITION_TIME_HUE = 9000 # 1/10th seconds -LIGHT_TRANSITION_TIME = timedelta(seconds=LIGHT_TRANSITION_TIME_HUE/10) - -class HueTrigger(object): - def __init__(self, config, eventbus, statemachine, device_tracker, weather): - self.eventbus = eventbus - self.statemachine = statemachine - self.weather = weather - - self.bridge = Bridge(config.get("hue","host") if config.has_option("hue","host") else None) - self.lights = self.bridge.get_light_objects() - self.logger = logging.getLogger(__name__) - - # 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 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 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) - - # If the sun is already above horizon schedule the time-based pre-sun set event - if statemachine.is_state(STATE_CATEGORY_SUN, SUN_STATE_ABOVE_HORIZON): - self.handle_sun_rising(None, None, None) - - - def get_lights_status(self): - lights_are_on = sum([1 for light in self.lights if light.on]) > 0 - - light_needed = not lights_are_on and self.statemachine.is_state(STATE_CATEGORY_SUN, SUN_STATE_BELOW_HORIZON) - - return lights_are_on, light_needed - - def turn_light_on(self, light_id=None, transitiontime=None): - if light_id is None: - light_id = [light.light_id for light in self.lights] - - command = {'on': True, 'xy': [0.5119, 0.4147], 'bri':164} - - if transitiontime is not None: - command['transitiontime'] = transitiontime - - self.bridge.set_light(light_id, command) - - - def turn_light_off(self, light_id=None, transitiontime=None): - if light_id is None: - light_id = [light.light_id for light in self.lights] - - command = {'on': False} - - if transitiontime is not None: - command['transitiontime'] = transitiontime - - self.bridge.set_light(light_id, command) - - - def handle_sun_rising(self, category, old_state, new_state): - """The moment sun sets we want to have all the lights on. - We will schedule to have each light start after one another - and slowly transition in.""" - - start_point = self.weather.next_sun_setting() - LIGHT_TRANSITION_TIME * len(self.lights) - - # 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): - return lambda now: self.turn_light_on_before_sunset(light_id) - - for index, light in enumerate(self.lights): - track_time_change(self.eventbus, turn_on(light.light_id), - point_in_time=start_point + index * LIGHT_TRANSITION_TIME) - - - 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.bridge.get_light(light_id, 'on'): - self.turn_light_on(light_id, LIGHT_TRANSITION_TIME_HUE) - - - def handle_device_state_change(self, category, old_state, new_state): - lights_are_on, light_needed = self.get_lights_status() - - # Specific device came home ? - if category != STATE_CATEGORY_ALL_DEVICES and new_state.state == STATE_DEVICE_HOME and light_needed: - self.logger.info("Home coming event for {}. Turning lights on".format(category)) - self.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: - self.logger.info("Everyone has left but lights are on. Turning lights off") - self.turn_light_off() diff --git a/app/actor/LightTrigger.py b/app/actor/LightTrigger.py new file mode 100644 index 00000000000..8f812ffe8f7 --- /dev/null +++ b/app/actor/LightTrigger.py @@ -0,0 +1,75 @@ +import logging +from datetime import datetime, timedelta + +from app.observer.WeatherWatcher import STATE_CATEGORY_SUN, SUN_STATE_BELOW_HORIZON, SUN_STATE_ABOVE_HORIZON +from app.observer.DeviceTracker import STATE_CATEGORY_ALL_DEVICES, STATE_DEVICE_HOME, STATE_DEVICE_NOT_HOME +from app.StateMachine import track_state_change +from app.observer.Timer import track_time_change + +LIGHT_TRANSITION_TIME = timedelta(minutes=15) + +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): + self.eventbus = eventbus + self.statemachine = statemachine + self.weather = weather + self.light_control = light_control + + self.logger = logging.getLogger(__name__) + + # 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 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 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) + + # If the sun is already above horizon schedule the time-based pre-sun set event + if statemachine.is_state(STATE_CATEGORY_SUN, SUN_STATE_ABOVE_HORIZON): + self._handle_sun_rising(None, None, None) + + + def _handle_sun_rising(self, category, old_state, new_state): + """The moment sun sets we want to have all the lights on. + We will schedule to have each light start after one another + and slowly transition in.""" + + 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): + return lambda now: self._turn_light_on_before_sunset(light_id) + + for index, light_id in enumerate(self.light_control.light_ids): + track_time_change(self.eventbus, turn_on(light_id), + point_in_time=start_point + index * LIGHT_TRANSITION_TIME) + + + 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): + self.light_control.turn_light_on(light_id, LIGHT_TRANSITION_TIME.seconds) + + + def _handle_device_state_change(self, category, old_state, new_state): + """ Function to handle tracked device state changes. """ + lights_are_on = self.light_control.is_light_on() + + 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: + 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: + self.logger.info("Everyone has left but lights are on. Turning lights off") + self.light_control.turn_light_off() diff --git a/app/DeviceTracker.py b/app/observer/DeviceTracker.py similarity index 83% rename from app/DeviceTracker.py rename to app/observer/DeviceTracker.py index b8f4f401e25..c3d04db53a8 100644 --- a/app/DeviceTracker.py +++ b/app/observer/DeviceTracker.py @@ -16,6 +16,7 @@ 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 @@ -40,18 +41,12 @@ class DeviceTracker(object): 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 set_state(self, device, state): - if state == STATE_DEVICE_HOME: - self.devices_to_track[device]['last_seen'] = datetime.now() - - self.statemachine.set_state(self.devices_to_track[device]['category'], state) - - def update_devices(self, found_devices): - """Keep track of devices that are home, all that are not will be marked not home""" + """ Keep track of devices that are home, all that are not will be marked not home. """ temp_tracking_devices = self.devices_to_track.keys() @@ -60,7 +55,8 @@ class DeviceTracker(object): if device in temp_tracking_devices: temp_tracking_devices.remove(device) - self.set_state(device, STATE_DEVICE_HOME) + 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 @@ -68,7 +64,7 @@ class DeviceTracker(object): # 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.set_state(device, STATE_DEVICE_NOT_HOME) + 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] diff --git a/app/observer/Timer.py b/app/observer/Timer.py index f6cbd46ff63..df8b9c98b05 100644 --- a/app/observer/Timer.py +++ b/app/observer/Timer.py @@ -1,27 +1,40 @@ +import logging from datetime import datetime import threading import time from app.EventBus import Event -from app.util import ensure_list, matcher +from app.util import 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() + def stop(self): + """ Tell the timer to stop. """ self._stop.set() + def run(self): + """ Start the timer. """ + + logging.getLogger(__name__).info("Starting") + now = datetime.now() while True: @@ -40,8 +53,8 @@ class Timer(threading.Thread): 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) + year, month, day = list(year), list(month), list(day) + hour, minute, second = list(hour), list(minute), list(second) def listener(event): assert isinstance(event, Event), "event needs to be of Event type" diff --git a/app/observer/TomatoDeviceScanner.py b/app/observer/TomatoDeviceScanner.py index 3fac045a231..4e64312d0b7 100644 --- a/app/observer/TomatoDeviceScanner.py +++ b/app/observer/TomatoDeviceScanner.py @@ -11,7 +11,7 @@ MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5) KNOWN_DEVICES_FILE = "tomato_known_devices.csv" class TomatoDeviceScanner(object): - # self.logger + """ This class tracks devices connected to a wireless router running Tomato firmware. """ def __init__(self, config): self.config = config @@ -30,7 +30,7 @@ class TomatoDeviceScanner(object): writer = csv.writer(outp) # Query for new devices - exec(self.tomato_request("devlist")) + exec(self._tomato_request("devlist")) for name, _, mac, _ in dhcpd_lease: if mac not in known_devices: @@ -48,32 +48,34 @@ class TomatoDeviceScanner(object): # 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.logging.warning("Found no devices to track. Please update {}.".format(KNOWN_DEVICES_FILE)) + 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 for new devices") + self.logger.info("Scanning") try: # Query for new devices - exec(self.tomato_request("devlist")) + exec(self._tomato_request("devlist")) self.last_results = [mac for iface, mac, rssi, tx, rx, quality, unknown_num in wldev] - except Exception: + except Exception as e: self.logger.exception("Scanning failed") self.lock.release() return self.last_results - def tomato_request(self, action): + 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}, @@ -84,22 +86,21 @@ class TomatoDeviceScanner(object): """ +Tomato API: for ip, mac, iface in arplist: - pass + pass -# print wlnoise +print wlnoise -# print dhcpd_static +print dhcpd_static for iface, mac, rssi, tx, rx, quality, unknown_num in wldev: - print mac, quality - -print "" + print mac, quality for name, ip, mac, lease in dhcpd_lease: - if name: - print name, ip + if name: + print name, ip - else: - print ip + else: + print ip """ diff --git a/app/observer/WeatherWatcher.py b/app/observer/WeatherWatcher.py index b7d4db6c94c..0ae3752782f 100644 --- a/app/observer/WeatherWatcher.py +++ b/app/observer/WeatherWatcher.py @@ -11,6 +11,8 @@ 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 @@ -25,15 +27,21 @@ class WeatherWatcher(object): statemachine.add_category(STATE_CATEGORY_SUN, SUN_STATE_BELOW_HORIZON) - self.update_sun_state() + self._update_sun_state() + def next_sun_rising(self): + """ Returns a datetime object that points at the next sun rising. """ return ephem.localtime(self.observer.next_rising(self.sun)) + def next_sun_setting(self): + """ Returns a datetime object that points at the next sun setting. """ return ephem.localtime(self.observer.next_setting(self.sun)) - def update_sun_state(self, now=None): + + def _update_sun_state(self, now=None): + """ Updates the state of the sun and schedules when to check next. """ next_rising = self.next_sun_rising() next_setting = self.next_sun_setting() @@ -45,9 +53,9 @@ class WeatherWatcher(object): new_state = SUN_STATE_BELOW_HORIZON next_change = next_rising - self.logger.info("Updating sun state to {}. Next change: {}".format(new_state, next_change)) + 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)) + track_time_change(self.eventbus, self._update_sun_state, point_in_time=next_change + timedelta(seconds=10)) diff --git a/app/util.py b/app/util.py index 78fa8da6959..e1db519b150 100644 --- a/app/util.py +++ b/app/util.py @@ -1,5 +1,4 @@ -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/start.py b/start.py index b27c4fc5eb8..4206bbb3093 100644 --- a/start.py +++ b/start.py @@ -1,11 +1,12 @@ from app.HomeAssistant import HomeAssistant +from app.actor.HueLightControl import HueLightControl from app.observer.TomatoDeviceScanner import TomatoDeviceScanner ha = HomeAssistant() ha.setup_device_tracker(TomatoDeviceScanner(ha.get_config())) -ha.setup_hue_trigger() +ha.setup_light_trigger(HueLightControl(ha.get_config())) ha.setup_http_interface() ha.start()