diff --git a/app/HomeAssistant.py b/app/HomeAssistant.py index 1150761496f..f34a245d114 100644 --- a/app/HomeAssistant.py +++ b/app/HomeAssistant.py @@ -3,7 +3,6 @@ import time from app.StateMachine import StateMachine from app.EventBus import EventBus -from app.Logging import EventLogger from app.DeviceTracker import DeviceTracker from app.observer.WeatherWatcher import WeatherWatcher @@ -72,7 +71,7 @@ class HomeAssistant: if self.huetrigger is None: assert self.devicetracker is not None, "Cannot setup Hue Trigger without a device tracker being setup" - self.huetrigger = HueTrigger(self.get_config(), self.get_event_bus(), self.get_state_machine(), self.devicetracker) + self.huetrigger = HueTrigger(self.get_config(), self.get_event_bus(), self.get_state_machine(), self.devicetracker, self.setup_weather_watcher()) return self.huetrigger diff --git a/app/actor/HueTrigger.py b/app/actor/HueTrigger.py index 0f5216a59b7..eb6dfe23c52 100644 --- a/app/actor/HueTrigger.py +++ b/app/actor/HueTrigger.py @@ -1,15 +1,19 @@ import logging -from datetime import datetime +from datetime import datetime, timedelta from phue import Bridge -from app.observer.WeatherWatcher import EVENT_PRE_SUN_SET_WARNING, STATE_CATEGORY_SUN, SOLAR_STATE_BELOW_HORIZON +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 +LIGHTS_TURNING_ON_BEFORE_SUN_SET_PERIOD = timedelta(minutes=20) + class HueTrigger: - def __init__(self, config, eventbus, statemachine, device_tracker): + 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")) self.lights = self.bridge.get_light_objects() @@ -22,14 +26,17 @@ class HueTrigger: # 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) - # Listen for when sun is about to set - eventbus.listen(EVENT_PRE_SUN_SET_WARNING, self.handle_sun_setting) + # 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.get_state(STATE_CATEGORY_SUN, SUN_STATE_ABOVE_HORIZON): + self.handle_sun_rising() 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.get_state(STATE_CATEGORY_SUN).state == SOLAR_STATE_BELOW_HORIZON + light_needed = not lights_are_on and self.statemachine.get_state(STATE_CATEGORY_SUN).state == SUN_STATE_BELOW_HORIZON return lights_are_on, light_needed @@ -52,16 +59,21 @@ class HueTrigger: self.bridge.set_light([1,2,3], command) + def handle_sun_rising(self, event=None): + # Schedule an event X minutes prior to sun setting + track_time_change(self.eventBus, self.handle_sun_setting, datetime=self.weather.next_sun_setting()-LIGHTS_TURNING_ON_BEFORE_SUN_SET_PERIOD) + + # Gets called when darkness starts falling in, slowly turn on the lights - def handle_sun_setting(self, event): + def handle_sun_setting(self, now): lights_are_on, light_needed = self.get_lights_status() - if light_needed and self.statemachine.get_state(STATE_CATEGORY_ALL_DEVICES).state == STATE_DEVICE_HOME: + if not lights_are_on and self.statemachine.get_state(STATE_CATEGORY_ALL_DEVICES).state == STATE_DEVICE_HOME: self.logger.info("Sun setting and devices home. Turning on lights.") # We will start the lights now and by the time the sun sets # the lights will be at full brightness - transitiontime = (event.data['sun_setting'] - datetime.now()).seconds * 10 + transitiontime = (self.weather.next_sun_setting() - datetime.now()).seconds * 10 self.turn_lights_on(transitiontime) diff --git a/app/observer/TomatoDeviceScanner.py b/app/observer/TomatoDeviceScanner.py index e27b7f96b94..4670e20367b 100644 --- a/app/observer/TomatoDeviceScanner.py +++ b/app/observer/TomatoDeviceScanner.py @@ -1,21 +1,32 @@ 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: # self.logger def __init__(self, config): self.config = config self.logger = logging.getLogger("TomatoDeviceScanner") + self.lock = Lock() + self.date_updated = None + self.last_results = None # Read known devices - with open('tomato_known_devices.csv') as inp: - known_devices = { row['mac']: row for row in csv.DictReader(inp) } + 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('tomato_known_devices.csv', 'a') as outp: + with open(KNOWN_DEVICES_FILE, 'a') as outp: writer = csv.writer(outp) # Query for new devices @@ -40,18 +51,24 @@ class TomatoDeviceScanner: return self.devices_to_track def scan_devices(self): - self.logger.info("Scanning for new devices") + 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") + + 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] + + except: + self.logger.error("Scanning failed") + - # Query for new devices - try: - exec(self.tomato_request("devlist")) - - return [mac for iface, mac, rssi, tx, rx, quality, unknown_num in wldev] - - except: - self.logger.error("Scanning failed") - - return [] + self.lock.release() + return self.last_results def tomato_request(self, action): # Get router info diff --git a/app/observer/WeatherWatcher.py b/app/observer/WeatherWatcher.py index 03e2c8bc7f0..3a409f86430 100644 --- a/app/observer/WeatherWatcher.py +++ b/app/observer/WeatherWatcher.py @@ -7,16 +7,10 @@ from app.EventBus import Event from app.observer.Timer import track_time_change -PRE_SUN_SET_WARNING_TIME = 20 # minutes +STATE_CATEGORY_SUN = "weather.sun" -EVENT_PRE_SUN_SET_WARNING = "sun_set_soon" - -STATE_CATEGORY_TEMPLATE_SOLAR = "solar.{}" - -STATE_CATEGORY_SUN = STATE_CATEGORY_TEMPLATE_SOLAR.format("sun") - -SOLAR_STATE_ABOVE_HORIZON = "above_horizon" -SOLAR_STATE_BELOW_HORIZON = "below_horizon" +SUN_STATE_ABOVE_HORIZON = "above_horizon" +SUN_STATE_BELOW_HORIZON = "below_horizon" class WeatherWatcher: def __init__(self, config, eventbus, statemachine): @@ -25,40 +19,39 @@ class WeatherWatcher: self.eventbus = eventbus self.statemachine = statemachine + self.observer = ephem.Observer() + self.observer.lat = self.config.get('common','latitude') + self.observer.long = self.config.get('common','longitude') + + self.sun = ephem.Sun() + statemachine.add_category(STATE_CATEGORY_SUN, SOLAR_STATE_BELOW_HORIZON) self.update_sun_state() - def update_sun_state(self, now=datetime.now()): - self.update_solar_state(ephem.Sun(), STATE_CATEGORY_SUN, self.update_sun_state) + def next_sun_rising(self): + return ephem.localtime(self.observer.next_rising(self.sun)) - def update_solar_state(self, solar_body, state_category, update_callback): - # We don't cache these objects because we use them so rarely - observer = ephem.Observer() - observer.lat = self.config.get('common','latitude') - observer.long = self.config.get('common','longitude') + def next_sun_setting(self): + return ephem.localtime(self.observer.next_setting(self.sun)) - next_rising = ephem.localtime(observer.next_rising(solar_body)) - next_setting = ephem.localtime(observer.next_setting(solar_body)) + def update_sun_state(self, update_callback): + next_rising = ephem.localtime(self.observer.next_rising(self.sun)) + next_setting = ephem.localtime(self.observer.next_setting(self.sun)) if next_rising > next_setting: - new_state = SOLAR_STATE_ABOVE_HORIZON + new_state = SUN_STATE_ABOVE_HORIZON next_change = next_setting else: - new_state = SOLAR_STATE_BELOW_HORIZON + new_state = SUN_STATE_BELOW_HORIZON next_change = next_rising - self.logger.info("Updating solar state for {} to {}. Next change: {}".format(state_category, new_state, next_change)) + self.logger.info("Updating solar state for {} to {}. Next change: {}".format(STATE_CATEGORY_SUN, new_state, next_change)) - self.statemachine.set_state(state_category, new_state) + self.statemachine.set_state(STATE_CATEGORY_SUN, new_state) # +10 seconds to be sure that the change has occured track_time_change(self.eventbus, update_callback, datetime=next_change + timedelta(seconds=10)) - - # If the sun is visible, schedule to fire an event X minutes before sun set - if solar_body.name == 'Sun' and new_state == SOLAR_STATE_ABOVE_HORIZON: - track_time_change(self.eventbus, lambda time: self.eventbus.fire(Event(EVENT_PRE_SUN_SET_WARNING, {'sun_setting':next_change})), - datetime=next_change - timedelta(minutes=PRE_SUN_SET_WARNING_TIME)) - + diff --git a/start.py b/start.py index dc081968c0e..5b0fefbd2a4 100644 --- a/start.py +++ b/start.py @@ -4,7 +4,6 @@ from app.observer.TomatoDeviceScanner import TomatoDeviceScanner ha = HomeAssistant() -ha.setup_weather_watcher() ha.setup_device_tracker(TomatoDeviceScanner(ha.get_config())) ha.setup_hue_trigger()