mirror of
https://github.com/home-assistant/core.git
synced 2025-07-18 18:57:06 +00:00
Code reorganized into a more Pythonic structure.
This commit is contained in:
parent
0981ea49d1
commit
a1f238816b
@ -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])
|
@ -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
|
@ -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):
|
||||
|
@ -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)
|
@ -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
|
||||
|
||||
|
@ -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)
|
@ -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)
|
@ -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
|
151
homeassistant/core.py
Normal file
151
homeassistant/core.py
Normal file
@ -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())]
|
@ -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)
|
@ -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)
|
@ -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
|
||||
"""
|
@ -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
|
349
homeassistant/observers.py
Normal file
349
homeassistant/observers.py
Normal file
@ -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
|
||||
"""
|
21
start.py
21
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()
|
||||
|
Loading…
x
Reference in New Issue
Block a user