From b67f1fed52a2643e090022796634a61a5ec8d0a5 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 5 Nov 2016 08:53:13 -0700 Subject: [PATCH 001/127] Version bump to 0.33.0.dev0 --- homeassistant/const.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index ec05a7e24ac..a25c73abaeb 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,8 +1,8 @@ # coding: utf-8 """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 -MINOR_VERSION = 32 -PATCH_VERSION = '0' +MINOR_VERSION = 33 +PATCH_VERSION = '0.dev0' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 4, 2) From ece58ce78fe44b521122e3f41b79e670010bc905 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Sat, 5 Nov 2016 17:27:55 +0100 Subject: [PATCH 002/127] Remove ThreadPool with async executor (#4154) * Remove ThreadPool with async executor * Fix zigbee * update unittest * fix remote api * add pending task to remote * fix lint * remove unused import * remove old stuff for lazy tests * fix bug and add a exception handler to executor * change executor handling * change to wait from gather * fix unittest --- homeassistant/bootstrap.py | 9 -- homeassistant/components/sensor/zigbee.py | 2 +- homeassistant/components/zigbee.py | 4 +- homeassistant/core.py | 154 +++++---------------- homeassistant/remote.py | 5 +- homeassistant/util/__init__.py | 109 --------------- tests/common.py | 17 --- tests/components/camera/test_generic.py | 2 - tests/components/camera/test_local_file.py | 2 - tests/test_core.py | 79 +---------- 10 files changed, 37 insertions(+), 346 deletions(-) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 31e404ad87a..2b6c4711691 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -165,15 +165,6 @@ def _async_setup_component(hass: core.HomeAssistant, hass.config.components.append(component.DOMAIN) - # Assumption: if a component does not depend on groups - # it communicates with devices - if (not async_comp and - 'group' not in getattr(component, 'DEPENDENCIES', [])): - if hass.pool is None: - hass.async_init_pool() - if hass.pool.worker_count <= 10: - hass.pool.add_worker() - hass.bus.async_fire( EVENT_COMPONENT_LOADED, {ATTR_COMPONENT: component.DOMAIN} ) diff --git a/homeassistant/components/sensor/zigbee.py b/homeassistant/components/sensor/zigbee.py index 7d4ead138e3..78dab2547f3 100644 --- a/homeassistant/components/sensor/zigbee.py +++ b/homeassistant/components/sensor/zigbee.py @@ -55,7 +55,7 @@ class ZigBeeTemperatureSensor(Entity): self._config = config self._temp = None # Get initial state - hass.add_job(self.update_ha_state, True) + hass.add_job(self.async_update_ha_state, True) @property def name(self): diff --git a/homeassistant/components/zigbee.py b/homeassistant/components/zigbee.py index a428d03efc1..8fbc3f0a9c3 100644 --- a/homeassistant/components/zigbee.py +++ b/homeassistant/components/zigbee.py @@ -307,7 +307,7 @@ class ZigBeeDigitalIn(Entity): subscribe(hass, handle_frame) # Get initial state - hass.add_job(self.update_ha_state, True) + hass.add_job(self.async_update_ha_state, True) @property def name(self): @@ -433,7 +433,7 @@ class ZigBeeAnalogIn(Entity): subscribe(hass, handle_frame) # Get initial state - hass.add_job(self.update_ha_state, True) + hass.add_job(self.async_update_ha_state, True) @property def name(self): diff --git a/homeassistant/core.py b/homeassistant/core.py index 8de1e2b2535..5f340bdd941 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -14,7 +14,7 @@ import re import signal import sys import threading -import time +import weakref from types import MappingProxyType from typing import Optional, Any, Callable, List # NOQA @@ -53,16 +53,11 @@ TIMER_INTERVAL = 1 # seconds # How long we wait for the result of a service call SERVICE_CALL_LIMIT = 10 # seconds -# Define number of MINIMUM worker threads. -# During bootstrap of HA (see bootstrap._setup_component()) worker threads -# will be added for each component that polls devices. -MIN_WORKER_THREAD = 2 - # Pattern for validating entity IDs (format: .) ENTITY_ID_PATTERN = re.compile(r"^(\w+)\.(\w+)$") -# Interval at which we check if the pool is getting busy -MONITOR_POOL_INTERVAL = 30 +# Size of a executor pool +EXECUTOR_POOL_SIZE = 10 _LOGGER = logging.getLogger(__name__) @@ -115,7 +110,7 @@ class HomeAssistant(object): self.executor = ThreadPoolExecutor(max_workers=5) self.loop.set_default_executor(self.executor) self.loop.set_exception_handler(self._async_exception_handler) - self.pool = None + self._pending_tasks = weakref.WeakSet() self.bus = EventBus(self) self.services = ServiceRegistry(self.bus, self.async_add_job, self.loop) @@ -190,20 +185,16 @@ class HomeAssistant(object): self.loop._thread_ident = threading.get_ident() _async_create_timer(self) self.bus.async_fire(EVENT_HOMEASSISTANT_START) - if self.pool is not None: - yield from self.loop.run_in_executor( - None, self.pool.block_till_done) self.state = CoreState.running def add_job(self, target: Callable[..., None], *args: Any) -> None: - """Add job to the worker pool. + """Add job to the executor pool. target: target to call. args: parameters for method to call. """ - if self.pool is None: - run_callback_threadsafe(self.pool, self.async_init_pool).result() - self.pool.add_job((target,) + args) + run_callback_threadsafe( + self.loop, self.async_add_job, target, *args).result() @callback def async_add_job(self, target: Callable[..., None], *args: Any) -> None: @@ -214,14 +205,18 @@ class HomeAssistant(object): target: target to call. args: parameters for method to call. """ + task = None + if is_callback(target): self.loop.call_soon(target, *args) elif asyncio.iscoroutinefunction(target): - self.loop.create_task(target(*args)) + task = self.loop.create_task(target(*args)) else: - if self.pool is None: - self.async_init_pool() - self.pool.add_job((target,) + args) + task = self.loop.run_in_executor(None, target, *args) + + # if a task is sheduled + if task is not None: + self._pending_tasks.add(task) @callback def async_run_job(self, target: Callable[..., None], *args: Any) -> None: @@ -249,37 +244,21 @@ class HomeAssistant(object): def block_till_done(self) -> None: """Block till all pending work is done.""" - complete = threading.Event() + run_coroutine_threadsafe( + self.async_block_till_done(), loop=self.loop).result() - @asyncio.coroutine - def sleep_wait(): - """Sleep in thread pool.""" - yield from self.loop.run_in_executor(None, time.sleep, 0) + @asyncio.coroutine + def async_block_till_done(self): + """Block till all pending work is done.""" + while True: + # Wait for the pending tasks are down + if len(self._pending_tasks) > 0: + yield from asyncio.wait(self._pending_tasks, loop=self.loop) - def notify_when_done(): - """Notify event loop when pool done.""" - count = 0 - while True: - # Wait for the work queue to empty - if self.pool is not None: - self.pool.block_till_done() - - # Verify the loop is empty - if self._loop_empty(): - count += 1 - - if count == 2: - break - - # sleep in the loop executor, this forces execution back into - # the event loop to avoid the block thread from starving the - # async loop - run_coroutine_threadsafe(sleep_wait(), self.loop).result() - - complete.set() - - threading.Thread(name="BlockThread", target=notify_when_done).start() - complete.wait() + # Verify the loop is empty + ret = yield from self.loop.run_in_executor(None, self._loop_empty) + if ret: + break def stop(self) -> None: """Stop Home Assistant and shuts down all threads.""" @@ -293,10 +272,7 @@ class HomeAssistant(object): """ self.state = CoreState.stopping self.bus.async_fire(EVENT_HOMEASSISTANT_STOP) - if self.pool is not None: - yield from self.loop.run_in_executor( - None, self.pool.block_till_done) - yield from self.loop.run_in_executor(None, self.pool.stop) + yield from self.async_block_till_done() self.executor.shutdown() if self._websession is not None: yield from self._websession.close() @@ -323,23 +299,17 @@ class HomeAssistant(object): exc_info=exc_info ) - @callback - def async_init_pool(self): - """Initialize the worker pool.""" - self.pool = create_worker_pool() - _async_monitor_worker_pool(self) - @callback def _async_stop_handler(self, *args): """Stop Home Assistant.""" self.exit_code = 0 - self.async_add_job(self.async_stop) + self.loop.create_task(self.async_stop()) @callback def _async_restart_handler(self, *args): """Restart Home Assistant.""" self.exit_code = RESTART_EXIT_CODE - self.async_add_job(self.async_stop) + self.loop.create_task(self.async_stop()) class EventOrigin(enum.Enum): @@ -1196,65 +1166,3 @@ def _async_create_timer(hass, interval=TIMER_INTERVAL): hass.loop.create_task(timer(interval, stop_event)) hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, start_timer) - - -def create_worker_pool(worker_count=None): - """Create a worker pool.""" - if worker_count is None: - worker_count = MIN_WORKER_THREAD - - def job_handler(job): - """Called whenever a job is available to do.""" - try: - func, *args = job - func(*args) - 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("BusHandler:Exception doing job") - - return util.ThreadPool(job_handler, worker_count) - - -def _async_monitor_worker_pool(hass): - """Create a monitor for the thread pool to check if pool is misbehaving.""" - busy_threshold = hass.pool.worker_count * 3 - - handle = None - - def schedule(): - """Schedule the monitor.""" - nonlocal handle - handle = hass.loop.call_later(MONITOR_POOL_INTERVAL, - check_pool_threshold) - - def check_pool_threshold(): - """Check pool size.""" - nonlocal busy_threshold - - pending_jobs = hass.pool.queue_size - - if pending_jobs < busy_threshold: - schedule() - return - - _LOGGER.warning( - "WorkerPool:All %d threads are busy and %d jobs pending", - hass.pool.worker_count, pending_jobs) - - for start, job in hass.pool.current_jobs: - _LOGGER.warning("WorkerPool:Current job started at %s: %s", - dt_util.as_local(start).isoformat(), job) - - busy_threshold *= 2 - - schedule() - - schedule() - - @callback - def stop_monitor(event): - """Stop the monitor.""" - handle.cancel() - - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_monitor) diff --git a/homeassistant/remote.py b/homeassistant/remote.py index ad616de5544..ae96a397826 100644 --- a/homeassistant/remote.py +++ b/homeassistant/remote.py @@ -16,6 +16,7 @@ import logging import time import threading import urllib.parse +import weakref from typing import Optional @@ -127,7 +128,7 @@ class HomeAssistant(ha.HomeAssistant): self.executor = ThreadPoolExecutor(max_workers=5) self.loop.set_default_executor(self.executor) self.loop.set_exception_handler(self._async_exception_handler) - self.pool = ha.create_worker_pool() + self._pending_tasks = weakref.WeakSet() self.bus = EventBus(remote_api, self) self.services = ha.ServiceRegistry(self.bus, self.add_job, self.loop) @@ -176,8 +177,6 @@ class HomeAssistant(ha.HomeAssistant): self.bus.fire(ha.EVENT_HOMEASSISTANT_STOP, origin=ha.EventOrigin.remote) - self.pool.stop() - # Disconnect master event forwarding disconnect_remote_events(self.remote_api, self.config.api) self.state = ha.CoreState.not_running diff --git a/homeassistant/util/__init__.py b/homeassistant/util/__init__.py index fe769f51129..de16a2d23d2 100644 --- a/homeassistant/util/__init__.py +++ b/homeassistant/util/__init__.py @@ -2,7 +2,6 @@ from collections.abc import MutableSet from itertools import chain import threading -import queue from datetime import datetime import re import enum @@ -302,111 +301,3 @@ class Throttle(object): throttle[0].release() return wrapper - - -class ThreadPool(object): - """A priority queue-based thread pool.""" - - def __init__(self, job_handler, worker_count=0): - """Initialize the pool. - - job_handler: method to be called from worker thread to handle job - worker_count: number of threads to run that handle jobs - busy_callback: method to be called when queue gets too big. - Parameters: worker_count, list of current_jobs, - pending_jobs_count - """ - self._job_handler = job_handler - - self.worker_count = 0 - self._work_queue = queue.Queue() - self.current_jobs = [] - self._quit_task = object() - - self.running = True - - for _ in range(worker_count): - self.add_worker() - - @property - def queue_size(self): - """Return estimated number of jobs that are waiting to be processed.""" - return self._work_queue.qsize() - - def add_worker(self): - """Add worker to the thread pool and reset warning limit.""" - if not self.running: - raise RuntimeError("ThreadPool not running") - - threading.Thread( - target=self._worker, daemon=True, - name='ThreadPool Worker {}'.format(self.worker_count)).start() - - self.worker_count += 1 - - def remove_worker(self): - """Remove worker from the thread pool and reset warning limit.""" - if not self.running: - raise RuntimeError("ThreadPool not running") - - self._work_queue.put(self._quit_task) - - self.worker_count -= 1 - - def add_job(self, job): - """Add a job to the queue.""" - if not self.running: - raise RuntimeError("ThreadPool not running") - - self._work_queue.put(job) - - def add_many_jobs(self, jobs): - """Add a list of jobs to the queue.""" - if not self.running: - raise RuntimeError("ThreadPool not running") - - for job in jobs: - self._work_queue.put(job) - - def block_till_done(self): - """Block till current work is done.""" - self._work_queue.join() - - def stop(self): - """Finish all the jobs and stops all the threads.""" - self.block_till_done() - - if not self.running: - return - - # Tell the workers to quit - for _ in range(self.worker_count): - self.remove_worker() - - self.running = False - - # Wait till all workers have quit - self.block_till_done() - - def _worker(self): - """Handle jobs for the thread pool.""" - while True: - # Get new item from work_queue - job = self._work_queue.get() - - if job is self._quit_task: - self._work_queue.task_done() - return - - # Add to current running jobs - job_log = (utcnow(), job) - self.current_jobs.append(job_log) - - # Do the job - self._job_handler(job) - - # Remove from current running job - self.current_jobs.remove(job_log) - - # Tell work_queue the task is done - self._work_queue.task_done() diff --git a/tests/common.py b/tests/common.py index d665e17a503..fd72a6b635b 100644 --- a/tests/common.py +++ b/tests/common.py @@ -40,7 +40,6 @@ def get_test_home_assistant(): loop = asyncio.new_event_loop() hass = loop.run_until_complete(async_test_home_assistant(loop)) - hass.allow_pool = True # FIXME should not be a daemon. Means hass.stop() not called in teardown stop_event = threading.Event() @@ -97,8 +96,6 @@ def async_test_home_assistant(loop): hass.state = ha.CoreState.running - hass.allow_pool = False - # Mock async_start orig_start = hass.async_start @@ -110,20 +107,6 @@ def async_test_home_assistant(loop): hass.async_start = mock_async_start - # Mock async_init_pool - orig_init = hass.async_init_pool - - @ha.callback - def mock_async_init_pool(): - """Prevent worker pool from being initialized.""" - if hass.allow_pool: - with patch('homeassistant.core._async_monitor_worker_pool'): - orig_init() - else: - assert False, 'Thread pool not allowed. Set hass.allow_pool = True' - - hass.async_init_pool = mock_async_init_pool - return hass diff --git a/tests/components/camera/test_generic.py b/tests/components/camera/test_generic.py index fde4bb2fbd4..e2ce9c15936 100644 --- a/tests/components/camera/test_generic.py +++ b/tests/components/camera/test_generic.py @@ -8,7 +8,6 @@ from homeassistant.bootstrap import setup_component @asyncio.coroutine def test_fetching_url(aioclient_mock, hass, test_client): """Test that it fetches the given url.""" - hass.allow_pool = True aioclient_mock.get('http://example.com', text='hello world') def setup_platform(): @@ -40,7 +39,6 @@ def test_fetching_url(aioclient_mock, hass, test_client): @asyncio.coroutine def test_limit_refetch(aioclient_mock, hass, test_client): """Test that it fetches the given url.""" - hass.allow_pool = True aioclient_mock.get('http://example.com/5a', text='hello world') aioclient_mock.get('http://example.com/10a', text='hello world') aioclient_mock.get('http://example.com/15a', text='hello planet') diff --git a/tests/components/camera/test_local_file.py b/tests/components/camera/test_local_file.py index 9a692b0a4ee..d43c138c570 100644 --- a/tests/components/camera/test_local_file.py +++ b/tests/components/camera/test_local_file.py @@ -14,8 +14,6 @@ from tests.common import assert_setup_component, mock_http_component @asyncio.coroutine def test_loading_file(hass, test_client): """Test that it loads image from disk.""" - hass.allow_pool = True - @mock.patch('os.path.isfile', mock.Mock(return_value=True)) @mock.patch('os.access', mock.Mock(return_value=True)) def setup_platform(): diff --git a/tests/test_core.py b/tests/test_core.py index 8a9fb8f6d4a..d3a2d4f353f 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -56,7 +56,7 @@ def test_async_add_job_add_threaded_job_to_pool(mock_iscoro): ha.HomeAssistant.async_add_job(hass, job) assert len(hass.loop.call_soon.mock_calls) == 0 assert len(hass.loop.create_task.mock_calls) == 0 - assert len(hass.pool.add_job.mock_calls) == 1 + assert len(hass.loop.run_in_executor.mock_calls) == 1 def test_async_run_job_calls_callback(): @@ -195,7 +195,6 @@ class TestEventBus(unittest.TestCase): def test_unsubscribe_listener(self): """Test unsubscribe listener from returned function.""" - self.hass.allow_pool = False calls = [] @ha.callback @@ -219,7 +218,6 @@ class TestEventBus(unittest.TestCase): def test_listen_once_event_with_callback(self): """Test listen_once_event method.""" - self.hass.allow_pool = False runs = [] @ha.callback @@ -237,7 +235,6 @@ class TestEventBus(unittest.TestCase): def test_listen_once_event_with_coroutine(self): """Test listen_once_event method.""" - self.hass.allow_pool = False runs = [] @asyncio.coroutine @@ -283,7 +280,6 @@ class TestEventBus(unittest.TestCase): def test_callback_event_listener(self): """Test a event listener listeners.""" - self.hass.allow_pool = False callback_calls = [] @ha.callback @@ -297,7 +293,6 @@ class TestEventBus(unittest.TestCase): def test_coroutine_event_listener(self): """Test a event listener listeners.""" - self.hass.allow_pool = False coroutine_calls = [] @asyncio.coroutine @@ -376,7 +371,6 @@ class TestStateMachine(unittest.TestCase): self.states = self.hass.states self.states.set("light.Bowl", "on") self.states.set("switch.AC", "off") - self.hass.allow_pool = False # pylint: disable=invalid-name def tearDown(self): @@ -523,7 +517,6 @@ class TestServiceRegistry(unittest.TestCase): def test_has_service(self): """Test has_service method.""" - self.hass.allow_pool = False self.assertTrue( self.services.has_service("tesT_domaiN", "tesT_servicE")) self.assertFalse( @@ -533,7 +526,6 @@ class TestServiceRegistry(unittest.TestCase): def test_services(self): """Test services.""" - self.hass.allow_pool = False expected = { 'test_domain': {'test_service': {'description': '', 'fields': {}}} } @@ -556,7 +548,6 @@ class TestServiceRegistry(unittest.TestCase): def test_call_non_existing_with_blocking(self): """Test non-existing with blocking.""" - self.hass.allow_pool = False prior = ha.SERVICE_CALL_LIMIT try: ha.SERVICE_CALL_LIMIT = 0.01 @@ -567,7 +558,6 @@ class TestServiceRegistry(unittest.TestCase): def test_async_service(self): """Test registering and calling an async service.""" - self.hass.allow_pool = False calls = [] @asyncio.coroutine @@ -584,7 +574,6 @@ class TestServiceRegistry(unittest.TestCase): def test_callback_service(self): """Test registering and calling an async service.""" - self.hass.allow_pool = False calls = [] @ha.callback @@ -638,72 +627,6 @@ class TestConfig(unittest.TestCase): self.assertEqual(expected, self.config.as_dict()) -class TestWorkerPool(unittest.TestCase): - """Test WorkerPool methods.""" - - def test_exception_during_job(self): - """Test exception during a job.""" - pool = ha.create_worker_pool(1) - - def malicious_job(_): - raise Exception("Test breaking worker pool") - - calls = [] - - def register_call(_): - calls.append(1) - - pool.add_job((malicious_job, None)) - pool.block_till_done() - pool.add_job((register_call, None)) - pool.block_till_done() - self.assertEqual(1, len(calls)) - - -class TestWorkerPoolMonitor(object): - """Test monitor_worker_pool.""" - - @patch('homeassistant.core._LOGGER.warning') - def test_worker_pool_monitor(self, mock_warning, event_loop): - """Test we log an error and increase threshold.""" - hass = MagicMock() - hass.pool.worker_count = 3 - schedule_handle = MagicMock() - hass.loop.call_later.return_value = schedule_handle - - ha._async_monitor_worker_pool(hass) - assert hass.loop.call_later.called - assert hass.bus.async_listen_once.called - assert not schedule_handle.called - - check_threshold = hass.loop.call_later.mock_calls[0][1][1] - - hass.pool.queue_size = 8 - check_threshold() - assert not mock_warning.called - - hass.pool.queue_size = 9 - check_threshold() - assert mock_warning.called - - mock_warning.reset_mock() - assert not mock_warning.called - - check_threshold() - assert not mock_warning.called - - hass.pool.queue_size = 17 - check_threshold() - assert not mock_warning.called - - hass.pool.queue_size = 18 - check_threshold() - assert mock_warning.called - - hass.bus.async_listen_once.mock_calls[0][1][1](None) - assert schedule_handle.cancel.called - - class TestAsyncCreateTimer(object): """Test create timer.""" From 1463fc4fe03a8fada97a8a7f0bbe12a3b79a5340 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20B=C3=A9trisey?= Date: Sat, 5 Nov 2016 21:04:44 +0100 Subject: [PATCH 003/127] Add Swisscom Internet-Box device tracker (#4123) * Add Swisscom Internet-Box device tracker * Add Swisscom device tracker to .coveragerc * Add timeout to requests Fix formatting and add missing comments to pass the lint test * Remove authentication which was not required I realised that there was no need to be authenticated to get the connected devices. Thanks Swisscom :/ * Moving config to a PLATFORM_SCHEMA and using voluptuous --- .coveragerc | 1 + .../components/device_tracker/swisscom.py | 109 ++++++++++++++++++ 2 files changed, 110 insertions(+) create mode 100644 homeassistant/components/device_tracker/swisscom.py diff --git a/.coveragerc b/.coveragerc index cd86d001e37..dbf9e37f7f9 100644 --- a/.coveragerc +++ b/.coveragerc @@ -150,6 +150,7 @@ omit = homeassistant/components/device_tracker/netgear.py homeassistant/components/device_tracker/nmap_tracker.py homeassistant/components/device_tracker/snmp.py + homeassistant/components/device_tracker/swisscom.py homeassistant/components/device_tracker/thomson.py homeassistant/components/device_tracker/tomato.py homeassistant/components/device_tracker/tplink.py diff --git a/homeassistant/components/device_tracker/swisscom.py b/homeassistant/components/device_tracker/swisscom.py new file mode 100644 index 00000000000..6afbb1ee47f --- /dev/null +++ b/homeassistant/components/device_tracker/swisscom.py @@ -0,0 +1,109 @@ +""" +Support for Swisscom routers (Internet-Box). + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/device_tracker.swisscom/ +""" +import logging +import threading +from datetime import timedelta + +import requests +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.device_tracker import DOMAIN, PLATFORM_SCHEMA +from homeassistant.const import CONF_HOST +from homeassistant.util import Throttle + +# Return cached results if last scan was less then this time ago. +MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5) + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_IP = '192.168.1.1' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_HOST, default=DEFAULT_IP): cv.string +}) + + +def get_scanner(hass, config): + """Return the Swisscom device scanner.""" + scanner = SwisscomDeviceScanner(config[DOMAIN]) + + return scanner if scanner.success_init else None + + +class SwisscomDeviceScanner(object): + """This class queries a router running Swisscom Internet-Box firmware.""" + + def __init__(self, config): + """Initialize the scanner.""" + self.host = config[CONF_HOST] + + self.lock = threading.Lock() + + self.last_results = {} + + # Test the router is accessible. + data = self.get_swisscom_data() + self.success_init = data is not None + + def scan_devices(self): + """Scan for new devices and return a list with found device IDs.""" + self._update_info() + return [client['mac'] for client in self.last_results] + + def get_device_name(self, device): + """Return the name of the given device or None if we don't know.""" + if not self.last_results: + return None + for client in self.last_results: + if client['mac'] == device: + return client['host'] + return None + + @Throttle(MIN_TIME_BETWEEN_SCANS) + def _update_info(self): + """Ensure the information from the Swisscom router is up to date. + + Return boolean if scanning successful. + """ + if not self.success_init: + return False + + with self.lock: + _LOGGER.info("Loading data from Swisscom Internet Box") + data = self.get_swisscom_data() + if not data: + return False + + active_clients = [client for client in data.values() if + client['status']] + self.last_results = active_clients + return True + + def get_swisscom_data(self): + """Retrieve data from Swisscom and return parsed result.""" + request = requests.post('http://' + self.host + '/ws', headers={ + 'Content-Type': 'application/x-sah-ws-4-call+json' + }, + data="""{"service":"Devices", + "method":"get", + "parameters": + {"expression":"lan and not self"}}""", + timeout=10) + + devices = {} + for device in request.json()['status']: + try: + devices[device['Key']] = { + 'ip': device['IPAddress'], + 'mac': device['PhysAddress'], + 'host': device['Name'], + 'status': device['Active'] + } + except (KeyError, requests.exceptions.RequestException): + pass + return devices From 88fc64c8a0c7a759f0a09b20b7fb3feec235153c Mon Sep 17 00:00:00 2001 From: Danijel Stojnic Date: Sat, 5 Nov 2016 21:05:15 +0100 Subject: [PATCH 004/127] Add Map support for Locative component (#4174) * Add Map support for Locative component The Locative App on the mobile is sending an HTTP request to the server where also the GPS location is sent. But the GPS location was not passed to the event device_tracker.see. Use the passed GPS location from Locative and pass it to the device_tracker.see event. With this the device is then also shown on the HA Map component. * Use existing constants for latitude and longitude Use the existing constants from homeassistant.consts: ATTR_LATITUDE for 'latitude' ATTR_LONGITUDE for 'longitude' * Reuse the "yield from self.hass.loop.run_in_executor" again * Use variable gps_location --- homeassistant/components/device_tracker/locative.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/device_tracker/locative.py b/homeassistant/components/device_tracker/locative.py index f6419ae2490..e6bd74e57c9 100644 --- a/homeassistant/components/device_tracker/locative.py +++ b/homeassistant/components/device_tracker/locative.py @@ -8,7 +8,9 @@ import asyncio from functools import partial import logging -from homeassistant.const import HTTP_UNPROCESSABLE_ENTITY, STATE_NOT_HOME +from homeassistant.const import (ATTR_LATITUDE, ATTR_LONGITUDE, + STATE_NOT_HOME, + HTTP_UNPROCESSABLE_ENTITY) from homeassistant.components.http import HomeAssistantView # pylint: disable=unused-import from homeassistant.components.device_tracker import ( # NOQA @@ -76,11 +78,13 @@ class LocativeView(HomeAssistantView): device = data['device'].replace('-', '') location_name = data['id'].lower() direction = data['trigger'] + gps_location = (data[ATTR_LATITUDE], data[ATTR_LONGITUDE]) if direction == 'enter': yield from self.hass.loop.run_in_executor( None, partial(self.see, dev_id=device, - location_name=location_name)) + location_name=location_name, + gps=gps_location)) return 'Setting location to {}'.format(location_name) elif direction == 'exit': @@ -88,9 +92,11 @@ class LocativeView(HomeAssistantView): '{}.{}'.format(DOMAIN, device)) if current_state is None or current_state.state == location_name: + location_name = STATE_NOT_HOME yield from self.hass.loop.run_in_executor( None, partial(self.see, dev_id=device, - location_name=STATE_NOT_HOME)) + location_name=location_name, + gps=gps_location)) return 'Setting location to not home' else: # Ignore the message if it is telling us to exit a zone that we From 3f3127a290b14627866484babbb76c2c663ba247 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 5 Nov 2016 13:28:11 -0700 Subject: [PATCH 005/127] Fix radiotherm I/O inside properties (#4227) --- homeassistant/components/climate/radiotherm.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/climate/radiotherm.py b/homeassistant/components/climate/radiotherm.py index c2d712e19bd..5fa3f891aac 100644 --- a/homeassistant/components/climate/radiotherm.py +++ b/homeassistant/components/climate/radiotherm.py @@ -69,6 +69,8 @@ class RadioThermostat(ClimateDevice): self._current_temperature = None self._current_operation = STATE_IDLE self._name = None + self._fmode = None + self._tmode = None self.hold_temp = hold_temp self.update() self._operation_list = [STATE_AUTO, STATE_COOL, STATE_HEAT, STATE_OFF] @@ -87,8 +89,8 @@ class RadioThermostat(ClimateDevice): def device_state_attributes(self): """Return the device specific state attributes.""" return { - ATTR_FAN: self.device.fmode['human'], - ATTR_MODE: self.device.tmode['human'] + ATTR_FAN: self._fmode, + ATTR_MODE: self._tmode, } @property @@ -115,10 +117,13 @@ class RadioThermostat(ClimateDevice): """Update the data from the thermostat.""" self._current_temperature = self.device.temp['raw'] self._name = self.device.name['raw'] - if self.device.tmode['human'] == 'Cool': + self._fmode = self.device.fmode['human'] + self._tmode = self.device.tmode['human'] + + if self._tmode == 'Cool': self._target_temperature = self.device.t_cool['raw'] self._current_operation = STATE_COOL - elif self.device.tmode['human'] == 'Heat': + elif self._tmode == 'Heat': self._target_temperature = self.device.t_heat['raw'] self._current_operation = STATE_HEAT else: From 22c3d014aaf9198863f1dc321eb8176972b051e6 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 5 Nov 2016 15:29:22 -0700 Subject: [PATCH 006/127] Remove chunked encoding (#4230) --- homeassistant/components/camera/__init__.py | 1 - homeassistant/components/camera/ffmpeg.py | 1 - homeassistant/components/camera/mjpeg.py | 1 - homeassistant/components/camera/synology.py | 1 - 4 files changed, 4 deletions(-) diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index d02e7954349..6724598419f 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -101,7 +101,6 @@ class Camera(Entity): response.content_type = ('multipart/x-mixed-replace; ' 'boundary=--jpegboundary') - response.enable_chunked_encoding() yield from response.prepare(request) def write(img_bytes): diff --git a/homeassistant/components/camera/ffmpeg.py b/homeassistant/components/camera/ffmpeg.py index 8e238bfdea7..c3f0ffbfe0b 100644 --- a/homeassistant/components/camera/ffmpeg.py +++ b/homeassistant/components/camera/ffmpeg.py @@ -75,7 +75,6 @@ class FFmpegCamera(Camera): response = web.StreamResponse() response.content_type = 'multipart/x-mixed-replace;boundary=ffserver' - response.enable_chunked_encoding() yield from response.prepare(request) diff --git a/homeassistant/components/camera/mjpeg.py b/homeassistant/components/camera/mjpeg.py index 81759fa86df..e8799d1be34 100644 --- a/homeassistant/components/camera/mjpeg.py +++ b/homeassistant/components/camera/mjpeg.py @@ -112,7 +112,6 @@ class MjpegCamera(Camera): response = web.StreamResponse() response.content_type = stream.headers.get(CONTENT_TYPE_HEADER) - response.enable_chunked_encoding() yield from response.prepare(request) diff --git a/homeassistant/components/camera/synology.py b/homeassistant/components/camera/synology.py index 4abdf8d22dd..4ca63c16d7d 100644 --- a/homeassistant/components/camera/synology.py +++ b/homeassistant/components/camera/synology.py @@ -271,7 +271,6 @@ class SynologyCamera(Camera): response = web.StreamResponse() response.content_type = stream.headers.get(CONTENT_TYPE_HEADER) - response.enable_chunked_encoding() yield from response.prepare(request) From 62785c24316ef0f389663f249dc3b0626211fc11 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 5 Nov 2016 16:36:20 -0700 Subject: [PATCH 007/127] More async tests (#4223) * Annotate test callbacks to be async * Convert device_sun_light_trigger to be async --- .../components/device_sun_light_trigger.py | 29 ++++++++++++------- homeassistant/components/light/__init__.py | 26 +++++++++++++++-- homeassistant/components/sun.py | 20 ++++++++++--- tests/common.py | 6 +++- tests/components/automation/test_event.py | 2 ++ tests/components/automation/test_init.py | 2 ++ tests/components/automation/test_mqtt.py | 2 ++ .../automation/test_numeric_state.py | 2 ++ tests/components/automation/test_state.py | 2 ++ tests/components/automation/test_sun.py | 2 ++ tests/components/automation/test_template.py | 2 ++ tests/components/automation/test_time.py | 2 ++ tests/components/automation/test_zone.py | 2 ++ .../climate/test_generic_thermostat.py | 6 +++- tests/components/switch/test_template.py | 3 +- tests/components/test_alexa.py | 7 ++++- tests/components/test_api.py | 2 ++ tests/components/test_conversation.py | 3 ++ tests/components/test_script.py | 1 + tests/test_core.py | 8 ++++- tests/test_remote.py | 2 ++ 21 files changed, 110 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/device_sun_light_trigger.py b/homeassistant/components/device_sun_light_trigger.py index ed31e624b91..9da4348362d 100644 --- a/homeassistant/components/device_sun_light_trigger.py +++ b/homeassistant/components/device_sun_light_trigger.py @@ -9,6 +9,7 @@ from datetime import timedelta import voluptuous as vol +from homeassistant.core import callback import homeassistant.util.dt as dt_util from homeassistant.const import STATE_HOME, STATE_NOT_HOME from homeassistant.helpers.event import track_point_in_time @@ -79,21 +80,22 @@ def setup(hass, config): return None return next_setting - LIGHT_TRANSITION_TIME * len(light_ids) - def turn_light_on_before_sunset(light_id): + def async_turn_on_before_sunset(light_id): """Helper function to turn on lights. Speed is slow if there are devices home and the light is not on yet. """ if not device_tracker.is_on(hass) or light.is_on(hass, light_id): return - light.turn_on(hass, light_id, - transition=LIGHT_TRANSITION_TIME.seconds, - profile=light_profile) + light.async_turn_on(hass, light_id, + transition=LIGHT_TRANSITION_TIME.seconds, + profile=light_profile) # Track every time sun rises so we can schedule a time-based # pre-sun set event @track_state_change(sun.ENTITY_ID, sun.STATE_BELOW_HORIZON, sun.STATE_ABOVE_HORIZON) + @callback def schedule_lights_at_sun_set(hass, entity, old_state, new_state): """The moment sun sets we want to have all the lights on. @@ -104,16 +106,21 @@ def setup(hass, config): if not start_point: return - def turn_on(light_id): + def async_turn_on_factory(light_id): """Lambda can keep track of function parameters. No local parameters. If we put the lambda directly in the below statement only the last light will be turned on. """ - return lambda now: turn_light_on_before_sunset(light_id) + @callback + def async_turn_on_light(now): + """Turn on specific light.""" + async_turn_on_before_sunset(light_id) + + return async_turn_on_light for index, light_id in enumerate(light_ids): - track_point_in_time(hass, turn_on(light_id), + track_point_in_time(hass, async_turn_on_factory(light_id), start_point + index * LIGHT_TRANSITION_TIME) # If the sun is already above horizon schedule the time-based pre-sun set @@ -122,6 +129,7 @@ def setup(hass, config): schedule_lights_at_sun_set(hass, None, None, None) @track_state_change(device_entity_ids, STATE_NOT_HOME, STATE_HOME) + @callback def check_light_on_dev_state_change(hass, entity, old_state, new_state): """Handle tracked device state changes.""" # pylint: disable=unused-variable @@ -136,7 +144,7 @@ def setup(hass, config): # Do we need lights? if light_needed: logger.info("Home coming event for %s. Turning lights on", entity) - light.turn_on(hass, light_ids, profile=light_profile) + light.async_turn_on(hass, light_ids, profile=light_profile) # Are we in the time span were we would turn on the lights # if someone would be home? @@ -149,7 +157,7 @@ def setup(hass, config): # when the fading in started and turn it on if so for index, light_id in enumerate(light_ids): if now > start_point + index * LIGHT_TRANSITION_TIME: - light.turn_on(hass, light_id) + light.async_turn_on(hass, light_id) else: # If this light didn't happen to be turned on yet so @@ -158,6 +166,7 @@ def setup(hass, config): if not disable_turn_off: @track_state_change(device_group, STATE_HOME, STATE_NOT_HOME) + @callback def turn_off_lights_when_all_leave(hass, entity, old_state, new_state): """Handle device group state change.""" # pylint: disable=unused-variable @@ -166,6 +175,6 @@ def setup(hass, config): logger.info( "Everyone has left but there are lights on. Turning them off") - light.turn_off(hass, light_ids) + light.async_turn_off(hass, light_ids) return True diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index e3437d89e72..b4708164fe2 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -10,6 +10,7 @@ import csv import voluptuous as vol +from homeassistant.core import callback from homeassistant.components import group from homeassistant.config import load_yaml_config_file from homeassistant.const import ( @@ -20,6 +21,7 @@ from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa import homeassistant.helpers.config_validation as cv import homeassistant.util.color as color_util +from homeassistant.util.async import run_callback_threadsafe DOMAIN = "light" @@ -128,6 +130,18 @@ def turn_on(hass, entity_id=None, transition=None, brightness=None, rgb_color=None, xy_color=None, color_temp=None, white_value=None, profile=None, flash=None, effect=None, color_name=None): """Turn all or specified light on.""" + run_callback_threadsafe( + hass.loop, async_turn_on, hass, entity_id, transition, brightness, + rgb_color, xy_color, color_temp, white_value, + profile, flash, effect, color_name).result() + + +@callback +def async_turn_on(hass, entity_id=None, transition=None, brightness=None, + rgb_color=None, xy_color=None, color_temp=None, + white_value=None, profile=None, flash=None, effect=None, + color_name=None): + """Turn all or specified light on.""" data = { key: value for key, value in [ (ATTR_ENTITY_ID, entity_id), @@ -144,10 +158,17 @@ def turn_on(hass, entity_id=None, transition=None, brightness=None, ] if value is not None } - hass.services.call(DOMAIN, SERVICE_TURN_ON, data) + hass.async_add_job(hass.services.async_call, DOMAIN, SERVICE_TURN_ON, data) def turn_off(hass, entity_id=None, transition=None): + """Turn all or specified light off.""" + run_callback_threadsafe( + hass.loop, async_turn_off, hass, entity_id, transition).result() + + +@callback +def async_turn_off(hass, entity_id=None, transition=None): """Turn all or specified light off.""" data = { key: value for key, value in [ @@ -156,7 +177,8 @@ def turn_off(hass, entity_id=None, transition=None): ] if value is not None } - hass.services.call(DOMAIN, SERVICE_TURN_OFF, data) + hass.async_add_job(hass.services.async_call, DOMAIN, SERVICE_TURN_OFF, + data) def toggle(hass, entity_id=None, transition=None): diff --git a/homeassistant/components/sun.py b/homeassistant/components/sun.py index 858d49a8e43..20bebe79f9e 100644 --- a/homeassistant/components/sun.py +++ b/homeassistant/components/sun.py @@ -49,14 +49,20 @@ def is_on(hass, entity_id=None): def next_setting(hass, entity_id=None): - """Local datetime object of the next sun setting.""" + """Local datetime object of the next sun setting. + + Async friendly. + """ utc_next = next_setting_utc(hass, entity_id) return dt_util.as_local(utc_next) if utc_next else None def next_setting_utc(hass, entity_id=None): - """UTC datetime object of the next sun setting.""" + """UTC datetime object of the next sun setting. + + Async friendly. + """ entity_id = entity_id or ENTITY_ID state = hass.states.get(ENTITY_ID) @@ -71,14 +77,20 @@ def next_setting_utc(hass, entity_id=None): def next_rising(hass, entity_id=None): - """Local datetime object of the next sun rising.""" + """Local datetime object of the next sun rising. + + Async friendly. + """ utc_next = next_rising_utc(hass, entity_id) return dt_util.as_local(utc_next) if utc_next else None def next_rising_utc(hass, entity_id=None): - """UTC datetime object of the next sun rising.""" + """UTC datetime object of the next sun rising. + + Async friendly. + """ entity_id = entity_id or ENTITY_ID state = hass.states.get(ENTITY_ID) diff --git a/tests/common.py b/tests/common.py index fd72a6b635b..6f017d29b46 100644 --- a/tests/common.py +++ b/tests/common.py @@ -129,8 +129,12 @@ def mock_service(hass, domain, service): """ calls = [] + @ha.callback + def mock_service(call): + calls.append(call) + # pylint: disable=unnecessary-lambda - hass.services.register(domain, service, lambda call: calls.append(call)) + hass.services.register(domain, service, mock_service) return calls diff --git a/tests/components/automation/test_event.py b/tests/components/automation/test_event.py index 2ab62833eda..22158402ff9 100644 --- a/tests/components/automation/test_event.py +++ b/tests/components/automation/test_event.py @@ -1,6 +1,7 @@ """The tests for the Event automation.""" import unittest +from homeassistant.core import callback from homeassistant.bootstrap import setup_component import homeassistant.components.automation as automation @@ -16,6 +17,7 @@ class TestAutomationEvent(unittest.TestCase): self.hass.config.components.append('group') self.calls = [] + @callback def record_call(service): """Helper for recording the call.""" self.calls.append(service) diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index 2956be98b00..2459542b629 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -2,6 +2,7 @@ import unittest from unittest.mock import patch +from homeassistant.core import callback from homeassistant.bootstrap import setup_component import homeassistant.components.automation as automation from homeassistant.const import ATTR_ENTITY_ID @@ -22,6 +23,7 @@ class TestAutomation(unittest.TestCase): self.hass.config.components.append('group') self.calls = [] + @callback def record_call(service): """Record call.""" self.calls.append(service) diff --git a/tests/components/automation/test_mqtt.py b/tests/components/automation/test_mqtt.py index b7da76fda20..4e58dc7a442 100644 --- a/tests/components/automation/test_mqtt.py +++ b/tests/components/automation/test_mqtt.py @@ -1,6 +1,7 @@ """The tests for the MQTT automation.""" import unittest +from homeassistant.core import callback from homeassistant.bootstrap import setup_component import homeassistant.components.automation as automation from tests.common import ( @@ -17,6 +18,7 @@ class TestAutomationMQTT(unittest.TestCase): mock_mqtt_component(self.hass) self.calls = [] + @callback def record_call(service): self.calls.append(service) diff --git a/tests/components/automation/test_numeric_state.py b/tests/components/automation/test_numeric_state.py index fa2d237ee00..d0aedd87f46 100644 --- a/tests/components/automation/test_numeric_state.py +++ b/tests/components/automation/test_numeric_state.py @@ -1,6 +1,7 @@ """The tests for numeric state automation.""" import unittest +from homeassistant.core import callback from homeassistant.bootstrap import setup_component import homeassistant.components.automation as automation @@ -16,6 +17,7 @@ class TestAutomationNumericState(unittest.TestCase): self.hass.config.components.append('group') self.calls = [] + @callback def record_call(service): """Helper to record calls.""" self.calls.append(service) diff --git a/tests/components/automation/test_state.py b/tests/components/automation/test_state.py index 06c127ca6b7..3b4e4486112 100644 --- a/tests/components/automation/test_state.py +++ b/tests/components/automation/test_state.py @@ -3,6 +3,7 @@ import unittest from datetime import timedelta from unittest.mock import patch +from homeassistant.core import callback from homeassistant.bootstrap import setup_component import homeassistant.util.dt as dt_util import homeassistant.components.automation as automation @@ -21,6 +22,7 @@ class TestAutomationState(unittest.TestCase): self.hass.states.set('test.entity', 'hello') self.calls = [] + @callback def record_call(service): self.calls.append(service) diff --git a/tests/components/automation/test_sun.py b/tests/components/automation/test_sun.py index ca3d1618013..475a8f55259 100644 --- a/tests/components/automation/test_sun.py +++ b/tests/components/automation/test_sun.py @@ -3,6 +3,7 @@ from datetime import datetime import unittest from unittest.mock import patch +from homeassistant.core import callback from homeassistant.bootstrap import setup_component from homeassistant.components import sun import homeassistant.components.automation as automation @@ -22,6 +23,7 @@ class TestAutomationSun(unittest.TestCase): self.calls = [] + @callback def record_call(service): self.calls.append(service) diff --git a/tests/components/automation/test_template.py b/tests/components/automation/test_template.py index fcd1a48983a..1430d303140 100644 --- a/tests/components/automation/test_template.py +++ b/tests/components/automation/test_template.py @@ -1,6 +1,7 @@ """The tests for the Template automation.""" import unittest +from homeassistant.core import callback from homeassistant.bootstrap import setup_component import homeassistant.components.automation as automation @@ -17,6 +18,7 @@ class TestAutomationTemplate(unittest.TestCase): self.hass.states.set('test.entity', 'hello') self.calls = [] + @callback def record_call(service): """helper for recording calls.""" self.calls.append(service) diff --git a/tests/components/automation/test_time.py b/tests/components/automation/test_time.py index dba100aa345..ff2d20145d9 100644 --- a/tests/components/automation/test_time.py +++ b/tests/components/automation/test_time.py @@ -3,6 +3,7 @@ from datetime import timedelta import unittest from unittest.mock import patch +from homeassistant.core import callback from homeassistant.bootstrap import setup_component import homeassistant.util.dt as dt_util import homeassistant.components.automation as automation @@ -20,6 +21,7 @@ class TestAutomationTime(unittest.TestCase): self.hass.config.components.append('group') self.calls = [] + @callback def record_call(service): self.calls.append(service) diff --git a/tests/components/automation/test_zone.py b/tests/components/automation/test_zone.py index e454b8b5b8b..d81cb8f0bd5 100644 --- a/tests/components/automation/test_zone.py +++ b/tests/components/automation/test_zone.py @@ -1,6 +1,7 @@ """The tests for the location automation.""" import unittest +from homeassistant.core import callback from homeassistant.bootstrap import setup_component from homeassistant.components import automation, zone @@ -25,6 +26,7 @@ class TestAutomationZone(unittest.TestCase): self.calls = [] + @callback def record_call(service): self.calls.append(service) diff --git a/tests/components/climate/test_generic_thermostat.py b/tests/components/climate/test_generic_thermostat.py index 070ca31f8df..d11d925ef41 100644 --- a/tests/components/climate/test_generic_thermostat.py +++ b/tests/components/climate/test_generic_thermostat.py @@ -3,7 +3,7 @@ import datetime import unittest from unittest import mock - +from homeassistant.core import callback from homeassistant.bootstrap import setup_component from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, @@ -216,6 +216,7 @@ class TestClimateGenericThermostat(unittest.TestCase): self.hass.states.set(ENT_SWITCH, STATE_ON if is_on else STATE_OFF) self.calls = [] + @callback def log_call(call): """Log service calls.""" self.calls.append(call) @@ -306,6 +307,7 @@ class TestClimateGenericThermostatACMode(unittest.TestCase): self.hass.states.set(ENT_SWITCH, STATE_ON if is_on else STATE_OFF) self.calls = [] + @callback def log_call(call): """Log service calls.""" self.calls.append(call) @@ -397,6 +399,7 @@ class TestClimateGenericThermostatACModeMinCycle(unittest.TestCase): self.hass.states.set(ENT_SWITCH, STATE_ON if is_on else STATE_OFF) self.calls = [] + @callback def log_call(call): """Log service calls.""" self.calls.append(call) @@ -487,6 +490,7 @@ class TestClimateGenericThermostatMinCycle(unittest.TestCase): self.hass.states.set(ENT_SWITCH, STATE_ON if is_on else STATE_OFF) self.calls = [] + @callback def log_call(call): """Log service calls.""" self.calls.append(call) diff --git a/tests/components/switch/test_template.py b/tests/components/switch/test_template.py index af91c9a565b..2f67564e6e8 100644 --- a/tests/components/switch/test_template.py +++ b/tests/components/switch/test_template.py @@ -1,7 +1,7 @@ """The tests for the Template switch platform.""" +from homeassistant.core import callback import homeassistant.bootstrap as bootstrap import homeassistant.components as core - from homeassistant.const import ( STATE_ON, STATE_OFF) @@ -21,6 +21,7 @@ class TestTemplateSwitch: self.hass = get_test_home_assistant() self.calls = [] + @callback def record_call(service): """Track function calls..""" self.calls.append(service) diff --git a/tests/components/test_alexa.py b/tests/components/test_alexa.py index 28a80868163..41e7474974d 100644 --- a/tests/components/test_alexa.py +++ b/tests/components/test_alexa.py @@ -6,6 +6,7 @@ import unittest import requests +from homeassistant.core import callback from homeassistant import bootstrap, const from homeassistant.components import alexa, http @@ -47,7 +48,11 @@ def setUpModule(): {http.DOMAIN: {http.CONF_API_PASSWORD: API_PASSWORD, http.CONF_SERVER_PORT: SERVER_PORT}}) - hass.services.register("test", "alexa", lambda call: calls.append(call)) + @callback + def mock_service(call): + calls.append(call) + + hass.services.register("test", "alexa", mock_service) bootstrap.setup_component(hass, alexa.DOMAIN, { # Key is here to verify we allow other keys in config too diff --git a/tests/components/test_api.py b/tests/components/test_api.py index a70048956eb..28ffa7405e7 100644 --- a/tests/components/test_api.py +++ b/tests/components/test_api.py @@ -288,6 +288,7 @@ class TestAPI(unittest.TestCase): """Test if the API allows us to call a service.""" test_value = [] + @ha.callback def listener(service_call): """Helper method that will verify that our service got called.""" test_value.append(1) @@ -307,6 +308,7 @@ class TestAPI(unittest.TestCase): """Test if the API allows us to call a service.""" test_value = [] + @ha.callback def listener(service_call): """Helper method that will verify that our service got called. diff --git a/tests/components/test_conversation.py b/tests/components/test_conversation.py index 1172221f16f..919c95be4c5 100644 --- a/tests/components/test_conversation.py +++ b/tests/components/test_conversation.py @@ -3,6 +3,7 @@ import unittest from unittest.mock import patch +from homeassistant.core import callback from homeassistant.bootstrap import setup_component import homeassistant.components as core_components from homeassistant.components import conversation @@ -38,6 +39,7 @@ class TestConversation(unittest.TestCase): """Setup and perform good turn on requests.""" calls = [] + @callback def record_call(service): calls.append(service) @@ -56,6 +58,7 @@ class TestConversation(unittest.TestCase): """Setup and perform good turn off requests.""" calls = [] + @callback def record_call(service): calls.append(service) diff --git a/tests/components/test_script.py b/tests/components/test_script.py index 979e435456c..76df0a24055 100644 --- a/tests/components/test_script.py +++ b/tests/components/test_script.py @@ -130,6 +130,7 @@ class TestScriptComponent(unittest.TestCase): """Test different ways of passing in variables.""" calls = [] + @callback def record_call(service): """Add recorded event to set.""" calls.append(service) diff --git a/tests/test_core.py b/tests/test_core.py index d3a2d4f353f..51653500b6e 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -508,7 +508,12 @@ class TestServiceRegistry(unittest.TestCase): """Setup things to be run when tests are started.""" self.hass = get_test_home_assistant() self.services = self.hass.services - self.services.register("Test_Domain", "TEST_SERVICE", lambda x: None) + + @ha.callback + def mock_service(call): + pass + + self.services.register("Test_Domain", "TEST_SERVICE", mock_service) # pylint: disable=invalid-name def tearDown(self): @@ -535,6 +540,7 @@ class TestServiceRegistry(unittest.TestCase): """Test call with blocking.""" calls = [] + @ha.callback def service_handler(call): """Service handler.""" calls.append(call) diff --git a/tests/test_remote.py b/tests/test_remote.py index 8692fd4a133..55d8ca18b5f 100644 --- a/tests/test_remote.py +++ b/tests/test_remote.py @@ -116,6 +116,7 @@ class TestRemoteMethods(unittest.TestCase): """Test Python API fire_event.""" test_value = [] + @ha.callback def listener(event): """Helper method that will verify our event got called.""" test_value.append(1) @@ -200,6 +201,7 @@ class TestRemoteMethods(unittest.TestCase): """Test Python API services.call.""" test_value = [] + @ha.callback def listener(service_call): """Helper method that will verify that our service got called.""" test_value.append(1) From ad8645baf4d7f44aa5594dc82ccbbaef3ce0b300 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Sun, 6 Nov 2016 00:58:29 +0100 Subject: [PATCH 008/127] Sonos fix for slow update (#4232) * Sonos fix for slow update * fix auto update on discovery * fix unittest --- .../components/media_player/sonos.py | 29 ++++++++++--------- tests/components/media_player/test_sonos.py | 23 ++++++++++----- 2 files changed, 31 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/media_player/sonos.py b/homeassistant/components/media_player/sonos.py index 39b9559aa59..89f5d7b07ed 100644 --- a/homeassistant/components/media_player/sonos.py +++ b/homeassistant/components/media_player/sonos.py @@ -80,7 +80,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): if player.is_visible: device = SonosDevice(hass, player) - add_devices([device]) + add_devices([device], True) if not DEVICES: register_services(hass) DEVICES.append(device) @@ -106,7 +106,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): return False DEVICES = [SonosDevice(hass, p) for p in players] - add_devices(DEVICES) + add_devices(DEVICES, True) register_services(hass) _LOGGER.info('Added %s Sonos speakers', len(players)) return True @@ -256,6 +256,7 @@ class SonosDevice(MediaPlayerDevice): self.hass = hass self.volume_increment = 5 + self._unique_id = player.uid self._player = player self._player_volume = None self._player_volume_muted = None @@ -278,7 +279,8 @@ class SonosDevice(MediaPlayerDevice): self._current_track_is_radio_stream = False self._queue = None self._last_avtransport_event = None - self.update() + self._is_playing_line_in = None + self._is_playing_tv = None self.soco_snapshot = Snapshot(self._player) @property @@ -286,14 +288,10 @@ class SonosDevice(MediaPlayerDevice): """Polling needed.""" return True - def update_sonos(self, now): - """Update state, called by track_utc_time_change.""" - self.update_ha_state(True) - @property def unique_id(self): """Return an unique ID.""" - return self._player.uid + return self._unique_id @property def name(self): @@ -354,6 +352,9 @@ class SonosDevice(MediaPlayerDevice): if is_available: + self._is_playing_tv = self._player.is_playing_tv + self._is_playing_line_in = self._player.is_playing_line_in + track_info = None if self._last_avtransport_event: variables = self._last_avtransport_event.variables @@ -511,10 +512,10 @@ class SonosDevice(MediaPlayerDevice): # update state of the whole group # pylint: disable=protected-access for device in [x for x in DEVICES if x._coordinator == self]: - if device.entity_id: - device.update_ha_state(False) + if device.entity_id is not self.entity_id: + self.hass.add_job(device.async_update_ha_state) - if self._queue is None and self.entity_id: + if self._queue is None: self._subscribe_to_player_events() else: self._player_volume = None @@ -534,6 +535,8 @@ class SonosDevice(MediaPlayerDevice): self._support_previous_track = False self._support_next_track = False self._support_pause = False + self._is_playing_tv = False + self._is_playing_line_in = False self._last_avtransport_event = None @@ -713,9 +716,9 @@ class SonosDevice(MediaPlayerDevice): @property def source(self): """Name of the current input source.""" - if self._player.is_playing_line_in: + if self._is_playing_line_in: return SUPPORT_SOURCE_LINEIN - if self._player.is_playing_tv: + if self._is_playing_tv: return SUPPORT_SOURCE_TV return None diff --git a/tests/components/media_player/test_sonos.py b/tests/components/media_player/test_sonos.py index dfd308d5459..b170f14c372 100644 --- a/tests/components/media_player/test_sonos.py +++ b/tests/components/media_player/test_sonos.py @@ -92,6 +92,13 @@ class SoCoMock(): return "RINCON_XXXXXXXXXXXXXXXXX" +def fake_add_device(devices, update_befor_add=False): + """Fake add device / update.""" + if update_befor_add: + for speaker in devices: + speaker.update() + + class TestSonosMediaPlayer(unittest.TestCase): """Test the media_player module.""" @@ -117,7 +124,7 @@ class TestSonosMediaPlayer(unittest.TestCase): @mock.patch('socket.create_connection', side_effect=socket.error()) def test_ensure_setup_discovery(self, *args): """Test a single device using the autodiscovery provided by HASS.""" - sonos.setup_platform(self.hass, {}, mock.MagicMock(), '192.0.2.1') + sonos.setup_platform(self.hass, {}, fake_add_device, '192.0.2.1') # Ensure registration took place (#2558) self.assertEqual(len(sonos.DEVICES), 1) @@ -129,7 +136,7 @@ class TestSonosMediaPlayer(unittest.TestCase): """Test a single address config'd by the HASS config file.""" sonos.setup_platform(self.hass, {'hosts': '192.0.2.1'}, - mock.MagicMock()) + fake_add_device) # Ensure registration took place (#2558) self.assertEqual(len(sonos.DEVICES), 1) @@ -140,7 +147,7 @@ class TestSonosMediaPlayer(unittest.TestCase): @mock.patch('socket.create_connection', side_effect=socket.error()) def test_ensure_setup_sonos_discovery(self, *args): """Test a single device using the autodiscovery provided by Sonos.""" - sonos.setup_platform(self.hass, {}, mock.MagicMock()) + sonos.setup_platform(self.hass, {}, fake_add_device) self.assertEqual(len(sonos.DEVICES), 1) self.assertEqual(sonos.DEVICES[0].name, 'Kitchen') @@ -149,7 +156,7 @@ class TestSonosMediaPlayer(unittest.TestCase): @mock.patch.object(SoCoMock, 'partymode') def test_sonos_group_players(self, partymodeMock, *args): """Ensuring soco methods called for sonos_group_players service.""" - sonos.setup_platform(self.hass, {}, mock.MagicMock(), '192.0.2.1') + sonos.setup_platform(self.hass, {}, fake_add_device, '192.0.2.1') device = sonos.DEVICES[-1] partymodeMock.return_value = True device.group_players() @@ -161,7 +168,7 @@ class TestSonosMediaPlayer(unittest.TestCase): @mock.patch.object(SoCoMock, 'unjoin') def test_sonos_unjoin(self, unjoinMock, *args): """Ensuring soco methods called for sonos_unjoin service.""" - sonos.setup_platform(self.hass, {}, mock.MagicMock(), '192.0.2.1') + sonos.setup_platform(self.hass, {}, fake_add_device, '192.0.2.1') device = sonos.DEVICES[-1] unjoinMock.return_value = True device.unjoin() @@ -173,7 +180,7 @@ class TestSonosMediaPlayer(unittest.TestCase): @mock.patch.object(SoCoMock, 'set_sleep_timer') def test_sonos_set_sleep_timer(self, set_sleep_timerMock, *args): """Ensuring soco methods called for sonos_set_sleep_timer service.""" - sonos.setup_platform(self.hass, {}, mock.MagicMock(), '192.0.2.1') + sonos.setup_platform(self.hass, {}, fake_add_device, '192.0.2.1') device = sonos.DEVICES[-1] device.set_sleep_timer(30) set_sleep_timerMock.assert_called_once_with(30) @@ -193,7 +200,7 @@ class TestSonosMediaPlayer(unittest.TestCase): @mock.patch.object(soco.snapshot.Snapshot, 'snapshot') def test_sonos_snapshot(self, snapshotMock, *args): """Ensuring soco methods called for sonos_snapshot service.""" - sonos.setup_platform(self.hass, {}, mock.MagicMock(), '192.0.2.1') + sonos.setup_platform(self.hass, {}, fake_add_device, '192.0.2.1') device = sonos.DEVICES[-1] snapshotMock.return_value = True device.snapshot() @@ -205,7 +212,7 @@ class TestSonosMediaPlayer(unittest.TestCase): @mock.patch.object(soco.snapshot.Snapshot, 'restore') def test_sonos_restore(self, restoreMock, *args): """Ensuring soco methods called for sonos_restor service.""" - sonos.setup_platform(self.hass, {}, mock.MagicMock(), '192.0.2.1') + sonos.setup_platform(self.hass, {}, fake_add_device, '192.0.2.1') device = sonos.DEVICES[-1] restoreMock.return_value = True device.restore() From 382ac5c3b56659c1b8721e3fdb422e045c325b32 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Sun, 6 Nov 2016 01:01:03 +0100 Subject: [PATCH 009/127] Async cleanups with new handling and executor (#4234) --- homeassistant/components/binary_sensor/template.py | 4 ++-- homeassistant/components/sensor/min_max.py | 6 +++--- homeassistant/components/sensor/random.py | 3 +-- homeassistant/components/sensor/statistics.py | 6 +++--- homeassistant/components/sensor/template.py | 4 ++-- homeassistant/components/sensor/time_date.py | 2 +- homeassistant/components/sensor/worldclock.py | 3 +-- homeassistant/components/switch/template.py | 4 ++-- 8 files changed, 15 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/binary_sensor/template.py b/homeassistant/components/binary_sensor/template.py index 6d470f335e3..e097c7c0ea4 100644 --- a/homeassistant/components/binary_sensor/template.py +++ b/homeassistant/components/binary_sensor/template.py @@ -63,7 +63,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): _LOGGER.error('No sensors added') return False - hass.loop.create_task(async_add_devices(sensors, True)) + yield from async_add_devices(sensors, True) return True @@ -84,7 +84,7 @@ class BinarySensorTemplate(BinarySensorDevice): @callback def template_bsensor_state_listener(entity, old_state, new_state): """Called when the target device changes state.""" - hass.loop.create_task(self.async_update_ha_state(True)) + hass.async_add_job(self.async_update_ha_state, True) async_track_state_change( hass, entity_ids, template_bsensor_state_listener) diff --git a/homeassistant/components/sensor/min_max.py b/homeassistant/components/sensor/min_max.py index 9fd8cad4c38..b41966ac861 100644 --- a/homeassistant/components/sensor/min_max.py +++ b/homeassistant/components/sensor/min_max.py @@ -58,8 +58,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): name = config.get(CONF_NAME) sensor_type = config.get(CONF_TYPE) - hass.loop.create_task(async_add_devices( - [MinMaxSensor(hass, entity_ids, name, sensor_type)], True)) + yield from async_add_devices( + [MinMaxSensor(hass, entity_ids, name, sensor_type)], True) return True @@ -100,7 +100,7 @@ class MinMaxSensor(Entity): _LOGGER.warning("Unable to store state. " "Only numerical states are supported") - hass.loop.create_task(self.async_update_ha_state(True)) + hass.async_add_job(self.async_update_ha_state, True) async_track_state_change( hass, entity_ids, async_min_max_sensor_state_listener) diff --git a/homeassistant/components/sensor/random.py b/homeassistant/components/sensor/random.py index 5a48ea40863..a495c4ddb8b 100644 --- a/homeassistant/components/sensor/random.py +++ b/homeassistant/components/sensor/random.py @@ -36,8 +36,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): minimum = config.get(CONF_MINIMUM) maximum = config.get(CONF_MAXIMUM) - hass.loop.create_task(async_add_devices( - [RandomSensor(name, minimum, maximum)], True)) + yield from async_add_devices([RandomSensor(name, minimum, maximum)], True) return True diff --git a/homeassistant/components/sensor/statistics.py b/homeassistant/components/sensor/statistics.py index e5672f3a510..15184fbbc11 100644 --- a/homeassistant/components/sensor/statistics.py +++ b/homeassistant/components/sensor/statistics.py @@ -50,8 +50,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): name = config.get(CONF_NAME) sampling_size = config.get(CONF_SAMPLING_SIZE) - hass.loop.create_task(async_add_devices( - [StatisticsSensor(hass, entity_id, name, sampling_size)], True)) + yield from async_add_devices( + [StatisticsSensor(hass, entity_id, name, sampling_size)], True) return True @@ -90,7 +90,7 @@ class StatisticsSensor(Entity): except ValueError: self.count = self.count + 1 - hass.loop.create_task(self.async_update_ha_state(True)) + hass.async_add_job(self.async_update_ha_state, True) async_track_state_change( hass, entity_id, async_stats_sensor_state_listener) diff --git a/homeassistant/components/sensor/template.py b/homeassistant/components/sensor/template.py index 4262eb3d55f..44c6d2e08ea 100644 --- a/homeassistant/components/sensor/template.py +++ b/homeassistant/components/sensor/template.py @@ -61,7 +61,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): _LOGGER.error("No sensors added") return False - hass.loop.create_task(async_add_devices(sensors, True)) + yield from async_add_devices(sensors, True) return True @@ -82,7 +82,7 @@ class SensorTemplate(Entity): @callback def template_sensor_state_listener(entity, old_state, new_state): """Called when the target device changes state.""" - hass.loop.create_task(self.async_update_ha_state(True)) + hass.async_add_job(self.async_update_ha_state, True) async_track_state_change( hass, entity_ids, template_sensor_state_listener) diff --git a/homeassistant/components/sensor/time_date.py b/homeassistant/components/sensor/time_date.py index 79127c60063..04bd8a5aa0f 100644 --- a/homeassistant/components/sensor/time_date.py +++ b/homeassistant/components/sensor/time_date.py @@ -46,7 +46,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): for variable in config[CONF_DISPLAY_OPTIONS]: devices.append(TimeDateSensor(variable)) - hass.loop.create_task(async_add_devices(devices, True)) + yield from async_add_devices(devices, True) return True diff --git a/homeassistant/components/sensor/worldclock.py b/homeassistant/components/sensor/worldclock.py index 22bedfcd21c..bce4895e408 100644 --- a/homeassistant/components/sensor/worldclock.py +++ b/homeassistant/components/sensor/worldclock.py @@ -35,8 +35,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): name = config.get(CONF_NAME) time_zone = dt_util.get_time_zone(config.get(CONF_TIME_ZONE)) - hass.loop.create_task(async_add_devices( - [WorldClockSensor(time_zone, name)], True)) + yield from async_add_devices([WorldClockSensor(time_zone, name)], True) return True diff --git a/homeassistant/components/switch/template.py b/homeassistant/components/switch/template.py index 5383caf7f54..83bd1aff9b1 100644 --- a/homeassistant/components/switch/template.py +++ b/homeassistant/components/switch/template.py @@ -70,7 +70,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): _LOGGER.error("No switches added") return False - hass.loop.create_task(async_add_devices(switches, True)) + yield from async_add_devices(switches, True) return True @@ -92,7 +92,7 @@ class SwitchTemplate(SwitchDevice): @callback def template_switch_state_listener(entity, old_state, new_state): """Called when the target device changes state.""" - hass.loop.create_task(self.async_update_ha_state(True)) + hass.async_add_job(self.async_update_ha_state, True) async_track_state_change( hass, entity_ids, template_switch_state_listener) From 1ad14b8227eae4001ddbdf0d4763aa28b8a50492 Mon Sep 17 00:00:00 2001 From: Brent Hughes Date: Sat, 5 Nov 2016 19:08:54 -0500 Subject: [PATCH 010/127] Updated Emulated_Hue to send request info as variables to scripts (#4010) * Updated Emulated_Hue to send request info as variables to scripts * Updated tests to not use the old mqtt * Updated test to actualy use and validate the script variables * Fixed the removal of time in a recent merge * fixed test to not use a timer --- homeassistant/components/emulated_hue.py | 20 ++++++++- tests/components/test_emulated_hue.py | 56 +++++++++++++++++++++++- 2 files changed, 73 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/emulated_hue.py b/homeassistant/components/emulated_hue.py index 187ee0de603..667a73c6a16 100644 --- a/homeassistant/components/emulated_hue.py +++ b/homeassistant/components/emulated_hue.py @@ -18,7 +18,7 @@ from homeassistant import util, core from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, SERVICE_TURN_OFF, SERVICE_TURN_ON, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, - STATE_ON, HTTP_BAD_REQUEST, HTTP_NOT_FOUND, + STATE_ON, STATE_OFF, HTTP_BAD_REQUEST, HTTP_NOT_FOUND, ) from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_SUPPORTED_FEATURES, SUPPORT_BRIGHTNESS @@ -317,7 +317,16 @@ class HueLightsView(HomeAssistantView): # Construct what we need to send to the service data = {ATTR_ENTITY_ID: entity_id} - if brightness is not None: + # If the requested entity is a script add some variables + if entity.domain.lower() == "script": + data['variables'] = { + 'requested_state': STATE_ON if result else STATE_OFF + } + + if brightness is not None: + data['variables']['requested_level'] = brightness + + elif brightness is not None: data[ATTR_BRIGHTNESS] = brightness if entity.domain.lower() in config.off_maps_to_on_domains: @@ -401,6 +410,13 @@ def parse_hue_api_put_light_body(request_json, entity): report_brightness = True result = (brightness > 0) + elif entity.domain.lower() == "script": + # Convert 0-255 to 0-100 + level = int(request_json[HUE_API_STATE_BRI]) / 255 * 100 + + brightness = round(level) + report_brightness = True + result = True return (result, brightness) if report_brightness else (result, None) diff --git a/tests/components/test_emulated_hue.py b/tests/components/test_emulated_hue.py index edb2181c813..db46fcdbcc3 100755 --- a/tests/components/test_emulated_hue.py +++ b/tests/components/test_emulated_hue.py @@ -6,7 +6,7 @@ import requests from homeassistant import bootstrap, const, core import homeassistant.components as core_components -from homeassistant.components import emulated_hue, http, light +from homeassistant.components import emulated_hue, http, light, script from homeassistant.const import STATE_ON, STATE_OFF from homeassistant.components.emulated_hue import ( HUE_API_STATE_ON, HUE_API_STATE_BRI) @@ -129,6 +129,22 @@ class TestEmulatedHueExposedByDefault(unittest.TestCase): ] }) + bootstrap.setup_component(cls.hass, script.DOMAIN, { + 'script': { + 'set_kitchen_light': { + 'sequence': [ + { + 'service_template': "light.turn_{{ requested_state }}", + 'data_template': { + 'entity_id': 'light.kitchen_lights', + 'brightness': "{{ requested_level }}" + } + } + ] + } + } + }) + start_hass_instance(cls.hass) # Kitchen light is explicitly excluded from being exposed @@ -139,6 +155,14 @@ class TestEmulatedHueExposedByDefault(unittest.TestCase): kitchen_light_entity.entity_id, kitchen_light_entity.state, attributes=attrs) + # Expose the script + script_entity = cls.hass.states.get('script.set_kitchen_light') + attrs = dict(script_entity.attributes) + attrs[emulated_hue.ATTR_EMULATED_HUE] = True + cls.hass.states.set( + script_entity.entity_id, script_entity.state, attributes=attrs + ) + @classmethod def tearDownClass(cls): """Stop the class.""" @@ -157,6 +181,7 @@ class TestEmulatedHueExposedByDefault(unittest.TestCase): # Make sure the lights we added to the config are there self.assertTrue('light.ceiling_lights' in result_json) self.assertTrue('light.bed_light' in result_json) + self.assertTrue('script.set_kitchen_light' in result_json) self.assertTrue('light.kitchen_lights' not in result_json) def test_get_light_state(self): @@ -231,6 +256,35 @@ class TestEmulatedHueExposedByDefault(unittest.TestCase): 'light.kitchen_light', True) self.assertEqual(kitchen_result.status_code, 404) + def test_put_light_state_script(self): + """Test the setting of script variables.""" + # Turn the kitchen light off first + self.hass.services.call( + light.DOMAIN, const.SERVICE_TURN_OFF, + {const.ATTR_ENTITY_ID: 'light.kitchen_lights'}, + blocking=True) + + # Emulated hue converts 0-100% to 0-255. + level = 23 + brightness = round(level * 255 / 100) + + script_result = self.perform_put_light_state( + 'script.set_kitchen_light', True, brightness) + + script_result_json = script_result.json() + + self.assertEqual(script_result.status_code, 200) + self.assertEqual(len(script_result_json), 2) + + # Wait until script is complete before continuing + self.hass.block_till_done() + + kitchen_light = self.hass.states.get('light.kitchen_lights') + self.assertEqual(kitchen_light.state, 'on') + self.assertEqual( + kitchen_light.attributes[light.ATTR_BRIGHTNESS], + level) + def test_put_with_form_urlencoded_content_type(self): """Test the form with urlencoded content.""" # Needed for Alexa From c980d26aae66f4a7ab3ffa4f87630f9608c3a81d Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 6 Nov 2016 13:00:41 +0100 Subject: [PATCH 011/127] Upgrade distro to 1.0.1 (#4239) --- homeassistant/components/updater.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/updater.py b/homeassistant/components/updater.py index f597c8d1c52..d4eb5d2211c 100644 --- a/homeassistant/components/updater.py +++ b/homeassistant/components/updater.py @@ -30,7 +30,7 @@ ATTR_RELEASE_NOTES = 'release_notes' UPDATER_UUID_FILE = '.uuid' CONF_REPORTING = 'reporting' -REQUIREMENTS = ['distro==1.0.0'] +REQUIREMENTS = ['distro==1.0.1'] CONFIG_SCHEMA = vol.Schema({DOMAIN: { vol.Optional(CONF_REPORTING, default=True): cv.boolean diff --git a/requirements_all.txt b/requirements_all.txt index af5519f1d15..01b297c8c03 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -73,7 +73,7 @@ concord232==0.14 directpy==0.1 # homeassistant.components.updater -distro==1.0.0 +distro==1.0.1 # homeassistant.components.notify.xmpp dnspython3==1.15.0 From bab8d574fef1806472b2b52339f57b9b5d4e3cad Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Sun, 6 Nov 2016 10:27:15 -0500 Subject: [PATCH 012/127] Wink Thermostat support and NoneType error fixes (#4175) --- .../components/binary_sensor/wink.py | 18 +- homeassistant/components/climate/wink.py | 331 ++++++++++++++++++ homeassistant/components/light/wink.py | 7 +- homeassistant/components/sensor/wink.py | 36 +- homeassistant/components/wink.py | 16 +- requirements_all.txt | 2 +- 6 files changed, 377 insertions(+), 33 deletions(-) create mode 100644 homeassistant/components/climate/wink.py diff --git a/homeassistant/components/binary_sensor/wink.py b/homeassistant/components/binary_sensor/wink.py index 9813ca213e6..e4448d96e36 100644 --- a/homeassistant/components/binary_sensor/wink.py +++ b/homeassistant/components/binary_sensor/wink.py @@ -5,6 +5,7 @@ For more details about this platform, please refer to the documentation at at https://home-assistant.io/components/binary_sensor.wink/ """ import json +import logging from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.components.sensor.wink import WinkDevice @@ -53,12 +54,17 @@ class WinkBinarySensorDevice(WinkDevice, BinarySensorDevice, Entity): self.capability = self.wink.capability() def _pubnub_update(self, message, channel): - if 'data' in message: - json_data = json.dumps(message.get('data')) - else: - json_data = message - self.wink.pubnub_update(json.loads(json_data)) - self.update_ha_state() + try: + if 'data' in message: + json_data = json.dumps(message.get('data')) + else: + json_data = message + self.wink.pubnub_update(json.loads(json_data)) + self.update_ha_state() + except (AttributeError, KeyError): + error = "Pubnub returned invalid json for " + self.name + logging.getLogger(__name__).error(error) + self.update_ha_state(True) @property def is_on(self): diff --git a/homeassistant/components/climate/wink.py b/homeassistant/components/climate/wink.py new file mode 100644 index 00000000000..a0094a7c290 --- /dev/null +++ b/homeassistant/components/climate/wink.py @@ -0,0 +1,331 @@ +""" +Support for Wink thermostats. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/climate.wink/ +""" +from homeassistant.components.wink import WinkDevice +from homeassistant.components.climate import ( + STATE_AUTO, STATE_COOL, STATE_HEAT, ClimateDevice, + ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, + ATTR_TEMPERATURE, + ATTR_CURRENT_HUMIDITY) +from homeassistant.const import ( + TEMP_CELSIUS, STATE_ON, + STATE_OFF, STATE_UNKNOWN) +from homeassistant.loader import get_component + +DEPENDENCIES = ['wink'] + +STATE_AUX = 'aux' +STATE_ECO = 'eco' + +ATTR_EXTERNAL_TEMPERATURE = "external_temperature" +ATTR_SMART_TEMPERATURE = "smart_temperature" +ATTR_ECO_TARGET = "eco_target" +ATTR_OCCUPIED = "occupied" + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the Wink thermostat.""" + import pywink + temp_unit = hass.config.units.temperature_unit + add_devices(WinkThermostat(thermostat, temp_unit) + for thermostat in pywink.get_thermostats()) + + +# pylint: disable=abstract-method,too-many-public-methods, too-many-branches +class WinkThermostat(WinkDevice, ClimateDevice): + """Representation of a Wink thermostat.""" + + def __init__(self, wink, temp_unit): + """Initialize the Wink device.""" + super().__init__(wink) + wink = get_component('wink') + self._config_temp_unit = temp_unit + + @property + def temperature_unit(self): + """Return the unit of measurement.""" + # The Wink API always returns temp in Celsius + return TEMP_CELSIUS + + @property + def device_state_attributes(self): + """Return the optional state attributes.""" + data = {} + target_temp_high = self.target_temperature_high + target_temp_low = self.target_temperature_low + if target_temp_high is not None: + data[ATTR_TARGET_TEMP_HIGH] = self._convert_for_display( + self.target_temperature_high) + if target_temp_low is not None: + data[ATTR_TARGET_TEMP_LOW] = self._convert_for_display( + self.target_temperature_low) + + if self.external_temperature: + data[ATTR_EXTERNAL_TEMPERATURE] = self._convert_for_display( + self.external_temperature) + + if self.smart_temperature: + data[ATTR_SMART_TEMPERATURE] = self.smart_temperature + + if self.occupied: + data[ATTR_OCCUPIED] = self.occupied + + if self.eco_target: + data[ATTR_ECO_TARGET] = self.eco_target + + current_humidity = self.current_humidity + if current_humidity is not None: + data[ATTR_CURRENT_HUMIDITY] = current_humidity + + return data + + @property + def current_temperature(self): + """Return the current temperature.""" + return self.wink.current_temperature() + + @property + def current_humidity(self): + """Return the current humidity.""" + if self.wink.current_humidity() is not None: + # The API states humidity will be a float 0-1 + # the only example API response with humidity listed show an int + # This will address both possibilities + if self.wink.current_humidity() < 1: + return self.wink.current_humidity() * 100 + else: + return self.wink.current_humidity() + + @property + def external_temperature(self): + """Return the current external temperature.""" + return self.wink.current_external_temperature() + + @property + def smart_temperature(self): + """Return the current average temp of all remote sensor.""" + return self.wink.current_smart_temperature() + + @property + def eco_target(self): + """Return status of eco target (Is the termostat in eco mode).""" + return self.wink.eco_target() + + @property + def occupied(self): + """Return status of if the thermostat has detected occupancy.""" + return self.wink.occupied() + + @property + def current_operation(self): + """Return current operation ie. heat, cool, idle.""" + if not self.wink.is_on(): + current_op = STATE_OFF + elif self.wink.current_hvac_mode() == 'cool_only': + current_op = STATE_COOL + elif self.wink.current_hvac_mode() == 'heat_only': + current_op = STATE_HEAT + elif self.wink.current_hvac_mode() == 'aux': + current_op = STATE_HEAT + elif self.wink.current_hvac_mode() == 'auto': + current_op = STATE_AUTO + elif self.wink.current_hvac_mode() == 'eco': + current_op = STATE_ECO + else: + current_op = STATE_UNKNOWN + return current_op + + @property + def target_humidity(self): + """Return the humidity we try to reach.""" + target_hum = None + if self.wink.current_humidifier_mode() == 'on': + if self.wink.current_humidifier_set_point() is not None: + target_hum = self.wink.current_humidifier_set_point() * 100 + elif self.wink.current_dehumidifier_mode() == 'on': + if self.wink.current_dehumidifier_set_point() is not None: + target_hum = self.wink.current_dehumidifier_set_point() * 100 + else: + target_hum = None + return target_hum + + @property + def target_temperature(self): + """Return the temperature we try to reach.""" + if self.current_operation != STATE_AUTO and not self.is_away_mode_on: + if self.current_operation == STATE_COOL: + return self.wink.current_max_set_point() + elif self.current_operation == STATE_HEAT: + return self.wink.current_min_set_point() + else: + return None + else: + return None + + @property + def target_temperature_low(self): + """Return the lower bound temperature we try to reach.""" + if self.current_operation == STATE_AUTO: + return self.wink.current_min_set_point() + return None + + @property + def target_temperature_high(self): + """Return the higher bound temperature we try to reach.""" + if self.current_operation == STATE_AUTO: + return self.wink.current_max_set_point() + return None + + @property + def is_away_mode_on(self): + """Return if away mode is on.""" + return self.wink.away() + + @property + def is_aux_heat_on(self): + """Return true if aux heater.""" + if self.wink.current_hvac_mode() == 'aux' and self.wink.is_on(): + return True + elif self.wink.current_hvac_mode() == 'aux' and not self.wink.is_on(): + return False + else: + return None + + def set_temperature(self, **kwargs): + """Set new target temperature.""" + target_temp = kwargs.get(ATTR_TEMPERATURE) + target_temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW) + target_temp_high = kwargs.get(ATTR_TARGET_TEMP_HIGH) + if target_temp is not None: + if self.current_operation == STATE_COOL: + target_temp_high = target_temp + if self.current_operation == STATE_HEAT: + target_temp_low = target_temp + if target_temp_low is not None: + target_temp_low = target_temp_low + if target_temp_high is not None: + target_temp_high = target_temp_high + self.wink.set_temperature(target_temp_low, target_temp_high) + + def set_operation_mode(self, operation_mode): + """Set operation mode.""" + if operation_mode == STATE_HEAT: + self.wink.set_operation_mode('heat_only') + elif operation_mode == STATE_COOL: + self.wink.set_operation_mode('cool_only') + elif operation_mode == STATE_AUTO: + self.wink.set_operation_mode('auto') + elif operation_mode == STATE_OFF: + self.wink.set_operation_mode('off') + elif operation_mode == STATE_AUX: + self.wink.set_operation_mode('aux') + elif operation_mode == STATE_ECO: + self.wink.set_operation_mode('eco') + + @property + def operation_list(self): + """List of available operation modes.""" + op_list = ['off'] + modes = self.wink.hvac_modes() + if 'cool_only' in modes: + op_list.append(STATE_COOL) + if 'heat_only' in modes or 'aux' in modes: + op_list.append(STATE_HEAT) + if 'auto' in modes: + op_list.append(STATE_AUTO) + if 'eco' in modes: + op_list.append(STATE_ECO) + return op_list + + def turn_away_mode_on(self): + """Turn away on.""" + self.wink.set_away_mode() + + def turn_away_mode_off(self): + """Turn away off.""" + self.wink.set_away_mode(False) + + @property + def current_fan_mode(self): + """Return whether the fan is on.""" + if self.wink.current_fan_mode() == 'on': + return STATE_ON + elif self.wink.current_fan_mode() == 'auto': + return STATE_AUTO + else: + # No Fan available so disable slider + return None + + @property + def fan_list(self): + """List of available fan modes.""" + if self.wink.has_fan(): + return self.wink.fan_modes() + return None + + def set_fan_mode(self, fan): + """Turn fan on/off.""" + self.wink.set_fan_mode(fan.lower()) + + def turn_aux_heat_on(self): + """Turn auxillary heater on.""" + self.set_operation_mode(STATE_AUX) + + def turn_aux_heat_off(self): + """Turn auxillary heater off.""" + self.set_operation_mode(STATE_AUTO) + + @property + def min_temp(self): + """Return the minimum temperature.""" + minimum = 7 # Default minimum + min_min = self.wink.min_min_set_point() + min_max = self.wink.min_max_set_point() + return_value = minimum + if self.current_operation == STATE_HEAT: + if min_min: + return_value = min_min + else: + return_value = minimum + elif self.current_operation == STATE_COOL: + if min_max: + return_value = min_max + else: + return_value = minimum + elif self.current_operation == STATE_AUTO: + if min_min and min_max: + return_value = min(min_min, min_max) + else: + return_value = minimum + else: + return_value = minimum + return return_value + + @property + def max_temp(self): + """Return the maximum temperature.""" + maximum = 35 # Default maximum + max_min = self.wink.max_min_set_point() + max_max = self.wink.max_max_set_point() + return_value = maximum + if self.current_operation == STATE_HEAT: + if max_min: + return_value = max_min + else: + return_value = maximum + elif self.current_operation == STATE_COOL: + if max_max: + return_value = max_max + else: + return_value = maximum + elif self.current_operation == STATE_AUTO: + if max_min and max_max: + return_value = min(max_min, max_max) + else: + return_value = maximum + else: + return_value = maximum + return return_value diff --git a/homeassistant/components/light/wink.py b/homeassistant/components/light/wink.py index d117b66df79..1d292a53419 100644 --- a/homeassistant/components/light/wink.py +++ b/homeassistant/components/light/wink.py @@ -41,7 +41,10 @@ class WinkLight(WinkDevice, Light): @property def brightness(self): """Return the brightness of the light.""" - return int(self.wink.brightness() * 255) + if self.wink.brightness() is not None: + return int(self.wink.brightness() * 255) + else: + return None @property def rgb_color(self): @@ -52,6 +55,8 @@ class WinkLight(WinkDevice, Light): hue = self.wink.color_hue() saturation = self.wink.color_saturation() value = int(self.wink.brightness() * 255) + if hue is None or saturation is None or value is None: + return None rgb = colorsys.hsv_to_rgb(hue, saturation, value) r_value = int(round(rgb[0])) g_value = int(round(rgb[1])) diff --git a/homeassistant/components/sensor/wink.py b/homeassistant/components/sensor/wink.py index 569beba4866..455b3b03290 100644 --- a/homeassistant/components/sensor/wink.py +++ b/homeassistant/components/sensor/wink.py @@ -6,8 +6,7 @@ at https://home-assistant.io/components/sensor.wink/ """ import logging -from homeassistant.const import ( - STATE_CLOSED, STATE_OPEN, TEMP_CELSIUS) +from homeassistant.const import TEMP_CELSIUS from homeassistant.helpers.entity import Entity from homeassistant.components.wink import WinkDevice from homeassistant.loader import get_component @@ -51,27 +50,29 @@ class WinkSensorDevice(WinkDevice, Entity): @property def state(self): """Return the state.""" + state = None if self.capability == 'humidity': - return round(self.wink.humidity_percentage()) + if self.wink.humidity_percentage() is not None: + state = round(self.wink.humidity_percentage()) elif self.capability == 'temperature': - return round(self.wink.temperature_float(), 1) + if self.wink.temperature_float() is not None: + state = round(self.wink.temperature_float(), 1) elif self.capability == 'balance': - return round(self.wink.balance() / 100, 2) + if self.wink.balance() is not None: + state = round(self.wink.balance() / 100, 2) elif self.capability == 'proximity': - return self.wink.proximity_float() + if self.wink.proximity_float() is not None: + state = self.wink.proximity_float() else: - return STATE_OPEN if self.is_open else STATE_CLOSED + # A sensor should never get here, anything that does + # will require an update to python-wink + logging.getLogger(__name__).error("Please report this as an issue") + state = None + return state @property def available(self): - """ - True if connection == True. - - Always return true for Wink porkfolio due to - bug in API. - """ - if self.capability == 'balance': - return True + """True if connection == True.""" return self.wink.available @property @@ -79,11 +80,6 @@ class WinkSensorDevice(WinkDevice, Entity): """Return the unit of measurement of this entity, if any.""" return self._unit_of_measurement - @property - def is_open(self): - """Return true if door is open.""" - return self.wink.state() - class WinkEggMinder(WinkDevice, Entity): """Representation of a Wink Egg Minder.""" diff --git a/homeassistant/components/wink.py b/homeassistant/components/wink.py index 22c6c992838..c678024f6a3 100644 --- a/homeassistant/components/wink.py +++ b/homeassistant/components/wink.py @@ -15,7 +15,7 @@ from homeassistant.const import CONF_ACCESS_TOKEN, ATTR_BATTERY_LEVEL, \ from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['python-wink==0.9.0', 'pubnub==3.8.2'] +REQUIREMENTS = ['python-wink==0.10.0', 'pubnub==3.8.2'] _LOGGER = logging.getLogger(__name__) @@ -50,7 +50,7 @@ CONFIG_SCHEMA = vol.Schema({ }, extra=vol.ALLOW_EXTRA) WINK_COMPONENTS = [ - 'binary_sensor', 'sensor', 'light', 'switch', 'lock', 'cover' + 'binary_sensor', 'sensor', 'light', 'switch', 'lock', 'cover', 'climate' ] @@ -108,8 +108,13 @@ class WinkDevice(Entity): error=self._pubnub_error) def _pubnub_update(self, message, channel): - self.wink.pubnub_update(json.loads(message)) - self.update_ha_state() + try: + self.wink.pubnub_update(json.loads(message)) + self.update_ha_state() + except (AttributeError, KeyError): + error = "Pubnub returned invalid json for " + self.name + logging.getLogger(__name__).error(error) + self.update_ha_state(True) def _pubnub_error(self, message): _LOGGER.error("Error on pubnub update for " + self.wink.name()) @@ -149,4 +154,5 @@ class WinkDevice(Entity): @property def _battery_level(self): """Return the battery level.""" - return self.wink.battery_level * 100 + if self.wink.battery_level is not None: + return self.wink.battery_level * 100 diff --git a/requirements_all.txt b/requirements_all.txt index 01b297c8c03..21573c114a3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -443,7 +443,7 @@ python-telegram-bot==5.2.0 python-twitch==1.3.0 # homeassistant.components.wink -python-wink==0.9.0 +python-wink==0.10.0 # homeassistant.components.keyboard # pyuserinput==0.1.11 From 5d862e426e80d37fddbc58df033b7c987563aea2 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 6 Nov 2016 16:36:03 +0100 Subject: [PATCH 013/127] Upgrade fuzzywuzzy to 0.14.0 (#4240) --- homeassistant/components/conversation.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/conversation.py b/homeassistant/components/conversation.py index b688e3d7082..e589ff9155c 100644 --- a/homeassistant/components/conversation.py +++ b/homeassistant/components/conversation.py @@ -15,7 +15,7 @@ from homeassistant.const import ( ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['fuzzywuzzy==0.12.0'] +REQUIREMENTS = ['fuzzywuzzy==0.14.0'] ATTR_TEXT = 'text' diff --git a/requirements_all.txt b/requirements_all.txt index 21573c114a3..5ca789e9250 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -113,7 +113,7 @@ fixerio==0.1.1 freesms==0.1.0 # homeassistant.components.conversation -fuzzywuzzy==0.12.0 +fuzzywuzzy==0.14.0 # homeassistant.components.device_tracker.bluetooth_le_tracker # gattlib==0.20150805 From c35e5c9997fb6606588bb43452de3415972c3fbe Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 6 Nov 2016 16:36:16 +0100 Subject: [PATCH 014/127] Upgrade astral to 1.3 (#4238) --- homeassistant/components/sun.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sun.py b/homeassistant/components/sun.py index 20bebe79f9e..09cd4307443 100644 --- a/homeassistant/components/sun.py +++ b/homeassistant/components/sun.py @@ -18,7 +18,7 @@ import homeassistant.helpers.config_validation as cv import homeassistant.util as util -REQUIREMENTS = ['astral==1.2'] +REQUIREMENTS = ['astral==1.3'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 5ca789e9250..5ba502e1ce6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -37,7 +37,7 @@ apcaccess==0.0.4 apns2==0.1.1 # homeassistant.components.sun -astral==1.2 +astral==1.3 # homeassistant.components.sensor.linux_battery batinfo==0.3 From 7774a03a55e21b5c8743015dcd0dbb40af816482 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 6 Nov 2016 07:53:54 -0800 Subject: [PATCH 015/127] Move Honeywell I/O out of event loop (#4244) --- homeassistant/components/climate/honeywell.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/climate/honeywell.py b/homeassistant/components/climate/honeywell.py index 540a9a941d1..0d31cdd1387 100644 --- a/homeassistant/components/climate/honeywell.py +++ b/homeassistant/components/climate/honeywell.py @@ -223,7 +223,6 @@ class HoneywellUSThermostat(ClimateDevice): @property def current_temperature(self): """Return the current temperature.""" - self._device.refresh() return self._device.current_temperature @property @@ -274,3 +273,7 @@ class HoneywellUSThermostat(ClimateDevice): """Set the system mode (Cool, Heat, etc).""" if hasattr(self._device, ATTR_SYSTEM_MODE): self._device.system_mode = operation_mode + + def update(self): + """Update the state.""" + self._device.refresh() From 98f41d6b84b15d00231aebcc21de88e2d07d5a4b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 6 Nov 2016 08:43:32 -0800 Subject: [PATCH 016/127] Tweak block_till_done (#4245) --- homeassistant/core.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/core.py b/homeassistant/core.py index 5f340bdd941..18cb6e2e2c1 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -252,8 +252,9 @@ class HomeAssistant(object): """Block till all pending work is done.""" while True: # Wait for the pending tasks are down - if len(self._pending_tasks) > 0: - yield from asyncio.wait(self._pending_tasks, loop=self.loop) + pending = list(self._pending_tasks) + if len(pending) > 0: + yield from asyncio.wait(pending, loop=self.loop) # Verify the loop is empty ret = yield from self.loop.run_in_executor(None, self._loop_empty) From d4e8b831a03f4ce90f6f33194117ddcc645bde48 Mon Sep 17 00:00:00 2001 From: Antoine Bertin Date: Sun, 6 Nov 2016 18:09:01 +0100 Subject: [PATCH 017/127] Add mqtt_template light component (#4233) * Add mqtt_template component * Docstring copy paste party on overriden methods * pep8 E501 :star2: * Add missing docstrings on unittests --- .../components/light/mqtt_template.py | 252 ++++++++++++ tests/components/light/test_mqtt_template.py | 373 ++++++++++++++++++ 2 files changed, 625 insertions(+) create mode 100755 homeassistant/components/light/mqtt_template.py create mode 100755 tests/components/light/test_mqtt_template.py diff --git a/homeassistant/components/light/mqtt_template.py b/homeassistant/components/light/mqtt_template.py new file mode 100755 index 00000000000..4566a383645 --- /dev/null +++ b/homeassistant/components/light/mqtt_template.py @@ -0,0 +1,252 @@ +""" +Support for MQTT Template lights. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/light.mqtt_template/ +""" + +import logging +import voluptuous as vol + +import homeassistant.components.mqtt as mqtt +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, ATTR_RGB_COLOR, ATTR_TRANSITION, PLATFORM_SCHEMA, + ATTR_FLASH, SUPPORT_BRIGHTNESS, SUPPORT_FLASH, + SUPPORT_RGB_COLOR, SUPPORT_TRANSITION, Light) +from homeassistant.const import CONF_NAME, CONF_OPTIMISTIC, STATE_ON, STATE_OFF +from homeassistant.components.mqtt import ( + CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN) +import homeassistant.helpers.config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'mqtt_template' + +DEPENDENCIES = ['mqtt'] + +DEFAULT_NAME = 'MQTT Template Light' +DEFAULT_OPTIMISTIC = False + +CONF_COMMAND_ON_TEMPLATE = 'command_on_template' +CONF_COMMAND_OFF_TEMPLATE = 'command_off_template' +CONF_STATE_TEMPLATE = 'state_template' +CONF_BRIGHTNESS_TEMPLATE = 'brightness_template' +CONF_RED_TEMPLATE = 'red_template' +CONF_GREEN_TEMPLATE = 'green_template' +CONF_BLUE_TEMPLATE = 'blue_template' + +SUPPORT_MQTT_TEMPLATE = (SUPPORT_BRIGHTNESS | SUPPORT_FLASH | + SUPPORT_RGB_COLOR | SUPPORT_TRANSITION) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Required(CONF_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Required(CONF_COMMAND_ON_TEMPLATE): cv.template, + vol.Required(CONF_COMMAND_OFF_TEMPLATE): cv.template, + vol.Optional(CONF_STATE_TEMPLATE): cv.template, + vol.Optional(CONF_BRIGHTNESS_TEMPLATE): cv.template, + vol.Optional(CONF_RED_TEMPLATE): cv.template, + vol.Optional(CONF_GREEN_TEMPLATE): cv.template, + vol.Optional(CONF_BLUE_TEMPLATE): cv.template, + vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, + vol.Optional(CONF_QOS, default=mqtt.DEFAULT_QOS): + vol.All(vol.Coerce(int), vol.In([0, 1, 2])), + vol.Optional(CONF_RETAIN, default=mqtt.DEFAULT_RETAIN): cv.boolean +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup a MQTT Template light.""" + add_devices([MqttTemplate( + hass, + config.get(CONF_NAME), + { + key: config.get(key) for key in ( + CONF_STATE_TOPIC, + CONF_COMMAND_TOPIC + ) + }, + { + key: config.get(key) for key in ( + CONF_COMMAND_ON_TEMPLATE, + CONF_COMMAND_OFF_TEMPLATE, + CONF_STATE_TEMPLATE, + CONF_BRIGHTNESS_TEMPLATE, + CONF_RED_TEMPLATE, + CONF_GREEN_TEMPLATE, + CONF_BLUE_TEMPLATE + ) + }, + config.get(CONF_OPTIMISTIC), + config.get(CONF_QOS), + config.get(CONF_RETAIN) + )]) + + +class MqttTemplate(Light): + """Representation of a MQTT Template light.""" + + def __init__(self, hass, name, topics, templates, optimistic, qos, retain): + """Initialize MQTT Template light.""" + self._hass = hass + self._name = name + self._topics = topics + self._templates = templates + for tpl in self._templates.values(): + if tpl is not None: + tpl.hass = hass + self._optimistic = optimistic or topics[CONF_STATE_TOPIC] is None \ + or templates[CONF_STATE_TEMPLATE] is None + self._qos = qos + self._retain = retain + + # features + self._state = False + if self._templates[CONF_BRIGHTNESS_TEMPLATE] is not None: + self._brightness = 255 + else: + self._brightness = None + + if (self._templates[CONF_RED_TEMPLATE] is not None and + self._templates[CONF_GREEN_TEMPLATE] is not None and + self._templates[CONF_BLUE_TEMPLATE] is not None): + self._rgb = [0, 0, 0] + else: + self._rgb = None + + def state_received(topic, payload, qos): + """A new MQTT message has been received.""" + # read state + state = self._templates[CONF_STATE_TEMPLATE].\ + render_with_possible_json_value(payload) + if state == STATE_ON: + self._state = True + elif state == STATE_OFF: + self._state = False + else: + _LOGGER.warning('Invalid state value received') + + # read brightness + if self._brightness is not None: + try: + self._brightness = int( + self._templates[CONF_BRIGHTNESS_TEMPLATE]. + render_with_possible_json_value(payload) + ) + except ValueError: + _LOGGER.warning('Invalid brightness value received') + + # read color + if self._rgb is not None: + try: + self._rgb[0] = int( + self._templates[CONF_RED_TEMPLATE]. + render_with_possible_json_value(payload)) + self._rgb[1] = int( + self._templates[CONF_GREEN_TEMPLATE]. + render_with_possible_json_value(payload)) + self._rgb[2] = int( + self._templates[CONF_BLUE_TEMPLATE]. + render_with_possible_json_value(payload)) + except ValueError: + _LOGGER.warning('Invalid color value received') + + self.update_ha_state() + + if self._topics[CONF_STATE_TOPIC] is not None: + mqtt.subscribe(self._hass, self._topics[CONF_STATE_TOPIC], + state_received, self._qos) + + @property + def brightness(self): + """Return the brightness of this light between 0..255.""" + return self._brightness + + @property + def rgb_color(self): + """Return the RGB color value [int, int, int].""" + return self._rgb + + @property + def should_poll(self): + """Return True if entity has to be polled for state. + + False if entity pushes its state to HA. + """ + return False + + @property + def name(self): + """Return the name of the entity.""" + return self._name + + @property + def is_on(self): + """Return True if entity is on.""" + return self._state + + @property + def assumed_state(self): + """Return True if unable to access real state of the entity.""" + return self._optimistic + + def turn_on(self, **kwargs): + """Turn the entity on.""" + # state + values = {'state': True} + if self._optimistic: + self._state = True + + # brightness + if ATTR_BRIGHTNESS in kwargs: + values['brightness'] = int(kwargs[ATTR_BRIGHTNESS]) + + if self._optimistic: + self._brightness = kwargs[ATTR_BRIGHTNESS] + + # color + if ATTR_RGB_COLOR in kwargs: + values['red'] = kwargs[ATTR_RGB_COLOR][0] + values['green'] = kwargs[ATTR_RGB_COLOR][1] + values['blue'] = kwargs[ATTR_RGB_COLOR][2] + + if self._optimistic: + self._rgb = kwargs[ATTR_RGB_COLOR] + + # flash + if ATTR_FLASH in kwargs: + values['flash'] = kwargs.get(ATTR_FLASH) + + # transition + if ATTR_TRANSITION in kwargs: + values['transition'] = kwargs[ATTR_TRANSITION] + + mqtt.publish( + self._hass, self._topics[CONF_COMMAND_TOPIC], + self._templates[CONF_COMMAND_ON_TEMPLATE].render(**values), + self._qos, self._retain + ) + + if self._optimistic: + self.update_ha_state() + + def turn_off(self, **kwargs): + """Turn the entity off.""" + # state + values = {'state': False} + if self._optimistic: + self._state = False + + # transition + if ATTR_TRANSITION in kwargs: + values['transition'] = kwargs[ATTR_TRANSITION] + + mqtt.publish( + self._hass, self._topics[CONF_COMMAND_TOPIC], + self._templates[CONF_COMMAND_OFF_TEMPLATE].render(**values), + self._qos, self._retain + ) + + if self._optimistic: + self.update_ha_state() diff --git a/tests/components/light/test_mqtt_template.py b/tests/components/light/test_mqtt_template.py new file mode 100755 index 00000000000..94cd2a0a19f --- /dev/null +++ b/tests/components/light/test_mqtt_template.py @@ -0,0 +1,373 @@ +"""The tests for the MQTT Template light platform. + +Configuration example with all features: + +light: + platform: mqtt_template + name: mqtt_template_light_1 + state_topic: 'home/rgb1' + command_topic: 'home/rgb1/set' + command_on_template: > + on,{{ brightness|d }},{{ red|d }}-{{ green|d }}-{{ blue|d }} + command_off_template: 'off' + state_template: '{{ value.split(",")[0] }}' + brightness_template: '{{ value.split(",")[1] }}' + red_template: '{{ value.split(",")[2].split("-")[0] }}' + green_template: '{{ value.split(",")[2].split("-")[1] }}' + blue_template: '{{ value.split(",")[2].split("-")[2] }}' + +If your light doesn't support brightness feature, omit `brightness_template`. + +If your light doesn't support rgb feature, omit `(red|green|blue)_template`. +""" +import unittest + +from homeassistant.bootstrap import setup_component +from homeassistant.const import STATE_ON, STATE_OFF, ATTR_ASSUMED_STATE +import homeassistant.components.light as light +from tests.common import ( + get_test_home_assistant, mock_mqtt_component, fire_mqtt_message, + assert_setup_component) + + +class TestLightMQTTTemplate(unittest.TestCase): + """Test the MQTT Template light.""" + + def setUp(self): # pylint: disable=invalid-name + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() + self.mock_publish = mock_mqtt_component(self.hass) + + def tearDown(self): # pylint: disable=invalid-name + """Stop everything that was started.""" + self.hass.stop() + + def test_setup_fails(self): \ + # pylint: disable=invalid-name + """Test that setup fails with missing required configuration items.""" + self.hass.config.components = ['mqtt'] + with assert_setup_component(0): + assert setup_component(self.hass, light.DOMAIN, { + light.DOMAIN: { + 'platform': 'mqtt_template', + 'name': 'test', + } + }) + self.assertIsNone(self.hass.states.get('light.test')) + + def test_state_change_via_topic(self): \ + # pylint: disable=invalid-name + """Test state change via topic.""" + self.hass.config.components = ['mqtt'] + with assert_setup_component(1): + assert setup_component(self.hass, light.DOMAIN, { + light.DOMAIN: { + 'platform': 'mqtt_template', + 'name': 'test', + 'state_topic': 'test_light_rgb', + 'command_topic': 'test_light_rgb/set', + 'command_on_template': 'on,' + '{{ brightness|d }},' + '{{ red|d }}-' + '{{ green|d }}-' + '{{ blue|d }}', + 'command_off_template': 'off', + 'state_template': '{{ value.split(",")[0] }}' + } + }) + + state = self.hass.states.get('light.test') + self.assertEqual(STATE_OFF, state.state) + self.assertIsNone(state.attributes.get('rgb_color')) + self.assertIsNone(state.attributes.get('brightness')) + self.assertIsNone(state.attributes.get(ATTR_ASSUMED_STATE)) + + fire_mqtt_message(self.hass, 'test_light_rgb', 'on') + self.hass.block_till_done() + + state = self.hass.states.get('light.test') + self.assertEqual(STATE_ON, state.state) + self.assertIsNone(state.attributes.get('rgb_color')) + self.assertIsNone(state.attributes.get('brightness')) + + def test_state_brightness_color_change_via_topic(self): \ + # pylint: disable=invalid-name + """Test state, brightness and color change via topic.""" + self.hass.config.components = ['mqtt'] + with assert_setup_component(1): + assert setup_component(self.hass, light.DOMAIN, { + light.DOMAIN: { + 'platform': 'mqtt_template', + 'name': 'test', + 'state_topic': 'test_light_rgb', + 'command_topic': 'test_light_rgb/set', + 'command_on_template': 'on,' + '{{ brightness|d }},' + '{{ red|d }}-' + '{{ green|d }}-' + '{{ blue|d }}', + 'command_off_template': 'off', + 'state_template': '{{ value.split(",")[0] }}', + 'brightness_template': '{{ value.split(",")[1] }}', + 'red_template': '{{ value.split(",")[2].' + 'split("-")[0] }}', + 'green_template': '{{ value.split(",")[2].' + 'split("-")[1] }}', + 'blue_template': '{{ value.split(",")[2].' + 'split("-")[2] }}' + } + }) + + state = self.hass.states.get('light.test') + self.assertEqual(STATE_OFF, state.state) + self.assertIsNone(state.attributes.get('rgb_color')) + self.assertIsNone(state.attributes.get('brightness')) + self.assertIsNone(state.attributes.get(ATTR_ASSUMED_STATE)) + + # turn on the light, full white + fire_mqtt_message(self.hass, 'test_light_rgb', 'on,255,255-255-255') + self.hass.block_till_done() + + state = self.hass.states.get('light.test') + self.assertEqual(STATE_ON, state.state) + self.assertEqual([255, 255, 255], state.attributes.get('rgb_color')) + self.assertEqual(255, state.attributes.get('brightness')) + + # turn the light off + fire_mqtt_message(self.hass, 'test_light_rgb', 'off') + self.hass.block_till_done() + + state = self.hass.states.get('light.test') + self.assertEqual(STATE_OFF, state.state) + + # lower the brightness + fire_mqtt_message(self.hass, 'test_light_rgb', 'on,100') + self.hass.block_till_done() + + light_state = self.hass.states.get('light.test') + self.hass.block_till_done() + self.assertEqual(100, light_state.attributes['brightness']) + + # change the color + fire_mqtt_message(self.hass, 'test_light_rgb', 'on,,41-42-43') + self.hass.block_till_done() + + light_state = self.hass.states.get('light.test') + self.assertEqual([41, 42, 43], light_state.attributes.get('rgb_color')) + + def test_optimistic(self): \ + # pylint: disable=invalid-name + """Test optimistic mode.""" + self.hass.config.components = ['mqtt'] + with assert_setup_component(1): + assert setup_component(self.hass, light.DOMAIN, { + light.DOMAIN: { + 'platform': 'mqtt_template', + 'name': 'test', + 'command_topic': 'test_light_rgb/set', + 'command_on_template': 'on,' + '{{ brightness|d }},' + '{{ red|d }}-' + '{{ green|d }}-' + '{{ blue|d }}', + 'command_off_template': 'off', + 'qos': 2 + } + }) + + state = self.hass.states.get('light.test') + self.assertEqual(STATE_OFF, state.state) + self.assertTrue(state.attributes.get(ATTR_ASSUMED_STATE)) + + # turn on the light + light.turn_on(self.hass, 'light.test') + self.hass.block_till_done() + + self.assertEqual(('test_light_rgb/set', 'on,,--', 2, False), + self.mock_publish.mock_calls[-1][1]) + state = self.hass.states.get('light.test') + self.assertEqual(STATE_ON, state.state) + + # turn the light off + light.turn_off(self.hass, 'light.test') + self.hass.block_till_done() + + self.assertEqual(('test_light_rgb/set', 'off', 2, False), + self.mock_publish.mock_calls[-1][1]) + state = self.hass.states.get('light.test') + self.assertEqual(STATE_OFF, state.state) + + # turn on the light with brightness and color + light.turn_on(self.hass, 'light.test', brightness=50, + rgb_color=[75, 75, 75]) + self.hass.block_till_done() + + self.assertEqual('test_light_rgb/set', + self.mock_publish.mock_calls[-1][1][0]) + self.assertEqual(2, self.mock_publish.mock_calls[-1][1][2]) + self.assertEqual(False, self.mock_publish.mock_calls[-1][1][3]) + + # check the payload + payload = self.mock_publish.mock_calls[-1][1][1] + self.assertEqual('on,50,75-75-75', payload) + + # check the state + state = self.hass.states.get('light.test') + self.assertEqual(STATE_ON, state.state) + self.assertEqual((75, 75, 75), state.attributes['rgb_color']) + self.assertEqual(50, state.attributes['brightness']) + + def test_flash(self): \ + # pylint: disable=invalid-name + """Test flash.""" + self.hass.config.components = ['mqtt'] + with assert_setup_component(1): + assert setup_component(self.hass, light.DOMAIN, { + light.DOMAIN: { + 'platform': 'mqtt_template', + 'name': 'test', + 'command_topic': 'test_light_rgb/set', + 'command_on_template': 'on,{{ flash }}', + 'command_off_template': 'off', + 'qos': 0 + } + }) + + state = self.hass.states.get('light.test') + self.assertEqual(STATE_OFF, state.state) + + # short flash + light.turn_on(self.hass, 'light.test', flash='short') + self.hass.block_till_done() + + self.assertEqual('test_light_rgb/set', + self.mock_publish.mock_calls[-1][1][0]) + self.assertEqual(0, self.mock_publish.mock_calls[-1][1][2]) + self.assertEqual(False, self.mock_publish.mock_calls[-1][1][3]) + + # check the payload + payload = self.mock_publish.mock_calls[-1][1][1] + self.assertEqual('on,short', payload) + + # long flash + light.turn_on(self.hass, 'light.test', flash='long') + self.hass.block_till_done() + + self.assertEqual('test_light_rgb/set', + self.mock_publish.mock_calls[-1][1][0]) + self.assertEqual(0, self.mock_publish.mock_calls[-1][1][2]) + self.assertEqual(False, self.mock_publish.mock_calls[-1][1][3]) + + # check the payload + payload = self.mock_publish.mock_calls[-1][1][1] + self.assertEqual('on,long', payload) + + def test_transition(self): + """Test for transition time being sent when included.""" + self.hass.config.components = ['mqtt'] + with assert_setup_component(1): + assert setup_component(self.hass, light.DOMAIN, { + light.DOMAIN: { + 'platform': 'mqtt_template', + 'name': 'test', + 'command_topic': 'test_light_rgb/set', + 'command_on_template': 'on,{{ transition }}', + 'command_off_template': 'off,{{ transition|d }}' + } + }) + + state = self.hass.states.get('light.test') + self.assertEqual(STATE_OFF, state.state) + + # transition on + light.turn_on(self.hass, 'light.test', transition=10) + self.hass.block_till_done() + + self.assertEqual('test_light_rgb/set', + self.mock_publish.mock_calls[-1][1][0]) + self.assertEqual(0, self.mock_publish.mock_calls[-1][1][2]) + self.assertEqual(False, self.mock_publish.mock_calls[-1][1][3]) + + # check the payload + payload = self.mock_publish.mock_calls[-1][1][1] + self.assertEqual('on,10', payload) + + # transition off + light.turn_off(self.hass, 'light.test', transition=4) + self.hass.block_till_done() + + self.assertEqual('test_light_rgb/set', + self.mock_publish.mock_calls[-1][1][0]) + self.assertEqual(0, self.mock_publish.mock_calls[-1][1][2]) + self.assertEqual(False, self.mock_publish.mock_calls[-1][1][3]) + + # check the payload + payload = self.mock_publish.mock_calls[-1][1][1] + self.assertEqual('off,4', payload) + + def test_invalid_values(self): \ + # pylint: disable=invalid-name + """Test that invalid values are ignored.""" + self.hass.config.components = ['mqtt'] + with assert_setup_component(1): + assert setup_component(self.hass, light.DOMAIN, { + light.DOMAIN: { + 'platform': 'mqtt_template', + 'name': 'test', + 'state_topic': 'test_light_rgb', + 'command_topic': 'test_light_rgb/set', + 'command_on_template': 'on,' + '{{ brightness|d }},' + '{{ red|d }}-' + '{{ green|d }}-' + '{{ blue|d }}', + 'command_off_template': 'off', + 'state_template': '{{ value.split(",")[0] }}', + 'brightness_template': '{{ value.split(",")[1] }}', + 'red_template': '{{ value.split(",")[2].' + 'split("-")[0] }}', + 'green_template': '{{ value.split(",")[2].' + 'split("-")[1] }}', + 'blue_template': '{{ value.split(",")[2].' + 'split("-")[2] }}' + } + }) + + state = self.hass.states.get('light.test') + self.assertEqual(STATE_OFF, state.state) + self.assertIsNone(state.attributes.get('rgb_color')) + self.assertIsNone(state.attributes.get('brightness')) + self.assertIsNone(state.attributes.get(ATTR_ASSUMED_STATE)) + + # turn on the light, full white + fire_mqtt_message(self.hass, 'test_light_rgb', 'on,255,255-255-255') + self.hass.block_till_done() + + state = self.hass.states.get('light.test') + self.assertEqual(STATE_ON, state.state) + self.assertEqual(255, state.attributes.get('brightness')) + self.assertEqual([255, 255, 255], state.attributes.get('rgb_color')) + + # bad state value + fire_mqtt_message(self.hass, 'test_light_rgb', 'offf') + self.hass.block_till_done() + + # state should not have changed + state = self.hass.states.get('light.test') + self.assertEqual(STATE_ON, state.state) + + # bad brightness values + fire_mqtt_message(self.hass, 'test_light_rgb', 'on,off,255-255-255') + self.hass.block_till_done() + + # brightness should not have changed + state = self.hass.states.get('light.test') + self.assertEqual(255, state.attributes.get('brightness')) + + # bad color values + fire_mqtt_message(self.hass, 'test_light_rgb', 'on,255,a-b-c') + self.hass.block_till_done() + + # color should not have changed + state = self.hass.states.get('light.test') + self.assertEqual([255, 255, 255], state.attributes.get('rgb_color')) From a343c2040419056b8db3754b14c6a6a7c583dcd8 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 6 Nov 2016 09:26:40 -0800 Subject: [PATCH 018/127] Async gather wait (#4247) * Fix config validation for input_*, script * Allow scheduling coroutines * Validate entity ids when entity ids set by platform * Async: gather -> wait * Script/Group: use async_add_job instead of create_task --- homeassistant/components/__init__.py | 2 +- .../components/automation/__init__.py | 12 ++++--- homeassistant/components/camera/synology.py | 2 +- homeassistant/components/group.py | 11 +++--- homeassistant/components/input_boolean.py | 24 ++++++++----- homeassistant/components/input_select.py | 24 +++++++------ homeassistant/components/input_slider.py | 27 +++++++------- homeassistant/components/script.py | 2 +- homeassistant/components/sensor/yr.py | 3 +- homeassistant/components/zone.py | 2 +- homeassistant/core.py | 4 ++- homeassistant/helpers/entity_component.py | 35 +++++++++++++------ homeassistant/helpers/script.py | 2 +- 13 files changed, 90 insertions(+), 60 deletions(-) diff --git a/homeassistant/components/__init__.py b/homeassistant/components/__init__.py index 81450c726f1..a4f18250d17 100644 --- a/homeassistant/components/__init__.py +++ b/homeassistant/components/__init__.py @@ -119,7 +119,7 @@ def async_setup(hass, config): tasks.append(hass.services.async_call( domain, service.service, data, blocking)) - yield from asyncio.gather(*tasks, loop=hass.loop) + yield from asyncio.wait(tasks, loop=hass.loop) hass.services.async_register( ha.DOMAIN, SERVICE_TURN_OFF, handle_turn_service) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index e88caab6824..c49bc15ff64 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -165,7 +165,7 @@ def async_setup(hass, config): for entity in component.async_extract_from_service(service_call): tasks.append(entity.async_trigger( service_call.data.get(ATTR_VARIABLES), True)) - yield from asyncio.gather(*tasks, loop=hass.loop) + yield from asyncio.wait(tasks, loop=hass.loop) @asyncio.coroutine def turn_onoff_service_handler(service_call): @@ -174,7 +174,7 @@ def async_setup(hass, config): method = 'async_{}'.format(service_call.service) for entity in component.async_extract_from_service(service_call): tasks.append(getattr(entity, method)()) - yield from asyncio.gather(*tasks, loop=hass.loop) + yield from asyncio.wait(tasks, loop=hass.loop) @asyncio.coroutine def toggle_service_handler(service_call): @@ -185,7 +185,7 @@ def async_setup(hass, config): tasks.append(entity.async_turn_off()) else: tasks.append(entity.async_turn_on()) - yield from asyncio.gather(*tasks, loop=hass.loop) + yield from asyncio.wait(tasks, loop=hass.loop) @asyncio.coroutine def reload_service_handler(service_call): @@ -348,8 +348,10 @@ def _async_process_config(hass, config, component): tasks.append(entity.async_enable()) entities.append(entity) - yield from asyncio.gather(*tasks, loop=hass.loop) - yield from component.async_add_entities(entities) + if tasks: + yield from asyncio.wait(tasks, loop=hass.loop) + if entities: + yield from component.async_add_entities(entities) return len(entities) > 0 diff --git a/homeassistant/components/camera/synology.py b/homeassistant/components/camera/synology.py index 4ca63c16d7d..cd0387a9b3b 100644 --- a/homeassistant/components/camera/synology.py +++ b/homeassistant/components/camera/synology.py @@ -144,7 +144,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): tasks.append(device.async_read_sid()) devices.append(device) - yield from asyncio.gather(*tasks, loop=hass.loop) + yield from asyncio.wait(tasks, loop=hass.loop) hass.loop.create_task(async_add_devices(devices)) diff --git a/homeassistant/components/group.py b/homeassistant/components/group.py index f57c56f17db..cbdfef85942 100644 --- a/homeassistant/components/group.py +++ b/homeassistant/components/group.py @@ -184,7 +184,7 @@ def async_setup(hass, config): tasks = [group.async_set_visible(visible) for group in component.async_extract_from_service(service, expand_group=False)] - yield from asyncio.gather(*tasks, loop=hass.loop) + yield from asyncio.wait(tasks, loop=hass.loop) hass.services.async_register( DOMAIN, SERVICE_SET_VISIBILITY, visibility_service_handler, @@ -207,13 +207,14 @@ def _async_process_config(hass, config, component): icon = conf.get(CONF_ICON) view = conf.get(CONF_VIEW) - # This order is important as groups get a number based on creation - # order. + # Don't create tasks and await them all. The order is important as + # groups get a number based on creation order. group = yield from Group.async_create_group( hass, name, entity_ids, icon=icon, view=view, object_id=object_id) groups.append(group) - yield from component.async_add_entities(groups) + if groups: + yield from component.async_add_entities(groups) class Group(Entity): @@ -394,7 +395,7 @@ class Group(Entity): This method must be run in the event loop. """ self._async_update_group_state(new_state) - self.hass.loop.create_task(self.async_update_ha_state()) + self.hass.async_add_job(self.async_update_ha_state()) @property def _tracking_states(self): diff --git a/homeassistant/components/input_boolean.py b/homeassistant/components/input_boolean.py index 1a510fbf6ec..579e6bade3e 100644 --- a/homeassistant/components/input_boolean.py +++ b/homeassistant/components/input_boolean.py @@ -23,17 +23,23 @@ ENTITY_ID_FORMAT = DOMAIN + '.{}' _LOGGER = logging.getLogger(__name__) CONF_INITIAL = 'initial' +DEFAULT_INITIAL = False SERVICE_SCHEMA = vol.Schema({ vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, }) -CONFIG_SCHEMA = vol.Schema({DOMAIN: { - cv.slug: vol.Any({ - vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_INITIAL, default=False): cv.boolean, - vol.Optional(CONF_ICON): cv.icon, - }, None)}}, extra=vol.ALLOW_EXTRA) +DEFAULT_CONFIG = {CONF_INITIAL: DEFAULT_INITIAL} + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + cv.slug: vol.Any({ + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_INITIAL, default=DEFAULT_INITIAL): cv.boolean, + vol.Optional(CONF_ICON): cv.icon, + }, None) + }) +}, extra=vol.ALLOW_EXTRA) def is_on(hass, entity_id): @@ -65,10 +71,10 @@ def async_setup(hass, config): for object_id, cfg in config[DOMAIN].items(): if not cfg: - cfg = {} + cfg = DEFAULT_CONFIG name = cfg.get(CONF_NAME) - state = cfg.get(CONF_INITIAL, False) + state = cfg.get(CONF_INITIAL) icon = cfg.get(CONF_ICON) entities.append(InputBoolean(object_id, name, state, icon)) @@ -89,7 +95,7 @@ def async_setup(hass, config): attr = 'async_toggle' tasks = [getattr(input_b, attr)() for input_b in target_inputs] - yield from asyncio.gather(*tasks, loop=hass.loop) + yield from asyncio.wait(tasks, loop=hass.loop) hass.services.async_register( DOMAIN, SERVICE_TURN_OFF, async_handler_service, schema=SERVICE_SCHEMA) diff --git a/homeassistant/components/input_select.py b/homeassistant/components/input_select.py index 61385c46cd6..9b563d271f5 100644 --- a/homeassistant/components/input_select.py +++ b/homeassistant/components/input_select.py @@ -55,14 +55,16 @@ def _cv_input_select(cfg): return cfg -CONFIG_SCHEMA = vol.Schema({DOMAIN: { - cv.slug: vol.All({ - vol.Optional(CONF_NAME): cv.string, - vol.Required(CONF_OPTIONS): vol.All(cv.ensure_list, vol.Length(min=1), - [cv.string]), - vol.Optional(CONF_INITIAL): cv.string, - vol.Optional(CONF_ICON): cv.icon, - }, _cv_input_select)}}, required=True, extra=vol.ALLOW_EXTRA) +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + cv.slug: vol.All({ + vol.Optional(CONF_NAME): cv.string, + vol.Required(CONF_OPTIONS): + vol.All(cv.ensure_list, vol.Length(min=1), [cv.string]), + vol.Optional(CONF_INITIAL): cv.string, + vol.Optional(CONF_ICON): cv.icon, + }, _cv_input_select)}) +}, required=True, extra=vol.ALLOW_EXTRA) def select_option(hass, entity_id, option): @@ -111,7 +113,7 @@ def async_setup(hass, config): tasks = [input_select.async_select_option(call.data[ATTR_OPTION]) for input_select in target_inputs] - yield from asyncio.gather(*tasks, loop=hass.loop) + yield from asyncio.wait(tasks, loop=hass.loop) hass.services.async_register( DOMAIN, SERVICE_SELECT_OPTION, async_select_option_service, @@ -124,7 +126,7 @@ def async_setup(hass, config): tasks = [input_select.async_offset_index(1) for input_select in target_inputs] - yield from asyncio.gather(*tasks, loop=hass.loop) + yield from asyncio.wait(tasks, loop=hass.loop) hass.services.async_register( DOMAIN, SERVICE_SELECT_NEXT, async_select_next_service, @@ -137,7 +139,7 @@ def async_setup(hass, config): tasks = [input_select.async_offset_index(-1) for input_select in target_inputs] - yield from asyncio.gather(*tasks, loop=hass.loop) + yield from asyncio.wait(tasks, loop=hass.loop) hass.services.async_register( DOMAIN, SERVICE_SELECT_PREVIOUS, async_select_previous_service, diff --git a/homeassistant/components/input_slider.py b/homeassistant/components/input_slider.py index 2a942829517..e425cd4e3e4 100644 --- a/homeassistant/components/input_slider.py +++ b/homeassistant/components/input_slider.py @@ -51,17 +51,20 @@ def _cv_input_slider(cfg): cfg[CONF_INITIAL] = state return cfg -CONFIG_SCHEMA = vol.Schema({DOMAIN: { - cv.slug: vol.All({ - vol.Optional(CONF_NAME): cv.string, - vol.Required(CONF_MIN): vol.Coerce(float), - vol.Required(CONF_MAX): vol.Coerce(float), - vol.Optional(CONF_INITIAL): vol.Coerce(float), - vol.Optional(CONF_STEP, default=1): vol.All(vol.Coerce(float), - vol.Range(min=1e-3)), - vol.Optional(CONF_ICON): cv.icon, - vol.Optional(ATTR_UNIT_OF_MEASUREMENT): cv.string - }, _cv_input_slider)}}, required=True, extra=vol.ALLOW_EXTRA) +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + cv.slug: vol.All({ + vol.Optional(CONF_NAME): cv.string, + vol.Required(CONF_MIN): vol.Coerce(float), + vol.Required(CONF_MAX): vol.Coerce(float), + vol.Optional(CONF_INITIAL): vol.Coerce(float), + vol.Optional(CONF_STEP, default=1): vol.All(vol.Coerce(float), + vol.Range(min=1e-3)), + vol.Optional(CONF_ICON): cv.icon, + vol.Optional(ATTR_UNIT_OF_MEASUREMENT): cv.string + }, _cv_input_slider) + }) +}, required=True, extra=vol.ALLOW_EXTRA) def select_value(hass, entity_id, value): @@ -101,7 +104,7 @@ def async_setup(hass, config): tasks = [input_slider.async_select_value(call.data[ATTR_VALUE]) for input_slider in target_inputs] - yield from asyncio.gather(*tasks, loop=hass.loop) + yield from asyncio.wait(tasks, loop=hass.loop) hass.services.async_register( DOMAIN, SERVICE_SELECT_VALUE, async_select_value_service, diff --git a/homeassistant/components/script.py b/homeassistant/components/script.py index bc66e562e0a..8c521c33856 100644 --- a/homeassistant/components/script.py +++ b/homeassistant/components/script.py @@ -40,7 +40,7 @@ _SCRIPT_ENTRY_SCHEMA = vol.Schema({ }) CONFIG_SCHEMA = vol.Schema({ - vol.Required(DOMAIN): {cv.slug: _SCRIPT_ENTRY_SCHEMA} + vol.Required(DOMAIN): vol.Schema({cv.slug: _SCRIPT_ENTRY_SCHEMA}) }, extra=vol.ALLOW_EXTRA) SCRIPT_SERVICE_SCHEMA = vol.Schema(dict) diff --git a/homeassistant/components/sensor/yr.py b/homeassistant/components/sensor/yr.py index 51616062475..7bede98213e 100644 --- a/homeassistant/components/sensor/yr.py +++ b/homeassistant/components/sensor/yr.py @@ -218,4 +218,5 @@ class YrData(object): dev._state = new_state tasks.append(dev.async_update_ha_state()) - yield from asyncio.gather(*tasks, loop=self.hass.loop) + if tasks: + yield from asyncio.wait(tasks, loop=self.hass.loop) diff --git a/homeassistant/components/zone.py b/homeassistant/components/zone.py index 2514dfc0083..d106c9f9cbe 100644 --- a/homeassistant/components/zone.py +++ b/homeassistant/components/zone.py @@ -110,7 +110,7 @@ def async_setup(hass, config): zone.entity_id = ENTITY_ID_HOME tasks.append(zone.async_update_ha_state()) - yield from asyncio.gather(*tasks, loop=hass.loop) + yield from asyncio.wait(tasks, loop=hass.loop) return True diff --git a/homeassistant/core.py b/homeassistant/core.py index 18cb6e2e2c1..3cb2a98db32 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -207,7 +207,9 @@ class HomeAssistant(object): """ task = None - if is_callback(target): + if asyncio.iscoroutine(target): + task = self.loop.create_task(target) + elif is_callback(target): self.loop.call_soon(target, *args) elif asyncio.iscoroutinefunction(target): task = self.loop.create_task(target(*args)) diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index a648de1d650..bfa51b1e281 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -7,7 +7,7 @@ from homeassistant.bootstrap import ( from homeassistant.const import ( ATTR_ENTITY_ID, CONF_SCAN_INTERVAL, CONF_ENTITY_NAMESPACE, DEVICE_DEFAULT_NAME) -from homeassistant.core import callback +from homeassistant.core import callback, valid_entity_id from homeassistant.exceptions import HomeAssistantError from homeassistant.loader import get_component from homeassistant.helpers import config_per_platform, discovery @@ -71,14 +71,15 @@ class EntityComponent(object): for p_type, p_config in config_per_platform(config, self.domain): tasks.append(self._async_setup_platform(p_type, p_config)) - yield from asyncio.gather(*tasks, loop=self.hass.loop) + if tasks: + yield from asyncio.wait(tasks, loop=self.hass.loop) # Generic discovery listener for loading platform dynamically # Refer to: homeassistant.components.discovery.load_platform() @callback def component_platform_discovered(platform, info): """Callback to load a platform.""" - self.hass.loop.create_task( + self.hass.async_add_job( self._async_setup_platform(platform, {}, info)) discovery.async_listen_platform( @@ -190,6 +191,14 @@ class EntityComponent(object): self.entity_id_format, object_id, self.entities.keys()) + # Make sure it is valid in case an entity set the value themselves + if entity.entity_id in self.entities: + raise HomeAssistantError( + 'Entity id already exists: {}'.format(entity.entity_id)) + elif not valid_entity_id(entity.entity_id): + raise HomeAssistantError( + 'Invalid entity id: {}'.format(entity.entity_id)) + self.entities[entity.entity_id] = entity yield from entity.async_update_ha_state() @@ -229,7 +238,8 @@ class EntityComponent(object): tasks = [platform.async_reset() for platform in self._platforms.values()] - yield from asyncio.gather(*tasks, loop=self.hass.loop) + if tasks: + yield from asyncio.wait(tasks, loop=self.hass.loop) self._platforms = { 'core': self._platforms['core'] @@ -293,14 +303,14 @@ class EntityPlatform(object): This method must be run in the event loop. """ + # handle empty list from component/platform + if not new_entities: + return + tasks = [self._async_process_entity(entity, update_before_add) for entity in new_entities] - # handle empty list from component/platform - if not tasks: - return - - yield from asyncio.gather(*tasks, loop=self.component.hass.loop) + yield from asyncio.wait(tasks, loop=self.component.hass.loop) yield from self.component.async_update_group() if self._async_unsub_polling is not None or \ @@ -327,9 +337,12 @@ class EntityPlatform(object): This method must be run in the event loop. """ + if not self.platform_entities: + return + tasks = [entity.async_remove() for entity in self.platform_entities] - yield from asyncio.gather(*tasks, loop=self.component.hass.loop) + yield from asyncio.wait(tasks, loop=self.component.hass.loop) if self._async_unsub_polling is not None: self._async_unsub_polling() @@ -343,6 +356,6 @@ class EntityPlatform(object): """ for entity in self.platform_entities: if entity.should_poll: - self.component.hass.loop.create_task( + self.component.hass.async_add_job( entity.async_update_ha_state(True) ) diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 09e9e15a5dc..4d6a2b01df7 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -85,7 +85,7 @@ class Script(): def script_delay(now): """Called after delay is done.""" self._async_unsub_delay_listener = None - self.hass.loop.create_task(self.async_run(variables)) + self.hass.async_add_job(self.async_run(variables)) delay = action[CONF_DELAY] From 1ed2f8ae91a13f9b7e16a430cd9f3aa188d88f1a Mon Sep 17 00:00:00 2001 From: Nicolas Graziano Date: Sun, 6 Nov 2016 18:27:55 +0100 Subject: [PATCH 019/127] Update braviarc to 0.3.6 (#4246) Add HDMI sources. Sources ordered. --- homeassistant/components/media_player/braviatv.py | 4 ++-- requirements_all.txt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/media_player/braviatv.py b/homeassistant/components/media_player/braviatv.py index d1c60bf2ec1..dbcecd6097a 100644 --- a/homeassistant/components/media_player/braviatv.py +++ b/homeassistant/components/media_player/braviatv.py @@ -21,8 +21,8 @@ from homeassistant.const import (CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON) import homeassistant.helpers.config_validation as cv REQUIREMENTS = [ - 'https://github.com/aparraga/braviarc/archive/0.3.5.zip' - '#braviarc==0.3.5'] + 'https://github.com/aparraga/braviarc/archive/0.3.6.zip' + '#braviarc==0.3.6'] BRAVIA_CONFIG_FILE = 'bravia.conf' diff --git a/requirements_all.txt b/requirements_all.txt index 5ba502e1ce6..e98d4e8e030 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -172,7 +172,7 @@ https://github.com/TheRealLink/pythinkingcleaner/archive/v0.0.2.zip#pythinkingcl https://github.com/Xorso/pyalarmdotcom/archive/0.1.1.zip#pyalarmdotcom==0.1.1 # homeassistant.components.media_player.braviatv -https://github.com/aparraga/braviarc/archive/0.3.5.zip#braviarc==0.3.5 +https://github.com/aparraga/braviarc/archive/0.3.6.zip#braviarc==0.3.6 # homeassistant.components.media_player.roku https://github.com/bah2830/python-roku/archive/3.1.2.zip#roku==3.1.2 From 0c5e07709145689504be37e66f5ae9f1170df824 Mon Sep 17 00:00:00 2001 From: Frantz Date: Sun, 6 Nov 2016 20:43:13 +0200 Subject: [PATCH 020/127] Updated netdisco to 0.7.6 (#4250) --- homeassistant/components/discovery.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery.py index 65a5af79bfb..780f2ab75d5 100644 --- a/homeassistant/components/discovery.py +++ b/homeassistant/components/discovery.py @@ -14,7 +14,7 @@ import voluptuous as vol from homeassistant.const import EVENT_HOMEASSISTANT_START from homeassistant.helpers.discovery import load_platform, discover -REQUIREMENTS = ['netdisco==0.7.5'] +REQUIREMENTS = ['netdisco==0.7.6'] DOMAIN = 'discovery' diff --git a/requirements_all.txt b/requirements_all.txt index e98d4e8e030..bbfb527ae8c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -278,7 +278,7 @@ mficlient==0.3.0 miflora==0.1.9 # homeassistant.components.discovery -netdisco==0.7.5 +netdisco==0.7.6 # homeassistant.components.sensor.neurio_energy neurio==0.2.10 From 734bd75fd33e48464c01f738b8072406dc0e84d8 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Sun, 6 Nov 2016 19:49:43 +0100 Subject: [PATCH 021/127] Fix mysensors overwriting gateway in GATEWAYS (#4013) GATEWAYS was a dict, so would overwrite item if key was the same. This would happen when using multiple MQTT gateways, since the device id is the same (`mqtt`). * Fix by changing GATEWAYS from dict into list. * Use hass data to store mysensors gateways instead of having GATEWAYS be a global. --- .../components/binary_sensor/mysensors.py | 6 ++++- homeassistant/components/climate/mysensors.py | 7 +++++- homeassistant/components/cover/mysensors.py | 7 +++++- homeassistant/components/light/mysensors.py | 6 ++++- homeassistant/components/mysensors.py | 22 +++++++++++++------ homeassistant/components/sensor/mysensors.py | 6 ++++- homeassistant/components/switch/mysensors.py | 6 ++++- 7 files changed, 47 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/binary_sensor/mysensors.py b/homeassistant/components/binary_sensor/mysensors.py index 789e188537e..e938f946457 100644 --- a/homeassistant/components/binary_sensor/mysensors.py +++ b/homeassistant/components/binary_sensor/mysensors.py @@ -22,7 +22,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None): if discovery_info is None: return - for gateway in mysensors.GATEWAYS.values(): + gateways = hass.data.get(mysensors.MYSENSORS_GATEWAYS) + if not gateways: + return + + for gateway in gateways: # Define the S_TYPES and V_TYPES that the platform should handle as # states. Map them in a dict of lists. pres = gateway.const.Presentation diff --git a/homeassistant/components/climate/mysensors.py b/homeassistant/components/climate/mysensors.py index 2a815625434..13a062a335e 100755 --- a/homeassistant/components/climate/mysensors.py +++ b/homeassistant/components/climate/mysensors.py @@ -24,7 +24,12 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the mysensors climate.""" if discovery_info is None: return - for gateway in mysensors.GATEWAYS.values(): + + gateways = hass.data.get(mysensors.MYSENSORS_GATEWAYS) + if not gateways: + return + + for gateway in gateways: if float(gateway.protocol_version) < 1.5: continue pres = gateway.const.Presentation diff --git a/homeassistant/components/cover/mysensors.py b/homeassistant/components/cover/mysensors.py index aa3d866bcd6..7dd63a8c745 100644 --- a/homeassistant/components/cover/mysensors.py +++ b/homeassistant/components/cover/mysensors.py @@ -18,7 +18,12 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the mysensors platform for covers.""" if discovery_info is None: return - for gateway in mysensors.GATEWAYS.values(): + + gateways = hass.data.get(mysensors.MYSENSORS_GATEWAYS) + if not gateways: + return + + for gateway in gateways: pres = gateway.const.Presentation set_req = gateway.const.SetReq map_sv_types = { diff --git a/homeassistant/components/light/mysensors.py b/homeassistant/components/light/mysensors.py index 3bd53ff9064..20da91682ad 100644 --- a/homeassistant/components/light/mysensors.py +++ b/homeassistant/components/light/mysensors.py @@ -31,7 +31,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None): if discovery_info is None: return - for gateway in mysensors.GATEWAYS.values(): + gateways = hass.data.get(mysensors.MYSENSORS_GATEWAYS) + if not gateways: + return + + for gateway in gateways: # Define the S_TYPES and V_TYPES that the platform should handle as # states. Map them in a dict of lists. pres = gateway.const.Presentation diff --git a/homeassistant/components/mysensors.py b/homeassistant/components/mysensors.py index b86bed57b82..b6778760b1a 100644 --- a/homeassistant/components/mysensors.py +++ b/homeassistant/components/mysensors.py @@ -38,7 +38,7 @@ DEFAULT_VERSION = 1.4 DEFAULT_BAUD_RATE = 115200 DEFAULT_TCP_PORT = 5003 DOMAIN = 'mysensors' -GATEWAYS = None +MYSENSORS_GATEWAYS = 'mysensors_gateways' MQTT_COMPONENT = 'mqtt' REQUIREMENTS = [ 'https://github.com/theolind/pymysensors/archive/' @@ -132,9 +132,15 @@ def setup(hass, config): return gateway + gateways = hass.data.get(MYSENSORS_GATEWAYS) + if gateways is not None: + _LOGGER.error( + '%s already exists in %s, will not setup %s component', + MYSENSORS_GATEWAYS, hass.data, DOMAIN) + return False + # Setup all devices from config - global GATEWAYS - GATEWAYS = {} + gateways = [] conf_gateways = config[DOMAIN][CONF_GATEWAYS] for index, gway in enumerate(conf_gateways): @@ -146,17 +152,19 @@ def setup(hass, config): tcp_port = gway.get(CONF_TCP_PORT) in_prefix = gway.get(CONF_TOPIC_IN_PREFIX) out_prefix = gway.get(CONF_TOPIC_OUT_PREFIX) - GATEWAYS[device] = setup_gateway( + ready_gateway = setup_gateway( device, persistence_file, baud_rate, tcp_port, in_prefix, out_prefix) - if GATEWAYS[device] is None: - GATEWAYS.pop(device) + if ready_gateway is not None: + gateways.append(ready_gateway) - if not GATEWAYS: + if not gateways: _LOGGER.error( 'No devices could be setup as gateways, check your configuration') return False + hass.data[MYSENSORS_GATEWAYS] = gateways + for component in ['sensor', 'switch', 'light', 'binary_sensor', 'climate', 'cover']: discovery.load_platform(hass, component, DOMAIN, {}, config) diff --git a/homeassistant/components/sensor/mysensors.py b/homeassistant/components/sensor/mysensors.py index ec39f0aea56..2742713cb24 100644 --- a/homeassistant/components/sensor/mysensors.py +++ b/homeassistant/components/sensor/mysensors.py @@ -20,7 +20,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None): if discovery_info is None: return - for gateway in mysensors.GATEWAYS.values(): + gateways = hass.data.get(mysensors.MYSENSORS_GATEWAYS) + if not gateways: + return + + for gateway in gateways: # Define the S_TYPES and V_TYPES that the platform should handle as # states. Map them in a dict of lists. pres = gateway.const.Presentation diff --git a/homeassistant/components/switch/mysensors.py b/homeassistant/components/switch/mysensors.py index f303a07686c..6c26a79f21a 100644 --- a/homeassistant/components/switch/mysensors.py +++ b/homeassistant/components/switch/mysensors.py @@ -34,7 +34,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None): if discovery_info is None: return - for gateway in mysensors.GATEWAYS.values(): + gateways = hass.data.get(mysensors.MYSENSORS_GATEWAYS) + if not gateways: + return + + for gateway in gateways: # Define the S_TYPES and V_TYPES that the platform should handle as # states. Map them in a dict of lists. pres = gateway.const.Presentation From 0aba227300fdf292b86cf3365e7736d522d5e11d Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Sun, 6 Nov 2016 19:04:57 -0500 Subject: [PATCH 022/127] Catch AttributeError (#4253) --- homeassistant/components/binary_sensor/wink.py | 2 +- homeassistant/components/wink.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/binary_sensor/wink.py b/homeassistant/components/binary_sensor/wink.py index e4448d96e36..ca88d8f6df8 100644 --- a/homeassistant/components/binary_sensor/wink.py +++ b/homeassistant/components/binary_sensor/wink.py @@ -61,7 +61,7 @@ class WinkBinarySensorDevice(WinkDevice, BinarySensorDevice, Entity): json_data = message self.wink.pubnub_update(json.loads(json_data)) self.update_ha_state() - except (AttributeError, KeyError): + except (AttributeError, KeyError, AttributeError): error = "Pubnub returned invalid json for " + self.name logging.getLogger(__name__).error(error) self.update_ha_state(True) diff --git a/homeassistant/components/wink.py b/homeassistant/components/wink.py index c678024f6a3..3d6afe0ef5a 100644 --- a/homeassistant/components/wink.py +++ b/homeassistant/components/wink.py @@ -111,7 +111,7 @@ class WinkDevice(Entity): try: self.wink.pubnub_update(json.loads(message)) self.update_ha_state() - except (AttributeError, KeyError): + except (AttributeError, KeyError, AttributeError): error = "Pubnub returned invalid json for " + self.name logging.getLogger(__name__).error(error) self.update_ha_state(True) From 95124c7ddb5ce9383e92dced3646cfe3bd97edd1 Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Sun, 6 Nov 2016 23:05:42 -0500 Subject: [PATCH 023/127] Revert "Catch AttributeError on Wink PubNub update" (#4263) --- homeassistant/components/binary_sensor/wink.py | 2 +- homeassistant/components/wink.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/binary_sensor/wink.py b/homeassistant/components/binary_sensor/wink.py index ca88d8f6df8..e4448d96e36 100644 --- a/homeassistant/components/binary_sensor/wink.py +++ b/homeassistant/components/binary_sensor/wink.py @@ -61,7 +61,7 @@ class WinkBinarySensorDevice(WinkDevice, BinarySensorDevice, Entity): json_data = message self.wink.pubnub_update(json.loads(json_data)) self.update_ha_state() - except (AttributeError, KeyError, AttributeError): + except (AttributeError, KeyError): error = "Pubnub returned invalid json for " + self.name logging.getLogger(__name__).error(error) self.update_ha_state(True) diff --git a/homeassistant/components/wink.py b/homeassistant/components/wink.py index 3d6afe0ef5a..c678024f6a3 100644 --- a/homeassistant/components/wink.py +++ b/homeassistant/components/wink.py @@ -111,7 +111,7 @@ class WinkDevice(Entity): try: self.wink.pubnub_update(json.loads(message)) self.update_ha_state() - except (AttributeError, KeyError, AttributeError): + except (AttributeError, KeyError): error = "Pubnub returned invalid json for " + self.name logging.getLogger(__name__).error(error) self.update_ha_state(True) From 880ef8af4868388721353f0b0e794f1aa258d424 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 6 Nov 2016 22:17:56 -0800 Subject: [PATCH 024/127] Remove broken disable verify ssl synology (#4269) --- homeassistant/components/camera/synology.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/camera/synology.py b/homeassistant/components/camera/synology.py index cd0387a9b3b..7edbc0005d8 100644 --- a/homeassistant/components/camera/synology.py +++ b/homeassistant/components/camera/synology.py @@ -57,6 +57,10 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ @asyncio.coroutine def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Setup a Synology IP Camera.""" + if not config.get(CONF_VERIFY_SSL): + _LOGGER.warning('SSL verification currently cannot be disabled. ' + 'See https://goo.gl/1h1119') + # Determine API to use for authentication syno_api_url = SYNO_API_URL.format( config.get(CONF_URL), WEBAPI_PATH, QUERY_CGI) @@ -72,7 +76,6 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): query_req = yield from hass.websession.get( syno_api_url, params=query_payload, - verify_ssl=config.get(CONF_VERIFY_SSL) ) except asyncio.TimeoutError: _LOGGER.error("Timeout on %s", syno_api_url) @@ -113,7 +116,6 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): camera_req = yield from hass.websession.get( syno_camera_url, params=camera_payload, - verify_ssl=config.get(CONF_VERIFY_SSL), cookies={'id': session_id} ) except asyncio.TimeoutError: @@ -165,7 +167,6 @@ def get_session_id(hass, username, password, login_url, valid_cert): auth_req = yield from hass.websession.get( login_url, params=auth_payload, - verify_ssl=valid_cert ) except asyncio.TimeoutError: _LOGGER.error("Timeout on %s", login_url) @@ -233,7 +234,6 @@ class SynologyCamera(Camera): response = yield from self.hass.websession.get( image_url, params=image_payload, - verify_ssl=self._valid_cert, cookies={'id': self._session_id} ) except asyncio.TimeoutError: @@ -263,7 +263,6 @@ class SynologyCamera(Camera): stream = yield from self.hass.websession.get( streaming_url, payload=streaming_payload, - verify_ssl=self._valid_cert, cookies={'id': self._session_id} ) except asyncio.TimeoutError: From 618a86a37c683fd36c6195249ae9615eac384461 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Mon, 7 Nov 2016 07:28:03 +0100 Subject: [PATCH 025/127] Set executor to 15 and help to reduce flooting async core with updates (#4252) * Set executor to 15 and help to reduce flooting async core with udpates * fix typing * if it a executor, wait * address comments from paulus * add space for style :) * fix spell * Update entity_component.py * Update entity_component.py --- homeassistant/core.py | 2 +- homeassistant/helpers/entity_component.py | 36 +++++++++++++++++++---- 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/homeassistant/core.py b/homeassistant/core.py index 3cb2a98db32..9a3211d8472 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -57,7 +57,7 @@ SERVICE_CALL_LIMIT = 10 # seconds ENTITY_ID_PATTERN = re.compile(r"^(\w+)\.(\w+)$") # Size of a executor pool -EXECUTOR_POOL_SIZE = 10 +EXECUTOR_POOL_SIZE = 15 _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index bfa51b1e281..cb021fff4a4 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -289,6 +289,7 @@ class EntityPlatform(object): self.entity_namespace = entity_namespace self.platform_entities = [] self._async_unsub_polling = None + self._process_updates = False def add_entities(self, new_entities, update_before_add=False): """Add entities for a single platform.""" @@ -348,14 +349,37 @@ class EntityPlatform(object): self._async_unsub_polling() self._async_unsub_polling = None - @callback + @asyncio.coroutine def _update_entity_states(self, now): """Update the states of all the polling entities. + To protect from flooding the executor, we will update async entities + in parallel and other entities sequential. + This method must be run in the event loop. """ - for entity in self.platform_entities: - if entity.should_poll: - self.component.hass.async_add_job( - entity.async_update_ha_state(True) - ) + if self._process_updates: + return + self._process_updates = True + + try: + tasks = [] + to_update = [] + + for entity in self.platform_entities: + if not entity.should_poll: + continue + + update_coro = entity.async_update_ha_state(True) + if hasattr(entity, 'async_update'): + tasks.append(update_coro) + else: + to_update.append(update_coro) + + for update_coro in to_update: + yield from update_coro + + if tasks: + yield from asyncio.wait(tasks, loop=self.component.hass.loop) + finally: + self._process_updates = False From 7d28d9d6b44d3e9e039d6e0e98363f4f9e947346 Mon Sep 17 00:00:00 2001 From: andyat Date: Sun, 6 Nov 2016 23:18:06 -0800 Subject: [PATCH 026/127] Fix setting temperature in Celsius on radiotherm CT50 (#4270) --- homeassistant/components/climate/radiotherm.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/climate/radiotherm.py b/homeassistant/components/climate/radiotherm.py index 5fa3f891aac..d06e148cfdd 100644 --- a/homeassistant/components/climate/radiotherm.py +++ b/homeassistant/components/climate/radiotherm.py @@ -135,9 +135,9 @@ class RadioThermostat(ClimateDevice): if temperature is None: return if self._current_operation == STATE_COOL: - self.device.t_cool = temperature + self.device.t_cool = round(temperature * 2.0) / 2.0 elif self._current_operation == STATE_HEAT: - self.device.t_heat = temperature + self.device.t_heat = round(temperature * 2.0) / 2.0 if self.hold_temp: self.device.hold = 1 else: @@ -159,6 +159,6 @@ class RadioThermostat(ClimateDevice): elif operation_mode == STATE_AUTO: self.device.tmode = 3 elif operation_mode == STATE_COOL: - self.device.t_cool = self._target_temperature + self.device.t_cool = round(self._target_temperature * 2.0) / 2.0 elif operation_mode == STATE_HEAT: - self.device.t_heat = self._target_temperature + self.device.t_heat = round(self._target_temperature * 2.0) / 2.0 From 67336a111b0cd157252765dddb035224ea78238d Mon Sep 17 00:00:00 2001 From: David-Leon Pohl Date: Mon, 7 Nov 2016 08:34:32 +0100 Subject: [PATCH 027/127] Hotfix #4272 (#4273) --- homeassistant/components/switch/pilight.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/switch/pilight.py b/homeassistant/components/switch/pilight.py index 1818372f1dc..f07d91ca9fb 100644 --- a/homeassistant/components/switch/pilight.py +++ b/homeassistant/components/switch/pilight.py @@ -37,8 +37,10 @@ SWITCHES_SCHEMA = vol.Schema({ vol.Required(CONF_ON_CODE): COMMAND_SCHEMA, vol.Required(CONF_OFF_CODE): COMMAND_SCHEMA, vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_OFF_CODE_RECIEVE): COMMAND_SCHEMA, - vol.Optional(CONF_ON_CODE_RECIEVE): COMMAND_SCHEMA, + vol.Optional(CONF_OFF_CODE_RECIEVE, default=[]): vol.All(cv.ensure_list, + [COMMAND_SCHEMA]), + vol.Optional(CONF_ON_CODE_RECIEVE, default=[]): vol.All(cv.ensure_list, + [COMMAND_SCHEMA]) }) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ From 8cc5fc1369a208c80122fd4b12598979e1d68102 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Mon, 7 Nov 2016 08:49:11 +0100 Subject: [PATCH 028/127] Upgrade psutil to 5.0.0 (#4275) --- homeassistant/components/sensor/systemmonitor.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/systemmonitor.py b/homeassistant/components/sensor/systemmonitor.py index 864a3352f8b..0963786b7d0 100755 --- a/homeassistant/components/sensor/systemmonitor.py +++ b/homeassistant/components/sensor/systemmonitor.py @@ -15,7 +15,7 @@ from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv import homeassistant.util.dt as dt_util -REQUIREMENTS = ['psutil==4.4.2'] +REQUIREMENTS = ['psutil==5.0.0'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index bbfb527ae8c..44b8ecf0be7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -315,7 +315,7 @@ pmsensor==0.3 proliphix==0.4.0 # homeassistant.components.sensor.systemmonitor -psutil==4.4.2 +psutil==5.0.0 # homeassistant.components.wink pubnub==3.8.2 From b4159c7dc9af2cfff3a855b5f634829b829b68b8 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Mon, 7 Nov 2016 08:49:25 +0100 Subject: [PATCH 029/127] Upgrade python-digitalocean to 1.10.1 (#4276) --- homeassistant/components/digital_ocean.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/digital_ocean.py b/homeassistant/components/digital_ocean.py index f976c17ae9d..dd8c7de99d4 100644 --- a/homeassistant/components/digital_ocean.py +++ b/homeassistant/components/digital_ocean.py @@ -13,7 +13,7 @@ from homeassistant.const import CONF_ACCESS_TOKEN from homeassistant.util import Throttle import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['python-digitalocean==1.10.0'] +REQUIREMENTS = ['python-digitalocean==1.10.1'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 44b8ecf0be7..1e9cc60b2c0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -407,7 +407,7 @@ pyserial==3.1.1 pysnmp==4.3.2 # homeassistant.components.digital_ocean -python-digitalocean==1.10.0 +python-digitalocean==1.10.1 # homeassistant.components.sensor.darksky python-forecastio==1.3.5 From 231ef40f53113766ff6e0409fb7990f50d371c8d Mon Sep 17 00:00:00 2001 From: Johann Kellerman Date: Tue, 8 Nov 2016 01:07:24 +0200 Subject: [PATCH 030/127] iOS links (#4295) --- homeassistant/components/ios.py | 2 +- homeassistant/components/notify/ios.py | 6 +++--- homeassistant/components/sensor/ios.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/ios.py b/homeassistant/components/ios.py index f67ad966ead..f9b17b552de 100644 --- a/homeassistant/components/ios.py +++ b/homeassistant/components/ios.py @@ -2,7 +2,7 @@ Native Home Assistant iOS app component. For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/ios/ +https://home-assistant.io/ecosystem/ios/ """ import asyncio import os diff --git a/homeassistant/components/notify/ios.py b/homeassistant/components/notify/ios.py index 8dc4c7d9701..5cd18640487 100644 --- a/homeassistant/components/notify/ios.py +++ b/homeassistant/components/notify/ios.py @@ -2,7 +2,7 @@ iOS push notification platform for notify component. For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/notify.ios/ +https://home-assistant.io/ecosystem/ios/notifications/ """ import logging from datetime import datetime, timezone @@ -48,8 +48,8 @@ def get_service(hass, config): if not ios.devices_with_push(): _LOGGER.error(("The notify.ios platform was loaded but no " "devices exist! Please check the documentation at " - "https://home-assistant.io/components/notify.ios/ " - "for more information")) + "https://home-assistant.io/ecosystem/ios/notifications" + "/ for more information")) return None return iOSNotificationService() diff --git a/homeassistant/components/sensor/ios.py b/homeassistant/components/sensor/ios.py index c4c8f1eba69..ba963e44b6c 100644 --- a/homeassistant/components/sensor/ios.py +++ b/homeassistant/components/sensor/ios.py @@ -2,7 +2,7 @@ Support for Home Assistant iOS app sensors. For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.ios/ +https://home-assistant.io/ecosystem/ios/ """ from homeassistant.components import ios from homeassistant.helpers.entity import Entity From 2e0c185740a2cdc1773fd81c34e1bb0ba17a94fc Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 8 Nov 2016 07:31:40 +0100 Subject: [PATCH 031/127] Async cleanup part 3 (#4302) --- homeassistant/components/camera/ffmpeg.py | 4 ++-- homeassistant/components/camera/generic.py | 2 +- homeassistant/components/camera/mjpeg.py | 4 ++-- homeassistant/components/camera/synology.py | 4 ++-- homeassistant/components/light/litejet.py | 2 +- homeassistant/components/persistent_notification.py | 3 +-- homeassistant/components/sensor/torque.py | 2 +- homeassistant/components/sensor/yr.py | 2 +- homeassistant/components/switch/litejet.py | 4 ++-- homeassistant/components/switch/netio.py | 2 +- 10 files changed, 14 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/camera/ffmpeg.py b/homeassistant/components/camera/ffmpeg.py index c3f0ffbfe0b..bb7c7ac6cdc 100644 --- a/homeassistant/components/camera/ffmpeg.py +++ b/homeassistant/components/camera/ffmpeg.py @@ -35,7 +35,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Setup a FFmpeg Camera.""" if not async_run_test(hass, config.get(CONF_INPUT)): return - hass.loop.create_task(async_add_devices([FFmpegCamera(hass, config)])) + yield from async_add_devices([FFmpegCamera(hass, config)]) class FFmpegCamera(Camera): @@ -85,7 +85,7 @@ class FFmpegCamera(Camera): break response.write(data) finally: - self.hass.loop.create_task(stream.close()) + self.hass.async_add_job(stream.close()) yield from response.write_eof() @property diff --git a/homeassistant/components/camera/generic.py b/homeassistant/components/camera/generic.py index c6664ed70b2..e298bed2a92 100644 --- a/homeassistant/components/camera/generic.py +++ b/homeassistant/components/camera/generic.py @@ -43,7 +43,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ # pylint: disable=unused-argument def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Setup a generic IP Camera.""" - hass.loop.create_task(async_add_devices([GenericCamera(hass, config)])) + yield from async_add_devices([GenericCamera(hass, config)]) class GenericCamera(Camera): diff --git a/homeassistant/components/camera/mjpeg.py b/homeassistant/components/camera/mjpeg.py index e8799d1be34..a2c35410c55 100644 --- a/homeassistant/components/camera/mjpeg.py +++ b/homeassistant/components/camera/mjpeg.py @@ -43,7 +43,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ # pylint: disable=unused-argument def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Setup a MJPEG IP Camera.""" - hass.loop.create_task(async_add_devices([MjpegCamera(hass, config)])) + yield from async_add_devices([MjpegCamera(hass, config)]) def extract_image_from_mjpeg(stream): @@ -122,7 +122,7 @@ class MjpegCamera(Camera): break response.write(data) finally: - self.hass.loop.create_task(stream.release()) + self.hass.async_add_job(stream.release()) yield from response.write_eof() @property diff --git a/homeassistant/components/camera/synology.py b/homeassistant/components/camera/synology.py index 7edbc0005d8..d6cc0a3e506 100644 --- a/homeassistant/components/camera/synology.py +++ b/homeassistant/components/camera/synology.py @@ -147,7 +147,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): devices.append(device) yield from asyncio.wait(tasks, loop=hass.loop) - hass.loop.create_task(async_add_devices(devices)) + yield from async_add_devices(devices) @asyncio.coroutine @@ -280,7 +280,7 @@ class SynologyCamera(Camera): break response.write(data) finally: - self.hass.loop.create_task(stream.release()) + self.hass.async_add_job(stream.release()) yield from response.write_eof() @property diff --git a/homeassistant/components/light/litejet.py b/homeassistant/components/light/litejet.py index 3ff8067ec8c..33185dad07b 100644 --- a/homeassistant/components/light/litejet.py +++ b/homeassistant/components/light/litejet.py @@ -47,7 +47,7 @@ class LiteJetLight(Light): def _on_load_changed(self): """Called on a LiteJet thread when a load's state changes.""" _LOGGER.debug("Updating due to notification for %s", self._name) - self._hass.loop.create_task(self.async_update_ha_state(True)) + self._hass.async_add_job(self.async_update_ha_state(True)) @property def name(self): diff --git a/homeassistant/components/persistent_notification.py b/homeassistant/components/persistent_notification.py index 5e91aef4d9f..b4dde02baff 100644 --- a/homeassistant/components/persistent_notification.py +++ b/homeassistant/components/persistent_notification.py @@ -55,8 +55,7 @@ def async_create(hass, message, title=None, notification_id=None): ] if value is not None } - hass.loop.create_task( - hass.services.async_call(DOMAIN, SERVICE_CREATE, data)) + hass.async_add_job(hass.services.async_call(DOMAIN, SERVICE_CREATE, data)) @asyncio.coroutine diff --git a/homeassistant/components/sensor/torque.py b/homeassistant/components/sensor/torque.py index fd08e0bc511..8c88a4e22d2 100644 --- a/homeassistant/components/sensor/torque.py +++ b/homeassistant/components/sensor/torque.py @@ -146,4 +146,4 @@ class TorqueSensor(Entity): def async_on_update(self, value): """Receive an update.""" self._state = value - self.hass.loop.create_task(self.async_update_ha_state()) + self.hass.async_add_job(self.async_update_ha_state()) diff --git a/homeassistant/components/sensor/yr.py b/homeassistant/components/sensor/yr.py index 7bede98213e..dd53df9a7fe 100644 --- a/homeassistant/components/sensor/yr.py +++ b/homeassistant/components/sensor/yr.py @@ -154,7 +154,7 @@ class YrData(object): try_again('{} returned {}'.format(self._url, resp.status)) return text = yield from resp.text() - self.hass.loop.create_task(resp.release()) + self.hass.async_add_job(resp.release()) except asyncio.TimeoutError as err: try_again(err) return diff --git a/homeassistant/components/switch/litejet.py b/homeassistant/components/switch/litejet.py index f51984b411b..4ea565ff62f 100644 --- a/homeassistant/components/switch/litejet.py +++ b/homeassistant/components/switch/litejet.py @@ -47,12 +47,12 @@ class LiteJetSwitch(SwitchDevice): def _on_switch_pressed(self): _LOGGER.debug("Updating pressed for %s", self._name) self._state = True - self._hass.loop.create_task(self.async_update_ha_state()) + self._hass.async_add_job(self.async_update_ha_state()) def _on_switch_released(self): _LOGGER.debug("Updating released for %s", self._name) self._state = False - self._hass.loop.create_task(self.async_update_ha_state()) + self._hass.async_add_job(self.async_update_ha_state()) @property def name(self): diff --git a/homeassistant/components/switch/netio.py b/homeassistant/components/switch/netio.py index dde7b791d90..9d292cc1b9a 100644 --- a/homeassistant/components/switch/netio.py +++ b/homeassistant/components/switch/netio.py @@ -119,7 +119,7 @@ class NetioApiView(HomeAssistantView): ndev.start_dates = start_dates for dev in DEVICES[host].entities: - self.hass.loop.create_task(dev.async_update_ha_state()) + self.hass.async_add_job(dev.async_update_ha_state()) return self.json(True) From c05815cced45b0b875c265d57ab578ad67fe13b1 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Tue, 8 Nov 2016 08:08:17 +0100 Subject: [PATCH 032/127] Upgrade sqlalchemy to 1.1.3 (#4277) --- homeassistant/components/recorder/__init__.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index 858ed2c1cf3..6e8869e2d63 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -27,7 +27,7 @@ import homeassistant.util.dt as dt_util DOMAIN = 'recorder' -REQUIREMENTS = ['sqlalchemy==1.1.2'] +REQUIREMENTS = ['sqlalchemy==1.1.3'] DEFAULT_URL = 'sqlite:///{hass_config_path}' DEFAULT_DB_FILE = 'home-assistant_v2.db' diff --git a/requirements_all.txt b/requirements_all.txt index 1e9cc60b2c0..4d759beae1c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -498,7 +498,7 @@ speedtest-cli==0.3.4 # homeassistant.components.recorder # homeassistant.scripts.db_migrator -sqlalchemy==1.1.2 +sqlalchemy==1.1.3 # homeassistant.components.statsd statsd==3.2.1 From 114ece1848625ef547655a01ade1fc44558cf474 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 8 Nov 2016 10:24:50 +0100 Subject: [PATCH 033/127] Fix possible sigterm / unittest / Fix all lazy test (#4297) * replace weakref with a list * add unittest * fix lint * fix handling * fix unittest * change code style * fix lazy tests --- homeassistant/core.py | 16 +++++++++++----- homeassistant/remote.py | 3 +-- tests/test_core.py | 37 +++++++++++++++++++++++++++++++++++++ 3 files changed, 49 insertions(+), 7 deletions(-) diff --git a/homeassistant/core.py b/homeassistant/core.py index 9a3211d8472..eea48d536e3 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -14,7 +14,6 @@ import re import signal import sys import threading -import weakref from types import MappingProxyType from typing import Optional, Any, Callable, List # NOQA @@ -110,7 +109,7 @@ class HomeAssistant(object): self.executor = ThreadPoolExecutor(max_workers=5) self.loop.set_default_executor(self.executor) self.loop.set_exception_handler(self._async_exception_handler) - self._pending_tasks = weakref.WeakSet() + self._pending_tasks = [] self.bus = EventBus(self) self.services = ServiceRegistry(self.bus, self.async_add_job, self.loop) @@ -218,7 +217,12 @@ class HomeAssistant(object): # if a task is sheduled if task is not None: - self._pending_tasks.add(task) + self._pending_tasks.append(task) + + # cleanup + if len(self._pending_tasks) > 50: + self._pending_tasks = [sheduled for sheduled in self._pending_tasks + if not sheduled.done()] @callback def async_run_job(self, target: Callable[..., None], *args: Any) -> None: @@ -254,13 +258,15 @@ class HomeAssistant(object): """Block till all pending work is done.""" while True: # Wait for the pending tasks are down - pending = list(self._pending_tasks) + pending = [task for task in self._pending_tasks + if not task.done()] + self._pending_tasks.clear() if len(pending) > 0: yield from asyncio.wait(pending, loop=self.loop) # Verify the loop is empty ret = yield from self.loop.run_in_executor(None, self._loop_empty) - if ret: + if ret and not self._pending_tasks: break def stop(self) -> None: diff --git a/homeassistant/remote.py b/homeassistant/remote.py index ae96a397826..ba69d3249a9 100644 --- a/homeassistant/remote.py +++ b/homeassistant/remote.py @@ -16,7 +16,6 @@ import logging import time import threading import urllib.parse -import weakref from typing import Optional @@ -128,7 +127,7 @@ class HomeAssistant(ha.HomeAssistant): self.executor = ThreadPoolExecutor(max_workers=5) self.loop.set_default_executor(self.executor) self.loop.set_exception_handler(self._async_exception_handler) - self._pending_tasks = weakref.WeakSet() + self._pending_tasks = [] self.bus = EventBus(remote_api, self) self.services = ha.ServiceRegistry(self.bus, self.add_job, self.loop) diff --git a/tests/test_core.py b/tests/test_core.py index 51653500b6e..025617f97fe 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -118,6 +118,43 @@ class TestHomeAssistant(unittest.TestCase): # self.assertEqual(1, len(calls)) + def test_async_add_job_pending_taks_add(self): + """Add a coro to pending tasks.""" + call_count = [] + + @asyncio.coroutine + def test_coro(): + """Test Coro.""" + call_count.append('call') + + self.hass.add_job(test_coro()) + + assert len(self.hass._pending_tasks) == 1 + self.hass.block_till_done() + assert len(call_count) == 1 + + def test_async_add_job_pending_taks_cleanup(self): + """Add a coro to pending tasks.""" + call_count = [] + + @asyncio.coroutine + def test_coro(): + """Test Coro.""" + call_count.append('call') + + for i in range(50): + self.hass.add_job(test_coro()) + + assert len(self.hass._pending_tasks) == 50 + self.hass.block_till_done() + assert len(call_count) == 50 + + self.hass.add_job(test_coro()) + + assert len(self.hass._pending_tasks) == 1 + self.hass.block_till_done() + assert len(call_count) == 51 + class TestEvent(unittest.TestCase): """A Test Event class.""" From 67b599475e6ddd1f97c2a91e96f5c4704b7eb3a0 Mon Sep 17 00:00:00 2001 From: Lewis Juggins Date: Wed, 9 Nov 2016 02:57:56 +0000 Subject: [PATCH 034/127] Fix OWM async I/O (#4298) --- homeassistant/components/weather/openweathermap.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/weather/openweathermap.py b/homeassistant/components/weather/openweathermap.py index b029b4d44bb..a93b0142d90 100644 --- a/homeassistant/components/weather/openweathermap.py +++ b/homeassistant/components/weather/openweathermap.py @@ -67,7 +67,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): data = WeatherData(owm, latitude, longitude) add_devices([OpenWeatherMapWeather( - name, data, hass.config.units.temperature_unit)]) + name, data, hass.config.units.temperature_unit)], True) class OpenWeatherMapWeather(WeatherEntity): @@ -78,8 +78,7 @@ class OpenWeatherMapWeather(WeatherEntity): self._name = name self._owm = owm self._temperature_unit = temperature_unit - self.date = None - self.update() + self.data = None @property def name(self): From 9561fed6506d674444089d62d17b031bdeeadb37 Mon Sep 17 00:00:00 2001 From: Lewis Juggins Date: Wed, 9 Nov 2016 03:46:44 +0000 Subject: [PATCH 035/127] Fix Dark Sky async I/O (#4299) --- homeassistant/components/sensor/darksky.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/sensor/darksky.py b/homeassistant/components/sensor/darksky.py index 5b0631e2830..c57b4ba30e8 100644 --- a/homeassistant/components/sensor/darksky.py +++ b/homeassistant/components/sensor/darksky.py @@ -113,6 +113,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): longitude=hass.config.longitude, units=units, interval=config.get(CONF_UPDATE_INTERVAL)) + forecast_data.update() forecast_data.update_currently() except ValueError as error: _LOGGER.error(error) @@ -124,7 +125,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): for variable in config[CONF_MONITORED_CONDITIONS]: sensors.append(DarkSkySensor(forecast_data, variable, name)) - add_devices(sensors) + add_devices(sensors, True) class DarkSkySensor(Entity): @@ -139,8 +140,6 @@ class DarkSkySensor(Entity): self._state = None self._unit_of_measurement = None - self.update() - @property def name(self): """Return the name of the sensor.""" @@ -277,8 +276,6 @@ class DarkSkyData(object): self.update_hourly = Throttle(interval)(self._update_hourly) self.update_daily = Throttle(interval)(self._update_daily) - self.update() - def _update(self): """Get the latest data from Dark Sky.""" import forecastio From 4f86c9ecda68dafff6be02870d5a6bfbee8f1683 Mon Sep 17 00:00:00 2001 From: Jesse Newland Date: Tue, 8 Nov 2016 19:57:46 -0800 Subject: [PATCH 036/127] Fix alarm.com I/O inside properties (#4307) * Fix alarm.com I/O inside properties * First line should end with a period * Not needed * Fetch state on init --- .../components/alarm_control_panel/alarmdotcom.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/alarmdotcom.py b/homeassistant/components/alarm_control_panel/alarmdotcom.py index 8bf36e176e5..cd37fc6a828 100644 --- a/homeassistant/components/alarm_control_panel/alarmdotcom.py +++ b/homeassistant/components/alarm_control_panel/alarmdotcom.py @@ -39,7 +39,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): username = config.get(CONF_USERNAME) password = config.get(CONF_PASSWORD) - add_devices([AlarmDotCom(hass, name, code, username, password)]) + add_devices([AlarmDotCom(hass, name, code, username, password)], True) class AlarmDotCom(alarm.AlarmControlPanel): @@ -54,12 +54,17 @@ class AlarmDotCom(alarm.AlarmControlPanel): self._code = str(code) if code else None self._username = username self._password = password + self._state = STATE_UNKNOWN @property def should_poll(self): """No polling needed.""" return True + def update(self): + """Fetch the latest state.""" + self._state = self._alarm.state + @property def name(self): """Return the name of the alarm.""" @@ -73,11 +78,11 @@ class AlarmDotCom(alarm.AlarmControlPanel): @property def state(self): """Return the state of the device.""" - if self._alarm.state == 'Disarmed': + if self._state == 'Disarmed': return STATE_ALARM_DISARMED - elif self._alarm.state == 'Armed Stay': + elif self._state == 'Armed Stay': return STATE_ALARM_ARMED_HOME - elif self._alarm.state == 'Armed Away': + elif self._state == 'Armed Away': return STATE_ALARM_ARMED_AWAY else: return STATE_UNKNOWN From 8cbb8f652742f05068e8bb9053a309fd646124af Mon Sep 17 00:00:00 2001 From: sustah Date: Wed, 9 Nov 2016 04:58:27 +0100 Subject: [PATCH 037/127] Update dlink.py (#4317) corrected "total consumption" units from W to kWh --- homeassistant/components/switch/dlink.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/switch/dlink.py b/homeassistant/components/switch/dlink.py index 31b63db7f79..c8c330a6f4c 100644 --- a/homeassistant/components/switch/dlink.py +++ b/homeassistant/components/switch/dlink.py @@ -87,7 +87,7 @@ class SmartPlugSwitch(SwitchDevice): current_consumption = STATE_UNKNOWN try: - total_consumption = "%.1f W" % \ + total_consumption = "%.1f kWh" % \ float(self.smartplug.total_consumption) except ValueError: total_consumption = STATE_UNKNOWN From 8fc853ba11d8be322d737acb2e38d9c708590a76 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Wed, 9 Nov 2016 05:01:05 +0100 Subject: [PATCH 038/127] Add more unittest for async_add_job (#4320) * Add more unittest for async_add_job * fix test * lint --- tests/test_core.py | 37 ++++++++++++++++++++++++++++++++++--- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/tests/test_core.py b/tests/test_core.py index 025617f97fe..71830a20719 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -118,7 +118,7 @@ class TestHomeAssistant(unittest.TestCase): # self.assertEqual(1, len(calls)) - def test_async_add_job_pending_taks_add(self): + def test_async_add_job_pending_tasks_add(self): """Add a coro to pending tasks.""" call_count = [] @@ -133,8 +133,8 @@ class TestHomeAssistant(unittest.TestCase): self.hass.block_till_done() assert len(call_count) == 1 - def test_async_add_job_pending_taks_cleanup(self): - """Add a coro to pending tasks.""" + def test_async_add_job_pending_tasks_cleanup(self): + """Add a coro to pending tasks and test cleanup.""" call_count = [] @asyncio.coroutine @@ -155,6 +155,37 @@ class TestHomeAssistant(unittest.TestCase): self.hass.block_till_done() assert len(call_count) == 51 + def test_async_add_job_pending_tasks_executor(self): + """Run a executor in pending tasks.""" + call_count = [] + + def test_executor(): + """Test executor.""" + call_count.append('call') + + for i in range(40): + self.hass.add_job(test_executor) + + assert len(self.hass._pending_tasks) == 40 + self.hass.block_till_done() + assert len(call_count) == 40 + + def test_async_add_job_pending_tasks_callback(self): + """Run a callback in pending tasks.""" + call_count = [] + + @ha.callback + def test_callback(): + """Test callback.""" + call_count.append('call') + + for i in range(40): + self.hass.add_job(test_callback) + + assert len(self.hass._pending_tasks) == 0 + self.hass.block_till_done() + assert len(call_count) == 40 + class TestEvent(unittest.TestCase): """A Test Event class.""" From 1e0025acaec4947087d6b335677b973c35edf563 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 8 Nov 2016 20:25:19 -0800 Subject: [PATCH 039/127] Fix Tellstick doing I/O inside event loop (#4268) --- homeassistant/components/sensor/tellstick.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/tellstick.py b/homeassistant/components/sensor/tellstick.py index 464e3554324..08e15cd332f 100644 --- a/homeassistant/components/sensor/tellstick.py +++ b/homeassistant/components/sensor/tellstick.py @@ -98,6 +98,7 @@ class TellstickSensor(Entity): self.datatype = datatype self.sensor = sensor self._unit_of_measurement = sensor_info.unit or None + self._value = None self._name = '{} {}'.format(name, sensor_info.name) @@ -109,9 +110,13 @@ class TellstickSensor(Entity): @property def state(self): """Return the state of the sensor.""" - return self.sensor.value(self.datatype).value + return self._value @property def unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return self._unit_of_measurement + + def update(self): + """Update tellstick sensor.""" + self._value = self.sensor.value(self.datatype).value From 6d0e08cf7ddc31f4baf4a564ab61f91d6c5b6309 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 8 Nov 2016 21:00:33 -0800 Subject: [PATCH 040/127] Fix KNX async I/O (#4267) --- homeassistant/components/climate/knx.py | 19 +++++++++++++------ homeassistant/components/knx.py | 2 -- homeassistant/components/sensor/knx.py | 15 ++++++++++++--- 3 files changed, 25 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/climate/knx.py b/homeassistant/components/climate/knx.py index ef7445c35fd..888a217d90c 100644 --- a/homeassistant/components/climate/knx.py +++ b/homeassistant/components/climate/knx.py @@ -56,6 +56,8 @@ class KNXThermostat(KNXMultiAddressDevice, ClimateDevice): self._unit_of_measurement = TEMP_CELSIUS # KNX always used celsius self._away = False # not yet supported self._is_fan_on = False # not yet supported + self._current_temp = None + self._target_temp = None @property def should_poll(self): @@ -70,16 +72,12 @@ class KNXThermostat(KNXMultiAddressDevice, ClimateDevice): @property def current_temperature(self): """Return the current temperature.""" - from knxip.conversion import knx2_to_float - - return knx2_to_float(self.value('temperature')) + return self._current_temp @property def target_temperature(self): """Return the temperature we try to reach.""" - from knxip.conversion import knx2_to_float - - return knx2_to_float(self.value('setpoint')) + return self._target_temp def set_temperature(self, **kwargs): """Set new target temperature.""" @@ -94,3 +92,12 @@ class KNXThermostat(KNXMultiAddressDevice, ClimateDevice): def set_operation_mode(self, operation_mode): """Set operation mode.""" raise NotImplementedError() + + def update(self): + """Update KNX climate.""" + from knxip.conversion import knx2_to_float + + super().update() + + self._current_temp = knx2_to_float(self.value('temperature')) + self._target_temp = knx2_to_float(self.value('setpoint')) diff --git a/homeassistant/components/knx.py b/homeassistant/components/knx.py index 5d096b30ee0..8653f33c663 100644 --- a/homeassistant/components/knx.py +++ b/homeassistant/components/knx.py @@ -161,8 +161,6 @@ class KNXGroupAddress(Entity): @property def is_on(self): """Return True if the value is not 0 is on, else False.""" - if self.should_poll: - self.update() return self._state != 0 @property diff --git a/homeassistant/components/sensor/knx.py b/homeassistant/components/sensor/knx.py index 007291f5fb1..1f5c9a76520 100644 --- a/homeassistant/components/sensor/knx.py +++ b/homeassistant/components/sensor/knx.py @@ -113,15 +113,24 @@ class KNXSensorFloatClass(KNXGroupAddress, KNXSensorBaseClass): self._unit_of_measurement = unit_of_measurement self._minimum_value = minimum_sensor_value self._maximum_value = maximum_sensor_value + self._value = None KNXGroupAddress.__init__(self, hass, config) @property def state(self): """Return the Value of the KNX Sensor.""" + return self._value + + def update(self): + """Update KNX sensor.""" + from knxip.conversion import knx2_to_float + + super().update() + + self._value = None + if self._data: - from knxip.conversion import knx2_to_float value = knx2_to_float(self._data) if self._minimum_value <= value <= self._maximum_value: - return value - return None + self._value = value From 04dbc992ece95f73c5cc8424aa3fdec8ba76ee96 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 9 Nov 2016 07:21:58 -0800 Subject: [PATCH 041/127] Increase update delay (#4321) --- homeassistant/helpers/entity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index ecb04aca9d9..ac058f89143 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -242,7 +242,7 @@ class Entity(object): end = timer() - if end - start > 0.2: + if end - start > 0.4: _LOGGER.warning('Updating state for %s took %.3f seconds. ' 'Please report platform to the developers at ' 'https://goo.gl/Nvioub', self.entity_id, From 71da21dcc8ef8c50171b4bd9ece5713997f8be10 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Wed, 9 Nov 2016 17:41:17 +0100 Subject: [PATCH 042/127] Change pending task sheduler to time based cleanup (#4324) * Change pending task sheduler to time based cleanup * update unittest --- homeassistant/core.py | 25 ++++++++++++++++++++----- homeassistant/remote.py | 1 + tests/common.py | 3 ++- tests/test_core.py | 32 +++++++++++++++++++------------- 4 files changed, 42 insertions(+), 19 deletions(-) diff --git a/homeassistant/core.py b/homeassistant/core.py index eea48d536e3..c1be6e760d5 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -58,6 +58,9 @@ ENTITY_ID_PATTERN = re.compile(r"^(\w+)\.(\w+)$") # Size of a executor pool EXECUTOR_POOL_SIZE = 15 +# Time for cleanup internal pending tasks +TIME_INTERVAL_TASKS_CLEANUP = 10 + _LOGGER = logging.getLogger(__name__) @@ -110,6 +113,7 @@ class HomeAssistant(object): self.loop.set_default_executor(self.executor) self.loop.set_exception_handler(self._async_exception_handler) self._pending_tasks = [] + self._pending_sheduler = None self.bus = EventBus(self) self.services = ServiceRegistry(self.bus, self.async_add_job, self.loop) @@ -182,10 +186,24 @@ class HomeAssistant(object): # pylint: disable=protected-access self.loop._thread_ident = threading.get_ident() + self._async_tasks_cleanup() _async_create_timer(self) self.bus.async_fire(EVENT_HOMEASSISTANT_START) self.state = CoreState.running + @callback + def _async_tasks_cleanup(self): + """Cleanup all pending tasks in a time interval. + + This method must be run in the event loop. + """ + self._pending_tasks = [task for task in self._pending_tasks + if not task.done()] + + # sheduled next cleanup + self._pending_sheduler = self.loop.call_later( + TIME_INTERVAL_TASKS_CLEANUP, self._async_tasks_cleanup) + def add_job(self, target: Callable[..., None], *args: Any) -> None: """Add job to the executor pool. @@ -219,11 +237,6 @@ class HomeAssistant(object): if task is not None: self._pending_tasks.append(task) - # cleanup - if len(self._pending_tasks) > 50: - self._pending_tasks = [sheduled for sheduled in self._pending_tasks - if not sheduled.done()] - @callback def async_run_job(self, target: Callable[..., None], *args: Any) -> None: """Run a job from within the event loop. @@ -281,6 +294,8 @@ class HomeAssistant(object): """ self.state = CoreState.stopping self.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + if self._pending_sheduler is not None: + self._pending_sheduler.cancel() yield from self.async_block_till_done() self.executor.shutdown() if self._websession is not None: diff --git a/homeassistant/remote.py b/homeassistant/remote.py index ba69d3249a9..c4293680ec5 100644 --- a/homeassistant/remote.py +++ b/homeassistant/remote.py @@ -128,6 +128,7 @@ class HomeAssistant(ha.HomeAssistant): self.loop.set_default_executor(self.executor) self.loop.set_exception_handler(self._async_exception_handler) self._pending_tasks = [] + self._pending_sheduler = None self.bus = EventBus(remote_api, self) self.services = ha.ServiceRegistry(self.bus, self.add_job, self.loop) diff --git a/tests/common.py b/tests/common.py index 6f017d29b46..244ad0b6723 100644 --- a/tests/common.py +++ b/tests/common.py @@ -102,7 +102,8 @@ def async_test_home_assistant(loop): @asyncio.coroutine def mock_async_start(): with patch.object(loop, 'add_signal_handler'), \ - patch('homeassistant.core._async_create_timer'): + patch('homeassistant.core._async_create_timer'), \ + patch.object(hass, '_async_tasks_cleanup', return_value=None): yield from orig_start() hass.async_start = mock_async_start diff --git a/tests/test_core.py b/tests/test_core.py index 71830a20719..60057e57ad1 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -9,6 +9,8 @@ import pytz import homeassistant.core as ha from homeassistant.exceptions import InvalidEntityFormatError +from homeassistant.util.async import ( + run_callback_threadsafe, run_coroutine_threadsafe) import homeassistant.util.dt as dt_util from homeassistant.util.unit_system import (METRIC_SYSTEM) from homeassistant.const import ( @@ -118,7 +120,7 @@ class TestHomeAssistant(unittest.TestCase): # self.assertEqual(1, len(calls)) - def test_async_add_job_pending_tasks_add(self): + def test_pending_sheduler(self): """Add a coro to pending tasks.""" call_count = [] @@ -127,14 +129,24 @@ class TestHomeAssistant(unittest.TestCase): """Test Coro.""" call_count.append('call') - self.hass.add_job(test_coro()) + for i in range(50): + self.hass.add_job(test_coro()) - assert len(self.hass._pending_tasks) == 1 - self.hass.block_till_done() - assert len(call_count) == 1 + run_coroutine_threadsafe( + asyncio.wait(self.hass._pending_tasks, loop=self.hass.loop), + loop=self.hass.loop + ).result() - def test_async_add_job_pending_tasks_cleanup(self): - """Add a coro to pending tasks and test cleanup.""" + with patch.object(self.hass.loop, 'call_later') as mock_later: + run_callback_threadsafe( + self.hass.loop, self.hass._async_tasks_cleanup).result() + assert mock_later.called + + assert len(self.hass._pending_tasks) == 0 + assert len(call_count) == 50 + + def test_async_add_job_pending_tasks_coro(self): + """Add a coro to pending tasks.""" call_count = [] @asyncio.coroutine @@ -149,12 +161,6 @@ class TestHomeAssistant(unittest.TestCase): self.hass.block_till_done() assert len(call_count) == 50 - self.hass.add_job(test_coro()) - - assert len(self.hass._pending_tasks) == 1 - self.hass.block_till_done() - assert len(call_count) == 51 - def test_async_add_job_pending_tasks_executor(self): """Run a executor in pending tasks.""" call_count = [] From d404ac89787d11915759fb0f8745546d66e58536 Mon Sep 17 00:00:00 2001 From: Christopher Viel Date: Wed, 9 Nov 2016 11:44:30 -0500 Subject: [PATCH 043/127] Add support for off script to WOL switch (#4258) --- homeassistant/components/switch/wake_on_lan.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/switch/wake_on_lan.py b/homeassistant/components/switch/wake_on_lan.py index 66652fb106c..29f9ca5096a 100644 --- a/homeassistant/components/switch/wake_on_lan.py +++ b/homeassistant/components/switch/wake_on_lan.py @@ -12,6 +12,7 @@ import voluptuous as vol from homeassistant.components.switch import (SwitchDevice, PLATFORM_SCHEMA) import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.script import Script from homeassistant.const import (CONF_HOST, CONF_NAME) REQUIREMENTS = ['wakeonlan==0.2.2'] @@ -19,6 +20,7 @@ REQUIREMENTS = ['wakeonlan==0.2.2'] _LOGGER = logging.getLogger(__name__) CONF_MAC_ADDRESS = 'mac_address' +CONF_OFF_ACTION = 'turn_off' DEFAULT_NAME = 'Wake on LAN' DEFAULT_PING_TIMEOUT = 1 @@ -27,6 +29,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_MAC_ADDRESS): cv.string, vol.Optional(CONF_HOST): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_OFF_ACTION): cv.SCRIPT_SCHEMA, }) @@ -35,20 +38,22 @@ def setup_platform(hass, config, add_devices, discovery_info=None): name = config.get(CONF_NAME) host = config.get(CONF_HOST) mac_address = config.get(CONF_MAC_ADDRESS) + off_action = config.get(CONF_OFF_ACTION) - add_devices([WOLSwitch(hass, name, host, mac_address)]) + add_devices([WOLSwitch(hass, name, host, mac_address, off_action)]) class WOLSwitch(SwitchDevice): """Representation of a wake on lan switch.""" - def __init__(self, hass, name, host, mac_address): + def __init__(self, hass, name, host, mac_address, off_action): """Initialize the WOL switch.""" from wakeonlan import wol self._hass = hass self._name = name self._host = host self._mac_address = mac_address + self._off_script = Script(hass, off_action) if off_action else None self._state = False self._wol = wol self.update() @@ -74,8 +79,10 @@ class WOLSwitch(SwitchDevice): self.update_ha_state() def turn_off(self): - """Do nothing.""" - pass + """Turn the device off if an off action is present.""" + if self._off_script is not None: + self._off_script.run() + self.update_ha_state() def update(self): """Check if device is on and update the state.""" From 0d4141bf1320a6af59184f275ebbc4788716b95d Mon Sep 17 00:00:00 2001 From: John Arild Berentsen Date: Wed, 9 Nov 2016 19:11:24 +0100 Subject: [PATCH 044/127] Add missing Index labels (#4328) --- homeassistant/components/climate/zwave.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/climate/zwave.py b/homeassistant/components/climate/zwave.py index d94c4f1b94b..64529265874 100755 --- a/homeassistant/components/climate/zwave.py +++ b/homeassistant/components/climate/zwave.py @@ -38,6 +38,7 @@ DEVICE_MAPPINGS = { SET_TEMP_TO_INDEX = { 'Heat': 1, + 'Comfort': 1, 'Cool': 2, 'Auto': 3, 'Aux Heat': 4, @@ -48,9 +49,11 @@ SET_TEMP_TO_INDEX = { 'Moist Air': 9, 'Auto Changeover': 10, 'Heat Econ': 11, + 'Energy Saving': 11, 'Cool Econ': 12, 'Away': 13, - 'Unknown': 14 + 'Unknown': 14, + 'Direct Valve Control': 31 } From ee322dbbdc6f8f27b1b86f437103785250637c9d Mon Sep 17 00:00:00 2001 From: Finbarr Brady Date: Wed, 9 Nov 2016 20:36:57 +0000 Subject: [PATCH 045/127] Cisco IOS device tracker support (#4193) --- .coveragerc | 1 + .../components/device_tracker/cisco_ios.py | 162 ++++++++++++++++++ requirements_all.txt | 1 + 3 files changed, 164 insertions(+) create mode 100644 homeassistant/components/device_tracker/cisco_ios.py diff --git a/.coveragerc b/.coveragerc index dbf9e37f7f9..09d06ec1082 100644 --- a/.coveragerc +++ b/.coveragerc @@ -144,6 +144,7 @@ omit = homeassistant/components/device_tracker/bluetooth_le_tracker.py homeassistant/components/device_tracker/bluetooth_tracker.py homeassistant/components/device_tracker/bt_home_hub_5.py + homeassistant/components/device_tracker/cisco_ios.py homeassistant/components/device_tracker/fritz.py homeassistant/components/device_tracker/icloud.py homeassistant/components/device_tracker/luci.py diff --git a/homeassistant/components/device_tracker/cisco_ios.py b/homeassistant/components/device_tracker/cisco_ios.py new file mode 100644 index 00000000000..0d42282b17c --- /dev/null +++ b/homeassistant/components/device_tracker/cisco_ios.py @@ -0,0 +1,162 @@ +""" +Support for Cisco IOS Routers. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/device_tracker.cisco_ios/ +""" +import logging +from datetime import timedelta + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.device_tracker import DOMAIN, PLATFORM_SCHEMA +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, \ + CONF_PORT +from homeassistant.util import Throttle + +# Return cached results if last scan was less then this time ago. +MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5) + +_LOGGER = logging.getLogger(__name__) + +REQUIREMENTS = ['pexpect==4.0.1'] + +PLATFORM_SCHEMA = vol.All( + PLATFORM_SCHEMA.extend({ + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_USERNAME): cv.string, + vol.Optional(CONF_PASSWORD, default=''): cv.string, + vol.Optional(CONF_PORT): cv.port, + }) +) + + +def get_scanner(hass, config): + """Validate the configuration and return a Cisco scanner.""" + scanner = CiscoDeviceScanner(config[DOMAIN]) + + return scanner if scanner.success_init else None + + +class CiscoDeviceScanner(object): + """This class queries a wireless router running Cisco IOS firmware.""" + + def __init__(self, config): + """Initialize the scanner.""" + self.host = config[CONF_HOST] + self.username = config[CONF_USERNAME] + self.port = config.get(CONF_PORT) + self.password = config.get(CONF_PASSWORD) + + self.last_results = {} + + self.success_init = self._update_info() + _LOGGER.info('cisco_ios scanner initialized') + + # pylint: disable=no-self-use + def get_device_name(self, device): + """The firmware doesn't save the name of the wireless device.""" + return None + + def scan_devices(self): + """Scan for new devices and return a list with found device IDs.""" + self._update_info() + + return self.last_results + + @Throttle(MIN_TIME_BETWEEN_SCANS) + def _update_info(self): + """ + Ensure the information from the Cisco router is up to date. + + Returns boolean if scanning successful. + """ + string_result = self._get_arp_data() + + if string_result: + self.last_results = [] + last_results = [] + + lines_result = string_result.splitlines() + + # Remove the first two lines, as they contains the arp command + # and the arp table titles e.g. + # show ip arp + # Protocol Address | Age (min) | Hardware Addr | Type | Interface + lines_result = lines_result[2:] + + for line in lines_result: + if len(line.split()) is 6: + parts = line.split() + if len(parts) != 6: + continue + + # ['Internet', '10.10.11.1', '-', '0027.d32d.0123', 'ARPA', + # 'GigabitEthernet0'] + age = parts[2] + hw_addr = parts[3] + + if age != "-": + mac = _parse_cisco_mac_address(hw_addr) + age = int(age) + if age < 1: + last_results.append(mac) + + self.last_results = last_results + return True + + return False + + def _get_arp_data(self): + """Open connection to the router and get arp entries.""" + from pexpect import pxssh + import re + + try: + cisco_ssh = pxssh.pxssh() + cisco_ssh.login(self.host, self.username, self.password, + port=self.port, auto_prompt_reset=False) + + # Find the hostname + initial_line = cisco_ssh.before.decode('utf-8').splitlines() + router_hostname = initial_line[len(initial_line) - 1] + router_hostname += "#" + # Set the discovered hostname as prompt + regex_expression = ('(?i)^%s' % router_hostname).encode() + cisco_ssh.PROMPT = re.compile(regex_expression, re.MULTILINE) + # Allow full arp table to print at once + cisco_ssh.sendline("terminal length 0") + cisco_ssh.prompt(1) + + cisco_ssh.sendline("show ip arp") + cisco_ssh.prompt(1) + + devices_result = cisco_ssh.before + + return devices_result.decode("utf-8") + except pxssh.ExceptionPxssh as px_e: + _LOGGER.error("pxssh failed on login.") + _LOGGER.error(px_e) + + return None + + +def _parse_cisco_mac_address(cisco_hardware_addr): + """ + Parse a Cisco formatted HW address to normal MAC. + + e.g. convert + 001d.ec02.07ab + + to: + 00:1D:EC:02:07:AB + + Takes in cisco_hwaddr: HWAddr String from Cisco ARP table + Returns a regular standard MAC address + """ + cisco_hardware_addr = cisco_hardware_addr.replace('.', '') + blocks = [cisco_hardware_addr[x:x + 2] + for x in range(0, len(cisco_hardware_addr), 2)] + + return ':'.join(blocks).upper() diff --git a/requirements_all.txt b/requirements_all.txt index 4d759beae1c..409abeaac3c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -294,6 +294,7 @@ panasonic_viera==0.2 # homeassistant.components.device_tracker.aruba # homeassistant.components.device_tracker.asuswrt +# homeassistant.components.device_tracker.cisco_ios # homeassistant.components.media_player.pandora pexpect==4.0.1 From ade62faa381dbd5bfcb25a1a70c8ecfdc043b1e6 Mon Sep 17 00:00:00 2001 From: Erik Eriksson Date: Thu, 10 Nov 2016 17:46:32 +0100 Subject: [PATCH 046/127] Don't fail if component name is None. Fixes (#4334) https://github.com/home-assistant/home-assistant/issues/4326 Might fix https://github.com/home-assistant/home-assistant/issues/4326 --- homeassistant/components/tellduslive.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/tellduslive.py b/homeassistant/components/tellduslive.py index 961e4edd891..105c5323e83 100644 --- a/homeassistant/components/tellduslive.py +++ b/homeassistant/components/tellduslive.py @@ -77,7 +77,7 @@ def request_sensors(): units = NETWORK.request('sensors/list') # One unit can contain many sensors. if units and 'sensor' in units: - return {unit['id']+sensor['name']: dict(unit, data=sensor) + return {unit['id']+str(sensor['name']): dict(unit, data=sensor) for unit in units['sensor'] for sensor in unit['data']} return None @@ -117,7 +117,7 @@ class TelldusLiveData(object): def _discover(self, found_devices, component_name): """Send discovery event if component not yet discovered.""" - if not len(found_devices): + if not found_devices: return _LOGGER.info("discovered %d new %s devices", From ba2ea35089a9d92a96bd2917e0e616dd700219f7 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 10 Nov 2016 18:46:31 +0100 Subject: [PATCH 047/127] Add logging to platform/component setup (#4300) * Add timeout to platform/component * Revert "Add timeout to platform/component" This reverts commit 280a311e485a54f731de5cf5368e0ff8ac83417f. * Add logging data * Change log message with paulus comments --- homeassistant/bootstrap.py | 1 + homeassistant/helpers/entity_component.py | 1 + 2 files changed, 2 insertions(+) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 2b6c4711691..923237d4e63 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -142,6 +142,7 @@ def _async_setup_component(hass: core.HomeAssistant, async_comp = hasattr(component, 'async_setup') try: + _LOGGER.info("Setting up %s", domain) if async_comp: result = yield from component.async_setup(hass, config) else: diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index cb021fff4a4..c8b0e0e58eb 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -138,6 +138,7 @@ class EntityComponent(object): entity_platform = self._platforms[key] try: + self.logger.info("Setting up %s.%s", self.domain, platform_type) if getattr(platform, 'async_setup_platform', None): yield from platform.async_setup_platform( self.hass, platform_config, From 7d2ab4fce6d86547bf61fe03feef593281a23d3b Mon Sep 17 00:00:00 2001 From: Jan Losinski Date: Thu, 10 Nov 2016 22:14:40 +0100 Subject: [PATCH 048/127] Change pilight systemcode validation to integer (#4286) * Change pilight systemcode validation to integer According to the pilight code the systemcode should be an integer and not a string (it is an int in the pilight code). Passing this as a string caused errors from pilight: "ERROR: elro_800_switch: insufficient number of arguments" This fixes #4282 * Change pilight unit-id to positive integer According to the pilight code the unit of an entity is also evrywhere handled as an integer. So converting and passing this as string causes pilight not to work. This fixes #4282 Signed-off-by: Jan Losinski --- homeassistant/components/switch/pilight.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/switch/pilight.py b/homeassistant/components/switch/pilight.py index f07d91ca9fb..80a36756d79 100644 --- a/homeassistant/components/switch/pilight.py +++ b/homeassistant/components/switch/pilight.py @@ -27,10 +27,10 @@ DEPENDENCIES = ['pilight'] COMMAND_SCHEMA = pilight.RF_CODE_SCHEMA.extend({ vol.Optional('on'): cv.positive_int, vol.Optional('off'): cv.positive_int, - vol.Optional(CONF_UNIT): cv.string, + vol.Optional(CONF_UNIT): cv.positive_int, vol.Optional(CONF_ID): cv.positive_int, vol.Optional(CONF_STATE): cv.string, - vol.Optional(CONF_SYSTEMCODE): cv.string, + vol.Optional(CONF_SYSTEMCODE): cv.positive_int, }) SWITCHES_SCHEMA = vol.Schema({ From e9d19c1dcce4de738755b9922aa632db1901cdbb Mon Sep 17 00:00:00 2001 From: Sean Dague Date: Thu, 10 Nov 2016 23:44:38 -0500 Subject: [PATCH 049/127] Fix "argument of type 'NoneType' is not iterable" during discovery (#4279) * Fix "argument of type 'NoneType' is not iterable" during discovery When yamaha receivers are dynamically discovered, there config is empty, which means that we need to set zone_ignore to [] otherwise the iteration over receivers fails. * Bump rxv library version to fix play_status bug rxv version 0.3 will issue the play_status command even for sources that don't support it, causing stack traces during updates when receivers are on HDMI inputs. This was fixed in rxv 0.3.1. Bump to fix bug #4226. * Don't discovery receivers that we've already configured The discovery component doesn't know anything about already configured receivers. This means that specifying a receiver manually will make it show up twice if you have the discovery component enabled. This puts a platform specific work around here that ensures that if the media_player is found, we ignore the discovery system. --- homeassistant/components/media_player/yamaha.py | 14 +++++++++++++- requirements_all.txt | 2 +- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_player/yamaha.py b/homeassistant/components/media_player/yamaha.py index 94191862f44..0e265199fce 100644 --- a/homeassistant/components/media_player/yamaha.py +++ b/homeassistant/components/media_player/yamaha.py @@ -18,7 +18,7 @@ from homeassistant.const import (CONF_NAME, CONF_HOST, STATE_OFF, STATE_ON, STATE_PLAYING, STATE_IDLE) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['rxv==0.3.0'] +REQUIREMENTS = ['rxv==0.3.1'] _LOGGER = logging.getLogger(__name__) @@ -35,6 +35,7 @@ CONF_SOURCE_IGNORE = 'source_ignore' CONF_ZONE_IGNORE = 'zone_ignore' DEFAULT_NAME = 'Yamaha Receiver' +KNOWN = 'yamaha_known_receivers' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, @@ -50,6 +51,11 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the Yamaha platform.""" import rxv + # keep track of configured receivers so that we don't end up + # discovering a receiver dynamically that we have static config + # for. + if hass.data.get(KNOWN, None) is None: + hass.data[KNOWN] = set() name = config.get(CONF_NAME) host = config.get(CONF_HOST) @@ -62,12 +68,17 @@ def setup_platform(hass, config, add_devices, discovery_info=None): model = discovery_info[1] ctrl_url = discovery_info[2] desc_url = discovery_info[3] + if ctrl_url in hass.data[KNOWN]: + _LOGGER.info("%s already manually configured", ctrl_url) + return receivers = rxv.RXV( ctrl_url, model_name=model, friendly_name=name, unit_desc_url=desc_url).zone_controllers() _LOGGER.info("Receivers: %s", receivers) + # when we are dynamically discovered config is empty + zone_ignore = [] elif host is None: receivers = [] for recv in rxv.find(): @@ -78,6 +89,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): for receiver in receivers: if receiver.zone not in zone_ignore: + hass.data[KNOWN].add(receiver.ctrl_url) add_devices([ YamahaDevice(name, receiver, source_ignore, source_names)]) diff --git a/requirements_all.txt b/requirements_all.txt index 409abeaac3c..0ca9b373228 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -465,7 +465,7 @@ radiotherm==1.2 # rpi-rf==0.9.5 # homeassistant.components.media_player.yamaha -rxv==0.3.0 +rxv==0.3.1 # homeassistant.components.media_player.samsungtv samsungctl==0.5.1 From e005ebe98965f69fd3ade004bdfc0e063df4f1bf Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Fri, 11 Nov 2016 06:01:42 +0100 Subject: [PATCH 050/127] Update SoCo to 0.12 (#4337) * Update SoCo to 0.12 * fix req --- homeassistant/components/media_player/sonos.py | 4 +--- requirements_all.txt | 6 +++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/media_player/sonos.py b/homeassistant/components/media_player/sonos.py index 89f5d7b07ed..ebc8d58874a 100644 --- a/homeassistant/components/media_player/sonos.py +++ b/homeassistant/components/media_player/sonos.py @@ -21,9 +21,7 @@ from homeassistant.const import ( from homeassistant.config import load_yaml_config_file import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['https://github.com/SoCo/SoCo/archive/' - 'cf8c2701165562eccbf1ecc879bf7060ceb0993e.zip#' - 'SoCo==0.12'] +REQUIREMENTS = ['SoCo==0.12'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 0ca9b373228..b6a74c7084f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -24,6 +24,9 @@ PyMata==2.13 # homeassistant.components.rpi_gpio # RPi.GPIO==0.6.1 +# homeassistant.components.media_player.sonos +SoCo==0.12 + # homeassistant.components.notify.twitter TwitterAPI==2.4.2 @@ -157,9 +160,6 @@ https://github.com/GadgetReactor/pyHS100/archive/1f771b7d8090a91c6a58931532e4273 # homeassistant.components.switch.dlink https://github.com/LinuxChristian/pyW215/archive/v0.3.5.zip#pyW215==0.3.5 -# homeassistant.components.media_player.sonos -https://github.com/SoCo/SoCo/archive/cf8c2701165562eccbf1ecc879bf7060ceb0993e.zip#SoCo==0.12 - # homeassistant.components.media_player.webostv # homeassistant.components.notify.webostv https://github.com/TheRealLink/pylgtv/archive/v0.1.2.zip#pylgtv==0.1.2 From 844799a1f7f7ef1ad9d1806ac82764eb29910db8 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Fri, 11 Nov 2016 06:04:47 +0100 Subject: [PATCH 051/127] Synology SSL fix & Error handling (#4325) * Synology SSL fix & Error handling * change handling for cookies/ssl * fix use not deprecated functions * fix lint * change verify * fix connector close to coro * fix force close * not needed since websession close connector too * fix params * fix lint --- homeassistant/components/camera/synology.py | 104 ++++++++++---------- 1 file changed, 54 insertions(+), 50 deletions(-) diff --git a/homeassistant/components/camera/synology.py b/homeassistant/components/camera/synology.py index d6cc0a3e506..bbca25fd6b6 100644 --- a/homeassistant/components/camera/synology.py +++ b/homeassistant/components/camera/synology.py @@ -9,13 +9,14 @@ import logging import voluptuous as vol +import aiohttp from aiohttp import web from aiohttp.web_exceptions import HTTPGatewayTimeout import async_timeout from homeassistant.const import ( CONF_NAME, CONF_USERNAME, CONF_PASSWORD, - CONF_URL, CONF_WHITELIST, CONF_VERIFY_SSL) + CONF_URL, CONF_WHITELIST, CONF_VERIFY_SSL, EVENT_HOMEASSISTANT_STOP) from homeassistant.components.camera import ( Camera, PLATFORM_SCHEMA) import homeassistant.helpers.config_validation as cv @@ -58,8 +59,14 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Setup a Synology IP Camera.""" if not config.get(CONF_VERIFY_SSL): - _LOGGER.warning('SSL verification currently cannot be disabled. ' - 'See https://goo.gl/1h1119') + connector = aiohttp.TCPConnector(verify_ssl=False) + else: + connector = None + + websession_init = aiohttp.ClientSession( + loop=hass.loop, + connector=connector + ) # Determine API to use for authentication syno_api_url = SYNO_API_URL.format( @@ -73,12 +80,12 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): } try: with async_timeout.timeout(TIMEOUT, loop=hass.loop): - query_req = yield from hass.websession.get( + query_req = yield from websession_init.get( syno_api_url, - params=query_payload, + params=query_payload ) - except asyncio.TimeoutError: - _LOGGER.error("Timeout on %s", syno_api_url) + except (asyncio.TimeoutError, aiohttp.errors.ClientError): + _LOGGER.exception("Error on %s", syno_api_url) return False query_resp = yield from query_req.json() @@ -96,12 +103,26 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): session_id = yield from get_session_id( hass, + websession_init, config.get(CONF_USERNAME), config.get(CONF_PASSWORD), - syno_auth_url, - config.get(CONF_VERIFY_SSL) + syno_auth_url ) + websession_init.detach() + + # init websession + websession = aiohttp.ClientSession( + loop=hass.loop, connector=connector, cookies={'id': session_id}) + + @asyncio.coroutine + def _async_close_websession(event): + """Close webssesion on shutdown.""" + yield from websession.close() + + hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, _async_close_websession) + # Use SessionID to get cameras in system syno_camera_url = SYNO_API_URL.format( config.get(CONF_URL), WEBAPI_PATH, camera_api) @@ -113,13 +134,12 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): } try: with async_timeout.timeout(TIMEOUT, loop=hass.loop): - camera_req = yield from hass.websession.get( + camera_req = yield from websession.get( syno_camera_url, - params=camera_payload, - cookies={'id': session_id} + params=camera_payload ) - except asyncio.TimeoutError: - _LOGGER.error("Timeout on %s", syno_camera_url) + except (asyncio.TimeoutError, aiohttp.errors.ClientError): + _LOGGER.exception("Error on %s", syno_camera_url) return False camera_resp = yield from camera_req.json() @@ -128,13 +148,14 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): # add cameras devices = [] - tasks = [] for camera in cameras: if not config.get(CONF_WHITELIST): camera_id = camera['id'] snapshot_path = camera['snapshot_path'] device = SynologyCamera( + hass, + websession, config, camera_id, camera['name'], @@ -143,15 +164,13 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): camera_path, auth_path ) - tasks.append(device.async_read_sid()) devices.append(device) - yield from asyncio.wait(tasks, loop=hass.loop) yield from async_add_devices(devices) @asyncio.coroutine -def get_session_id(hass, username, password, login_url, valid_cert): +def get_session_id(hass, websession, username, password, login_url): """Get a session id.""" auth_payload = { 'api': AUTH_API, @@ -164,12 +183,12 @@ def get_session_id(hass, username, password, login_url, valid_cert): } try: with async_timeout.timeout(TIMEOUT, loop=hass.loop): - auth_req = yield from hass.websession.get( + auth_req = yield from websession.get( login_url, - params=auth_payload, + params=auth_payload ) - except asyncio.TimeoutError: - _LOGGER.error("Timeout on %s", login_url) + except (asyncio.TimeoutError, aiohttp.errors.ClientError): + _LOGGER.exception("Error on %s", login_url) return False auth_resp = yield from auth_req.json() @@ -181,36 +200,22 @@ def get_session_id(hass, username, password, login_url, valid_cert): class SynologyCamera(Camera): """An implementation of a Synology NAS based IP camera.""" - def __init__(self, config, camera_id, camera_name, - snapshot_path, streaming_path, camera_path, auth_path): + def __init__(self, hass, websession, config, camera_id, + camera_name, snapshot_path, streaming_path, camera_path, + auth_path): """Initialize a Synology Surveillance Station camera.""" super().__init__() + self.hass = hass + self._websession = websession self._name = camera_name - self._username = config.get(CONF_USERNAME) - self._password = config.get(CONF_PASSWORD) self._synology_url = config.get(CONF_URL) - self._api_url = config.get(CONF_URL) + 'webapi/' - self._login_url = config.get(CONF_URL) + '/webapi/' + 'auth.cgi' self._camera_name = config.get(CONF_CAMERA_NAME) self._stream_id = config.get(CONF_STREAM_ID) - self._valid_cert = config.get(CONF_VERIFY_SSL) self._camera_id = camera_id self._snapshot_path = snapshot_path self._streaming_path = streaming_path self._camera_path = camera_path self._auth_path = auth_path - self._session_id = None - - @asyncio.coroutine - def async_read_sid(self): - """Get a session id.""" - self._session_id = yield from get_session_id( - self.hass, - self._username, - self._password, - self._login_url, - self._valid_cert - ) def camera_image(self): """Return bytes of camera image.""" @@ -231,13 +236,12 @@ class SynologyCamera(Camera): } try: with async_timeout.timeout(TIMEOUT, loop=self.hass.loop): - response = yield from self.hass.websession.get( + response = yield from self._websession.get( image_url, - params=image_payload, - cookies={'id': self._session_id} + params=image_payload ) - except asyncio.TimeoutError: - _LOGGER.error("Timeout on %s", image_url) + except (asyncio.TimeoutError, aiohttp.errors.ClientError): + _LOGGER.exception("Error on %s", image_url) return None image = yield from response.read() @@ -260,12 +264,12 @@ class SynologyCamera(Camera): } try: with async_timeout.timeout(TIMEOUT, loop=self.hass.loop): - stream = yield from self.hass.websession.get( + stream = yield from self._websession.get( streaming_url, - payload=streaming_payload, - cookies={'id': self._session_id} + params=streaming_payload ) - except asyncio.TimeoutError: + except (asyncio.TimeoutError, aiohttp.errors.ClientError): + _LOGGER.exception("Error on %s", streaming_url) raise HTTPGatewayTimeout() response = web.StreamResponse() From e76d553513efef21f8f587e939f29686c84956c3 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 10 Nov 2016 21:17:44 -0800 Subject: [PATCH 052/127] fix panasonic viera doing I/O in event loop (#4341) --- .../components/media_player/panasonic_viera.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/media_player/panasonic_viera.py b/homeassistant/components/media_player/panasonic_viera.py index d1a971eb91e..987364bbf63 100644 --- a/homeassistant/components/media_player/panasonic_viera.py +++ b/homeassistant/components/media_player/panasonic_viera.py @@ -79,16 +79,16 @@ class PanasonicVieraTVDevice(MediaPlayerDevice): self._playing = True self._state = STATE_UNKNOWN self._remote = remote + self._volume = 0 def update(self): """Retrieve the latest data.""" try: self._muted = self._remote.get_mute() + self._volume = self._remote.get_volume() / 100 self._state = STATE_ON except OSError: self._state = STATE_OFF - return False - return True def send_key(self, key): """Send a key to the tv and handles exceptions.""" @@ -113,13 +113,7 @@ class PanasonicVieraTVDevice(MediaPlayerDevice): @property def volume_level(self): """Volume level of the media player (0..1).""" - volume = 0 - try: - volume = self._remote.get_volume() / 100 - self._state = STATE_ON - except OSError: - self._state = STATE_OFF - return volume + return self._volume @property def is_volume_muted(self): From 9bb94a45120448b83630728292f142a61c128aaa Mon Sep 17 00:00:00 2001 From: Johann Kellerman Date: Fri, 11 Nov 2016 07:28:22 +0200 Subject: [PATCH 053/127] Improve async generic camera's error handling (#4316) * Handle errors * Feedback * DisconnectedError --- homeassistant/components/camera/generic.py | 16 +++++++++------- homeassistant/components/sensor/yr.py | 9 +++------ 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/camera/generic.py b/homeassistant/components/camera/generic.py index e298bed2a92..ec85e6306d4 100644 --- a/homeassistant/components/camera/generic.py +++ b/homeassistant/components/camera/generic.py @@ -91,7 +91,7 @@ class GenericCamera(Camera): if url == self._last_url and self._limit_refetch: return self._last_image - # aiohttp don't support DigestAuth jet + # aiohttp don't support DigestAuth yet if self._authentication == HTTP_DIGEST_AUTHENTICATION: def fetch(): """Read image from a URL.""" @@ -109,15 +109,17 @@ class GenericCamera(Camera): else: try: with async_timeout.timeout(10, loop=self.hass.loop): - respone = yield from self.hass.websession.get( - url, - auth=self._auth - ) - self._last_image = yield from respone.read() - yield from respone.release() + response = yield from self.hass.websession.get( + url, auth=self._auth) + self._last_image = yield from response.read() + yield from response.release() except asyncio.TimeoutError: _LOGGER.error('Timeout getting camera image') return self._last_image + except (aiohttp.errors.ClientError, + aiohttp.errors.ClientDisconnectedError) as err: + _LOGGER.error('Error getting new camera image: %s', err) + return self._last_image self._last_url = url return self._last_image diff --git a/homeassistant/components/sensor/yr.py b/homeassistant/components/sensor/yr.py index dd53df9a7fe..3436288b627 100644 --- a/homeassistant/components/sensor/yr.py +++ b/homeassistant/components/sensor/yr.py @@ -10,7 +10,7 @@ import logging from xml.parsers.expat import ExpatError import async_timeout -from aiohttp.web import HTTPException +import aiohttp import voluptuous as vol import homeassistant.helpers.config_validation as cv @@ -155,11 +155,8 @@ class YrData(object): return text = yield from resp.text() self.hass.async_add_job(resp.release()) - except asyncio.TimeoutError as err: - try_again(err) - return - except HTTPException as err: - resp.close() + except (asyncio.TimeoutError, aiohttp.errors.ClientError, + aiohttp.errors.ClientDisconnectedError) as err: try_again(err) return From b07d887d77b02e0095c03646a5e8dbf960d35f9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Fri, 11 Nov 2016 06:30:52 +0100 Subject: [PATCH 054/127] Add support for rgb light in led flux, fixes issue #4303 (#4332) --- homeassistant/components/light/flux_led.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/light/flux_led.py b/homeassistant/components/light/flux_led.py index 095733afd8e..9de4aa6b0fc 100644 --- a/homeassistant/components/light/flux_led.py +++ b/homeassistant/components/light/flux_led.py @@ -23,6 +23,7 @@ REQUIREMENTS = ['https://github.com/Danielhiversen/flux_led/archive/0.8.zip' _LOGGER = logging.getLogger(__name__) CONF_AUTOMATIC_ADD = 'automatic_add' +ATTR_MODE = 'mode' DOMAIN = 'flux_led' @@ -31,6 +32,8 @@ SUPPORT_FLUX_LED = (SUPPORT_BRIGHTNESS | SUPPORT_EFFECT | DEVICE_SCHEMA = vol.Schema({ vol.Optional(CONF_NAME): cv.string, + vol.Optional(ATTR_MODE, default='rgbw'): + vol.All(cv.string, vol.In(['rgbw', 'rgb'])), }) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ @@ -48,6 +51,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): device = {} device['name'] = device_config[CONF_NAME] device['ipaddr'] = ipaddr + device[ATTR_MODE] = device_config[ATTR_MODE] light = FluxLight(device) if light.is_valid: lights.append(light) @@ -65,6 +69,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): if ipaddr in light_ips: continue device['name'] = device['id'] + " " + ipaddr + device[ATTR_MODE] = 'rgbw' light = FluxLight(device) if light.is_valid: lights.append(light) @@ -82,6 +87,7 @@ class FluxLight(Light): self._name = device['name'] self._ipaddr = device['ipaddr'] + self._mode = device[ATTR_MODE] self.is_valid = True self._bulb = None try: @@ -132,7 +138,11 @@ class FluxLight(Light): if rgb: self._bulb.setRgb(*tuple(rgb)) elif brightness: - self._bulb.setWarmWhite255(brightness) + if self._mode == 'rgbw': + self._bulb.setWarmWhite255(brightness) + elif self._mode == 'rgb': + (red, green, blue) = self._bulb.getRgb() + self._bulb.setRgb(red, green, blue, brightness=brightness) elif effect == EFFECT_RANDOM: self._bulb.setRgb(random.randrange(0, 255), random.randrange(0, 255), From 749fc583ea5f1305f502e4cf7d94e5d93977d1be Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Fri, 11 Nov 2016 06:32:08 +0100 Subject: [PATCH 055/127] Fix rest switch default template (#4331) --- homeassistant/components/switch/rest.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/switch/rest.py b/homeassistant/components/switch/rest.py index 056bcef0281..36674c16d16 100644 --- a/homeassistant/components/switch/rest.py +++ b/homeassistant/components/switch/rest.py @@ -12,11 +12,12 @@ import voluptuous as vol from homeassistant.components.switch import (SwitchDevice, PLATFORM_SCHEMA) from homeassistant.const import (CONF_NAME, CONF_RESOURCE, CONF_TIMEOUT) import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.template import Template CONF_BODY_OFF = 'body_off' CONF_BODY_ON = 'body_on' -DEFAULT_BODY_OFF = 'OFF' -DEFAULT_BODY_ON = 'ON' +DEFAULT_BODY_OFF = Template('OFF') +DEFAULT_BODY_ON = Template('ON') DEFAULT_NAME = 'REST Switch' DEFAULT_TIMEOUT = 10 CONF_IS_ON_TEMPLATE = 'is_on_template' From 20af5cb5b4a560807b85bde51cfe48d975aa5e70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Gonz=C3=A1lez=20Calleja?= Date: Fri, 11 Nov 2016 07:43:16 +0100 Subject: [PATCH 056/127] Daily consumption information for HS110 (#4206) * Add daily consumption information for HS110 * Fixing code review * Fixing code review * Fixing code review --- homeassistant/components/switch/tplink.py | 33 +++++++++++++++-------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/switch/tplink.py b/homeassistant/components/switch/tplink.py index 3554e0b933f..0707ee3756b 100644 --- a/homeassistant/components/switch/tplink.py +++ b/homeassistant/components/switch/tplink.py @@ -6,6 +6,8 @@ https://home-assistant.io/components/switch.tplink/ """ import logging +import time + import voluptuous as vol from homeassistant.components.switch import (SwitchDevice, PLATFORM_SCHEMA) @@ -21,6 +23,7 @@ DEFAULT_NAME = 'TPLink Switch HS100' ATTR_CURRENT_CONSUMPTION = 'Current consumption' ATTR_TOTAL_CONSUMPTION = 'Total consumption' +ATTR_DAILY_CONSUMPTION = 'Daily consumption' ATTR_VOLTAGE = 'Voltage' ATTR_CURRENT = 'Current' @@ -78,16 +81,24 @@ class SmartPlugSwitch(SwitchDevice): def update(self): """Update the TP-Link switch's state.""" - self._state = self.smartplug.state + try: + self._state = self.smartplug.state - if self._emeter_present: - emeter_readings = self.smartplug.get_emeter_realtime() + if self._emeter_present: + emeter_readings = self.smartplug.get_emeter_realtime() - self._emeter_params[ATTR_CURRENT_CONSUMPTION] \ - = "%.1f W" % emeter_readings["power"] - self._emeter_params[ATTR_TOTAL_CONSUMPTION] \ - = "%.2f kW" % emeter_readings["total"] - self._emeter_params[ATTR_VOLTAGE] \ - = "%.2f V" % emeter_readings["voltage"] - self._emeter_params[ATTR_CURRENT] \ - = "%.1f A" % emeter_readings["current"] + self._emeter_params[ATTR_CURRENT_CONSUMPTION] \ + = "%.1f W" % emeter_readings["power"] + self._emeter_params[ATTR_TOTAL_CONSUMPTION] \ + = "%.2f kW" % emeter_readings["total"] + self._emeter_params[ATTR_VOLTAGE] \ + = "%.2f V" % emeter_readings["voltage"] + self._emeter_params[ATTR_CURRENT] \ + = "%.1f A" % emeter_readings["current"] + + emeter_statics = self.smartplug.get_emeter_daily() + self._emeter_params[ATTR_DAILY_CONSUMPTION] \ + = "%.2f kW" % emeter_statics[int(time.strftime("%e"))] + + except OSError: + _LOGGER.warning('Could not update status for %s', self.name) From 2bfded7153cababe7702e2b187b7f229ec4e6b06 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 10 Nov 2016 22:45:38 -0800 Subject: [PATCH 057/127] MQTT.Server will use HASS eventloop (#3429) --- homeassistant/components/mqtt/server.py | 49 +++++-------------------- tests/components/mqtt/test_server.py | 18 ++++----- 2 files changed, 19 insertions(+), 48 deletions(-) diff --git a/homeassistant/components/mqtt/server.py b/homeassistant/components/mqtt/server.py index cffde56b319..cc240e41a30 100644 --- a/homeassistant/components/mqtt/server.py +++ b/homeassistant/components/mqtt/server.py @@ -4,41 +4,21 @@ Support for a local MQTT broker. For more details about this component, please refer to the documentation at https://home-assistant.io/components/mqtt/#use-the-embedded-broker """ -import asyncio import logging import tempfile -import threading +from homeassistant.core import callback from homeassistant.components.mqtt import PROTOCOL_311 from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.util.async import run_coroutine_threadsafe REQUIREMENTS = ['hbmqtt==0.7.1'] DEPENDENCIES = ['http'] -@asyncio.coroutine -def broker_coro(loop, config): - """Start broker coroutine.""" - from hbmqtt.broker import Broker - broker = Broker(config, loop) - yield from broker.start() - return broker - - -def loop_run(loop, broker, shutdown_complete): - """Run broker and clean up when done.""" - loop.run_forever() - # run_forever ends when stop is called because we're shutting down - loop.run_until_complete(broker.shutdown()) - loop.close() - shutdown_complete.set() - - def start(hass, server_config): """Initialize MQTT Server.""" - from hbmqtt.broker import BrokerException - - loop = asyncio.new_event_loop() + from hbmqtt.broker import Broker, BrokerException try: passwd = tempfile.NamedTemporaryFile() @@ -48,29 +28,20 @@ def start(hass, server_config): else: client_config = None - start_server = asyncio.gather(broker_coro(loop, server_config), - loop=loop) - loop.run_until_complete(start_server) - # Result raises exception if one was raised during startup - broker = start_server.result()[0] + broker = Broker(server_config, hass.loop) + run_coroutine_threadsafe(broker.start(), hass.loop).result() except BrokerException: logging.getLogger(__name__).exception('Error initializing MQTT server') - loop.close() return False, None finally: passwd.close() - shutdown_complete = threading.Event() + @callback + def shutdown_mqtt_server(event): + """Shut down the MQTT server.""" + hass.async_add_job(broker.shutdown()) - def shutdown(event): - """Gracefully shutdown MQTT broker.""" - loop.call_soon_threadsafe(loop.stop) - shutdown_complete.wait() - - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, shutdown) - - threading.Thread(target=loop_run, args=(loop, broker, shutdown_complete), - name="MQTT-server").start() + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, shutdown_mqtt_server) return True, client_config diff --git a/tests/components/mqtt/test_server.py b/tests/components/mqtt/test_server.py index 7b0963da23c..03caf8cc4c0 100644 --- a/tests/components/mqtt/test_server.py +++ b/tests/components/mqtt/test_server.py @@ -13,6 +13,7 @@ class TestMQTT: def setup_method(self, method): """Setup things to be run when tests are started.""" self.hass = get_test_home_assistant() + self.hass.config.components.append('http') def teardown_method(self, method): """Stop everything that was started.""" @@ -20,12 +21,13 @@ class TestMQTT: @patch('passlib.apps.custom_app_context', Mock(return_value='')) @patch('tempfile.NamedTemporaryFile', Mock(return_value=MagicMock())) - @patch('asyncio.new_event_loop', Mock()) + @patch('homeassistant.components.mqtt.server.run_coroutine_threadsafe', + Mock(return_value=MagicMock())) + @patch('hbmqtt.broker.Broker', Mock(return_value=MagicMock())) @patch('homeassistant.components.mqtt.MQTT') - @patch('asyncio.gather') - def test_creating_config_with_http_pass(self, mock_gather, mock_mqtt): + def test_creating_config_with_http_pass(self, mock_mqtt): """Test if the MQTT server gets started and subscribe/publish msg.""" - self.hass.config.components.append('http') + self.hass.bus.listen_once = MagicMock() password = 'super_secret' self.hass.config.api = MagicMock(api_password=password) @@ -44,14 +46,12 @@ class TestMQTT: assert mock_mqtt.mock_calls[0][1][6] is None @patch('tempfile.NamedTemporaryFile', Mock(return_value=MagicMock())) - @patch('asyncio.new_event_loop', Mock()) - @patch('asyncio.gather') - def test_broker_config_fails(self, mock_gather): + @patch('homeassistant.components.mqtt.server.run_coroutine_threadsafe') + def test_broker_config_fails(self, mock_run): """Test if the MQTT component fails if server fails.""" - self.hass.config.components.append('http') from hbmqtt.broker import BrokerException - mock_gather.side_effect = BrokerException + mock_run.side_effect = BrokerException self.hass.config.api = MagicMock(api_password=None) From cd1b0ac67d707faa606c982edb72e869a858dc70 Mon Sep 17 00:00:00 2001 From: Jeffrey Lin Date: Thu, 10 Nov 2016 22:46:58 -0800 Subject: [PATCH 058/127] Added NVRAM-based MAC to IP mapping as backup to ARP tables (#4189) --- .../components/device_tracker/asuswrt.py | 48 +++++++++++++++++-- 1 file changed, 44 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/device_tracker/asuswrt.py b/homeassistant/components/device_tracker/asuswrt.py index 50411591cb7..390bebd80c8 100644 --- a/homeassistant/components/device_tracker/asuswrt.py +++ b/homeassistant/components/device_tracker/asuswrt.py @@ -76,6 +76,15 @@ _IP_NEIGH_REGEX = re.compile( r'(\w+\s(?P(([0-9a-f]{2}[:-]){5}([0-9a-f]{2}))))?\s' + r'(?P(\w+))') +_NVRAM_CMD = 'nvram get client_info_tmp' +_NVRAM_REGEX = re.compile( + r'.*>.*>' + + r'(?P([0-9]{1,3}[\.]){3}[0-9]{1,3})' + + r'>' + + r'(?P(([0-9a-fA-F]{2}[:-]){5}([0-9a-fA-F]{2})))' + + r'>' + + r'.*') + # pylint: disable=unused-argument def get_scanner(hass, config): @@ -84,7 +93,7 @@ def get_scanner(hass, config): return scanner if scanner.success_init else None -AsusWrtResult = namedtuple('AsusWrtResult', 'neighbors leases arp') +AsusWrtResult = namedtuple('AsusWrtResult', 'neighbors leases arp nvram') class AsusWrtDeviceScanner(object): @@ -155,7 +164,8 @@ class AsusWrtDeviceScanner(object): active_clients = [client for client in data.values() if client['status'] == 'REACHABLE' or client['status'] == 'DELAY' or - client['status'] == 'STALE'] + client['status'] == 'STALE' or + client['status'] == 'IN_NVRAM'] self.last_results = active_clients return True @@ -184,13 +194,18 @@ class AsusWrtDeviceScanner(object): ssh.sendline(_WL_CMD) ssh.prompt() leases_result = ssh.before.split(b'\n')[1:-1] + ssh.sendline(_NVRAM_CMD) + ssh.prompt() + nvram_result = ssh.before.split(b'\n')[1].split(b'<')[1:] else: arp_result = [''] + nvram_result = [''] ssh.sendline(_LEASES_CMD) ssh.prompt() leases_result = ssh.before.split(b'\n')[1:-1] ssh.logout() - return AsusWrtResult(neighbors, leases_result, arp_result) + return AsusWrtResult(neighbors, leases_result, arp_result, + nvram_result) except pxssh.ExceptionPxssh as exc: _LOGGER.error('Unexpected response from router: %s', exc) return None @@ -213,13 +228,18 @@ class AsusWrtDeviceScanner(object): telnet.write('{}\n'.format(_WL_CMD).encode('ascii')) leases_result = (telnet.read_until(prompt_string). split(b'\n')[1:-1]) + telnet.write('{}\n'.format(_NVRAM_CMD).encode('ascii')) + nvram_result = (telnet.read_until(prompt_string). + split(b'\n')[1].split(b'<')[1:]) else: arp_result = [''] + nvram_result = [''] telnet.write('{}\n'.format(_LEASES_CMD).encode('ascii')) leases_result = (telnet.read_until(prompt_string). split(b'\n')[1:-1]) telnet.write('exit\n'.encode('ascii')) - return AsusWrtResult(neighbors, leases_result, arp_result) + return AsusWrtResult(neighbors, leases_result, arp_result, + nvram_result) except EOFError: _LOGGER.error('Unexpected response from router') return None @@ -277,6 +297,26 @@ class AsusWrtDeviceScanner(object): 'ip': arp_match.group('ip'), 'mac': match.group('mac').upper(), } + + # match mac addresses to IP addresses in NVRAM table + for nvr in result.nvram: + if match.group('mac').upper() in nvr.decode('utf-8'): + nvram_match = _NVRAM_REGEX.search(nvr.decode('utf-8')) + if not nvram_match: + _LOGGER.warning('Could not parse nvr row: %s', nvr) + continue + + # skip current check if already in ARP table + if nvram_match.group('ip') in devices.keys(): + continue + + devices[nvram_match.group('ip')] = { + 'host': host, + 'status': 'IN_NVRAM', + 'ip': nvram_match.group('ip'), + 'mac': match.group('mac').upper(), + } + else: for lease in result.leases: match = _LEASES_REGEX.search(lease.decode('utf-8')) From 01a6c1c1c857edcff2b01cda6c4adda6584585f4 Mon Sep 17 00:00:00 2001 From: Lewis Juggins Date: Fri, 11 Nov 2016 06:57:44 +0000 Subject: [PATCH 059/127] Add strptime template function (#3950) --- homeassistant/helpers/template.py | 10 ++++++++++ tests/helpers/test_template.py | 27 +++++++++++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 105260475e4..dac568439e4 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -1,4 +1,5 @@ """Template helper methods for rendering strings with HA data.""" +from datetime import datetime import json import logging import re @@ -386,6 +387,14 @@ def timestamp_utc(value): return value +def strptime(string, fmt): + """Parse a time string to datetime.""" + try: + return datetime.strptime(string, fmt) + except (ValueError, AttributeError): + return string + + def fail_when_undefined(value): """Filter to force a failure when the value is undefined.""" if isinstance(value, jinja2.Undefined): @@ -420,3 +429,4 @@ ENV.globals['now'] = dt_util.now ENV.globals['utcnow'] = dt_util.utcnow ENV.globals['as_timestamp'] = dt_util.as_timestamp ENV.globals['relative_time'] = dt_util.get_age +ENV.globals['strptime'] = strptime diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index 31f90233701..463f073eeb1 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -1,4 +1,5 @@ """Test Home Assistant template helper methods.""" +from datetime import datetime import unittest from unittest.mock import patch @@ -122,6 +123,32 @@ class TestHelpersTemplate(unittest.TestCase): template.Template('{{ %s | multiply(10) | round }}' % inp, self.hass).render()) + def test_strptime(self): + """Test the parse timestamp method.""" + tests = [ + ('2016-10-19 15:22:05.588122 UTC', + '%Y-%m-%d %H:%M:%S.%f %Z', None), + ('2016-10-19 15:22:05.588122+0100', + '%Y-%m-%d %H:%M:%S.%f%z', None), + ('2016-10-19 15:22:05.588122', + '%Y-%m-%d %H:%M:%S.%f', None), + ('2016-10-19', '%Y-%m-%d', None), + ('2016', '%Y', None), + ('15:22:05', '%H:%M:%S', None), + ('1469119144', '%Y', '1469119144'), + ('invalid', '%Y', 'invalid') + ] + + for inp, fmt, expected in tests: + if expected is None: + expected = datetime.strptime(inp, fmt) + + temp = '{{ strptime(\'%s\', \'%s\') }}' % (inp, fmt) + + self.assertEqual( + str(expected), + template.Template(temp, self.hass).render()) + def test_timestamp_custom(self): """Test the timestamps to custom filter.""" tests = [ From 5e44934e7ef1a99a1d0d19b92bc1e80b9ebbbce0 Mon Sep 17 00:00:00 2001 From: Marcelo Moreira de Mello Date: Fri, 11 Nov 2016 02:01:20 -0500 Subject: [PATCH 060/127] Added some extra options to Weather Underground component (#4306) * Added some extra options to Weather Underground component * Added Location and Elevation options * Fixed if statement * Fixed lint * Updated tests including elevation and location * Update wunderground.py --- .../components/sensor/wunderground.py | 68 ++++++++++++------- tests/components/sensor/test_wunderground.py | 19 +++++- 2 files changed, 61 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/sensor/wunderground.py b/homeassistant/components/sensor/wunderground.py index 93401dfe263..7da6d14fb94 100644 --- a/homeassistant/components/sensor/wunderground.py +++ b/homeassistant/components/sensor/wunderground.py @@ -31,33 +31,44 @@ MIN_TIME_BETWEEN_UPDATES_OBSERVATION = timedelta(minutes=5) # Sensor types are defined like: Name, units SENSOR_TYPES = { 'alerts': ['Alerts', None], - 'weather': ['Weather Summary', None], - 'station_id': ['Station ID', None], + 'dewpoint_c': ['Dewpoint (°C)', TEMP_CELSIUS], + 'dewpoint_f': ['Dewpoint (°F)', TEMP_FAHRENHEIT], + 'dewpoint_string': ['Dewpoint Summary', None], 'feelslike_c': ['Feels Like (°C)', TEMP_CELSIUS], 'feelslike_f': ['Feels Like (°F)', TEMP_FAHRENHEIT], 'feelslike_string': ['Feels Like', None], 'heat_index_c': ['Dewpoint (°C)', TEMP_CELSIUS], 'heat_index_f': ['Dewpoint (°F)', TEMP_FAHRENHEIT], 'heat_index_string': ['Heat Index Summary', None], - 'dewpoint_c': ['Dewpoint (°C)', TEMP_CELSIUS], - 'dewpoint_f': ['Dewpoint (°F)', TEMP_FAHRENHEIT], - 'dewpoint_string': ['Dewpoint Summary', None], - 'wind_kph': ['Wind Speed', 'kpH'], - 'wind_mph': ['Wind Speed', 'mpH'], - 'UV': ['UV', None], - 'pressure_in': ['Pressure', 'in'], - 'pressure_mb': ['Pressure', 'mbar'], - 'wind_dir': ['Wind Direction', None], - 'wind_string': ['Wind Summary', None], - 'temp_c': ['Temperature (°C)', TEMP_CELSIUS], - 'temp_f': ['Temperature (°F)', TEMP_FAHRENHEIT], - 'relative_humidity': ['Relative Humidity', '%'], - 'visibility_mi': ['Visibility (miles)', 'mi'], - 'visibility_km': ['Visibility (km)', 'km'], + 'elevation': ['Elevation', 'ft'], + 'location': ['Location', None], + 'observation_time': ['Observation Time', None], + 'precip_1hr_in': ['Precipation 1hr', 'in'], + 'precip_1hr_metric': ['Precipation 1hr', 'mm'], + 'precip_1hr_string': ['Precipation 1hr', None], 'precip_today_in': ['Precipation Today', 'in'], 'precip_today_metric': ['Precipitation Today', 'mm'], 'precip_today_string': ['Precipitation today', None], - 'solarradiation': ['Solar Radiation', None] + 'pressure_in': ['Pressure', 'in'], + 'pressure_mb': ['Pressure', 'mb'], + 'pressure_trend': ['Pressure Trend', None], + 'relative_humidity': ['Relative Humidity', '%'], + 'station_id': ['Station ID', None], + 'solarradiation': ['Solar Radiation', None], + 'temperature_string': ['Temperature Summary', None], + 'temp_c': ['Temperature (°C)', TEMP_CELSIUS], + 'temp_f': ['Temperature (°F)', TEMP_FAHRENHEIT], + 'UV': ['UV', None], + 'visibility_km': ['Visibility (km)', 'km'], + 'visibility_mi': ['Visibility (miles)', 'mi'], + 'weather': ['Weather Summary', None], + 'wind_degrees': ['Wind Degrees', None], + 'wind_dir': ['Wind Direction', None], + 'wind_gust_kph': ['Wind Gust', 'kpH'], + 'wind_gust_mph': ['Wind Gust', 'mpH'], + 'wind_kph': ['Wind Speed', 'kpH'], + 'wind_mph': ['Wind Speed', 'mpH'], + 'wind_string': ['Wind Summary', None], } # Alert Attributes @@ -112,11 +123,22 @@ class WUndergroundSensor(Entity): @property def state(self): """Return the state of the sensor.""" - if self.rest.data and self._condition in self.rest.data: - if self._condition == 'relative_humidity': - return int(self.rest.data[self._condition][:-1]) - else: - return self.rest.data[self._condition] + if self.rest.data: + + if self._condition == 'elevation' and \ + self._condition in self.rest.data['observation_location']: + return self.rest.data['observation_location'][self._condition]\ + .split()[0] + + if self._condition == 'location' and \ + 'full' in self.rest.data['display_location']: + return self.rest.data['display_location']['full'] + + if self._condition in self.rest.data: + if self._condition == 'relative_humidity': + return int(self.rest.data[self._condition][:-1]) + else: + return self.rest.data[self._condition] if self._condition == 'alerts': if self.rest.alerts: diff --git a/tests/components/sensor/test_wunderground.py b/tests/components/sensor/test_wunderground.py index 27df73d098c..fb76b93885a 100644 --- a/tests/components/sensor/test_wunderground.py +++ b/tests/components/sensor/test_wunderground.py @@ -11,7 +11,7 @@ VALID_CONFIG_PWS = { 'api_key': 'foo', 'pws_id': 'bar', 'monitored_conditions': [ - 'weather', 'feelslike_c', 'alerts' + 'weather', 'feelslike_c', 'alerts', 'elevation', 'location' ] } @@ -19,7 +19,7 @@ VALID_CONFIG = { 'platform': 'wunderground', 'api_key': 'foo', 'monitored_conditions': [ - 'weather', 'feelslike_c', 'alerts' + 'weather', 'feelslike_c', 'alerts', 'elevation', 'location' ] } @@ -61,7 +61,16 @@ def mocked_requests_get(*args, **kwargs): }, "feelslike_c": FEELS_LIKE, "weather": WEATHER, - "icon_url": ICON_URL + "icon_url": ICON_URL, + "display_location": { + "city": "Holly Springs", + "country": "US", + "full": "Holly Springs, NC" + }, + "observation_location": { + "elevation": "413 ft", + "full": "Twin Lake, Holly Springs, North Carolina" + }, }, "alerts": [ { "type": 'FLO', @@ -149,6 +158,10 @@ class TestWundergroundSetup(unittest.TestCase): self.assertEqual(ALERT_MESSAGE, device.device_state_attributes['Message']) self.assertIsNone(device.entity_picture) + elif device.name == 'PWS_location': + self.assertEqual('Holly Springs, NC', device.state) + elif device.name == 'PWS_elevation': + self.assertEqual('413', device.state) else: self.assertIsNone(device.entity_picture) self.assertEqual(FEELS_LIKE, device.state) From 60fabaec24271b4eee0b87dc6ee4a7ac5b0ddb71 Mon Sep 17 00:00:00 2001 From: Hugo Dupras Date: Fri, 11 Nov 2016 08:04:11 +0100 Subject: [PATCH 061/127] Add timeout for Netatmo binary sensor (#4280) * Add time limit for Netatmo binary sensor * Change limit to timeout Signed-off-by: Hugo D. (jabesq) * Update requirements_all.txt --- .../components/binary_sensor/netatmo.py | 18 ++++++++++++------ homeassistant/components/netatmo.py | 2 +- requirements_all.txt | 2 +- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/binary_sensor/netatmo.py b/homeassistant/components/binary_sensor/netatmo.py index e5004db0a4b..93b3bb5817c 100644 --- a/homeassistant/components/binary_sensor/netatmo.py +++ b/homeassistant/components/binary_sensor/netatmo.py @@ -13,7 +13,7 @@ from homeassistant.components.binary_sensor import ( BinarySensorDevice, PLATFORM_SCHEMA) from homeassistant.components.netatmo import WelcomeData from homeassistant.loader import get_component -from homeassistant.const import CONF_MONITORED_CONDITIONS +from homeassistant.const import CONF_MONITORED_CONDITIONS, CONF_TIMEOUT from homeassistant.helpers import config_validation as cv DEPENDENCIES = ["netatmo"] @@ -33,6 +33,7 @@ CONF_CAMERAS = 'cameras' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_HOME): cv.string, + vol.Optional(CONF_TIMEOUT): cv.positive_int, vol.Optional(CONF_CAMERAS, default=[]): vol.All(cv.ensure_list, [cv.string]), vol.Optional(CONF_MONITORED_CONDITIONS, default=SENSOR_TYPES.keys()): @@ -45,6 +46,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """Setup access to Netatmo binary sensor.""" netatmo = get_component('netatmo') home = config.get(CONF_HOME, None) + timeout = config.get(CONF_TIMEOUT, 15) import lnetatmo try: @@ -62,18 +64,19 @@ def setup_platform(hass, config, add_devices, discovery_info=None): camera_name not in config[CONF_CAMERAS]: continue for variable in sensors: - add_devices([WelcomeBinarySensor(data, camera_name, home, + add_devices([WelcomeBinarySensor(data, camera_name, home, timeout, variable)]) class WelcomeBinarySensor(BinarySensorDevice): """Represent a single binary sensor in a Netatmo Welcome device.""" - def __init__(self, data, camera_name, home, sensor): + def __init__(self, data, camera_name, home, timeout, sensor): """Setup for access to the Netatmo camera events.""" self._data = data self._camera_name = camera_name self._home = home + self._timeout = timeout if home: self._name = home + ' / ' + camera_name else: @@ -114,14 +117,17 @@ class WelcomeBinarySensor(BinarySensorDevice): if self._sensor_name == "Someone known": self._state =\ self._data.welcomedata.someoneKnownSeen(self._home, - self._camera_name) + self._camera_name, + self._timeout*60) elif self._sensor_name == "Someone unknown": self._state =\ self._data.welcomedata.someoneUnknownSeen(self._home, - self._camera_name) + self._camera_name, + self._timeout*60) elif self._sensor_name == "Motion": self._state =\ self._data.welcomedata.motionDetected(self._home, - self._camera_name) + self._camera_name, + self._timeout*60) else: return None diff --git a/homeassistant/components/netatmo.py b/homeassistant/components/netatmo.py index 77432411e1a..d6e0101e4e0 100644 --- a/homeassistant/components/netatmo.py +++ b/homeassistant/components/netatmo.py @@ -18,7 +18,7 @@ from homeassistant.util import Throttle REQUIREMENTS = [ 'https://github.com/jabesq/netatmo-api-python/archive/' - 'v0.6.0.zip#lnetatmo==0.6.0'] + 'v0.7.0.zip#lnetatmo==0.7.0'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index b6a74c7084f..1876bb992f9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -187,7 +187,7 @@ https://github.com/danieljkemp/onkyo-eiscp/archive/python3.zip#onkyo-eiscp==0.9. # https://github.com/deisi/fritzconnection/archive/b5c14515e1c8e2652b06b6316a7f3913df942841.zip#fritzconnection==0.4.6 # homeassistant.components.netatmo -https://github.com/jabesq/netatmo-api-python/archive/v0.6.0.zip#lnetatmo==0.6.0 +https://github.com/jabesq/netatmo-api-python/archive/v0.7.0.zip#lnetatmo==0.7.0 # homeassistant.components.switch.neato https://github.com/jabesq/pybotvac/archive/v0.0.1.zip#pybotvac==0.0.1 From 17cfcc981d0729c9d1094a79707aca6af5543d30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Br=C3=A6dstrup?= Date: Fri, 11 Nov 2016 17:38:12 +0100 Subject: [PATCH 062/127] D-Link switch version bump of external library (#4351) --- homeassistant/components/switch/dlink.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/switch/dlink.py b/homeassistant/components/switch/dlink.py index c8c330a6f4c..d0f3daa08f7 100644 --- a/homeassistant/components/switch/dlink.py +++ b/homeassistant/components/switch/dlink.py @@ -15,7 +15,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.const import TEMP_CELSIUS, STATE_UNKNOWN REQUIREMENTS = ['https://github.com/LinuxChristian/pyW215/archive/' - 'v0.3.5.zip#pyW215==0.3.5'] + 'v0.3.6.zip#pyW215==0.3.6'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 1876bb992f9..5d238cad688 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -158,7 +158,7 @@ https://github.com/Danielhiversen/flux_led/archive/0.8.zip#flux_led==0.8 https://github.com/GadgetReactor/pyHS100/archive/1f771b7d8090a91c6a58931532e42730b021cbde.zip#pyHS100==0.2.0 # homeassistant.components.switch.dlink -https://github.com/LinuxChristian/pyW215/archive/v0.3.5.zip#pyW215==0.3.5 +https://github.com/LinuxChristian/pyW215/archive/v0.3.6.zip#pyW215==0.3.6 # homeassistant.components.media_player.webostv # homeassistant.components.notify.webostv From 1663cc9084fac4105819577dbff72034c77aa9fc Mon Sep 17 00:00:00 2001 From: Sean Dague Date: Fri, 11 Nov 2016 11:42:58 -0500 Subject: [PATCH 063/127] Fix typo in generic thermostat (#4348) It looks like a copy / paste error was made when doing the min/max code. This fixes that. --- homeassistant/components/climate/generic_thermostat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/climate/generic_thermostat.py b/homeassistant/components/climate/generic_thermostat.py index 3030ea9090e..baa09439c2c 100644 --- a/homeassistant/components/climate/generic_thermostat.py +++ b/homeassistant/components/climate/generic_thermostat.py @@ -145,7 +145,7 @@ class GenericThermostat(ClimateDevice): def max_temp(self): """Return the maximum temperature.""" # pylint: disable=no-member - if self._min_temp: + if self._max_temp: return self._max_temp else: # Get default temp from super class From 75bcb1ff0f28d5078dbbfb4641dafca7608a429c Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 12 Nov 2016 21:30:05 +0100 Subject: [PATCH 064/127] Upgrade schiene to 0.18 (#4359) --- homeassistant/components/sensor/deutsche_bahn.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/deutsche_bahn.py b/homeassistant/components/sensor/deutsche_bahn.py index e2fd3575e05..51394e0f3ac 100644 --- a/homeassistant/components/sensor/deutsche_bahn.py +++ b/homeassistant/components/sensor/deutsche_bahn.py @@ -15,7 +15,7 @@ from homeassistant.util import Throttle from homeassistant.helpers.entity import Entity import homeassistant.util.dt as dt_util -REQUIREMENTS = ['schiene==0.17'] +REQUIREMENTS = ['schiene==0.18'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 5d238cad688..a48efed96ff 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -471,7 +471,7 @@ rxv==0.3.1 samsungctl==0.5.1 # homeassistant.components.sensor.deutsche_bahn -schiene==0.17 +schiene==0.18 # homeassistant.components.scsgate scsgate==0.1.0 From c823ea9f2a3f5ed10f2197a0441f976ba11d1f0c Mon Sep 17 00:00:00 2001 From: Erik Eriksson Date: Sat, 12 Nov 2016 23:16:27 +0100 Subject: [PATCH 065/127] Don't fail if component name is None. Fixes (#4345) https://github.com/home-assistant/home-assistant/issues/4326 Might fix https://github.com/home-assistant/home-assistant/issues/4326 --- homeassistant/components/sensor/tellduslive.py | 13 +++++++++---- homeassistant/components/tellduslive.py | 3 ++- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/sensor/tellduslive.py b/homeassistant/components/sensor/tellduslive.py index dc3adfed415..915bbac429f 100644 --- a/homeassistant/components/sensor/tellduslive.py +++ b/homeassistant/components/sensor/tellduslive.py @@ -109,7 +109,7 @@ class TelldusLiveSensor(Entity): def name(self): """Return the name of the sensor.""" return "{} {}".format(self._sensor_name or DEVICE_DEFAULT_NAME, - self.quantity_name) + self.quantity_name or "") @property def available(self): @@ -125,6 +125,8 @@ class TelldusLiveSensor(Entity): return self._value_as_humidity elif self._sensor_type == SENSOR_TYPE_LUMINANCE: return self._value_as_luminance + else: + return self._sensor_value @property def device_state_attributes(self): @@ -139,14 +141,17 @@ class TelldusLiveSensor(Entity): @property def quantity_name(self): """Name of quantity.""" - return SENSOR_TYPES[self._sensor_type][0] + return SENSOR_TYPES[self._sensor_type][0] \ + if self._sensor_type in SENSOR_TYPES else None @property def unit_of_measurement(self): """Return the unit of measurement.""" - return SENSOR_TYPES[self._sensor_type][1] + return SENSOR_TYPES[self._sensor_type][1] \ + if self._sensor_type in SENSOR_TYPES else None @property def icon(self): """Return the icon.""" - return SENSOR_TYPES[self._sensor_type][2] + return SENSOR_TYPES[self._sensor_type][2] \ + if self._sensor_type in SENSOR_TYPES else None diff --git a/homeassistant/components/tellduslive.py b/homeassistant/components/tellduslive.py index 105c5323e83..36e9b01d511 100644 --- a/homeassistant/components/tellduslive.py +++ b/homeassistant/components/tellduslive.py @@ -77,7 +77,8 @@ def request_sensors(): units = NETWORK.request('sensors/list') # One unit can contain many sensors. if units and 'sensor' in units: - return {unit['id']+str(sensor['name']): dict(unit, data=sensor) + return {(unit['id'], sensor['name'], sensor['scale']): + dict(unit, data=sensor) for unit in units['sensor'] for sensor in unit['data']} return None From 3d47ad501870aa09c9e94b15d9e6e528450db6ef Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Sun, 13 Nov 2016 01:00:31 +0100 Subject: [PATCH 066/127] Use hass aiohttp connector for ssl connection (#4344) --- homeassistant/components/camera/synology.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/camera/synology.py b/homeassistant/components/camera/synology.py index bbca25fd6b6..9292e839b53 100644 --- a/homeassistant/components/camera/synology.py +++ b/homeassistant/components/camera/synology.py @@ -14,6 +14,7 @@ from aiohttp import web from aiohttp.web_exceptions import HTTPGatewayTimeout import async_timeout +from homeassistant.core import callback from homeassistant.const import ( CONF_NAME, CONF_USERNAME, CONF_PASSWORD, CONF_URL, CONF_WHITELIST, CONF_VERIFY_SSL, EVENT_HOMEASSISTANT_STOP) @@ -60,8 +61,16 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Setup a Synology IP Camera.""" if not config.get(CONF_VERIFY_SSL): connector = aiohttp.TCPConnector(verify_ssl=False) + + @asyncio.coroutine + def _async_close_connector(event): + """Close websession on shutdown.""" + yield from connector.close() + + hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, _async_close_connector) else: - connector = None + connector = hass.websession.connector websession_init = aiohttp.ClientSession( loop=hass.loop, @@ -115,10 +124,10 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): websession = aiohttp.ClientSession( loop=hass.loop, connector=connector, cookies={'id': session_id}) - @asyncio.coroutine + @callback def _async_close_websession(event): - """Close webssesion on shutdown.""" - yield from websession.close() + """Close websession on shutdown.""" + websession.detach() hass.bus.async_listen_once( EVENT_HOMEASSISTANT_STOP, _async_close_websession) From e73634e6c797b43348b7dcf57d38c04094df5167 Mon Sep 17 00:00:00 2001 From: hexa- Date: Sun, 13 Nov 2016 01:14:39 +0100 Subject: [PATCH 067/127] http: reimplement X-Forwarded-For parsing (#4355) This feature needs to be enabled through the `http.use_x_forwarded_for` option, satisfying security concerns of spoofed remote addresses in untrusted network environments. The testsuite was enhanced to explicitly test the functionality of the header. Fixes #4265. Signed-off-by: Martin Weinelt --- homeassistant/components/emulated_hue.py | 1 + homeassistant/components/http.py | 23 +++++++++++------ homeassistant/const.py | 1 + tests/components/test_http.py | 32 ++++++++++++++++++++---- tests/scripts/test_check_config.py | 3 ++- 5 files changed, 47 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/emulated_hue.py b/homeassistant/components/emulated_hue.py index 667a73c6a16..ad89e001df0 100644 --- a/homeassistant/components/emulated_hue.py +++ b/homeassistant/components/emulated_hue.py @@ -76,6 +76,7 @@ def setup(hass, yaml_config): ssl_certificate=None, ssl_key=None, cors_origins=[], + use_x_forwarded_for=False, trusted_networks=[] ) diff --git a/homeassistant/components/http.py b/homeassistant/components/http.py index 5886693c64f..fabff7add53 100644 --- a/homeassistant/components/http.py +++ b/homeassistant/components/http.py @@ -28,7 +28,7 @@ from homeassistant import util from homeassistant.const import ( SERVER_PORT, HTTP_HEADER_HA_AUTH, # HTTP_HEADER_CACHE_CONTROL, CONTENT_TYPE_JSON, ALLOWED_CORS_HEADERS, EVENT_HOMEASSISTANT_STOP, - EVENT_HOMEASSISTANT_START) + EVENT_HOMEASSISTANT_START, HTTP_HEADER_X_FORWARDED_FOR) import homeassistant.helpers.config_validation as cv from homeassistant.components import persistent_notification @@ -42,6 +42,7 @@ CONF_DEVELOPMENT = 'development' CONF_SSL_CERTIFICATE = 'ssl_certificate' CONF_SSL_KEY = 'ssl_key' CONF_CORS_ORIGINS = 'cors_allowed_origins' +CONF_USE_X_FORWARDED_FOR = 'use_x_forwarded_for' CONF_TRUSTED_NETWORKS = 'trusted_networks' DATA_API_PASSWORD = 'api_password' @@ -82,6 +83,7 @@ CONFIG_SCHEMA = vol.Schema({ vol.Optional(CONF_SSL_CERTIFICATE): cv.isfile, vol.Optional(CONF_SSL_KEY): cv.isfile, vol.Optional(CONF_CORS_ORIGINS): vol.All(cv.ensure_list, [cv.string]), + vol.Optional(CONF_USE_X_FORWARDED_FOR, default=False): cv.boolean, vol.Optional(CONF_TRUSTED_NETWORKS): vol.All(cv.ensure_list, [ip_network]) }), @@ -125,6 +127,7 @@ def setup(hass, config): ssl_certificate = conf.get(CONF_SSL_CERTIFICATE) ssl_key = conf.get(CONF_SSL_KEY) cors_origins = conf.get(CONF_CORS_ORIGINS, []) + use_x_forwarded_for = conf.get(CONF_USE_X_FORWARDED_FOR, False) trusted_networks = [ ip_network(trusted_network) for trusted_network in conf.get(CONF_TRUSTED_NETWORKS, [])] @@ -138,6 +141,7 @@ def setup(hass, config): ssl_certificate=ssl_certificate, ssl_key=ssl_key, cors_origins=cors_origins, + use_x_forwarded_for=use_x_forwarded_for, trusted_networks=trusted_networks ) @@ -248,7 +252,7 @@ class HomeAssistantWSGI(object): def __init__(self, hass, development, api_password, ssl_certificate, ssl_key, server_host, server_port, cors_origins, - trusted_networks): + use_x_forwarded_for, trusted_networks): """Initialize the WSGI Home Assistant server.""" import aiohttp_cors @@ -260,6 +264,7 @@ class HomeAssistantWSGI(object): self.ssl_key = ssl_key self.server_host = server_host self.server_port = server_port + self.use_x_forwarded_for = use_x_forwarded_for self.trusted_networks = trusted_networks self.event_forwarder = None self._handler = None @@ -366,11 +371,15 @@ class HomeAssistantWSGI(object): yield from self._handler.finish_connections(60.0) yield from self.app.cleanup() - @staticmethod - def get_real_ip(request): + def get_real_ip(self, request): """Return the clients correct ip address, even in proxied setups.""" - peername = request.transport.get_extra_info('peername') - return peername[0] if peername is not None else None + if self.use_x_forwarded_for \ + and HTTP_HEADER_X_FORWARDED_FOR in request.headers: + return request.headers.get( + HTTP_HEADER_X_FORWARDED_FOR).split(',')[0] + else: + peername = request.transport.get_extra_info('peername') + return peername[0] if peername is not None else None def is_trusted_ip(self, remote_addr): """Match an ip address against trusted CIDR networks.""" @@ -452,7 +461,7 @@ def request_handler_factory(view, handler): @asyncio.coroutine def handle(request): """Handle incoming request.""" - remote_addr = HomeAssistantWSGI.get_real_ip(request) + remote_addr = view.hass.http.get_real_ip(request) # Auth code verbose on purpose authenticated = False diff --git a/homeassistant/const.py b/homeassistant/const.py index a25c73abaeb..baf0c6519c7 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -360,6 +360,7 @@ HTTP_HEADER_CONTENT_LENGTH = 'Content-Length' HTTP_HEADER_CACHE_CONTROL = 'Cache-Control' HTTP_HEADER_EXPIRES = 'Expires' HTTP_HEADER_ORIGIN = 'Origin' +HTTP_HEADER_X_FORWARDED_FOR = 'X-Forwarded-For' HTTP_HEADER_X_REQUESTED_WITH = 'X-Requested-With' HTTP_HEADER_ACCEPT = 'Accept' HTTP_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN = 'Access-Control-Allow-Origin' diff --git a/tests/components/test_http.py b/tests/components/test_http.py index 42a0498ae60..28ded4d6b44 100644 --- a/tests/components/test_http.py +++ b/tests/components/test_http.py @@ -22,6 +22,10 @@ HA_HEADERS = { # Don't add 127.0.0.1/::1 as trusted, as it may interfere with other test cases TRUSTED_NETWORKS = ['192.0.2.0/24', '2001:DB8:ABCD::/48', '100.64.0.1', 'FD01:DB8::1'] +TRUSTED_ADDRESSES = ['100.64.0.1', '192.0.2.100', 'FD01:DB8::1', + '2001:DB8:ABCD::1'] +UNTRUSTED_ADDRESSES = ['198.51.100.1', '2001:DB8:FA1::1', '127.0.0.1', '::1'] + CORS_ORIGINS = [HTTP_BASE_URL, HTTP_BASE] @@ -85,10 +89,19 @@ class TestHttp: assert req.status_code == 401 + def test_access_denied_with_x_forwarded_for(self, caplog): + """Test access denied through the X-Forwarded-For http header.""" + hass.http.use_x_forwarded_for = True + for remote_addr in UNTRUSTED_ADDRESSES: + req = requests.get(_url(const.URL_API), headers={ + const.HTTP_HEADER_X_FORWARDED_FOR: remote_addr}) + + assert req.status_code == 401, \ + "{} shouldn't be trusted".format(remote_addr) + def test_access_denied_with_untrusted_ip(self, caplog): """Test access with an untrusted ip address.""" - for remote_addr in ['198.51.100.1', '2001:DB8:FA1::1', '127.0.0.1', - '::1']: + for remote_addr in UNTRUSTED_ADDRESSES: with patch('homeassistant.components.http.' 'HomeAssistantWSGI.get_real_ip', return_value=remote_addr): @@ -138,10 +151,19 @@ class TestHttp: # assert const.URL_API in logs assert API_PASSWORD not in logs - def test_access_with_trusted_ip(self, caplog): + def test_access_granted_with_x_forwarded_for(self, caplog): + """Test access denied through the X-Forwarded-For http header.""" + hass.http.use_x_forwarded_for = True + for remote_addr in TRUSTED_ADDRESSES: + req = requests.get(_url(const.URL_API), headers={ + const.HTTP_HEADER_X_FORWARDED_FOR: remote_addr}) + + assert req.status_code == 200, \ + "{} should be trusted".format(remote_addr) + + def test_access_granted_with_trusted_ip(self, caplog): """Test access with trusted addresses.""" - for remote_addr in ['100.64.0.1', '192.0.2.100', 'FD01:DB8::1', - '2001:DB8:ABCD::1']: + for remote_addr in TRUSTED_ADDRESSES: with patch('homeassistant.components.http.' 'HomeAssistantWSGI.get_real_ip', return_value=remote_addr): diff --git a/tests/scripts/test_check_config.py b/tests/scripts/test_check_config.py index f0ef9efb2d1..e709d4693c7 100644 --- a/tests/scripts/test_check_config.py +++ b/tests/scripts/test_check_config.py @@ -165,7 +165,8 @@ class TestCheckConfig(unittest.TestCase): self.assertDictEqual({ 'components': {'http': {'api_password': 'abc123', - 'server_port': 8123}}, + 'server_port': 8123, + 'use_x_forwarded_for': False}}, 'except': {}, 'secret_cache': {secrets_path: {'http_pw': 'abc123'}}, 'secrets': {'http_pw': 'abc123'}, From 71a305ea455f190c425596d8943f8295331314fb Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Sun, 13 Nov 2016 01:19:13 +0100 Subject: [PATCH 068/127] Hotfix deadlock on platform setup (#4354) * Hotfix deadlock on platform setup * fix wrong import --- homeassistant/helpers/entity_component.py | 6 +++++- tests/helpers/test_entity_component.py | 25 +++++++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index c8b0e0e58eb..790b9399201 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -294,8 +294,12 @@ class EntityPlatform(object): def add_entities(self, new_entities, update_before_add=False): """Add entities for a single platform.""" + if update_before_add: + for entity in new_entities: + entity.update() + run_coroutine_threadsafe( - self.async_add_entities(list(new_entities), update_before_add), + self.async_add_entities(list(new_entities), False), self.component.hass.loop ).result() diff --git a/tests/helpers/test_entity_component.py b/tests/helpers/test_entity_component.py index 02d8d36dafa..a95c4a6d0fd 100644 --- a/tests/helpers/test_entity_component.py +++ b/tests/helpers/test_entity_component.py @@ -1,5 +1,6 @@ """The tests for the Entity component helper.""" # pylint: disable=protected-access +import asyncio from collections import OrderedDict import logging import unittest @@ -153,6 +154,30 @@ class TestHelpersEntityComponent(unittest.TestCase): assert 1 == len(self.hass.states.entity_ids()) assert not ent.update.called + def test_adds_entities_with_update_befor_add_true_deadlock_protect(self): + """Test if call update befor add to state machine. + + It need to run update inside executor and never call + async_add_entities with True + """ + call = [] + component = EntityComponent(_LOGGER, DOMAIN, self.hass) + + @asyncio.coroutine + def async_add_entities_fake(entities, update_befor_add): + """Fake add_entities_call.""" + call.append(update_befor_add) + component._platforms['core'].async_add_entities = \ + async_add_entities_fake + + ent = EntityTest() + ent.update = Mock(spec_set=True) + component.add_entities([ent], True) + + assert ent.update.called + assert len(call) == 1 + assert not call[0] + def test_not_adding_duplicate_entities(self): """Test for not adding duplicate entities.""" component = EntityComponent(_LOGGER, DOMAIN, self.hass) From 2109b7a1b9491d32fa087cbd0ebb26f4f63b18e7 Mon Sep 17 00:00:00 2001 From: Nathan Henrie Date: Sat, 12 Nov 2016 23:46:23 -0700 Subject: [PATCH 069/127] Use entity_id for backend, friendly name for frontend (#4343) * Use entity_id for backend, friendly name for frontend Closes https://github.com/home-assistant/home-assistant/issues/3434 Command line switches had the option to set a `friendly_name` reportedly for use in the front end. However, if set, it was also being used as the `entity_id`. This did not seem like obvious behavior to me. This PR changes the behavior so the entity_id is the object_id, which must already be unique, and is an obvious place to have a very predictable slug (even if long or unsightly), and the friendly name (if set) is used for the display. Example: ```yaml switch: platform: command_line switches: rf_kitchen_light_one: command_on: switch_command on kitchen command_off: switch_command off kitchen command_state: query_command kitchen value_template: '{{ value == "online" }}' friendly_name: "Beautiful bright kitchen light!" ``` If you were using in an automation or from dev tools, would use: `switch.rf_kitchen_light_one`, but your front end would still show `Beautiful bright kitchen light!` * Add new arg to test_assumed_state_should_be_true_if_command_state_is_false * Import ENTITY_ID _FORMAT from existing, rename device_name to object_id * Rename `device_name` to `object_id` * Test that `entity_id` and `name` are set as expected --- .../components/switch/command_line.py | 17 ++++---- tests/components/switch/test_command_line.py | 39 +++++++++++++++---- 2 files changed, 42 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/switch/command_line.py b/homeassistant/components/switch/command_line.py index e2954f90945..1ea108559e6 100644 --- a/homeassistant/components/switch/command_line.py +++ b/homeassistant/components/switch/command_line.py @@ -9,7 +9,8 @@ import subprocess import voluptuous as vol -from homeassistant.components.switch import (SwitchDevice, PLATFORM_SCHEMA) +from homeassistant.components.switch import (SwitchDevice, PLATFORM_SCHEMA, + ENTITY_ID_FORMAT) from homeassistant.const import ( CONF_FRIENDLY_NAME, CONF_SWITCHES, CONF_VALUE_TEMPLATE, CONF_COMMAND_OFF, CONF_COMMAND_ON, CONF_COMMAND_STATE) @@ -36,7 +37,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): devices = config.get(CONF_SWITCHES, {}) switches = [] - for device_name, device_config in devices.items(): + for object_id, device_config in devices.items(): value_template = device_config.get(CONF_VALUE_TEMPLATE) if value_template is not None: @@ -45,11 +46,12 @@ def setup_platform(hass, config, add_devices, discovery_info=None): switches.append( CommandSwitch( hass, - device_config.get(CONF_FRIENDLY_NAME, device_name), + object_id, + device_config.get(CONF_FRIENDLY_NAME, object_id), device_config.get(CONF_COMMAND_ON), device_config.get(CONF_COMMAND_OFF), device_config.get(CONF_COMMAND_STATE), - value_template, + value_template ) ) @@ -63,11 +65,12 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class CommandSwitch(SwitchDevice): """Representation a switch that can be toggled using shell commands.""" - def __init__(self, hass, name, command_on, command_off, - command_state, value_template): + def __init__(self, hass, object_id, friendly_name, command_on, + command_off, command_state, value_template): """Initialize the switch.""" self._hass = hass - self._name = name + self.entity_id = ENTITY_ID_FORMAT.format(object_id) + self._name = friendly_name self._state = False self._command_on = command_on self._command_off = command_off diff --git a/tests/components/switch/test_command_line.py b/tests/components/switch/test_command_line.py index e0f81ec9ee0..0d940451763 100644 --- a/tests/components/switch/test_command_line.py +++ b/tests/components/switch/test_command_line.py @@ -162,16 +162,41 @@ class TestCommandSwitch(unittest.TestCase): """Test with state value.""" self.hass = get_test_home_assistant() - # Set state command to false - statecmd = False + # args: hass, device_name, friendly_name, command_on, command_off, + # command_state, value_template + init_args = [ + self.hass, + "test_device_name", + "Test friendly name!", + "echo 'on command'", + "echo 'off command'", + False, + None + ] - no_state_device = command_line.CommandSwitch(self.hass, "Test", "echo", - "echo", statecmd, None) + no_state_device = command_line.CommandSwitch(*init_args) self.assertTrue(no_state_device.assumed_state) # Set state command - statecmd = 'cat {}' + init_args[-2] = 'cat {}' - state_device = command_line.CommandSwitch(self.hass, "Test", "echo", - "echo", statecmd, None) + state_device = command_line.CommandSwitch(*init_args) self.assertFalse(state_device.assumed_state) + + def test_entity_id_set_correctly(self): + """Test that entity_id is set correctly from object_id""" + self.hass = get_test_home_assistant() + + init_args = [ + self.hass, + "test_device_name", + "Test friendly name!", + "echo 'on command'", + "echo 'off command'", + False, + None + ] + + test_switch = command_line.CommandSwitch(*init_args) + self.assertEqual(test_switch.entity_id, 'switch.test_device_name') + self.assertEqual(test_switch.name, 'Test friendly name!') From 895454b6c3f0d7cfaae81020756dae36caacc8eb Mon Sep 17 00:00:00 2001 From: Daniel Hoyer Iversen Date: Sun, 13 Nov 2016 11:10:27 +0100 Subject: [PATCH 070/127] support color and brightness in flux_led light --- homeassistant/components/light/flux_led.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/light/flux_led.py b/homeassistant/components/light/flux_led.py index 9de4aa6b0fc..4e3d52b9577 100644 --- a/homeassistant/components/light/flux_led.py +++ b/homeassistant/components/light/flux_led.py @@ -135,7 +135,9 @@ class FluxLight(Light): rgb = kwargs.get(ATTR_RGB_COLOR) brightness = kwargs.get(ATTR_BRIGHTNESS) effect = kwargs.get(ATTR_EFFECT) - if rgb: + if rgb and brightness: + self._bulb.setRgb(*tuple(rgb), brightness=brightness) + elif rgb: self._bulb.setRgb(*tuple(rgb)) elif brightness: if self._mode == 'rgbw': From 0364498dee3d5bbb297d6a96845a4cbcf78f3b63 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 13 Nov 2016 11:34:01 -0800 Subject: [PATCH 071/127] Add .hound.yml --- .hound.yml | 2 ++ .../components/frontend/www_static/home-assistant-polymer | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 .hound.yml diff --git a/.hound.yml b/.hound.yml new file mode 100644 index 00000000000..c5ab91614dc --- /dev/null +++ b/.hound.yml @@ -0,0 +1,2 @@ +python: + enabled: true diff --git a/homeassistant/components/frontend/www_static/home-assistant-polymer b/homeassistant/components/frontend/www_static/home-assistant-polymer index 896e0427675..6071315b167 160000 --- a/homeassistant/components/frontend/www_static/home-assistant-polymer +++ b/homeassistant/components/frontend/www_static/home-assistant-polymer @@ -1 +1 @@ -Subproject commit 896e0427675bb99348de6f1453bd6f8cf48b5c6f +Subproject commit 6071315b1675dfef1090b4683c9639ef0f56cfc0 From 2e2a996a8e234d41686c1a4270608baff43d7426 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 13 Nov 2016 18:12:50 -0800 Subject: [PATCH 072/127] Do not serve HTTP requests while stopping --- homeassistant/components/http.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/http.py b/homeassistant/components/http.py index fabff7add53..d69080a8a43 100644 --- a/homeassistant/components/http.py +++ b/homeassistant/components/http.py @@ -461,6 +461,9 @@ def request_handler_factory(view, handler): @asyncio.coroutine def handle(request): """Handle incoming request.""" + if not view.hass.is_running: + return web.Response(status=503) + remote_addr = view.hass.http.get_real_ip(request) # Auth code verbose on purpose From d7a005ad0fa7a456a2dce375ed889c7117dd69d3 Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Mon, 14 Nov 2016 05:08:56 +0100 Subject: [PATCH 073/127] notify.html5: decode bytes values in registration data Occassionally the values of `keys` and `p256h` are bytes objects instead of strings. As JSON by default does not serialize bytes objects let's decode bytes objects to unicode strings. Resolves the registration issue mentioned in #4012. Signed-off-by: Martin Weinelt --- homeassistant/components/notify/html5.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/notify/html5.py b/homeassistant/components/notify/html5.py index 35f59af1135..c1456923a70 100644 --- a/homeassistant/components/notify/html5.py +++ b/homeassistant/components/notify/html5.py @@ -141,11 +141,23 @@ def _load_config(filename): return None +class JSONBytesDecoder(json.JSONEncoder): + """JSONEncoder to decode bytes objects to unicode.""" + + # pylint: disable=method-hidden + def default(self, obj): + """Decode object if it's a bytes object, else defer to baseclass.""" + if isinstance(obj, bytes): + return obj.decode() + return json.JSONEncoder.default(self, obj) + + def _save_config(filename, config): """Save configuration.""" try: with open(filename, 'w') as fdesc: - fdesc.write(json.dumps(config)) + fdesc.write(json.dumps( + config, cls=JSONBytesDecoder, indent=4, sort_keys=True)) except (IOError, TypeError) as error: _LOGGER.error('Saving config file failed: %s', error) return False From bd9429d3af11fcf2dfd3d811420b631bdb098880 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Mon, 14 Nov 2016 09:35:08 +0100 Subject: [PATCH 074/127] Upgrade sendgrid to 3.6.2 (#4370) --- homeassistant/components/notify/sendgrid.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/notify/sendgrid.py b/homeassistant/components/notify/sendgrid.py index c771240f80a..35e7d10cacb 100644 --- a/homeassistant/components/notify/sendgrid.py +++ b/homeassistant/components/notify/sendgrid.py @@ -13,7 +13,7 @@ from homeassistant.components.notify import ( from homeassistant.const import (CONF_API_KEY, CONF_SENDER, CONF_RECIPIENT) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['sendgrid==3.6.0'] +REQUIREMENTS = ['sendgrid==3.6.2'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index a48efed96ff..243656f06d5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -477,7 +477,7 @@ schiene==0.18 scsgate==0.1.0 # homeassistant.components.notify.sendgrid -sendgrid==3.6.0 +sendgrid==3.6.2 # homeassistant.components.notify.slack slacker==0.9.29 From cb242820405149267bbad2dc2045127ed5d09918 Mon Sep 17 00:00:00 2001 From: pvizeli Date: Mon, 14 Nov 2016 14:18:04 +0100 Subject: [PATCH 075/127] Faster async entity update on component. --- homeassistant/helpers/entity_component.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index 790b9399201..cd49a5e237e 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -377,7 +377,8 @@ class EntityPlatform(object): update_coro = entity.async_update_ha_state(True) if hasattr(entity, 'async_update'): - tasks.append(update_coro) + tasks.append( + self.component.hass.loop.create_task(update_coro)) else: to_update.append(update_coro) From 7bf5d1c66224d5d2ff9733600848670cf8e33477 Mon Sep 17 00:00:00 2001 From: Sean Dague Date: Mon, 14 Nov 2016 12:09:16 -0500 Subject: [PATCH 076/127] Pin versions on linters for tests The linters really need to specify an exact version, because when either flake8 or pylint release a new version, a whole lot of new issues are caught, causing failures on the code unrelated to the patches being pushed. Pinning is a best practice for linters. This allows patches which move forward the linter version to happen with any code fixes required for it to pass. --- requirements_test.txt | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/requirements_test.txt b/requirements_test.txt index 933bb8a7c7b..19a70665b56 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,5 +1,10 @@ -flake8>=3.0.4 -pylint>=1.5.6 +# linters such as flake8 and pylint should be pinned, as new releases +# make new things fail. Manually update these pins when pulling in a +# new version +flake8==3.0.4 +pylint==1.6.4 +mypy-lang==0.4.5 +pydocstyle==1.1.1 coveralls>=1.1 pytest>=2.9.2 pytest-aiohttp>=0.1.3 @@ -7,7 +12,5 @@ pytest-asyncio>=0.5.0 pytest-cov>=2.3.1 pytest-timeout>=1.0.0 pytest-catchlog>=1.2.2 -pydocstyle>=1.0.0 requests_mock>=1.0 -mypy-lang>=0.4 mock-open>=1.3.1 From e7ffec87acefe387a5056feeed541526902638de Mon Sep 17 00:00:00 2001 From: Martin Wood Date: Sun, 30 Oct 2016 21:28:53 +0000 Subject: [PATCH 077/127] Squeezebox name fix #4019 --- homeassistant/components/media_player/squeezebox.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_player/squeezebox.py b/homeassistant/components/media_player/squeezebox.py index ee21e67bf49..18081e9eebb 100644 --- a/homeassistant/components/media_player/squeezebox.py +++ b/homeassistant/components/media_player/squeezebox.py @@ -111,9 +111,11 @@ class LogitechMediaServer(object): def query(self, *parameters): """Send request and await response from server.""" - response = urllib.parse.unquote(self.get(' '.join(parameters))) + response = self.get(' '.join(parameters)) + response = response.split(' ')[-1].strip() + response = urllib.parse.unquote(response) - return response.split(' ')[-1].strip() + return response def get_player_status(self, player): """Get the status of a player.""" From 479457d6ec8a4d00a5e713bec47d6bec682ebdbc Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 14 Nov 2016 18:35:58 -0800 Subject: [PATCH 078/127] device_tracker.see should not call async methods (#4377) --- homeassistant/components/device_tracker/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index 6c6ae3adea6..082602b09f8 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -242,7 +242,7 @@ class DeviceTracker(object): if device.track: device.update_ha_state() - self.hass.bus.async_fire(EVENT_NEW_DEVICE, device) + self.hass.bus.fire(EVENT_NEW_DEVICE, device) # During init, we ignore the group if self.group is not None: From 4c37ee88848d14486b29d458d0041d0338fb6543 Mon Sep 17 00:00:00 2001 From: Lewis Juggins Date: Tue, 15 Nov 2016 04:11:22 +0000 Subject: [PATCH 079/127] Handle live content better in Kodi (#4388) --- homeassistant/components/media_player/kodi.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/media_player/kodi.py b/homeassistant/components/media_player/kodi.py index 1b2bc4f7fc7..ae9f8c8d721 100644 --- a/homeassistant/components/media_player/kodi.py +++ b/homeassistant/components/media_player/kodi.py @@ -104,7 +104,7 @@ class KodiDevice(MediaPlayerDevice): if len(self._players) == 0: return STATE_IDLE - if self._properties['speed'] == 0: + if self._properties['speed'] == 0 and not self._properties['live']: return STATE_PAUSED else: return STATE_PLAYING @@ -120,7 +120,7 @@ class KodiDevice(MediaPlayerDevice): self._properties = self._server.Player.GetProperties( player_id, - ['time', 'totaltime', 'speed'] + ['time', 'totaltime', 'speed', 'live'] ) self._item = self._server.Player.GetItem( @@ -163,7 +163,7 @@ class KodiDevice(MediaPlayerDevice): @property def media_duration(self): """Duration of current playing media in seconds.""" - if self._properties is not None: + if self._properties is not None and not self._properties['live']: total_time = self._properties['totaltime'] return ( From d774ba46c739070b154f40f51ed15a4e9faf7e95 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 14 Nov 2016 20:59:29 -0800 Subject: [PATCH 080/127] Fix device tracker sending invalid event data --- homeassistant/components/device_tracker/__init__.py | 7 +++++-- tests/components/device_tracker/test_init.py | 13 ++++++++++++- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index 082602b09f8..13194f88894 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -31,7 +31,7 @@ from homeassistant.util.yaml import dump from homeassistant.helpers.event import track_utc_time_change from homeassistant.const import ( ATTR_GPS_ACCURACY, ATTR_LATITUDE, ATTR_LONGITUDE, - DEVICE_DEFAULT_NAME, STATE_HOME, STATE_NOT_HOME) + DEVICE_DEFAULT_NAME, STATE_HOME, STATE_NOT_HOME, ATTR_ENTITY_ID) DOMAIN = 'device_tracker' DEPENDENCIES = ['zone'] @@ -242,7 +242,10 @@ class DeviceTracker(object): if device.track: device.update_ha_state() - self.hass.bus.fire(EVENT_NEW_DEVICE, device) + self.hass.bus.fire(EVENT_NEW_DEVICE, { + ATTR_ENTITY_ID: device.entity_id, + ATTR_HOST_NAME: device.host_name, + }) # During init, we ignore the group if self.group is not None: diff --git a/tests/components/device_tracker/test_init.py b/tests/components/device_tracker/test_init.py index 1f95c38cd7f..e4576ec9830 100644 --- a/tests/components/device_tracker/test_init.py +++ b/tests/components/device_tracker/test_init.py @@ -1,5 +1,6 @@ """The tests for the device tracker component.""" # pylint: disable=protected-access +import json import logging import unittest from unittest.mock import call, patch @@ -15,6 +16,7 @@ from homeassistant.const import ( STATE_HOME, STATE_NOT_HOME, CONF_PLATFORM) import homeassistant.components.device_tracker as device_tracker from homeassistant.exceptions import HomeAssistantError +from homeassistant.remote import JSONEncoder from tests.common import ( get_test_home_assistant, fire_time_changed, fire_service_discovered, @@ -324,7 +326,16 @@ class TestComponentsDeviceTracker(unittest.TestCase): device_tracker.see(self.hass, 'mac_1', host_name='hello') self.hass.block_till_done() - self.assertEqual(1, len(test_events)) + + assert len(test_events) == 1 + + # Assert we can serialize the event + json.dumps(test_events[0].as_dict(), cls=JSONEncoder) + + assert test_events[0].data == { + 'entity_id': 'device_tracker.hello', + 'host_name': 'hello', + } # pylint: disable=invalid-name def test_not_write_duplicate_yaml_keys(self): From 0e0ba28249c011c3b61f53afe1380b95c00d9896 Mon Sep 17 00:00:00 2001 From: bestlibre Date: Tue, 15 Nov 2016 07:18:33 +0100 Subject: [PATCH 081/127] support for last will and birth message for mqtt (#4381) --- homeassistant/components/mqtt/__init__.py | 46 +++++++++++++++++------ tests/components/mqtt/test_init.py | 8 ++++ 2 files changed, 43 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 85d99e5f7ee..e2323ca3714 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -17,8 +17,9 @@ from homeassistant.config import load_yaml_config_file from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import template, config_validation as cv from homeassistant.helpers.event import threaded_listener_factory -from homeassistant.const import ( - EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, CONF_VALUE_TEMPLATE) +from homeassistant.const import (EVENT_HOMEASSISTANT_START, + EVENT_HOMEASSISTANT_STOP, CONF_VALUE_TEMPLATE, + CONF_USERNAME, CONF_PASSWORD, CONF_PORT) _LOGGER = logging.getLogger(__name__) @@ -33,17 +34,17 @@ REQUIREMENTS = ['paho-mqtt==1.2'] CONF_EMBEDDED = 'embedded' CONF_BROKER = 'broker' -CONF_PORT = 'port' CONF_CLIENT_ID = 'client_id' CONF_KEEPALIVE = 'keepalive' -CONF_USERNAME = 'username' -CONF_PASSWORD = 'password' CONF_CERTIFICATE = 'certificate' CONF_CLIENT_KEY = 'client_key' CONF_CLIENT_CERT = 'client_cert' CONF_TLS_INSECURE = 'tls_insecure' CONF_PROTOCOL = 'protocol' +CONF_BIRTH_MESSAGE = 'birth_message' +CONF_WILL_MESSAGE = 'will_message' + CONF_STATE_TOPIC = 'state_topic' CONF_COMMAND_TOPIC = 'command_topic' CONF_QOS = 'qos' @@ -84,14 +85,20 @@ _HBMQTT_CONFIG_SCHEMA = vol.Schema(dict) CLIENT_KEY_AUTH_MSG = 'client_key and client_cert must both be present in ' \ 'the mqtt broker config' +MQTT_PUBLISH_SCHEMA = vol.Schema({ + vol.Required(ATTR_TOPIC): valid_publish_topic, + vol.Required(ATTR_PAYLOAD, 'payload'): cv.string, + vol.Required(ATTR_QOS, default=DEFAULT_QOS): _VALID_QOS_SCHEMA, + vol.Required(ATTR_RETAIN, default=DEFAULT_RETAIN): cv.boolean, +}, required=True) + CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ vol.Optional(CONF_CLIENT_ID): cv.string, vol.Optional(CONF_KEEPALIVE, default=DEFAULT_KEEPALIVE): vol.All(vol.Coerce(int), vol.Range(min=15)), vol.Optional(CONF_BROKER): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): - vol.All(vol.Coerce(int), vol.Range(min=1, max=65535)), + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, vol.Optional(CONF_USERNAME): cv.string, vol.Optional(CONF_PASSWORD): cv.string, vol.Optional(CONF_CERTIFICATE): cv.isfile, @@ -103,6 +110,8 @@ CONFIG_SCHEMA = vol.Schema({ vol.Optional(CONF_PROTOCOL, default=DEFAULT_PROTOCOL): vol.All(cv.string, vol.In([PROTOCOL_31, PROTOCOL_311])), vol.Optional(CONF_EMBEDDED): _HBMQTT_CONFIG_SCHEMA, + vol.Optional(CONF_WILL_MESSAGE): MQTT_PUBLISH_SCHEMA, + vol.Optional(CONF_BIRTH_MESSAGE): MQTT_PUBLISH_SCHEMA }), }, extra=vol.ALLOW_EXTRA) @@ -241,11 +250,15 @@ def setup(hass, config): certificate = os.path.join(os.path.dirname(__file__), 'addtrustexternalcaroot.crt') + will_message = conf.get(CONF_WILL_MESSAGE) + birth_message = conf.get(CONF_BIRTH_MESSAGE) + global MQTT_CLIENT try: MQTT_CLIENT = MQTT(hass, broker, port, client_id, keepalive, username, password, certificate, client_key, - client_cert, tls_insecure, protocol) + client_cert, tls_insecure, protocol, will_message, + birth_message) except socket.error: _LOGGER.exception("Can't connect to the broker. " "Please check your settings and the broker " @@ -296,13 +309,14 @@ class MQTT(object): def __init__(self, hass, broker, port, client_id, keepalive, username, password, certificate, client_key, client_cert, - tls_insecure, protocol): + tls_insecure, protocol, will_message, birth_message): """Initialize Home Assistant MQTT client.""" import paho.mqtt.client as mqtt self.hass = hass self.topics = {} self.progress = {} + self.birth_message = birth_message if protocol == PROTOCOL_31: proto = mqtt.MQTTv31 @@ -329,7 +343,11 @@ class MQTT(object): self._mqttc.on_connect = self._mqtt_on_connect self._mqttc.on_disconnect = self._mqtt_on_disconnect self._mqttc.on_message = self._mqtt_on_message - + if will_message: + self._mqttc.will_set(will_message.get(ATTR_TOPIC), + will_message.get(ATTR_PAYLOAD), + will_message.get(ATTR_QOS), + will_message.get(ATTR_RETAIN)) self._mqttc.connect(broker, port, keepalive) def publish(self, topic, payload, qos, retain): @@ -365,7 +383,8 @@ class MQTT(object): def _mqtt_on_connect(self, _mqttc, _userdata, _flags, result_code): """On connect callback. - Resubscribe to all topics we were subscribed to. + Resubscribe to all topics we were subscribed to and publish birth + message. """ if result_code != 0: _LOGGER.error('Unable to connect to the MQTT broker: %s', { @@ -387,6 +406,11 @@ class MQTT(object): # qos is None if we were in process of subscribing if qos is not None: self.subscribe(topic, qos) + if self.birth_message: + self.publish(self.birth_message.get(ATTR_TOPIC), + self.birth_message.get(ATTR_PAYLOAD), + self.birth_message.get(ATTR_QOS), + self.birth_message.get(ATTR_RETAIN)) def _mqtt_on_subscribe(self, _mqttc, _userdata, mid, granted_qos): """Subscribe successful callback.""" diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index 1f46ef01391..01eb81b261e 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -227,6 +227,8 @@ class TestMQTTCallbacks(unittest.TestCase): assert setup_component(self.hass, mqtt.DOMAIN, { mqtt.DOMAIN: { mqtt.CONF_BROKER: 'mock-broker', + mqtt.CONF_BIRTH_MESSAGE: {mqtt.ATTR_TOPIC: 'birth', + mqtt.ATTR_PAYLOAD: 'birth'} } }) @@ -291,6 +293,12 @@ class TestMQTTCallbacks(unittest.TestCase): 3: 'home/sensor', }, mqtt.MQTT_CLIENT.progress) + def test_mqtt_birth_message_on_connect(self): + """Test birth message on connect.""" + mqtt.MQTT_CLIENT._mqtt_on_connect(None, None, 0, 0) + mqtt.MQTT_CLIENT._mqttc.publish.assert_called_with('birth', 'birth', 0, + False) + def test_mqtt_disconnect_tries_no_reconnect_on_stop(self): """Test the disconnect tries.""" mqtt.MQTT_CLIENT._mqtt_on_disconnect(None, None, 0) From d5fff2f94aab6aacec6b2ad0c5d5a8c3cf06f8ab Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Tue, 15 Nov 2016 08:21:44 +0100 Subject: [PATCH 082/127] Fix validation and use consts (mqtt) --- homeassistant/components/mqtt/__init__.py | 34 +++++++++++------------ 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index e2323ca3714..b3891db82f0 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -17,13 +17,13 @@ from homeassistant.config import load_yaml_config_file from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import template, config_validation as cv from homeassistant.helpers.event import threaded_listener_factory -from homeassistant.const import (EVENT_HOMEASSISTANT_START, - EVENT_HOMEASSISTANT_STOP, CONF_VALUE_TEMPLATE, - CONF_USERNAME, CONF_PASSWORD, CONF_PORT) +from homeassistant.const import ( + EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, CONF_VALUE_TEMPLATE, + CONF_USERNAME, CONF_PASSWORD, CONF_PORT, CONF_PROTOCOL, CONF_PAYLOAD) _LOGGER = logging.getLogger(__name__) -DOMAIN = "mqtt" +DOMAIN = 'mqtt' MQTT_CLIENT = None @@ -40,7 +40,6 @@ CONF_CERTIFICATE = 'certificate' CONF_CLIENT_KEY = 'client_key' CONF_CLIENT_CERT = 'client_cert' CONF_TLS_INSECURE = 'tls_insecure' -CONF_PROTOCOL = 'protocol' CONF_BIRTH_MESSAGE = 'birth_message' CONF_WILL_MESSAGE = 'will_message' @@ -87,9 +86,9 @@ CLIENT_KEY_AUTH_MSG = 'client_key and client_cert must both be present in ' \ MQTT_PUBLISH_SCHEMA = vol.Schema({ vol.Required(ATTR_TOPIC): valid_publish_topic, - vol.Required(ATTR_PAYLOAD, 'payload'): cv.string, - vol.Required(ATTR_QOS, default=DEFAULT_QOS): _VALID_QOS_SCHEMA, - vol.Required(ATTR_RETAIN, default=DEFAULT_RETAIN): cv.boolean, + vol.Required(ATTR_PAYLOAD, CONF_PAYLOAD): cv.string, + vol.Optional(ATTR_QOS, default=DEFAULT_QOS): _VALID_QOS_SCHEMA, + vol.Optional(ATTR_RETAIN, default=DEFAULT_RETAIN): cv.boolean, }, required=True) CONFIG_SCHEMA = vol.Schema({ @@ -139,8 +138,8 @@ MQTT_RW_PLATFORM_SCHEMA = MQTT_BASE_PLATFORM_SCHEMA.extend({ # Service call validation schema MQTT_PUBLISH_SCHEMA = vol.Schema({ vol.Required(ATTR_TOPIC): valid_publish_topic, - vol.Exclusive(ATTR_PAYLOAD, 'payload'): object, - vol.Exclusive(ATTR_PAYLOAD_TEMPLATE, 'payload'): cv.string, + vol.Exclusive(ATTR_PAYLOAD, CONF_PAYLOAD): object, + vol.Exclusive(ATTR_PAYLOAD_TEMPLATE, CONF_PAYLOAD): cv.string, vol.Required(ATTR_QOS, default=DEFAULT_QOS): _VALID_QOS_SCHEMA, vol.Required(ATTR_RETAIN, default=DEFAULT_RETAIN): cv.boolean, }, required=True) @@ -205,7 +204,7 @@ def _setup_server(hass, config): server = prepare_setup_platform(hass, config, DOMAIN, 'server') if server is None: - _LOGGER.error('Unable to load embedded server.') + _LOGGER.error("Unable to load embedded server") return None success, broker_config = server.start(hass, conf.get(CONF_EMBEDDED)) @@ -230,7 +229,7 @@ def setup(hass, config): # Embedded broker doesn't have some ssl variables client_key, client_cert, tls_insecure = None, None, None elif not broker_config and not broker_in_conf: - _LOGGER.error('Unable to start broker and auto-configure MQTT.') + _LOGGER.error("Unable to start broker and auto-configure MQTT") return False if broker_in_conf: @@ -261,8 +260,7 @@ def setup(hass, config): birth_message) except socket.error: _LOGGER.exception("Can't connect to the broker. " - "Please check your settings and the broker " - "itself.") + "Please check your settings and the broker itself") return False def stop_mqtt(event): @@ -287,7 +285,7 @@ def setup(hass, config): except template.jinja2.TemplateError as exc: _LOGGER.error( "Unable to publish to '%s': rendering payload template of " - "'%s' failed because %s.", + "'%s' failed because %s", msg_topic, payload_template, exc) return MQTT_CLIENT.publish(msg_topic, payload, qos, retain) @@ -428,7 +426,7 @@ class MQTT(object): "MQTT topic: %s, Payload: %s", msg.topic, msg.payload) else: - _LOGGER.debug("received message on %s: %s", + _LOGGER.debug("Received message on %s: %s", msg.topic, payload) self.hass.bus.fire(EVENT_MQTT_MESSAGE_RECEIVED, { ATTR_TOPIC: msg.topic, @@ -464,14 +462,14 @@ class MQTT(object): while True: try: if self._mqttc.reconnect() == 0: - _LOGGER.info('Successfully reconnected to the MQTT server') + _LOGGER.info("Successfully reconnected to the MQTT server") break except socket.error: pass wait_time = min(2**tries, MAX_RECONNECT_WAIT) _LOGGER.warning( - 'Disconnected from MQTT (%s). Trying to reconnect in %ss', + "Disconnected from MQTT (%s). Trying to reconnect in %s s", result_code, wait_time) # It is ok to sleep here as we are in the MQTT thread. time.sleep(wait_time) From c6f5a5443f18d11dce3d1ebc64d0662c67001426 Mon Sep 17 00:00:00 2001 From: John Arild Berentsen Date: Tue, 15 Nov 2016 13:14:29 +0100 Subject: [PATCH 083/127] Make zwave climate entities contain it's respective setpoints (#4357) * Make zwave entities contain it's respective setpoints * Add fan state --- homeassistant/components/climate/zwave.py | 125 ++++++---------------- 1 file changed, 30 insertions(+), 95 deletions(-) diff --git a/homeassistant/components/climate/zwave.py b/homeassistant/components/climate/zwave.py index 64529265874..e0da5d48c5f 100755 --- a/homeassistant/components/climate/zwave.py +++ b/homeassistant/components/climate/zwave.py @@ -8,8 +8,7 @@ https://home-assistant.io/components/climate.zwave/ # pylint: disable=import-error import logging from homeassistant.components.climate import DOMAIN -from homeassistant.components.climate import ( - ClimateDevice, ATTR_OPERATION_MODE) +from homeassistant.components.climate import ClimateDevice from homeassistant.components.zwave import ZWaveDeviceEntity from homeassistant.components import zwave from homeassistant.const import ( @@ -24,36 +23,11 @@ REMOTEC = 0x5254 REMOTEC_ZXT_120 = 0x8377 REMOTEC_ZXT_120_THERMOSTAT = (REMOTEC, REMOTEC_ZXT_120) -HORSTMANN = 0x0059 -HORSTMANN_HRT4_ZW = 0x3 -HORSTMANN_HRT4_ZW_THERMOSTAT = (HORSTMANN, HORSTMANN_HRT4_ZW) WORKAROUND_ZXT_120 = 'zxt_120' -WORKAROUND_HRT4_ZW = 'hrt4_zw' DEVICE_MAPPINGS = { - REMOTEC_ZXT_120_THERMOSTAT: WORKAROUND_ZXT_120, - HORSTMANN_HRT4_ZW_THERMOSTAT: WORKAROUND_HRT4_ZW -} - -SET_TEMP_TO_INDEX = { - 'Heat': 1, - 'Comfort': 1, - 'Cool': 2, - 'Auto': 3, - 'Aux Heat': 4, - 'Resume': 5, - 'Fan Only': 6, - 'Furnace': 7, - 'Dry Air': 8, - 'Moist Air': 9, - 'Auto Changeover': 10, - 'Heat Econ': 11, - 'Energy Saving': 11, - 'Cool Econ': 12, - 'Away': 13, - 'Unknown': 14, - 'Direct Valve Control': 31 + REMOTEC_ZXT_120_THERMOSTAT: WORKAROUND_ZXT_120 } @@ -80,6 +54,7 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice): from openzwave.network import ZWaveNetwork from pydispatch import dispatcher ZWaveDeviceEntity.__init__(self, value, DOMAIN) + self._index = value.index self._node = value.node self._target_temperature = None self._current_temperature = None @@ -88,13 +63,12 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice): self._operating_state = None self._current_fan_mode = None self._fan_list = None + self._fan_state = None self._current_swing_mode = None self._swing_list = None self._unit = temp_unit - self._index_operation = None _LOGGER.debug("temp_unit is %s", self._unit) self._zxt_120 = None - self._hrt4_zw = None self.update_properties() # register listener dispatcher.connect( @@ -109,10 +83,6 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice): _LOGGER.debug("Remotec ZXT-120 Zwave Thermostat" " workaround") self._zxt_120 = 1 - if DEVICE_MAPPINGS[specific_sensor_key] == WORKAROUND_HRT4_ZW: - _LOGGER.debug("Horstmann HRT4-ZW Zwave Thermostat" - " workaround") - self._hrt4_zw = 1 def value_changed(self, value): """Called when a value has changed on the network.""" @@ -128,8 +98,6 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice): for value in self._node.get_values( class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_MODE).values(): self._current_operation = value.data - self._index_operation = SET_TEMP_TO_INDEX.get( - self._current_operation) self._operation_list = list(value.data_items) _LOGGER.debug("self._operation_list=%s", self._operation_list) _LOGGER.debug("self._current_operation=%s", @@ -139,7 +107,7 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice): class_id=zwave.const.COMMAND_CLASS_SENSOR_MULTILEVEL) .values()): if value.label == 'Temperature': - self._current_temperature = int(value.data) + self._current_temperature = round((float(value.data)), 1) self._unit = value.units # Fan Mode for value in (self._node.get_values( @@ -164,32 +132,33 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice): _LOGGER.debug("self._current_swing_mode=%s", self._current_swing_mode) # Set point + temps = [] for value in (self._node.get_values( class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_SETPOINT) .values()): - if value.data == 0: - _LOGGER.debug("Setpoint is 0, setting default to " - "current_temperature=%s", - self._current_temperature) - self._target_temperature = int(self._current_temperature) - break - if self.current_operation is not None and \ - self.current_operation != 'Off': - if self._index_operation != value.index: - continue - if self._zxt_120: + temps.append((round(float(value.data)), 1)) + if value.index == self._index: + if value.data == 0: + _LOGGER.debug("Setpoint is 0, setting default to " + "current_temperature=%s", + self._current_temperature) + self._target_temperature = ( + round((float(self._current_temperature)), 1)) break - self._target_temperature = int(value.data) - break - _LOGGER.debug("Device can't set setpoint based on operation mode." - " Defaulting to index=1") - self._target_temperature = int(value.data) + else: + self._target_temperature = round((float(value.data)), 1) # Operating state for value in (self._node.get_values( class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_OPERATING_STATE) .values()): self._operating_state = value.data + # Fan operating state + for value in (self._node.get_values( + class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_FAN_STATE) + .values()): + self._fan_state = value.data + @property def should_poll(self): """No polling on ZWave.""" @@ -251,53 +220,19 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice): temperature = kwargs.get(ATTR_TEMPERATURE) else: return - operation_mode = kwargs.get(ATTR_OPERATION_MODE) - _LOGGER.debug("set_temperature operation_mode=%s", operation_mode) for value in (self._node.get_values( class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_SETPOINT) .values()): - if operation_mode is not None: - setpoint_mode = SET_TEMP_TO_INDEX.get(operation_mode) - if value.index != setpoint_mode: - continue - _LOGGER.debug("setpoint_mode=%s", setpoint_mode) - value.data = temperature - break - - if self.current_operation is not None: - if self._hrt4_zw and self.current_operation == 'Off': - # HRT4-ZW can change setpoint when off. - value.data = int(temperature) - if self._index_operation != value.index: - continue - _LOGGER.debug("self._index_operation=%s and" - " self._current_operation=%s", - self._index_operation, - self._current_operation) + if value.index == self._index: if self._zxt_120: - _LOGGER.debug("zxt_120: Setting new setpoint for %s, " - " operation=%s, temp=%s", - self._index_operation, - self._current_operation, temperature) - # ZXT-120 does not support get setpoint - self._target_temperature = temperature # ZXT-120 responds only to whole int value.data = round(temperature, 0) + self._target_temperature = temperature self.update_ha_state() - break else: - _LOGGER.debug("Setting new setpoint for %s, " - "operation=%s, temp=%s", - self._index_operation, - self._current_operation, temperature) value.data = temperature - break - else: - _LOGGER.debug("Setting new setpoint for no known " - "operation mode. Index=1 and " - "temperature=%s", temperature) - value.data = temperature + self.update_ha_state() break def set_fan_mode(self, fan): @@ -334,9 +269,9 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice): @property def device_state_attributes(self): """Return the device specific state attributes.""" + data = {} if self._operating_state: - return { - "operating_state": self._operating_state, - } - else: - return {} + data["operating_state"] = self._operating_state, + if self._fan_state: + data["fan_state"] = self._fan_state + return data From 5d8a465c1890519e51b8c1c3f074bc63eea0c1fa Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Wed, 16 Nov 2016 06:02:17 +0100 Subject: [PATCH 084/127] Add timeout to requests (#4398) --- homeassistant/components/notify/nma.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/notify/nma.py b/homeassistant/components/notify/nma.py index a21a37bb323..7a05d08134f 100644 --- a/homeassistant/components/notify/nma.py +++ b/homeassistant/components/notify/nma.py @@ -26,8 +26,11 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ def get_service(hass, config): """Get the NMA notification service.""" - response = requests.get(_RESOURCE + 'verify', - params={"apikey": config[CONF_API_KEY]}) + parameters = { + 'apikey': config[CONF_API_KEY], + } + response = requests.get( + '{}{}'.format(_RESOURCE, 'verify'), params=parameters, timeout=5) tree = ET.fromstring(response.content) if tree[0].tag == 'error': @@ -47,14 +50,15 @@ class NmaNotificationService(BaseNotificationService): def send_message(self, message="", **kwargs): """Send a message to a user.""" data = { - "apikey": self._api_key, - "application": 'home-assistant', - "event": kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT), - "description": message, - "priority": 0, + 'apikey': self._api_key, + 'application': 'home-assistant', + 'event': kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT), + 'description': message, + 'priority': 0, } - response = requests.get(_RESOURCE + 'notify', params=data) + response = requests.get( + '{}{}'.format(_RESOURCE, 'notify'), params=data, timeout=5) tree = ET.fromstring(response.content) if tree[0].tag == 'error': From 41aaeb715a99eeb4f5d6ecf9c50aaaa154f1ef27 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Wed, 16 Nov 2016 06:06:50 +0100 Subject: [PATCH 085/127] Convert switch to AsnycIO (#4382) * Convert switch to AsnycIO * Move update entity to service * use time better for faster handling * Change to suggestion from paulus * Use new shedule_update_ha_state * fix lint * minimize executor calls --- homeassistant/components/switch/__init__.py | 51 ++++++++++++------- .../components/switch/command_line.py | 4 +- homeassistant/components/switch/demo.py | 4 +- homeassistant/components/switch/enocean.py | 2 +- homeassistant/components/switch/flux.py | 4 +- homeassistant/components/switch/knx.py | 4 +- homeassistant/components/switch/mqtt.py | 4 +- homeassistant/components/switch/mysensors.py | 8 +-- homeassistant/components/switch/netio.py | 2 +- homeassistant/components/switch/pilight.py | 4 +- .../components/switch/pulseaudio_loopback.py | 4 +- homeassistant/components/switch/rpi_gpio.py | 4 +- homeassistant/components/switch/scsgate.py | 4 +- homeassistant/components/switch/template.py | 2 +- homeassistant/components/switch/vera.py | 4 +- .../components/switch/wake_on_lan.py | 2 - homeassistant/components/switch/wemo.py | 4 +- homeassistant/helpers/entity.py | 25 ++++++--- 18 files changed, 80 insertions(+), 56 deletions(-) diff --git a/homeassistant/components/switch/__init__.py b/homeassistant/components/switch/__init__.py index 1f92b458d53..02a313c675f 100644 --- a/homeassistant/components/switch/__init__.py +++ b/homeassistant/components/switch/__init__.py @@ -4,6 +4,7 @@ Component to interface with various switches that can be controlled remotely. For more details about this component, please refer to the documentation at https://home-assistant.io/components/switch/ """ +import asyncio from datetime import timedelta import logging import os @@ -69,38 +70,50 @@ def toggle(hass, entity_id=None): hass.services.call(DOMAIN, SERVICE_TOGGLE, data) -def setup(hass, config): +@asyncio.coroutine +def async_setup(hass, config): """Track states and offer events for switches.""" component = EntityComponent( _LOGGER, DOMAIN, hass, SCAN_INTERVAL, GROUP_NAME_ALL_SWITCHES) - component.setup(config) + yield from component.async_setup(config) - def handle_switch_service(service): + @asyncio.coroutine + def async_handle_switch_service(service): """Handle calls to the switch services.""" - target_switches = component.extract_from_service(service) + target_switches = component.async_extract_from_service(service) + update_tasks = [] for switch in target_switches: if service.service == SERVICE_TURN_ON: - switch.turn_on() + yield from switch.async_turn_on() elif service.service == SERVICE_TOGGLE: - switch.toggle() + yield from switch.async_toggle() else: - switch.turn_off() + yield from switch.async_turn_off() if switch.should_poll: - switch.update_ha_state(True) + update_coro = switch.async_update_ha_state(True) + if hasattr(switch, 'async_update'): + update_tasks.append(hass.loop.create_task(update_coro)) + else: + yield from update_coro - descriptions = load_yaml_config_file( - os.path.join(os.path.dirname(__file__), 'services.yaml')) - hass.services.register(DOMAIN, SERVICE_TURN_OFF, handle_switch_service, - descriptions.get(SERVICE_TURN_OFF), - schema=SWITCH_SERVICE_SCHEMA) - hass.services.register(DOMAIN, SERVICE_TURN_ON, handle_switch_service, - descriptions.get(SERVICE_TURN_ON), - schema=SWITCH_SERVICE_SCHEMA) - hass.services.register(DOMAIN, SERVICE_TOGGLE, handle_switch_service, - descriptions.get(SERVICE_TOGGLE), - schema=SWITCH_SERVICE_SCHEMA) + if update_tasks: + yield from asyncio.wait(update_tasks, loop=hass.loop) + + descriptions = yield from hass.loop.run_in_executor( + None, load_yaml_config_file, os.path.join( + os.path.dirname(__file__), 'services.yaml')) + + hass.services.async_register( + DOMAIN, SERVICE_TURN_OFF, async_handle_switch_service, + descriptions.get(SERVICE_TURN_OFF), schema=SWITCH_SERVICE_SCHEMA) + hass.services.async_register( + DOMAIN, SERVICE_TURN_ON, async_handle_switch_service, + descriptions.get(SERVICE_TURN_ON), schema=SWITCH_SERVICE_SCHEMA) + hass.services.async_register( + DOMAIN, SERVICE_TOGGLE, async_handle_switch_service, + descriptions.get(SERVICE_TOGGLE), schema=SWITCH_SERVICE_SCHEMA) return True diff --git a/homeassistant/components/switch/command_line.py b/homeassistant/components/switch/command_line.py index 1ea108559e6..0fe804d71a3 100644 --- a/homeassistant/components/switch/command_line.py +++ b/homeassistant/components/switch/command_line.py @@ -149,11 +149,11 @@ class CommandSwitch(SwitchDevice): if (CommandSwitch._switch(self._command_on) and not self._command_state): self._state = True - self.update_ha_state() + self.shedule_update_ha_state() def turn_off(self, **kwargs): """Turn the device off.""" if (CommandSwitch._switch(self._command_off) and not self._command_state): self._state = False - self.update_ha_state() + self.shedule_update_ha_state() diff --git a/homeassistant/components/switch/demo.py b/homeassistant/components/switch/demo.py index f867473d441..bc5e90cefb4 100644 --- a/homeassistant/components/switch/demo.py +++ b/homeassistant/components/switch/demo.py @@ -66,9 +66,9 @@ class DemoSwitch(SwitchDevice): def turn_on(self, **kwargs): """Turn the switch on.""" self._state = True - self.update_ha_state() + self.shedule_update_ha_state() def turn_off(self, **kwargs): """Turn the device off.""" self._state = False - self.update_ha_state() + self.shedule_update_ha_state() diff --git a/homeassistant/components/switch/enocean.py b/homeassistant/components/switch/enocean.py index 87a89d148ab..84c126076e3 100644 --- a/homeassistant/components/switch/enocean.py +++ b/homeassistant/components/switch/enocean.py @@ -79,4 +79,4 @@ class EnOceanSwitch(enocean.EnOceanDevice, ToggleEntity): def value_changed(self, val): """Update the internal state of the switch.""" self._on_state = val - self.update_ha_state() + self.shedule_update_ha_state() diff --git a/homeassistant/components/switch/flux.py b/homeassistant/components/switch/flux.py index bd226ac087a..a781d72fa77 100644 --- a/homeassistant/components/switch/flux.py +++ b/homeassistant/components/switch/flux.py @@ -137,7 +137,7 @@ class FluxSwitch(SwitchDevice): self._state = True self.unsub_tracker = track_utc_time_change(self.hass, self.flux_update, second=[0, 30]) - self.update_ha_state() + self.shedule_update_ha_state() def turn_off(self, **kwargs): """Turn off flux.""" @@ -146,7 +146,7 @@ class FluxSwitch(SwitchDevice): self.unsub_tracker = None self._state = False - self.update_ha_state() + self.shedule_update_ha_state() def flux_update(self, now=None): """Update all the lights using flux.""" diff --git a/homeassistant/components/switch/knx.py b/homeassistant/components/switch/knx.py index e290f3ba4e1..1e02fd35844 100644 --- a/homeassistant/components/switch/knx.py +++ b/homeassistant/components/switch/knx.py @@ -40,7 +40,7 @@ class KNXSwitch(KNXGroupAddress, SwitchDevice): self.group_write(1) self._state = [1] if not self.should_poll: - self.update_ha_state() + self.shedule_update_ha_state() def turn_off(self, **kwargs): """Turn the switch off. @@ -50,4 +50,4 @@ class KNXSwitch(KNXGroupAddress, SwitchDevice): self.group_write(0) self._state = [0] if not self.should_poll: - self.update_ha_state() + self.shedule_update_ha_state() diff --git a/homeassistant/components/switch/mqtt.py b/homeassistant/components/switch/mqtt.py index 2283b8539ba..76ba9bf28bc 100644 --- a/homeassistant/components/switch/mqtt.py +++ b/homeassistant/components/switch/mqtt.py @@ -117,7 +117,7 @@ class MqttSwitch(SwitchDevice): if self._optimistic: # Optimistically assume that switch has changed state. self._state = True - self.update_ha_state() + self.shedule_update_ha_state() def turn_off(self, **kwargs): """Turn the device off.""" @@ -126,4 +126,4 @@ class MqttSwitch(SwitchDevice): if self._optimistic: # Optimistically assume that switch has changed state. self._state = False - self.update_ha_state() + self.shedule_update_ha_state() diff --git a/homeassistant/components/switch/mysensors.py b/homeassistant/components/switch/mysensors.py index 6c26a79f21a..04282a4fa66 100644 --- a/homeassistant/components/switch/mysensors.py +++ b/homeassistant/components/switch/mysensors.py @@ -137,7 +137,7 @@ class MySensorsSwitch(mysensors.MySensorsDeviceEntity, SwitchDevice): if self.gateway.optimistic: # optimistically assume that switch has changed state self._values[self.value_type] = STATE_ON - self.update_ha_state() + self.shedule_update_ha_state() def turn_off(self): """Turn the switch off.""" @@ -146,7 +146,7 @@ class MySensorsSwitch(mysensors.MySensorsDeviceEntity, SwitchDevice): if self.gateway.optimistic: # optimistically assume that switch has changed state self._values[self.value_type] = STATE_OFF - self.update_ha_state() + self.shedule_update_ha_state() class MySensorsIRSwitch(MySensorsSwitch): @@ -182,7 +182,7 @@ class MySensorsIRSwitch(MySensorsSwitch): # optimistically assume that switch has changed state self._values[self.value_type] = self._ir_code self._values[set_req.V_LIGHT] = STATE_ON - self.update_ha_state() + self.shedule_update_ha_state() # turn off switch after switch was turned on self.turn_off() @@ -198,7 +198,7 @@ class MySensorsIRSwitch(MySensorsSwitch): if self.gateway.optimistic: # optimistically assume that switch has changed state self._values[set_req.V_LIGHT] = STATE_OFF - self.update_ha_state() + self.shedule_update_ha_state() def update(self): """Update the controller with the latest value from a sensor.""" diff --git a/homeassistant/components/switch/netio.py b/homeassistant/components/switch/netio.py index 9d292cc1b9a..151a1001e38 100644 --- a/homeassistant/components/switch/netio.py +++ b/homeassistant/components/switch/netio.py @@ -156,7 +156,7 @@ class NetioSwitch(SwitchDevice): val[self.outlet - 1] = '1' if value else '0' self.netio.get('port list %s' % ''.join(val)) self.netio.states[self.outlet - 1] = value - self.update_ha_state() + self.shedule_update_ha_state() @property def is_on(self): diff --git a/homeassistant/components/switch/pilight.py b/homeassistant/components/switch/pilight.py index 80a36756d79..2f0e81c753d 100644 --- a/homeassistant/components/switch/pilight.py +++ b/homeassistant/components/switch/pilight.py @@ -127,11 +127,11 @@ class PilightSwitch(SwitchDevice): self._hass.services.call(pilight.DOMAIN, pilight.SERVICE_NAME, self._code_on, blocking=True) self._state = True - self.update_ha_state() + self.shedule_update_ha_state() def turn_off(self): """Turn the switch on by calling pilight.send service with off code.""" self._hass.services.call(pilight.DOMAIN, pilight.SERVICE_NAME, self._code_off, blocking=True) self._state = False - self.update_ha_state() + self.shedule_update_ha_state() diff --git a/homeassistant/components/switch/pulseaudio_loopback.py b/homeassistant/components/switch/pulseaudio_loopback.py index 250111ecfb5..b8584ab232b 100644 --- a/homeassistant/components/switch/pulseaudio_loopback.py +++ b/homeassistant/components/switch/pulseaudio_loopback.py @@ -170,7 +170,7 @@ class PALoopbackSwitch(SwitchDevice): self._pa_svr.update_module_state(no_throttle=True) self._module_idx = self._pa_svr.get_module_idx( self._sink_name, self._source_name) - self.update_ha_state() + self.shedule_update_ha_state() else: _LOGGER.warning(IGNORED_SWITCH_WARN) @@ -181,7 +181,7 @@ class PALoopbackSwitch(SwitchDevice): self._pa_svr.update_module_state(no_throttle=True) self._module_idx = self._pa_svr.get_module_idx( self._sink_name, self._source_name) - self.update_ha_state() + self.shedule_update_ha_state() else: _LOGGER.warning(IGNORED_SWITCH_WARN) diff --git a/homeassistant/components/switch/rpi_gpio.py b/homeassistant/components/switch/rpi_gpio.py index c6400432aa2..a08e6f86242 100644 --- a/homeassistant/components/switch/rpi_gpio.py +++ b/homeassistant/components/switch/rpi_gpio.py @@ -77,10 +77,10 @@ class RPiGPIOSwitch(ToggleEntity): """Turn the device on.""" rpi_gpio.write_output(self._port, 0 if self._invert_logic else 1) self._state = True - self.update_ha_state() + self.shedule_update_ha_state() def turn_off(self): """Turn the device off.""" rpi_gpio.write_output(self._port, 1 if self._invert_logic else 0) self._state = False - self.update_ha_state() + self.shedule_update_ha_state() diff --git a/homeassistant/components/switch/scsgate.py b/homeassistant/components/switch/scsgate.py index c1d5b19fdee..551704d032f 100644 --- a/homeassistant/components/switch/scsgate.py +++ b/homeassistant/components/switch/scsgate.py @@ -119,7 +119,7 @@ class SCSGateSwitch(SwitchDevice): ToggleStatusTask(target=self._scs_id, toggled=True)) self._toggled = True - self.update_ha_state() + self.shedule_update_ha_state() def turn_off(self, **kwargs): """Turn the device off.""" @@ -129,7 +129,7 @@ class SCSGateSwitch(SwitchDevice): ToggleStatusTask(target=self._scs_id, toggled=False)) self._toggled = False - self.update_ha_state() + self.shedule_update_ha_state() def process_event(self, message): """Handle a SCSGate message related with this switch.""" diff --git a/homeassistant/components/switch/template.py b/homeassistant/components/switch/template.py index 83bd1aff9b1..f17d95b21b3 100644 --- a/homeassistant/components/switch/template.py +++ b/homeassistant/components/switch/template.py @@ -92,7 +92,7 @@ class SwitchTemplate(SwitchDevice): @callback def template_switch_state_listener(entity, old_state, new_state): """Called when the target device changes state.""" - hass.async_add_job(self.async_update_ha_state, True) + hass.async_add_job(self.async_update_ha_state(True)) async_track_state_change( hass, entity_ids, template_switch_state_listener) diff --git a/homeassistant/components/switch/vera.py b/homeassistant/components/switch/vera.py index a8b360e2339..21d386ddadb 100644 --- a/homeassistant/components/switch/vera.py +++ b/homeassistant/components/switch/vera.py @@ -36,13 +36,13 @@ class VeraSwitch(VeraDevice, SwitchDevice): """Turn device on.""" self.vera_device.switch_on() self._state = STATE_ON - self.update_ha_state() + self.shedule_update_ha_state() def turn_off(self, **kwargs): """Turn device off.""" self.vera_device.switch_off() self._state = STATE_OFF - self.update_ha_state() + self.shedule_update_ha_state() @property def current_power_mwh(self): diff --git a/homeassistant/components/switch/wake_on_lan.py b/homeassistant/components/switch/wake_on_lan.py index 29f9ca5096a..e6efc1869af 100644 --- a/homeassistant/components/switch/wake_on_lan.py +++ b/homeassistant/components/switch/wake_on_lan.py @@ -76,13 +76,11 @@ class WOLSwitch(SwitchDevice): def turn_on(self): """Turn the device on.""" self._wol.send_magic_packet(self._mac_address) - self.update_ha_state() def turn_off(self): """Turn the device off if an off action is present.""" if self._off_script is not None: self._off_script.run() - self.update_ha_state() def update(self): """Check if device is on and update the state.""" diff --git a/homeassistant/components/switch/wemo.py b/homeassistant/components/switch/wemo.py index 63b2665449e..986d443fa37 100644 --- a/homeassistant/components/switch/wemo.py +++ b/homeassistant/components/switch/wemo.py @@ -150,14 +150,14 @@ class WemoSwitch(SwitchDevice): def turn_on(self, **kwargs): """Turn the switch on.""" self._state = WEMO_ON - self.update_ha_state() self.wemo.on() + self.shedule_update_ha_state() def turn_off(self): """Turn the switch off.""" self._state = WEMO_OFF - self.update_ha_state() self.wemo.off() + self.shedule_update_ha_state() def update(self): """Update WeMo state.""" diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index ac058f89143..8556d028062 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -1,6 +1,7 @@ """An abstract class for entities.""" import asyncio import logging +import functools as ft from timeit import default_timer as timer from typing import Any, Optional, List, Dict @@ -269,6 +270,18 @@ class Entity(object): self.hass.states.async_set( self.entity_id, state, attr, self.force_update) + def shedule_update_ha_state(self, force_refresh=False): + """Shedule a update ha state change task. + + That is only needed on executor to not block. + """ + # We're already in a thread, do the force refresh here. + if force_refresh and not hasattr(self, 'async_update'): + self.update() + force_refresh = False + + self.hass.add_job(self.async_update_ha_state(force_refresh)) + def remove(self) -> None: """Remove entitiy from HASS.""" run_coroutine_threadsafe( @@ -324,23 +337,23 @@ class ToggleEntity(Entity): def turn_on(self, **kwargs) -> None: """Turn the entity on.""" - run_coroutine_threadsafe(self.async_turn_on(**kwargs), - self.hass.loop).result() + raise NotImplementedError() @asyncio.coroutine def async_turn_on(self, **kwargs): """Turn the entity on.""" - raise NotImplementedError() + yield from self.hass.loop.run_in_executor( + None, ft.partial(self.turn_on, **kwargs)) def turn_off(self, **kwargs) -> None: """Turn the entity off.""" - run_coroutine_threadsafe(self.async_turn_off(**kwargs), - self.hass.loop).result() + raise NotImplementedError() @asyncio.coroutine def async_turn_off(self, **kwargs): """Turn the entity off.""" - raise NotImplementedError() + yield from self.hass.loop.run_in_executor( + None, ft.partial(self.turn_off, **kwargs)) def toggle(self) -> None: """Toggle the entity.""" From 7bdb79bd5492ebd46c1a3f681645946e351b4d10 Mon Sep 17 00:00:00 2001 From: Sean Dague Date: Wed, 16 Nov 2016 00:14:54 -0500 Subject: [PATCH 086/127] bump phue to 0.9 (#4404) This increases the phue library to 0.9, which includes some basic Scene support that could be consumed from home assistant. --- homeassistant/components/light/hue.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/light/hue.py b/homeassistant/components/light/hue.py index 249cc30498f..51ff77f2e2d 100644 --- a/homeassistant/components/light/hue.py +++ b/homeassistant/components/light/hue.py @@ -26,7 +26,7 @@ from homeassistant.const import (CONF_FILENAME, CONF_HOST, DEVICE_DEFAULT_NAME) from homeassistant.loader import get_component import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['phue==0.8'] +REQUIREMENTS = ['phue==0.9'] # Track previously setup bridges _CONFIGURED_BRIDGES = {} diff --git a/requirements_all.txt b/requirements_all.txt index 243656f06d5..bf8f5a24c4b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -299,7 +299,7 @@ panasonic_viera==0.2 pexpect==4.0.1 # homeassistant.components.light.hue -phue==0.8 +phue==0.9 # homeassistant.components.pilight pilight==0.1.1 From 2b86d89bb4c20ef51a26eff82d05033d1ffd8bd7 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 15 Nov 2016 21:25:54 -0800 Subject: [PATCH 087/127] Fix tplink test --- tests/components/device_tracker/test_tplink.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/components/device_tracker/test_tplink.py b/tests/components/device_tracker/test_tplink.py index da6243e6eff..6c424033a8a 100644 --- a/tests/components/device_tracker/test_tplink.py +++ b/tests/components/device_tracker/test_tplink.py @@ -31,7 +31,7 @@ class TestTplink4DeviceScanner(unittest.TestCase): """Test grabbing the mac addresses from 2.4 and 5 GHz clients pages.""" conf_dict = { CONF_PLATFORM: 'tplink', - CONF_HOST: 'fake_host', + CONF_HOST: 'fake-host', CONF_USERNAME: 'fake_user', CONF_PASSWORD: 'fake_pass' } From c06c82905ae50aca256917019e5447db84fdd144 Mon Sep 17 00:00:00 2001 From: Sean Dague Date: Wed, 16 Nov 2016 00:56:40 -0500 Subject: [PATCH 088/127] dynamically fetch yamaha media playback support (#4385) This makes it so that media playback support for inputs is dynamically fetched from the receiver, instead of assuming that all playback commands work for all inputs. Tests are added for this, using a FakeYamaha class, which has some sample data stubbed in for key methods that need to be called. We also include an example of the desc.xml needed to dynamically parse these features for these tests (as this is done in platform init). --- .../components/media_player/yamaha.py | 21 +- requirements_all.txt | 2 +- tests/components/media_player/test_yamaha.py | 87 + .../media_player/yamaha_samples/desc.xml | 3441 +++++++++++++++++ 4 files changed, 3541 insertions(+), 10 deletions(-) create mode 100644 tests/components/media_player/test_yamaha.py create mode 100644 tests/components/media_player/yamaha_samples/desc.xml diff --git a/homeassistant/components/media_player/yamaha.py b/homeassistant/components/media_player/yamaha.py index 0e265199fce..68c491d7a24 100644 --- a/homeassistant/components/media_player/yamaha.py +++ b/homeassistant/components/media_player/yamaha.py @@ -18,17 +18,12 @@ from homeassistant.const import (CONF_NAME, CONF_HOST, STATE_OFF, STATE_ON, STATE_PLAYING, STATE_IDLE) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['rxv==0.3.1'] +REQUIREMENTS = ['rxv==0.4.0'] _LOGGER = logging.getLogger(__name__) SUPPORT_YAMAHA = SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ - SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_SELECT_SOURCE | \ - SUPPORT_PLAY_MEDIA - -# Only supported by some sources -SUPPORT_PLAYBACK = SUPPORT_PLAY_MEDIA | SUPPORT_PAUSE | SUPPORT_STOP | \ - SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK + SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_SELECT_SOURCE CONF_SOURCE_NAMES = 'source_names' CONF_SOURCE_IGNORE = 'source_ignore' @@ -187,8 +182,16 @@ class YamahaDevice(MediaPlayerDevice): def supported_media_commands(self): """Flag of media commands that are supported.""" supported_commands = SUPPORT_YAMAHA - if self._is_playback_supported: - supported_commands |= SUPPORT_PLAYBACK + + supports = self._receiver.get_playback_support() + mapping = {'play': SUPPORT_PLAY_MEDIA, + 'pause': SUPPORT_PAUSE, + 'stop': SUPPORT_STOP, + 'skip_f': SUPPORT_NEXT_TRACK, + 'skip_r': SUPPORT_PREVIOUS_TRACK} + for attr, feature in mapping.items(): + if getattr(supports, attr, False): + supported_commands |= feature return supported_commands def turn_off(self): diff --git a/requirements_all.txt b/requirements_all.txt index bf8f5a24c4b..13b8c7bfb70 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -465,7 +465,7 @@ radiotherm==1.2 # rpi-rf==0.9.5 # homeassistant.components.media_player.yamaha -rxv==0.3.1 +rxv==0.4.0 # homeassistant.components.media_player.samsungtv samsungctl==0.5.1 diff --git a/tests/components/media_player/test_yamaha.py b/tests/components/media_player/test_yamaha.py new file mode 100644 index 00000000000..de31096e2b8 --- /dev/null +++ b/tests/components/media_player/test_yamaha.py @@ -0,0 +1,87 @@ +"""The tests for the Yamaha Media player platform.""" +import unittest +import xml.etree.ElementTree as ET + +import rxv + + +def sample_content(name): + """Read content into a string from a file.""" + with open('tests/components/media_player/yamaha_samples/%s' % name, + encoding='utf-8') as f: + return f.read() + + +class FakeYamaha(rxv.rxv.RXV): + """Fake Yamaha receiver. + + This inherits from RXV but overrides methods for testing that + would normally have hit the network. This makes it easier to + ensure that usage of the rxv library by HomeAssistant is as we'd + expect. + """ + _fake_input = "HDMI1" + + def _discover_features(self): + self._desc_xml = ET.fromstring(sample_content("desc.xml")) + + @property + def input(self): + return self._fake_input + + @input.setter + def input(self, input_name): + assert input_name in self.inputs() + self._fake_input = input_name + + def inputs(self): + return {'AUDIO1': None, + 'AUDIO2': None, + 'AV1': None, + 'AV2': None, + 'AV3': None, + 'AV4': None, + 'AV5': None, + 'AV6': None, + 'AirPlay': 'AirPlay', + 'HDMI1': None, + 'HDMI2': None, + 'HDMI3': None, + 'HDMI4': None, + 'HDMI5': None, + 'NET RADIO': 'NET_RADIO', + 'Pandora': 'Pandora', + 'Rhapsody': 'Rhapsody', + 'SERVER': 'SERVER', + 'SiriusXM': 'SiriusXM', + 'Spotify': 'Spotify', + 'TUNER': 'Tuner', + 'USB': 'USB', + 'V-AUX': None, + 'iPod (USB)': 'iPod_USB'} + + +class TestYamaha(unittest.TestCase): + """Test the media_player yamaha module.""" + + def setUp(self): # pylint: disable=invalid-name + """Setup things to be run when tests are started.""" + super(TestYamaha, self).setUp() + self.rec = FakeYamaha('10.0.0.0') + + def test_get_playback_support(self): + rec = self.rec + support = rec.get_playback_support() + self.assertFalse(support.play) + self.assertFalse(support.pause) + self.assertFalse(support.stop) + self.assertFalse(support.skip_f) + self.assertFalse(support.skip_r) + + rec.input = "NET RADIO" + support = rec.get_playback_support() + self.assertTrue(support.play) + self.assertFalse(support.pause) + self.assertTrue(support.stop) + self.assertFalse(support.skip_f) + self.assertFalse(support.skip_r) diff --git a/tests/components/media_player/yamaha_samples/desc.xml b/tests/components/media_player/yamaha_samples/desc.xml new file mode 100644 index 00000000000..d403fade5f7 --- /dev/null +++ b/tests/components/media_player/yamaha_samples/desc.xml @@ -0,0 +1,3441 @@ + + + Title_1 + + + On + Off + + Param_1 + + On + Off + + + + + + + Param_1 + + Available + Unavailable + + + + + + + Param_1 + + 1,15,UTF-8 + + + + Param_1 + + 1,15,UTF-8 + + + + + + On + Standby + + + On + Off + + Param_1 + + On + Off + + + + + + Disable + Enable + + Param_1 + + Disable + Enable + + + + + System,Misc,Event,Notice + System,Power_Control,Power + System,Misc,Network,Network_Name + System,Misc,Network,Network_Standby + System,Misc,Network,DMC_Control + System,Misc,Event,Notice + System,Misc,Network,Network_Name + System,Misc,Network,Network_Standby + System,Misc,Update,Yamaha_Network_Site,Status + System,Misc,Network,DMC_Control + + + + + + Param_1 + + 1,9,Latin-1 + + + + Name,Zone=Param_1 + + 1,9,Latin-1 + + + + + + + Param_1 + + + + + + Input,Input_Sel=Param_1 + + + + + + + + Param_1 + + + + + + Input,Input_Sel=Param_1 + + + + + + + + + + Val=Param_1:Exp=Param_2:Unit=Param_3 + + -805,165,5 + + + 1 + + + dB + + + + Volume,Lvl,Val=Param_1:Volume,Lvl,Exp=Param_2:Volume,Lvl,Unit=Param_3 + + -805,165,5 + + + 1 + + + dB + + + + + On + Off + + Volume,Mute=Param_1 + + On + Off + + + + + + + On + Standby + + Power_Control,Power=Param_1 + + On + Standby + + + + + Last + 120 min + 90 min + 60 min + 30 min + Off + + Power_Control,Sleep=Param_1 + + 120 min + 90 min + 60 min + 30 min + Off + + + + + + + Play + Pause + Stop + + + Skip Fwd + Skip Rev + + + + + Up + Down + Left + Right + Return + Sel + Return to Home + On Screen + Top Menu + Menu + Option + Display + + + + + + + Param_1 + + Hall in Munich + Hall in Vienna + Chamber + Cellar Club + The Roxy Theatre + The Bottom Line + Sports + Action Game + Roleplaying Game + Music Video + Standard + Spectacle + Sci-Fi + Adventure + Drama + Mono Movie + Surround Decoder + 2ch Stereo + 7ch Stereo + + + + Surround,Program_Sel,Current,Sound_Program=Param_1 + + Hall in Munich + Hall in Vienna + Chamber + Cellar Club + The Roxy Theatre + The Bottom Line + Sports + Action Game + Roleplaying Game + Music Video + Standard + Spectacle + Sci-Fi + Adventure + Drama + Mono Movie + Surround Decoder + 2ch Stereo + 7ch Stereo + + + + + On + Off + + Surround,Program_Sel,Current,Straight=Param_1 + + On + Off + + + + + On + Off + + Surround,Program_Sel,Current,Enhancer=Param_1 + + On + Off + + + + + + + + Val=Param_1:Exp=Param_2:Unit=Param_3 + + -60,60,5 + + + 1 + + + dB + + + + Sound_Video,Tone,Bass,Val=Param_1:Sound_Video,Tone,Bass,Exp=Param_2:Sound_Video,Tone,Bass,Unit=Param_3 + + -60,60,5 + + + 1 + + + dB + + + + + + Val=Param_1:Exp=Param_2:Unit=Param_3 + + -60,60,5 + + + 1 + + + dB + + + + Sound_Video,Tone,Treble,Val=Param_1:Sound_Video,Tone,Treble,Exp=Param_2:Sound_Video,Tone,Treble,Unit=Param_3 + + -60,60,5 + + + 1 + + + dB + + + + + + + Val=Param_1:Exp=Param_2:Unit=Param_3 + + -60,60,5 + + + 1 + + + dB + + + + Volume,Subwoofer_Trim,Val=Param_1:Volume,Subwoofer_Trim,Exp=Param_2:Volume,Subwoofer_Trim,Unit=Param_3 + + -60,60,5 + + + 1 + + + dB + + + + + Auto + Off + + Sound_Video,Adaptive_DRC=Param_1 + + Auto + Off + + + + + Auto + Off + + Surround,_3D_Cinema_DSP=Param_1 + + Auto + Off + + + + + + Param_1 + + 0,5,1 + + + + Sound_Video,Dialogue_Adjust,Dialogue_Lift=Param_1 + + 0,5,1 + + + + + + Param_1 + + 0,3,1 + + + + Sound_Video,Dialogue_Adjust,Dialogue_Lvl=Param_1 + + 0,3,1 + + + + + On + Off + + Sound_Video,Pure_Direct,Mode=Param_1 + + On + Off + + + + + + Sound_Video,HDMI,Standby_Through_Info=Param_1 + + On + Off + + + + + + Main_Zone,Power_Control,Power + Main_Zone,Volume,Lvl + Main_Zone,Volume,Mute + Main_Zone,Input,Input_Sel + Main_Zone,Config,Name,Zone + Main_Zone,Scene,Scene_Sel + Main_Zone,Sound_Video,Tone,Bass + Main_Zone,Sound_Video,Tone,Treble + Main_Zone,Surround,Program_Sel,Current,Sound_Program + Main_Zone,Surround,Program_Sel,Current,Straight + Main_Zone,Surround,Program_Sel,Current,Enhancer + Main_Zone,Sound_Video,Adaptive_DRC + Main_Zone,Surround,_3D_Cinema_DSP + Main_Zone,Sound_Video,Dialogue_Adjust,Dialogue_Lift + System,Sound_Video,HDMI,Video,Preset_Sel,Current + Main_Zone,Sound_Video,Pure_Direct,Mode + Main_Zone,Cursor_Control,Cursor + Main_Zone,Cursor_Control,Menu_Control + Main_Zone,Surround,Enhancer_Type + Main_Zone,Sound_Video,Dialogue_Adjust,Dialogue_Lvl + Main_Zone,Volume,Subwoofer_Trim + Main_Zone,Power_Control,Sleep + Main_Zone,Play_Control,Playback + Main_Zone,Basic_Status + Main_Zone,Input,Input_Sel_Item + Main_Zone,Config + Main_Zone,Scene,Scene_Sel_Item + + + + + + Param_1 + + 1,9,Latin-1 + + + + Name,Zone=Param_1 + + 1,9,Latin-1 + + + + + + Param_1 + + + + + + Input,Input_Sel=Param_1 + + + + + + + + + Val=Param_1:Exp=Param_2:Unit=Param_3 + + -805,165,5 + + + 1 + + + dB + + + + Volume,Lvl,Val=Param_1:Volume,Lvl,Exp=Param_2:Volume,Lvl,Unit=Param_3 + + -805,165,5 + + + 1 + + + dB + + + + + On + Off + + Volume,Mute=Param_1 + + On + Off + + + + + + Volume,Output_Info=Param_1 + + Fixed + Variable + + + + + + + On + Standby + + Power_Control,Power=Param_1 + + On + Standby + + + + + Last + 120 min + 90 min + 60 min + 30 min + Off + + Power_Control,Sleep=Param_1 + + 120 min + 90 min + 60 min + 30 min + Off + + + + + + + Play + Pause + Stop + + + Skip Fwd + Skip Rev + + + + Zone_2,Power_Control,Power + Zone_2,Volume,Lvl + Zone_2,Volume,Mute + Zone_2,Input,Input_Sel + Zone_2,Config,Name,Zone + Zone_2,Scene,Scene_Sel + Zone_2,Sound_Video,Tone,Bass + Zone_2,Sound_Video,Tone,Treble + Zone_2,Cursor_Control,Cursor + Zone_2,Cursor_Control,Menu_Control + Zone_2,Volume,Output + Zone_2,Power_Control,Sleep + Zone_2,Play_Control,Playback + Zone_2,Basic_Status + Zone_2,Input,Input_Sel_Item + Zone_2,Config + Zone_2,Scene,Scene_Sel_Item + + + + + + Feature_Availability=Param_1 + + Ready + Not Ready + + + + + + + Auto Up + Auto Down + Cancel + + Tuning,Freq,AM,Val=Param_1:Tuning,Freq,AM,Exp=Param_2:Tuning,Freq,AM,Unit=Param_3 + + 530,1710,10 + Auto Up + Auto Down + + + 0 + + + + + kHz + + + + + + + Auto Up + Auto Down + Cancel + + Tuning,Freq,FM,Val=Param_1:Tuning,Freq,FM,Exp=Param_2:Tuning,Freq,FM,Unit=Param_3 + + 8750,10790,20 + Auto Up + Auto Down + + + 2 + + + + + MHz + + + + + + + Up + Down + + Preset,Preset_Sel=Param_1 + + + + + + + + AM + FM + + Tuning,Band=Param_1 + + AM + FM + + + + + + + Val=Param_1:Exp=Param_2:Unit=Param_3 + + 530,1710,10 + + + 0 + + + kHz + + + + Tuning,Freq,AM,Val=Param_1:Tuning,Freq,AM,Exp=Param_2:Tuning,Freq,AM,Unit=Param_3 + + 530,1710,10 + Auto Up + Auto Down + + + 0 + + + + + kHz + + + + + + + + Val=Param_1:Exp=Param_2:Unit=Param_3 + + 8750,10790,20 + + + 2 + + + MHz + + + + Tuning,Freq,FM,Val=Param_1:Tuning,Freq,FM,Exp=Param_2:Tuning,Freq,FM,Unit=Param_3 + + 8750,10790,20 + Auto Up + Auto Down + + + 2 + + + + + MHz + + + + + + + + + Param_1 + + + + + + Preset,Preset_Sel=Param_1 + + + + + + + + + + Tuning,Band=Param_1 + + AM + FM + + + + + + Tuning,Freq,Current,Val=Param_1:Tuning,Freq,Current,Exp=Param_2:Tuning,Freq,Current,Unit=Param_3 + + 530,1710,10 + 8750,10790,20 + Auto Up + Auto Down + + + 0 + 2 + + + + + kHz + MHz + + + + + + + + + Signal_Info,Tuned=Param_1 + + Negate + Assert + + + + + + Signal_Info,Stereo=Param_1 + + Negate + Assert + + + + + + + Tuner,Play_Control,Search_Mode + Tuner,Play_Control,Preset,Preset_Sel + Tuner,Play_Control,Tuning,Band + Tuner,Play_Control,Tuning,Freq,FM + Tuner,Play_Control,Tuning,Freq,AM + Tuner,Play_Control,Tuning,Freq,FM,Val + Tuner,Play_Control,Tuning,Freq,AM,Val + Tuner,Play_Info + Tuner,Config + Tuner,Play_Control,Preset,Preset_Sel_Item + + + + + + Feature_Availability=Param_1 + + Ready + Not Ready + + + + + + Play + Pause + + Playback_Info=Param_1 + + Play + Stop + + + + + Skip Fwd + Skip Rev + + + + + + Meta_Info,Artist=Param_1 + + 0,128,UTF-8 + + + + + + Meta_Info,Album=Param_1 + + 0,128,UTF-8 + + + + + + Meta_Info,Song=Param_1 + + 0,128,UTF-8 + + + + + + Feature_Availability=Param_1 + + Ready + Not Ready + + + + + + Playback_Info=Param_1 + + Play + Stop + + + + + + Album_ART,URL=Param_1 + + 0,128,UTF-8 + + + + + + Album_ART,ID=Param_1 + + 0,255,1 + + + + + + Album_ART,Format=Param_1 + + BMP + YMF + + + + + + Input_Logo,URL_S=Param_1 + + 0,128,UTF-8 + + + + + + AirPlay,Play_Control,Playback + AirPlay,Play_Info + AirPlay,Config + + + + + + Feature_Availability=Param_1 + + Ready + Not Ready + + + + + + Play + Pause + + Playback_Info=Param_1 + + Play + Pause + Stop + + + + + Skip Fwd + Skip Rev + + + + + + Meta_Info,Artist=Param_1 + + 0,128,UTF-8 + + + + + + Meta_Info,Album=Param_1 + + 0,128,UTF-8 + + + + + + Meta_Info,Track=Param_1 + + 0,128,UTF-8 + + + + + + Feature_Availability=Param_1 + + Ready + Not Ready + + + + + + Playback_Info=Param_1 + + Play + Pause + Stop + + + + + + Input_Logo,URL_S=Param_1 + + 0,128,UTF-8 + + + + + + Spotify,Play_Control,Playback + Spotify,Play_Control,Play_Mode,Repeat + Spotify,Play_Control,Play_Mode,Shuffle + Spotify,Play_Info + Spotify,Config + + + + + Extended + + + + Feature_Availability=Param_1 + + Ready + Not Ready + + + + + + Off + One + All + + Play_Mode,Repeat=Param_1 + + Off + One + All + + + + + Off + Songs + Albums + + Play_Mode,Shuffle=Param_1 + + Off + Songs + Albums + + + + + Play + Pause + Stop + + Playback_Info=Param_1 + + Play + Pause + Stop + + + + + Skip Fwd + Skip Rev + + + + + + Meta_Info,Artist=Param_1 + + 0,128,UTF-8 + + + + + + Meta_Info,Album=Param_1 + + 0,128,UTF-8 + + + + + + Meta_Info,Song=Param_1 + + 0,128,UTF-8 + + + + + + Feature_Availability=Param_1 + + Ready + Not Ready + + + + + + Playback_Info=Param_1 + + Play + Pause + Stop + + + + + + Play_Mode,Repeat=Param_1 + + Off + One + All + + + + + + Play_Mode,Shuffle=Param_1 + + Off + Songs + Albums + + + + + + Album_ART,URL=Param_1 + + 0,128,UTF-8 + + + + + + Album_ART,ID=Param_1 + + 0,255,1 + + + + + + Album_ART,Format=Param_1 + + BMP + YMF + + + + + + + + + Param_1 + + 1,8,1,Line_% + + + + + Up + Down + Return + Sel + Return to Home + + + + Param_1 + + 1,65536,1 + + + + + Up + Down + + + + + + Menu_Status=Param_1 + + Ready + Busy + + + + + + Menu_Layer=Param_1 + + 1,16,1 + + + + + + Menu_Name=Param_1 + + 0,128,UTF-8 + + + + + + Line_1 + + Current_List,Line_1,Txt=Param_1:Current_List,Line_1,Attribute=Param_2 + + 0,128,UTF-8 + + + Container + Unplayable Item + Item + Unselectable + + + + + Line_2 + + Current_List,Line_2,Txt=Param_1:Current_List,Line_2,Attribute=Param_2 + + 0,128,UTF-8 + + + Container + Unplayable Item + Item + Unselectable + + + + + Line_3 + + Current_List,Line_3,Txt=Param_1:Current_List,Line_3,Attribute=Param_2 + + 0,128,UTF-8 + + + Container + Unplayable Item + Item + Unselectable + + + + + Line_4 + + Current_List,Line_4,Txt=Param_1:Current_List,Line_4,Attribute=Param_2 + + 0,128,UTF-8 + + + Container + Unplayable Item + Item + Unselectable + + + + + Line_5 + + Current_List,Line_5,Txt=Param_1:Current_List,Line_5,Attribute=Param_2 + + 0,128,UTF-8 + + + Container + Unplayable Item + Item + Unselectable + + + + + Line_6 + + Current_List,Line_6,Txt=Param_1:Current_List,Line_6,Attribute=Param_2 + + 0,128,UTF-8 + + + Container + Unplayable Item + Item + Unselectable + + + + + Line_7 + + Current_List,Line_7,Txt=Param_1:Current_List,Line_7,Attribute=Param_2 + + 0,128,UTF-8 + + + Container + Unplayable Item + Item + Unselectable + + + + + Line_8 + + Current_List,Line_8,Txt=Param_1:Current_List,Line_8,Attribute=Param_2 + + 0,128,UTF-8 + + + Container + Unplayable Item + Item + Unselectable + + + + + + + Cursor_Position,Current_Line=Param_1 + + 1,65536,1 + + + + + + Cursor_Position,Max_Line=Param_1 + + 0,65536,1 + + + + + + + iPod_USB,Play_Control,Playback + iPod_USB,List_Control,Direct_Sel + iPod_USB,List_Control,Jump_Line + iPod_USB,List_Control,Cursor + iPod_USB,List_Control,Page + iPod_USB,Play_Control,Play_Mode,Repeat + iPod_USB,Play_Control,Play_Mode,Shuffle + iPod_USB,Play_Control,iPod_Mode + iPod_USB,Play_Info + iPod_USB,List_Info + iPod_USB,Config + + + + + + Feature_Availability=Param_1 + + Ready + Not Ready + + + + + + Off + One + All + + Play_Mode,Repeat=Param_1 + + Off + One + All + + + + + Off + On + + Play_Mode,Shuffle=Param_1 + + Off + On + + + + + Play + Pause + Stop + + Playback_Info=Param_1 + + Play + Pause + Stop + + + + + Skip Fwd + Skip Rev + + + + + + Meta_Info,Artist=Param_1 + + 0,64,UTF-8 + + + + + + Meta_Info,Album=Param_1 + + 0,64,UTF-8 + + + + + + Meta_Info,Song=Param_1 + + 0,64,UTF-8 + + + + + + Feature_Availability=Param_1 + + Ready + Not Ready + + + + + + Playback_Info=Param_1 + + Play + Pause + Stop + + + + + + Play_Mode,Repeat=Param_1 + + Off + One + All + + + + + + Play_Mode,Shuffle=Param_1 + + Off + On + + + + + + Album_ART,URL=Param_1 + + 0,128,UTF-8 + + + + + + Album_ART,ID=Param_1 + + 0,255,1 + + + + + + Album_ART,Format=Param_1 + + BMP + YMF + + + + + + + + + Param_1 + + 1,8,1,Line_% + + + + + Up + Down + Return + Sel + Return to Home + + + + Param_1 + + 1,65536,1 + + + + + Up + Down + + + + + + Menu_Status=Param_1 + + Ready + Busy + + + + + + Menu_Layer=Param_1 + + 1,16,1 + + + + + + Menu_Name=Param_1 + + 0,128,UTF-8 + + + + + + Line_1 + + Current_List,Line_1,Txt=Param_1:Current_List,Line_1,Attribute=Param_2 + + 0,128,UTF-8 + + + Container + Unplayable Item + Item + Unselectable + + + + + Line_2 + + Current_List,Line_2,Txt=Param_1:Current_List,Line_2,Attribute=Param_2 + + 0,128,UTF-8 + + + Container + Unplayable Item + Item + Unselectable + + + + + Line_3 + + Current_List,Line_3,Txt=Param_1:Current_List,Line_3,Attribute=Param_2 + + 0,128,UTF-8 + + + Container + Unplayable Item + Item + Unselectable + + + + + Line_4 + + Current_List,Line_4,Txt=Param_1:Current_List,Line_4,Attribute=Param_2 + + 0,128,UTF-8 + + + Container + Unplayable Item + Item + Unselectable + + + + + Line_5 + + Current_List,Line_5,Txt=Param_1:Current_List,Line_5,Attribute=Param_2 + + 0,128,UTF-8 + + + Container + Unplayable Item + Item + Unselectable + + + + + Line_6 + + Current_List,Line_6,Txt=Param_1:Current_List,Line_6,Attribute=Param_2 + + 0,128,UTF-8 + + + Container + Unplayable Item + Item + Unselectable + + + + + Line_7 + + Current_List,Line_7,Txt=Param_1:Current_List,Line_7,Attribute=Param_2 + + 0,128,UTF-8 + + + Container + Unplayable Item + Item + Unselectable + + + + + Line_8 + + Current_List,Line_8,Txt=Param_1:Current_List,Line_8,Attribute=Param_2 + + 0,128,UTF-8 + + + Container + Unplayable Item + Item + Unselectable + + + + + + + Cursor_Position,Current_Line=Param_1 + + 1,65536,1 + + + + + + Cursor_Position,Max_Line=Param_1 + + 0,65536,1 + + + + + + + USB,Play_Control,Play_Mode,Repeat + USB,Play_Control,Play_Mode,Shuffle + USB,Play_Control,Playback + USB,Play_Control,Preset,Preset_Sel + USB,List_Control,Direct_Sel + USB,List_Control,Jump_Line + USB,List_Control,Cursor + USB,List_Control,Page + USB,Play_Info + USB,List_Info + USB,Config + USB,Play_Control,Preset,Preset_Sel_Item + + + + + + Feature_Availability=Param_1 + + Ready + Not Ready + + + + + + Play + Stop + + Playback_Info=Param_1 + + Play + Stop + + + + + + + + Meta_Info,Station=Param_1 + + 0,128,UTF-8 + + + + + + Meta_Info,Album=Param_1 + + 0,128,UTF-8 + + + + + + Meta_Info,Song=Param_1 + + 0,128,UTF-8 + + + + + + Feature_Availability=Param_1 + + Ready + Not Ready + + + + + + Playback_Info=Param_1 + + Play + Stop + + + + + + Album_ART,URL=Param_1 + + 0,128,UTF-8 + + + + + + Album_ART,ID=Param_1 + + 0,255,1 + + + + + + Album_ART,Format=Param_1 + + BMP + YMF + + + + + + + + + Param_1 + + 1,8,1,Line_% + + + + + Up + Down + Return + Sel + Return to Home + + + + Param_1 + + 1,65536,1 + + + + + Up + Down + + + + + + Menu_Status=Param_1 + + Ready + Busy + + + + + + Menu_Layer=Param_1 + + 1,16,1 + + + + + + Menu_Name=Param_1 + + 0,128,UTF-8 + + + + + + Line_1 + + Current_List,Line_1,Txt=Param_1:Current_List,Line_1,Attribute=Param_2 + + 0,128,UTF-8 + + + Container + Unplayable Item + Item + Unselectable + + + + + Line_2 + + Current_List,Line_2,Txt=Param_1:Current_List,Line_2,Attribute=Param_2 + + 0,128,UTF-8 + + + Container + Unplayable Item + Item + Unselectable + + + + + Line_3 + + Current_List,Line_3,Txt=Param_1:Current_List,Line_3,Attribute=Param_2 + + 0,128,UTF-8 + + + Container + Unplayable Item + Item + Unselectable + + + + + Line_4 + + Current_List,Line_4,Txt=Param_1:Current_List,Line_4,Attribute=Param_2 + + 0,128,UTF-8 + + + Container + Unplayable Item + Item + Unselectable + + + + + Line_5 + + Current_List,Line_5,Txt=Param_1:Current_List,Line_5,Attribute=Param_2 + + 0,128,UTF-8 + + + Container + Unplayable Item + Item + Unselectable + + + + + Line_6 + + Current_List,Line_6,Txt=Param_1:Current_List,Line_6,Attribute=Param_2 + + 0,128,UTF-8 + + + Container + Unplayable Item + Item + Unselectable + + + + + Line_7 + + Current_List,Line_7,Txt=Param_1:Current_List,Line_7,Attribute=Param_2 + + 0,128,UTF-8 + + + Container + Unplayable Item + Item + Unselectable + + + + + Line_8 + + Current_List,Line_8,Txt=Param_1:Current_List,Line_8,Attribute=Param_2 + + 0,128,UTF-8 + + + Container + Unplayable Item + Item + Unselectable + + + + + + + Cursor_Position,Current_Line=Param_1 + + 1,65536,1 + + + + + + Cursor_Position,Max_Line=Param_1 + + 0,65536,1 + + + + + + + NET_RADIO,Play_Control,Playback + NET_RADIO,List_Control,Direct_Sel + NET_RADIO,List_Control,Jump_Line + NET_RADIO,List_Control,Cursor + NET_RADIO,List_Control,Page + NET_RADIO,Play_Control,Preset,Preset_Sel + NET_RADIO,List_Control,Bookmark + NET_RADIO,Play_Info + NET_RADIO,List_Info + NET_RADIO,Config + NET_RADIO,Play_Control,Preset,Preset_Sel_Item + + + + + + Feature_Availability=Param_1 + + Ready + Not Ready + + + + + + Off + One + All + + Play_Mode,Repeat=Param_1 + + Off + One + All + + + + + Off + On + + Play_Mode,Shuffle=Param_1 + + Off + On + + + + + Play + Pause + Stop + + Playback_Info=Param_1 + + Play + Pause + Stop + + + + + Skip Fwd + Skip Rev + + + + + + Meta_Info,Artist=Param_1 + + 0,128,UTF-8 + + + + + + Meta_Info,Album=Param_1 + + 0,128,UTF-8 + + + + + + Meta_Info,Song=Param_1 + + 0,128,UTF-8 + + + + + + Feature_Availability=Param_1 + + Ready + Not Ready + + + + + + Playback_Info=Param_1 + + Play + Pause + Stop + + + + + + Play_Mode,Repeat=Param_1 + + Off + One + All + + + + + + Play_Mode,Shuffle=Param_1 + + Off + On + + + + + + Album_ART,URL=Param_1 + + 0,128,UTF-8 + + + + + + Album_ART,ID=Param_1 + + 0,255,1 + + + + + + Album_ART,Format=Param_1 + + BMP + YMF + + + + + + + + + Param_1 + + 1,8,1,Line_% + + + + + Up + Down + Return + Sel + Return to Home + + + + Param_1 + + 1,65536,1 + + + + + Up + Down + + + + + + Menu_Status=Param_1 + + Ready + Busy + + + + + + Menu_Layer=Param_1 + + 1,16,1 + + + + + + Menu_Name=Param_1 + + 0,128,UTF-8 + + + + + + Line_1 + + Current_List,Line_1,Txt=Param_1:Current_List,Line_1,Attribute=Param_2 + + 0,128,UTF-8 + + + Container + Unplayable Item + Item + Unselectable + + + + + Line_2 + + Current_List,Line_2,Txt=Param_1:Current_List,Line_2,Attribute=Param_2 + + 0,128,UTF-8 + + + Container + Unplayable Item + Item + Unselectable + + + + + Line_3 + + Current_List,Line_3,Txt=Param_1:Current_List,Line_3,Attribute=Param_2 + + 0,128,UTF-8 + + + Container + Unplayable Item + Item + Unselectable + + + + + Line_4 + + Current_List,Line_4,Txt=Param_1:Current_List,Line_4,Attribute=Param_2 + + 0,128,UTF-8 + + + Container + Unplayable Item + Item + Unselectable + + + + + Line_5 + + Current_List,Line_5,Txt=Param_1:Current_List,Line_5,Attribute=Param_2 + + 0,128,UTF-8 + + + Container + Unplayable Item + Item + Unselectable + + + + + Line_6 + + Current_List,Line_6,Txt=Param_1:Current_List,Line_6,Attribute=Param_2 + + 0,128,UTF-8 + + + Container + Unplayable Item + Item + Unselectable + + + + + Line_7 + + Current_List,Line_7,Txt=Param_1:Current_List,Line_7,Attribute=Param_2 + + 0,128,UTF-8 + + + Container + Unplayable Item + Item + Unselectable + + + + + Line_8 + + Current_List,Line_8,Txt=Param_1:Current_List,Line_8,Attribute=Param_2 + + 0,128,UTF-8 + + + Container + Unplayable Item + Item + Unselectable + + + + + + + Cursor_Position,Current_Line=Param_1 + + 1,65536,1 + + + + + + Cursor_Position,Max_Line=Param_1 + + 0,65536,1 + + + + + + + SERVER,Play_Control,Play_Mode,Repeat + SERVER,Play_Control,Play_Mode,Shuffle + SERVER,Play_Control,Playback + SERVER,Play_Control,Preset,Preset_Sel + SERVER,List_Control,Direct_Sel + SERVER,List_Control,Jump_Line + SERVER,List_Control,Cursor + SERVER,List_Control,Page + SERVER,Play_Control,Play_URI + SERVER,Play_Info + SERVER,List_Info + SERVER,Config + SERVER,Play_Control,Preset,Preset_Sel_Item + + + + + + Feature_Availability=Param_1 + + Ready + Not Ready + + + + + + Off + One + All + + Play_Mode,Repeat=Param_1 + + Off + One + All + + + + + Off + On + + Play_Mode,Shuffle=Param_1 + + Off + On + + + + + Play + Pause + Stop + + Playback_Info=Param_1 + + Play + Pause + Stop + + + + + Skip Fwd + Skip Rev + + + + + + Meta_Info,Artist=Param_1 + + 0,128,UTF-8 + + + + + + Meta_Info,Album=Param_1 + + 0,128,UTF-8 + + + + + + Meta_Info,Song=Param_1 + + 0,128,UTF-8 + + + + + + Feature_Availability=Param_1 + + Ready + Not Ready + + + + + + Playback_Info=Param_1 + + Play + Pause + Stop + + + + + + Play_Mode,Repeat=Param_1 + + Off + One + All + + + + + + Play_Mode,Shuffle=Param_1 + + Off + On + + + + + + Album_ART,URL=Param_1 + + 0,128,UTF-8 + + + + + + Album_ART,ID=Param_1 + + 0,255,1 + + + + + + Album_ART,Format=Param_1 + + BMP + YMF + + + + + + Input_Logo,URL_S=Param_1 + + 0,128,UTF-8 + + + + + + + + + Param_1 + + 1,8,1,Line_% + + + + + + Line=Param_1:Keyword=Param_2 + + 1,8,1,Line_% + + + 0,30,Ascii + + + + + Up + Down + Return + Sel + Return to Home + + + + Param_1 + + 1,65536,1 + + + + + Up + Down + + + + + + Menu_Status=Param_1 + + Ready + Busy + + + + + + Menu_Layer=Param_1 + + 1,16,1 + + + + + + Menu_Name=Param_1 + + 0,128,UTF-8 + + + + + + Line_1 + + Current_List,Line_1,Txt=Param_1:Current_List,Line_1,Attribute=Param_2 + + 0,128,UTF-8 + + + Container + Unplayable Item + Item + Unselectable + Keyword + + + + + Line_2 + + Current_List,Line_2,Txt=Param_1:Current_List,Line_2,Attribute=Param_2 + + 0,128,UTF-8 + + + Container + Unplayable Item + Item + Unselectable + Keyword + + + + + Line_3 + + Current_List,Line_3,Txt=Param_1:Current_List,Line_3,Attribute=Param_2 + + 0,128,UTF-8 + + + Container + Unplayable Item + Item + Unselectable + Keyword + + + + + Line_4 + + Current_List,Line_4,Txt=Param_1:Current_List,Line_4,Attribute=Param_2 + + 0,128,UTF-8 + + + Container + Unplayable Item + Item + Unselectable + Keyword + + + + + Line_5 + + Current_List,Line_5,Txt=Param_1:Current_List,Line_5,Attribute=Param_2 + + 0,128,UTF-8 + + + Container + Unplayable Item + Item + Unselectable + Keyword + + + + + Line_6 + + Current_List,Line_6,Txt=Param_1:Current_List,Line_6,Attribute=Param_2 + + 0,128,UTF-8 + + + Container + Unplayable Item + Item + Unselectable + Keyword + + + + + Line_7 + + Current_List,Line_7,Txt=Param_1:Current_List,Line_7,Attribute=Param_2 + + 0,128,UTF-8 + + + Container + Unplayable Item + Item + Unselectable + Keyword + + + + + Line_8 + + Current_List,Line_8,Txt=Param_1:Current_List,Line_8,Attribute=Param_2 + + 0,128,UTF-8 + + + Container + Unplayable Item + Item + Unselectable + Keyword + + + + + + + Cursor_Position,Current_Line=Param_1 + + 1,65536,1 + + + + + + Cursor_Position,Max_Line=Param_1 + + 0,65536,1 + + + + + + + Rhapsody,Play_Control,Play_Mode,Repeat + Rhapsody,Play_Control,Play_Mode,Shuffle + Rhapsody,Play_Control,Playback + Rhapsody,Play_Control,Preset,Preset_Sel + Rhapsody,List_Control,Direct_Sel + Rhapsody,List_Control,Jump_Line + Rhapsody,List_Control,Cursor + Rhapsody,List_Control,Page + Rhapsody,List_Control,Direct_Sel_with_Keyword + Rhapsody,Play_Info + Rhapsody,List_Info + Rhapsody,Config + Rhapsody,Play_Control,Preset,Preset_Sel_Item + + + + + + Feature_Availability=Param_1 + + Ready + Not Ready + + + + + + Play + Stop + + Playback_Info=Param_1 + + Play + Stop + + + + + + + + Meta_Info,Ch_Name=Param_1 + + 0,128,UTF-8 + + + + + + Meta_Info,Artist=Param_1 + + 0,128,UTF-8 + + + + + + Meta_Info,Song=Param_1 + + 0,128,UTF-8 + + + + + + Feature_Availability=Param_1 + + Ready + Not Ready + + + + + + Playback_Info=Param_1 + + Play + Stop + + + + + + Album_ART,URL=Param_1 + + 0,128,UTF-8 + + + + + + Album_ART,ID=Param_1 + + 0,255,1 + + + + + + Album_ART,Format=Param_1 + + BMP + YMF + + + + + + Input_Logo,URL_S=Param_1 + + 0,128,UTF-8 + + + + + + + + + Param_1 + + 1,8,1,Line_% + + + + + Up + Down + Return + Sel + Return to Home + + + + Param_1 + + 1,65536,1 + + + + + Up + Down + + + + + + Menu_Status=Param_1 + + Ready + Busy + + + + + + Menu_Layer=Param_1 + + 1,16,1 + + + + + + Menu_Name=Param_1 + + 0,128,UTF-8 + + + + + + Line_1 + + Current_List,Line_1,Txt=Param_1:Current_List,Line_1,Attribute=Param_2 + + 0,128,UTF-8 + + + Container + Unplayable Item + Item + Unselectable + + + + + Line_2 + + Current_List,Line_2,Txt=Param_1:Current_List,Line_2,Attribute=Param_2 + + 0,128,UTF-8 + + + Container + Unplayable Item + Item + Unselectable + + + + + Line_3 + + Current_List,Line_3,Txt=Param_1:Current_List,Line_3,Attribute=Param_2 + + 0,128,UTF-8 + + + Container + Unplayable Item + Item + Unselectable + + + + + Line_4 + + Current_List,Line_4,Txt=Param_1:Current_List,Line_4,Attribute=Param_2 + + 0,128,UTF-8 + + + Container + Unplayable Item + Item + Unselectable + + + + + Line_5 + + Current_List,Line_5,Txt=Param_1:Current_List,Line_5,Attribute=Param_2 + + 0,128,UTF-8 + + + Container + Unplayable Item + Item + Unselectable + + + + + Line_6 + + Current_List,Line_6,Txt=Param_1:Current_List,Line_6,Attribute=Param_2 + + 0,128,UTF-8 + + + Container + Unplayable Item + Item + Unselectable + + + + + Line_7 + + Current_List,Line_7,Txt=Param_1:Current_List,Line_7,Attribute=Param_2 + + 0,128,UTF-8 + + + Container + Unplayable Item + Item + Unselectable + + + + + Line_8 + + Current_List,Line_8,Txt=Param_1:Current_List,Line_8,Attribute=Param_2 + + 0,128,UTF-8 + + + Container + Unplayable Item + Item + Unselectable + + + + + + + Cursor_Position,Current_Line=Param_1 + + 1,65536,1 + + + + + + Cursor_Position,Max_Line=Param_1 + + 0,65536,1 + + + + + + + SiriusXM,Play_Control,Playback + SiriusXM,Play_Control,Preset,Preset_Sel + SiriusXM,List_Control,Direct_Sel + SiriusXM,List_Control,Jump_Line + SiriusXM,List_Control,Cursor + SiriusXM,List_Control,Page + SiriusXM,Play_Info + SiriusXM,List_Info + SiriusXM,Config + SiriusXM,Play_Control,Preset,Preset_Sel_Item + + + + + + Feature_Availability=Param_1 + + Ready + Not Ready + + + + + + Play + Pause + Stop + + Playback_Info=Param_1 + + Play + Pause + Stop + + + + + Skip Fwd + + + Thumb Up + Thumb Down + + Feedback=Param_1 + + --- + Thumb Up + Thumb Down + + + + + + + + Meta_Info,Station=Param_1 + + 0,128,UTF-8 + + + + + + Meta_Info,Album=Param_1 + + 0,128,UTF-8 + + + + + + Meta_Info,Track=Param_1 + + 0,128,UTF-8 + + + + + + Feedback=Param_1 + + --- + Thumb Up + Thumb Down + + + + + + Feature_Availability=Param_1 + + Ready + Not Ready + + + + + + Playback_Info=Param_1 + + Play + Pause + Stop + + + + + + Album_ART,URL=Param_1 + + 0,128,UTF-8 + + + + + + Album_ART,ID=Param_1 + + 0,255,1 + + + + + + Album_ART,Format=Param_1 + + BMP + YMF + + + + + + Input_Logo,URL_S=Param_1 + + 0,128,UTF-8 + + + + + + + + + Param_1 + + 1,8,1,Line_% + + + + + Up + Down + Return + Sel + Return to Home + + + + Param_1 + + 1,65536,1 + + + + + Up + Down + + + + + + Menu_Status=Param_1 + + Ready + Busy + + + + + + Menu_Layer=Param_1 + + 1,16,1 + + + + + + Menu_Name=Param_1 + + 0,128,UTF-8 + + + + + + Line_1 + + Current_List,Line_1,Txt=Param_1:Current_List,Line_1,Attribute=Param_2 + + 0,128,UTF-8 + + + Container + Unplayable Item + Item + Unselectable + + + + + Line_2 + + Current_List,Line_2,Txt=Param_1:Current_List,Line_2,Attribute=Param_2 + + 0,128,UTF-8 + + + Container + Unplayable Item + Item + Unselectable + + + + + Line_3 + + Current_List,Line_3,Txt=Param_1:Current_List,Line_3,Attribute=Param_2 + + 0,128,UTF-8 + + + Container + Unplayable Item + Item + Unselectable + + + + + Line_4 + + Current_List,Line_4,Txt=Param_1:Current_List,Line_4,Attribute=Param_2 + + 0,128,UTF-8 + + + Container + Unplayable Item + Item + Unselectable + + + + + Line_5 + + Current_List,Line_5,Txt=Param_1:Current_List,Line_5,Attribute=Param_2 + + 0,128,UTF-8 + + + Container + Unplayable Item + Item + Unselectable + + + + + Line_6 + + Current_List,Line_6,Txt=Param_1:Current_List,Line_6,Attribute=Param_2 + + 0,128,UTF-8 + + + Container + Unplayable Item + Item + Unselectable + + + + + Line_7 + + Current_List,Line_7,Txt=Param_1:Current_List,Line_7,Attribute=Param_2 + + 0,128,UTF-8 + + + Container + Unplayable Item + Item + Unselectable + + + + + Line_8 + + Current_List,Line_8,Txt=Param_1:Current_List,Line_8,Attribute=Param_2 + + 0,128,UTF-8 + + + Container + Unplayable Item + Item + Unselectable + + + + + + + Cursor_Position,Current_Line=Param_1 + + 1,65536,1 + + + + + + Cursor_Position,Max_Line=Param_1 + + 0,65536,1 + + + + + + + Pandora,Play_Control,Feedback + Pandora,Play_Control,Playback + Pandora,Play_Control,Preset,Preset_Sel + Pandora,List_Control,Direct_Sel + Pandora,List_Control,Jump_Line + Pandora,List_Control,Cursor + Pandora,List_Control,Page + Pandora,Play_Info + Pandora,List_Info + Pandora,Config + Pandora,Play_Control,Preset,Preset_Sel_Item + + + From 1fff6ce4380c788deab85fcfdb8e12259c6ab71b Mon Sep 17 00:00:00 2001 From: bestlibre Date: Wed, 16 Nov 2016 17:19:00 +0100 Subject: [PATCH 089/127] Deduplicate MQTT_PUBLISH_SCHEMA definition (#4411) --- homeassistant/components/mqtt/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index b3891db82f0..fd8c915aca3 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -84,7 +84,7 @@ _HBMQTT_CONFIG_SCHEMA = vol.Schema(dict) CLIENT_KEY_AUTH_MSG = 'client_key and client_cert must both be present in ' \ 'the mqtt broker config' -MQTT_PUBLISH_SCHEMA = vol.Schema({ +MQTT_WILL_BIRTH_SCHEMA = vol.Schema({ vol.Required(ATTR_TOPIC): valid_publish_topic, vol.Required(ATTR_PAYLOAD, CONF_PAYLOAD): cv.string, vol.Optional(ATTR_QOS, default=DEFAULT_QOS): _VALID_QOS_SCHEMA, @@ -109,8 +109,8 @@ CONFIG_SCHEMA = vol.Schema({ vol.Optional(CONF_PROTOCOL, default=DEFAULT_PROTOCOL): vol.All(cv.string, vol.In([PROTOCOL_31, PROTOCOL_311])), vol.Optional(CONF_EMBEDDED): _HBMQTT_CONFIG_SCHEMA, - vol.Optional(CONF_WILL_MESSAGE): MQTT_PUBLISH_SCHEMA, - vol.Optional(CONF_BIRTH_MESSAGE): MQTT_PUBLISH_SCHEMA + vol.Optional(CONF_WILL_MESSAGE): MQTT_WILL_BIRTH_SCHEMA, + vol.Optional(CONF_BIRTH_MESSAGE): MQTT_WILL_BIRTH_SCHEMA }), }, extra=vol.ALLOW_EXTRA) From f006b00dc1a308b850e4e2a75583201ce50e3207 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 16 Nov 2016 08:26:29 -0800 Subject: [PATCH 090/127] Fix spelling schedule_update_ha_state (#4415) --- homeassistant/components/switch/command_line.py | 4 ++-- homeassistant/components/switch/demo.py | 4 ++-- homeassistant/components/switch/enocean.py | 2 +- homeassistant/components/switch/flux.py | 4 ++-- homeassistant/components/switch/knx.py | 4 ++-- homeassistant/components/switch/mqtt.py | 4 ++-- homeassistant/components/switch/mysensors.py | 8 ++++---- homeassistant/components/switch/netio.py | 2 +- homeassistant/components/switch/pilight.py | 4 ++-- homeassistant/components/switch/pulseaudio_loopback.py | 4 ++-- homeassistant/components/switch/rpi_gpio.py | 4 ++-- homeassistant/components/switch/scsgate.py | 4 ++-- homeassistant/components/switch/vera.py | 4 ++-- homeassistant/components/switch/wemo.py | 4 ++-- homeassistant/helpers/entity.py | 2 +- 15 files changed, 29 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/switch/command_line.py b/homeassistant/components/switch/command_line.py index 0fe804d71a3..eca8f8b6023 100644 --- a/homeassistant/components/switch/command_line.py +++ b/homeassistant/components/switch/command_line.py @@ -149,11 +149,11 @@ class CommandSwitch(SwitchDevice): if (CommandSwitch._switch(self._command_on) and not self._command_state): self._state = True - self.shedule_update_ha_state() + self.schedule_update_ha_state() def turn_off(self, **kwargs): """Turn the device off.""" if (CommandSwitch._switch(self._command_off) and not self._command_state): self._state = False - self.shedule_update_ha_state() + self.schedule_update_ha_state() diff --git a/homeassistant/components/switch/demo.py b/homeassistant/components/switch/demo.py index bc5e90cefb4..359fc89353d 100644 --- a/homeassistant/components/switch/demo.py +++ b/homeassistant/components/switch/demo.py @@ -66,9 +66,9 @@ class DemoSwitch(SwitchDevice): def turn_on(self, **kwargs): """Turn the switch on.""" self._state = True - self.shedule_update_ha_state() + self.schedule_update_ha_state() def turn_off(self, **kwargs): """Turn the device off.""" self._state = False - self.shedule_update_ha_state() + self.schedule_update_ha_state() diff --git a/homeassistant/components/switch/enocean.py b/homeassistant/components/switch/enocean.py index 84c126076e3..71bd180ad10 100644 --- a/homeassistant/components/switch/enocean.py +++ b/homeassistant/components/switch/enocean.py @@ -79,4 +79,4 @@ class EnOceanSwitch(enocean.EnOceanDevice, ToggleEntity): def value_changed(self, val): """Update the internal state of the switch.""" self._on_state = val - self.shedule_update_ha_state() + self.schedule_update_ha_state() diff --git a/homeassistant/components/switch/flux.py b/homeassistant/components/switch/flux.py index a781d72fa77..9fccf75ea4f 100644 --- a/homeassistant/components/switch/flux.py +++ b/homeassistant/components/switch/flux.py @@ -137,7 +137,7 @@ class FluxSwitch(SwitchDevice): self._state = True self.unsub_tracker = track_utc_time_change(self.hass, self.flux_update, second=[0, 30]) - self.shedule_update_ha_state() + self.schedule_update_ha_state() def turn_off(self, **kwargs): """Turn off flux.""" @@ -146,7 +146,7 @@ class FluxSwitch(SwitchDevice): self.unsub_tracker = None self._state = False - self.shedule_update_ha_state() + self.schedule_update_ha_state() def flux_update(self, now=None): """Update all the lights using flux.""" diff --git a/homeassistant/components/switch/knx.py b/homeassistant/components/switch/knx.py index 1e02fd35844..5141e26cdf3 100644 --- a/homeassistant/components/switch/knx.py +++ b/homeassistant/components/switch/knx.py @@ -40,7 +40,7 @@ class KNXSwitch(KNXGroupAddress, SwitchDevice): self.group_write(1) self._state = [1] if not self.should_poll: - self.shedule_update_ha_state() + self.schedule_update_ha_state() def turn_off(self, **kwargs): """Turn the switch off. @@ -50,4 +50,4 @@ class KNXSwitch(KNXGroupAddress, SwitchDevice): self.group_write(0) self._state = [0] if not self.should_poll: - self.shedule_update_ha_state() + self.schedule_update_ha_state() diff --git a/homeassistant/components/switch/mqtt.py b/homeassistant/components/switch/mqtt.py index 76ba9bf28bc..794a0f0ced3 100644 --- a/homeassistant/components/switch/mqtt.py +++ b/homeassistant/components/switch/mqtt.py @@ -117,7 +117,7 @@ class MqttSwitch(SwitchDevice): if self._optimistic: # Optimistically assume that switch has changed state. self._state = True - self.shedule_update_ha_state() + self.schedule_update_ha_state() def turn_off(self, **kwargs): """Turn the device off.""" @@ -126,4 +126,4 @@ class MqttSwitch(SwitchDevice): if self._optimistic: # Optimistically assume that switch has changed state. self._state = False - self.shedule_update_ha_state() + self.schedule_update_ha_state() diff --git a/homeassistant/components/switch/mysensors.py b/homeassistant/components/switch/mysensors.py index 04282a4fa66..d6b348d8274 100644 --- a/homeassistant/components/switch/mysensors.py +++ b/homeassistant/components/switch/mysensors.py @@ -137,7 +137,7 @@ class MySensorsSwitch(mysensors.MySensorsDeviceEntity, SwitchDevice): if self.gateway.optimistic: # optimistically assume that switch has changed state self._values[self.value_type] = STATE_ON - self.shedule_update_ha_state() + self.schedule_update_ha_state() def turn_off(self): """Turn the switch off.""" @@ -146,7 +146,7 @@ class MySensorsSwitch(mysensors.MySensorsDeviceEntity, SwitchDevice): if self.gateway.optimistic: # optimistically assume that switch has changed state self._values[self.value_type] = STATE_OFF - self.shedule_update_ha_state() + self.schedule_update_ha_state() class MySensorsIRSwitch(MySensorsSwitch): @@ -182,7 +182,7 @@ class MySensorsIRSwitch(MySensorsSwitch): # optimistically assume that switch has changed state self._values[self.value_type] = self._ir_code self._values[set_req.V_LIGHT] = STATE_ON - self.shedule_update_ha_state() + self.schedule_update_ha_state() # turn off switch after switch was turned on self.turn_off() @@ -198,7 +198,7 @@ class MySensorsIRSwitch(MySensorsSwitch): if self.gateway.optimistic: # optimistically assume that switch has changed state self._values[set_req.V_LIGHT] = STATE_OFF - self.shedule_update_ha_state() + self.schedule_update_ha_state() def update(self): """Update the controller with the latest value from a sensor.""" diff --git a/homeassistant/components/switch/netio.py b/homeassistant/components/switch/netio.py index 151a1001e38..74505cdcdc2 100644 --- a/homeassistant/components/switch/netio.py +++ b/homeassistant/components/switch/netio.py @@ -156,7 +156,7 @@ class NetioSwitch(SwitchDevice): val[self.outlet - 1] = '1' if value else '0' self.netio.get('port list %s' % ''.join(val)) self.netio.states[self.outlet - 1] = value - self.shedule_update_ha_state() + self.schedule_update_ha_state() @property def is_on(self): diff --git a/homeassistant/components/switch/pilight.py b/homeassistant/components/switch/pilight.py index 2f0e81c753d..6e16c9fa2e5 100644 --- a/homeassistant/components/switch/pilight.py +++ b/homeassistant/components/switch/pilight.py @@ -127,11 +127,11 @@ class PilightSwitch(SwitchDevice): self._hass.services.call(pilight.DOMAIN, pilight.SERVICE_NAME, self._code_on, blocking=True) self._state = True - self.shedule_update_ha_state() + self.schedule_update_ha_state() def turn_off(self): """Turn the switch on by calling pilight.send service with off code.""" self._hass.services.call(pilight.DOMAIN, pilight.SERVICE_NAME, self._code_off, blocking=True) self._state = False - self.shedule_update_ha_state() + self.schedule_update_ha_state() diff --git a/homeassistant/components/switch/pulseaudio_loopback.py b/homeassistant/components/switch/pulseaudio_loopback.py index b8584ab232b..46f9173437c 100644 --- a/homeassistant/components/switch/pulseaudio_loopback.py +++ b/homeassistant/components/switch/pulseaudio_loopback.py @@ -170,7 +170,7 @@ class PALoopbackSwitch(SwitchDevice): self._pa_svr.update_module_state(no_throttle=True) self._module_idx = self._pa_svr.get_module_idx( self._sink_name, self._source_name) - self.shedule_update_ha_state() + self.schedule_update_ha_state() else: _LOGGER.warning(IGNORED_SWITCH_WARN) @@ -181,7 +181,7 @@ class PALoopbackSwitch(SwitchDevice): self._pa_svr.update_module_state(no_throttle=True) self._module_idx = self._pa_svr.get_module_idx( self._sink_name, self._source_name) - self.shedule_update_ha_state() + self.schedule_update_ha_state() else: _LOGGER.warning(IGNORED_SWITCH_WARN) diff --git a/homeassistant/components/switch/rpi_gpio.py b/homeassistant/components/switch/rpi_gpio.py index a08e6f86242..cc761250be4 100644 --- a/homeassistant/components/switch/rpi_gpio.py +++ b/homeassistant/components/switch/rpi_gpio.py @@ -77,10 +77,10 @@ class RPiGPIOSwitch(ToggleEntity): """Turn the device on.""" rpi_gpio.write_output(self._port, 0 if self._invert_logic else 1) self._state = True - self.shedule_update_ha_state() + self.schedule_update_ha_state() def turn_off(self): """Turn the device off.""" rpi_gpio.write_output(self._port, 1 if self._invert_logic else 0) self._state = False - self.shedule_update_ha_state() + self.schedule_update_ha_state() diff --git a/homeassistant/components/switch/scsgate.py b/homeassistant/components/switch/scsgate.py index 551704d032f..d7670dff067 100644 --- a/homeassistant/components/switch/scsgate.py +++ b/homeassistant/components/switch/scsgate.py @@ -119,7 +119,7 @@ class SCSGateSwitch(SwitchDevice): ToggleStatusTask(target=self._scs_id, toggled=True)) self._toggled = True - self.shedule_update_ha_state() + self.schedule_update_ha_state() def turn_off(self, **kwargs): """Turn the device off.""" @@ -129,7 +129,7 @@ class SCSGateSwitch(SwitchDevice): ToggleStatusTask(target=self._scs_id, toggled=False)) self._toggled = False - self.shedule_update_ha_state() + self.schedule_update_ha_state() def process_event(self, message): """Handle a SCSGate message related with this switch.""" diff --git a/homeassistant/components/switch/vera.py b/homeassistant/components/switch/vera.py index 21d386ddadb..8ab66b75fff 100644 --- a/homeassistant/components/switch/vera.py +++ b/homeassistant/components/switch/vera.py @@ -36,13 +36,13 @@ class VeraSwitch(VeraDevice, SwitchDevice): """Turn device on.""" self.vera_device.switch_on() self._state = STATE_ON - self.shedule_update_ha_state() + self.schedule_update_ha_state() def turn_off(self, **kwargs): """Turn device off.""" self.vera_device.switch_off() self._state = STATE_OFF - self.shedule_update_ha_state() + self.schedule_update_ha_state() @property def current_power_mwh(self): diff --git a/homeassistant/components/switch/wemo.py b/homeassistant/components/switch/wemo.py index 986d443fa37..d4f6b721e9d 100644 --- a/homeassistant/components/switch/wemo.py +++ b/homeassistant/components/switch/wemo.py @@ -151,13 +151,13 @@ class WemoSwitch(SwitchDevice): """Turn the switch on.""" self._state = WEMO_ON self.wemo.on() - self.shedule_update_ha_state() + self.schedule_update_ha_state() def turn_off(self): """Turn the switch off.""" self._state = WEMO_OFF self.wemo.off() - self.shedule_update_ha_state() + self.schedule_update_ha_state() def update(self): """Update WeMo state.""" diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 8556d028062..b707f2f7199 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -270,7 +270,7 @@ class Entity(object): self.hass.states.async_set( self.entity_id, state, attr, self.force_update) - def shedule_update_ha_state(self, force_refresh=False): + def schedule_update_ha_state(self, force_refresh=False): """Shedule a update ha state change task. That is only needed on executor to not block. From b0e3d5a576f5e00b5a31d3c2a9770a3be383ebaf Mon Sep 17 00:00:00 2001 From: Magnus Ihse Bursie Date: Thu, 17 Nov 2016 00:05:10 +0100 Subject: [PATCH 091/127] Better handling of accented characters in slugify (#4399) (#4423) * Better handling of accented characters in slugify (#4399) * Update __init__.py --- homeassistant/util/__init__.py | 3 ++- tests/util/test_init.py | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/util/__init__.py b/homeassistant/util/__init__.py index de16a2d23d2..6d77f67161d 100644 --- a/homeassistant/util/__init__.py +++ b/homeassistant/util/__init__.py @@ -10,6 +10,7 @@ import random import string from functools import wraps from types import MappingProxyType +from unicodedata import normalize from typing import Any, Optional, TypeVar, Callable, Sequence, KeysView, Union @@ -35,7 +36,7 @@ def sanitize_path(path: str) -> str: def slugify(text: str) -> str: """Slugify a given text.""" - text = text.lower().replace(" ", "_") + text = normalize('NFKD', text).lower().replace(" ", "_") return RE_SLUGIFY.sub("", text) diff --git a/tests/util/test_init.py b/tests/util/test_init.py index 9bfd6ebd6ed..d6d583342d7 100644 --- a/tests/util/test_init.py +++ b/tests/util/test_init.py @@ -30,6 +30,7 @@ class TestUtil(unittest.TestCase): self.assertEqual("test", util.slugify("T-!@#$!#@$!$est")) self.assertEqual("test_more", util.slugify("Test More")) self.assertEqual("test_more", util.slugify("Test_(More)")) + self.assertEqual("test_more", util.slugify("Tèst_Mörê")) def test_repr_helper(self): """Test repr_helper.""" From a862bc4edc09c2822999544fc8a2635a2612dda9 Mon Sep 17 00:00:00 2001 From: Lewis Juggins Date: Thu, 17 Nov 2016 02:55:58 +0000 Subject: [PATCH 092/127] Fix DLink async I/O (#4301) --- homeassistant/components/switch/dlink.py | 52 +++++++++++++++++------- 1 file changed, 38 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/switch/dlink.py b/homeassistant/components/switch/dlink.py index d0f3daa08f7..6a00fe71f20 100644 --- a/homeassistant/components/switch/dlink.py +++ b/homeassistant/components/switch/dlink.py @@ -48,20 +48,21 @@ def setup_platform(hass, config, add_devices, discovery_info=None): use_legacy_protocol = config.get(CONF_USE_LEGACY_PROTOCOL) name = config.get(CONF_NAME) - add_devices([SmartPlugSwitch(hass, SmartPlug(host, - password, - username, - use_legacy_protocol), - name)]) + data = SmartPlugData(SmartPlug(host, + password, + username, + use_legacy_protocol)) + + add_devices([SmartPlugSwitch(hass, data, name)], True) class SmartPlugSwitch(SwitchDevice): """Representation of a D-link Smart Plug switch.""" - def __init__(self, hass, smartplug, name): + def __init__(self, hass, data, name): """Initialize the switch.""" self.units = hass.config.units - self.smartplug = smartplug + self.data = data self._name = name @property @@ -73,7 +74,7 @@ class SmartPlugSwitch(SwitchDevice): def device_state_attributes(self): """Return the state attributes of the device.""" try: - ui_temp = self.units.temperature(int(self.smartplug.temperature), + ui_temp = self.units.temperature(int(self.data.temperature), TEMP_CELSIUS) temperature = "%i %s" % \ (ui_temp, self.units.temperature_unit) @@ -82,13 +83,13 @@ class SmartPlugSwitch(SwitchDevice): try: current_consumption = "%.2f W" % \ - float(self.smartplug.current_consumption) + float(self.data.current_consumption) except ValueError: current_consumption = STATE_UNKNOWN try: total_consumption = "%.1f kWh" % \ - float(self.smartplug.total_consumption) + float(self.data.total_consumption) except ValueError: total_consumption = STATE_UNKNOWN @@ -104,19 +105,42 @@ class SmartPlugSwitch(SwitchDevice): def current_power_watt(self): """Return the current power usage in Watt.""" try: - return float(self.smartplug.current_consumption) + return float(self.data.current_consumption) except ValueError: return None @property def is_on(self): """Return true if switch is on.""" - return self.smartplug.state == 'ON' + return self.data.state == 'ON' def turn_on(self, **kwargs): """Turn the switch on.""" - self.smartplug.state = 'ON' + self.data.smartplug.state = 'ON' def turn_off(self): """Turn the switch off.""" - self.smartplug.state = 'OFF' + self.data.smartplug.state = 'OFF' + + def update(self): + """Get the latest data from the smart plug and updates the states.""" + self.data.update() + + +class SmartPlugData(object): + """Get the latest data from smart plug.""" + + def __init__(self, smartplug): + """Initialize the data object.""" + self.smartplug = smartplug + self.state = None + self.temperature = None + self.current_consumption = None + self.total_consumption = None + + def update(self): + """Get the latest data from the smart plug.""" + self.state = self.smartplug.state + self.temperature = self.smartplug.temperature + self.current_consumption = self.smartplug.current_consumption + self.total_consumption = self.smartplug.total_consumption From c8bc1e3c5d586f9d81c614906698aebc958ea1b4 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 17 Nov 2016 05:00:08 +0100 Subject: [PATCH 093/127] change add_job to use call_soon_threadsafe (#4410) * change add_job to use call_soon_threadsafe * address comments from paulus * Tweak core tests * Fix tests Python 3.4.2 --- homeassistant/core.py | 3 +-- tests/test_core.py | 46 ++++++++++++++++++++++++++++++++++--------- 2 files changed, 38 insertions(+), 11 deletions(-) diff --git a/homeassistant/core.py b/homeassistant/core.py index c1be6e760d5..90f998508d8 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -210,8 +210,7 @@ class HomeAssistant(object): target: target to call. args: parameters for method to call. """ - run_callback_threadsafe( - self.loop, self.async_add_job, target, *args).result() + self.loop.call_soon_threadsafe(self.async_add_job, target, *args) @callback def async_add_job(self, target: Callable[..., None], *args: Any) -> None: diff --git a/tests/test_core.py b/tests/test_core.py index 60057e57ad1..212c6d41f70 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -154,12 +154,21 @@ class TestHomeAssistant(unittest.TestCase): """Test Coro.""" call_count.append('call') - for i in range(50): + for i in range(2): self.hass.add_job(test_coro()) - assert len(self.hass._pending_tasks) == 50 + @asyncio.coroutine + def wait_finish_callback(): + """Wait until all stuff is scheduled.""" + yield from asyncio.sleep(0, loop=self.hass.loop) + yield from asyncio.sleep(0, loop=self.hass.loop) + + run_coroutine_threadsafe( + wait_finish_callback(), self.hass.loop).result() + + assert len(self.hass._pending_tasks) == 2 self.hass.block_till_done() - assert len(call_count) == 50 + assert len(call_count) == 2 def test_async_add_job_pending_tasks_executor(self): """Run a executor in pending tasks.""" @@ -169,12 +178,21 @@ class TestHomeAssistant(unittest.TestCase): """Test executor.""" call_count.append('call') - for i in range(40): + @asyncio.coroutine + def wait_finish_callback(): + """Wait until all stuff is scheduled.""" + yield from asyncio.sleep(0, loop=self.hass.loop) + yield from asyncio.sleep(0, loop=self.hass.loop) + + for i in range(2): self.hass.add_job(test_executor) - assert len(self.hass._pending_tasks) == 40 + run_coroutine_threadsafe( + wait_finish_callback(), self.hass.loop).result() + + assert len(self.hass._pending_tasks) == 2 self.hass.block_till_done() - assert len(call_count) == 40 + assert len(call_count) == 2 def test_async_add_job_pending_tasks_callback(self): """Run a callback in pending tasks.""" @@ -185,12 +203,22 @@ class TestHomeAssistant(unittest.TestCase): """Test callback.""" call_count.append('call') - for i in range(40): + @asyncio.coroutine + def wait_finish_callback(): + """Wait until all stuff is scheduled.""" + yield from asyncio.sleep(0, loop=self.hass.loop) + yield from asyncio.sleep(0, loop=self.hass.loop) + + for i in range(2): self.hass.add_job(test_callback) - assert len(self.hass._pending_tasks) == 0 + run_coroutine_threadsafe( + wait_finish_callback(), self.hass.loop).result() + self.hass.block_till_done() - assert len(call_count) == 40 + + assert len(self.hass._pending_tasks) == 0 + assert len(call_count) == 2 class TestEvent(unittest.TestCase): From 38d201a54a70a9a55e1350e93c03a3f115b29079 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 16 Nov 2016 23:01:14 -0800 Subject: [PATCH 094/127] Increase logging level of errors while doing jobs (#4429) --- homeassistant/core.py | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/homeassistant/core.py b/homeassistant/core.py index 90f998508d8..86ce64a2120 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -306,21 +306,14 @@ class HomeAssistant(object): @callback def _async_exception_handler(self, loop, context): """Handle all exception inside the core loop.""" - message = context.get('message') - if message: - _LOGGER.warning( - "Error inside async loop: %s", - message - ) - - # for debug modus + kwargs = {} exception = context.get('exception') - if exception is not None: - exc_info = (type(exception), exception, exception.__traceback__) - _LOGGER.debug( - "Exception inside async loop: ", - exc_info=exc_info - ) + if exception: + kwargs['exc_info'] = (type(exception), exception, + exception.__traceback__) + + _LOGGER.error('Error doing job: %s', context['message'], + **kwargs) @callback def _async_stop_handler(self, *args): From 0f59bb208c8e412dfde17d20db8f08513a1c5dfa Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 17 Nov 2016 07:34:46 -0800 Subject: [PATCH 095/127] Migrate callbacks to use schedule_update_ha_state (#4426) * Migrate callbacks to use schedule_update_ha_state * Migrate MQTT sensor callback to async * Migrate wemo to not update inside schedule_update_ha_state * Make MQTT switch async * Fix nx584 test * Migrate tellstick callback * Migrate vera callback * Alarm control panel - manual: use async callbacks * Run the switch rest tests that work --- .../components/alarm_control_panel/manual.py | 8 ++++---- homeassistant/components/binary_sensor/ffmpeg.py | 2 +- homeassistant/components/binary_sensor/mqtt.py | 8 +++++--- homeassistant/components/binary_sensor/nx584.py | 2 +- homeassistant/components/binary_sensor/rpi_gpio.py | 2 +- homeassistant/components/binary_sensor/trend.py | 11 +++++++---- homeassistant/components/binary_sensor/wemo.py | 4 ++-- homeassistant/components/binary_sensor/zwave.py | 8 ++++---- .../components/climate/generic_thermostat.py | 2 +- homeassistant/components/climate/zwave.py | 2 +- homeassistant/components/cover/mqtt.py | 12 +++++++----- homeassistant/components/media_player/cast.py | 4 ++-- homeassistant/components/sensor/mqtt.py | 6 ++++-- homeassistant/components/switch/mqtt.py | 12 +++++++----- homeassistant/components/switch/wemo.py | 4 ++-- homeassistant/components/tellstick.py | 2 +- homeassistant/components/vera.py | 3 ++- tests/components/binary_sensor/test_nx584.py | 4 ++-- tests/components/switch/test_rest.py | 1 - 19 files changed, 54 insertions(+), 43 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/manual.py b/homeassistant/components/alarm_control_panel/manual.py index 9a7efbeaf5a..073d55508ed 100644 --- a/homeassistant/components/alarm_control_panel/manual.py +++ b/homeassistant/components/alarm_control_panel/manual.py @@ -129,7 +129,7 @@ class ManualAlarm(alarm.AlarmControlPanel): if self._pending_time: track_point_in_time( - self._hass, self.update_ha_state, + self._hass, self.async_update_ha_state, self._state_ts + self._pending_time) def alarm_arm_away(self, code=None): @@ -143,7 +143,7 @@ class ManualAlarm(alarm.AlarmControlPanel): if self._pending_time: track_point_in_time( - self._hass, self.update_ha_state, + self._hass, self.async_update_ha_state, self._state_ts + self._pending_time) def alarm_trigger(self, code=None): @@ -155,11 +155,11 @@ class ManualAlarm(alarm.AlarmControlPanel): if self._trigger_time: track_point_in_time( - self._hass, self.update_ha_state, + self._hass, self.async_update_ha_state, self._state_ts + self._pending_time) track_point_in_time( - self._hass, self.update_ha_state, + self._hass, self.async_update_ha_state, self._state_ts + self._pending_time + self._trigger_time) def _validate_code(self, code, state): diff --git a/homeassistant/components/binary_sensor/ffmpeg.py b/homeassistant/components/binary_sensor/ffmpeg.py index 72140936e18..818a6b5b387 100644 --- a/homeassistant/components/binary_sensor/ffmpeg.py +++ b/homeassistant/components/binary_sensor/ffmpeg.py @@ -138,7 +138,7 @@ class FFmpegBinarySensor(BinarySensorDevice): def _callback(self, state): """HA-FFmpeg callback for noise detection.""" self._state = state - self.update_ha_state() + self.schedule_update_ha_state() def _start_ffmpeg(self, config): """Start a FFmpeg instance.""" diff --git a/homeassistant/components/binary_sensor/mqtt.py b/homeassistant/components/binary_sensor/mqtt.py index 53c8e9a60b6..28d9566b2ab 100644 --- a/homeassistant/components/binary_sensor/mqtt.py +++ b/homeassistant/components/binary_sensor/mqtt.py @@ -8,6 +8,7 @@ import logging import voluptuous as vol +from homeassistant.core import callback import homeassistant.components.mqtt as mqtt from homeassistant.components.binary_sensor import ( BinarySensorDevice, SENSOR_CLASSES) @@ -66,17 +67,18 @@ class MqttBinarySensor(BinarySensorDevice): self._payload_off = payload_off self._qos = qos + @callback def message_received(topic, payload, qos): """A new MQTT message has been received.""" if value_template is not None: - payload = value_template.render_with_possible_json_value( + payload = value_template.async_render_with_possible_json_value( payload) if payload == self._payload_on: self._state = True - self.update_ha_state() + hass.async_add_job(self.async_update_ha_state()) elif payload == self._payload_off: self._state = False - self.update_ha_state() + hass.async_add_job(self.async_update_ha_state()) mqtt.subscribe(hass, self._state_topic, message_received, self._qos) diff --git a/homeassistant/components/binary_sensor/nx584.py b/homeassistant/components/binary_sensor/nx584.py index e158da02f2b..b21e40dc5dd 100644 --- a/homeassistant/components/binary_sensor/nx584.py +++ b/homeassistant/components/binary_sensor/nx584.py @@ -123,7 +123,7 @@ class NX584Watcher(threading.Thread): if not zone_sensor: return zone_sensor._zone['state'] = event['zone_state'] - zone_sensor.update_ha_state() + zone_sensor.schedule_update_ha_state() def _process_events(self, events): for event in events: diff --git a/homeassistant/components/binary_sensor/rpi_gpio.py b/homeassistant/components/binary_sensor/rpi_gpio.py index 13dd7d0b860..03978ac625b 100644 --- a/homeassistant/components/binary_sensor/rpi_gpio.py +++ b/homeassistant/components/binary_sensor/rpi_gpio.py @@ -72,7 +72,7 @@ class RPiGPIOBinarySensor(BinarySensorDevice): def read_gpio(port): """Read state from GPIO.""" self._state = rpi_gpio.read_input(self._port) - self.update_ha_state() + self.schedule_update_ha_state() rpi_gpio.edge_detect(self._port, read_gpio, self._bouncetime) diff --git a/homeassistant/components/binary_sensor/trend.py b/homeassistant/components/binary_sensor/trend.py index 2ef9c487d82..7c38d4505ae 100644 --- a/homeassistant/components/binary_sensor/trend.py +++ b/homeassistant/components/binary_sensor/trend.py @@ -4,8 +4,11 @@ A sensor that monitors trands in other components. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.trend/ """ +import asyncio import logging import voluptuous as vol + +from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.components.binary_sensor import ( @@ -87,13 +90,12 @@ class SensorTrend(BinarySensorDevice): self.from_state = None self.to_state = None - self.update() - + @callback def trend_sensor_state_listener(entity, old_state, new_state): """Called when the target device changes state.""" self.from_state = old_state self.to_state = new_state - self.update_ha_state(True) + hass.async_add_job(self.async_update_ha_state(True)) track_state_change(hass, target_entity, trend_sensor_state_listener) @@ -118,7 +120,8 @@ class SensorTrend(BinarySensorDevice): """No polling needed.""" return False - def update(self): + @asyncio.coroutine + def async_update(self): """Get the latest data and update the states.""" if self.from_state is None or self.to_state is None: return diff --git a/homeassistant/components/binary_sensor/wemo.py b/homeassistant/components/binary_sensor/wemo.py index 0e3259a3a96..07deea02f6e 100644 --- a/homeassistant/components/binary_sensor/wemo.py +++ b/homeassistant/components/binary_sensor/wemo.py @@ -45,10 +45,10 @@ class WemoBinarySensor(BinarySensorDevice): _LOGGER.info( 'Subscription update for %s', _device) + self.update() if not hasattr(self, 'hass'): - self.update() return - self.update_ha_state(True) + self.schedule_update_ha_state() @property def should_poll(self): diff --git a/homeassistant/components/binary_sensor/zwave.py b/homeassistant/components/binary_sensor/zwave.py index 69688c7e4f6..e99a2625ea2 100644 --- a/homeassistant/components/binary_sensor/zwave.py +++ b/homeassistant/components/binary_sensor/zwave.py @@ -96,7 +96,7 @@ class ZWaveBinarySensor(BinarySensorDevice, zwave.ZWaveDeviceEntity, Entity): """Called when a value has changed on the network.""" if self._value.value_id == value.value_id or \ self._value.node == value.node: - self.update_ha_state() + self.schedule_update_ha_state() class ZWaveTriggerSensor(ZWaveBinarySensor, Entity): @@ -112,19 +112,19 @@ class ZWaveTriggerSensor(ZWaveBinarySensor, Entity): # If it's active make sure that we set the timeout tracker if sensor_value.data: track_point_in_time( - self._hass, self.update_ha_state, + self._hass, self.async_update_ha_state, self.invalidate_after) def value_changed(self, value): """Called when a value has changed on the network.""" if self._value.value_id == value.value_id: - self.update_ha_state() + self.schedule_update_ha_state() if value.data: # only allow this value to be true for re_arm secs self.invalidate_after = dt_util.utcnow() + datetime.timedelta( seconds=self.re_arm_sec) track_point_in_time( - self._hass, self.update_ha_state, + self._hass, self.async_update_ha_state, self.invalidate_after) @property diff --git a/homeassistant/components/climate/generic_thermostat.py b/homeassistant/components/climate/generic_thermostat.py index baa09439c2c..1a0b20dc11e 100644 --- a/homeassistant/components/climate/generic_thermostat.py +++ b/homeassistant/components/climate/generic_thermostat.py @@ -158,7 +158,7 @@ class GenericThermostat(ClimateDevice): self._update_temp(new_state) self._control_heating() - self.update_ha_state() + self.schedule_update_ha_state() def _update_temp(self, state): """Update thermostat with latest state from sensor.""" diff --git a/homeassistant/components/climate/zwave.py b/homeassistant/components/climate/zwave.py index e0da5d48c5f..5e5d8d4e54a 100755 --- a/homeassistant/components/climate/zwave.py +++ b/homeassistant/components/climate/zwave.py @@ -89,7 +89,7 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice): if self._value.value_id == value.value_id or \ self._value.node == value.node: self.update_properties() - self.update_ha_state() + self.schedule_update_ha_state() _LOGGER.debug("Value changed on network %s", value) def update_properties(self): diff --git a/homeassistant/components/cover/mqtt.py b/homeassistant/components/cover/mqtt.py index 27b30e5e013..44b59133d21 100644 --- a/homeassistant/components/cover/mqtt.py +++ b/homeassistant/components/cover/mqtt.py @@ -8,6 +8,7 @@ import logging import voluptuous as vol +from homeassistant.core import callback import homeassistant.components.mqtt as mqtt from homeassistant.components.cover import CoverDevice from homeassistant.const import ( @@ -89,29 +90,30 @@ class MqttCover(CoverDevice): self._retain = retain self._optimistic = optimistic or state_topic is None + @callback def message_received(topic, payload, qos): """A new MQTT message has been received.""" if value_template is not None: - payload = value_template.render_with_possible_json_value( + payload = value_template.async_render_with_possible_json_value( payload) if payload == self._state_open: self._state = False - _LOGGER.warning("state=%s", int(self._state)) - self.update_ha_state() + hass.async_add_job(self.async_update_ha_state()) elif payload == self._state_closed: self._state = True - self.update_ha_state() + hass.async_add_job(self.async_update_ha_state()) elif payload.isnumeric() and 0 <= int(payload) <= 100: if int(payload) > 0: self._state = False else: self._state = True self._position = int(payload) - self.update_ha_state() + hass.async_add_job(self.async_update_ha_state()) else: _LOGGER.warning( "Payload is not True, False, or integer (0-100): %s", payload) + if self._state_topic is None: # Force into optimistic mode. self._optimistic = True diff --git a/homeassistant/components/media_player/cast.py b/homeassistant/components/media_player/cast.py index 5d6289587be..1ec61cc621a 100644 --- a/homeassistant/components/media_player/cast.py +++ b/homeassistant/components/media_player/cast.py @@ -280,9 +280,9 @@ class CastDevice(MediaPlayerDevice): def new_cast_status(self, status): """Called when a new cast status is received.""" self.cast_status = status - self.update_ha_state() + self.schedule_update_ha_state() def new_media_status(self, status): """Called when a new media status is received.""" self.media_status = status - self.update_ha_state() + self.schedule_update_ha_state() diff --git a/homeassistant/components/sensor/mqtt.py b/homeassistant/components/sensor/mqtt.py index ae9eeeafb32..81267cfc45e 100644 --- a/homeassistant/components/sensor/mqtt.py +++ b/homeassistant/components/sensor/mqtt.py @@ -8,6 +8,7 @@ import logging import voluptuous as vol +from homeassistant.core import callback from homeassistant.components.mqtt import CONF_STATE_TOPIC, CONF_QOS from homeassistant.const import ( CONF_NAME, CONF_VALUE_TEMPLATE, STATE_UNKNOWN, CONF_UNIT_OF_MEASUREMENT) @@ -55,13 +56,14 @@ class MqttSensor(Entity): self._qos = qos self._unit_of_measurement = unit_of_measurement + @callback def message_received(topic, payload, qos): """A new MQTT message has been received.""" if value_template is not None: - payload = value_template.render_with_possible_json_value( + payload = value_template.async_render_with_possible_json_value( payload, self._state) self._state = payload - self.update_ha_state() + hass.async_add_job(self.async_update_ha_state()) mqtt.subscribe(hass, self._state_topic, message_received, self._qos) diff --git a/homeassistant/components/switch/mqtt.py b/homeassistant/components/switch/mqtt.py index 794a0f0ced3..a7dfab62fb4 100644 --- a/homeassistant/components/switch/mqtt.py +++ b/homeassistant/components/switch/mqtt.py @@ -8,6 +8,7 @@ import logging import voluptuous as vol +from homeassistant.core import callback from homeassistant.components.mqtt import ( CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN) from homeassistant.components.switch import SwitchDevice @@ -71,17 +72,18 @@ class MqttSwitch(SwitchDevice): self._payload_off = payload_off self._optimistic = optimistic + @callback def message_received(topic, payload, qos): """A new MQTT message has been received.""" if value_template is not None: - payload = value_template.render_with_possible_json_value( + payload = value_template.async_render_with_possible_json_value( payload) if payload == self._payload_on: self._state = True - self.update_ha_state() + hass.async_add_job(self.async_update_ha_state()) elif payload == self._payload_off: self._state = False - self.update_ha_state() + hass.async_add_job(self.async_update_ha_state()) if self._state_topic is None: # Force into optimistic mode. @@ -117,7 +119,7 @@ class MqttSwitch(SwitchDevice): if self._optimistic: # Optimistically assume that switch has changed state. self._state = True - self.schedule_update_ha_state() + self.update_ha_state() def turn_off(self, **kwargs): """Turn the device off.""" @@ -126,4 +128,4 @@ class MqttSwitch(SwitchDevice): if self._optimistic: # Optimistically assume that switch has changed state. self._state = False - self.schedule_update_ha_state() + self.update_ha_state() diff --git a/homeassistant/components/switch/wemo.py b/homeassistant/components/switch/wemo.py index d4f6b721e9d..2d3d5ea5547 100644 --- a/homeassistant/components/switch/wemo.py +++ b/homeassistant/components/switch/wemo.py @@ -63,10 +63,10 @@ class WemoSwitch(SwitchDevice): _LOGGER.info( 'Subscription update for %s', _device) + self.update() if not hasattr(self, 'hass'): - self.update() return - self.update_ha_state(True) + self.schedule_update_ha_state() @property def should_poll(self): diff --git a/homeassistant/components/tellstick.py b/homeassistant/components/tellstick.py index d2e296d61b6..6d8ad967ad2 100644 --- a/homeassistant/components/tellstick.py +++ b/homeassistant/components/tellstick.py @@ -111,7 +111,7 @@ class TellstickRegistry(object): entity = self._id_to_entity_map.get(tellstick_id, None) if entity is not None: entity.set_tellstick_state(method, data) - entity.update_ha_state() + entity.schedule_update_ha_state() def _setup_device_callback(self, hass, tellcore_lib): """Register the callback handler.""" diff --git a/homeassistant/components/vera.py b/homeassistant/components/vera.py index 6dcea6c9354..c1cc5d7cbf7 100644 --- a/homeassistant/components/vera.py +++ b/homeassistant/components/vera.py @@ -132,7 +132,8 @@ class VeraDevice(Entity): self.update() def _update_callback(self, _device): - self.update_ha_state(True) + self.update() + self.schedule_update_ha_state() @property def name(self): diff --git a/tests/components/binary_sensor/test_nx584.py b/tests/components/binary_sensor/test_nx584.py index 49147279711..6ed2ae476f3 100644 --- a/tests/components/binary_sensor/test_nx584.py +++ b/tests/components/binary_sensor/test_nx584.py @@ -137,7 +137,7 @@ class TestNX584ZoneSensor(unittest.TestCase): class TestNX584Watcher(unittest.TestCase): """Test the NX584 watcher.""" - @mock.patch.object(nx584.NX584ZoneSensor, 'update_ha_state') + @mock.patch.object(nx584.NX584ZoneSensor, 'schedule_update_ha_state') def test_process_zone_event(self, mock_update): """Test the processing of zone events.""" zone1 = {'number': 1, 'name': 'foo', 'state': True} @@ -151,7 +151,7 @@ class TestNX584Watcher(unittest.TestCase): self.assertFalse(zone1['state']) self.assertEqual(1, mock_update.call_count) - @mock.patch.object(nx584.NX584ZoneSensor, 'update_ha_state') + @mock.patch.object(nx584.NX584ZoneSensor, 'schedule_update_ha_state') def test_process_zone_event_missing_zone(self, mock_update): """Test the processing of zone events with missing zones.""" watcher = nx584.NX584Watcher(None, {}) diff --git a/tests/components/switch/test_rest.py b/tests/components/switch/test_rest.py index 85a178dcc42..dc6c58db928 100644 --- a/tests/components/switch/test_rest.py +++ b/tests/components/switch/test_rest.py @@ -12,7 +12,6 @@ from homeassistant.bootstrap import setup_component from tests.common import get_test_home_assistant, assert_setup_component -@pytest.mark.skip class TestRestSwitchSetup(unittest.TestCase): """Tests for setting up the REST switch platform.""" From b615b3349f2cf5fbf9c8f08758e04e9b84a98d70 Mon Sep 17 00:00:00 2001 From: Open Home Automation Date: Thu, 17 Nov 2016 17:40:21 +0100 Subject: [PATCH 096/127] Fix for Miflora 2.6.6 firmware (#4436) --- homeassistant/components/sensor/miflora.py | 3 ++- requirements_all.txt | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/miflora.py b/homeassistant/components/sensor/miflora.py index e917162d095..28906dfaef4 100644 --- a/homeassistant/components/sensor/miflora.py +++ b/homeassistant/components/sensor/miflora.py @@ -16,7 +16,7 @@ from homeassistant.util import Throttle from homeassistant.const import ( CONF_MONITORED_CONDITIONS, CONF_NAME, CONF_MAC) -REQUIREMENTS = ['miflora==0.1.9'] +REQUIREMENTS = ['miflora==0.1.12'] _LOGGER = logging.getLogger(__name__) @@ -41,6 +41,7 @@ SENSOR_TYPES = { 'light': ['Light intensity', 'lux'], 'moisture': ['Moisture', '%'], 'conductivity': ['Conductivity', 'µS/cm'], + 'battery': ['Battery', '%'], } PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ diff --git a/requirements_all.txt b/requirements_all.txt index 13b8c7bfb70..eea460ad5dc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -275,7 +275,7 @@ messagebird==1.2.0 mficlient==0.3.0 # homeassistant.components.sensor.miflora -miflora==0.1.9 +miflora==0.1.12 # homeassistant.components.discovery netdisco==0.7.6 From 726bc5b670841eafab46d93e70c5027b1c30c219 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 17 Nov 2016 12:02:43 -0800 Subject: [PATCH 097/127] Do not report on shutting down errors (#4431) * Do not report on shutting down errors * Lint --- homeassistant/core.py | 10 +++++++--- homeassistant/exceptions.py | 6 ++++++ 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/homeassistant/core.py b/homeassistant/core.py index 86ce64a2120..f7847228338 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -30,7 +30,7 @@ from homeassistant.const import ( EVENT_TIME_CHANGED, MATCH_ALL, RESTART_EXIT_CODE, SERVICE_HOMEASSISTANT_RESTART, SERVICE_HOMEASSISTANT_STOP, __version__) from homeassistant.exceptions import ( - HomeAssistantError, InvalidEntityFormatError) + HomeAssistantError, InvalidEntityFormatError, ShuttingDown) from homeassistant.util.async import ( run_coroutine_threadsafe, run_callback_threadsafe) import homeassistant.util as util @@ -309,6 +309,10 @@ class HomeAssistant(object): kwargs = {} exception = context.get('exception') if exception: + # Do not report on shutting down exceptions. + if isinstance(exception, ShuttingDown): + return + kwargs['exc_info'] = (type(exception), exception, exception.__traceback__) @@ -422,7 +426,7 @@ class EventBus(object): """ if event_type != EVENT_HOMEASSISTANT_STOP and \ self._hass.state == CoreState.stopping: - raise HomeAssistantError('Home Assistant is shutting down.') + raise ShuttingDown('Home Assistant is shutting down.') # Copy the list of the current listeners because some listeners # remove themselves as a listener while being executed which @@ -1172,7 +1176,7 @@ def _async_create_timer(hass, interval=TIMER_INTERVAL): EVENT_TIME_CHANGED, {ATTR_NOW: now} ) - except HomeAssistantError: + except ShuttingDown: # HA raises error if firing event after it has shut down break diff --git a/homeassistant/exceptions.py b/homeassistant/exceptions.py index f45fd3c3841..f1ed646b02d 100644 --- a/homeassistant/exceptions.py +++ b/homeassistant/exceptions.py @@ -7,6 +7,12 @@ class HomeAssistantError(Exception): pass +class ShuttingDown(HomeAssistantError): + """When trying to change something during shutdown.""" + + pass + + class InvalidEntityFormatError(HomeAssistantError): """When an invalid formatted entity is encountered.""" From 23fb8c4cddeba13841504c84cace1ac0f4ad3b99 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 17 Nov 2016 21:50:01 -0800 Subject: [PATCH 098/127] Convert script component to async (#4427) --- homeassistant/components/script.py | 58 +++++++++++++++++++----------- tests/components/test_script.py | 5 +++ 2 files changed, 42 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/script.py b/homeassistant/components/script.py index 8c521c33856..df46fb5a03d 100644 --- a/homeassistant/components/script.py +++ b/homeassistant/components/script.py @@ -7,6 +7,7 @@ by the user or automatically based upon automation events, etc. For more details about this component, please refer to the documentation at https://home-assistant.io/components/script/ """ +import asyncio import logging import voluptuous as vol @@ -72,11 +73,13 @@ def toggle(hass, entity_id): hass.services.call(DOMAIN, SERVICE_TOGGLE, {ATTR_ENTITY_ID: entity_id}) -def setup(hass, config): +@asyncio.coroutine +def async_setup(hass, config): """Load the scripts from the configuration.""" component = EntityComponent(_LOGGER, DOMAIN, hass, group_name=GROUP_NAME_ALL_SCRIPTS) + @asyncio.coroutine def service_handler(service): """Execute a service call to script.