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

View file

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

View file

@ -113,7 +113,7 @@ def setup(bus, statemachine):
entity_id = util.ensure_unique_string(
ENTITY_ID_FORMAT.format(
util.slugify(cast.device.friendly_name)),
casts.keys())
list(casts.keys()))
casts[entity_id] = cast
@ -189,8 +189,7 @@ def setup(bus, statemachine):
yield entity_id, cast
else:
for item in casts.items():
yield item
yield from casts.items()
def turn_off_service(service):
""" 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)
if ramp:
ramp.next()
next(ramp)
update_chromecast_state(entity_id, cast)
def play_youtube_video_service(service, video_id):

View file

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

View file

@ -124,7 +124,7 @@ class DeviceTracker(object):
# 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 (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'],
components.STATE_NOT_HOME)

View file

@ -72,8 +72,8 @@ import threading
import logging
import re
import os
from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
from urlparse import urlparse, parse_qs
from http.server import BaseHTTPRequestHandler, HTTPServer
from urllib.parse import urlparse, parse_qs
import homeassistant as ha
import homeassistant.util as util
@ -138,6 +138,7 @@ class HTTPInterface(threading.Thread):
self.server.serve_forever()
# pylint: disable=too-many-public-methods
class RequestHandler(BaseHTTPRequestHandler):
""" Handles incoming HTTP requests """
@ -188,7 +189,8 @@ class RequestHandler(BaseHTTPRequestHandler):
content_length = int(self.headers.get('Content-Length', 0))
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:
api_password = data['api_password'][0]
@ -282,7 +284,7 @@ class RequestHandler(BaseHTTPRequestHandler):
"</form>"
"</div>"
"</body></html>").format(self.path))
"</body></html>").format(self.path).encode("UTF-8"))
return False
@ -290,7 +292,7 @@ class RequestHandler(BaseHTTPRequestHandler):
def _handle_get_root(self, path_match, data):
""" 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_header('Content-type', 'text/html; charset=utf-8')
@ -335,13 +337,13 @@ class RequestHandler(BaseHTTPRequestHandler):
state = self.server.statemachine.get_state(entity_id)
attributes = u"<br>".join(
[u"{}: {}".format(attr, state.attributes[attr])
attributes = "<br>".join(
["{}: {}".format(attr, state.attributes[attr])
for attr in state.attributes])
write((u"<tr>"
u"<td>{}</td><td>{}</td><td>{}</td><td>{}</td>"
u"</tr>").format(
write(("<tr>"
"<td>{}</td><td>{}</td><td>{}</td><td>{}</td>"
"</tr>").format(
entity_id,
state.state,
attributes,
@ -686,4 +688,5 @@ class RequestHandler(BaseHTTPRequestHandler):
self.end_headers()
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
# 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_FORMAT.format(util.slugify(name)),
ent_to_light.keys())
list(ent_to_light.keys()))
ent_to_light[entity_id] = light_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)
if os.path.isfile(file_path):
with open(file_path, 'rb') as inp:
with open(file_path) as inp:
reader = csv.reader(inp)
# Skip the header
@ -249,7 +249,7 @@ def setup(bus, statemachine, light_control):
if entity_id in ent_to_light]
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)

View file

@ -77,7 +77,7 @@ def setup(bus, statemachine):
# New device, set it up
entity_id = util.ensure_unique_string(
ENTITY_ID_FORMAT.format(util.slugify(device.name)),
ent_to_dev.keys())
list(ent_to_dev.keys()))
sno_to_ent[device.serialnumber] = entity_id
ent_to_dev[entity_id] = device
@ -115,7 +115,7 @@ def setup(bus, statemachine):
# Track all lights in a group
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):
""" 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 logging
import json
import urlparse
import urllib.parse
import requests
@ -34,7 +34,7 @@ def _setup_call_api(host, port, api_password):
data = data or {}
data['api_password'] = api_password
url = urlparse.urljoin(base_url, path)
url = urllib.parse.urljoin(base_url, path)
try:
if method == METHOD_GET:
@ -61,14 +61,12 @@ class JSONEncoder(json.JSONEncoder):
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
a remote bus.
"""
def __init__(self, host, api_password, port=None):
ha.Bus.__init__(self)
self.logger = logging.getLogger(__name__)
self._call_api = _setup_call_api(host, port, api_password)
@ -172,6 +170,12 @@ class Bus(ha.Bus):
self.logger.error("Bus:{}".format(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):
""" Not implemented for remote bus.
@ -192,14 +196,12 @@ class Bus(ha.Bus):
raise NotImplementedError
class StateMachine(ha.StateMachine):
class StateMachine(object):
""" Drop-in replacement for a normal statemachine that communicates with a
remote statemachine.
"""
def __init__(self, host, api_password, port=None):
ha.StateMachine.__init__(self, None)
self._call_api = _setup_call_api(host, port, api_password)
self.lock = threading.Lock()
@ -297,3 +299,11 @@ class StateMachine(ha.StateMachine):
self.logger.exception("StateMachine:Got unexpected result (2)")
raise ha.HomeAssistantError(
"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 Queue
import queue
import datetime
import re
RE_SANITIZE_FILENAME = re.compile(r'(~|\.\.|/|\\)')
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):
@ -59,13 +64,13 @@ def filter_entity_ids(entity_ids, domain_filter=None, strip_domain=False):
def repr_helper(inp):
""" Helps creating a more readable string representation of objects. """
if isinstance(inp, dict):
return u", ".join(
repr_helper(key)+u"="+repr_helper(item) for key, item
return ", ".join(
repr_helper(key)+"="+repr_helper(item) for key, item
in inp.items())
elif isinstance(inp, datetime.datetime):
return datetime_to_str(inp)
else:
return unicode(inp)
return str(inp)
# Taken from: http://www.cse.unr.edu/~quiroz/inc/colortransforms.py
@ -146,25 +151,38 @@ class ThreadPool(object):
# pylint: disable=too-few-public-methods
def __init__(self, worker_count, job_handler):
queue = self.queue = Queue.PriorityQueue()
work_queue = self.work_queue = queue.PriorityQueue()
current_jobs = self.current_jobs = []
for _ in xrange(worker_count):
for _ in range(worker_count):
worker = threading.Thread(target=_threadpool_worker,
args=(queue, current_jobs, job_handler))
args=(work_queue, current_jobs,
job_handler))
worker.daemon = True
worker.start()
def add_job(self, priority, job):
""" 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. """
while True:
# Get new item from queue
job = queue.get()[1]
# Get new item from work_queue
job = work_queue.get().item
# Add to current running jobs
job_log = (datetime.datetime.now(), job)
@ -176,5 +194,5 @@ def _threadpool_worker(queue, current_jobs, job_handler):
# Remove from current running job
current_jobs.remove(job_log)
# Tell queue a task is done
queue.task_done()
# Tell work_queue a task is 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. """
import homeassistant.bootstrap