Code cleanup and reorg

This commit is contained in:
Paulus Schoutsen 2013-09-21 17:59:31 -07:00
parent ec0ca6ac38
commit a68704750b
13 changed files with 269 additions and 185 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

75
app/actor/LightTrigger.py Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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