Ported codebase to Python 3. Long Live Python 3!
This commit is contained in:
parent
8fdf2d608a
commit
7e06d535ab
14 changed files with 105 additions and 73 deletions
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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"))
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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. """
|
||||
|
|
2
homeassistant/external/pynetgear
vendored
2
homeassistant/external/pynetgear
vendored
|
@ -1 +1 @@
|
|||
Subproject commit 9e094c322890bbcdd6cf4b0af4fd763f227cd64f
|
||||
Subproject commit bc635995789fc91c9b4a2fecaeb50241ab4fbf2a
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -1,2 +1 @@
|
|||
python -B -m unittest homeassistant.test
|
||||
|
||||
python3 -B -m unittest homeassistant.test
|
||||
|
|
1
start.py
1
start.py
|
@ -1,4 +1,3 @@
|
|||
#!/usr/bin/python2
|
||||
""" Starts home assistant with all possible functionality. """
|
||||
|
||||
import homeassistant.bootstrap
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue