From a8ee11e732cd4eb5d24b429e241f7155a06d5d68 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 25 Jan 2018 23:17:07 -0800 Subject: [PATCH 01/17] Version 0.62 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 560c99bb653..5682a65ab7d 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 62 -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 3d8e425113b617277fc1cbbc3ee6797f087447e8 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 25 Jan 2018 23:26:02 -0800 Subject: [PATCH 02/17] Update frontend to 20180126.0 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index ea8a4d92540..8f5a18ff843 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -23,7 +23,7 @@ from homeassistant.const import CONF_NAME, EVENT_THEMES_UPDATED from homeassistant.core import callback from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20180119.0', 'user-agents==1.1.0'] +REQUIREMENTS = ['home-assistant-frontend==20180126.0', 'user-agents==1.1.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index b429818651e..fa872ddb2d1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -352,7 +352,7 @@ hipnotify==1.0.8 holidays==0.9.3 # homeassistant.components.frontend -home-assistant-frontend==20180119.0 +home-assistant-frontend==20180126.0 # homeassistant.components.camera.onvif http://github.com/tgaugry/suds-passworddigest-py3/archive/86fc50e39b4d2b8997481967d6a7fe1c57118999.zip#suds-passworddigest-py3==0.1.2a diff --git a/requirements_test_all.txt b/requirements_test_all.txt index da8187d23ff..f21a20f7439 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -75,7 +75,7 @@ hbmqtt==0.9.1 holidays==0.9.3 # homeassistant.components.frontend -home-assistant-frontend==20180119.0 +home-assistant-frontend==20180126.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From df24ecf39504724b1326da783698178553209aed Mon Sep 17 00:00:00 2001 From: Andrey Date: Fri, 26 Jan 2018 13:41:52 +0200 Subject: [PATCH 03/17] Add "write" service to system_log (#11901) * Add API to write error log * Move write_error api to system_log.write service call * Restore empty line --- .../components/system_log/__init__.py | 23 ++++++- .../components/system_log/services.yaml | 12 ++++ tests/components/test_system_log.py | 63 ++++++++++++++++--- 3 files changed, 86 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/system_log/__init__.py b/homeassistant/components/system_log/__init__.py index 0d478ac9316..5c8fe3109a6 100644 --- a/homeassistant/components/system_log/__init__.py +++ b/homeassistant/components/system_log/__init__.py @@ -18,6 +18,9 @@ from homeassistant.components.http import HomeAssistantView import homeassistant.helpers.config_validation as cv CONF_MAX_ENTRIES = 'max_entries' +CONF_MESSAGE = 'message' +CONF_LEVEL = 'level' +CONF_LOGGER = 'logger' DATA_SYSTEM_LOG = 'system_log' DEFAULT_MAX_ENTRIES = 50 @@ -25,6 +28,7 @@ DEPENDENCIES = ['http'] DOMAIN = 'system_log' SERVICE_CLEAR = 'clear' +SERVICE_WRITE = 'write' CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ @@ -34,6 +38,12 @@ CONFIG_SCHEMA = vol.Schema({ }, extra=vol.ALLOW_EXTRA) SERVICE_CLEAR_SCHEMA = vol.Schema({}) +SERVICE_WRITE_SCHEMA = vol.Schema({ + vol.Required(CONF_MESSAGE): cv.string, + vol.Optional(CONF_LEVEL, default='error'): + vol.In(['debug', 'info', 'warning', 'error', 'critical']), + vol.Optional(CONF_LOGGER): cv.string, +}) class LogErrorHandler(logging.Handler): @@ -78,12 +88,21 @@ def async_setup(hass, config): @asyncio.coroutine def async_service_handler(service): """Handle logger services.""" - # Only one service so far - handler.records.clear() + if service.service == 'clear': + handler.records.clear() + return + if service.service == 'write': + logger = logging.getLogger( + service.data.get(CONF_LOGGER, '{}.external'.format(__name__))) + level = service.data[CONF_LEVEL] + getattr(logger, level)(service.data[CONF_MESSAGE]) hass.services.async_register( DOMAIN, SERVICE_CLEAR, async_service_handler, schema=SERVICE_CLEAR_SCHEMA) + hass.services.async_register( + DOMAIN, SERVICE_WRITE, async_service_handler, + schema=SERVICE_WRITE_SCHEMA) return True diff --git a/homeassistant/components/system_log/services.yaml b/homeassistant/components/system_log/services.yaml index 98f86e12f8c..c168185c9b3 100644 --- a/homeassistant/components/system_log/services.yaml +++ b/homeassistant/components/system_log/services.yaml @@ -1,3 +1,15 @@ system_log: clear: description: Clear all log entries. + write: + description: Write log entry. + fields: + message: + description: Message to log. [Required] + example: Something went wrong + level: + description: "Log level: debug, info, warning, error, critical. Defaults to 'error'." + example: debug + logger: + description: Logger name under which to log the message. Defaults to 'system_log.external'. + example: mycomponent.myplatform diff --git a/tests/components/test_system_log.py b/tests/components/test_system_log.py index 0f61986cf47..a3e7d662483 100644 --- a/tests/components/test_system_log.py +++ b/tests/components/test_system_log.py @@ -1,11 +1,12 @@ """Test system log component.""" import asyncio import logging +from unittest.mock import MagicMock, patch + import pytest from homeassistant.bootstrap import async_setup_component from homeassistant.components import system_log -from unittest.mock import MagicMock, patch _LOGGER = logging.getLogger('test_logger') @@ -117,11 +118,54 @@ def test_clear_logs(hass, test_client): yield from get_error_log(hass, test_client, 0) +@asyncio.coroutine +def test_write_log(hass): + """Test that error propagates to logger.""" + logger = MagicMock() + with patch('logging.getLogger', return_value=logger) as mock_logging: + hass.async_add_job( + hass.services.async_call( + system_log.DOMAIN, system_log.SERVICE_WRITE, + {'message': 'test_message'})) + yield from hass.async_block_till_done() + mock_logging.assert_called_once_with( + 'homeassistant.components.system_log.external') + assert logger.method_calls[0] == ('error', ('test_message',)) + + +@asyncio.coroutine +def test_write_choose_logger(hass): + """Test that correct logger is chosen.""" + with patch('logging.getLogger') as mock_logging: + hass.async_add_job( + hass.services.async_call( + system_log.DOMAIN, system_log.SERVICE_WRITE, + {'message': 'test_message', + 'logger': 'myLogger'})) + yield from hass.async_block_till_done() + mock_logging.assert_called_once_with( + 'myLogger') + + +@asyncio.coroutine +def test_write_choose_level(hass): + """Test that correct logger is chosen.""" + logger = MagicMock() + with patch('logging.getLogger', return_value=logger): + hass.async_add_job( + hass.services.async_call( + system_log.DOMAIN, system_log.SERVICE_WRITE, + {'message': 'test_message', + 'level': 'debug'})) + yield from hass.async_block_till_done() + assert logger.method_calls[0] == ('debug', ('test_message',)) + + @asyncio.coroutine def test_unknown_path(hass, test_client): """Test error logged from unknown path.""" _LOGGER.findCaller = MagicMock( - return_value=('unknown_path', 0, None, None)) + return_value=('unknown_path', 0, None, None)) _LOGGER.error('error message') log = (yield from get_error_log(hass, test_client, 1))[0] assert log['source'] == 'unknown_path' @@ -130,16 +174,15 @@ def test_unknown_path(hass, test_client): def log_error_from_test_path(path): """Log error while mocking the path.""" call_path = 'internal_path.py' - with patch.object( - _LOGGER, - 'findCaller', - MagicMock(return_value=(call_path, 0, None, None))): + with patch.object(_LOGGER, + 'findCaller', + MagicMock(return_value=(call_path, 0, None, None))): with patch('traceback.extract_stack', MagicMock(return_value=[ - get_frame('main_path/main.py'), - get_frame(path), - get_frame(call_path), - get_frame('venv_path/logging/log.py')])): + get_frame('main_path/main.py'), + get_frame(path), + get_frame(call_path), + get_frame('venv_path/logging/log.py')])): _LOGGER.error('error message') From 280c1601a2d06af3394c37588a539b3a018785a9 Mon Sep 17 00:00:00 2001 From: kennedyshead Date: Fri, 26 Jan 2018 19:30:48 +0100 Subject: [PATCH 04/17] fixes #11848 (#11915) Adding tests to check the component after latest patch --- .coveragerc | 1 - .../components/device_tracker/asuswrt.py | 59 ++-- .../components/device_tracker/test_asuswrt.py | 282 +++++++++++++++++- 3 files changed, 295 insertions(+), 47 deletions(-) diff --git a/.coveragerc b/.coveragerc index c1a9fa291fe..84ca187fb3a 100644 --- a/.coveragerc +++ b/.coveragerc @@ -341,7 +341,6 @@ omit = homeassistant/components/cover/scsgate.py homeassistant/components/device_tracker/actiontec.py homeassistant/components/device_tracker/aruba.py - homeassistant/components/device_tracker/asuswrt.py homeassistant/components/device_tracker/automatic.py homeassistant/components/device_tracker/bbox.py homeassistant/components/device_tracker/bluetooth_le_tracker.py diff --git a/homeassistant/components/device_tracker/asuswrt.py b/homeassistant/components/device_tracker/asuswrt.py index f49f54b3622..0d27c4b5efd 100644 --- a/homeassistant/components/device_tracker/asuswrt.py +++ b/homeassistant/components/device_tracker/asuswrt.py @@ -25,9 +25,7 @@ _LOGGER = logging.getLogger(__name__) CONF_PUB_KEY = 'pub_key' CONF_SSH_KEY = 'ssh_key' - DEFAULT_SSH_PORT = 22 - SECRET_GROUP = 'Password or SSH Key' PLATFORM_SCHEMA = vol.All( @@ -118,20 +116,10 @@ class AsusWrtDeviceScanner(DeviceScanner): self.port = config[CONF_PORT] if self.protocol == 'ssh': - if not (self.ssh_key or self.password): - _LOGGER.error("No password or private key specified") - self.success_init = False - return - self.connection = SshConnection( self.host, self.port, self.username, self.password, self.ssh_key, self.mode == 'ap') else: - if not self.password: - _LOGGER.error("No password specified") - self.success_init = False - return - self.connection = TelnetConnection( self.host, self.port, self.username, self.password, self.mode == 'ap') @@ -177,11 +165,16 @@ class AsusWrtDeviceScanner(DeviceScanner): """ devices = {} devices.update(self._get_wl()) - devices = self._get_arp(devices) - devices = self._get_neigh(devices) + devices.update(self._get_arp()) + devices.update(self._get_neigh(devices)) if not self.mode == 'ap': devices.update(self._get_leases(devices)) - return devices + + ret_devices = {} + for key in devices: + if devices[key].ip is not None: + ret_devices[key] = devices[key] + return ret_devices def _get_wl(self): lines = self.connection.run_command(_WL_CMD) @@ -219,18 +212,13 @@ class AsusWrtDeviceScanner(DeviceScanner): result = _parse_lines(lines, _IP_NEIGH_REGEX) devices = {} for device in result: - if device['mac']: + if device['mac'] is not None: mac = device['mac'].upper() - devices[mac] = Device(mac, None, None) - else: - cur_devices = { - k: v for k, v in - cur_devices.items() if v.ip != device['ip'] - } - cur_devices.update(devices) - return cur_devices + old_ip = cur_devices.get(mac, {}).ip or None + devices[mac] = Device(mac, device.get('ip', old_ip), None) + return devices - def _get_arp(self, cur_devices): + def _get_arp(self): lines = self.connection.run_command(_ARP_CMD) if not lines: return {} @@ -240,13 +228,7 @@ class AsusWrtDeviceScanner(DeviceScanner): if device['mac']: mac = device['mac'].upper() devices[mac] = Device(mac, device['ip'], None) - else: - cur_devices = { - k: v for k, v in - cur_devices.items() if v.ip != device['ip'] - } - cur_devices.update(devices) - return cur_devices + return devices class _Connection: @@ -272,7 +254,7 @@ class SshConnection(_Connection): def __init__(self, host, port, username, password, ssh_key, ap): """Initialize the SSH connection properties.""" - super(SshConnection, self).__init__() + super().__init__() self._ssh = None self._host = host @@ -322,7 +304,7 @@ class SshConnection(_Connection): self._ssh.login(self._host, self._username, password=self._password, port=self._port) - super(SshConnection, self).connect() + super().connect() def disconnect(self): \ # pylint: disable=broad-except @@ -334,7 +316,7 @@ class SshConnection(_Connection): finally: self._ssh = None - super(SshConnection, self).disconnect() + super().disconnect() class TelnetConnection(_Connection): @@ -342,7 +324,7 @@ class TelnetConnection(_Connection): def __init__(self, host, port, username, password, ap): """Initialize the Telnet connection properties.""" - super(TelnetConnection, self).__init__() + super().__init__() self._telnet = None self._host = host @@ -361,7 +343,6 @@ class TelnetConnection(_Connection): try: if not self.connected: self.connect() - self._telnet.write('{}\n'.format(command).encode('ascii')) data = (self._telnet.read_until(self._prompt_string). split(b'\n')[1:-1]) @@ -392,7 +373,7 @@ class TelnetConnection(_Connection): self._telnet.write((self._password + '\n').encode('ascii')) self._prompt_string = self._telnet.read_until(b'#').split(b'\n')[-1] - super(TelnetConnection, self).connect() + super().connect() def disconnect(self): \ # pylint: disable=broad-except @@ -402,4 +383,4 @@ class TelnetConnection(_Connection): except Exception: pass - super(TelnetConnection, self).disconnect() + super().disconnect() diff --git a/tests/components/device_tracker/test_asuswrt.py b/tests/components/device_tracker/test_asuswrt.py index 0159eec2eff..808d3569b8b 100644 --- a/tests/components/device_tracker/test_asuswrt.py +++ b/tests/components/device_tracker/test_asuswrt.py @@ -5,6 +5,7 @@ import unittest from unittest import mock import voluptuous as vol +from future.backports import socket from homeassistant.setup import setup_component from homeassistant.components import device_tracker @@ -12,8 +13,9 @@ from homeassistant.components.device_tracker import ( CONF_CONSIDER_HOME, CONF_TRACK_NEW, CONF_NEW_DEVICE_DEFAULTS, CONF_AWAY_HIDE) from homeassistant.components.device_tracker.asuswrt import ( - CONF_PROTOCOL, CONF_MODE, CONF_PUB_KEY, DOMAIN, - CONF_PORT, PLATFORM_SCHEMA) + CONF_PROTOCOL, CONF_MODE, CONF_PUB_KEY, DOMAIN, _ARP_REGEX, + CONF_PORT, PLATFORM_SCHEMA, Device, get_scanner, AsusWrtDeviceScanner, + _parse_lines, SshConnection, TelnetConnection) from homeassistant.const import (CONF_PLATFORM, CONF_PASSWORD, CONF_USERNAME, CONF_HOST) @@ -23,6 +25,84 @@ from tests.common import ( FAKEFILE = None +VALID_CONFIG_ROUTER_SSH = {DOMAIN: { + CONF_PLATFORM: 'asuswrt', + CONF_HOST: 'fake_host', + CONF_USERNAME: 'fake_user', + CONF_PROTOCOL: 'ssh', + CONF_MODE: 'router', + CONF_PORT: '22' +} +} + +WL_DATA = [ + 'assoclist 01:02:03:04:06:08\r', + 'assoclist 08:09:10:11:12:14\r', + 'assoclist 08:09:10:11:12:15\r' +] + +WL_DEVICES = { + '01:02:03:04:06:08': Device( + mac='01:02:03:04:06:08', ip=None, name=None), + '08:09:10:11:12:14': Device( + mac='08:09:10:11:12:14', ip=None, name=None), + '08:09:10:11:12:15': Device( + mac='08:09:10:11:12:15', ip=None, name=None) +} + +ARP_DATA = [ + '? (123.123.123.125) at 01:02:03:04:06:08 [ether] on eth0\r', + '? (123.123.123.126) at 08:09:10:11:12:14 [ether] on br0\r' + '? (123.123.123.127) at on br0\r' +] + +ARP_DEVICES = { + '01:02:03:04:06:08': Device( + mac='01:02:03:04:06:08', ip='123.123.123.125', name=None), + '08:09:10:11:12:14': Device( + mac='08:09:10:11:12:14', ip='123.123.123.126', name=None) +} + +NEIGH_DATA = [ + '123.123.123.125 dev eth0 lladdr 01:02:03:04:06:08 REACHABLE\r', + '123.123.123.126 dev br0 lladdr 08:09:10:11:12:14 STALE\r' + '123.123.123.127 dev br0 FAILED\r' +] + +NEIGH_DEVICES = { + '01:02:03:04:06:08': Device( + mac='01:02:03:04:06:08', ip='123.123.123.125', name=None), + '08:09:10:11:12:14': Device( + mac='08:09:10:11:12:14', ip='123.123.123.126', name=None) +} + +LEASES_DATA = [ + '51910 01:02:03:04:06:08 123.123.123.125 TV 01:02:03:04:06:08\r', + '79986 01:02:03:04:06:10 123.123.123.127 android 01:02:03:04:06:15\r', + '23523 08:09:10:11:12:14 123.123.123.126 * 08:09:10:11:12:14\r', +] + +LEASES_DEVICES = { + '01:02:03:04:06:08': Device( + mac='01:02:03:04:06:08', ip='123.123.123.125', name='TV'), + '08:09:10:11:12:14': Device( + mac='08:09:10:11:12:14', ip='123.123.123.126', name='') +} + +WAKE_DEVICES = { + '01:02:03:04:06:08': Device( + mac='01:02:03:04:06:08', ip='123.123.123.125', name='TV'), + '08:09:10:11:12:14': Device( + mac='08:09:10:11:12:14', ip='123.123.123.126', name='') +} + +WAKE_DEVICES_AP = { + '01:02:03:04:06:08': Device( + mac='01:02:03:04:06:08', ip='123.123.123.125', name=None), + '08:09:10:11:12:14': Device( + mac='08:09:10:11:12:14', ip='123.123.123.126', name=None) +} + def setup_module(): """Setup the test module.""" @@ -55,6 +135,24 @@ class TestComponentsDeviceTrackerASUSWRT(unittest.TestCase): except FileNotFoundError: pass + def test_parse_lines_wrong_input(self): + """Testing parse lines.""" + output = _parse_lines("asdf asdfdfsafad", _ARP_REGEX) + self.assertEqual(output, []) + + def test_get_device_name(self): + """Test for getting name.""" + scanner = get_scanner(self.hass, VALID_CONFIG_ROUTER_SSH) + scanner.last_results = WAKE_DEVICES + self.assertEqual('TV', scanner.get_device_name('01:02:03:04:06:08')) + self.assertEqual(None, scanner.get_device_name('01:02:03:04:08:08')) + + def test_scan_devices(self): + """Test for scan devices.""" + scanner = get_scanner(self.hass, VALID_CONFIG_ROUTER_SSH) + scanner.last_results = WAKE_DEVICES + self.assertEqual(list(WAKE_DEVICES), scanner.scan_devices()) + def test_password_or_pub_key_required(self): \ # pylint: disable=invalid-name """Test creating an AsusWRT scanner without a pass or pubkey.""" @@ -63,13 +161,14 @@ class TestComponentsDeviceTrackerASUSWRT(unittest.TestCase): self.hass, DOMAIN, {DOMAIN: { CONF_PLATFORM: 'asuswrt', CONF_HOST: 'fake_host', - CONF_USERNAME: 'fake_user' + CONF_USERNAME: 'fake_user', + CONF_PROTOCOL: 'ssh' }}) @mock.patch( 'homeassistant.components.device_tracker.asuswrt.AsusWrtDeviceScanner', return_value=mock.MagicMock()) - def test_get_scanner_with_password_no_pubkey(self, asuswrt_mock): \ + def test_get_scanner_with_password_no_pubkey(self, asuswrt_mock): \ # pylint: disable=invalid-name """Test creating an AsusWRT scanner with a password and no pubkey.""" conf_dict = { @@ -99,7 +198,7 @@ class TestComponentsDeviceTrackerASUSWRT(unittest.TestCase): @mock.patch( 'homeassistant.components.device_tracker.asuswrt.AsusWrtDeviceScanner', return_value=mock.MagicMock()) - def test_get_scanner_with_pubkey_no_password(self, asuswrt_mock): \ + def test_get_scanner_with_pubkey_no_password(self, asuswrt_mock): \ # pylint: disable=invalid-name """Test creating an AsusWRT scanner with a pubkey and no password.""" conf_dict = { @@ -178,7 +277,7 @@ class TestComponentsDeviceTrackerASUSWRT(unittest.TestCase): password='fake_pass', port=22) ) - def test_ssh_login_without_password_or_pubkey(self): \ + def test_ssh_login_without_password_or_pubkey(self): \ # pylint: disable=invalid-name """Test that login is not called without password or pub_key.""" ssh = mock.MagicMock() @@ -249,7 +348,7 @@ class TestComponentsDeviceTrackerASUSWRT(unittest.TestCase): mock.call(b'#') ) - def test_telnet_login_without_password(self): \ + def test_telnet_login_without_password(self): \ # pylint: disable=invalid-name """Test that login is not called without password or pub_key.""" telnet = mock.MagicMock() @@ -277,3 +376,172 @@ class TestComponentsDeviceTrackerASUSWRT(unittest.TestCase): assert setup_component(self.hass, DOMAIN, {DOMAIN: conf_dict}) telnet.login.assert_not_called() + + def test_get_asuswrt_data(self): + """Test aususwrt data fetch.""" + scanner = get_scanner(self.hass, VALID_CONFIG_ROUTER_SSH) + scanner._get_wl = mock.Mock() + scanner._get_arp = mock.Mock() + scanner._get_neigh = mock.Mock() + scanner._get_leases = mock.Mock() + scanner._get_wl.return_value = WL_DEVICES + scanner._get_arp.return_value = ARP_DEVICES + scanner._get_neigh.return_value = NEIGH_DEVICES + scanner._get_leases.return_value = LEASES_DEVICES + self.assertEqual(WAKE_DEVICES, scanner.get_asuswrt_data()) + + def test_get_asuswrt_data_ap(self): + """Test for get asuswrt_data in ap mode.""" + conf = VALID_CONFIG_ROUTER_SSH.copy()[DOMAIN] + conf[CONF_MODE] = 'ap' + scanner = AsusWrtDeviceScanner(conf) + scanner._get_wl = mock.Mock() + scanner._get_arp = mock.Mock() + scanner._get_neigh = mock.Mock() + scanner._get_leases = mock.Mock() + scanner._get_wl.return_value = WL_DEVICES + scanner._get_arp.return_value = ARP_DEVICES + scanner._get_neigh.return_value = NEIGH_DEVICES + scanner._get_leases.return_value = LEASES_DEVICES + self.assertEqual(WAKE_DEVICES_AP, scanner.get_asuswrt_data()) + + def test_update_info(self): + """Test for update info.""" + scanner = get_scanner(self.hass, VALID_CONFIG_ROUTER_SSH) + scanner.get_asuswrt_data = mock.Mock() + scanner.get_asuswrt_data.return_value = WAKE_DEVICES + self.assertTrue(scanner._update_info()) + self.assertTrue(scanner.last_results, WAKE_DEVICES) + scanner.success_init = False + self.assertFalse(scanner._update_info()) + + @mock.patch( + 'homeassistant.components.device_tracker.asuswrt.SshConnection') + def test_get_wl(self, mocked_ssh): + """Testing wl.""" + mocked_ssh.run_command.return_value = WL_DATA + scanner = get_scanner(self.hass, VALID_CONFIG_ROUTER_SSH) + scanner.connection = mocked_ssh + self.assertEqual(WL_DEVICES, scanner._get_wl()) + mocked_ssh.run_command.return_value = '' + self.assertEqual({}, scanner._get_wl()) + + @mock.patch( + 'homeassistant.components.device_tracker.asuswrt.SshConnection') + def test_get_arp(self, mocked_ssh): + """Testing arp.""" + mocked_ssh.run_command.return_value = ARP_DATA + + scanner = get_scanner(self.hass, VALID_CONFIG_ROUTER_SSH) + scanner.connection = mocked_ssh + self.assertEqual(ARP_DEVICES, scanner._get_arp()) + mocked_ssh.run_command.return_value = '' + self.assertEqual({}, scanner._get_arp()) + + @mock.patch( + 'homeassistant.components.device_tracker.asuswrt.SshConnection') + def test_get_neigh(self, mocked_ssh): + """Testing neigh.""" + mocked_ssh.run_command.return_value = NEIGH_DATA + + scanner = get_scanner(self.hass, VALID_CONFIG_ROUTER_SSH) + scanner.connection = mocked_ssh + self.assertEqual(NEIGH_DEVICES, scanner._get_neigh(ARP_DEVICES.copy())) + mocked_ssh.run_command.return_value = '' + self.assertEqual({}, scanner._get_neigh(ARP_DEVICES.copy())) + + @mock.patch( + 'homeassistant.components.device_tracker.asuswrt.SshConnection') + def test_get_leases(self, mocked_ssh): + """Testing leases.""" + mocked_ssh.run_command.return_value = LEASES_DATA + + scanner = get_scanner(self.hass, VALID_CONFIG_ROUTER_SSH) + scanner.connection = mocked_ssh + self.assertEqual( + LEASES_DEVICES, scanner._get_leases(NEIGH_DEVICES.copy())) + mocked_ssh.run_command.return_value = '' + self.assertEqual({}, scanner._get_leases(NEIGH_DEVICES.copy())) + + +class TestSshConnection(unittest.TestCase): + """Testing SshConnection.""" + + def setUp(self): + """Setup test env.""" + self.connection = SshConnection( + 'fake', 'fake', 'fake', 'fake', 'fake', 'fake') + self.connection._connected = True + + def test_run_command_exception_eof(self): + """Testing exception in run_command.""" + from pexpect import exceptions + self.connection._ssh = mock.Mock() + self.connection._ssh.sendline = mock.Mock() + self.connection._ssh.sendline.side_effect = exceptions.EOF('except') + self.connection.run_command('test') + self.assertFalse(self.connection._connected) + self.assertIsNone(self.connection._ssh) + + def test_run_command_exception_pxssh(self): + """Testing exception in run_command.""" + from pexpect import pxssh + self.connection._ssh = mock.Mock() + self.connection._ssh.sendline = mock.Mock() + self.connection._ssh.sendline.side_effect = pxssh.ExceptionPxssh( + 'except') + self.connection.run_command('test') + self.assertFalse(self.connection._connected) + self.assertIsNone(self.connection._ssh) + + def test_run_command_assertion_error(self): + """Testing exception in run_command.""" + self.connection._ssh = mock.Mock() + self.connection._ssh.sendline = mock.Mock() + self.connection._ssh.sendline.side_effect = AssertionError('except') + self.connection.run_command('test') + self.assertFalse(self.connection._connected) + self.assertIsNone(self.connection._ssh) + + +class TestTelnetConnection(unittest.TestCase): + """Testing TelnetConnection.""" + + def setUp(self): + """Setup test env.""" + self.connection = TelnetConnection( + 'fake', 'fake', 'fake', 'fake', 'fake') + self.connection._connected = True + + def test_run_command_exception_eof(self): + """Testing EOFException in run_command.""" + self.connection._telnet = mock.Mock() + self.connection._telnet.write = mock.Mock() + self.connection._telnet.write.side_effect = EOFError('except') + self.connection.run_command('test') + self.assertFalse(self.connection._connected) + + def test_run_command_exception_connection_refused(self): + """Testing ConnectionRefusedError in run_command.""" + self.connection._telnet = mock.Mock() + self.connection._telnet.write = mock.Mock() + self.connection._telnet.write.side_effect = ConnectionRefusedError( + 'except') + self.connection.run_command('test') + self.assertFalse(self.connection._connected) + + def test_run_command_exception_gaierror(self): + """Testing socket.gaierror in run_command.""" + self.connection._telnet = mock.Mock() + self.connection._telnet.write = mock.Mock() + self.connection._telnet.write.side_effect = socket.gaierror('except') + self.connection.run_command('test') + self.assertFalse(self.connection._connected) + + def test_run_command_exception_oserror(self): + """Testing OSError in run_command.""" + self.connection._telnet = mock.Mock() + self.connection._telnet.write = mock.Mock() + self.connection._telnet.write.side_effect = OSError('except') + self.connection.run_command('test') + self.assertFalse(self.connection._connected) From 07b2f38046a32fc69a0f06f6dba2d53f296c31b9 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 26 Jan 2018 03:37:06 -0800 Subject: [PATCH 05/17] Allow setting climate devices to AUTO mode via Google Assistant (#11923) * Allow setting climate devices to AUTO mode via Google Assistant * Remove cast to lower * Clarify const name --- homeassistant/components/climate/nest.py | 17 +++++------ .../components/google_assistant/const.py | 3 +- .../components/google_assistant/smart_home.py | 29 ++++++++++++------- tests/components/google_assistant/__init__.py | 2 +- 4 files changed, 29 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/climate/nest.py b/homeassistant/components/climate/nest.py index 3b550c43368..b4492821b1f 100644 --- a/homeassistant/components/climate/nest.py +++ b/homeassistant/components/climate/nest.py @@ -10,7 +10,7 @@ import voluptuous as vol from homeassistant.components.nest import DATA_NEST from homeassistant.components.climate import ( - STATE_AUTO, STATE_COOL, STATE_HEAT, ClimateDevice, + STATE_AUTO, STATE_COOL, STATE_HEAT, STATE_ECO, ClimateDevice, PLATFORM_SCHEMA, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, ATTR_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW, @@ -27,8 +27,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.All(vol.Coerce(int), vol.Range(min=1)), }) -STATE_ECO = 'eco' -STATE_HEAT_COOL = 'heat-cool' +NEST_MODE_HEAT_COOL = 'heat-cool' SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_TARGET_TEMPERATURE_HIGH | SUPPORT_TARGET_TEMPERATURE_LOW | SUPPORT_OPERATION_MODE | @@ -118,14 +117,14 @@ class NestThermostat(ClimateDevice): """Return current operation ie. heat, cool, idle.""" if self._mode in [STATE_HEAT, STATE_COOL, STATE_OFF, STATE_ECO]: return self._mode - elif self._mode == STATE_HEAT_COOL: + elif self._mode == NEST_MODE_HEAT_COOL: return STATE_AUTO return STATE_UNKNOWN @property def target_temperature(self): """Return the temperature we try to reach.""" - if self._mode != STATE_HEAT_COOL and not self.is_away_mode_on: + if self._mode != NEST_MODE_HEAT_COOL and not self.is_away_mode_on: return self._target_temperature return None @@ -136,7 +135,7 @@ class NestThermostat(ClimateDevice): self._eco_temperature[0]: # eco_temperature is always a low, high tuple return self._eco_temperature[0] - if self._mode == STATE_HEAT_COOL: + if self._mode == NEST_MODE_HEAT_COOL: return self._target_temperature[0] return None @@ -147,7 +146,7 @@ class NestThermostat(ClimateDevice): self._eco_temperature[1]: # eco_temperature is always a low, high tuple return self._eco_temperature[1] - if self._mode == STATE_HEAT_COOL: + if self._mode == NEST_MODE_HEAT_COOL: return self._target_temperature[1] return None @@ -160,7 +159,7 @@ class NestThermostat(ClimateDevice): """Set new target temperature.""" target_temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW) target_temp_high = kwargs.get(ATTR_TARGET_TEMP_HIGH) - if self._mode == STATE_HEAT_COOL: + if self._mode == NEST_MODE_HEAT_COOL: if target_temp_low is not None and target_temp_high is not None: temp = (target_temp_low, target_temp_high) else: @@ -173,7 +172,7 @@ class NestThermostat(ClimateDevice): if operation_mode in [STATE_HEAT, STATE_COOL, STATE_OFF, STATE_ECO]: device_mode = operation_mode elif operation_mode == STATE_AUTO: - device_mode = STATE_HEAT_COOL + device_mode = NEST_MODE_HEAT_COOL self.device.mode = device_mode @property diff --git a/homeassistant/components/google_assistant/const.py b/homeassistant/components/google_assistant/const.py index fc250c4b655..0483f424ca3 100644 --- a/homeassistant/components/google_assistant/const.py +++ b/homeassistant/components/google_assistant/const.py @@ -18,7 +18,8 @@ DEFAULT_EXPOSE_BY_DEFAULT = True DEFAULT_EXPOSED_DOMAINS = [ 'switch', 'light', 'group', 'media_player', 'fan', 'cover', 'climate' ] -CLIMATE_SUPPORTED_MODES = {'heat', 'cool', 'off', 'on', 'heatcool'} +CLIMATE_MODE_HEATCOOL = 'heatcool' +CLIMATE_SUPPORTED_MODES = {'heat', 'cool', 'off', 'on', CLIMATE_MODE_HEATCOOL} PREFIX_TRAITS = 'action.devices.traits.' TRAIT_ONOFF = PREFIX_TRAITS + 'OnOff' diff --git a/homeassistant/components/google_assistant/smart_home.py b/homeassistant/components/google_assistant/smart_home.py index e2ad609a007..d8e9f668c8e 100644 --- a/homeassistant/components/google_assistant/smart_home.py +++ b/homeassistant/components/google_assistant/smart_home.py @@ -33,7 +33,7 @@ from .const import ( TRAIT_ONOFF, TRAIT_BRIGHTNESS, TRAIT_COLOR_TEMP, TRAIT_RGB_COLOR, TRAIT_SCENE, TRAIT_TEMPERATURE_SETTING, TYPE_LIGHT, TYPE_SCENE, TYPE_SWITCH, TYPE_THERMOSTAT, - CONF_ALIASES, CLIMATE_SUPPORTED_MODES + CONF_ALIASES, CLIMATE_SUPPORTED_MODES, CLIMATE_MODE_HEATCOOL ) HANDLERS = Registry() @@ -147,12 +147,15 @@ def entity_to_device(entity: Entity, config: Config, units: UnitSystem): entity.attributes.get(light.ATTR_MIN_MIREDS)))) if entity.domain == climate.DOMAIN: - modes = ','.join( - m.lower() for m in entity.attributes.get( - climate.ATTR_OPERATION_LIST, []) - if m.lower() in CLIMATE_SUPPORTED_MODES) + modes = [] + for mode in entity.attributes.get(climate.ATTR_OPERATION_LIST, []): + if mode in CLIMATE_SUPPORTED_MODES: + modes.append(mode) + elif mode == climate.STATE_AUTO: + modes.append(CLIMATE_MODE_HEATCOOL) + device['attributes'] = { - 'availableThermostatModes': modes, + 'availableThermostatModes': ','.join(modes), 'thermostatTemperatureUnit': 'F' if units.temperature_unit == TEMP_FAHRENHEIT else 'C', } @@ -323,9 +326,9 @@ def determine_service( # special climate handling if domain == climate.DOMAIN: if command == COMMAND_THERMOSTAT_TEMPERATURE_SETPOINT: - service_data['temperature'] = units.temperature( - params.get('thermostatTemperatureSetpoint', 25), - TEMP_CELSIUS) + service_data['temperature'] = \ + units.temperature( + params['thermostatTemperatureSetpoint'], TEMP_CELSIUS) return (climate.SERVICE_SET_TEMPERATURE, service_data) if command == COMMAND_THERMOSTAT_TEMPERATURE_SET_RANGE: service_data['target_temp_high'] = units.temperature( @@ -336,8 +339,12 @@ def determine_service( TEMP_CELSIUS) return (climate.SERVICE_SET_TEMPERATURE, service_data) if command == COMMAND_THERMOSTAT_SET_MODE: - service_data['operation_mode'] = params.get( - 'thermostatMode', 'off') + mode = params['thermostatMode'] + + if mode == CLIMATE_MODE_HEATCOOL: + mode = climate.STATE_AUTO + + service_data['operation_mode'] = mode return (climate.SERVICE_SET_OPERATION_MODE, service_data) if command == COMMAND_BRIGHTNESS: diff --git a/tests/components/google_assistant/__init__.py b/tests/components/google_assistant/__init__.py index db075fb6789..022cf852b88 100644 --- a/tests/components/google_assistant/__init__.py +++ b/tests/components/google_assistant/__init__.py @@ -211,7 +211,7 @@ DEMO_DEVICES = [{ 'type': 'action.devices.types.THERMOSTAT', 'willReportState': False, 'attributes': { - 'availableThermostatModes': 'heat,cool,off', + 'availableThermostatModes': 'heat,cool,heatcool,off', 'thermostatTemperatureUnit': 'C', }, }, { From abde8c40c91c4394443a6f10ec57363456095d01 Mon Sep 17 00:00:00 2001 From: Bas Schipper Date: Fri, 26 Jan 2018 13:45:02 +0100 Subject: [PATCH 06/17] Fixed rfxtrx binary_sensor KeyError on missing optional device_class (#11925) * Fixed rfxtrx binary_sensor KeyError on missing optional device_class * Fixed rfxtrx binary_sensor KeyError on missing optional device_class --- homeassistant/components/binary_sensor/rfxtrx.py | 5 +++-- homeassistant/components/rfxtrx.py | 1 - 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/binary_sensor/rfxtrx.py b/homeassistant/components/binary_sensor/rfxtrx.py index 763003cab03..2cc0aee2c7b 100644 --- a/homeassistant/components/binary_sensor/rfxtrx.py +++ b/homeassistant/components/binary_sensor/rfxtrx.py @@ -10,7 +10,7 @@ import voluptuous as vol from homeassistant.components import rfxtrx from homeassistant.components.binary_sensor import ( - PLATFORM_SCHEMA, BinarySensorDevice) + PLATFORM_SCHEMA, DEVICE_CLASSES_SCHEMA, BinarySensorDevice) from homeassistant.components.rfxtrx import ( ATTR_NAME, CONF_AUTOMATIC_ADD, CONF_DATA_BITS, CONF_DEVICES, CONF_FIRE_EVENT, CONF_OFF_DELAY) @@ -29,7 +29,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_DEVICES, default={}): { cv.string: vol.Schema({ vol.Optional(CONF_NAME, default=None): cv.string, - vol.Optional(CONF_DEVICE_CLASS, default=None): cv.string, + vol.Optional(CONF_DEVICE_CLASS, default=None): + DEVICE_CLASSES_SCHEMA, vol.Optional(CONF_FIRE_EVENT, default=False): cv.boolean, vol.Optional(CONF_OFF_DELAY, default=None): vol.Any(cv.time_period, cv.positive_timedelta), diff --git a/homeassistant/components/rfxtrx.py b/homeassistant/components/rfxtrx.py index 4994e333eda..7d2e428c56b 100644 --- a/homeassistant/components/rfxtrx.py +++ b/homeassistant/components/rfxtrx.py @@ -39,7 +39,6 @@ CONF_AUTOMATIC_ADD = 'automatic_add' CONF_DATA_TYPE = 'data_type' CONF_SIGNAL_REPETITIONS = 'signal_repetitions' CONF_FIRE_EVENT = 'fire_event' -CONF_DATA_BITS = 'data_bits' CONF_DUMMY = 'dummy' CONF_DEVICE = 'device' CONF_DEBUG = 'debug' From 7bbef68b2a0c48b874ee7b15fddf1039021107f1 Mon Sep 17 00:00:00 2001 From: Phil Frost Date: Fri, 26 Jan 2018 18:40:39 +0000 Subject: [PATCH 07/17] Implement Alexa temperature sensors (#11930) --- homeassistant/components/alexa/smart_home.py | 84 ++++++++++++++++++-- tests/components/alexa/test_smart_home.py | 56 ++++++++++++- 2 files changed, 133 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/alexa/smart_home.py b/homeassistant/components/alexa/smart_home.py index 2a37fba8b43..2fae0b323a0 100644 --- a/homeassistant/components/alexa/smart_home.py +++ b/homeassistant/components/alexa/smart_home.py @@ -7,7 +7,7 @@ from uuid import uuid4 from homeassistant.components import ( alert, automation, cover, fan, group, input_boolean, light, lock, - media_player, scene, script, switch, http) + media_player, scene, script, switch, http, sensor) import homeassistant.core as ha import homeassistant.util.color as color_util from homeassistant.util.decorator import Registry @@ -16,7 +16,8 @@ from homeassistant.const import ( SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_STOP, SERVICE_SET_COVER_POSITION, SERVICE_TURN_OFF, SERVICE_TURN_ON, - SERVICE_UNLOCK, SERVICE_VOLUME_SET) + SERVICE_UNLOCK, SERVICE_VOLUME_SET, TEMP_FAHRENHEIT, TEMP_CELSIUS, + CONF_UNIT_OF_MEASUREMENT) from .const import CONF_FILTER, CONF_ENTITY_CONFIG _LOGGER = logging.getLogger(__name__) @@ -24,9 +25,15 @@ _LOGGER = logging.getLogger(__name__) API_DIRECTIVE = 'directive' API_ENDPOINT = 'endpoint' API_EVENT = 'event' +API_CONTEXT = 'context' API_HEADER = 'header' API_PAYLOAD = 'payload' +API_TEMP_UNITS = { + TEMP_FAHRENHEIT: 'FAHRENHEIT', + TEMP_CELSIUS: 'CELSIUS', +} + SMART_HOME_HTTP_ENDPOINT = '/api/alexa/smart_home' CONF_DESCRIPTION = 'description' @@ -94,6 +101,8 @@ class _DisplayCategory(object): def _capability(interface, version=3, supports_deactivation=None, + retrievable=None, + properties_supported=None, cap_type='AlexaInterface'): """Return a Smart Home API capability object. @@ -102,9 +111,7 @@ def _capability(interface, There are some additional fields allowed but not implemented here since we've no use case for them yet: - - properties.supported - proactively_reported - - retrievable `supports_deactivation` applies only to scenes. """ @@ -117,6 +124,12 @@ def _capability(interface, if supports_deactivation is not None: result['supportsDeactivation'] = supports_deactivation + if retrievable is not None: + result['retrievable'] = retrievable + + if properties_supported is not None: + result['properties'] = {'supported': properties_supported} + return result @@ -144,6 +157,8 @@ class _EntityCapabilities(object): def capabilities(self): """Return a list of supported capabilities. + If the returned list is empty, the entity will not be discovered. + You might find _capability() useful. """ raise NotImplementedError @@ -269,6 +284,28 @@ class _GroupCapabilities(_EntityCapabilities): supports_deactivation=True)] +class _SensorCapabilities(_EntityCapabilities): + def default_display_categories(self): + # although there are other kinds of sensors, all but temperature + # sensors are currently ignored. + return [_DisplayCategory.TEMPERATURE_SENSOR] + + def capabilities(self): + capabilities = [] + + attrs = self.entity.attributes + if attrs.get(CONF_UNIT_OF_MEASUREMENT) in ( + TEMP_FAHRENHEIT, + TEMP_CELSIUS, + ): + capabilities.append(_capability( + 'Alexa.TemperatureSensor', + retrievable=True, + properties_supported=[{'name': 'temperature'}])) + + return capabilities + + class _UnknownEntityDomainError(Exception): pass @@ -296,6 +333,7 @@ _CAPABILITIES_FOR_DOMAIN = { scene.DOMAIN: _SceneCapabilities, script.DOMAIN: _ScriptCapabilities, switch.DOMAIN: _SwitchCapabilities, + sensor.DOMAIN: _SensorCapabilities, } @@ -407,7 +445,11 @@ def async_handle_message(hass, config, message): return (yield from funct_ref(hass, config, message)) -def api_message(request, name='Response', namespace='Alexa', payload=None): +def api_message(request, + name='Response', + namespace='Alexa', + payload=None, + context=None): """Create a API formatted response message. Async friendly. @@ -435,6 +477,9 @@ def api_message(request, name='Response', namespace='Alexa', payload=None): if API_ENDPOINT in request: response[API_EVENT][API_ENDPOINT] = request[API_ENDPOINT].copy() + if context is not None: + response[API_CONTEXT] = context + return response @@ -490,7 +535,12 @@ def async_api_discovery(hass, config, request): 'manufacturerName': 'Home Assistant', } - endpoint['capabilities'] = entity_capabilities.capabilities() + alexa_capabilities = entity_capabilities.capabilities() + if not alexa_capabilities: + _LOGGER.debug("Not exposing %s because it has no capabilities", + entity.entity_id) + continue + endpoint['capabilities'] = alexa_capabilities discovery_endpoints.append(endpoint) return api_message( @@ -976,3 +1026,25 @@ def async_api_previous(hass, config, request, entity): data, blocking=False) return api_message(request) + + +@HANDLERS.register(('Alexa', 'ReportState')) +@extract_entity +@asyncio.coroutine +def async_api_reportstate(hass, config, request, entity): + """Process a ReportState request.""" + unit = entity.attributes[CONF_UNIT_OF_MEASUREMENT] + temp_property = { + 'namespace': 'Alexa.TemperatureSensor', + 'name': 'temperature', + 'value': { + 'value': float(entity.state), + 'scale': API_TEMP_UNITS[unit], + }, + } + + return api_message( + request, + name='StateReport', + context={'properties': [temp_property]} + ) diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 0f81d687278..3416dfbe367 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -5,6 +5,7 @@ from uuid import uuid4 import pytest +from homeassistant.const import TEMP_FAHRENHEIT, CONF_UNIT_OF_MEASUREMENT from homeassistant.setup import async_setup_component from homeassistant.components import alexa from homeassistant.components.alexa import smart_home @@ -166,13 +167,27 @@ def test_discovery_request(hass): 'position': 85 }) + hass.states.async_set( + 'sensor.test_temp', '59', { + 'friendly_name': "Test Temp Sensor", + 'unit_of_measurement': TEMP_FAHRENHEIT, + }) + + # This sensor measures a quantity not applicable to Alexa, and should not + # be discovered. + hass.states.async_set( + 'sensor.test_sickness', '0.1', { + 'friendly_name': "Test Space Sickness Sensor", + 'unit_of_measurement': 'garn', + }) + msg = yield from smart_home.async_handle_message( hass, DEFAULT_CONFIG, request) assert 'event' in msg msg = msg['event'] - assert len(msg['payload']['endpoints']) == 16 + assert len(msg['payload']['endpoints']) == 17 assert msg['header']['name'] == 'Discover.Response' assert msg['header']['namespace'] == 'Alexa.Discovery' @@ -334,6 +349,17 @@ def test_discovery_request(hass): assert 'Alexa.PowerController' in caps continue + if appliance['endpointId'] == 'sensor#test_temp': + assert appliance['displayCategories'][0] == 'TEMPERATURE_SENSOR' + assert appliance['friendlyName'] == 'Test Temp Sensor' + assert len(appliance['capabilities']) == 1 + capability = appliance['capabilities'][0] + assert capability['interface'] == 'Alexa.TemperatureSensor' + assert capability['retrievable'] is True + properties = capability['properties'] + assert {'name': 'temperature'} in properties['supported'] + continue + raise AssertionError("Unknown appliance!") @@ -1170,6 +1196,34 @@ def test_api_mute(hass, domain): assert msg['header']['name'] == 'Response' +@asyncio.coroutine +def test_api_report_temperature(hass): + """Test API ReportState response for a temperature sensor.""" + request = get_new_request('Alexa', 'ReportState', 'sensor#test') + + # setup test devices + hass.states.async_set( + 'sensor.test', '42', { + 'friendly_name': 'test sensor', + CONF_UNIT_OF_MEASUREMENT: TEMP_FAHRENHEIT, + }) + + msg = yield from smart_home.async_handle_message( + hass, DEFAULT_CONFIG, request) + yield from hass.async_block_till_done() + + header = msg['event']['header'] + assert header['namespace'] == 'Alexa' + assert header['name'] == 'StateReport' + + properties = msg['context']['properties'] + assert len(properties) == 1 + prop = properties[0] + assert prop['namespace'] == 'Alexa.TemperatureSensor' + assert prop['name'] == 'temperature' + assert prop['value'] == {'value': 42.0, 'scale': 'FAHRENHEIT'} + + @asyncio.coroutine def test_entity_config(hass): """Test that we can configure things via entity config.""" From af69a307a8aff02c1de1cf2455b4c1f457880ce3 Mon Sep 17 00:00:00 2001 From: Daniel Perna Date: Fri, 26 Jan 2018 21:41:43 +0100 Subject: [PATCH 08/17] Update pyhomematic to 0.1.38 (#11936) --- homeassistant/components/homematic/__init__.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/homematic/__init__.py b/homeassistant/components/homematic/__init__.py index b2f6384d467..db2a43d8728 100644 --- a/homeassistant/components/homematic/__init__.py +++ b/homeassistant/components/homematic/__init__.py @@ -20,7 +20,7 @@ from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv from homeassistant.loader import bind_hass -REQUIREMENTS = ['pyhomematic==0.1.37'] +REQUIREMENTS = ['pyhomematic==0.1.38'] DOMAIN = 'homematic' _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index fa872ddb2d1..aa3c654a7ca 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -732,7 +732,7 @@ pyhik==0.1.4 pyhiveapi==0.2.11 # homeassistant.components.homematic -pyhomematic==0.1.37 +pyhomematic==0.1.38 # homeassistant.components.sensor.hydroquebec pyhydroquebec==2.1.0 From 5bdbf3dfc03258e4801a53ffd564e573afff1ab8 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 29 Jan 2018 15:40:08 -0800 Subject: [PATCH 09/17] Version bump to 0.62.1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 5682a65ab7d..6470f50d460 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 62 -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 0713dbc7a35b23bb07e43630376a4ac6e0b46050 Mon Sep 17 00:00:00 2001 From: tschmidty69 Date: Sat, 27 Jan 2018 14:55:47 -0500 Subject: [PATCH 10/17] Snips - (fix/change) remove response when intent not handled (#11929) * Remove snips endSession response on unknownIntent * Removed snips_response for unknown and error. From 9fed3acc90c9c4cd585069ccf21ab050cbc8d416 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sat, 27 Jan 2018 16:20:28 +0200 Subject: [PATCH 11/17] Fix asuswrt AttributeError on neigh for unknown device (#11960) --- homeassistant/components/device_tracker/asuswrt.py | 3 ++- tests/components/device_tracker/test_asuswrt.py | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/device_tracker/asuswrt.py b/homeassistant/components/device_tracker/asuswrt.py index 0d27c4b5efd..2196dd78fdb 100644 --- a/homeassistant/components/device_tracker/asuswrt.py +++ b/homeassistant/components/device_tracker/asuswrt.py @@ -214,7 +214,8 @@ class AsusWrtDeviceScanner(DeviceScanner): for device in result: if device['mac'] is not None: mac = device['mac'].upper() - old_ip = cur_devices.get(mac, {}).ip or None + old_device = cur_devices.get(mac) + old_ip = old_device.ip if old_device else None devices[mac] = Device(mac, device.get('ip', old_ip), None) return devices diff --git a/tests/components/device_tracker/test_asuswrt.py b/tests/components/device_tracker/test_asuswrt.py index 808d3569b8b..6e646e9862d 100644 --- a/tests/components/device_tracker/test_asuswrt.py +++ b/tests/components/device_tracker/test_asuswrt.py @@ -447,6 +447,9 @@ class TestComponentsDeviceTrackerASUSWRT(unittest.TestCase): scanner = get_scanner(self.hass, VALID_CONFIG_ROUTER_SSH) scanner.connection = mocked_ssh self.assertEqual(NEIGH_DEVICES, scanner._get_neigh(ARP_DEVICES.copy())) + self.assertEqual(NEIGH_DEVICES, scanner._get_neigh({ + 'UN:KN:WN:DE:VI:CE': Device('UN:KN:WN:DE:VI:CE', None, None), + })) mocked_ssh.run_command.return_value = '' self.assertEqual({}, scanner._get_neigh(ARP_DEVICES.copy())) From 8709a397f6823551394d6779b420847f575234a6 Mon Sep 17 00:00:00 2001 From: Frantz Date: Tue, 30 Jan 2018 00:56:55 +0200 Subject: [PATCH 12/17] Set default values for Daikin devices that don't support fan direction and fan speed features (#12000) --- homeassistant/components/climate/daikin.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/homeassistant/components/climate/daikin.py b/homeassistant/components/climate/daikin.py index 1f38fdf3c82..fea1fcee3a3 100644 --- a/homeassistant/components/climate/daikin.py +++ b/homeassistant/components/climate/daikin.py @@ -98,10 +98,16 @@ class DaikinClimate(ClimateDevice): daikin_attr = HA_ATTR_TO_DAIKIN[ATTR_FAN_MODE] if self._api.device.values.get(daikin_attr) is not None: self._supported_features |= SUPPORT_FAN_MODE + else: + # even devices without support must have a default valid value + self._api.device.values[daikin_attr] = 'A' daikin_attr = HA_ATTR_TO_DAIKIN[ATTR_SWING_MODE] if self._api.device.values.get(daikin_attr) is not None: self._supported_features |= SUPPORT_SWING_MODE + else: + # even devices without support must have a default valid value + self._api.device.values[daikin_attr] = '0' def get(self, key): """Retrieve device settings from API library cache.""" From e084a260c610bac888c9f4f1dea9acb15242558a Mon Sep 17 00:00:00 2001 From: smoldaner Date: Tue, 30 Jan 2018 00:02:26 +0100 Subject: [PATCH 13/17] Fix parameter escaping (#12008) From rfc3986: The characters slash ("/") and question mark ("?") may represent data within the query component See https://tools.ietf.org/html/rfc3986#section-3.4 --- homeassistant/components/media_player/squeezebox.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/media_player/squeezebox.py b/homeassistant/components/media_player/squeezebox.py index 13f05cc59f7..22f701de1cc 100644 --- a/homeassistant/components/media_player/squeezebox.py +++ b/homeassistant/components/media_player/squeezebox.py @@ -494,5 +494,5 @@ class SqueezeBoxDevice(MediaPlayerDevice): all_params = [command] if parameters: for parameter in parameters: - all_params.append(urllib.parse.quote(parameter, safe=':=')) + all_params.append(urllib.parse.quote(parameter, safe=':=/?')) return self.async_query(*all_params) From 170a0c9888d803e49c58fe8f3dcae9fc1138ea1d Mon Sep 17 00:00:00 2001 From: Rene Nulsch <33263735+ReneNulschDE@users.noreply.github.com> Date: Mon, 29 Jan 2018 23:49:38 +0100 Subject: [PATCH 14/17] Error handling, in case no connections are available (#12010) * Error handling, in case no connections are available * Fix elif to if --- homeassistant/components/sensor/deutsche_bahn.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sensor/deutsche_bahn.py b/homeassistant/components/sensor/deutsche_bahn.py index c13fc930ed1..278cf5382c1 100644 --- a/homeassistant/components/sensor/deutsche_bahn.py +++ b/homeassistant/components/sensor/deutsche_bahn.py @@ -67,15 +67,17 @@ class DeutscheBahnSensor(Entity): def device_state_attributes(self): """Return the state attributes.""" connections = self.data.connections[0] - connections['next'] = self.data.connections[1]['departure'] - connections['next_on'] = self.data.connections[2]['departure'] + if len(self.data.connections) > 1: + connections['next'] = self.data.connections[1]['departure'] + if len(self.data.connections) > 2: + connections['next_on'] = self.data.connections[2]['departure'] return connections def update(self): """Get the latest delay from bahn.de and updates the state.""" self.data.update() self._state = self.data.connections[0].get('departure', 'Unknown') - if self.data.connections[0]['delay'] != 0: + if self.data.connections[0].get('delay', 0) != 0: self._state += " + {}".format(self.data.connections[0]['delay']) @@ -96,6 +98,9 @@ class SchieneData(object): self.connections = self.schiene.connections( self.start, self.goal, dt_util.as_local(dt_util.utcnow())) + if not self.connections: + self.connections = [{}] + for con in self.connections: # Detail info is not useful. Having a more consistent interface # simplifies usage of template sensors. From a59d26b1faee871bbf8ce7061ba9b05b3aaa05da Mon Sep 17 00:00:00 2001 From: c727 Date: Mon, 29 Jan 2018 23:18:33 +0100 Subject: [PATCH 15/17] Fix 404 for Hass.io panel using frontend dev (#12039) * Fix 404 for Hass.io panel using frontend dev * Hound --- homeassistant/components/frontend/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 8f5a18ff843..70f203d2df2 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -300,7 +300,8 @@ def async_setup(hass, config): if is_dev: for subpath in ["src", "build-translations", "build-temp", "build", - "hass_frontend", "bower_components", "panels"]: + "hass_frontend", "bower_components", "panels", + "hassio"]: hass.http.register_static_path( "/home-assistant-polymer/{}".format(subpath), os.path.join(repo_path, subpath), From a1c0544e3512a19332e76a32b58aca8e433edb82 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Tue, 30 Jan 2018 00:54:49 +0100 Subject: [PATCH 16/17] Upgrade pyharmony to 1.0.20 (#12043) --- homeassistant/components/remote/harmony.py | 7 ++++--- requirements_all.txt | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/remote/harmony.py b/homeassistant/components/remote/harmony.py index 89cdc7529cb..39f09ea66a2 100644 --- a/homeassistant/components/remote/harmony.py +++ b/homeassistant/components/remote/harmony.py @@ -17,9 +17,10 @@ from homeassistant.components.remote import ( from homeassistant.const import ( ATTR_ENTITY_ID, CONF_HOST, CONF_NAME, CONF_PORT, EVENT_HOMEASSISTANT_STOP) import homeassistant.helpers.config_validation as cv +from homeassistant.exceptions import PlatformNotReady from homeassistant.util import slugify -REQUIREMENTS = ['pyharmony==1.0.18'] +REQUIREMENTS = ['pyharmony==1.0.20'] _LOGGER = logging.getLogger(__name__) @@ -97,8 +98,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): DEVICES.append(device) add_devices([device]) register_services(hass) - except ValueError: - _LOGGER.warning("Failed to initialize remote: %s", name) + except (ValueError, AttributeError): + raise PlatformNotReady def register_services(hass): diff --git a/requirements_all.txt b/requirements_all.txt index aa3c654a7ca..42b7e23aa06 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -723,7 +723,7 @@ pyflexit==0.3 pyfttt==0.3 # homeassistant.components.remote.harmony -pyharmony==1.0.18 +pyharmony==1.0.20 # homeassistant.components.binary_sensor.hikvision pyhik==0.1.4 From 6f84fa4ce5969138a2a09f5c7c7fc937d5b20093 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 29 Jan 2018 15:51:43 -0800 Subject: [PATCH 17/17] Bump frontend to 20180130.0 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 70f203d2df2..a601dcbdc51 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -23,7 +23,7 @@ from homeassistant.const import CONF_NAME, EVENT_THEMES_UPDATED from homeassistant.core import callback from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20180126.0', 'user-agents==1.1.0'] +REQUIREMENTS = ['home-assistant-frontend==20180130.0', 'user-agents==1.1.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index 42b7e23aa06..ad6602464a6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -352,7 +352,7 @@ hipnotify==1.0.8 holidays==0.9.3 # homeassistant.components.frontend -home-assistant-frontend==20180126.0 +home-assistant-frontend==20180130.0 # homeassistant.components.camera.onvif http://github.com/tgaugry/suds-passworddigest-py3/archive/86fc50e39b4d2b8997481967d6a7fe1c57118999.zip#suds-passworddigest-py3==0.1.2a diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f21a20f7439..c162dc2fd02 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -75,7 +75,7 @@ hbmqtt==0.9.1 holidays==0.9.3 # homeassistant.components.frontend -home-assistant-frontend==20180126.0 +home-assistant-frontend==20180130.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb