Ported codebase to Python 3. Long Live Python 3!

This commit is contained in:
Paulus Schoutsen 2014-04-14 00:10:24 -07:00
parent 8fdf2d608a
commit 7e06d535ab
14 changed files with 105 additions and 73 deletions

View file

@ -1,7 +1,7 @@
Home Assistant Home Assistant
============== ==============
Home Assistant provides a platform for home automation. It does so by having modules that observe and trigger actors to do various tasks. Home Assistant is a home automation platform running on Python 3. It provides modules that observe and trigger actors to do various tasks.
It is currently able to do the following things: It is currently able to do the following things:
* Track if devices are home by monitoring connected devices to a wireless router (currently supporting modern Netgear routers or routers running Tomato firmware) * Track if devices are home by monitoring connected devices to a wireless router (currently supporting modern Netgear routers or routers running Tomato firmware)

View file

@ -171,7 +171,7 @@ def create_bus_job_handler(logger):
except Exception: # pylint: disable=broad-except except Exception: # pylint: disable=broad-except
# Catch any exception our service/event_listener might throw # Catch any exception our service/event_listener might throw
# We do not want to crash our ThreadPool # We do not want to crash our ThreadPool
logger.exception(u"BusHandler:Exception doing job") logger.exception("BusHandler:Exception doing job")
return job_handler return job_handler
@ -189,10 +189,10 @@ class ServiceCall(object):
def __repr__(self): def __repr__(self):
if self.data: if self.data:
return u"<ServiceCall {}.{}: {}>".format( return "<ServiceCall {}.{}: {}>".format(
self.domain, self.service, util.repr_helper(self.data)) self.domain, self.service, util.repr_helper(self.data))
else: else:
return u"<ServiceCall {}.{}>".format(self.domain, self.service) return "<ServiceCall {}.{}>".format(self.domain, self.service)
# pylint: disable=too-few-public-methods # pylint: disable=too-few-public-methods
@ -207,10 +207,10 @@ class Event(object):
def __repr__(self): def __repr__(self):
if self.data: if self.data:
return u"<Event {}: {}>".format( return "<Event {}: {}>".format(
self.event_type, util.repr_helper(self.data)) self.event_type, util.repr_helper(self.data))
else: else:
return u"<Event {}>".format(self.event_type) return "<Event {}>".format(self.event_type)
class Bus(object): class Bus(object):
@ -235,7 +235,7 @@ class Bus(object):
def services(self): def services(self):
""" Dict with per domain a list of available services. """ """ Dict with per domain a list of available services. """
with self.service_lock: with self.service_lock:
return {domain: self._services[domain].keys() return {domain: list(self._services[domain].keys())
for domain in self._services} for domain in self._services}
@property @property
@ -268,7 +268,7 @@ class Bus(object):
except KeyError: # if key domain or service does not exist except KeyError: # if key domain or service does not exist
raise ServiceDoesNotExistError( raise ServiceDoesNotExistError(
u"Service does not exist: {}/{}".format(domain, service)) "Service does not exist: {}/{}".format(domain, service))
def register_service(self, domain, service, service_func): def register_service(self, domain, service, service_func):
""" Register a service. """ """ Register a service. """
@ -290,7 +290,7 @@ class Bus(object):
event = Event(event_type, event_data) event = Event(event_type, event_data)
self.logger.info(u"Bus:Handling {}".format(event)) self.logger.info("Bus:Handling {}".format(event))
if not listeners: if not listeners:
return return
@ -363,7 +363,7 @@ class Bus(object):
def _check_busy(self): def _check_busy(self):
""" Complain if we have more than twice as many jobs queued as threads """ Complain if we have more than twice as many jobs queued as threads
and if we didn't complain about it recently. """ and if we didn't complain about it recently. """
if self.pool.queue.qsize() / self.thread_count >= 2 and \ if self.pool.work_queue.qsize() / self.thread_count >= 2 and \
dt.datetime.now()-self.last_busy_notice > BUS_REPORT_BUSY_TIMEOUT: dt.datetime.now()-self.last_busy_notice > BUS_REPORT_BUSY_TIMEOUT:
self.last_busy_notice = dt.datetime.now() self.last_busy_notice = dt.datetime.now()
@ -371,13 +371,13 @@ class Bus(object):
log_error = self.logger.error log_error = self.logger.error
log_error( log_error(
u"Bus:All {} threads are busy and {} jobs pending".format( "Bus:All {} threads are busy and {} jobs pending".format(
self.thread_count, self.pool.queue.qsize())) self.thread_count, self.pool.work_queue.qsize()))
jobs = self.pool.current_jobs jobs = self.pool.current_jobs
for start, job in jobs: for start, job in jobs:
log_error(u"Bus:Current job from {}: {}".format( log_error("Bus:Current job from {}: {}".format(
util.datetime_to_str(start), job)) util.datetime_to_str(start), job))
@ -436,11 +436,11 @@ class State(object):
def __repr__(self): def __repr__(self):
if self.attributes: if self.attributes:
return u"<state {}:{} @ {}>".format( return "<state {}:{} @ {}>".format(
self.state, util.repr_helper(self.attributes), self.state, util.repr_helper(self.attributes),
util.datetime_to_str(self.last_changed)) util.datetime_to_str(self.last_changed))
else: else:
return u"<state {} @ {}>".format( return "<state {} @ {}>".format(
self.state, util.datetime_to_str(self.last_changed)) self.state, util.datetime_to_str(self.last_changed))
@ -456,7 +456,7 @@ class StateMachine(object):
def entity_ids(self): def entity_ids(self):
""" List of entitie ids that are being tracked. """ """ List of entitie ids that are being tracked. """
with self.lock: with self.lock:
return self.states.keys() return list(self.states.keys())
def remove_entity(self, entity_id): def remove_entity(self, entity_id):
""" Removes a entity from the state machine. """ Removes a entity from the state machine.
@ -515,9 +515,9 @@ class StateMachine(object):
def is_state(self, entity_id, state): def is_state(self, entity_id, state):
""" Returns True if entity exists and is specified state. """ """ Returns True if entity exists and is specified state. """
try: try:
return self.get_state(entity_id).state == state return self.states.get(entity_id).state == state
except AttributeError: except AttributeError:
# get_state returned None # states.get returned None
return False return False

View file

@ -3,7 +3,7 @@ Provides methods to bootstrap a home assistant instance.
""" """
import importlib import importlib
import ConfigParser import configparser
import logging import logging
import homeassistant as ha import homeassistant as ha
@ -33,7 +33,7 @@ def from_config_file(config_path):
statusses = [] statusses = []
# Read config # Read config
config = ConfigParser.SafeConfigParser() config = configparser.SafeConfigParser()
config.read(config_path) config.read(config_path)
# Init core # Init core
@ -51,7 +51,7 @@ def from_config_file(config_path):
""" Failure proof option retriever. """ """ Failure proof option retriever. """
try: try:
return config.get(section, option) return config.get(section, option)
except (ConfigParser.NoSectionError, ConfigParser.NoOptionError): except (configparser.NoSectionError, configparser.NoOptionError):
return default return default
# Device scanner # Device scanner
@ -83,7 +83,7 @@ def from_config_file(config_path):
get_opt('device_tracker.netgear', 'username'), get_opt('device_tracker.netgear', 'username'),
get_opt('device_tracker.netgear', 'password')) get_opt('device_tracker.netgear', 'password'))
except ConfigParser.NoOptionError: except configparser.NoOptionError:
# If one of the options didn't exist # If one of the options didn't exist
logger.exception(("Error initializing {}DeviceScanner, " logger.exception(("Error initializing {}DeviceScanner, "
"could not find one of the following config " "could not find one of the following config "
@ -142,7 +142,11 @@ def from_config_file(config_path):
add_status("Light - Hue", light_control.success_init) add_status("Light - Hue", light_control.success_init)
light.setup(bus, statemachine, light_control) if light_control.success_init:
light.setup(bus, statemachine, light_control)
else:
light_control = None
else: else:
light_control = None light_control = None

View file

@ -113,7 +113,7 @@ def setup(bus, statemachine):
entity_id = util.ensure_unique_string( entity_id = util.ensure_unique_string(
ENTITY_ID_FORMAT.format( ENTITY_ID_FORMAT.format(
util.slugify(cast.device.friendly_name)), util.slugify(cast.device.friendly_name)),
casts.keys()) list(casts.keys()))
casts[entity_id] = cast casts[entity_id] = cast
@ -189,8 +189,7 @@ def setup(bus, statemachine):
yield entity_id, cast yield entity_id, cast
else: else:
for item in casts.items(): yield from casts.items()
yield item
def turn_off_service(service): def turn_off_service(service):
""" Service to exit any running app on the specified ChromeCast and """ Service to exit any running app on the specified ChromeCast and
@ -230,7 +229,7 @@ def setup(bus, statemachine):
ramp = cast.get_protocol(pychromecast.PROTOCOL_RAMP) ramp = cast.get_protocol(pychromecast.PROTOCOL_RAMP)
if ramp: if ramp:
ramp.next() next(ramp)
update_chromecast_state(entity_id, cast) update_chromecast_state(entity_id, cast)
def play_youtube_video_service(service, video_id): def play_youtube_video_service(service, video_id):

View file

@ -51,7 +51,7 @@ def setup(bus, statemachine,
next_setting = sun.next_setting(statemachine) next_setting = sun.next_setting(statemachine)
if next_setting: if next_setting:
return (next_setting - LIGHT_TRANSITION_TIME * len(light_ids)) return next_setting - LIGHT_TRANSITION_TIME * len(light_ids)
else: else:
return None return None

View file

@ -124,7 +124,7 @@ class DeviceTracker(object):
# Because we do not want to have stuff happening when the device does # Because we do not want to have stuff happening when the device does
# not show up for 1 scan beacuse of reboot etc # not show up for 1 scan beacuse of reboot etc
for device in temp_tracking_devices: for device in temp_tracking_devices:
if (now - known_dev[device]['last_seen'] > self.error_scanning): if now - known_dev[device]['last_seen'] > self.error_scanning:
self.statemachine.set_state(known_dev[device]['entity_id'], self.statemachine.set_state(known_dev[device]['entity_id'],
components.STATE_NOT_HOME) components.STATE_NOT_HOME)

View file

@ -72,8 +72,8 @@ import threading
import logging import logging
import re import re
import os import os
from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer from http.server import BaseHTTPRequestHandler, HTTPServer
from urlparse import urlparse, parse_qs from urllib.parse import urlparse, parse_qs
import homeassistant as ha import homeassistant as ha
import homeassistant.util as util import homeassistant.util as util
@ -138,6 +138,7 @@ class HTTPInterface(threading.Thread):
self.server.serve_forever() self.server.serve_forever()
# pylint: disable=too-many-public-methods
class RequestHandler(BaseHTTPRequestHandler): class RequestHandler(BaseHTTPRequestHandler):
""" Handles incoming HTTP requests """ """ Handles incoming HTTP requests """
@ -188,7 +189,8 @@ class RequestHandler(BaseHTTPRequestHandler):
content_length = int(self.headers.get('Content-Length', 0)) content_length = int(self.headers.get('Content-Length', 0))
if content_length: if content_length:
data.update(parse_qs(self.rfile.read(content_length))) data.update(parse_qs(self.rfile.read(
content_length).decode("UTF-8")))
try: try:
api_password = data['api_password'][0] api_password = data['api_password'][0]
@ -282,7 +284,7 @@ class RequestHandler(BaseHTTPRequestHandler):
"</form>" "</form>"
"</div>" "</div>"
"</body></html>").format(self.path)) "</body></html>").format(self.path).encode("UTF-8"))
return False return False
@ -290,7 +292,7 @@ class RequestHandler(BaseHTTPRequestHandler):
def _handle_get_root(self, path_match, data): def _handle_get_root(self, path_match, data):
""" Renders the debug interface. """ """ Renders the debug interface. """
write = lambda txt: self.wfile.write(txt.encode("UTF-8")+"\n") write = lambda txt: self.wfile.write((txt + "\n").encode("UTF-8"))
self.send_response(HTTP_OK) self.send_response(HTTP_OK)
self.send_header('Content-type', 'text/html; charset=utf-8') self.send_header('Content-type', 'text/html; charset=utf-8')
@ -335,13 +337,13 @@ class RequestHandler(BaseHTTPRequestHandler):
state = self.server.statemachine.get_state(entity_id) state = self.server.statemachine.get_state(entity_id)
attributes = u"<br>".join( attributes = "<br>".join(
[u"{}: {}".format(attr, state.attributes[attr]) ["{}: {}".format(attr, state.attributes[attr])
for attr in state.attributes]) for attr in state.attributes])
write((u"<tr>" write(("<tr>"
u"<td>{}</td><td>{}</td><td>{}</td><td>{}</td>" "<td>{}</td><td>{}</td><td>{}</td><td>{}</td>"
u"</tr>").format( "</tr>").format(
entity_id, entity_id,
state.state, state.state,
attributes, attributes,
@ -686,4 +688,5 @@ class RequestHandler(BaseHTTPRequestHandler):
self.end_headers() self.end_headers()
if data: if data:
self.wfile.write(json.dumps(data, indent=4, sort_keys=True)) self.wfile.write(
json.dumps(data, indent=4, sort_keys=True).encode("UTF-8"))

View file

@ -155,11 +155,11 @@ def setup(bus, statemachine, light_control):
# We have not seen this light before, set it up # We have not seen this light before, set it up
# Create entity id # Create entity id
logger.info(u"Found new light {}".format(name)) logger.info("Found new light {}".format(name))
entity_id = util.ensure_unique_string( entity_id = util.ensure_unique_string(
ENTITY_ID_FORMAT.format(util.slugify(name)), ENTITY_ID_FORMAT.format(util.slugify(name)),
ent_to_light.keys()) list(ent_to_light.keys()))
ent_to_light[entity_id] = light_id ent_to_light[entity_id] = light_id
light_to_ent[light_id] = entity_id light_to_ent[light_id] = entity_id
@ -218,7 +218,7 @@ def setup(bus, statemachine, light_control):
file_path = os.path.join(dir_path, LIGHT_PROFILES_FILE) file_path = os.path.join(dir_path, LIGHT_PROFILES_FILE)
if os.path.isfile(file_path): if os.path.isfile(file_path):
with open(file_path, 'rb') as inp: with open(file_path) as inp:
reader = csv.reader(inp) reader = csv.reader(inp)
# Skip the header # Skip the header
@ -249,7 +249,7 @@ def setup(bus, statemachine, light_control):
if entity_id in ent_to_light] if entity_id in ent_to_light]
if not light_ids: if not light_ids:
light_ids = ent_to_light.values() light_ids = list(ent_to_light.values())
transition = util.convert(dat.get(ATTR_TRANSITION), int) transition = util.convert(dat.get(ATTR_TRANSITION), int)

View file

@ -77,7 +77,7 @@ def setup(bus, statemachine):
# New device, set it up # New device, set it up
entity_id = util.ensure_unique_string( entity_id = util.ensure_unique_string(
ENTITY_ID_FORMAT.format(util.slugify(device.name)), ENTITY_ID_FORMAT.format(util.slugify(device.name)),
ent_to_dev.keys()) list(ent_to_dev.keys()))
sno_to_ent[device.serialnumber] = entity_id sno_to_ent[device.serialnumber] = entity_id
ent_to_dev[entity_id] = device ent_to_dev[entity_id] = device
@ -115,7 +115,7 @@ def setup(bus, statemachine):
# Track all lights in a group # Track all lights in a group
group.setup(bus, statemachine, group.setup(bus, statemachine,
GROUP_NAME_ALL_WEMOS, sno_to_ent.values()) GROUP_NAME_ALL_WEMOS, list(sno_to_ent.values()))
def _handle_wemo_service(service): def _handle_wemo_service(service):
""" Handles calls to the WeMo service. """ """ Handles calls to the WeMo service. """

@ -1 +1 @@
Subproject commit 9e094c322890bbcdd6cf4b0af4fd763f227cd64f Subproject commit bc635995789fc91c9b4a2fecaeb50241ab4fbf2a

View file

@ -12,7 +12,7 @@ HomeAssistantError will be raised.
import threading import threading
import logging import logging
import json import json
import urlparse import urllib.parse
import requests import requests
@ -34,7 +34,7 @@ def _setup_call_api(host, port, api_password):
data = data or {} data = data or {}
data['api_password'] = api_password data['api_password'] = api_password
url = urlparse.urljoin(base_url, path) url = urllib.parse.urljoin(base_url, path)
try: try:
if method == METHOD_GET: if method == METHOD_GET:
@ -61,14 +61,12 @@ class JSONEncoder(json.JSONEncoder):
return json.JSONEncoder.default(self, obj) return json.JSONEncoder.default(self, obj)
class Bus(ha.Bus): class Bus(object):
""" Drop-in replacement for a normal bus that will forward interaction to """ Drop-in replacement for a normal bus that will forward interaction to
a remote bus. a remote bus.
""" """
def __init__(self, host, api_password, port=None): def __init__(self, host, api_password, port=None):
ha.Bus.__init__(self)
self.logger = logging.getLogger(__name__) self.logger = logging.getLogger(__name__)
self._call_api = _setup_call_api(host, port, api_password) self._call_api = _setup_call_api(host, port, api_password)
@ -172,6 +170,12 @@ class Bus(ha.Bus):
self.logger.error("Bus:{}".format(error)) self.logger.error("Bus:{}".format(error))
raise ha.HomeAssistantError(error) raise ha.HomeAssistantError(error)
def has_service(self, domain, service):
""" Not implemented for remote bus.
Will throw NotImplementedError. """
raise NotImplementedError
def listen_event(self, event_type, listener): def listen_event(self, event_type, listener):
""" Not implemented for remote bus. """ Not implemented for remote bus.
@ -192,14 +196,12 @@ class Bus(ha.Bus):
raise NotImplementedError raise NotImplementedError
class StateMachine(ha.StateMachine): class StateMachine(object):
""" Drop-in replacement for a normal statemachine that communicates with a """ Drop-in replacement for a normal statemachine that communicates with a
remote statemachine. remote statemachine.
""" """
def __init__(self, host, api_password, port=None): def __init__(self, host, api_password, port=None):
ha.StateMachine.__init__(self, None)
self._call_api = _setup_call_api(host, port, api_password) self._call_api = _setup_call_api(host, port, api_password)
self.lock = threading.Lock() self.lock = threading.Lock()
@ -297,3 +299,11 @@ class StateMachine(ha.StateMachine):
self.logger.exception("StateMachine:Got unexpected result (2)") self.logger.exception("StateMachine:Got unexpected result (2)")
raise ha.HomeAssistantError( raise ha.HomeAssistantError(
"Got unexpected result (2): {}".format(req.text)) "Got unexpected result (2): {}".format(req.text))
def is_state(self, entity_id, state):
""" Returns True if entity exists and is specified state. """
try:
return self.get_state(entity_id).state == state
except AttributeError:
# get_state returned None
return False

View file

@ -1,13 +1,18 @@
""" Helper methods for various modules. """ """
homeassistant.util
~~~~~~~~~~~~~~~~~~
Helper methods for various modules.
"""
import threading import threading
import Queue import queue
import datetime import datetime
import re import re
RE_SANITIZE_FILENAME = re.compile(r'(~|\.\.|/|\\)') RE_SANITIZE_FILENAME = re.compile(r'(~|\.\.|/|\\)')
RE_SLUGIFY = re.compile(r'[^A-Za-z0-9_]+') RE_SLUGIFY = re.compile(r'[^A-Za-z0-9_]+')
DATE_STR_FORMAT = u"%H:%M:%S %d-%m-%Y" DATE_STR_FORMAT = "%H:%M:%S %d-%m-%Y"
def sanitize_filename(filename): def sanitize_filename(filename):
@ -59,13 +64,13 @@ def filter_entity_ids(entity_ids, domain_filter=None, strip_domain=False):
def repr_helper(inp): def repr_helper(inp):
""" Helps creating a more readable string representation of objects. """ """ Helps creating a more readable string representation of objects. """
if isinstance(inp, dict): if isinstance(inp, dict):
return u", ".join( return ", ".join(
repr_helper(key)+u"="+repr_helper(item) for key, item repr_helper(key)+"="+repr_helper(item) for key, item
in inp.items()) in inp.items())
elif isinstance(inp, datetime.datetime): elif isinstance(inp, datetime.datetime):
return datetime_to_str(inp) return datetime_to_str(inp)
else: else:
return unicode(inp) return str(inp)
# Taken from: http://www.cse.unr.edu/~quiroz/inc/colortransforms.py # Taken from: http://www.cse.unr.edu/~quiroz/inc/colortransforms.py
@ -146,25 +151,38 @@ class ThreadPool(object):
# pylint: disable=too-few-public-methods # pylint: disable=too-few-public-methods
def __init__(self, worker_count, job_handler): def __init__(self, worker_count, job_handler):
queue = self.queue = Queue.PriorityQueue() work_queue = self.work_queue = queue.PriorityQueue()
current_jobs = self.current_jobs = [] current_jobs = self.current_jobs = []
for _ in xrange(worker_count): for _ in range(worker_count):
worker = threading.Thread(target=_threadpool_worker, worker = threading.Thread(target=_threadpool_worker,
args=(queue, current_jobs, job_handler)) args=(work_queue, current_jobs,
job_handler))
worker.daemon = True worker.daemon = True
worker.start() worker.start()
def add_job(self, priority, job): def add_job(self, priority, job):
""" Add a job to be sent to the workers. """ """ Add a job to be sent to the workers. """
self.queue.put((priority, job)) self.work_queue.put(PriorityQueueItem(priority, job))
def _threadpool_worker(queue, current_jobs, job_handler): class PriorityQueueItem(object):
""" Holds a priority and a value. Used within PriorityQueue. """
# pylint: disable=too-few-public-methods
def __init__(self, priority, item):
self.priority = priority
self.item = item
def __lt__(self, other):
return self.priority < other.priority
def _threadpool_worker(work_queue, current_jobs, job_handler):
""" Provides the base functionality of a worker for the thread pool. """ """ Provides the base functionality of a worker for the thread pool. """
while True: while True:
# Get new item from queue # Get new item from work_queue
job = queue.get()[1] job = work_queue.get().item
# Add to current running jobs # Add to current running jobs
job_log = (datetime.datetime.now(), job) job_log = (datetime.datetime.now(), job)
@ -176,5 +194,5 @@ def _threadpool_worker(queue, current_jobs, job_handler):
# Remove from current running job # Remove from current running job
current_jobs.remove(job_log) current_jobs.remove(job_log)
# Tell queue a task is done # Tell work_queue a task is done
queue.task_done() work_queue.task_done()

View file

@ -1,2 +1 @@
python -B -m unittest homeassistant.test python3 -B -m unittest homeassistant.test

View file

@ -1,4 +1,3 @@
#!/usr/bin/python2
""" Starts home assistant with all possible functionality. """ """ Starts home assistant with all possible functionality. """
import homeassistant.bootstrap import homeassistant.bootstrap