hass-core/homeassistant/observers.py
2013-09-30 18:59:43 -07:00

279 lines
10 KiB
Python

"""
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 re
import json
import requests
import ephem
from . import track_time_change
STATE_CATEGORY_SUN = "weather.sun"
STATE_CATEGORY_ALL_DEVICES = 'all_devices'
STATE_CATEGORY_DEVICE_FORMAT = '{}'
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 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()
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 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
initial_search = device_scanner.get_active_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.set_state(self.devices_to_track[device]['category'], new_state)
# Update all devices state
statemachine.set_state(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.get_active_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()
self.date_updated = None
self.last_results = None
# Read known devices if file exists
if os.path.isfile(TOMATO_KNOWN_DEVICES_FILE):
with open(TOMATO_KNOWN_DEVICES_FILE) as inp:
self.known_devices = { row['mac']: row for row in csv.DictReader(inp) }
# Create a dict with ID: NAME of the devices to track
self.devices_to_track = {mac: info['name'] for mac, info in self.known_devices.items() if info['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 get_active_devices(self):
""" Scans for new devices and returns a list containing device_ids. """
self._update_tomato_info()
return [item[1] for item in self.last_results['wldev']]
def _update_tomato_info(self):
""" Ensures the information from the Tomato router is up to date.
Returns boolean if successful. """
# if date_updated is not defined (update has never ran) or the date is too old we scan for new data
if self.date_updated is None or datetime.now() - self.date_updated > TOMATO_MIN_TIME_BETWEEN_SCANS:
self.lock.acquire()
self.logger.info("Tomato:Scanning")
try:
req = requests.post('http://{}/update.cgi'.format(self.host),
data={'_http_id':self.http_id, 'exec':'devlist'},
auth=requests.auth.HTTPBasicAuth(self.username, self.password))
"""
Tomato API:
arplist contains a list of lists with items:
- ip (string)
- mac (string)
- iface (string)
wldev contains list of lists with items:
- iface (string)
- mac (string)
- rssi (int)
- tx (int)
- rx (int)
- quality (int)
- unknown_num (int)
dhcpd_lease contains a list of lists with items:
- name (string)
- ip (string)
- mac (string)
- lease_age (string)
"""
self.last_results = {param: json.loads(value.replace("'",'"'))
for param, value in re.findall(r"(?P<param>\w*) = (?P<value>.*);", req.text)
if param in ["wldev","dhcpd_lease"]}
self.date_updated = datetime.now()
# If we come along any unknown devices we will write them to the known devices file
unknown_devices = [(name, mac) for name, _, mac, _ in self.last_results['dhcpd_lease'] if mac not in self.known_devices]
if len(unknown_devices) > 0:
self.logger.info("Tomato:Found {} new devices, updating {}".format(len(unknown_devices), TOMATO_KNOWN_DEVICES_FILE))
with open(TOMATO_KNOWN_DEVICES_FILE, 'a') as outp:
writer = csv.writer(outp)
for name, mac in unknown_devices:
writer.writerow((mac, name, 0))
self.known_devices[mac] = {'name':name, 'track': '0'}
except requests.ConnectionError:
self.logger.exception("Tomato:Scanning failed")
return False
except IOError:
self.logger.exception("Tomato:Updating {} failed".format(TOMATO_KNOWN_DEVICES_FILE))
return True
finally:
self.lock.release()
return True