diff --git a/.coveragerc b/.coveragerc index bf5582c2115..bd23599aa0d 100644 --- a/.coveragerc +++ b/.coveragerc @@ -55,6 +55,7 @@ omit = homeassistant/components/discovery.py homeassistant/components/downloader.py homeassistant/components/ifttt.py + homeassistant/components/influxdb.py homeassistant/components/keyboard.py homeassistant/components/light/hue.py homeassistant/components/light/mqtt.py diff --git a/homeassistant/components/demo.py b/homeassistant/components/demo.py index 8a8c5ed31e5..8b4b3fcce6c 100644 --- a/homeassistant/components/demo.py +++ b/homeassistant/components/demo.py @@ -53,9 +53,10 @@ def setup(hass, config): bootstrap.setup_component(hass, 'sun') # Setup demo platforms + demo_config = config.copy() for component in COMPONENTS_WITH_DEMO_PLATFORM: - bootstrap.setup_component( - hass, component, {component: {CONF_PLATFORM: 'demo'}}) + demo_config[component] = {CONF_PLATFORM: 'demo'} + bootstrap.setup_component(hass, component, demo_config) # Setup room groups lights = sorted(hass.states.entity_ids('light')) diff --git a/homeassistant/components/ecobee.py b/homeassistant/components/ecobee.py index a517e2d5418..f1ce746b48e 100644 --- a/homeassistant/components/ecobee.py +++ b/homeassistant/components/ecobee.py @@ -44,7 +44,7 @@ HOLD_TEMP = 'hold_temp' REQUIREMENTS = [ '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__) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index dac2041fa56..fc955e30d44 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -22,8 +22,7 @@ _LOGGER = logging.getLogger(__name__) FRONTEND_URLS = [ URL_ROOT, '/logbook', '/history', '/map', '/devService', '/devState', - '/devEvent', '/devInfo'] -STATES_URL = re.compile(r'/states(/([a-zA-Z\._\-0-9/]+)|)') + '/devEvent', '/devInfo', '/states'] _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: 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 hass.http.register_path( @@ -78,6 +78,17 @@ def _handle_get_root(handler, path_match, data): 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): """ Returns a static file for the frontend. """ req_file = util.sanitize_path(path_match.group('file')) diff --git a/homeassistant/components/frontend/index.html.template b/homeassistant/components/frontend/index.html.template index 87c5f6638a7..c0631c9d9db 100644 --- a/homeassistant/components/frontend/index.html.template +++ b/homeassistant/components/frontend/index.html.template @@ -1,5 +1,5 @@ - + Home Assistant @@ -31,11 +31,21 @@ margin-bottom: 123px; } +
- - + diff --git a/homeassistant/components/frontend/version.py b/homeassistant/components/frontend/version.py index 5c9371f67e4..83383baf11d 100644 --- a/homeassistant/components/frontend/version.py +++ b/homeassistant/components/frontend/version.py @@ -1,2 +1,2 @@ """ DO NOT MODIFY. Auto-generated by build_frontend script """ -VERSION = "33a9830ccda8000eb88700de9d4cd03b" +VERSION = "aac488c33cd4291cd0924e60a55bd309" diff --git a/homeassistant/components/frontend/www_static/frontend.html b/homeassistant/components/frontend/www_static/frontend.html index 6e46b80ef88..907722b09ee 100644 --- a/homeassistant/components/frontend/www_static/frontend.html +++ b/homeassistant/components/frontend/www_static/frontend.html @@ -3718,7 +3718,7 @@ case"touchend":return this.addPointerListenerEnd(t,e,i,n);case"touchmove":return text-align: right; white-space: nowrap; width: 127px; - } \ No newline at end of file + } \ No newline at end of file diff --git a/homeassistant/components/frontend/www_static/home-assistant-polymer b/homeassistant/components/frontend/www_static/home-assistant-polymer index 2e8ad266eeb..e51b8add369 160000 --- a/homeassistant/components/frontend/www_static/home-assistant-polymer +++ b/homeassistant/components/frontend/www_static/home-assistant-polymer @@ -1 +1 @@ -Subproject commit 2e8ad266eeb8cd0136df498b995f584e01338000 +Subproject commit e51b8add369f9e81d22b25b4be2400675361afdb diff --git a/homeassistant/components/frontend/www_static/service_worker.js b/homeassistant/components/frontend/www_static/service_worker.js new file mode 100644 index 00000000000..eb9c41c3abc --- /dev/null +++ b/homeassistant/components/frontend/www_static/service_worker.js @@ -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 \ No newline at end of file diff --git a/homeassistant/components/http.py b/homeassistant/components/http.py index f5e337ccf9c..2b260b0e841 100644 --- a/homeassistant/components/http.py +++ b/homeassistant/components/http.py @@ -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 https://home-assistant.io/developers/api/ """ -import json -import threading -import logging -import time -import gzip -import os from datetime import timedelta -from http.server import SimpleHTTPRequestHandler, HTTPServer +import gzip from http import cookies +from http.server import SimpleHTTPRequestHandler, HTTPServer +import json +import logging +import os from socketserver import ThreadingMixIn +import ssl +import threading +import time from urllib.parse import urlparse, parse_qs import homeassistant.core as ha @@ -36,7 +37,8 @@ CONF_API_PASSWORD = "api_password" CONF_SERVER_HOST = "server_host" CONF_SERVER_PORT = "server_port" CONF_DEVELOPMENT = "development" -CONF_SESSIONS_ENABLED = "sessions_enabled" +CONF_SSL_CERTIFICATE = 'ssl_certificate' +CONF_SSL_KEY = 'ssl_key' 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_port = conf.get(CONF_SERVER_PORT, SERVER_PORT) development = str(conf.get(CONF_DEVELOPMENT, "")) == "1" + ssl_certificate = conf.get(CONF_SSL_CERTIFICATE) + ssl_key = conf.get(CONF_SSL_KEY) try: server = HomeAssistantHTTPServer( (server_host, server_port), RequestHandler, hass, api_password, - development) + development, ssl_certificate, ssl_key) except OSError: # If address already in use _LOGGER.exception("Error setting up HTTP server") @@ -74,7 +78,8 @@ def setup(hass, config): threading.Thread(target=server.start, daemon=True).start()) 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 @@ -89,7 +94,7 @@ class HomeAssistantHTTPServer(ThreadingMixIn, HTTPServer): # pylint: disable=too-many-arguments 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) self.server_address = server_address @@ -98,6 +103,7 @@ class HomeAssistantHTTPServer(ThreadingMixIn, HTTPServer): self.development = development self.paths = [] self.sessions = SessionStore() + self.use_ssl = ssl_certificate is not None # We will lazy init this one if needed self.event_forwarder = None @@ -105,6 +111,12 @@ class HomeAssistantHTTPServer(ThreadingMixIn, HTTPServer): if development: _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): """ Starts the HTTP server. """ def stop_http(event): @@ -113,8 +125,11 @@ class HomeAssistantHTTPServer(ThreadingMixIn, HTTPServer): self.hass.bus.listen_once(ha.EVENT_HOMEASSISTANT_STOP, stop_http) + protocol = 'https' if self.use_ssl else 'http' + _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 # To prevent stuff from breaking, load the two extracted components @@ -187,17 +202,12 @@ class RequestHandler(SimpleHTTPRequestHandler): "Error parsing JSON", HTTP_UNPROCESSABLE_ENTITY) return - if self.server.api_password is None: - self.authenticated = True - elif HTTP_HEADER_HA_AUTH in self.headers: - api_password = self.headers.get(HTTP_HEADER_HA_AUTH) - - if not api_password and DATA_API_PASSWORD in data: - api_password = data[DATA_API_PASSWORD] - - self.authenticated = api_password == self.server.api_password - else: - self.authenticated = self.verify_session() + self.authenticated = (self.server.api_password is None + or self.headers.get(HTTP_HEADER_HA_AUTH) == + self.server.api_password + or data.get(DATA_API_PASSWORD) == + self.server.api_password + or self.verify_session()) if '_METHOD' in data: method = data.pop('_METHOD') diff --git a/homeassistant/components/influxdb.py b/homeassistant/components/influxdb.py new file mode 100644 index 00000000000..2286dd2d659 --- /dev/null +++ b/homeassistant/components/influxdb.py @@ -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 diff --git a/homeassistant/components/rollershutter/demo.py b/homeassistant/components/rollershutter/demo.py index a57bf4ddeec..4ffeb6be6dd 100644 --- a/homeassistant/components/rollershutter/demo.py +++ b/homeassistant/components/rollershutter/demo.py @@ -1,7 +1,7 @@ """ homeassistant.components.rollershutter.demo ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Demo platform for rollorshutter component. +Demo platform for the rollorshutter component. """ from homeassistant.const import EVENT_TIME_CHANGED 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): - """ Sets up the Demo binary sensors. """ + """ Sets up the Demo rollershutters. """ add_devices([ DemoRollershutter(hass, 'Kitchen Window', 0), DemoRollershutter(hass, 'Living Room Window', 100), @@ -17,7 +17,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class DemoRollershutter(RollershutterDevice): - """ Represents a rollershutter within Home Assistant. """ + """ Represents a rollershutter.. """ # pylint: disable=no-self-use def __init__(self, hass, name, position): @@ -29,14 +29,17 @@ class DemoRollershutter(RollershutterDevice): @property def name(self): + """ Returns the name of the rollershutter. """ return self._name @property def should_poll(self): + """ No polling needed for a demo rollershutter. """ return False @property def current_position(self): + """ Returns the current position of the rollershutter. """ return self._position def move_up(self, **kwargs): @@ -62,11 +65,13 @@ class DemoRollershutter(RollershutterDevice): self._listener = None def _listen(self): + """ Listens for changes. """ if self._listener is None: self._listener = track_utc_time_change(self.hass, self._time_changed) def _time_changed(self, now): + """ Track time changes. """ if self._moving_up: self._position -= 10 else: diff --git a/homeassistant/components/script.py b/homeassistant/components/script.py index bf7c51fe3fb..3e13db66699 100644 --- a/homeassistant/components/script.py +++ b/homeassistant/components/script.py @@ -201,7 +201,7 @@ class Script(ToggleEntity): self._last_action) domain, service = split_entity_id(conf_service) 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): """ Fires an event. """ diff --git a/homeassistant/components/sensor/arest.py b/homeassistant/components/sensor/arest.py index f1faa7cc932..3a0ce01719b 100644 --- a/homeassistant/components/sensor/arest.py +++ b/homeassistant/components/sensor/arest.py @@ -127,15 +127,14 @@ class ArestSensor(Entity): if 'error' in values: return values['error'] elif 'value' in values: - if self._corr_factor is not None \ - and self._decimal_places is not None: - return round((float(values['value']) * - float(self._corr_factor)), self._decimal_places) - elif self._corr_factor is not None \ - and self._decimal_places is None: - return round(float(values['value']) * float(self._corr_factor)) - else: - return values['value'] + value = values['value'] + if self._corr_factor is not None: + value = float(value) * float(self._corr_factor) + if self._decimal_places is not None: + value = round(value, self._decimal_places) + if self._decimal_places == 0: + value = int(value) + return value else: return values.get(self._variable, 'n/a') diff --git a/homeassistant/components/sensor/command_sensor.py b/homeassistant/components/sensor/command_sensor.py index b5beaab13d0..e60723f6bfa 100644 --- a/homeassistant/components/sensor/command_sensor.py +++ b/homeassistant/components/sensor/command_sensor.py @@ -35,8 +35,8 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): data, config.get('name', DEFAULT_NAME), config.get('unit_of_measurement'), - config.get('correction_factor', 1.0), - config.get('decimal_places', 0) + config.get('correction_factor', None), + config.get('decimal_places', None) )]) @@ -49,7 +49,7 @@ class CommandSensor(Entity): self._name = name self._state = False self._unit_of_measurement = unit_of_measurement - self._corr_factor = float(corr_factor) + self._corr_factor = corr_factor self._decimal_places = decimal_places self.update() @@ -76,10 +76,12 @@ class CommandSensor(Entity): try: if value is not None: if self._corr_factor is not None: - self._state = round((float(value) * self._corr_factor), - self._decimal_places) - else: - self._state = value + value = float(value) * float(self._corr_factor) + if self._decimal_places is not None: + value = round(value, self._decimal_places) + if self._decimal_places == 0: + value = int(value) + self._state = value except ValueError: self._state = value diff --git a/homeassistant/components/sensor/forecast.py b/homeassistant/components/sensor/forecast.py index c024c19b5f7..42fbe26b9cf 100644 --- a/homeassistant/components/sensor/forecast.py +++ b/homeassistant/components/sensor/forecast.py @@ -147,17 +147,8 @@ class ForeCastSensor(Entity): self._state = data.nearestStormBearing elif self.type == 'precip_intensity': 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': - if data.precipType is None: - self._state = 'None' - self._unit_of_measurement = '' - else: - self._state = data.precipType + self._state = data.precipType elif self.type == 'precip_probability': self._state = round(data.precipProbability * 100, 1) elif self.type == 'dew_point': diff --git a/homeassistant/components/sensor/rest.py b/homeassistant/components/sensor/rest.py index 53609dbb237..f50113350da 100644 --- a/homeassistant/components/sensor/rest.py +++ b/homeassistant/components/sensor/rest.py @@ -136,18 +136,13 @@ class RestSensor(Entity): try: if value is not None: value = RestSensor.extract_value(value, self._variable) - if self._corr_factor is not None \ - and self._decimal_places is not None: - self._state = round( - (float(value) * - float(self._corr_factor)), - self._decimal_places) - elif self._corr_factor is not None \ - and self._decimal_places is None: - self._state = round(float(value) * - float(self._corr_factor)) - else: - self._state = value + if self._corr_factor is not None: + value = float(value) * float(self._corr_factor) + if self._decimal_places is not None: + value = round(value, self._decimal_places) + if self._decimal_places == 0: + value = int(value) + self._state = value except ValueError: self._state = RestSensor.extract_value(value, self._variable) diff --git a/homeassistant/const.py b/homeassistant/const.py index 2a1b7108cdd..9a17622a7dd 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ # coding: utf-8 """ 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. MATCH_ALL = '*' diff --git a/homeassistant/core.py b/homeassistant/core.py index d7ec3abe458..8ea55c653e3 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -25,7 +25,7 @@ from homeassistant.const import ( from homeassistant.exceptions import ( HomeAssistantError, InvalidEntityFormatError) 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.helpers.temperature as temp_helper from homeassistant.config import get_default_config_dir @@ -196,8 +196,8 @@ class Event(object): self.event_type = event_type self.data = data or {} self.origin = origin - self.time_fired = date_util.strip_microseconds( - time_fired or date_util.utcnow()) + self.time_fired = dt_util.strip_microseconds( + time_fired or dt_util.utcnow()) def as_dict(self): """ Returns a dict representation of this Event. """ @@ -205,7 +205,7 @@ class Event(object): 'event_type': self.event_type, 'data': dict(self.data), '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): @@ -351,14 +351,14 @@ class State(object): self.entity_id = entity_id.lower() self.state = state self.attributes = attributes or {} - self.last_updated = date_util.strip_microseconds( - last_updated or date_util.utcnow()) + self.last_updated = dt_util.strip_microseconds( + last_updated or dt_util.utcnow()) # Strip microsecond from last_changed else we cannot guarantee # state == State.from_dict(state.as_dict()) # This behavior occurs because to_dict uses datetime_to_str # 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) @property @@ -381,7 +381,8 @@ class State(object): def copy(self): """ Creates a copy of itself. """ 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): """ Converts State to a dict to be used within JSON. @@ -390,8 +391,8 @@ class State(object): return {'entity_id': self.entity_id, 'state': self.state, 'attributes': self.attributes, - 'last_changed': date_util.datetime_to_str(self.last_changed), - 'last_updated': date_util.datetime_to_str(self.last_updated)} + 'last_changed': dt_util.datetime_to_str(self.last_changed), + 'last_updated': dt_util.datetime_to_str(self.last_updated)} @classmethod def from_dict(cls, json_dict): @@ -406,12 +407,12 @@ class State(object): last_changed = json_dict.get('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') 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'], json_dict.get('attributes'), last_changed, last_updated) @@ -428,7 +429,7 @@ class State(object): return "".format( 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): @@ -732,7 +733,7 @@ class Config(object): def as_dict(self): """ Converts config to a dictionary. """ - time_zone = self.time_zone or date_util.UTC + time_zone = self.time_zone or dt_util.UTC return { 'latitude': self.latitude, @@ -766,7 +767,7 @@ def create_timer(hass, interval=TIMER_INTERVAL): last_fired_on_second = -1 - calc_now = date_util.utcnow + calc_now = dt_util.utcnow while not stop_event.isSet(): now = calc_now() @@ -832,6 +833,6 @@ def create_worker_pool(worker_count=None): for start, job in current_jobs: _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) diff --git a/homeassistant/remote.py b/homeassistant/remote.py index 72f95ca389a..3b47b60365c 100644 --- a/homeassistant/remote.py +++ b/homeassistant/remote.py @@ -51,11 +51,14 @@ class API(object): """ Object to pass around Home Assistant API location and credentials. """ # 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.port = port or SERVER_PORT 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._headers = {} diff --git a/requirements_all.txt b/requirements_all.txt index 25e1fd50618..5852cc45dee 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -21,11 +21,14 @@ pysnmp==4.2.5 netdisco==0.5.2 # 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 pyfttt==0.3 +# homeassistant.components.influxdb +influxdb==2.10.0 + # homeassistant.components.isy994 PyISY==1.0.5 @@ -157,7 +160,7 @@ orvibo==1.0.1 pywemo==0.3.3 # homeassistant.components.thermostat.honeywell -evohomeclient==0.2.3 +evohomeclient==0.2.4 # homeassistant.components.thermostat.nest python-nest==2.6.0 diff --git a/script/build_frontend b/script/build_frontend index 70eacdb6baf..5920026486e 100755 --- a/script/build_frontend +++ b/script/build_frontend @@ -7,6 +7,7 @@ npm run frontend_prod cp bower_components/webcomponentsjs/webcomponents-lite.min.js .. cp build/frontend.html .. +cp build/service_worker.js .. # Generate the MD5 hash of the new frontend cd ../.. diff --git a/tests/components/test_api.py b/tests/components/test_api.py index 56694289303..ab76ed0e3db 100644 --- a/tests/components/test_api.py +++ b/tests/components/test_api.py @@ -66,18 +66,31 @@ class TestAPI(unittest.TestCase): # TODO move back to http component and test with use_auth. def test_access_denied_without_password(self): - req = requests.get( - _url(const.URL_API_STATES_ENTITY.format("test"))) + req = requests.get(_url(const.URL_API)) self.assertEqual(401, req.status_code) def test_access_denied_with_wrong_password(self): req = requests.get( - _url(const.URL_API_STATES_ENTITY.format("test")), + _url(const.URL_API), headers={const.HTTP_HEADER_HA_AUTH: 'wrongpassword'}) 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): """ Test if the debug interface allows us to list state entities. """ req = requests.get(_url(const.URL_API_STATES), diff --git a/tests/test_core.py b/tests/test_core.py index bb59aac03fa..fee46fe2dd4 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -268,7 +268,15 @@ class TestState(unittest.TestCase): def test_copy(self): 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): state = ha.State('domain.hello', 'world', {'some': 'attr'})