From eb55fc8e77a41d6cff821931eb903b8b5f21731b Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Thu, 6 Apr 2017 09:36:57 +0200 Subject: [PATCH 01/32] Update for 0.42 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index bb882ca4ea9..6b13186a730 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 42 -PATCH_VERSION = '0.dev0' +PATCH_VERSION = '0' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 4, 2) From 0bb224d8c74e0eb8be82321cf2328be095926e6f Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Fri, 7 Apr 2017 07:19:08 +0200 Subject: [PATCH 02/32] Initial import for HassIO (#6935) * Initial import for HassIO * Cleanup api code for views * First unittest for view * Add test for edit view * Finish unittest * fix addons test * cleanup service.yaml * Address first round with ping command * handle timeout dynamic * fix lint --- homeassistant/components/hassio.py | 272 +++++++++++++ homeassistant/components/services.yaml | 69 ++++ tests/components/test_hassio.py | 543 +++++++++++++++++++++++++ tests/test_util/aiohttp.py | 4 +- 4 files changed, 887 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/hassio.py create mode 100644 tests/components/test_hassio.py diff --git a/homeassistant/components/hassio.py b/homeassistant/components/hassio.py new file mode 100644 index 00000000000..154be0917bb --- /dev/null +++ b/homeassistant/components/hassio.py @@ -0,0 +1,272 @@ +""" +Exposes regular rest commands as services. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/hassio/ +""" +import asyncio +import logging +import os + +import aiohttp +from aiohttp import web +from aiohttp.web_exceptions import HTTPBadGateway +import async_timeout +import voluptuous as vol + +from homeassistant.config import load_yaml_config_file +from homeassistant.components.http import HomeAssistantView +from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv + +DOMAIN = 'hassio' +DEPENDENCIES = ['http'] + +_LOGGER = logging.getLogger(__name__) + +LONG_TASK_TIMEOUT = 900 +DEFAULT_TIMEOUT = 10 + +SERVICE_HOST_SHUTDOWN = 'host_shutdown' +SERVICE_HOST_REBOOT = 'host_reboot' + +SERVICE_HOST_UPDATE = 'host_update' +SERVICE_SUPERVISOR_UPDATE = 'supervisor_update' +SERVICE_HOMEASSISTANT_UPDATE = 'homeassistant_update' + +SERVICE_ADDON_INSTALL = 'addon_install' +SERVICE_ADDON_UNINSTALL = 'addon_uninstall' +SERVICE_ADDON_UPDATE = 'addon_update' +SERVICE_ADDON_START = 'addon_start' +SERVICE_ADDON_STOP = 'addon_stop' + +ATTR_ADDON = 'addon' +ATTR_VERSION = 'version' + + +SCHEMA_SERVICE_UPDATE = vol.Schema({ + vol.Optional(ATTR_VERSION): cv.string, +}) + +SCHEMA_SERVICE_ADDONS = vol.Schema({ + vol.Required(ATTR_ADDON): cv.slug, +}) + +SCHEMA_SERVICE_ADDONS_VERSION = SCHEMA_SERVICE_ADDONS.extend({ + vol.Optional(ATTR_VERSION): cv.string, +}) + + +SERVICE_MAP = { + SERVICE_HOST_SHUTDOWN: None, + SERVICE_HOST_REBOOT: None, + SERVICE_HOST_UPDATE: SCHEMA_SERVICE_UPDATE, + SERVICE_SUPERVISOR_UPDATE: SCHEMA_SERVICE_UPDATE, + SERVICE_HOMEASSISTANT_UPDATE: SCHEMA_SERVICE_UPDATE, + SERVICE_ADDON_INSTALL: SCHEMA_SERVICE_ADDONS_VERSION, + SERVICE_ADDON_UNINSTALL: SCHEMA_SERVICE_ADDONS, + SERVICE_ADDON_START: SCHEMA_SERVICE_ADDONS, + SERVICE_ADDON_STOP: SCHEMA_SERVICE_ADDONS, + SERVICE_ADDON_UPDATE: SCHEMA_SERVICE_ADDONS_VERSION, +} + + +@asyncio.coroutine +def async_setup(hass, config): + """Setup the hassio component.""" + try: + host = os.environ['HASSIO'] + except KeyError: + _LOGGER.error("No HassIO supervisor detect!") + return False + + websession = async_get_clientsession(hass) + hassio = HassIO(hass.loop, websession, host) + + api_ok = yield from hassio.is_connected() + if not api_ok: + _LOGGER.error("Not connected with HassIO!") + return False + + # register base api views + for base in ('host', 'homeassistant'): + hass.http.register_view(HassIOBaseView(hassio, base)) + for base in ('supervisor', 'network'): + hass.http.register_view(HassIOBaseEditView(hassio, base)) + + # register view for addons + hass.http.register_view(HassIOAddonsView(hassio)) + + @asyncio.coroutine + def async_service_handler(service): + """Handle HassIO service calls.""" + addon = service.data.get(ATTR_ADDON) + if ATTR_VERSION in service.data: + version = {ATTR_VERSION: service.data[ATTR_VERSION]} + else: + version = None + + # map to api call + if service.service == SERVICE_HOST_UPDATE: + yield from hassio.send_command( + "/host/update", payload=version) + elif service.service == SERVICE_HOST_REBOOT: + yield from hassio.send_command("/host/reboot") + elif service.service == SERVICE_HOST_SHUTDOWN: + yield from hassio.send_command("/host/shutdown") + elif service.service == SERVICE_SUPERVISOR_UPDATE: + yield from hassio.send_command( + "/supervisor/update", payload=version) + elif service.service == SERVICE_HOMEASSISTANT_UPDATE: + yield from hassio.send_command( + "/homeassistant/update", payload=version, + timeout=LONG_TASK_TIMEOUT) + elif service.service == SERVICE_ADDON_INSTALL: + yield from hassio.send_command( + "/addons/{}/install".format(addon), payload=version, + timeout=LONG_TASK_TIMEOUT) + elif service.service == SERVICE_ADDON_UNINSTALL: + yield from hassio.send_command( + "/addons/{}/uninstall".format(addon)) + elif service.service == SERVICE_ADDON_START: + yield from hassio.send_command("/addons/{}/start".format(addon)) + elif service.service == SERVICE_ADDON_STOP: + yield from hassio.send_command("/addons/{}/stop".format(addon)) + elif service.service == SERVICE_ADDON_UPDATE: + yield from hassio.send_command( + "/addons/{}/update".format(addon), payload=version, + timeout=LONG_TASK_TIMEOUT) + + descriptions = yield from hass.loop.run_in_executor( + None, load_yaml_config_file, os.path.join( + os.path.dirname(__file__), 'services.yaml')) + + for service, schema in SERVICE_MAP.items(): + hass.services.async_register( + DOMAIN, service, async_service_handler, + descriptions[DOMAIN][service], schema=schema) + + return True + + +class HassIO(object): + """Small API wrapper for HassIO.""" + + def __init__(self, loop, websession, ip): + """Initialze HassIO api.""" + self.loop = loop + self.websession = websession + self._ip = ip + + def is_connected(self): + """Return True if it connected to HassIO supervisor. + + Return a coroutine. + """ + return self.send_command("/supervisor/ping") + + @asyncio.coroutine + def send_command(self, cmd, payload=None, timeout=DEFAULT_TIMEOUT): + """Send request to API.""" + answer = yield from self.send_raw(cmd, payload=payload) + if answer['result'] == 'ok': + return answer['data'] if answer['data'] else True + + _LOGGER.error("%s return error %s.", cmd, answer['message']) + return False + + @asyncio.coroutine + def send_raw(self, cmd, payload=None, timeout=DEFAULT_TIMEOUT): + """Send raw request to API.""" + try: + with async_timeout.timeout(timeout, loop=self.loop): + request = yield from self.websession.get( + "http://{}{}".format(self._ip, cmd), + timeout=None, json=payload + ) + + if request.status != 200: + _LOGGER.error("%s return code %d.", cmd, request.status) + return + + return (yield from request.json()) + + except asyncio.TimeoutError: + _LOGGER.error("Timeout on api request %s.", cmd) + + except aiohttp.ClientError: + _LOGGER.error("Client error on api request %s.", cmd) + + +class HassIOBaseView(HomeAssistantView): + """HassIO view to handle base part.""" + + requires_auth = True + + def __init__(self, hassio, base): + """Initialize a hassio base view.""" + self.hassio = hassio + self._url_info = "/{}/info".format(base) + + self.url = "/api/hassio/{}".format(base) + self.name = "api:hassio:{}".format(base) + + @asyncio.coroutine + def get(self, request): + """Get base data.""" + data = yield from self.hassio.send_command(self._url_info) + if not data: + raise HTTPBadGateway() + return web.json_response(data) + + +class HassIOBaseEditView(HassIOBaseView): + """HassIO view to handle base with options support.""" + + def __init__(self, hassio, base): + """Initialize a hassio base edit view.""" + super().__init__(hassio, base) + self._url_options = "/{}/options".format(base) + + @asyncio.coroutine + def post(self, request): + """Set options on host.""" + data = yield from request.json() + + response = yield from self.hassio.send_raw( + self._url_options, payload=data) + if not response: + raise HTTPBadGateway() + return web.json_response(response) + + +class HassIOAddonsView(HomeAssistantView): + """HassIO view to handle addons part.""" + + requires_auth = True + url = "/api/hassio/addons/{addon}" + name = "api:hassio:addons" + + def __init__(self, hassio): + """Initialize a hassio addon view.""" + self.hassio = hassio + + @asyncio.coroutine + def get(self, request, addon): + """Get addon data.""" + data = yield from self.hassio.send_command( + "/addons/{}/info".format(addon)) + if not data: + raise HTTPBadGateway() + return web.json_response(data) + + @asyncio.coroutine + def post(self, request, addon): + """Set options on host.""" + data = yield from request.json() + + response = yield from self.hassio.send_raw( + "/addons/{}/options".format(addon), payload=data) + if not response: + raise HTTPBadGateway() + return web.json_response(response) diff --git a/homeassistant/components/services.yaml b/homeassistant/components/services.yaml index a28a95969fb..cf5999200d8 100644 --- a/homeassistant/components/services.yaml +++ b/homeassistant/components/services.yaml @@ -316,3 +316,72 @@ ffmpeg: logger: set_level: description: Set log level for components. + +hassio: + host_reboot: + description: Reboot host computer. + + host_shutdown: + description: Poweroff host computer. + + host_update: + description: Update host computer. + fields: + version: + description: Optional or it will be use the latest version. + example: '0.3' + + supervisor_update: + description: Update HassIO supervisor. + fields: + version: + description: Optional or it will be use the latest version. + example: '0.3' + + homeassistant_update: + description: Update HomeAssistant docker image. + fields: + version: + description: Optional or it will be use the latest version. + example: '0.40.1' + + addon_install: + description: Install a HassIO docker addon. + fields: + addon: + description: Name of addon. + example: 'smb_config' + version: + description: Optional or it will be use the latest version. + example: '0.2' + + addon_uninstall: + description: Uninstall a HassIO docker addon. + fields: + addon: + description: Name of addon. + example: 'smb_config' + + addon_update: + description: Update a HassIO docker addon. + fields: + addon: + description: Name of addon. + example: 'smb_config' + version: + description: Optional or it will be use the latest version. + example: '0.2' + + addon_start: + description: Start a HassIO docker addon. + fields: + addon: + description: Name of addon. + example: 'smb_config' + + addon_stop: + description: Stop a HassIO docker addon. + fields: + addon: + description: Name of addon. + example: 'smb_config' diff --git a/tests/components/test_hassio.py b/tests/components/test_hassio.py new file mode 100644 index 00000000000..bde419c4104 --- /dev/null +++ b/tests/components/test_hassio.py @@ -0,0 +1,543 @@ +"""The tests for the hassio component.""" +import asyncio +import os + +import aiohttp + +import homeassistant.components.hassio as ho +from homeassistant.setup import setup_component, async_setup_component + +from tests.common import ( + get_test_home_assistant, assert_setup_component) + + +class TestHassIOSetup(object): + """Test the hassio component.""" + + def setup_method(self): + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() + + self.config = { + ho.DOMAIN: {}, + } + + os.environ['HASSIO'] = "127.0.0.1" + + def teardown_method(self): + """Stop everything that was started.""" + self.hass.stop() + + def test_setup_component(self, aioclient_mock): + """Test setup component.""" + aioclient_mock.get("http://127.0.0.1/supervisor/ping", json={ + 'result': 'ok', 'data': {} + }) + with assert_setup_component(0, ho.DOMAIN): + setup_component(self.hass, ho.DOMAIN, self.config) + + def test_setup_component_test_service(self, aioclient_mock): + """Test setup component and check if service exits.""" + aioclient_mock.get("http://127.0.0.1/supervisor/ping", json={ + 'result': 'ok', 'data': {} + }) + with assert_setup_component(0, ho.DOMAIN): + setup_component(self.hass, ho.DOMAIN, self.config) + + assert self.hass.services.has_service( + ho.DOMAIN, ho.SERVICE_HOST_REBOOT) + assert self.hass.services.has_service( + ho.DOMAIN, ho.SERVICE_HOST_SHUTDOWN) + assert self.hass.services.has_service( + ho.DOMAIN, ho.SERVICE_HOST_UPDATE) + + assert self.hass.services.has_service( + ho.DOMAIN, ho.SERVICE_SUPERVISOR_UPDATE) + + assert self.hass.services.has_service( + ho.DOMAIN, ho.SERVICE_ADDON_INSTALL) + assert self.hass.services.has_service( + ho.DOMAIN, ho.SERVICE_ADDON_UNINSTALL) + assert self.hass.services.has_service( + ho.DOMAIN, ho.SERVICE_ADDON_UPDATE) + assert self.hass.services.has_service( + ho.DOMAIN, ho.SERVICE_ADDON_START) + assert self.hass.services.has_service( + ho.DOMAIN, ho.SERVICE_ADDON_STOP) + + +class TestHassIOComponent(object): + """Test the HassIO component.""" + + def setup_method(self): + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() + self.config = { + ho.DOMAIN: {}, + } + + os.environ['HASSIO'] = "127.0.0.1" + self.url = "http://127.0.0.1/{}" + + self.error_msg = { + 'result': 'error', + 'message': 'Test error', + } + self.ok_msg = { + 'result': 'ok', + 'data': {}, + } + + def teardown_method(self): + """Stop everything that was started.""" + self.hass.stop() + + def test_rest_command_timeout(self, aioclient_mock): + """Call a hassio with timeout.""" + aioclient_mock.get( + "http://127.0.0.1/supervisor/ping", json=self.ok_msg) + with assert_setup_component(0, ho.DOMAIN): + setup_component(self.hass, ho.DOMAIN, self.config) + + aioclient_mock.get( + self.url.format("host/update"), exc=asyncio.TimeoutError()) + + self.hass.services.call(ho.DOMAIN, ho.SERVICE_HOST_UPDATE, {}) + self.hass.block_till_done() + + assert len(aioclient_mock.mock_calls) == 2 + + def test_rest_command_aiohttp_error(self, aioclient_mock): + """Call a hassio with aiohttp exception.""" + aioclient_mock.get( + "http://127.0.0.1/supervisor/ping", json=self.ok_msg) + with assert_setup_component(0, ho.DOMAIN): + setup_component(self.hass, ho.DOMAIN, self.config) + + aioclient_mock.get( + self.url.format("host/update"), exc=aiohttp.ClientError()) + + self.hass.services.call(ho.DOMAIN, ho.SERVICE_HOST_UPDATE, {}) + self.hass.block_till_done() + + assert len(aioclient_mock.mock_calls) == 2 + + def test_rest_command_http_error(self, aioclient_mock): + """Call a hassio with status code 503.""" + aioclient_mock.get( + "http://127.0.0.1/supervisor/ping", json=self.ok_msg) + with assert_setup_component(0, ho.DOMAIN): + setup_component(self.hass, ho.DOMAIN, self.config) + + aioclient_mock.get( + self.url.format("host/update"), status=503) + + self.hass.services.call(ho.DOMAIN, ho.SERVICE_HOST_UPDATE, {}) + self.hass.block_till_done() + + assert len(aioclient_mock.mock_calls) == 2 + + def test_rest_command_http_error_api(self, aioclient_mock): + """Call a hassio with status code 503.""" + aioclient_mock.get( + "http://127.0.0.1/supervisor/ping", json=self.ok_msg) + with assert_setup_component(0, ho.DOMAIN): + setup_component(self.hass, ho.DOMAIN, self.config) + + aioclient_mock.get( + self.url.format("host/update"), json=self.error_msg) + + self.hass.services.call(ho.DOMAIN, ho.SERVICE_HOST_UPDATE, {}) + self.hass.block_till_done() + + assert len(aioclient_mock.mock_calls) == 2 + + def test_rest_command_http_host_reboot(self, aioclient_mock): + """Call a hassio for host reboot.""" + aioclient_mock.get( + "http://127.0.0.1/supervisor/ping", json=self.ok_msg) + with assert_setup_component(0, ho.DOMAIN): + setup_component(self.hass, ho.DOMAIN, self.config) + + aioclient_mock.get( + self.url.format("host/reboot"), json=self.ok_msg) + + self.hass.services.call(ho.DOMAIN, ho.SERVICE_HOST_REBOOT, {}) + self.hass.block_till_done() + + assert len(aioclient_mock.mock_calls) == 2 + + def test_rest_command_http_host_shutdown(self, aioclient_mock): + """Call a hassio for host shutdown.""" + aioclient_mock.get( + "http://127.0.0.1/supervisor/ping", json=self.ok_msg) + with assert_setup_component(0, ho.DOMAIN): + setup_component(self.hass, ho.DOMAIN, self.config) + + aioclient_mock.get( + self.url.format("host/shutdown"), json=self.ok_msg) + + self.hass.services.call(ho.DOMAIN, ho.SERVICE_HOST_SHUTDOWN, {}) + self.hass.block_till_done() + + assert len(aioclient_mock.mock_calls) == 2 + + def test_rest_command_http_host_update(self, aioclient_mock): + """Call a hassio for host update.""" + aioclient_mock.get( + "http://127.0.0.1/supervisor/ping", json=self.ok_msg) + with assert_setup_component(0, ho.DOMAIN): + setup_component(self.hass, ho.DOMAIN, self.config) + + aioclient_mock.get( + self.url.format("host/update"), json=self.ok_msg) + + self.hass.services.call( + ho.DOMAIN, ho.SERVICE_HOST_UPDATE, {'version': '0.4'}) + self.hass.block_till_done() + + assert len(aioclient_mock.mock_calls) == 2 + assert aioclient_mock.mock_calls[-1][2]['version'] == '0.4' + + def test_rest_command_http_supervisor_update(self, aioclient_mock): + """Call a hassio for supervisor update.""" + aioclient_mock.get( + "http://127.0.0.1/supervisor/ping", json=self.ok_msg) + with assert_setup_component(0, ho.DOMAIN): + setup_component(self.hass, ho.DOMAIN, self.config) + + aioclient_mock.get( + self.url.format("supervisor/update"), json=self.ok_msg) + + self.hass.services.call( + ho.DOMAIN, ho.SERVICE_SUPERVISOR_UPDATE, {'version': '0.4'}) + self.hass.block_till_done() + + assert len(aioclient_mock.mock_calls) == 2 + assert aioclient_mock.mock_calls[-1][2]['version'] == '0.4' + + def test_rest_command_http_homeassistant_update(self, aioclient_mock): + """Call a hassio for homeassistant update.""" + aioclient_mock.get( + "http://127.0.0.1/supervisor/ping", json=self.ok_msg) + with assert_setup_component(0, ho.DOMAIN): + setup_component(self.hass, ho.DOMAIN, self.config) + + aioclient_mock.get( + self.url.format("homeassistant/update"), json=self.ok_msg) + + self.hass.services.call( + ho.DOMAIN, ho.SERVICE_HOMEASSISTANT_UPDATE, {'version': '0.4'}) + self.hass.block_till_done() + + assert len(aioclient_mock.mock_calls) == 2 + assert aioclient_mock.mock_calls[-1][2]['version'] == '0.4' + + def test_rest_command_http_addon_install(self, aioclient_mock): + """Call a hassio for addon install.""" + aioclient_mock.get( + "http://127.0.0.1/supervisor/ping", json=self.ok_msg) + with assert_setup_component(0, ho.DOMAIN): + setup_component(self.hass, ho.DOMAIN, self.config) + + aioclient_mock.get( + self.url.format("addons/smb_config/install"), json=self.ok_msg) + + self.hass.services.call( + ho.DOMAIN, ho.SERVICE_ADDON_INSTALL, { + 'addon': 'smb_config', + 'version': '0.4' + }) + self.hass.block_till_done() + + assert len(aioclient_mock.mock_calls) == 2 + assert aioclient_mock.mock_calls[-1][2]['version'] == '0.4' + + def test_rest_command_http_addon_uninstall(self, aioclient_mock): + """Call a hassio for addon uninstall.""" + aioclient_mock.get( + "http://127.0.0.1/supervisor/ping", json=self.ok_msg) + with assert_setup_component(0, ho.DOMAIN): + setup_component(self.hass, ho.DOMAIN, self.config) + + aioclient_mock.get( + self.url.format("addons/smb_config/uninstall"), json=self.ok_msg) + + self.hass.services.call( + ho.DOMAIN, ho.SERVICE_ADDON_UNINSTALL, { + 'addon': 'smb_config' + }) + self.hass.block_till_done() + + assert len(aioclient_mock.mock_calls) == 2 + + def test_rest_command_http_addon_update(self, aioclient_mock): + """Call a hassio for addon update.""" + aioclient_mock.get( + "http://127.0.0.1/supervisor/ping", json=self.ok_msg) + with assert_setup_component(0, ho.DOMAIN): + setup_component(self.hass, ho.DOMAIN, self.config) + + aioclient_mock.get( + self.url.format("addons/smb_config/update"), json=self.ok_msg) + + self.hass.services.call( + ho.DOMAIN, ho.SERVICE_ADDON_UPDATE, { + 'addon': 'smb_config', + 'version': '0.4' + }) + self.hass.block_till_done() + + assert len(aioclient_mock.mock_calls) == 2 + assert aioclient_mock.mock_calls[-1][2]['version'] == '0.4' + + def test_rest_command_http_addon_start(self, aioclient_mock): + """Call a hassio for addon start.""" + aioclient_mock.get( + "http://127.0.0.1/supervisor/ping", json=self.ok_msg) + with assert_setup_component(0, ho.DOMAIN): + setup_component(self.hass, ho.DOMAIN, self.config) + + aioclient_mock.get( + self.url.format("addons/smb_config/start"), json=self.ok_msg) + + self.hass.services.call( + ho.DOMAIN, ho.SERVICE_ADDON_START, { + 'addon': 'smb_config', + }) + self.hass.block_till_done() + + assert len(aioclient_mock.mock_calls) == 2 + + def test_rest_command_http_addon_stop(self, aioclient_mock): + """Call a hassio for addon stop.""" + aioclient_mock.get( + "http://127.0.0.1/supervisor/ping", json=self.ok_msg) + with assert_setup_component(0, ho.DOMAIN): + setup_component(self.hass, ho.DOMAIN, self.config) + + aioclient_mock.get( + self.url.format("addons/smb_config/stop"), json=self.ok_msg) + + self.hass.services.call( + ho.DOMAIN, ho.SERVICE_ADDON_STOP, { + 'addon': 'smb_config' + }) + self.hass.block_till_done() + + assert len(aioclient_mock.mock_calls) == 2 + + +@asyncio.coroutine +def test_async_hassio_host_view(aioclient_mock, hass, test_client): + """Test that it fetches the given url.""" + os.environ['HASSIO'] = "127.0.0.1" + + aioclient_mock.get("http://127.0.0.1/supervisor/ping", json={ + 'result': 'ok', 'data': {} + }) + result = yield from async_setup_component(hass, ho.DOMAIN, {ho.DOMAIN: {}}) + assert result, 'Failed to setup hasio' + + client = yield from test_client(hass.http.app) + + aioclient_mock.get('http://127.0.0.1/host/info', json={ + 'result': 'ok', + 'data': { + 'os': 'resinos', + 'version': '0.3', + 'current': '0.4', + 'level': 16, + 'hostname': 'test', + } + }) + + resp = yield from client.get('/api/hassio/host') + data = yield from resp.json() + + assert len(aioclient_mock.mock_calls) == 2 + assert resp.status == 200 + assert data['os'] == 'resinos' + assert data['version'] == '0.3' + assert data['current'] == '0.4' + assert data['level'] == 16 + assert data['hostname'] == 'test' + + +@asyncio.coroutine +def test_async_hassio_homeassistant_view(aioclient_mock, hass, test_client): + """Test that it fetches the given url.""" + os.environ['HASSIO'] = "127.0.0.1" + + aioclient_mock.get("http://127.0.0.1/supervisor/ping", json={ + 'result': 'ok', 'data': {} + }) + result = yield from async_setup_component(hass, ho.DOMAIN, {ho.DOMAIN: {}}) + assert result, 'Failed to setup hasio' + + client = yield from test_client(hass.http.app) + + aioclient_mock.get('http://127.0.0.1/homeassistant/info', json={ + 'result': 'ok', + 'data': { + 'version': '0.41', + 'current': '0.41.1', + } + }) + + resp = yield from client.get('/api/hassio/homeassistant') + data = yield from resp.json() + + assert len(aioclient_mock.mock_calls) == 2 + assert resp.status == 200 + assert data['version'] == '0.41' + assert data['current'] == '0.41.1' + + +@asyncio.coroutine +def test_async_hassio_supervisor_view(aioclient_mock, hass, test_client): + """Test that it fetches the given url.""" + os.environ['HASSIO'] = "127.0.0.1" + + aioclient_mock.get("http://127.0.0.1/supervisor/ping", json={ + 'result': 'ok', 'data': {} + }) + result = yield from async_setup_component(hass, ho.DOMAIN, {ho.DOMAIN: {}}) + assert result, 'Failed to setup hasio' + + client = yield from test_client(hass.http.app) + + aioclient_mock.get('http://127.0.0.1/supervisor/info', json={ + 'result': 'ok', + 'data': { + 'version': '0.3', + 'current': '0.4', + 'beta': False, + } + }) + + resp = yield from client.get('/api/hassio/supervisor') + data = yield from resp.json() + + assert len(aioclient_mock.mock_calls) == 2 + assert resp.status == 200 + assert data['version'] == '0.3' + assert data['current'] == '0.4' + assert not data['beta'] + + aioclient_mock.get('http://127.0.0.1/supervisor/options', json={ + 'result': 'ok', + 'data': {}, + }) + + resp = yield from client.post('/api/hassio/supervisor', json={ + 'beta': True, + }) + data = yield from resp.json() + + assert len(aioclient_mock.mock_calls) == 3 + assert resp.status == 200 + assert aioclient_mock.mock_calls[-1][2]['beta'] + + +@asyncio.coroutine +def test_async_hassio_network_view(aioclient_mock, hass, test_client): + """Test that it fetches the given url.""" + os.environ['HASSIO'] = "127.0.0.1" + + aioclient_mock.get("http://127.0.0.1/supervisor/ping", json={ + 'result': 'ok', 'data': {} + }) + result = yield from async_setup_component(hass, ho.DOMAIN, {ho.DOMAIN: {}}) + assert result, 'Failed to setup hasio' + + client = yield from test_client(hass.http.app) + + aioclient_mock.get('http://127.0.0.1/network/info', json={ + 'result': 'ok', + 'data': { + 'mode': 'dhcp', + 'ssid': 'my_wlan', + 'password': '123456', + } + }) + + resp = yield from client.get('/api/hassio/network') + data = yield from resp.json() + + assert len(aioclient_mock.mock_calls) == 2 + assert resp.status == 200 + assert data['mode'] == 'dhcp' + assert data['ssid'] == 'my_wlan' + assert data['password'] == '123456' + + aioclient_mock.get('http://127.0.0.1/network/options', json={ + 'result': 'ok', + 'data': {}, + }) + + resp = yield from client.post('/api/hassio/network', json={ + 'mode': 'dhcp', + 'ssid': 'my_wlan2', + 'password': '654321', + }) + data = yield from resp.json() + + assert len(aioclient_mock.mock_calls) == 3 + assert resp.status == 200 + assert aioclient_mock.mock_calls[-1][2]['ssid'] == 'my_wlan2' + assert aioclient_mock.mock_calls[-1][2]['password'] == '654321' + + +@asyncio.coroutine +def test_async_hassio_addon_view(aioclient_mock, hass, test_client): + """Test that it fetches the given url.""" + os.environ['HASSIO'] = "127.0.0.1" + + aioclient_mock.get("http://127.0.0.1/supervisor/ping", json={ + 'result': 'ok', 'data': {} + }) + result = yield from async_setup_component(hass, ho.DOMAIN, {ho.DOMAIN: {}}) + assert result, 'Failed to setup hasio' + + client = yield from test_client(hass.http.app) + + aioclient_mock.get('http://127.0.0.1/addons/smb_config/info', json={ + 'result': 'ok', + 'data': { + 'name': 'SMB Config', + 'state': 'running', + 'boot': 'auto', + 'options': { + 'bla': False, + } + } + }) + + resp = yield from client.get('/api/hassio/addons/smb_config') + data = yield from resp.json() + + assert len(aioclient_mock.mock_calls) == 2 + assert resp.status == 200 + assert data['name'] == 'SMB Config' + assert data['state'] == 'running' + assert data['boot'] == 'auto' + assert not data['options']['bla'] + + aioclient_mock.get('http://127.0.0.1/addons/smb_config/options', json={ + 'result': 'ok', + 'data': {}, + }) + + resp = yield from client.post('/api/hassio/addons/smb_config', json={ + 'boot': 'manual', + 'options': { + 'bla': True, + } + }) + data = yield from resp.json() + + assert len(aioclient_mock.mock_calls) == 3 + assert resp.status == 200 + assert aioclient_mock.mock_calls[-1][2]['boot'] == 'manual' + assert aioclient_mock.mock_calls[-1][2]['options']['bla'] diff --git a/tests/test_util/aiohttp.py b/tests/test_util/aiohttp.py index 23e24cac0cd..39e926ab7e7 100644 --- a/tests/test_util/aiohttp.py +++ b/tests/test_util/aiohttp.py @@ -75,8 +75,10 @@ class AiohttpClientMocker: @asyncio.coroutine # pylint: disable=unused-variable def match_request(self, method, url, *, data=None, auth=None, params=None, - headers=None, allow_redirects=None): + headers=None, allow_redirects=None, timeout=None, + json=None): """Match a request against pre-registered requests.""" + data = data or json for response in self._mocks: if response.match_request(method, url, params): self.mock_calls.append((method, url, data)) From 6d8af5889159cd0ff495753f2823668eaee6bb64 Mon Sep 17 00:00:00 2001 From: Teemu R Date: Fri, 7 Apr 2017 07:41:47 +0200 Subject: [PATCH 03/32] light.yeelight: catch i/o related exceptions from the backend lib (#6952) Fixes/mitigates problems with #5949 and #6624 --- homeassistant/components/light/yeelight.py | 50 ++++++++++++++++------ 1 file changed, 38 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/light/yeelight.py b/homeassistant/components/light/yeelight.py index 65b32786ce7..9253f471431 100644 --- a/homeassistant/components/light/yeelight.py +++ b/homeassistant/components/light/yeelight.py @@ -255,7 +255,8 @@ class YeelightLight(Light): def set_flash(self, flash) -> None: """Activate flash.""" if flash: - from yeelight import RGBTransition, SleepTransition, Flow + from yeelight import (RGBTransition, SleepTransition, Flow, + BulbException) if self._bulb.last_properties["color_mode"] != 1: _LOGGER.error("Flash supported currently only in RGB mode.") return @@ -280,10 +281,14 @@ class YeelightLight(Light): duration=duration)) flow = Flow(count=count, transitions=transitions) - self._bulb.start_flow(flow) + try: + self._bulb.start_flow(flow) + except BulbException as ex: + _LOGGER.error("Unable to set flash: %s", ex) def turn_on(self, **kwargs) -> None: """Turn the bulb on.""" + import yeelight brightness = kwargs.get(ATTR_BRIGHTNESS) colortemp = kwargs.get(ATTR_COLOR_TEMP) rgb = kwargs.get(ATTR_RGB_COLOR) @@ -293,22 +298,43 @@ class YeelightLight(Light): if ATTR_TRANSITION in kwargs: # passed kwarg overrides config duration = int(kwargs.get(ATTR_TRANSITION) * 1000) # kwarg in s - self._bulb.turn_on(duration=duration) + try: + self._bulb.turn_on(duration=duration) + except yeelight.BulbException as ex: + _LOGGER.error("Unable to turn the bulb on: %s", ex) + return if self.config[CONF_MODE_MUSIC] and not self._bulb.music_mode: - self.set_music_mode(self.config[CONF_MODE_MUSIC]) + try: + self.set_music_mode(self.config[CONF_MODE_MUSIC]) + except yeelight.BulbException as ex: + _LOGGER.error("Unable to turn on music mode," + "consider disabling it: %s", ex) - # values checked for none in methods - self.set_rgb(rgb, duration) - self.set_colortemp(colortemp, duration) - self.set_brightness(brightness, duration) - self.set_flash(flash) + try: + # values checked for none in methods + self.set_rgb(rgb, duration) + self.set_colortemp(colortemp, duration) + self.set_brightness(brightness, duration) + self.set_flash(flash) + except yeelight.BulbException as ex: + _LOGGER.error("Unable to set bulb properties: %s", ex) + return # save the current state if we had a manual change. - if self.config[CONF_SAVE_ON_CHANGE]: - if brightness or colortemp or rgb: + if self.config[CONF_SAVE_ON_CHANGE] and (brightness + or colortemp + or rgb): + try: self.set_default() + except yeelight.BulbException as ex: + _LOGGER.error("Unable to set the defaults: %s", ex) + return def turn_off(self, **kwargs) -> None: """Turn off.""" - self._bulb.turn_off() + import yeelight + try: + self._bulb.turn_off() + except yeelight.BulbException as ex: + _LOGGER.error("Unable to turn the bulb off: %s", ex) From 69dee168a10f7aaf6640d07f37fd56655deefc2c Mon Sep 17 00:00:00 2001 From: aufano Date: Fri, 7 Apr 2017 02:40:59 +0200 Subject: [PATCH 04/32] Fix current_temperature is rounded (#6960) * Fix current_temperature is rounded * fix Unnecessary parens after 'if' --- homeassistant/components/climate/__init__.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/climate/__init__.py b/homeassistant/components/climate/__init__.py index bd2e38433d6..2a0bd2eb5c1 100644 --- a/homeassistant/components/climate/__init__.py +++ b/homeassistant/components/climate/__init__.py @@ -692,18 +692,16 @@ class ClimateDevice(Entity): def _convert_for_display(self, temp): """Convert temperature into preferred units for display purposes.""" - if (temp is None or not isinstance(temp, Number) or - self.temperature_unit == self.unit_of_measurement): + if temp is None or not isinstance(temp, Number): return temp - - value = convert_temperature(temp, self.temperature_unit, - self.unit_of_measurement) - + if self.temperature_unit != self.unit_of_measurement: + temp = convert_temperature(temp, self.temperature_unit, + self.unit_of_measurement) # Round in the units appropriate if self.precision == PRECISION_HALVES: - return round(value * 2) / 2.0 + return round(temp * 2) / 2.0 elif self.precision == PRECISION_TENTHS: - return round(value, 1) + return round(temp, 1) else: # PRECISION_WHOLE as a fall back - return round(value) + return round(temp) From 5d1dbd61b28c1108a20fac5d0382a085dec2cd1e Mon Sep 17 00:00:00 2001 From: Andrey Date: Fri, 7 Apr 2017 08:39:35 +0300 Subject: [PATCH 05/32] Preserve customize glob order. (#6963) * Preserve customize glob order. * add tests --- homeassistant/components/zwave/__init__.py | 2 +- homeassistant/config.py | 2 +- tests/components/zwave/test_init.py | 15 +++++++++++++++ tests/test_config.py | 7 +++++++ 4 files changed, 24 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zwave/__init__.py b/homeassistant/components/zwave/__init__.py index f8903fd28cf..254b488ccbb 100755 --- a/homeassistant/components/zwave/__init__.py +++ b/homeassistant/components/zwave/__init__.py @@ -124,7 +124,7 @@ CONFIG_SCHEMA = vol.Schema({ vol.Optional(CONF_DEVICE_CONFIG, default={}): vol.Schema({cv.entity_id: DEVICE_CONFIG_SCHEMA_ENTRY}), vol.Optional(CONF_DEVICE_CONFIG_GLOB, default={}): - vol.Schema({cv.string: DEVICE_CONFIG_SCHEMA_ENTRY}), + cv.ordered_dict(DEVICE_CONFIG_SCHEMA_ENTRY, cv.string), vol.Optional(CONF_DEVICE_CONFIG_DOMAIN, default={}): vol.Schema({cv.string: DEVICE_CONFIG_SCHEMA_ENTRY}), vol.Optional(CONF_DEBUG, default=DEFAULT_DEBUG): cv.boolean, diff --git a/homeassistant/config.py b/homeassistant/config.py index 3968ea571c5..1508493a6d1 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -108,7 +108,7 @@ CUSTOMIZE_CONFIG_SCHEMA = vol.Schema({ vol.Optional(CONF_CUSTOMIZE_DOMAIN, default={}): vol.Schema({cv.string: dict}), vol.Optional(CONF_CUSTOMIZE_GLOB, default={}): - vol.Schema({cv.string: dict}), + cv.ordered_dict(OrderedDict, cv.string), }) CORE_CONFIG_SCHEMA = CUSTOMIZE_CONFIG_SCHEMA.extend({ diff --git a/tests/components/zwave/test_init.py b/tests/components/zwave/test_init.py index b982b357849..14b2a0226fe 100644 --- a/tests/components/zwave/test_init.py +++ b/tests/components/zwave/test_init.py @@ -1,7 +1,11 @@ """Tests for the Z-Wave init.""" import asyncio +import unittest +from collections import OrderedDict from homeassistant.bootstrap import async_setup_component +from homeassistant.components.zwave import ( + CONFIG_SCHEMA, CONF_DEVICE_CONFIG_GLOB) @asyncio.coroutine @@ -34,3 +38,14 @@ def test_invalid_device_config(hass, mock_openzwave): }}) assert not result + + +class TestZwave(unittest.TestCase): + """Test zwave init.""" + + def test_device_config_glob_is_ordered(self): + """Test that device_config_glob preserves order.""" + conf = CONFIG_SCHEMA( + {'zwave': {CONF_DEVICE_CONFIG_GLOB: OrderedDict()}}) + self.assertIsInstance( + conf['zwave'][CONF_DEVICE_CONFIG_GLOB], OrderedDict) diff --git a/tests/test_config.py b/tests/test_config.py index 990bd557e70..1d1208f8859 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -3,6 +3,7 @@ import os import unittest import unittest.mock as mock +from collections import OrderedDict import pytest from voluptuous import MultipleInvalid @@ -205,6 +206,12 @@ class TestConfig(unittest.TestCase): }, }) + def test_customize_glob_is_ordered(self): + """Test that customize_glob preserves order.""" + conf = config_util.CORE_CONFIG_SCHEMA( + {'customize_glob': OrderedDict()}) + self.assertIsInstance(conf['customize_glob'], OrderedDict) + def _compute_state(self, config): run_coroutine_threadsafe( config_util.async_process_ha_core_config(self.hass, config), From 382519e0820de5cbb0e8824c929482ba96bbb03e Mon Sep 17 00:00:00 2001 From: viswa-swami Date: Fri, 7 Apr 2017 01:40:33 -0400 Subject: [PATCH 06/32] Foscam Camera: Adding exception handling when fetching the camera image to avoid python exception errors when host is not reachable or rather any url error to camera (#6964) * Adding exception handling when fetching the camera image to avoid python errors when host is not reachable or any url errors to camera * Added exception as ConnectionError instead of plain except * Added exception as ConnectionError instead of plain except. Removed the unused error handle --- homeassistant/components/camera/foscam.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/camera/foscam.py b/homeassistant/components/camera/foscam.py index a374d19f4d1..c1f9513d2c6 100644 --- a/homeassistant/components/camera/foscam.py +++ b/homeassistant/components/camera/foscam.py @@ -66,9 +66,13 @@ class FoscamCamera(Camera): def camera_image(self): """Return a still image reponse from the camera.""" # Send the request to snap a picture and return raw jpg data - response = requests.get(self._snap_picture_url, timeout=10) - - return response.content + # Handle exception if host is not reachable or url failed + try: + response = requests.get(self._snap_picture_url, timeout=10) + except requests.exceptions.ConnectionError: + return None + else: + return response.content @property def name(self): From 01672e63eaf9272f9d5d1fc748d0e7b799e2cad5 Mon Sep 17 00:00:00 2001 From: happyleavesaoc Date: Fri, 7 Apr 2017 01:47:03 -0400 Subject: [PATCH 07/32] Crime Reports sensor (#6966) * add crimereports * add crimereports metadata * implicit interval * remove zone support --- .coveragerc | 1 + .../components/sensor/crimereports.py | 123 ++++++++++++++++++ requirements_all.txt | 3 + 3 files changed, 127 insertions(+) create mode 100644 homeassistant/components/sensor/crimereports.py diff --git a/.coveragerc b/.coveragerc index edf66c6252b..3d32256e9fb 100644 --- a/.coveragerc +++ b/.coveragerc @@ -326,6 +326,7 @@ omit = homeassistant/components/sensor/coinmarketcap.py homeassistant/components/sensor/comed_hourly_pricing.py homeassistant/components/sensor/cpuspeed.py + homeassistant/components/sensor/crimereports.py homeassistant/components/sensor/cups.py homeassistant/components/sensor/currencylayer.py homeassistant/components/sensor/darksky.py diff --git a/homeassistant/components/sensor/crimereports.py b/homeassistant/components/sensor/crimereports.py new file mode 100644 index 00000000000..8f118fc3c32 --- /dev/null +++ b/homeassistant/components/sensor/crimereports.py @@ -0,0 +1,123 @@ +""" +Sensor for Crime Reports. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.crimereports/ +""" +from collections import defaultdict +from datetime import timedelta +import logging + +import voluptuous as vol + +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import ( + CONF_INCLUDE, CONF_EXCLUDE, CONF_NAME, CONF_LATITUDE, CONF_LONGITUDE, + ATTR_ATTRIBUTION, ATTR_LATITUDE, ATTR_LONGITUDE, + LENGTH_KILOMETERS, LENGTH_METERS) +from homeassistant.helpers.entity import Entity +from homeassistant.util import slugify +from homeassistant.util.distance import convert +from homeassistant.util.dt import now +import homeassistant.helpers.config_validation as cv + +REQUIREMENTS = ['crimereports==1.0.0'] + +_LOGGER = logging.getLogger(__name__) + +SCAN_INTERVAL = timedelta(minutes=30) +DOMAIN = 'crimereports' +EVENT_INCIDENT = '{}_incident'.format(DOMAIN) +CONF_RADIUS = 'radius' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_NAME): cv.string, + vol.Required(CONF_RADIUS): vol.Coerce(float), + vol.Inclusive(CONF_LATITUDE, 'coordinates'): cv.latitude, + vol.Inclusive(CONF_LONGITUDE, 'coordinates'): cv.longitude, + vol.Optional(CONF_INCLUDE): vol.All(cv.ensure_list, [cv.string]), + vol.Optional(CONF_EXCLUDE): vol.All(cv.ensure_list, [cv.string]) +}) + + +# pylint: disable=unused-argument +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the Crime Reports platform.""" + latitude = config.get(CONF_LATITUDE, hass.config.latitude) + longitude = config.get(CONF_LONGITUDE, hass.config.longitude) + add_devices([CrimeReportsSensor(hass, config.get(CONF_NAME), + latitude, longitude, + config.get(CONF_RADIUS), + config.get(CONF_INCLUDE), + config.get(CONF_EXCLUDE))], True) + + +class CrimeReportsSensor(Entity): + """Crime Reports Sensor.""" + + def __init__(self, hass, name, latitude, longitude, radius, + include, exclude): + """Initialize the sensor.""" + import crimereports + self._hass = hass + self._name = name + self._include = include + self._exclude = exclude + radius_kilometers = convert(radius, LENGTH_METERS, LENGTH_KILOMETERS) + self._crimereports = crimereports.CrimeReports((latitude, longitude), + radius_kilometers) + self._attributes = None + self._state = None + self._previous_incidents = set() + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return self._attributes + + def _incident_event(self, incident): + data = { + 'type': incident.get('type'), + 'description': incident.get('friendly_description'), + 'timestamp': incident.get('timestamp'), + 'location': incident.get('location') + } + if incident.get('coordinates'): + data.update({ + ATTR_LATITUDE: incident.get('coordinates')[0], + ATTR_LONGITUDE: incident.get('coordinates')[1] + }) + self._hass.bus.fire(EVENT_INCIDENT, data) + + def update(self): + """Update device state.""" + import crimereports + incident_counts = defaultdict(int) + incidents = self._crimereports.get_incidents(now().date(), + include=self._include, + exclude=self._exclude) + fire_events = len(self._previous_incidents) > 0 + if len(incidents) < len(self._previous_incidents): + self._previous_incidents = set() + for incident in incidents: + incident_type = slugify(incident.get('type')) + incident_counts[incident_type] += 1 + if (fire_events and incident.get('id') + not in self._previous_incidents): + self._incident_event(incident) + self._previous_incidents.add(incident.get('id')) + self._attributes = { + ATTR_ATTRIBUTION: crimereports.ATTRIBUTION + } + self._attributes.update(incident_counts) + self._state = len(incidents) diff --git a/requirements_all.txt b/requirements_all.txt index 86305ed8722..a8f8da0ee9e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -108,6 +108,9 @@ colorlog>2.1,<3 # homeassistant.components.binary_sensor.concord232 concord232==0.14 +# homeassistant.components.sensor.crimereports +crimereports==1.0.0 + # homeassistant.components.sensor.metoffice # homeassistant.components.weather.metoffice datapoint==0.4.3 From d3c1a48475c69e0eb92539cf145326a42a4cc7c8 Mon Sep 17 00:00:00 2001 From: Adam Mills Date: Thu, 6 Apr 2017 18:12:24 -0400 Subject: [PATCH 08/32] Update kodi for aiohttp2 (#6967) --- homeassistant/components/media_player/kodi.py | 2 +- homeassistant/components/notify/kodi.py | 2 +- requirements_all.txt | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/media_player/kodi.py b/homeassistant/components/media_player/kodi.py index 97b10d03f0e..a137a332f7e 100644 --- a/homeassistant/components/media_player/kodi.py +++ b/homeassistant/components/media_player/kodi.py @@ -27,7 +27,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.deprecation import get_deprecated -REQUIREMENTS = ['jsonrpc-async==0.4', 'jsonrpc-websocket==0.3'] +REQUIREMENTS = ['jsonrpc-async==0.6', 'jsonrpc-websocket==0.5'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/notify/kodi.py b/homeassistant/components/notify/kodi.py index be0d94afd55..db72fff37d5 100644 --- a/homeassistant/components/notify/kodi.py +++ b/homeassistant/components/notify/kodi.py @@ -19,7 +19,7 @@ from homeassistant.components.notify import ( from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['jsonrpc-async==0.4'] +REQUIREMENTS = ['jsonrpc-async==0.6'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index a8f8da0ee9e..01539981b8f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -340,10 +340,10 @@ insteonplm==0.7.4 # homeassistant.components.media_player.kodi # homeassistant.components.notify.kodi -jsonrpc-async==0.4 +jsonrpc-async==0.6 # homeassistant.components.media_player.kodi -jsonrpc-websocket==0.3 +jsonrpc-websocket==0.5 # homeassistant.scripts.keyring keyring>=9.3,<10.0 From a93c01788d63bb47072a9d763537238cd0bbf74c Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Fri, 7 Apr 2017 06:00:58 +0200 Subject: [PATCH 09/32] Bugfix time and task coro (#6968) * Bugfix time and task coro * fix also other create_task * fix tests * fix lint in test --- homeassistant/core.py | 22 +++++++++------------- tests/test_core.py | 44 +++++++++++++++++-------------------------- 2 files changed, 26 insertions(+), 40 deletions(-) diff --git a/homeassistant/core.py b/homeassistant/core.py index 7c4ff43bd7a..03cc3d4e9f5 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -31,7 +31,8 @@ from homeassistant.const import ( from homeassistant.exceptions import ( HomeAssistantError, InvalidEntityFormatError) from homeassistant.util.async import ( - run_coroutine_threadsafe, run_callback_threadsafe) + run_coroutine_threadsafe, run_callback_threadsafe, + fire_coroutine_threadsafe) import homeassistant.util as util import homeassistant.util.dt as dt_util import homeassistant.util.location as location @@ -131,7 +132,7 @@ class HomeAssistant(object): def start(self) -> None: """Start home assistant.""" # Register the async start - self.add_job(self.async_start()) + fire_coroutine_threadsafe(self.async_start(), self.loop) # Run forever and catch keyboard interrupt try: @@ -140,7 +141,7 @@ class HomeAssistant(object): self.loop.run_forever() return self.exit_code except KeyboardInterrupt: - self.loop.create_task(self.async_stop()) + fire_coroutine_threadsafe(self.async_stop(), self.loop) self.loop.run_forever() finally: self.loop.close() @@ -246,8 +247,7 @@ class HomeAssistant(object): def stop(self) -> None: """Stop Home Assistant and shuts down all threads.""" - self.loop.call_soon_threadsafe( - self.loop.create_task, self.async_stop()) + fire_coroutine_threadsafe(self.async_stop(), self.loop) @asyncio.coroutine def async_stop(self, exit_code=0) -> None: @@ -1091,17 +1091,13 @@ def _async_create_timer(hass): handle = hass.loop.call_later(slp_seconds, fire_time_event, nxt) - @callback - def start_timer(event): - """Create an async timer.""" - _LOGGER.info("Timer:starting") - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_timer) - fire_time_event(monotonic()) - @callback def stop_timer(event): """Stop the timer.""" if handle is not None: handle.cancel() - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, start_timer) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_timer) + + _LOGGER.info("Timer:starting") + fire_time_event(monotonic()) diff --git a/tests/test_core.py b/tests/test_core.py index a68373b2ecc..ea7f40aaff7 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -16,8 +16,7 @@ from homeassistant.util.unit_system import (METRIC_SYSTEM) from homeassistant.const import ( __version__, EVENT_STATE_CHANGED, ATTR_FRIENDLY_NAME, CONF_UNIT_SYSTEM, ATTR_NOW, EVENT_TIME_CHANGED, EVENT_HOMEASSISTANT_STOP, - EVENT_HOMEASSISTANT_CLOSE, EVENT_HOMEASSISTANT_START, - EVENT_SERVICE_REGISTERED, EVENT_SERVICE_REMOVED) + EVENT_HOMEASSISTANT_CLOSE, EVENT_SERVICE_REGISTERED, EVENT_SERVICE_REMOVED) from tests.common import get_test_home_assistant @@ -813,28 +812,21 @@ def test_create_timer(mock_monotonic, loop): funcs.append(func) return orig_callback(func) - with patch.object(ha, 'callback', mock_callback): - ha._async_create_timer(hass) - - assert len(funcs) == 3 - fire_time_event, start_timer, stop_timer = funcs - - assert len(hass.bus.async_listen_once.mock_calls) == 1 - event_type, callback = hass.bus.async_listen_once.mock_calls[0][1] - assert event_type == EVENT_HOMEASSISTANT_START - assert callback is start_timer - mock_monotonic.side_effect = 10.2, 10.3 - with patch('homeassistant.core.dt_util.utcnow', - return_value=sentinel.mock_date): - start_timer(None) + with patch.object(ha, 'callback', mock_callback), \ + patch('homeassistant.core.dt_util.utcnow', + return_value=sentinel.mock_date): + ha._async_create_timer(hass) - assert len(hass.bus.async_listen_once.mock_calls) == 2 + assert len(funcs) == 2 + fire_time_event, stop_timer = funcs + + assert len(hass.bus.async_listen_once.mock_calls) == 1 assert len(hass.bus.async_fire.mock_calls) == 1 assert len(hass.loop.call_later.mock_calls) == 1 - event_type, callback = hass.bus.async_listen_once.mock_calls[1][1] + event_type, callback = hass.bus.async_listen_once.mock_calls[0][1] assert event_type == EVENT_HOMEASSISTANT_STOP assert callback is stop_timer @@ -859,17 +851,15 @@ def test_timer_out_of_sync(mock_monotonic, loop): funcs.append(func) return orig_callback(func) - with patch.object(ha, 'callback', mock_callback): - ha._async_create_timer(hass) - - assert len(funcs) == 3 - fire_time_event, start_timer, stop_timer = funcs - mock_monotonic.side_effect = 10.2, 11.3, 11.3 - with patch('homeassistant.core.dt_util.utcnow', - return_value=sentinel.mock_date): - start_timer(None) + with patch.object(ha, 'callback', mock_callback), \ + patch('homeassistant.core.dt_util.utcnow', + return_value=sentinel.mock_date): + ha._async_create_timer(hass) + + assert len(funcs) == 2 + fire_time_event, stop_timer = funcs assert len(hass.loop.call_later.mock_calls) == 1 From d8558ad173b95e1b2b39c26b9b83177a4f85c256 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 7 Apr 2017 12:02:49 -0700 Subject: [PATCH 10/32] Fix control+c quitting HASS (#6974) --- homeassistant/core.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/core.py b/homeassistant/core.py index 03cc3d4e9f5..ba19f392931 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -141,7 +141,8 @@ class HomeAssistant(object): self.loop.run_forever() return self.exit_code except KeyboardInterrupt: - fire_coroutine_threadsafe(self.async_stop(), self.loop) + self.loop.call_soon_threadsafe( + self.loop.create_task, self.async_stop()) self.loop.run_forever() finally: self.loop.close() From 660b1b616b9e918c6ec6f1ded0878885db1dcbe8 Mon Sep 17 00:00:00 2001 From: John Mihalic Date: Fri, 7 Apr 2017 22:17:10 -0400 Subject: [PATCH 11/32] Update Emby for aiohttp v2 (#6981) --- homeassistant/components/media_player/emby.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_player/emby.py b/homeassistant/components/media_player/emby.py index 3dc0131531f..3ed6d42e76a 100644 --- a/homeassistant/components/media_player/emby.py +++ b/homeassistant/components/media_player/emby.py @@ -21,7 +21,7 @@ from homeassistant.core import callback import homeassistant.helpers.config_validation as cv import homeassistant.util.dt as dt_util -REQUIREMENTS = ['pyemby==1.1'] +REQUIREMENTS = ['pyemby==1.2'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 01539981b8f..61a14c72ae4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -512,7 +512,7 @@ pydroid-ipcam==0.8 pyebox==0.1.0 # homeassistant.components.media_player.emby -pyemby==1.1 +pyemby==1.2 # homeassistant.components.envisalink pyenvisalink==2.0 From 5a2ab3167b50820fdf24cbddf42489ecf937ed28 Mon Sep 17 00:00:00 2001 From: Teemu R Date: Sat, 8 Apr 2017 15:33:25 +0200 Subject: [PATCH 12/32] switch.tplink: bump pyhs100 version requirement (#6986) --- homeassistant/components/switch/tplink.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/switch/tplink.py b/homeassistant/components/switch/tplink.py index b4c1df3db73..cc00a3691ee 100644 --- a/homeassistant/components/switch/tplink.py +++ b/homeassistant/components/switch/tplink.py @@ -14,7 +14,7 @@ from homeassistant.components.switch import (SwitchDevice, PLATFORM_SCHEMA) from homeassistant.const import (CONF_HOST, CONF_NAME) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['pyHS100==0.2.4.1'] +REQUIREMENTS = ['pyHS100==0.2.4.2'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 61a14c72ae4..43c0240452c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -469,7 +469,7 @@ py-cpuinfo==3.0.0 pyCEC==0.4.13 # homeassistant.components.switch.tplink -pyHS100==0.2.4.1 +pyHS100==0.2.4.2 # homeassistant.components.rfxtrx pyRFXtrx==0.17.0 From dea9aec268ac805dee7b997fe9641258f6ff1d9a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 8 Apr 2017 14:53:32 -0700 Subject: [PATCH 13/32] Warn if start takes a long time. (#6975) * Warn if start takes a long time. * ps - cleanup * Tweak message * Add tests * Tweak messagE --- homeassistant/core.py | 16 +++++++++++++++- tests/test_core.py | 43 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/homeassistant/core.py b/homeassistant/core.py index ba19f392931..899bed064ed 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -18,6 +18,7 @@ from time import monotonic from types import MappingProxyType from typing import Optional, Any, Callable, List # NOQA +from async_timeout import timeout import voluptuous as vol from voluptuous.humanize import humanize_error @@ -49,6 +50,8 @@ ENTITY_ID_PATTERN = re.compile(r"^(\w+)\.(\w+)$") # Size of a executor pool EXECUTOR_POOL_SIZE = 10 +# How long to wait till things that run on startup have to finish. +TIMEOUT_EVENT_START = 15 _LOGGER = logging.getLogger(__name__) @@ -159,7 +162,18 @@ class HomeAssistant(object): # pylint: disable=protected-access self.loop._thread_ident = threading.get_ident() self.bus.async_fire(EVENT_HOMEASSISTANT_START) - yield from self.async_stop_track_tasks() + + try: + with timeout(TIMEOUT_EVENT_START, loop=self.loop): + yield from self.async_stop_track_tasks() + except asyncio.TimeoutError: + _LOGGER.warning( + 'Something is blocking Home Assistant from wrapping up the ' + 'start up phase. We\'re going to continue anyway. Please ' + 'report the following info at http://bit.ly/2ogP58T : %s', + ', '.join(self.config.components)) + self._track_task = False + self.state = CoreState.running _async_create_timer(self) diff --git a/tests/test_core.py b/tests/test_core.py index ea7f40aaff7..b2fca047292 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -5,6 +5,7 @@ import unittest from unittest.mock import patch, MagicMock, sentinel from datetime import datetime, timedelta +import logging import pytz import pytest @@ -867,3 +868,45 @@ def test_timer_out_of_sync(mock_monotonic, loop): assert slp_seconds == 1 assert callback is fire_time_event assert abs(nxt - 12.3) < 0.001 + + +@asyncio.coroutine +def test_hass_start_starts_the_timer(loop): + """Test when hass starts, it starts the timer.""" + hass = ha.HomeAssistant(loop=loop) + + try: + with patch('homeassistant.core._async_create_timer') as mock_timer: + yield from hass.async_start() + + assert hass.state == ha.CoreState.running + assert not hass._track_task + assert len(mock_timer.mock_calls) == 1 + assert mock_timer.mock_calls[0][1][0] is hass + + finally: + yield from hass.async_stop() + assert hass.state == ha.CoreState.not_running + + +@asyncio.coroutine +def test_start_taking_too_long(loop, caplog): + """Test when async_start takes too long.""" + hass = ha.HomeAssistant(loop=loop) + caplog.set_level(logging.WARNING) + + try: + with patch('homeassistant.core.timeout', + side_effect=asyncio.TimeoutError), \ + patch('homeassistant.core._async_create_timer') as mock_timer: + yield from hass.async_start() + + assert not hass._track_task + assert hass.state == ha.CoreState.running + assert len(mock_timer.mock_calls) == 1 + assert mock_timer.mock_calls[0][1][0] is hass + assert 'Something is blocking Home Assistant' in caplog.text + + finally: + yield from hass.async_stop() + assert hass.state == ha.CoreState.not_running From 50cc2ed97ca3e960bfa0bb4d7dbffd5a3794b665 Mon Sep 17 00:00:00 2001 From: Marcelo Moreira de Mello Date: Sat, 8 Apr 2017 17:52:10 -0400 Subject: [PATCH 14/32] Bump Amcrest module to 1.1.8 (#6990) Fixed traceback when calculating SD card percent storage self._state = self._camera.percent(sd_used[0], sd_total[0]) AttributeError: 'Http' object has no attribute 'percent' --- homeassistant/components/camera/amcrest.py | 2 +- homeassistant/components/sensor/amcrest.py | 4 ++-- requirements_all.txt | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/camera/amcrest.py b/homeassistant/components/camera/amcrest.py index 294a63bcff9..a50cdc859a7 100644 --- a/homeassistant/components/camera/amcrest.py +++ b/homeassistant/components/camera/amcrest.py @@ -18,7 +18,7 @@ from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import ( async_get_clientsession, async_aiohttp_proxy_web) -REQUIREMENTS = ['amcrest==1.1.5'] +REQUIREMENTS = ['amcrest==1.1.8'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sensor/amcrest.py b/homeassistant/components/sensor/amcrest.py index 5a349d28b7c..2d05372220b 100644 --- a/homeassistant/components/sensor/amcrest.py +++ b/homeassistant/components/sensor/amcrest.py @@ -19,7 +19,7 @@ import homeassistant.loader as loader from requests.exceptions import HTTPError, ConnectTimeout -REQUIREMENTS = ['amcrest==1.1.5'] +REQUIREMENTS = ['amcrest==1.1.8'] _LOGGER = logging.getLogger(__name__) @@ -146,4 +146,4 @@ class AmcrestSensor(Entity): sd_total = self._camera.storage_total self._attrs['Total'] = '{0} {1}'.format(*sd_total) self._attrs['Used'] = '{0} {1}'.format(*sd_used) - self._state = self._camera.percent(sd_used[0], sd_total[0]) + self._state = self._camera.storage_used_percent diff --git a/requirements_all.txt b/requirements_all.txt index 43c0240452c..7e800d62548 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -48,7 +48,7 @@ aiolifx==0.4.2 # homeassistant.components.camera.amcrest # homeassistant.components.sensor.amcrest -amcrest==1.1.5 +amcrest==1.1.8 # homeassistant.components.media_player.anthemav anthemav==1.1.8 From d6758041196804a15ceabe63fa7653cfa35aec8e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 8 Apr 2017 18:29:39 -0700 Subject: [PATCH 15/32] Version bump to 0.42.1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 6b13186a730..24bc879dc7b 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 42 -PATCH_VERSION = '0' +PATCH_VERSION = '1' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 4, 2) From 62d0df4f7342f59a5e300de69c5ec00ee1a5218f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 8 Apr 2017 18:29:28 -0700 Subject: [PATCH 16/32] Upgrade to aiohttp 2.0.6 (#6992) --- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index a8e8df3d250..2f9b45377ad 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -5,5 +5,5 @@ pip>=7.1.0 jinja2>=2.9.5 voluptuous==0.9.3 typing>=3,<4 -aiohttp==2.0.5 +aiohttp==2.0.6 async_timeout==1.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 7e800d62548..b79e51cb103 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -6,7 +6,7 @@ pip>=7.1.0 jinja2>=2.9.5 voluptuous==0.9.3 typing>=3,<4 -aiohttp==2.0.5 +aiohttp==2.0.6 async_timeout==1.2.0 # homeassistant.components.nuimo_controller diff --git a/setup.py b/setup.py index cdc4c43e7a2..ef6eb0f79a7 100755 --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ REQUIRES = [ 'jinja2>=2.9.5', 'voluptuous==0.9.3', 'typing>=3,<4', - 'aiohttp==2.0.5', + 'aiohttp==2.0.6', 'async_timeout==1.2.0', ] From f51d705ac7e0519172d2f946ad78ad98df422a2a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 9 Apr 2017 01:05:34 -0700 Subject: [PATCH 17/32] Make discovery not block start (#6991) * Make discovery not block start * Fix tests --- homeassistant/components/discovery.py | 10 +++- tests/components/test_discovery.py | 79 +++++++++------------------ 2 files changed, 35 insertions(+), 54 deletions(-) diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery.py index 26036342452..891e34ee8a9 100644 --- a/homeassistant/components/discovery.py +++ b/homeassistant/components/discovery.py @@ -13,6 +13,7 @@ import logging import voluptuous as vol +from homeassistant.core import callback from homeassistant.const import EVENT_HOMEASSISTANT_START import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import async_track_point_in_utc_time @@ -105,7 +106,7 @@ def async_setup(hass, config): hass, component, platform, info, config) @asyncio.coroutine - def scan_devices(_): + def scan_devices(now): """Scan for devices.""" results = yield from hass.loop.run_in_executor( None, _discover, netdisco) @@ -116,7 +117,12 @@ def async_setup(hass, config): async_track_point_in_utc_time(hass, scan_devices, dt_util.utcnow() + SCAN_INTERVAL) - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, scan_devices) + @callback + def schedule_first(event): + """Schedule the first discovery when Home Assistant starts up.""" + async_track_point_in_utc_time(hass, scan_devices, dt_util.utcnow()) + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, schedule_first) return True diff --git a/tests/components/test_discovery.py b/tests/components/test_discovery.py index abffc3b17cd..6b03ffa34e7 100644 --- a/tests/components/test_discovery.py +++ b/tests/components/test_discovery.py @@ -5,9 +5,9 @@ from unittest.mock import patch from homeassistant.bootstrap import async_setup_component from homeassistant.components import discovery -from homeassistant.const import EVENT_HOMEASSISTANT_START +from homeassistant.util.dt import utcnow -from tests.common import mock_coro +from tests.common import mock_coro, fire_time_changed # One might consider to "mock" services, but it's easy enough to just use # what is already available. @@ -34,24 +34,34 @@ IGNORE_CONFIG = { @asyncio.coroutine -def test_unknown_service(hass): - """Test that unknown service is ignored.""" - result = yield from async_setup_component(hass, 'discovery', { - 'discovery': {}, - }) +def mock_discovery(hass, discoveries, config=BASE_CONFIG): + """Helper to mock discoveries.""" + result = yield from async_setup_component(hass, 'discovery', config) assert result - def discover(netdisco): - """Fake discovery.""" - return [('this_service_will_never_be_supported', {'info': 'some'})] + yield from hass.async_start() - with patch.object(discovery, '_discover', discover), \ + with patch.object(discovery, '_discover', discoveries), \ patch('homeassistant.components.discovery.async_discover', return_value=mock_coro()) as mock_discover, \ patch('homeassistant.components.discovery.async_load_platform', return_value=mock_coro()) as mock_platform: - hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + fire_time_changed(hass, utcnow()) + # Work around an issue where our loop.call_soon not get caught yield from hass.async_block_till_done() + yield from hass.async_block_till_done() + + return mock_discover, mock_platform + + +@asyncio.coroutine +def test_unknown_service(hass): + """Test that unknown service is ignored.""" + def discover(netdisco): + """Fake discovery.""" + return [('this_service_will_never_be_supported', {'info': 'some'})] + + mock_discover, mock_platform = yield from mock_discovery(hass, discover) assert not mock_discover.called assert not mock_platform.called @@ -60,20 +70,11 @@ def test_unknown_service(hass): @asyncio.coroutine def test_load_platform(hass): """Test load a platform.""" - result = yield from async_setup_component(hass, 'discovery', BASE_CONFIG) - assert result - def discover(netdisco): """Fake discovery.""" return [(SERVICE, SERVICE_INFO)] - with patch.object(discovery, '_discover', discover), \ - patch('homeassistant.components.discovery.async_discover', - return_value=mock_coro()) as mock_discover, \ - patch('homeassistant.components.discovery.async_load_platform', - return_value=mock_coro()) as mock_platform: - hass.bus.async_fire(EVENT_HOMEASSISTANT_START) - yield from hass.async_block_till_done() + mock_discover, mock_platform = yield from mock_discovery(hass, discover) assert not mock_discover.called assert mock_platform.called @@ -84,20 +85,11 @@ def test_load_platform(hass): @asyncio.coroutine def test_load_component(hass): """Test load a component.""" - result = yield from async_setup_component(hass, 'discovery', BASE_CONFIG) - assert result - def discover(netdisco): """Fake discovery.""" return [(SERVICE_NO_PLATFORM, SERVICE_INFO)] - with patch.object(discovery, '_discover', discover), \ - patch('homeassistant.components.discovery.async_discover', - return_value=mock_coro()) as mock_discover, \ - patch('homeassistant.components.discovery.async_load_platform', - return_value=mock_coro()) as mock_platform: - hass.bus.async_fire(EVENT_HOMEASSISTANT_START) - yield from hass.async_block_till_done() + mock_discover, mock_platform = yield from mock_discovery(hass, discover) assert mock_discover.called assert not mock_platform.called @@ -109,20 +101,12 @@ def test_load_component(hass): @asyncio.coroutine def test_ignore_service(hass): """Test ignore service.""" - result = yield from async_setup_component(hass, 'discovery', IGNORE_CONFIG) - assert result - def discover(netdisco): """Fake discovery.""" return [(SERVICE_NO_PLATFORM, SERVICE_INFO)] - with patch.object(discovery, '_discover', discover), \ - patch('homeassistant.components.discovery.async_discover', - return_value=mock_coro()) as mock_discover, \ - patch('homeassistant.components.discovery.async_load_platform', - return_value=mock_coro()) as mock_platform: - hass.bus.async_fire(EVENT_HOMEASSISTANT_START) - yield from hass.async_block_till_done() + mock_discover, mock_platform = yield from mock_discovery(hass, discover, + IGNORE_CONFIG) assert not mock_discover.called assert not mock_platform.called @@ -131,21 +115,12 @@ def test_ignore_service(hass): @asyncio.coroutine def test_discover_duplicates(hass): """Test load a component.""" - result = yield from async_setup_component(hass, 'discovery', BASE_CONFIG) - assert result - def discover(netdisco): """Fake discovery.""" return [(SERVICE_NO_PLATFORM, SERVICE_INFO), (SERVICE_NO_PLATFORM, SERVICE_INFO)] - with patch.object(discovery, '_discover', discover), \ - patch('homeassistant.components.discovery.async_discover', - return_value=mock_coro()) as mock_discover, \ - patch('homeassistant.components.discovery.async_load_platform', - return_value=mock_coro()) as mock_platform: - hass.bus.async_fire(EVENT_HOMEASSISTANT_START) - yield from hass.async_block_till_done() + mock_discover, mock_platform = yield from mock_discovery(hass, discover) assert mock_discover.called assert mock_discover.call_count == 1 From a6dc86fa750512894e100099cef7d4dc647938a1 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 9 Apr 2017 01:31:46 -0700 Subject: [PATCH 18/32] version bump to 0.42.2 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 24bc879dc7b..85c3ba2260d 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 42 -PATCH_VERSION = '1' +PATCH_VERSION = '2' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 4, 2) From f6e819e7994879c06de1f6838a84f94e17af0c6b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 9 Apr 2017 01:36:01 -0700 Subject: [PATCH 19/32] Downgrade aiohttp to 205 (#6994) --- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 2f9b45377ad..a8e8df3d250 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -5,5 +5,5 @@ pip>=7.1.0 jinja2>=2.9.5 voluptuous==0.9.3 typing>=3,<4 -aiohttp==2.0.6 +aiohttp==2.0.5 async_timeout==1.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index b79e51cb103..7e800d62548 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -6,7 +6,7 @@ pip>=7.1.0 jinja2>=2.9.5 voluptuous==0.9.3 typing>=3,<4 -aiohttp==2.0.6 +aiohttp==2.0.5 async_timeout==1.2.0 # homeassistant.components.nuimo_controller diff --git a/setup.py b/setup.py index ef6eb0f79a7..cdc4c43e7a2 100755 --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ REQUIRES = [ 'jinja2>=2.9.5', 'voluptuous==0.9.3', 'typing>=3,<4', - 'aiohttp==2.0.6', + 'aiohttp==2.0.5', 'async_timeout==1.2.0', ] From a60e8b16c030bcb0c87a3248d6a2f0e2c1e5302e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 11 Apr 2017 09:14:37 -0700 Subject: [PATCH 20/32] Version bump to 0.42.3 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 85c3ba2260d..e75dd5a2125 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 42 -PATCH_VERSION = '2' +PATCH_VERSION = '3' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 4, 2) From 7a3df037bad6d0e2d447f4b445e57c8f94a92705 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 9 Apr 2017 23:19:22 -0700 Subject: [PATCH 21/32] Fix Synology camera content type (#7010) --- homeassistant/components/camera/synology.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/camera/synology.py b/homeassistant/components/camera/synology.py index e986d81887b..dd6061dcacf 100644 --- a/homeassistant/components/camera/synology.py +++ b/homeassistant/components/camera/synology.py @@ -81,7 +81,9 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): params=query_payload ) - query_resp = yield from query_req.json() + # Skip content type check because Synology doesn't return JSON with + # right content type + query_resp = yield from query_req.json(content_type=None) auth_path = query_resp['data'][AUTH_API]['path'] camera_api = query_resp['data'][CAMERA_API]['path'] camera_path = query_resp['data'][CAMERA_API]['path'] From b295451d46121b60faa7e73cbfe15c11f24116f8 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 10 Apr 2017 01:18:37 -0700 Subject: [PATCH 22/32] Fix two more instances of JSON parsing synology (#7014) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add an optional extended description… --- homeassistant/components/camera/synology.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/camera/synology.py b/homeassistant/components/camera/synology.py index dd6061dcacf..378d75ac26d 100644 --- a/homeassistant/components/camera/synology.py +++ b/homeassistant/components/camera/synology.py @@ -129,7 +129,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): _LOGGER.exception("Error on %s", syno_camera_url) return False - camera_resp = yield from camera_req.json() + camera_resp = yield from camera_req.json(content_type=None) cameras = camera_resp['data']['cameras'] # add cameras @@ -174,7 +174,7 @@ def get_session_id(hass, websession, username, password, login_url, timeout): login_url, params=auth_payload ) - auth_resp = yield from auth_req.json() + auth_resp = yield from auth_req.json(content_type=None) return auth_resp['data']['sid'] except (asyncio.TimeoutError, aiohttp.ClientError): From f00d7212936e43472e1c223b6735b9227e3014ac Mon Sep 17 00:00:00 2001 From: Xorso Date: Mon, 10 Apr 2017 08:13:07 -0600 Subject: [PATCH 23/32] Bump pyalarmdotcom to support new version of aiohttp (#7021) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add an optional extended description… --- homeassistant/components/alarm_control_panel/alarmdotcom.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/alarmdotcom.py b/homeassistant/components/alarm_control_panel/alarmdotcom.py index 22cbdefd403..6e99ba67257 100644 --- a/homeassistant/components/alarm_control_panel/alarmdotcom.py +++ b/homeassistant/components/alarm_control_panel/alarmdotcom.py @@ -17,7 +17,7 @@ from homeassistant.const import ( import homeassistant.helpers.config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession -REQUIREMENTS = ['pyalarmdotcom==0.2.9'] +REQUIREMENTS = ['pyalarmdotcom==0.3.0'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 7e800d62548..ee76e53c68e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -475,7 +475,7 @@ pyHS100==0.2.4.2 pyRFXtrx==0.17.0 # homeassistant.components.alarm_control_panel.alarmdotcom -pyalarmdotcom==0.2.9 +pyalarmdotcom==0.3.0 # homeassistant.components.notify.xmpp pyasn1-modules==0.0.8 From e3c2d27f4abdbe7da0214d4b796f57895b11e861 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Mon, 10 Apr 2017 16:13:43 +0200 Subject: [PATCH 24/32] Fix US states check (fixes #7015) (#7017) --- homeassistant/components/binary_sensor/workday.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/binary_sensor/workday.py b/homeassistant/components/binary_sensor/workday.py index c2590925df7..c25ea81922b 100644 --- a/homeassistant/components/binary_sensor/workday.py +++ b/homeassistant/components/binary_sensor/workday.py @@ -66,8 +66,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None): obj_holidays = getattr(holidays, country)(years=year) if province: - if province not in obj_holidays.PROVINCES: - _LOGGER.error('There is no province/state %s in country %s', + if province not in obj_holidays.PROVINCES and \ + province not in obj_holidays.STATES: + _LOGGER.error("There is no province/state %s in country %s", province, country) return False else: From 00131395914ae2b845315824aa97221c0732f9c0 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Tue, 11 Apr 2017 17:58:51 +0200 Subject: [PATCH 25/32] Plug file leak on LIFX unregister (#7031) * Plug file leak on LIFX unregister The aiolifx 0.4.4 release closes its socket when the unregister callback is called. This plugs a file descriptor leak but also means that we must be careful to not use the device after it goes unavailable. Also, when a light reappears, it has a new device that must be used. * Do not test self.available in service calls The core will learn to handle that. --- homeassistant/components/light/lifx.py | 3 ++- requirements_all.txt | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/light/lifx.py b/homeassistant/components/light/lifx.py index 76a2a9e907d..da09601c1bb 100644 --- a/homeassistant/components/light/lifx.py +++ b/homeassistant/components/light/lifx.py @@ -26,7 +26,7 @@ import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['aiolifx==0.4.2'] +REQUIREMENTS = ['aiolifx==0.4.4'] UDP_BROADCAST_PORT = 56700 @@ -84,6 +84,7 @@ class LIFXManager(object): entity = self.entities[device.mac_addr] _LOGGER.debug("%s register AGAIN", entity.ipaddr) entity.available = True + entity.device = device self.hass.async_add_job(entity.async_update_ha_state()) else: _LOGGER.debug("%s register NEW", device.ip_addr) diff --git a/requirements_all.txt b/requirements_all.txt index ee76e53c68e..07712308235 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -44,7 +44,7 @@ aiodns==1.1.1 aiohttp_cors==0.5.2 # homeassistant.components.light.lifx -aiolifx==0.4.2 +aiolifx==0.4.4 # homeassistant.components.camera.amcrest # homeassistant.components.sensor.amcrest From 7377ce2640fdd48695296a24ac7a58a09def4bc2 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 11 Apr 2017 18:09:31 +0200 Subject: [PATCH 26/32] Bugfix wait on start event (#7013) * Bugfix wait on start event * Paulus sugestion * Change handling with stop_track_task * Add new unittests * Update test_core.py --- homeassistant/core.py | 12 +++++------- tests/common.py | 3 +-- tests/test_core.py | 17 ++++++++++++++++- 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/homeassistant/core.py b/homeassistant/core.py index 899bed064ed..320e857ac9e 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -164,15 +164,16 @@ class HomeAssistant(object): self.bus.async_fire(EVENT_HOMEASSISTANT_START) try: + # only block for EVENT_HOMEASSISTANT_START listener + self.async_stop_track_tasks() with timeout(TIMEOUT_EVENT_START, loop=self.loop): - yield from self.async_stop_track_tasks() + yield from self.async_block_till_done() except asyncio.TimeoutError: _LOGGER.warning( 'Something is blocking Home Assistant from wrapping up the ' 'start up phase. We\'re going to continue anyway. Please ' 'report the following info at http://bit.ly/2ogP58T : %s', ', '.join(self.config.components)) - self._track_task = False self.state = CoreState.running _async_create_timer(self) @@ -218,10 +219,9 @@ class HomeAssistant(object): """Track tasks so you can wait for all tasks to be done.""" self._track_task = True - @asyncio.coroutine + @callback def async_stop_track_tasks(self): - """Track tasks so you can wait for all tasks to be done.""" - yield from self.async_block_till_done() + """Stop track tasks so you can't wait for all tasks to be done.""" self._track_task = False @callback @@ -246,8 +246,6 @@ class HomeAssistant(object): @asyncio.coroutine def async_block_till_done(self): """Block till all pending work is done.""" - assert self._track_task, 'Not tracking tasks' - # To flush out any call_soon_threadsafe yield from asyncio.sleep(0, loop=self.loop) diff --git a/tests/common.py b/tests/common.py index 9dc077dc3f7..03a4de235d7 100644 --- a/tests/common.py +++ b/tests/common.py @@ -122,8 +122,7 @@ def async_test_home_assistant(loop): # 1. We only mock time during tests # 2. We want block_till_done that is called inside stop_track_tasks with patch('homeassistant.core._async_create_timer'), \ - patch.object(hass, 'async_stop_track_tasks', - hass.async_block_till_done): + patch.object(hass, 'async_stop_track_tasks'): yield from orig_start() hass.async_start = mock_async_start diff --git a/tests/test_core.py b/tests/test_core.py index b2fca047292..89ae6c5f651 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -901,7 +901,6 @@ def test_start_taking_too_long(loop, caplog): patch('homeassistant.core._async_create_timer') as mock_timer: yield from hass.async_start() - assert not hass._track_task assert hass.state == ha.CoreState.running assert len(mock_timer.mock_calls) == 1 assert mock_timer.mock_calls[0][1][0] is hass @@ -910,3 +909,19 @@ def test_start_taking_too_long(loop, caplog): finally: yield from hass.async_stop() assert hass.state == ha.CoreState.not_running + + +@asyncio.coroutine +def test_track_task_functions(loop): + """Test function to start/stop track task and initial state.""" + hass = ha.HomeAssistant(loop=loop) + try: + assert hass._track_task + + hass.async_stop_track_tasks() + assert not hass._track_task + + hass.async_track_tasks() + assert hass._track_task + finally: + yield from hass.async_stop() From c91cf66dec9f91f5e1ab5d9fd3b5bf1f929e50b6 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 11 Apr 2017 18:23:41 +0200 Subject: [PATCH 27/32] Bugfix slider (#7047) * Bugfix slider * Update input_slider.py * Update input_slider.py --- homeassistant/components/input_slider.py | 4 ++-- tests/components/test_input_slider.py | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/input_slider.py b/homeassistant/components/input_slider.py index d10120e673b..c4976bb43e8 100644 --- a/homeassistant/components/input_slider.py +++ b/homeassistant/components/input_slider.py @@ -174,8 +174,8 @@ class InputSlider(Entity): state = yield from async_get_last_state(self.hass, self.entity_id) value = state and float(state.state) - # Check against False because value can be 0 - if value is not False and self._minimum < value < self._maximum: + # Check against None because value can be 0 + if value is not None and self._minimum <= value <= self._maximum: self._current_value = value else: self._current_value = self._minimum diff --git a/tests/components/test_input_slider.py b/tests/components/test_input_slider.py index 7097e87e646..f550091e31f 100644 --- a/tests/components/test_input_slider.py +++ b/tests/components/test_input_slider.py @@ -133,3 +133,21 @@ def test_initial_state_overrules_restore_state(hass): state = hass.states.get('input_slider.b2') assert state assert float(state.state) == 60 + + +@asyncio.coroutine +def test_no_initial_state_and_no_restore_state(hass): + """Ensure that entity is create without initial and restore feature.""" + hass.state = CoreState.starting + + yield from async_setup_component(hass, DOMAIN, { + DOMAIN: { + 'b1': { + 'min': 0, + 'max': 100, + }, + }}) + + state = hass.states.get('input_slider.b1') + assert state + assert float(state.state) == 0 From 9744ec584ad765e1b5724bd964e35c1a68a6658a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 16 Apr 2017 14:59:15 -0700 Subject: [PATCH 28/32] Version bump to 0.42.4 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index e75dd5a2125..65b15797380 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 42 -PATCH_VERSION = '3' +PATCH_VERSION = '4' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 4, 2) From 1efa6eaf0f563aa7144031057f14fdfd55708d88 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Wed, 12 Apr 2017 04:17:09 +0200 Subject: [PATCH 29/32] Fix mysensors callback (#7057) * Fix mysensors callback * All messages was not triggering proper updates. Fix by checking all child value types each update. * Upgrade mysensors dep * Fix pickle persistence when upgrading. --- .../components/device_tracker/mysensors.py | 54 +++++++++--------- homeassistant/components/mysensors.py | 56 +++++++++---------- requirements_all.txt | 2 +- 3 files changed, 56 insertions(+), 56 deletions(-) diff --git a/homeassistant/components/device_tracker/mysensors.py b/homeassistant/components/device_tracker/mysensors.py index e1a16a017e4..04801f834df 100644 --- a/homeassistant/components/device_tracker/mysensors.py +++ b/homeassistant/components/device_tracker/mysensors.py @@ -20,37 +20,39 @@ def setup_scanner(hass, config, see, discovery_info=None): """Callback for mysensors platform.""" node = gateway.sensors[msg.node_id] if node.sketch_name is None: - _LOGGER.info('No sketch_name: node %s', msg.node_id) + _LOGGER.debug('No sketch_name: node %s', msg.node_id) return pres = gateway.const.Presentation set_req = gateway.const.SetReq - for child in node.children.values(): - position = child.values.get(set_req.V_POSITION) - if child.type != pres.S_GPS or position is None: - continue - try: - latitude, longitude, _ = position.split(',') - except ValueError: - _LOGGER.error('Payload for V_POSITION %s is not of format ' - 'latitude,longitude,altitude', position) - continue - name = '{} {} {}'.format( - node.sketch_name, msg.node_id, child.id) - attr = { - mysensors.ATTR_CHILD_ID: child.id, - mysensors.ATTR_DESCRIPTION: child.description, - mysensors.ATTR_DEVICE: gateway.device, - mysensors.ATTR_NODE_ID: msg.node_id, - } - see( - dev_id=slugify(name), - host_name=name, - gps=(latitude, longitude), - battery=node.battery_level, - attributes=attr - ) + child = node.children.get(msg.child_id) + if child is None: + return + position = child.values.get(set_req.V_POSITION) + if child.type != pres.S_GPS or position is None: + return + try: + latitude, longitude, _ = position.split(',') + except ValueError: + _LOGGER.error('Payload for V_POSITION %s is not of format ' + 'latitude,longitude,altitude', position) + return + name = '{} {} {}'.format( + node.sketch_name, msg.node_id, child.id) + attr = { + mysensors.ATTR_CHILD_ID: child.id, + mysensors.ATTR_DESCRIPTION: child.description, + mysensors.ATTR_DEVICE: gateway.device, + mysensors.ATTR_NODE_ID: msg.node_id, + } + see( + dev_id=slugify(name), + host_name=name, + gps=(latitude, longitude), + battery=node.battery_level, + attributes=attr + ) gateways = hass.data.get(mysensors.MYSENSORS_GATEWAYS) diff --git a/homeassistant/components/mysensors.py b/homeassistant/components/mysensors.py index afe5fc11762..dbf66c2288b 100644 --- a/homeassistant/components/mysensors.py +++ b/homeassistant/components/mysensors.py @@ -46,7 +46,7 @@ MYSENSORS_GATEWAYS = 'mysensors_gateways' MQTT_COMPONENT = 'mqtt' REQUIREMENTS = [ 'https://github.com/theolind/pymysensors/archive/' - 'ff3476b70edc9c995b939cddb9d51f8d2d018581.zip#pymysensors==0.9.0'] + 'c6990eaaa741444a638608e6e00488195e2ca74c.zip#pymysensors==0.9.1'] def is_socket_address(value): @@ -206,12 +206,9 @@ def setup(hass, config): for node_id in gateway.sensors: node = gateway.sensors[node_id] for child_id in node.children: - child = node.children[child_id] - for value_type in child.values: - msg = mysensors.Message().modify( - node_id=node_id, child_id=child_id, type=1, - sub_type=value_type) - gateway.event_callback(msg) + msg = mysensors.Message().modify( + node_id=node_id, child_id=child_id) + gateway.event_callback(msg) gateway.start() hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, lambda event: gateway.stop()) @@ -274,32 +271,33 @@ def pf_callback_factory(map_sv_types, devices, entity_class, add_devices=None): _LOGGER.debug('No sketch_name: node %s', msg.node_id) return child = gateway.sensors[msg.node_id].children.get(msg.child_id) - if child is None or child.values.get(msg.sub_type) is None: + if child is None: return - key = msg.node_id, child.id, msg.sub_type - if child.type not in map_sv_types or \ - msg.sub_type not in map_sv_types[child.type]: - return - if key in devices: + for value_type in child.values: + key = msg.node_id, child.id, value_type + if child.type not in map_sv_types or \ + value_type not in map_sv_types[child.type]: + continue + if key in devices: + if add_devices: + devices[key].schedule_update_ha_state(True) + else: + devices[key].update() + continue + name = '{} {} {}'.format( + gateway.sensors[msg.node_id].sketch_name, msg.node_id, + child.id) + if isinstance(entity_class, dict): + device_class = entity_class[child.type] + else: + device_class = entity_class + devices[key] = device_class( + gateway, msg.node_id, child.id, name, value_type) if add_devices: - devices[key].schedule_update_ha_state(True) + _LOGGER.info('Adding new devices: %s', [devices[key]]) + add_devices([devices[key]], True) else: devices[key].update() - return - name = '{} {} {}'.format( - gateway.sensors[msg.node_id].sketch_name, msg.node_id, - child.id) - if isinstance(entity_class, dict): - device_class = entity_class[child.type] - else: - device_class = entity_class - devices[key] = device_class( - gateway, msg.node_id, child.id, name, msg.sub_type) - if add_devices: - _LOGGER.info('Adding new devices: %s', [devices[key]]) - add_devices([devices[key]], True) - else: - devices[key].update() return mysensors_callback diff --git a/requirements_all.txt b/requirements_all.txt index 07712308235..07088f8407c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -314,7 +314,7 @@ https://github.com/tfriedel/python-lightify/archive/d6eadcf311e6e21746182d1480e9 https://github.com/thecynic/pylutron/archive/v0.1.0.zip#pylutron==0.1.0 # homeassistant.components.mysensors -https://github.com/theolind/pymysensors/archive/ff3476b70edc9c995b939cddb9d51f8d2d018581.zip#pymysensors==0.9.0 +https://github.com/theolind/pymysensors/archive/c6990eaaa741444a638608e6e00488195e2ca74c.zip#pymysensors==0.9.1 # homeassistant.components.sensor.modem_callerid https://github.com/vroomfonde1/basicmodem/archive/0.7.zip#basicmodem==0.7 From 904b0175525c45b43f70f54660f7bccf1bfee9e7 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 15 Apr 2017 00:18:39 +0200 Subject: [PATCH 30/32] Upgrade aiohttp to 2.0.7 (#7106) --- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index a8e8df3d250..343e17ff7b5 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -5,5 +5,5 @@ pip>=7.1.0 jinja2>=2.9.5 voluptuous==0.9.3 typing>=3,<4 -aiohttp==2.0.5 +aiohttp==2.0.7 async_timeout==1.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 07088f8407c..b0d909f74a6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -6,7 +6,7 @@ pip>=7.1.0 jinja2>=2.9.5 voluptuous==0.9.3 typing>=3,<4 -aiohttp==2.0.5 +aiohttp==2.0.7 async_timeout==1.2.0 # homeassistant.components.nuimo_controller diff --git a/setup.py b/setup.py index cdc4c43e7a2..161484d1f83 100755 --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ REQUIRES = [ 'jinja2>=2.9.5', 'voluptuous==0.9.3', 'typing>=3,<4', - 'aiohttp==2.0.5', + 'aiohttp==2.0.7', 'async_timeout==1.2.0', ] From fadd33bcb2c958e46f911fec633a7c5cb80d85b4 Mon Sep 17 00:00:00 2001 From: Robbie Trencheny Date: Sun, 16 Apr 2017 14:53:03 -0700 Subject: [PATCH 31/32] Make version number optional and a string to fix identify issue introduced in iOS 1.0.1 (#7141) --- homeassistant/components/ios.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/ios.py b/homeassistant/components/ios.py index 22f8b832b3d..c5ffc34864c 100644 --- a/homeassistant/components/ios.py +++ b/homeassistant/components/ios.py @@ -138,7 +138,7 @@ IDENTIFY_DEVICE_SCHEMA_CONTAINER = vol.All(dict, IDENTIFY_DEVICE_SCHEMA) IDENTIFY_APP_SCHEMA = vol.Schema({ vol.Required(ATTR_APP_BUNDLE_IDENTIFER): cv.string, vol.Required(ATTR_APP_BUILD_NUMBER): cv.positive_int, - vol.Required(ATTR_APP_VERSION_NUMBER): cv.positive_int + vol.Optional(ATTR_APP_VERSION_NUMBER): cv.string }, extra=vol.ALLOW_EXTRA) IDENTIFY_APP_SCHEMA_CONTAINER = vol.All(dict, IDENTIFY_APP_SCHEMA) From 527223b99242017b237959c652120b1b24169dcc Mon Sep 17 00:00:00 2001 From: Adam Mills Date: Sun, 16 Apr 2017 18:06:59 -0400 Subject: [PATCH 32/32] Fix for zwave RGB setting (#7137) --- homeassistant/components/light/zwave.py | 16 +++++----- tests/components/light/test_zwave.py | 42 ++++++++++++------------- 2 files changed, 29 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/light/zwave.py b/homeassistant/components/light/zwave.py index 45660474fde..ce85276cae9 100644 --- a/homeassistant/components/light/zwave.py +++ b/homeassistant/components/light/zwave.py @@ -310,25 +310,25 @@ class ZwaveColorLight(ZwaveDimmer): if self._zw098: if kwargs[ATTR_COLOR_TEMP] > TEMP_MID_HASS: self._ct = TEMP_WARM_HASS - rgbw = b'#000000ff00' + rgbw = '#000000ff00' else: self._ct = TEMP_COLD_HASS - rgbw = b'#00000000ff' + rgbw = '#00000000ff' elif ATTR_RGB_COLOR in kwargs: self._rgb = kwargs[ATTR_RGB_COLOR] if (not self._zw098 and ( self._color_channels & COLOR_CHANNEL_WARM_WHITE or self._color_channels & COLOR_CHANNEL_COLD_WHITE)): - rgbw = b'#' + rgbw = '#' for colorval in color_rgb_to_rgbw(*self._rgb): - rgbw += format(colorval, '02x').encode('utf-8') - rgbw += b'00' + rgbw += format(colorval, '02x') + rgbw += '00' else: - rgbw = b'#' + rgbw = '#' for colorval in self._rgb: - rgbw += format(colorval, '02x').encode('utf-8') - rgbw += b'0000' + rgbw += format(colorval, '02x') + rgbw += '0000' if rgbw and self.values.color: self.values.color.data = rgbw diff --git a/tests/components/light/test_zwave.py b/tests/components/light/test_zwave.py index 0afe9ec8f6a..9629744bc16 100644 --- a/tests/components/light/test_zwave.py +++ b/tests/components/light/test_zwave.py @@ -207,36 +207,36 @@ def test_set_rgb_color(mock_openzwave): """Test setting zwave light color.""" node = MockNode(command_classes=[const.COMMAND_CLASS_SWITCH_COLOR]) value = MockValue(data=0, node=node) - color = MockValue(data=b'#0000000000', node=node) + color = MockValue(data='#0000000000', node=node) # Suppoorts RGB only color_channels = MockValue(data=0x1c, node=node) values = MockLightValues(primary=value, color=color, color_channels=color_channels) device = zwave.get_device(node=node, values=values, node_config={}) - assert color.data == b'#0000000000' + assert color.data == '#0000000000' device.turn_on(**{ATTR_RGB_COLOR: (200, 150, 100)}) - assert color.data == b'#c896640000' + assert color.data == '#c896640000' def test_set_rgbw_color(mock_openzwave): """Test setting zwave light color.""" node = MockNode(command_classes=[const.COMMAND_CLASS_SWITCH_COLOR]) value = MockValue(data=0, node=node) - color = MockValue(data=b'#0000000000', node=node) + color = MockValue(data='#0000000000', node=node) # Suppoorts RGBW color_channels = MockValue(data=0x1d, node=node) values = MockLightValues(primary=value, color=color, color_channels=color_channels) device = zwave.get_device(node=node, values=values, node_config={}) - assert color.data == b'#0000000000' + assert color.data == '#0000000000' device.turn_on(**{ATTR_RGB_COLOR: (200, 150, 100)}) - assert color.data == b'#c86400c800' + assert color.data == '#c86400c800' def test_zw098_set_color_temp(mock_openzwave): @@ -244,29 +244,29 @@ def test_zw098_set_color_temp(mock_openzwave): node = MockNode(manufacturer_id='0086', product_id='0062', command_classes=[const.COMMAND_CLASS_SWITCH_COLOR]) value = MockValue(data=0, node=node) - color = MockValue(data=b'#0000000000', node=node) + color = MockValue(data='#0000000000', node=node) # Suppoorts RGB, warm white, cold white color_channels = MockValue(data=0x1f, node=node) values = MockLightValues(primary=value, color=color, color_channels=color_channels) device = zwave.get_device(node=node, values=values, node_config={}) - assert color.data == b'#0000000000' + assert color.data == '#0000000000' device.turn_on(**{ATTR_COLOR_TEMP: 200}) - assert color.data == b'#00000000ff' + assert color.data == '#00000000ff' device.turn_on(**{ATTR_COLOR_TEMP: 400}) - assert color.data == b'#000000ff00' + assert color.data == '#000000ff00' def test_rgb_not_supported(mock_openzwave): """Test value changed for rgb lights.""" node = MockNode(command_classes=[const.COMMAND_CLASS_SWITCH_COLOR]) value = MockValue(data=0, node=node) - color = MockValue(data=b'#0000000000', node=node) + color = MockValue(data='#0000000000', node=node) # Suppoorts color temperature only color_channels = MockValue(data=0x01, node=node) values = MockLightValues(primary=value, color=color, @@ -290,7 +290,7 @@ def test_no_color_channels_value(mock_openzwave): """Test value changed for rgb lights.""" node = MockNode(command_classes=[const.COMMAND_CLASS_SWITCH_COLOR]) value = MockValue(data=0, node=node) - color = MockValue(data=b'#0000000000', node=node) + color = MockValue(data='#0000000000', node=node) values = MockLightValues(primary=value, color=color) device = zwave.get_device(node=node, values=values, node_config={}) @@ -301,7 +301,7 @@ def test_rgb_value_changed(mock_openzwave): """Test value changed for rgb lights.""" node = MockNode(command_classes=[const.COMMAND_CLASS_SWITCH_COLOR]) value = MockValue(data=0, node=node) - color = MockValue(data=b'#0000000000', node=node) + color = MockValue(data='#0000000000', node=node) # Suppoorts RGB only color_channels = MockValue(data=0x1c, node=node) values = MockLightValues(primary=value, color=color, @@ -310,7 +310,7 @@ def test_rgb_value_changed(mock_openzwave): assert device.rgb_color == [0, 0, 0] - color.data = b'#c896640000' + color.data = '#c896640000' value_changed(color) assert device.rgb_color == [200, 150, 100] @@ -320,7 +320,7 @@ def test_rgbww_value_changed(mock_openzwave): """Test value changed for rgb lights.""" node = MockNode(command_classes=[const.COMMAND_CLASS_SWITCH_COLOR]) value = MockValue(data=0, node=node) - color = MockValue(data=b'#0000000000', node=node) + color = MockValue(data='#0000000000', node=node) # Suppoorts RGB, Warm White color_channels = MockValue(data=0x1d, node=node) values = MockLightValues(primary=value, color=color, @@ -329,7 +329,7 @@ def test_rgbww_value_changed(mock_openzwave): assert device.rgb_color == [0, 0, 0] - color.data = b'#c86400c800' + color.data = '#c86400c800' value_changed(color) assert device.rgb_color == [200, 150, 100] @@ -339,7 +339,7 @@ def test_rgbcw_value_changed(mock_openzwave): """Test value changed for rgb lights.""" node = MockNode(command_classes=[const.COMMAND_CLASS_SWITCH_COLOR]) value = MockValue(data=0, node=node) - color = MockValue(data=b'#0000000000', node=node) + color = MockValue(data='#0000000000', node=node) # Suppoorts RGB, Cold White color_channels = MockValue(data=0x1e, node=node) values = MockLightValues(primary=value, color=color, @@ -348,7 +348,7 @@ def test_rgbcw_value_changed(mock_openzwave): assert device.rgb_color == [0, 0, 0] - color.data = b'#c86400c800' + color.data = '#c86400c800' value_changed(color) assert device.rgb_color == [200, 150, 100] @@ -359,7 +359,7 @@ def test_ct_value_changed(mock_openzwave): node = MockNode(manufacturer_id='0086', product_id='0062', command_classes=[const.COMMAND_CLASS_SWITCH_COLOR]) value = MockValue(data=0, node=node) - color = MockValue(data=b'#0000000000', node=node) + color = MockValue(data='#0000000000', node=node) # Suppoorts RGB, Cold White color_channels = MockValue(data=0x1f, node=node) values = MockLightValues(primary=value, color=color, @@ -368,12 +368,12 @@ def test_ct_value_changed(mock_openzwave): assert device.color_temp == zwave.TEMP_MID_HASS - color.data = b'#000000ff00' + color.data = '#000000ff00' value_changed(color) assert device.color_temp == zwave.TEMP_WARM_HASS - color.data = b'#00000000ff' + color.data = '#00000000ff' value_changed(color) assert device.color_temp == zwave.TEMP_COLD_HASS