Merge branch 'dev' into mysensors-component-switch

This commit is contained in:
MartinHjelmare 2015-12-18 03:39:41 +01:00
commit eae07c070a
24 changed files with 278 additions and 117 deletions

View File

@ -55,6 +55,7 @@ omit =
homeassistant/components/discovery.py homeassistant/components/discovery.py
homeassistant/components/downloader.py homeassistant/components/downloader.py
homeassistant/components/ifttt.py homeassistant/components/ifttt.py
homeassistant/components/influxdb.py
homeassistant/components/keyboard.py homeassistant/components/keyboard.py
homeassistant/components/light/hue.py homeassistant/components/light/hue.py
homeassistant/components/light/mqtt.py homeassistant/components/light/mqtt.py

View File

@ -53,9 +53,10 @@ def setup(hass, config):
bootstrap.setup_component(hass, 'sun') bootstrap.setup_component(hass, 'sun')
# Setup demo platforms # Setup demo platforms
demo_config = config.copy()
for component in COMPONENTS_WITH_DEMO_PLATFORM: for component in COMPONENTS_WITH_DEMO_PLATFORM:
bootstrap.setup_component( demo_config[component] = {CONF_PLATFORM: 'demo'}
hass, component, {component: {CONF_PLATFORM: 'demo'}}) bootstrap.setup_component(hass, component, demo_config)
# Setup room groups # Setup room groups
lights = sorted(hass.states.entity_ids('light')) lights = sorted(hass.states.entity_ids('light'))

View File

@ -44,7 +44,7 @@ HOLD_TEMP = 'hold_temp'
REQUIREMENTS = [ REQUIREMENTS = [
'https://github.com/nkgilley/python-ecobee-api/archive/' 'https://github.com/nkgilley/python-ecobee-api/archive/'
'5645f843b64ac4f6e59dfb96233a07083c5e10c1.zip#python-ecobee==0.0.3'] '92a2f330cbaf601d0618456fdd97e5a8c42c1c47.zip#python-ecobee==0.0.4']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -22,8 +22,7 @@ _LOGGER = logging.getLogger(__name__)
FRONTEND_URLS = [ FRONTEND_URLS = [
URL_ROOT, '/logbook', '/history', '/map', '/devService', '/devState', URL_ROOT, '/logbook', '/history', '/map', '/devService', '/devState',
'/devEvent', '/devInfo'] '/devEvent', '/devInfo', '/states']
STATES_URL = re.compile(r'/states(/([a-zA-Z\._\-0-9/]+)|)')
_FINGERPRINT = re.compile(r'^(\w+)-[a-z0-9]{32}\.(\w+)$', re.IGNORECASE) _FINGERPRINT = re.compile(r'^(\w+)-[a-z0-9]{32}\.(\w+)$', re.IGNORECASE)
@ -37,7 +36,8 @@ def setup(hass, config):
for url in FRONTEND_URLS: for url in FRONTEND_URLS:
hass.http.register_path('GET', url, _handle_get_root, False) hass.http.register_path('GET', url, _handle_get_root, False)
hass.http.register_path('GET', STATES_URL, _handle_get_root, False) hass.http.register_path('GET', '/service_worker.js',
_handle_get_service_worker, False)
# Static files # Static files
hass.http.register_path( hass.http.register_path(
@ -78,6 +78,17 @@ def _handle_get_root(handler, path_match, data):
handler.wfile.write(template_html.encode("UTF-8")) handler.wfile.write(template_html.encode("UTF-8"))
def _handle_get_service_worker(handler, path_match, data):
""" Returns service worker for the frontend. """
if handler.server.development:
sw_path = "home-assistant-polymer/build/service_worker.js"
else:
sw_path = "service_worker.js"
handler.write_file(os.path.join(os.path.dirname(__file__), 'www_static',
sw_path))
def _handle_get_static(handler, path_match, data): def _handle_get_static(handler, path_match, data):
""" Returns a static file for the frontend. """ """ Returns a static file for the frontend. """
req_file = util.sanitize_path(path_match.group('file')) req_file = util.sanitize_path(path_match.group('file'))

View File

@ -1,5 +1,5 @@
<!doctype html> <!doctype html>
<html> <html lang="en">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>Home Assistant</title> <title>Home Assistant</title>
@ -31,11 +31,21 @@
margin-bottom: 123px; margin-bottom: 123px;
} }
</style> </style>
<link rel='import' href='/static/{{ app_url }}' async>
</head> </head>
<body fullbleed> <body fullbleed>
<div id='init'><img src='/static/favicon-192x192.png' height='192'></div> <div id='init'><img src='/static/favicon-192x192.png' height='192'></div>
<script src='/static/webcomponents-lite.min.js'></script> <script>
<link rel='import' href='/static/{{ app_url }}' /> var webComponentsSupported = ('registerElement' in document &&
'import' in document.createElement('link') &&
'content' in document.createElement('template'))
if (!webComponentsSupported) {
var script = document.createElement('script')
script.async = true
script.src = '/static/webcomponents-lite.min.js'
document.head.appendChild(script)
}
</script>
<home-assistant auth='{{ auth }}' icons='{{ icons }}'></home-assistant> <home-assistant auth='{{ auth }}' icons='{{ icons }}'></home-assistant>
</body> </body>
</html> </html>

View File

@ -1,2 +1,2 @@
""" DO NOT MODIFY. Auto-generated by build_frontend script """ """ DO NOT MODIFY. Auto-generated by build_frontend script """
VERSION = "33a9830ccda8000eb88700de9d4cd03b" VERSION = "aac488c33cd4291cd0924e60a55bd309"

File diff suppressed because one or more lines are too long

@ -1 +1 @@
Subproject commit 2e8ad266eeb8cd0136df498b995f584e01338000 Subproject commit e51b8add369f9e81d22b25b4be2400675361afdb

View File

@ -0,0 +1,5 @@
!function(e){function t(r){if(n[r])return n[r].exports;var s=n[r]={exports:{},id:r,loaded:!1};return e[r].call(s.exports,s,s.exports,t),s.loaded=!0,s.exports}var n={};return t.m=e,t.c=n,t.p="",t(0)}([/*!*************************************!*\
!*** ./src/service-worker/index.js ***!
\*************************************/
function(e,t,n){"use strict";var r="0.10",s="/",c=["/","/logbook","/history","/map","/devService","/devState","/devEvent","/devInfo","/states"],i=["/static/favicon-192x192.png"];self.addEventListener("install",function(e){e.waitUntil(caches.open(r).then(function(e){return e.addAll(i.concat(s))}))}),self.addEventListener("activate",function(e){}),self.addEventListener("message",function(e){}),self.addEventListener("fetch",function(e){var t=e.request.url.substr(e.request.url.indexOf("/",8));i.includes(t)&&e.respondWith(caches.open(r).then(function(t){return t.match(e.request)})),c.includes(t)&&e.respondWith(caches.open(r).then(function(t){return t.match(s).then(function(n){var r=fetch(e.request).then(function(e){return t.put(s,e.clone()),e});return n||r})}))})}]);
//# sourceMappingURL=service_worker.js.map

View File

@ -6,16 +6,17 @@ This module provides an API and a HTTP interface for debug purposes.
For more details about the RESTful API, please refer to the documentation at For more details about the RESTful API, please refer to the documentation at
https://home-assistant.io/developers/api/ https://home-assistant.io/developers/api/
""" """
import json
import threading
import logging
import time
import gzip
import os
from datetime import timedelta from datetime import timedelta
from http.server import SimpleHTTPRequestHandler, HTTPServer import gzip
from http import cookies from http import cookies
from http.server import SimpleHTTPRequestHandler, HTTPServer
import json
import logging
import os
from socketserver import ThreadingMixIn from socketserver import ThreadingMixIn
import ssl
import threading
import time
from urllib.parse import urlparse, parse_qs from urllib.parse import urlparse, parse_qs
import homeassistant.core as ha import homeassistant.core as ha
@ -36,7 +37,8 @@ CONF_API_PASSWORD = "api_password"
CONF_SERVER_HOST = "server_host" CONF_SERVER_HOST = "server_host"
CONF_SERVER_PORT = "server_port" CONF_SERVER_PORT = "server_port"
CONF_DEVELOPMENT = "development" CONF_DEVELOPMENT = "development"
CONF_SESSIONS_ENABLED = "sessions_enabled" CONF_SSL_CERTIFICATE = 'ssl_certificate'
CONF_SSL_KEY = 'ssl_key'
DATA_API_PASSWORD = 'api_password' DATA_API_PASSWORD = 'api_password'
@ -58,11 +60,13 @@ def setup(hass, config):
server_host = conf.get(CONF_SERVER_HOST, '0.0.0.0') server_host = conf.get(CONF_SERVER_HOST, '0.0.0.0')
server_port = conf.get(CONF_SERVER_PORT, SERVER_PORT) server_port = conf.get(CONF_SERVER_PORT, SERVER_PORT)
development = str(conf.get(CONF_DEVELOPMENT, "")) == "1" development = str(conf.get(CONF_DEVELOPMENT, "")) == "1"
ssl_certificate = conf.get(CONF_SSL_CERTIFICATE)
ssl_key = conf.get(CONF_SSL_KEY)
try: try:
server = HomeAssistantHTTPServer( server = HomeAssistantHTTPServer(
(server_host, server_port), RequestHandler, hass, api_password, (server_host, server_port), RequestHandler, hass, api_password,
development) development, ssl_certificate, ssl_key)
except OSError: except OSError:
# If address already in use # If address already in use
_LOGGER.exception("Error setting up HTTP server") _LOGGER.exception("Error setting up HTTP server")
@ -74,7 +78,8 @@ def setup(hass, config):
threading.Thread(target=server.start, daemon=True).start()) threading.Thread(target=server.start, daemon=True).start())
hass.http = server hass.http = server
hass.config.api = rem.API(util.get_local_ip(), api_password, server_port) hass.config.api = rem.API(util.get_local_ip(), api_password, server_port,
ssl_certificate is not None)
return True return True
@ -89,7 +94,7 @@ class HomeAssistantHTTPServer(ThreadingMixIn, HTTPServer):
# pylint: disable=too-many-arguments # pylint: disable=too-many-arguments
def __init__(self, server_address, request_handler_class, def __init__(self, server_address, request_handler_class,
hass, api_password, development): hass, api_password, development, ssl_certificate, ssl_key):
super().__init__(server_address, request_handler_class) super().__init__(server_address, request_handler_class)
self.server_address = server_address self.server_address = server_address
@ -98,6 +103,7 @@ class HomeAssistantHTTPServer(ThreadingMixIn, HTTPServer):
self.development = development self.development = development
self.paths = [] self.paths = []
self.sessions = SessionStore() self.sessions = SessionStore()
self.use_ssl = ssl_certificate is not None
# We will lazy init this one if needed # We will lazy init this one if needed
self.event_forwarder = None self.event_forwarder = None
@ -105,6 +111,12 @@ class HomeAssistantHTTPServer(ThreadingMixIn, HTTPServer):
if development: if development:
_LOGGER.info("running http in development mode") _LOGGER.info("running http in development mode")
if ssl_certificate is not None:
wrap_kwargs = {'certfile': ssl_certificate}
if ssl_key is not None:
wrap_kwargs['keyfile'] = ssl_key
self.socket = ssl.wrap_socket(self.socket, **wrap_kwargs)
def start(self): def start(self):
""" Starts the HTTP server. """ """ Starts the HTTP server. """
def stop_http(event): def stop_http(event):
@ -113,8 +125,11 @@ class HomeAssistantHTTPServer(ThreadingMixIn, HTTPServer):
self.hass.bus.listen_once(ha.EVENT_HOMEASSISTANT_STOP, stop_http) self.hass.bus.listen_once(ha.EVENT_HOMEASSISTANT_STOP, stop_http)
protocol = 'https' if self.use_ssl else 'http'
_LOGGER.info( _LOGGER.info(
"Starting web interface at http://%s:%d", *self.server_address) "Starting web interface at %s://%s:%d",
protocol, self.server_address[0], self.server_address[1])
# 31-1-2015: Refactored frontend/api components out of this component # 31-1-2015: Refactored frontend/api components out of this component
# To prevent stuff from breaking, load the two extracted components # To prevent stuff from breaking, load the two extracted components
@ -187,17 +202,12 @@ class RequestHandler(SimpleHTTPRequestHandler):
"Error parsing JSON", HTTP_UNPROCESSABLE_ENTITY) "Error parsing JSON", HTTP_UNPROCESSABLE_ENTITY)
return return
if self.server.api_password is None: self.authenticated = (self.server.api_password is None
self.authenticated = True or self.headers.get(HTTP_HEADER_HA_AUTH) ==
elif HTTP_HEADER_HA_AUTH in self.headers: self.server.api_password
api_password = self.headers.get(HTTP_HEADER_HA_AUTH) or data.get(DATA_API_PASSWORD) ==
self.server.api_password
if not api_password and DATA_API_PASSWORD in data: or self.verify_session())
api_password = data[DATA_API_PASSWORD]
self.authenticated = api_password == self.server.api_password
else:
self.authenticated = self.verify_session()
if '_METHOD' in data: if '_METHOD' in data:
method = data.pop('_METHOD') method = data.pop('_METHOD')

View File

@ -0,0 +1,104 @@
"""
homeassistant.components.influxdb
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
InfluxDB component which allows you to send data to an Influx database.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/influxdb/
"""
import logging
import homeassistant.util as util
from homeassistant.helpers import validate_config
from homeassistant.const import (EVENT_STATE_CHANGED, STATE_ON, STATE_OFF,
STATE_UNLOCKED, STATE_LOCKED, STATE_UNKNOWN)
from homeassistant.components.sun import (STATE_ABOVE_HORIZON,
STATE_BELOW_HORIZON)
_LOGGER = logging.getLogger(__name__)
DOMAIN = "influxdb"
DEPENDENCIES = []
DEFAULT_HOST = 'localhost'
DEFAULT_PORT = 8086
DEFAULT_DATABASE = 'home_assistant'
REQUIREMENTS = ['influxdb==2.10.0']
CONF_HOST = 'host'
CONF_PORT = 'port'
CONF_DB_NAME = 'database'
CONF_USERNAME = 'username'
CONF_PASSWORD = 'password'
def setup(hass, config):
""" Setup the InfluxDB component. """
from influxdb import InfluxDBClient, exceptions
if not validate_config(config, {DOMAIN: ['host']}, _LOGGER):
return False
conf = config[DOMAIN]
host = conf[CONF_HOST]
port = util.convert(conf.get(CONF_PORT), int, DEFAULT_PORT)
database = util.convert(conf.get(CONF_DB_NAME), str, DEFAULT_DATABASE)
username = util.convert(conf.get(CONF_USERNAME), str)
password = util.convert(conf.get(CONF_PASSWORD), str)
try:
influx = InfluxDBClient(host=host, port=port, username=username,
password=password, database=database)
databases = [i['name'] for i in influx.get_list_database()]
except exceptions.InfluxDBClientError:
_LOGGER.error("Database host is not accessible. "
"Please check your entries in the configuration file.")
return False
if database not in databases:
_LOGGER.error("Database %s doesn't exist", database)
return False
def influx_event_listener(event):
""" Listen for new messages on the bus and sends them to Influx. """
state = event.data.get('new_state')
if state is None:
return
if state.state in (STATE_ON, STATE_LOCKED, STATE_ABOVE_HORIZON):
_state = 1
elif state.state in (STATE_OFF, STATE_UNLOCKED, STATE_UNKNOWN,
STATE_BELOW_HORIZON):
_state = 0
else:
_state = state.state
measurement = state.attributes.get('unit_of_measurement', state.domain)
json_body = [
{
'measurement': measurement,
'tags': {
'domain': state.domain,
'entity_id': state.object_id,
},
'time': event.time_fired,
'fields': {
'value': _state,
}
}
]
try:
influx.write_points(json_body)
except exceptions.InfluxDBClientError:
_LOGGER.exception('Error saving event to InfluxDB')
hass.bus.listen(EVENT_STATE_CHANGED, influx_event_listener)
return True

View File

@ -1,7 +1,7 @@
""" """
homeassistant.components.rollershutter.demo homeassistant.components.rollershutter.demo
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Demo platform for rollorshutter component. Demo platform for the rollorshutter component.
""" """
from homeassistant.const import EVENT_TIME_CHANGED from homeassistant.const import EVENT_TIME_CHANGED
from homeassistant.helpers.event import track_utc_time_change from homeassistant.helpers.event import track_utc_time_change
@ -9,7 +9,7 @@ from homeassistant.components.rollershutter import RollershutterDevice
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
""" Sets up the Demo binary sensors. """ """ Sets up the Demo rollershutters. """
add_devices([ add_devices([
DemoRollershutter(hass, 'Kitchen Window', 0), DemoRollershutter(hass, 'Kitchen Window', 0),
DemoRollershutter(hass, 'Living Room Window', 100), DemoRollershutter(hass, 'Living Room Window', 100),
@ -17,7 +17,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
class DemoRollershutter(RollershutterDevice): class DemoRollershutter(RollershutterDevice):
""" Represents a rollershutter within Home Assistant. """ """ Represents a rollershutter.. """
# pylint: disable=no-self-use # pylint: disable=no-self-use
def __init__(self, hass, name, position): def __init__(self, hass, name, position):
@ -29,14 +29,17 @@ class DemoRollershutter(RollershutterDevice):
@property @property
def name(self): def name(self):
""" Returns the name of the rollershutter. """
return self._name return self._name
@property @property
def should_poll(self): def should_poll(self):
""" No polling needed for a demo rollershutter. """
return False return False
@property @property
def current_position(self): def current_position(self):
""" Returns the current position of the rollershutter. """
return self._position return self._position
def move_up(self, **kwargs): def move_up(self, **kwargs):
@ -62,11 +65,13 @@ class DemoRollershutter(RollershutterDevice):
self._listener = None self._listener = None
def _listen(self): def _listen(self):
""" Listens for changes. """
if self._listener is None: if self._listener is None:
self._listener = track_utc_time_change(self.hass, self._listener = track_utc_time_change(self.hass,
self._time_changed) self._time_changed)
def _time_changed(self, now): def _time_changed(self, now):
""" Track time changes. """
if self._moving_up: if self._moving_up:
self._position -= 10 self._position -= 10
else: else:

View File

@ -201,7 +201,7 @@ class Script(ToggleEntity):
self._last_action) self._last_action)
domain, service = split_entity_id(conf_service) domain, service = split_entity_id(conf_service)
data = action.get(CONF_SERVICE_DATA, {}) data = action.get(CONF_SERVICE_DATA, {})
self.hass.services.call(domain, service, data) self.hass.services.call(domain, service, data, True)
def _fire_event(self, action): def _fire_event(self, action):
""" Fires an event. """ """ Fires an event. """

View File

@ -127,15 +127,14 @@ class ArestSensor(Entity):
if 'error' in values: if 'error' in values:
return values['error'] return values['error']
elif 'value' in values: elif 'value' in values:
if self._corr_factor is not None \ value = values['value']
and self._decimal_places is not None: if self._corr_factor is not None:
return round((float(values['value']) * value = float(value) * float(self._corr_factor)
float(self._corr_factor)), self._decimal_places) if self._decimal_places is not None:
elif self._corr_factor is not None \ value = round(value, self._decimal_places)
and self._decimal_places is None: if self._decimal_places == 0:
return round(float(values['value']) * float(self._corr_factor)) value = int(value)
else: return value
return values['value']
else: else:
return values.get(self._variable, 'n/a') return values.get(self._variable, 'n/a')

View File

@ -35,8 +35,8 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
data, data,
config.get('name', DEFAULT_NAME), config.get('name', DEFAULT_NAME),
config.get('unit_of_measurement'), config.get('unit_of_measurement'),
config.get('correction_factor', 1.0), config.get('correction_factor', None),
config.get('decimal_places', 0) config.get('decimal_places', None)
)]) )])
@ -49,7 +49,7 @@ class CommandSensor(Entity):
self._name = name self._name = name
self._state = False self._state = False
self._unit_of_measurement = unit_of_measurement self._unit_of_measurement = unit_of_measurement
self._corr_factor = float(corr_factor) self._corr_factor = corr_factor
self._decimal_places = decimal_places self._decimal_places = decimal_places
self.update() self.update()
@ -76,10 +76,12 @@ class CommandSensor(Entity):
try: try:
if value is not None: if value is not None:
if self._corr_factor is not None: if self._corr_factor is not None:
self._state = round((float(value) * self._corr_factor), value = float(value) * float(self._corr_factor)
self._decimal_places) if self._decimal_places is not None:
else: value = round(value, self._decimal_places)
self._state = value if self._decimal_places == 0:
value = int(value)
self._state = value
except ValueError: except ValueError:
self._state = value self._state = value

View File

@ -147,17 +147,8 @@ class ForeCastSensor(Entity):
self._state = data.nearestStormBearing self._state = data.nearestStormBearing
elif self.type == 'precip_intensity': elif self.type == 'precip_intensity':
self._state = data.precipIntensity self._state = data.precipIntensity
if data.precipIntensity == 0:
self._state = 'None'
self._unit_of_measurement = ''
else:
self._state = data.precipIntensity
elif self.type == 'precip_type': elif self.type == 'precip_type':
if data.precipType is None: self._state = data.precipType
self._state = 'None'
self._unit_of_measurement = ''
else:
self._state = data.precipType
elif self.type == 'precip_probability': elif self.type == 'precip_probability':
self._state = round(data.precipProbability * 100, 1) self._state = round(data.precipProbability * 100, 1)
elif self.type == 'dew_point': elif self.type == 'dew_point':

View File

@ -136,18 +136,13 @@ class RestSensor(Entity):
try: try:
if value is not None: if value is not None:
value = RestSensor.extract_value(value, self._variable) value = RestSensor.extract_value(value, self._variable)
if self._corr_factor is not None \ if self._corr_factor is not None:
and self._decimal_places is not None: value = float(value) * float(self._corr_factor)
self._state = round( if self._decimal_places is not None:
(float(value) * value = round(value, self._decimal_places)
float(self._corr_factor)), if self._decimal_places == 0:
self._decimal_places) value = int(value)
elif self._corr_factor is not None \ self._state = value
and self._decimal_places is None:
self._state = round(float(value) *
float(self._corr_factor))
else:
self._state = value
except ValueError: except ValueError:
self._state = RestSensor.extract_value(value, self._variable) self._state = RestSensor.extract_value(value, self._variable)

View File

@ -1,7 +1,7 @@
# coding: utf-8 # coding: utf-8
""" Constants used by Home Assistant components. """ """ Constants used by Home Assistant components. """
__version__ = "0.9.0.dev0" __version__ = "0.10.0.dev0"
# Can be used to specify a catch all when registering state or event listeners. # Can be used to specify a catch all when registering state or event listeners.
MATCH_ALL = '*' MATCH_ALL = '*'

View File

@ -25,7 +25,7 @@ from homeassistant.const import (
from homeassistant.exceptions import ( from homeassistant.exceptions import (
HomeAssistantError, InvalidEntityFormatError) HomeAssistantError, InvalidEntityFormatError)
import homeassistant.util as util import homeassistant.util as util
import homeassistant.util.dt as date_util import homeassistant.util.dt as dt_util
import homeassistant.util.location as location import homeassistant.util.location as location
import homeassistant.helpers.temperature as temp_helper import homeassistant.helpers.temperature as temp_helper
from homeassistant.config import get_default_config_dir from homeassistant.config import get_default_config_dir
@ -196,8 +196,8 @@ class Event(object):
self.event_type = event_type self.event_type = event_type
self.data = data or {} self.data = data or {}
self.origin = origin self.origin = origin
self.time_fired = date_util.strip_microseconds( self.time_fired = dt_util.strip_microseconds(
time_fired or date_util.utcnow()) time_fired or dt_util.utcnow())
def as_dict(self): def as_dict(self):
""" Returns a dict representation of this Event. """ """ Returns a dict representation of this Event. """
@ -205,7 +205,7 @@ class Event(object):
'event_type': self.event_type, 'event_type': self.event_type,
'data': dict(self.data), 'data': dict(self.data),
'origin': str(self.origin), 'origin': str(self.origin),
'time_fired': date_util.datetime_to_str(self.time_fired), 'time_fired': dt_util.datetime_to_str(self.time_fired),
} }
def __repr__(self): def __repr__(self):
@ -351,14 +351,14 @@ class State(object):
self.entity_id = entity_id.lower() self.entity_id = entity_id.lower()
self.state = state self.state = state
self.attributes = attributes or {} self.attributes = attributes or {}
self.last_updated = date_util.strip_microseconds( self.last_updated = dt_util.strip_microseconds(
last_updated or date_util.utcnow()) last_updated or dt_util.utcnow())
# Strip microsecond from last_changed else we cannot guarantee # Strip microsecond from last_changed else we cannot guarantee
# state == State.from_dict(state.as_dict()) # state == State.from_dict(state.as_dict())
# This behavior occurs because to_dict uses datetime_to_str # This behavior occurs because to_dict uses datetime_to_str
# which does not preserve microseconds # which does not preserve microseconds
self.last_changed = date_util.strip_microseconds( self.last_changed = dt_util.strip_microseconds(
last_changed or self.last_updated) last_changed or self.last_updated)
@property @property
@ -381,7 +381,8 @@ class State(object):
def copy(self): def copy(self):
""" Creates a copy of itself. """ """ Creates a copy of itself. """
return State(self.entity_id, self.state, return State(self.entity_id, self.state,
dict(self.attributes), self.last_changed) dict(self.attributes), self.last_changed,
self.last_updated)
def as_dict(self): def as_dict(self):
""" Converts State to a dict to be used within JSON. """ Converts State to a dict to be used within JSON.
@ -390,8 +391,8 @@ class State(object):
return {'entity_id': self.entity_id, return {'entity_id': self.entity_id,
'state': self.state, 'state': self.state,
'attributes': self.attributes, 'attributes': self.attributes,
'last_changed': date_util.datetime_to_str(self.last_changed), 'last_changed': dt_util.datetime_to_str(self.last_changed),
'last_updated': date_util.datetime_to_str(self.last_updated)} 'last_updated': dt_util.datetime_to_str(self.last_updated)}
@classmethod @classmethod
def from_dict(cls, json_dict): def from_dict(cls, json_dict):
@ -406,12 +407,12 @@ class State(object):
last_changed = json_dict.get('last_changed') last_changed = json_dict.get('last_changed')
if last_changed: if last_changed:
last_changed = date_util.str_to_datetime(last_changed) last_changed = dt_util.str_to_datetime(last_changed)
last_updated = json_dict.get('last_updated') last_updated = json_dict.get('last_updated')
if last_updated: if last_updated:
last_updated = date_util.str_to_datetime(last_updated) last_updated = dt_util.str_to_datetime(last_updated)
return cls(json_dict['entity_id'], json_dict['state'], return cls(json_dict['entity_id'], json_dict['state'],
json_dict.get('attributes'), last_changed, last_updated) json_dict.get('attributes'), last_changed, last_updated)
@ -428,7 +429,7 @@ class State(object):
return "<state {}={}{} @ {}>".format( return "<state {}={}{} @ {}>".format(
self.entity_id, self.state, attr, self.entity_id, self.state, attr,
date_util.datetime_to_local_str(self.last_changed)) dt_util.datetime_to_local_str(self.last_changed))
class StateMachine(object): class StateMachine(object):
@ -732,7 +733,7 @@ class Config(object):
def as_dict(self): def as_dict(self):
""" Converts config to a dictionary. """ """ Converts config to a dictionary. """
time_zone = self.time_zone or date_util.UTC time_zone = self.time_zone or dt_util.UTC
return { return {
'latitude': self.latitude, 'latitude': self.latitude,
@ -766,7 +767,7 @@ def create_timer(hass, interval=TIMER_INTERVAL):
last_fired_on_second = -1 last_fired_on_second = -1
calc_now = date_util.utcnow calc_now = dt_util.utcnow
while not stop_event.isSet(): while not stop_event.isSet():
now = calc_now() now = calc_now()
@ -832,6 +833,6 @@ def create_worker_pool(worker_count=None):
for start, job in current_jobs: for start, job in current_jobs:
_LOGGER.warning("WorkerPool:Current job from %s: %s", _LOGGER.warning("WorkerPool:Current job from %s: %s",
date_util.datetime_to_local_str(start), job) dt_util.datetime_to_local_str(start), job)
return util.ThreadPool(job_handler, worker_count, busy_callback) return util.ThreadPool(job_handler, worker_count, busy_callback)

View File

@ -51,11 +51,14 @@ class API(object):
""" Object to pass around Home Assistant API location and credentials. """ """ Object to pass around Home Assistant API location and credentials. """
# pylint: disable=too-few-public-methods # pylint: disable=too-few-public-methods
def __init__(self, host, api_password=None, port=None): def __init__(self, host, api_password=None, port=None, use_ssl=False):
self.host = host self.host = host
self.port = port or SERVER_PORT self.port = port or SERVER_PORT
self.api_password = api_password self.api_password = api_password
self.base_url = "http://{}:{}".format(host, self.port) if use_ssl:
self.base_url = "https://{}:{}".format(host, self.port)
else:
self.base_url = "http://{}:{}".format(host, self.port)
self.status = None self.status = None
self._headers = {} self._headers = {}

View File

@ -21,11 +21,14 @@ pysnmp==4.2.5
netdisco==0.5.2 netdisco==0.5.2
# homeassistant.components.ecobee # homeassistant.components.ecobee
https://github.com/nkgilley/python-ecobee-api/archive/5645f843b64ac4f6e59dfb96233a07083c5e10c1.zip#python-ecobee==0.0.3 https://github.com/nkgilley/python-ecobee-api/archive/92a2f330cbaf601d0618456fdd97e5a8c42c1c47.zip#python-ecobee==0.0.4
# homeassistant.components.ifttt # homeassistant.components.ifttt
pyfttt==0.3 pyfttt==0.3
# homeassistant.components.influxdb
influxdb==2.10.0
# homeassistant.components.isy994 # homeassistant.components.isy994
PyISY==1.0.5 PyISY==1.0.5
@ -157,7 +160,7 @@ orvibo==1.0.1
pywemo==0.3.3 pywemo==0.3.3
# homeassistant.components.thermostat.honeywell # homeassistant.components.thermostat.honeywell
evohomeclient==0.2.3 evohomeclient==0.2.4
# homeassistant.components.thermostat.nest # homeassistant.components.thermostat.nest
python-nest==2.6.0 python-nest==2.6.0

View File

@ -7,6 +7,7 @@ npm run frontend_prod
cp bower_components/webcomponentsjs/webcomponents-lite.min.js .. cp bower_components/webcomponentsjs/webcomponents-lite.min.js ..
cp build/frontend.html .. cp build/frontend.html ..
cp build/service_worker.js ..
# Generate the MD5 hash of the new frontend # Generate the MD5 hash of the new frontend
cd ../.. cd ../..

View File

@ -66,18 +66,31 @@ class TestAPI(unittest.TestCase):
# TODO move back to http component and test with use_auth. # TODO move back to http component and test with use_auth.
def test_access_denied_without_password(self): def test_access_denied_without_password(self):
req = requests.get( req = requests.get(_url(const.URL_API))
_url(const.URL_API_STATES_ENTITY.format("test")))
self.assertEqual(401, req.status_code) self.assertEqual(401, req.status_code)
def test_access_denied_with_wrong_password(self): def test_access_denied_with_wrong_password(self):
req = requests.get( req = requests.get(
_url(const.URL_API_STATES_ENTITY.format("test")), _url(const.URL_API),
headers={const.HTTP_HEADER_HA_AUTH: 'wrongpassword'}) headers={const.HTTP_HEADER_HA_AUTH: 'wrongpassword'})
self.assertEqual(401, req.status_code) self.assertEqual(401, req.status_code)
def test_access_with_password_in_url(self):
req = requests.get(
"{}?api_password={}".format(_url(const.URL_API), API_PASSWORD))
self.assertEqual(200, req.status_code)
def test_access_via_session(self):
session = requests.Session()
req = session.get(_url(const.URL_API), headers=HA_HEADERS)
self.assertEqual(200, req.status_code)
req = session.get(_url(const.URL_API))
self.assertEqual(200, req.status_code)
def test_api_list_state_entities(self): def test_api_list_state_entities(self):
""" Test if the debug interface allows us to list state entities. """ """ Test if the debug interface allows us to list state entities. """
req = requests.get(_url(const.URL_API_STATES), req = requests.get(_url(const.URL_API_STATES),

View File

@ -268,7 +268,15 @@ class TestState(unittest.TestCase):
def test_copy(self): def test_copy(self):
state = ha.State('domain.hello', 'world', {'some': 'attr'}) state = ha.State('domain.hello', 'world', {'some': 'attr'})
self.assertEqual(state, state.copy()) # Patch dt_util.utcnow() so we know last_updated got copied too
with patch('homeassistant.core.dt_util.utcnow',
return_value=dt_util.utcnow() + timedelta(seconds=10)):
copy = state.copy()
self.assertEqual(state.entity_id, copy.entity_id)
self.assertEqual(state.state, copy.state)
self.assertEqual(state.attributes, copy.attributes)
self.assertEqual(state.last_changed, copy.last_changed)
self.assertEqual(state.last_updated, copy.last_updated)
def test_dict_conversion(self): def test_dict_conversion(self):
state = ha.State('domain.hello', 'world', {'some': 'attr'}) state = ha.State('domain.hello', 'world', {'some': 'attr'})