From 033e06868ec9a54619616879a6c50c122cf48aaf Mon Sep 17 00:00:00 2001 From: Jack Fan Date: Thu, 11 Jan 2018 23:14:37 -0500 Subject: [PATCH 01/13] Avoid returning empty media_image_url string (#11557) This relates to issue https://github.com/home-assistant/home-assistant/issues/11556 --- homeassistant/components/media_player/cast.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/media_player/cast.py b/homeassistant/components/media_player/cast.py index 4cf8f72f074..2aaff646885 100644 --- a/homeassistant/components/media_player/cast.py +++ b/homeassistant/components/media_player/cast.py @@ -186,7 +186,7 @@ class CastDevice(MediaPlayerDevice): images = self.media_status.images - return images[0].url if images else None + return images[0].url if images and images[0].url else None @property def media_title(self): From f2cc00cc647f0bc429f17742a29a1de0105deb9f Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Fri, 12 Jan 2018 15:29:58 +0100 Subject: [PATCH 02/13] Core support for hass.io calls & Bugfix check_config (#11571) * Initial overwrites * Add check_config function. * Update hassio.py * Address comments * add hassio support * add more tests * revert core changes * Address check_config * Address comment with api_bool * Bugfix check_config * Update core.py * Update test_core.py * Update config.py * Update hassio.py * Update config.py * Update test_config.py --- homeassistant/components/hassio.py | 86 ++++++++++++++++++++++++++--- homeassistant/components/updater.py | 7 +++ homeassistant/config.py | 18 ++++-- tests/components/test_hassio.py | 58 ++++++++++++++++++- tests/components/test_updater.py | 23 +++++++- tests/test_config.py | 4 +- 6 files changed, 179 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/hassio.py b/homeassistant/components/hassio.py index 8bd1b11cf0d..cc6db5fbab3 100644 --- a/homeassistant/components/hassio.py +++ b/homeassistant/components/hassio.py @@ -17,13 +17,16 @@ from aiohttp.hdrs import CONTENT_TYPE import async_timeout import voluptuous as vol -import homeassistant.helpers.config_validation as cv +from homeassistant.core import callback, DOMAIN as HASS_DOMAIN from homeassistant.const import ( - CONTENT_TYPE_TEXT_PLAIN, SERVER_PORT, CONF_TIME_ZONE) + CONTENT_TYPE_TEXT_PLAIN, SERVER_PORT, CONF_TIME_ZONE, + SERVICE_HOMEASSISTANT_STOP, SERVICE_HOMEASSISTANT_RESTART) +from homeassistant.components import SERVICE_CHECK_CONFIG from homeassistant.components.http import ( HomeAssistantView, KEY_AUTHENTICATED, CONF_API_PASSWORD, CONF_SERVER_PORT, CONF_SERVER_HOST, CONF_SSL_CERTIFICATE) from homeassistant.loader import bind_hass +import homeassistant.helpers.config_validation as cv from homeassistant.util.dt import utcnow _LOGGER = logging.getLogger(__name__) @@ -34,7 +37,7 @@ DEPENDENCIES = ['http'] X_HASSIO = 'X-HASSIO-KEY' DATA_HOMEASSISTANT_VERSION = 'hassio_hass_version' -HASSIO_UPDATE_INTERVAL = timedelta(hours=1) +HASSIO_UPDATE_INTERVAL = timedelta(minutes=55) SERVICE_ADDON_START = 'addon_start' SERVICE_ADDON_STOP = 'addon_stop' @@ -120,12 +123,40 @@ MAP_SERVICE_API = { } +@callback @bind_hass def get_homeassistant_version(hass): - """Return last available HomeAssistant version.""" + """Return latest available HomeAssistant version. + + Async friendly. + """ return hass.data.get(DATA_HOMEASSISTANT_VERSION) +@callback +@bind_hass +def is_hassio(hass): + """Return True if hass.io is loaded. + + Async friendly. + """ + return DOMAIN in hass.config.components + + +@bind_hass +@asyncio.coroutine +def async_check_config(hass): + """Check config over Hass.io API.""" + result = yield from hass.data[DOMAIN].send_command( + '/homeassistant/check', timeout=300) + + if not result: + return "Hass.io config check API error" + elif result['result'] == "error": + return result['message'] + return None + + @asyncio.coroutine def async_setup(hass, config): """Set up the HASSio component.""" @@ -136,7 +167,7 @@ def async_setup(hass, config): return False websession = hass.helpers.aiohttp_client.async_get_clientsession() - hassio = HassIO(hass.loop, websession, host) + hass.data[DOMAIN] = hassio = HassIO(hass.loop, websession, host) if not (yield from hassio.is_connected()): _LOGGER.error("Not connected with HassIO!") @@ -170,11 +201,14 @@ def async_setup(hass, config): payload = data # Call API - yield from hassio.send_command( + ret = yield from hassio.send_command( api_command.format(addon=addon, snapshot=snapshot), payload=payload, timeout=MAP_SERVICE_API[service.service][2] ) + if not ret or ret['result'] != "ok": + _LOGGER.error("Error on Hass.io API: %s", ret['message']) + for service, settings in MAP_SERVICE_API.items(): hass.services.async_register( DOMAIN, service, async_service_handler, schema=settings[1]) @@ -193,9 +227,44 @@ def async_setup(hass, config): # Fetch last version yield from update_homeassistant_version(None) + @asyncio.coroutine + def async_handle_core_service(call): + """Service handler for handling core services.""" + if call.service == SERVICE_HOMEASSISTANT_STOP: + yield from hassio.send_command('/homeassistant/stop') + return + + error = yield from async_check_config(hass) + if error: + _LOGGER.error(error) + hass.components.persistent_notification.async_create( + "Config error. See dev-info panel for details.", + "Config validating", "{0}.check_config".format(HASS_DOMAIN)) + return + + if call.service == SERVICE_HOMEASSISTANT_RESTART: + yield from hassio.send_command('/homeassistant/restart') + + # Mock core services + for service in (SERVICE_HOMEASSISTANT_STOP, SERVICE_HOMEASSISTANT_RESTART, + SERVICE_CHECK_CONFIG): + hass.services.async_register( + HASS_DOMAIN, service, async_handle_core_service) + return True +def _api_bool(funct): + """API wrapper to return Boolean.""" + @asyncio.coroutine + def _wrapper(*argv, **kwargs): + """Wrapper function.""" + data = yield from funct(*argv, **kwargs) + return data and data['result'] == "ok" + + return _wrapper + + class HassIO(object): """Small API wrapper for HassIO.""" @@ -205,6 +274,7 @@ class HassIO(object): self.websession = websession self._ip = ip + @_api_bool def is_connected(self): """Return True if it connected to HassIO supervisor. @@ -219,6 +289,7 @@ class HassIO(object): """ return self.send_command("/homeassistant/info", method="get") + @_api_bool def update_hass_api(self, http_config): """Update Home-Assistant API data on HassIO. @@ -238,6 +309,7 @@ class HassIO(object): return self.send_command("/homeassistant/options", payload=options) + @_api_bool def update_hass_timezone(self, core_config): """Update Home-Assistant timezone data on HassIO. @@ -261,7 +333,7 @@ class HassIO(object): X_HASSIO: os.environ.get('HASSIO_TOKEN') }) - if request.status != 200: + if request.status not in (200, 400): _LOGGER.error( "%s return code %d.", command, request.status) return None diff --git a/homeassistant/components/updater.py b/homeassistant/components/updater.py index f1f5b7dd1fd..f7bf9774e42 100644 --- a/homeassistant/components/updater.py +++ b/homeassistant/components/updater.py @@ -97,9 +97,15 @@ def async_setup(hass, config): newest, releasenotes = result + # Skip on dev if newest is None or 'dev' in current_version: return + # Load data from supervisor on hass.io + if hass.components.hassio.is_hassio(): + newest = hass.components.hassio.get_homeassistant_version() + + # Validate version if StrictVersion(newest) > StrictVersion(current_version): _LOGGER.info("The latest available version is %s", newest) hass.states.async_set( @@ -131,6 +137,7 @@ def get_system_info(hass, include_components): 'timezone': dt_util.DEFAULT_TIME_ZONE.zone, 'version': current_version, 'virtualenv': os.environ.get('VIRTUAL_ENV') is not None, + 'hassio': hass.components.hassio.is_hassio(), } if include_components: diff --git a/homeassistant/config.py b/homeassistant/config.py index fee7572a2c2..3f4c4c174d7 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -33,6 +33,8 @@ from homeassistant.helpers import config_per_platform, extract_domain_configs _LOGGER = logging.getLogger(__name__) DATA_PERSISTENT_ERRORS = 'bootstrap_persistent_errors' +RE_YAML_ERROR = re.compile(r"homeassistant\.util\.yaml") +RE_ASCII = re.compile(r"\033\[[^m]*m") HA_COMPONENT_URL = '[{}](https://home-assistant.io/components/{}/)' YAML_CONFIG_FILE = 'configuration.yaml' VERSION_FILE = '.HA_VERSION' @@ -655,15 +657,19 @@ def async_check_ha_config_file(hass): proc = yield from asyncio.create_subprocess_exec( sys.executable, '-m', 'homeassistant', '--script', 'check_config', '--config', hass.config.config_dir, - stdout=asyncio.subprocess.PIPE, loop=hass.loop) + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.STDOUT, loop=hass.loop) + # Wait for the subprocess exit - stdout_data, dummy = yield from proc.communicate() - result = yield from proc.wait() + log, _ = yield from proc.communicate() + exit_code = yield from proc.wait() - if not result: - return None + # Convert to ASCII + log = RE_ASCII.sub('', log.decode()) - return re.sub(r'\033\[[^m]*m', '', str(stdout_data, 'utf-8')) + if exit_code != 0 or RE_YAML_ERROR.search(log): + return log + return None @callback diff --git a/tests/components/test_hassio.py b/tests/components/test_hassio.py index b6be6f5a6a1..48443658fc4 100644 --- a/tests/components/test_hassio.py +++ b/tests/components/test_hassio.py @@ -7,6 +7,7 @@ import pytest from homeassistant.const import HTTP_HEADER_HA_AUTH from homeassistant.setup import async_setup_component +from homeassistant.components.hassio import async_check_config from tests.common import mock_coro @@ -60,6 +61,8 @@ def test_fail_setup_cannot_connect(hass): result = yield from async_setup_component(hass, 'hassio', {}) assert not result + assert not hass.components.hassio.is_hassio() + @asyncio.coroutine def test_setup_api_ping(hass, aioclient_mock): @@ -75,7 +78,8 @@ def test_setup_api_ping(hass, aioclient_mock): assert result assert aioclient_mock.call_count == 2 - assert hass.data['hassio_hass_version'] == "10.0" + assert hass.components.hassio.get_homeassistant_version() == "10.0" + assert hass.components.hassio.is_hassio() @asyncio.coroutine @@ -215,6 +219,7 @@ def test_service_register(hassio_env, hass): assert hass.services.has_service('hassio', 'addon_stdin') assert hass.services.has_service('hassio', 'host_shutdown') assert hass.services.has_service('hassio', 'host_reboot') + assert hass.services.has_service('hassio', 'host_reboot') assert hass.services.has_service('hassio', 'snapshot_full') assert hass.services.has_service('hassio', 'snapshot_partial') assert hass.services.has_service('hassio', 'restore_full') @@ -294,6 +299,57 @@ def test_service_calls(hassio_env, hass, aioclient_mock): 'addons': ['test'], 'folders': ['ssl'], 'homeassistant': False} +@asyncio.coroutine +def test_service_calls_core(hassio_env, hass, aioclient_mock): + """Call core service and check the API calls behind that.""" + assert (yield from async_setup_component(hass, 'hassio', {})) + + aioclient_mock.post( + "http://127.0.0.1/homeassistant/restart", json={'result': 'ok'}) + aioclient_mock.post( + "http://127.0.0.1/homeassistant/stop", json={'result': 'ok'}) + aioclient_mock.post( + "http://127.0.0.1/homeassistant/check", json={'result': 'ok'}) + + yield from hass.services.async_call('homeassistant', 'stop') + yield from hass.async_block_till_done() + + assert aioclient_mock.call_count == 1 + + yield from hass.services.async_call('homeassistant', 'check_config') + yield from hass.async_block_till_done() + + assert aioclient_mock.call_count == 2 + + yield from hass.services.async_call('homeassistant', 'restart') + yield from hass.async_block_till_done() + + assert aioclient_mock.call_count == 4 + + +@asyncio.coroutine +def test_check_config_ok(hassio_env, hass, aioclient_mock): + """Check Config that is okay.""" + assert (yield from async_setup_component(hass, 'hassio', {})) + + aioclient_mock.post( + "http://127.0.0.1/homeassistant/check", json={'result': 'ok'}) + + assert (yield from async_check_config(hass)) is None + + +@asyncio.coroutine +def test_check_config_fail(hassio_env, hass, aioclient_mock): + """Check Config that is wrong.""" + assert (yield from async_setup_component(hass, 'hassio', {})) + + aioclient_mock.post( + "http://127.0.0.1/homeassistant/check", json={ + 'result': 'error', 'message': "Error"}) + + assert (yield from async_check_config(hass)) == "Error" + + @asyncio.coroutine def test_forward_request(hassio_client): """Test fetching normal path.""" diff --git a/tests/components/test_updater.py b/tests/components/test_updater.py index 6d68add93a5..28ffcac2b13 100644 --- a/tests/components/test_updater.py +++ b/tests/components/test_updater.py @@ -8,7 +8,7 @@ import pytest from homeassistant.setup import async_setup_component from homeassistant.components import updater import homeassistant.util.dt as dt_util -from tests.common import async_fire_time_changed, mock_coro +from tests.common import async_fire_time_changed, mock_coro, mock_component NEW_VERSION = '10000.0' MOCK_VERSION = '10.0' @@ -174,3 +174,24 @@ def test_error_fetching_new_version_invalid_response(hass, aioclient_mock): Mock(return_value=mock_coro({'fake': 'bla'}))): res = yield from updater.get_newest_version(hass, MOCK_HUUID, False) assert res is None + + +@asyncio.coroutine +def test_new_version_shows_entity_after_hour_hassio( + hass, mock_get_uuid, mock_get_newest_version): + """Test if new entity is created if new version is available / hass.io.""" + mock_get_uuid.return_value = MOCK_HUUID + mock_get_newest_version.return_value = mock_coro((NEW_VERSION, '')) + mock_component(hass, 'hassio') + hass.data['hassio_hass_version'] = "999.0" + + res = yield from async_setup_component( + hass, updater.DOMAIN, {updater.DOMAIN: {}}) + assert res, 'Updater failed to setup' + + with patch('homeassistant.components.updater.current_version', + MOCK_VERSION): + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(hours=1)) + yield from hass.async_block_till_done() + + assert hass.states.is_state(updater.ENTITY_ID, "999.0") diff --git a/tests/test_config.py b/tests/test_config.py index 2c8edc32f82..377c650e91f 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -531,7 +531,7 @@ class TestConfig(unittest.TestCase): """Check that restart propagates to stop.""" process_mock = mock.MagicMock() attrs = { - 'communicate.return_value': mock_coro(('output', 'error')), + 'communicate.return_value': mock_coro((b'output', None)), 'wait.return_value': mock_coro(0)} process_mock.configure_mock(**attrs) mock_create.return_value = mock_coro(process_mock) @@ -546,7 +546,7 @@ class TestConfig(unittest.TestCase): process_mock = mock.MagicMock() attrs = { 'communicate.return_value': - mock_coro(('\033[34mhello'.encode('utf-8'), 'error')), + mock_coro(('\033[34mhello'.encode('utf-8'), None)), 'wait.return_value': mock_coro(1)} process_mock.configure_mock(**attrs) mock_create.return_value = mock_coro(process_mock) From 1802c0a922670084d398de05fa9461b0c4c7309f Mon Sep 17 00:00:00 2001 From: Thijs de Jong Date: Fri, 12 Jan 2018 16:04:44 +0100 Subject: [PATCH 03/13] Fix Tahoma stop command for 2 types of shutters (#11588) * add working stop command This fixes the stop command for 2 types of roller shutters * fix line too long * fix indentation * fix indentation --- homeassistant/components/cover/tahoma.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/cover/tahoma.py b/homeassistant/components/cover/tahoma.py index d492ad50866..7ec09c781d2 100644 --- a/homeassistant/components/cover/tahoma.py +++ b/homeassistant/components/cover/tahoma.py @@ -15,6 +15,11 @@ DEPENDENCIES = ['tahoma'] _LOGGER = logging.getLogger(__name__) +TAHOMA_STOP_COMMAND = { + 'io:RollerShutterWithLowSpeedManagementIOComponent': 'my', + 'io:RollerShutterVeluxIOComponent': 'my', +} + SCAN_INTERVAL = timedelta(seconds=60) @@ -73,7 +78,8 @@ class TahomaCover(TahomaDevice, CoverDevice): def stop_cover(self, **kwargs): """Stop the cover.""" - self.apply_action('stopIdentify') + self.apply_action(TAHOMA_STOP_COMMAND.get(self.tahoma_device.type, + 'stopIdentify')) def device_class(self): """Return the class of this device, from component DEVICE_CLASSES.""" From 92bec562aba89ac5388561c3e2f2c0be94a10c6a Mon Sep 17 00:00:00 2001 From: tschmidty69 Date: Fri, 12 Jan 2018 14:06:42 -0500 Subject: [PATCH 04/13] Pushbullet email support (fix) (#11590) * Simplified push calls * Cleaned up and added unittests * Fixed email parameter * Fixed email parameter --- homeassistant/components/notify/pushbullet.py | 29 ++++++------- tests/components/notify/test_pushbullet.py | 42 +++++++++++++++++++ 2 files changed, 57 insertions(+), 14 deletions(-) create mode 100644 tests/components/notify/test_pushbullet.py diff --git a/homeassistant/components/notify/pushbullet.py b/homeassistant/components/notify/pushbullet.py index 0e846ebaf84..359810bb6bc 100644 --- a/homeassistant/components/notify/pushbullet.py +++ b/homeassistant/components/notify/pushbullet.py @@ -22,6 +22,7 @@ _LOGGER = logging.getLogger(__name__) ATTR_URL = 'url' ATTR_FILE = 'file' ATTR_FILE_URL = 'file_url' +ATTR_LIST = 'list' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_API_KEY): cv.string, @@ -99,7 +100,7 @@ class PushBulletNotificationService(BaseNotificationService): continue # Target is email, send directly, don't use a target object. - # This also seems works to send to all devices in own account. + # This also seems to work to send to all devices in own account. if ttype == 'email': self._push_data(message, title, data, self.pushbullet, tname) _LOGGER.info("Sent notification to email %s", tname) @@ -127,20 +128,18 @@ class PushBulletNotificationService(BaseNotificationService): _LOGGER.error("No such target: %s/%s", ttype, tname) continue - def _push_data(self, message, title, data, pusher, tname=None): + def _push_data(self, message, title, data, pusher, email=None): """Helper for creating the message content.""" from pushbullet import PushError if data is None: data = {} + data_list = data.get(ATTR_LIST) url = data.get(ATTR_URL) filepath = data.get(ATTR_FILE) file_url = data.get(ATTR_FILE_URL) try: if url: - if tname: - pusher.push_link(title, url, body=message, email=tname) - else: - pusher.push_link(title, url, body=message) + pusher.push_link(title, url, body=message, email=email) elif filepath: if not self.hass.config.is_allowed_path(filepath): _LOGGER.error("Filepath is not valid or allowed") @@ -150,18 +149,20 @@ class PushBulletNotificationService(BaseNotificationService): if filedata.get('file_type') == 'application/x-empty': _LOGGER.error("Can not send an empty file") return - pusher.push_file(title=title, body=message, **filedata) + + pusher.push_file(title=title, body=message, + email=email, **filedata) elif file_url: if not file_url.startswith('http'): _LOGGER.error("URL should start with http or https") return - pusher.push_file(title=title, body=message, file_name=file_url, - file_url=file_url, - file_type=mimetypes.guess_type(file_url)[0]) + pusher.push_file(title=title, body=message, email=email, + file_name=file_url, file_url=file_url, + file_type=(mimetypes + .guess_type(file_url)[0])) + elif data_list: + pusher.push_note(title, data_list, email=email) else: - if tname: - pusher.push_note(title, message, email=tname) - else: - pusher.push_note(title, message) + pusher.push_note(title, message, email=email) except PushError as err: _LOGGER.error("Notify failed: %s", err) diff --git a/tests/components/notify/test_pushbullet.py b/tests/components/notify/test_pushbullet.py new file mode 100644 index 00000000000..ba3046e8fd7 --- /dev/null +++ b/tests/components/notify/test_pushbullet.py @@ -0,0 +1,42 @@ +"""The tests for the pushbullet notification platform.""" + +import unittest + +from homeassistant.setup import setup_component +import homeassistant.components.notify as notify +from tests.common import assert_setup_component, get_test_home_assistant + + +class TestPushbullet(unittest.TestCase): + """Test the pushbullet notifications.""" + + def setUp(self): # pylint: disable=invalid-name + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() + + def tearDown(self): # pylint: disable=invalid-name + """Stop down everything that was started.""" + self.hass.stop() + + def test_setup(self): + """Test setup.""" + with assert_setup_component(1) as handle_config: + assert setup_component(self.hass, 'notify', { + 'notify': { + 'name': 'test', + 'platform': 'pushbullet', + 'api_key': 'MYFAKEKEY', } + }) + assert handle_config[notify.DOMAIN] + + def test_bad_config(self): + """Test set up the platform with bad/missing configuration.""" + config = { + notify.DOMAIN: { + 'name': 'test', + 'platform': 'pushbullet', + } + } + with assert_setup_component(0) as handle_config: + assert setup_component(self.hass, notify.DOMAIN, config) + assert not handle_config[notify.DOMAIN] From b8dfa4c3d27ca7e5ccb9a04eab18b40b869c780b Mon Sep 17 00:00:00 2001 From: Adam Mills Date: Fri, 12 Jan 2018 01:06:09 -0500 Subject: [PATCH 05/13] Fix state for trigger with forced updates (#11595) --- homeassistant/components/automation/state.py | 2 +- tests/components/automation/test_state.py | 34 ++++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/automation/state.py b/homeassistant/components/automation/state.py index e4d096d35fd..9243f960850 100644 --- a/homeassistant/components/automation/state.py +++ b/homeassistant/components/automation/state.py @@ -55,7 +55,7 @@ def async_trigger(hass, config, action): # Ignore changes to state attributes if from/to is in use if (not match_all and from_s is not None and to_s is not None and - from_s.last_changed == to_s.last_changed): + from_s.state == to_s.state): return if not time_delta: diff --git a/tests/components/automation/test_state.py b/tests/components/automation/test_state.py index b1ee0841e2d..bf54d24492a 100644 --- a/tests/components/automation/test_state.py +++ b/tests/components/automation/test_state.py @@ -409,6 +409,40 @@ class TestAutomationState(unittest.TestCase): self.hass.block_till_done() self.assertEqual(1, len(self.calls)) + def test_if_fires_on_entity_change_with_for_multiple_force_update(self): + """Test for firing on entity change with for and force update.""" + assert setup_component(self.hass, automation.DOMAIN, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'state', + 'entity_id': 'test.force_entity', + 'to': 'world', + 'for': { + 'seconds': 5 + }, + }, + 'action': { + 'service': 'test.automation' + } + } + }) + + utcnow = dt_util.utcnow() + with patch('homeassistant.core.dt_util.utcnow') as mock_utcnow: + mock_utcnow.return_value = utcnow + self.hass.states.set('test.force_entity', 'world', None, True) + self.hass.block_till_done() + for _ in range(0, 4): + mock_utcnow.return_value += timedelta(seconds=1) + fire_time_changed(self.hass, mock_utcnow.return_value) + self.hass.states.set('test.force_entity', 'world', None, True) + self.hass.block_till_done() + self.assertEqual(0, len(self.calls)) + mock_utcnow.return_value += timedelta(seconds=4) + fire_time_changed(self.hass, mock_utcnow.return_value) + self.hass.block_till_done() + self.assertEqual(1, len(self.calls)) + def test_if_fires_on_entity_change_with_for(self): """Test for firing on entity change with for.""" assert setup_component(self.hass, automation.DOMAIN, { From 179d99381d334a77ca52f00b5a1794c6d68c1533 Mon Sep 17 00:00:00 2001 From: tschmidty69 Date: Fri, 12 Jan 2018 13:19:43 -0500 Subject: [PATCH 06/13] Snips add say and say_actions services (new) (#11596) * Added snips.say and snips.say_action services * Added snips.say and snips.say_action services * Merged services.yaml changes I missed * added tests for new service configs * Woof * Woof Woof * Changed attribute names to follow hass standards. * updated test_snips with new attribute names --- homeassistant/components/services.yaml | 32 ++++++++++++ homeassistant/components/snips.py | 59 ++++++++++++++++++++++ tests/components/test_snips.py | 68 +++++++++++++++++++++++++- 3 files changed, 158 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/services.yaml b/homeassistant/components/services.yaml index 03c1d24184a..522939a213a 100644 --- a/homeassistant/components/services.yaml +++ b/homeassistant/components/services.yaml @@ -364,6 +364,38 @@ abode: description: Entity id of the quick action to trigger. example: 'binary_sensor.home_quick_action' +snips: + say: + description: Send a TTS message to Snips. + fields: + text: + description: Text to say. + example: My name is snips + site_id: + description: Site to use to start session, defaults to default (optional) + example: bedroom + custom_data: + description: custom data that will be included with all messages in this session + example: user=UserName + say_action: + description: Send a TTS message to Snips to listen for a response. + fields: + text: + description: Text to say + example: My name is snips + site_id: + description: Site to use to start session, defaults to default (optional) + example: bedroom + custom_data: + description: custom data that will be included with all messages in this session + example: user=UserName + can_be_enqueued: + description: If True, session waits for an open session to end, if False session is dropped if one is running + example: True + intent_filter: + description: Optional Array of Strings - A list of intents names to restrict the NLU resolution to on the first query. + example: turnOnLights, turnOffLights + input_boolean: toggle: description: Toggles an input boolean. diff --git a/homeassistant/components/snips.py b/homeassistant/components/snips.py index ae387f7ab4c..d221c8512c6 100644 --- a/homeassistant/components/snips.py +++ b/homeassistant/components/snips.py @@ -8,17 +8,29 @@ import asyncio import json import logging from datetime import timedelta + import voluptuous as vol + from homeassistant.helpers import intent, config_validation as cv import homeassistant.components.mqtt as mqtt DOMAIN = 'snips' DEPENDENCIES = ['mqtt'] + CONF_INTENTS = 'intents' CONF_ACTION = 'action' +SERVICE_SAY = 'say' +SERVICE_SAY_ACTION = 'say_action' + INTENT_TOPIC = 'hermes/intent/#' +ATTR_TEXT = 'text' +ATTR_SITE_ID = 'site_id' +ATTR_CUSTOM_DATA = 'custom_data' +ATTR_CAN_BE_ENQUEUED = 'can_be_enqueued' +ATTR_INTENT_FILTER = 'intent_filter' + _LOGGER = logging.getLogger(__name__) CONFIG_SCHEMA = vol.Schema({ @@ -40,6 +52,20 @@ INTENT_SCHEMA = vol.Schema({ }] }, extra=vol.ALLOW_EXTRA) +SERVICE_SCHEMA_SAY = vol.Schema({ + vol.Required(ATTR_TEXT): str, + vol.Optional(ATTR_SITE_ID, default='default'): str, + vol.Optional(ATTR_CUSTOM_DATA, default=''): str +}) + +SERVICE_SCHEMA_SAY_ACTION = vol.Schema({ + vol.Required(ATTR_TEXT): str, + vol.Optional(ATTR_SITE_ID, default='default'): str, + vol.Optional(ATTR_CUSTOM_DATA, default=''): str, + vol.Optional(ATTR_CAN_BE_ENQUEUED, default=True): cv.boolean, + vol.Optional(ATTR_INTENT_FILTER): vol.All(cv.ensure_list), +}) + @asyncio.coroutine def async_setup(hass, config): @@ -93,6 +119,39 @@ def async_setup(hass, config): yield from hass.components.mqtt.async_subscribe( INTENT_TOPIC, message_received) + @asyncio.coroutine + def snips_say(call): + """Send a Snips notification message.""" + notification = {'siteId': call.data.get(ATTR_SITE_ID, 'default'), + 'customData': call.data.get(ATTR_CUSTOM_DATA, ''), + 'init': {'type': 'notification', + 'text': call.data.get(ATTR_TEXT)}} + mqtt.async_publish(hass, 'hermes/dialogueManager/startSession', + json.dumps(notification)) + return + + @asyncio.coroutine + def snips_say_action(call): + """Send a Snips action message.""" + notification = {'siteId': call.data.get(ATTR_SITE_ID, 'default'), + 'customData': call.data.get(ATTR_CUSTOM_DATA, ''), + 'init': {'type': 'action', + 'text': call.data.get(ATTR_TEXT), + 'canBeEnqueued': call.data.get( + ATTR_CAN_BE_ENQUEUED, True), + 'intentFilter': + call.data.get(ATTR_INTENT_FILTER, [])}} + mqtt.async_publish(hass, 'hermes/dialogueManager/startSession', + json.dumps(notification)) + return + + hass.services.async_register( + DOMAIN, SERVICE_SAY, snips_say, + schema=SERVICE_SCHEMA_SAY) + hass.services.async_register( + DOMAIN, SERVICE_SAY_ACTION, snips_say_action, + schema=SERVICE_SCHEMA_SAY_ACTION) + return True diff --git a/tests/components/test_snips.py b/tests/components/test_snips.py index 9ee500bb4c7..711d13dc341 100644 --- a/tests/components/test_snips.py +++ b/tests/components/test_snips.py @@ -4,7 +4,10 @@ import json from homeassistant.core import callback from homeassistant.bootstrap import async_setup_component -from tests.common import async_fire_mqtt_message, async_mock_intent +from tests.common import (async_fire_mqtt_message, async_mock_intent, + async_mock_service) +from homeassistant.components.snips import (SERVICE_SCHEMA_SAY, + SERVICE_SCHEMA_SAY_ACTION) @asyncio.coroutine @@ -238,3 +241,66 @@ def test_snips_intent_username(hass, mqtt_mock): intent = intents[0] assert intent.platform == 'snips' assert intent.intent_type == 'Lights' + + +@asyncio.coroutine +def test_snips_say(hass, caplog): + """Test snips say with invalid config.""" + calls = async_mock_service(hass, 'snips', 'say', + SERVICE_SCHEMA_SAY) + + data = {'text': 'Hello'} + yield from hass.services.async_call('snips', 'say', data) + yield from hass.async_block_till_done() + + assert len(calls) == 1 + assert calls[0].domain == 'snips' + assert calls[0].service == 'say' + assert calls[0].data['text'] == 'Hello' + + +@asyncio.coroutine +def test_snips_say_action(hass, caplog): + """Test snips say_action with invalid config.""" + calls = async_mock_service(hass, 'snips', 'say_action', + SERVICE_SCHEMA_SAY_ACTION) + + data = {'text': 'Hello', 'intent_filter': ['myIntent']} + yield from hass.services.async_call('snips', 'say_action', data) + yield from hass.async_block_till_done() + + assert len(calls) == 1 + assert calls[0].domain == 'snips' + assert calls[0].service == 'say_action' + assert calls[0].data['text'] == 'Hello' + assert calls[0].data['intent_filter'] == ['myIntent'] + + +@asyncio.coroutine +def test_snips_say_invalid_config(hass, caplog): + """Test snips say with invalid config.""" + calls = async_mock_service(hass, 'snips', 'say', + SERVICE_SCHEMA_SAY) + + data = {'text': 'Hello', 'badKey': 'boo'} + yield from hass.services.async_call('snips', 'say', data) + yield from hass.async_block_till_done() + + assert len(calls) == 0 + assert 'ERROR' in caplog.text + assert 'Invalid service data' in caplog.text + + +@asyncio.coroutine +def test_snips_say_action_invalid_config(hass, caplog): + """Test snips say_action with invalid config.""" + calls = async_mock_service(hass, 'snips', 'say_action', + SERVICE_SCHEMA_SAY_ACTION) + + data = {'text': 'Hello', 'can_be_enqueued': 'notabool'} + yield from hass.services.async_call('snips', 'say_action', data) + yield from hass.async_block_till_done() + + assert len(calls) == 0 + assert 'ERROR' in caplog.text + assert 'Invalid service data' in caplog.text From 8d19192c9ca0dc8b17fea842454c262a104c3694 Mon Sep 17 00:00:00 2001 From: Bob Anderson Date: Thu, 11 Jan 2018 23:45:01 -0800 Subject: [PATCH 07/13] Concord232 alarm arm away fix (#11597) * fix arming away cmd for concord232 client * bump required version of concord232 to 0.15 --- homeassistant/components/alarm_control_panel/concord232.py | 4 ++-- homeassistant/components/binary_sensor/concord232.py | 2 +- requirements_all.txt | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/concord232.py b/homeassistant/components/alarm_control_panel/concord232.py index 291d4bc80b5..af91bc78e67 100644 --- a/homeassistant/components/alarm_control_panel/concord232.py +++ b/homeassistant/components/alarm_control_panel/concord232.py @@ -18,7 +18,7 @@ from homeassistant.const import ( STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, STATE_UNKNOWN) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['concord232==0.14'] +REQUIREMENTS = ['concord232==0.15'] _LOGGER = logging.getLogger(__name__) @@ -121,4 +121,4 @@ class Concord232Alarm(alarm.AlarmControlPanel): def alarm_arm_away(self, code=None): """Send arm away command.""" - self._alarm.arm('auto') + self._alarm.arm('away') diff --git a/homeassistant/components/binary_sensor/concord232.py b/homeassistant/components/binary_sensor/concord232.py index 73cf77f2b93..c8442491b29 100644 --- a/homeassistant/components/binary_sensor/concord232.py +++ b/homeassistant/components/binary_sensor/concord232.py @@ -15,7 +15,7 @@ from homeassistant.components.binary_sensor import ( from homeassistant.const import (CONF_HOST, CONF_PORT) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['concord232==0.14'] +REQUIREMENTS = ['concord232==0.15'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 813e4e4dcf1..31da31d56a9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -177,7 +177,7 @@ colorlog==3.0.1 # homeassistant.components.alarm_control_panel.concord232 # homeassistant.components.binary_sensor.concord232 -concord232==0.14 +concord232==0.15 # homeassistant.scripts.credstash # credstash==1.14.0 From 218e97d965964613351903ad0a9efc6601792ea7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Fri, 12 Jan 2018 20:52:53 +0100 Subject: [PATCH 08/13] Bugfix and cleanup for Rfxtrx (#11600) * rfxtrx clean up * rfxtrx clean up * rfxtrx clean up --- .../components/binary_sensor/rfxtrx.py | 76 +++++---- homeassistant/components/cover/rfxtrx.py | 21 ++- homeassistant/components/light/rfxtrx.py | 23 ++- homeassistant/components/rfxtrx.py | 156 +++++------------- homeassistant/components/sensor/rfxtrx.py | 25 ++- homeassistant/components/switch/rfxtrx.py | 21 ++- requirements_all.txt | 2 +- 7 files changed, 156 insertions(+), 168 deletions(-) diff --git a/homeassistant/components/binary_sensor/rfxtrx.py b/homeassistant/components/binary_sensor/rfxtrx.py index edaee574232..4073cb9eac1 100644 --- a/homeassistant/components/binary_sensor/rfxtrx.py +++ b/homeassistant/components/binary_sensor/rfxtrx.py @@ -7,30 +7,40 @@ tested. Other types may need some work. """ import logging + import voluptuous as vol + +from homeassistant.const import ( + CONF_DEVICE_CLASS, CONF_COMMAND_ON, CONF_COMMAND_OFF, CONF_NAME) from homeassistant.components import rfxtrx +from homeassistant.helpers import event as evt +from homeassistant.helpers import config_validation as cv +from homeassistant.components.binary_sensor import ( + BinarySensorDevice, PLATFORM_SCHEMA) +from homeassistant.components.rfxtrx import ( + ATTR_NAME, ATTR_DATA_BITS, ATTR_OFF_DELAY, ATTR_FIRE_EVENT, + CONF_AUTOMATIC_ADD, CONF_FIRE_EVENT, + CONF_DATA_BITS, CONF_DEVICES) from homeassistant.util import slugify from homeassistant.util import dt as dt_util -from homeassistant.helpers import config_validation as cv -from homeassistant.helpers import event as evt -from homeassistant.components.binary_sensor import BinarySensorDevice -from homeassistant.components.rfxtrx import ( - ATTR_AUTOMATIC_ADD, ATTR_NAME, ATTR_OFF_DELAY, ATTR_FIREEVENT, - ATTR_DATA_BITS, CONF_DEVICES -) -from homeassistant.const import ( - CONF_DEVICE_CLASS, CONF_COMMAND_ON, CONF_COMMAND_OFF -) + DEPENDENCIES = ["rfxtrx"] _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = vol.Schema({ - vol.Required("platform"): rfxtrx.DOMAIN, - vol.Optional(CONF_DEVICES, default={}): vol.All( - dict, rfxtrx.valid_binary_sensor), - vol.Optional(ATTR_AUTOMATIC_ADD, default=False): cv.boolean, +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_DEVICES, default={}): { + cv.string: vol.Schema({ + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_DEVICE_CLASS): cv.string, + vol.Optional(CONF_FIRE_EVENT, default=False): cv.boolean, + vol.Optional(CONF_DATA_BITS): cv.positive_int, + vol.Optional(CONF_COMMAND_ON): cv.byte, + vol.Optional(CONF_COMMAND_OFF): cv.byte + }) + }, + vol.Optional(CONF_AUTOMATIC_ADD, default=False): cv.boolean, }, extra=vol.ALLOW_EXTRA) @@ -46,17 +56,17 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): if device_id in rfxtrx.RFX_DEVICES: continue - if entity[ATTR_DATA_BITS] is not None: - _LOGGER.info("Masked device id: %s", - rfxtrx.get_pt2262_deviceid(device_id, - entity[ATTR_DATA_BITS])) + if entity[CONF_DATA_BITS] is not None: + _LOGGER.debug("Masked device id: %s", + rfxtrx.get_pt2262_deviceid(device_id, + entity[ATTR_DATA_BITS])) - _LOGGER.info("Add %s rfxtrx.binary_sensor (class %s)", - entity[ATTR_NAME], entity[CONF_DEVICE_CLASS]) + _LOGGER.debug("Add %s rfxtrx.binary_sensor (class %s)", + entity[ATTR_NAME], entity[CONF_DEVICE_CLASS]) device = RfxtrxBinarySensor(event, entity[ATTR_NAME], entity[CONF_DEVICE_CLASS], - entity[ATTR_FIREEVENT], + entity[ATTR_FIRE_EVENT], entity[ATTR_OFF_DELAY], entity[ATTR_DATA_BITS], entity[CONF_COMMAND_ON], @@ -82,15 +92,15 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): if sensor is None: # Add the entity if not exists and automatic_add is True - if not config[ATTR_AUTOMATIC_ADD]: + if not config[CONF_AUTOMATIC_ADD]: return if event.device.packettype == 0x13: poss_dev = rfxtrx.find_possible_pt2262_device(device_id) if poss_dev is not None: poss_id = slugify(poss_dev.event.device.id_string.lower()) - _LOGGER.info("Found possible matching deviceid %s.", - poss_id) + _LOGGER.debug("Found possible matching deviceid %s.", + poss_id) pkt_id = "".join("{0:02x}".format(x) for x in event.data) sensor = RfxtrxBinarySensor(event, pkt_id) @@ -107,11 +117,11 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): elif not isinstance(sensor, RfxtrxBinarySensor): return else: - _LOGGER.info("Binary sensor update " - "(Device_id: %s Class: %s Sub: %s)", - slugify(event.device.id_string.lower()), - event.device.__class__.__name__, - event.device.subtype) + _LOGGER.debug("Binary sensor update " + "(Device_id: %s Class: %s Sub: %s)", + slugify(event.device.id_string.lower()), + event.device.__class__.__name__, + event.device.subtype) if sensor.is_lighting4: if sensor.data_bits is not None: @@ -163,10 +173,8 @@ class RfxtrxBinarySensor(BinarySensorDevice): self._masked_id = rfxtrx.get_pt2262_deviceid( event.device.id_string.lower(), data_bits) - - def __str__(self): - """Return the name of the sensor.""" - return self._name + else: + self._masked_id = None @property def name(self): diff --git a/homeassistant/components/cover/rfxtrx.py b/homeassistant/components/cover/rfxtrx.py index 0e28d3ef701..66f2fde52f4 100644 --- a/homeassistant/components/cover/rfxtrx.py +++ b/homeassistant/components/cover/rfxtrx.py @@ -4,12 +4,29 @@ Support for RFXtrx cover components. For more details about this platform, please refer to the documentation https://home-assistant.io/components/cover.rfxtrx/ """ +import voluptuous as vol + import homeassistant.components.rfxtrx as rfxtrx -from homeassistant.components.cover import CoverDevice +from homeassistant.components.cover import CoverDevice, PLATFORM_SCHEMA +from homeassistant.const import CONF_NAME +from homeassistant.components.rfxtrx import ( + CONF_AUTOMATIC_ADD, CONF_FIRE_EVENT, DEFAULT_SIGNAL_REPETITIONS, + CONF_SIGNAL_REPETITIONS, CONF_DEVICES) +from homeassistant.helpers import config_validation as cv DEPENDENCIES = ['rfxtrx'] -PLATFORM_SCHEMA = rfxtrx.DEFAULT_SCHEMA +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_DEVICES, default={}): { + cv.string: vol.Schema({ + vol.Required(CONF_NAME): cv.string, + vol.Optional(CONF_FIRE_EVENT, default=False): cv.boolean + }) + }, + vol.Optional(CONF_AUTOMATIC_ADD, default=False): cv.boolean, + vol.Optional(CONF_SIGNAL_REPETITIONS, default=DEFAULT_SIGNAL_REPETITIONS): + vol.Coerce(int), +}) def setup_platform(hass, config, add_devices_callback, discovery_info=None): diff --git a/homeassistant/components/light/rfxtrx.py b/homeassistant/components/light/rfxtrx.py index 9248b0131f1..cdfe2fe5671 100644 --- a/homeassistant/components/light/rfxtrx.py +++ b/homeassistant/components/light/rfxtrx.py @@ -6,15 +6,32 @@ https://home-assistant.io/components/light.rfxtrx/ """ import logging +import voluptuous as vol + import homeassistant.components.rfxtrx as rfxtrx -from homeassistant.components.light import (ATTR_BRIGHTNESS, - SUPPORT_BRIGHTNESS, Light) +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, Light, PLATFORM_SCHEMA) +from homeassistant.const import CONF_NAME +from homeassistant.components.rfxtrx import ( + CONF_AUTOMATIC_ADD, CONF_FIRE_EVENT, DEFAULT_SIGNAL_REPETITIONS, + CONF_SIGNAL_REPETITIONS, CONF_DEVICES) +from homeassistant.helpers import config_validation as cv DEPENDENCIES = ['rfxtrx'] _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = rfxtrx.DEFAULT_SCHEMA +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_DEVICES, default={}): { + cv.string: vol.Schema({ + vol.Required(CONF_NAME): cv.string, + vol.Optional(CONF_FIRE_EVENT, default=False): cv.boolean + }) + }, + vol.Optional(CONF_AUTOMATIC_ADD, default=False): cv.boolean, + vol.Optional(CONF_SIGNAL_REPETITIONS, default=DEFAULT_SIGNAL_REPETITIONS): + vol.Coerce(int), +}) SUPPORT_RFXTRX = SUPPORT_BRIGHTNESS diff --git a/homeassistant/components/rfxtrx.py b/homeassistant/components/rfxtrx.py index 8b730bf97f2..f28a9aafb19 100644 --- a/homeassistant/components/rfxtrx.py +++ b/homeassistant/components/rfxtrx.py @@ -16,11 +16,11 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, ATTR_ENTITY_ID, TEMP_CELSIUS, - CONF_DEVICE_CLASS, CONF_COMMAND_ON, CONF_COMMAND_OFF + CONF_DEVICES ) from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['pyRFXtrx==0.20.1'] +REQUIREMENTS = ['pyRFXtrx==0.21.1'] DOMAIN = 'rfxtrx' @@ -31,13 +31,19 @@ ATTR_DEVICE = 'device' ATTR_DEBUG = 'debug' ATTR_STATE = 'state' ATTR_NAME = 'name' -ATTR_FIREEVENT = 'fire_event' +ATTR_FIRE_EVENT = 'fire_event' ATTR_DATA_TYPE = 'data_type' ATTR_DATA_BITS = 'data_bits' ATTR_DUMMY = 'dummy' ATTR_OFF_DELAY = 'off_delay' +CONF_AUTOMATIC_ADD = 'automatic_add' +CONF_DATA_TYPE = 'data_type' CONF_SIGNAL_REPETITIONS = 'signal_repetitions' -CONF_DEVICES = 'devices' +CONF_FIRE_EVENT = 'fire_event' +CONF_DATA_BITS = 'data_bits' +CONF_DUMMY = 'dummy' +CONF_DEVICE = 'device' +CONF_DEBUG = 'debug' EVENT_BUTTON_PRESSED = 'button_pressed' DATA_TYPES = OrderedDict([ @@ -57,93 +63,13 @@ DATA_TYPES = OrderedDict([ RECEIVED_EVT_SUBSCRIBERS = [] RFX_DEVICES = {} _LOGGER = logging.getLogger(__name__) -RFXOBJECT = 'rfxobject' - - -def _valid_device(value, device_type): - """Validate a dictionary of devices definitions.""" - config = OrderedDict() - for key, device in value.items(): - - # Still accept old configuration - if 'packetid' in device.keys(): - msg = 'You are using an outdated configuration of the rfxtrx ' +\ - 'device, {}.'.format(key) +\ - ' Your new config should be:\n {}: \n name: {}'\ - .format(device.get('packetid'), - device.get(ATTR_NAME, 'deivce_name')) - _LOGGER.warning(msg) - key = device.get('packetid') - device.pop('packetid') - - key = str(key) - if not len(key) % 2 == 0: - key = '0' + key - - if device_type == 'sensor': - config[key] = DEVICE_SCHEMA_SENSOR(device) - elif device_type == 'binary_sensor': - config[key] = DEVICE_SCHEMA_BINARYSENSOR(device) - elif device_type == 'light_switch': - config[key] = DEVICE_SCHEMA(device) - else: - raise vol.Invalid('Rfxtrx device is invalid') - - if not config[key][ATTR_NAME]: - config[key][ATTR_NAME] = key - return config - - -def valid_sensor(value): - """Validate sensor configuration.""" - return _valid_device(value, "sensor") - - -def valid_binary_sensor(value): - """Validate binary sensor configuration.""" - return _valid_device(value, "binary_sensor") - - -def _valid_light_switch(value): - return _valid_device(value, "light_switch") - - -DEVICE_SCHEMA = vol.Schema({ - vol.Required(ATTR_NAME): cv.string, - vol.Optional(ATTR_FIREEVENT, default=False): cv.boolean, -}) - -DEVICE_SCHEMA_SENSOR = vol.Schema({ - vol.Optional(ATTR_NAME, default=None): cv.string, - vol.Optional(ATTR_FIREEVENT, default=False): cv.boolean, - vol.Optional(ATTR_DATA_TYPE, default=[]): - vol.All(cv.ensure_list, [vol.In(DATA_TYPES.keys())]), -}) - -DEVICE_SCHEMA_BINARYSENSOR = vol.Schema({ - vol.Optional(ATTR_NAME, default=None): cv.string, - vol.Optional(CONF_DEVICE_CLASS, default=None): cv.string, - vol.Optional(ATTR_FIREEVENT, default=False): cv.boolean, - vol.Optional(ATTR_OFF_DELAY, default=None): - vol.Any(cv.time_period, cv.positive_timedelta), - vol.Optional(ATTR_DATA_BITS, default=None): cv.positive_int, - vol.Optional(CONF_COMMAND_ON, default=None): cv.byte, - vol.Optional(CONF_COMMAND_OFF, default=None): cv.byte -}) - -DEFAULT_SCHEMA = vol.Schema({ - vol.Required("platform"): DOMAIN, - vol.Optional(CONF_DEVICES, default={}): vol.All(dict, _valid_light_switch), - vol.Optional(ATTR_AUTOMATIC_ADD, default=False): cv.boolean, - vol.Optional(CONF_SIGNAL_REPETITIONS, default=DEFAULT_SIGNAL_REPETITIONS): - vol.Coerce(int), -}) +DATA_RFXOBJECT = 'rfxobject' CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ - vol.Required(ATTR_DEVICE): cv.string, - vol.Optional(ATTR_DEBUG, default=False): cv.boolean, - vol.Optional(ATTR_DUMMY, default=False): cv.boolean, + vol.Required(CONF_DEVICE): cv.string, + vol.Optional(CONF_DEBUG, default=False): cv.boolean, + vol.Optional(CONF_DUMMY, default=False): cv.boolean, }), }, extra=vol.ALLOW_EXTRA) @@ -152,7 +78,7 @@ def setup(hass, config): """Set up the RFXtrx component.""" # Declare the Handle event def handle_receive(event): - """Handle revieved messgaes from RFXtrx gateway.""" + """Handle revieved messages from RFXtrx gateway.""" # Log RFXCOM event if not event.device.id_string: return @@ -175,21 +101,22 @@ def setup(hass, config): dummy_connection = config[DOMAIN][ATTR_DUMMY] if dummy_connection: - hass.data[RFXOBJECT] =\ - rfxtrxmod.Connect(device, None, debug=debug, - transport_protocol=rfxtrxmod.DummyTransport2) + rfx_object = rfxtrxmod.Connect( + device, None, debug=debug, + transport_protocol=rfxtrxmod.DummyTransport2) else: - hass.data[RFXOBJECT] = rfxtrxmod.Connect(device, None, debug=debug) + rfx_object = rfxtrxmod.Connect(device, None, debug=debug) def _start_rfxtrx(event): - hass.data[RFXOBJECT].event_callback = handle_receive + rfx_object.event_callback = handle_receive hass.bus.listen_once(EVENT_HOMEASSISTANT_START, _start_rfxtrx) def _shutdown_rfxtrx(event): """Close connection with RFXtrx.""" - hass.data[RFXOBJECT].close_connection() + rfx_object.close_connection() hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, _shutdown_rfxtrx) + hass.data[DATA_RFXOBJECT] = rfx_object return True @@ -248,9 +175,9 @@ def get_pt2262_device(device_id): if (hasattr(device, 'is_lighting4') and device.masked_id == get_pt2262_deviceid(device_id, device.data_bits)): - _LOGGER.info("rfxtrx: found matching device %s for %s", - device_id, - device.masked_id) + _LOGGER.debug("rfxtrx: found matching device %s for %s", + device_id, + device.masked_id) return device return None @@ -295,11 +222,11 @@ def get_devices_from_config(config, device): device_id = slugify(event.device.id_string.lower()) if device_id in RFX_DEVICES: continue - _LOGGER.info("Add %s rfxtrx", entity_info[ATTR_NAME]) + _LOGGER.debug("Add %s rfxtrx", entity_info[ATTR_NAME]) # Check if i must fire event - fire_event = entity_info[ATTR_FIREEVENT] - datas = {ATTR_STATE: False, ATTR_FIREEVENT: fire_event} + fire_event = entity_info[ATTR_FIRE_EVENT] + datas = {ATTR_STATE: False, ATTR_FIRE_EVENT: fire_event} new_device = device(entity_info[ATTR_NAME], event, datas, signal_repetitions) @@ -318,14 +245,14 @@ def get_new_device(event, config, device): return pkt_id = "".join("{0:02x}".format(x) for x in event.data) - _LOGGER.info( + _LOGGER.debug( "Automatic add %s rfxtrx device (Class: %s Sub: %s Packet_id: %s)", device_id, event.device.__class__.__name__, event.device.subtype, pkt_id ) - datas = {ATTR_STATE: False, ATTR_FIREEVENT: False} + datas = {ATTR_STATE: False, ATTR_FIRE_EVENT: False} signal_repetitions = config[CONF_SIGNAL_REPETITIONS] new_device = device(pkt_id, event, datas, signal_repetitions) @@ -370,7 +297,7 @@ def apply_received_command(event): ATTR_STATE: event.values['Command'].lower() } ) - _LOGGER.info( + _LOGGER.debug( "Rfxtrx fired event: (event_type: %s, %s: %s, %s: %s)", EVENT_BUTTON_PRESSED, ATTR_ENTITY_ID, @@ -392,7 +319,7 @@ class RfxtrxDevice(Entity): self._name = name self._event = event self._state = datas[ATTR_STATE] - self._should_fire_event = datas[ATTR_FIREEVENT] + self._should_fire_event = datas[ATTR_FIRE_EVENT] self._brightness = 0 self.added_to_hass = False @@ -440,40 +367,35 @@ class RfxtrxDevice(Entity): def _send_command(self, command, brightness=0): if not self._event: return + rfx_object = self.hass.data[DATA_RFXOBJECT] if command == "turn_on": for _ in range(self.signal_repetitions): - self._event.device.send_on(self.hass.data[RFXOBJECT] - .transport) + self._event.device.send_on(rfx_object.transport) self._state = True elif command == "dim": for _ in range(self.signal_repetitions): - self._event.device.send_dim(self.hass.data[RFXOBJECT] - .transport, brightness) + self._event.device.send_dim(rfx_object.transport, brightness) self._state = True elif command == 'turn_off': for _ in range(self.signal_repetitions): - self._event.device.send_off(self.hass.data[RFXOBJECT] - .transport) + self._event.device.send_off(rfx_object.transport) self._state = False self._brightness = 0 elif command == "roll_up": for _ in range(self.signal_repetitions): - self._event.device.send_open(self.hass.data[RFXOBJECT] - .transport) + self._event.device.send_open(rfx_object.transport) elif command == "roll_down": for _ in range(self.signal_repetitions): - self._event.device.send_close(self.hass.data[RFXOBJECT] - .transport) + self._event.device.send_close(rfx_object.transport) elif command == "stop_roll": for _ in range(self.signal_repetitions): - self._event.device.send_stop(self.hass.data[RFXOBJECT] - .transport) + self._event.device.send_stop(rfx_object.transport) if self.added_to_hass: self.schedule_update_ha_state() diff --git a/homeassistant/components/sensor/rfxtrx.py b/homeassistant/components/sensor/rfxtrx.py index e01dbc83422..1c09bc01909 100644 --- a/homeassistant/components/sensor/rfxtrx.py +++ b/homeassistant/components/sensor/rfxtrx.py @@ -10,21 +10,28 @@ import voluptuous as vol import homeassistant.components.rfxtrx as rfxtrx import homeassistant.helpers.config_validation as cv -from homeassistant.const import CONF_PLATFORM +from homeassistant.const import ATTR_ENTITY_ID, CONF_NAME from homeassistant.helpers.entity import Entity from homeassistant.util import slugify +from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.components.rfxtrx import ( - ATTR_AUTOMATIC_ADD, ATTR_NAME, ATTR_FIREEVENT, CONF_DEVICES, DATA_TYPES, - ATTR_DATA_TYPE, ATTR_ENTITY_ID) + ATTR_NAME, ATTR_FIRE_EVENT, ATTR_DATA_TYPE, CONF_AUTOMATIC_ADD, + CONF_FIRE_EVENT, CONF_DEVICES, DATA_TYPES, CONF_DATA_TYPE) DEPENDENCIES = ['rfxtrx'] _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = vol.Schema({ - vol.Required(CONF_PLATFORM): rfxtrx.DOMAIN, - vol.Optional(CONF_DEVICES, default={}): vol.All(dict, rfxtrx.valid_sensor), - vol.Optional(ATTR_AUTOMATIC_ADD, default=False): cv.boolean, +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_DEVICES, default={}): { + cv.string: vol.Schema({ + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_FIRE_EVENT, default=False): cv.boolean, + vol.Optional(CONF_DATA_TYPE, default=[]): + vol.All(cv.ensure_list, [vol.In(DATA_TYPES.keys())]), + }) + }, + vol.Optional(CONF_AUTOMATIC_ADD, default=False): cv.boolean, }, extra=vol.ALLOW_EXTRA) @@ -49,7 +56,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): break for _data_type in data_types: new_sensor = RfxtrxSensor(None, entity_info[ATTR_NAME], - _data_type, entity_info[ATTR_FIREEVENT]) + _data_type, entity_info[ATTR_FIRE_EVENT]) sensors.append(new_sensor) sub_sensors[_data_type] = new_sensor rfxtrx.RFX_DEVICES[device_id] = sub_sensors @@ -78,7 +85,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): return # Add entity if not exist and the automatic_add is True - if not config[ATTR_AUTOMATIC_ADD]: + if not config[CONF_AUTOMATIC_ADD]: return pkt_id = "".join("{0:02x}".format(x) for x in event.data) diff --git a/homeassistant/components/switch/rfxtrx.py b/homeassistant/components/switch/rfxtrx.py index 1361d22de18..7dd1d25ad94 100644 --- a/homeassistant/components/switch/rfxtrx.py +++ b/homeassistant/components/switch/rfxtrx.py @@ -6,14 +6,31 @@ https://home-assistant.io/components/switch.rfxtrx/ """ import logging +import voluptuous as vol + import homeassistant.components.rfxtrx as rfxtrx -from homeassistant.components.switch import SwitchDevice +from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA +from homeassistant.const import CONF_NAME +from homeassistant.components.rfxtrx import ( + CONF_AUTOMATIC_ADD, CONF_FIRE_EVENT, DEFAULT_SIGNAL_REPETITIONS, + CONF_SIGNAL_REPETITIONS, CONF_DEVICES) +from homeassistant.helpers import config_validation as cv DEPENDENCIES = ['rfxtrx'] _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = rfxtrx.DEFAULT_SCHEMA +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_DEVICES, default={}): { + cv.string: vol.Schema({ + vol.Required(CONF_NAME): cv.string, + vol.Optional(CONF_FIRE_EVENT, default=False): cv.boolean + }) + }, + vol.Optional(CONF_AUTOMATIC_ADD, default=False): cv.boolean, + vol.Optional(CONF_SIGNAL_REPETITIONS, default=DEFAULT_SIGNAL_REPETITIONS): + vol.Coerce(int), +}) def setup_platform(hass, config, add_devices_callback, discovery_info=None): diff --git a/requirements_all.txt b/requirements_all.txt index 31da31d56a9..b17c22a5f4f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -622,7 +622,7 @@ pyCEC==0.4.13 pyHS100==0.3.0 # homeassistant.components.rfxtrx -pyRFXtrx==0.20.1 +pyRFXtrx==0.21.1 # homeassistant.components.sensor.tibber pyTibber==0.2.1 From 3afa4726bf09852f5a13e36f6b1b4e2d21d8a723 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Fri, 12 Jan 2018 22:35:32 +0100 Subject: [PATCH 09/13] Xiaomi lib upgrade (#11603) * upgrade xiaomi lib * xiaomi lib --- homeassistant/components/xiaomi_aqara.py | 6 +++--- requirements_all.txt | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/xiaomi_aqara.py b/homeassistant/components/xiaomi_aqara.py index 678ead981c1..e059d3d8772 100644 --- a/homeassistant/components/xiaomi_aqara.py +++ b/homeassistant/components/xiaomi_aqara.py @@ -9,7 +9,7 @@ from homeassistant.components.discovery import SERVICE_XIAOMI_GW from homeassistant.const import (ATTR_BATTERY_LEVEL, EVENT_HOMEASSISTANT_STOP, CONF_MAC, CONF_HOST, CONF_PORT) -REQUIREMENTS = ['PyXiaomiGateway==0.6.0'] +REQUIREMENTS = ['PyXiaomiGateway==0.7.0'] ATTR_GW_MAC = 'gw_mac' ATTR_RINGTONE_ID = 'ringtone_id' @@ -105,8 +105,8 @@ def setup(hass, config): discovery.listen(hass, SERVICE_XIAOMI_GW, xiaomi_gw_discovered) - from PyXiaomiGateway import PyXiaomiGateway - xiaomi = hass.data[PY_XIAOMI_GATEWAY] = PyXiaomiGateway( + from xiaomi_gateway import XiaomiGatewayDiscovery + xiaomi = hass.data[PY_XIAOMI_GATEWAY] = XiaomiGatewayDiscovery( hass.add_job, gateways, interface) _LOGGER.debug("Expecting %s gateways", len(gateways)) diff --git a/requirements_all.txt b/requirements_all.txt index b17c22a5f4f..d06c1b4a539 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -35,7 +35,7 @@ PyMVGLive==1.1.4 PyMata==2.14 # homeassistant.components.xiaomi_aqara -PyXiaomiGateway==0.6.0 +PyXiaomiGateway==0.7.0 # homeassistant.components.rpi_gpio # RPi.GPIO==0.6.1 From 7a50450b922fd313be7dc048fb15a0a61bd89cac Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 13 Jan 2018 09:01:05 +0100 Subject: [PATCH 10/13] Upgrade yarl to 0.18.0 (#11609) --- 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 8ec1c648c35..243c6d418df 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -6,7 +6,7 @@ jinja2>=2.9.6 voluptuous==0.10.5 typing>=3,<4 aiohttp==2.3.7 -yarl==0.17.0 +yarl==0.18.0 async_timeout==2.0.0 chardet==3.0.4 astral==1.4 diff --git a/requirements_all.txt b/requirements_all.txt index d06c1b4a539..c949847ec86 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -7,7 +7,7 @@ jinja2>=2.9.6 voluptuous==0.10.5 typing>=3,<4 aiohttp==2.3.7 -yarl==0.17.0 +yarl==0.18.0 async_timeout==2.0.0 chardet==3.0.4 astral==1.4 diff --git a/setup.py b/setup.py index 0d7c746d564..4b19e47fb2c 100755 --- a/setup.py +++ b/setup.py @@ -54,7 +54,7 @@ REQUIRES = [ 'voluptuous==0.10.5', 'typing>=3,<4', 'aiohttp==2.3.7', # If updated, check if yarl also needs an update! - 'yarl==0.17.0', + 'yarl==0.18.0', 'async_timeout==2.0.0', 'chardet==3.0.4', 'astral==1.4', From d88edb0661b375c35ade21984c7ec70a9e2e37b7 Mon Sep 17 00:00:00 2001 From: Thijs de Jong Date: Sat, 13 Jan 2018 09:06:37 +0100 Subject: [PATCH 11/13] patch stop command (#11612) --- homeassistant/components/cover/tahoma.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/cover/tahoma.py b/homeassistant/components/cover/tahoma.py index 7ec09c781d2..9968e3d6503 100644 --- a/homeassistant/components/cover/tahoma.py +++ b/homeassistant/components/cover/tahoma.py @@ -15,11 +15,6 @@ DEPENDENCIES = ['tahoma'] _LOGGER = logging.getLogger(__name__) -TAHOMA_STOP_COMMAND = { - 'io:RollerShutterWithLowSpeedManagementIOComponent': 'my', - 'io:RollerShutterVeluxIOComponent': 'my', -} - SCAN_INTERVAL = timedelta(seconds=60) @@ -78,8 +73,11 @@ class TahomaCover(TahomaDevice, CoverDevice): def stop_cover(self, **kwargs): """Stop the cover.""" - self.apply_action(TAHOMA_STOP_COMMAND.get(self.tahoma_device.type, - 'stopIdentify')) + if self.tahoma_device.type == \ + 'io:RollerShutterWithLowSpeedManagementIOComponent': + self.apply_action('setPosition', 'secured') + else: + self.apply_action('stopIdentify') def device_class(self): """Return the class of this device, from component DEVICE_CLASSES.""" From 7837b4893ff98e5782a57b7bfd6176ed88f1af63 Mon Sep 17 00:00:00 2001 From: Jesse Hills Date: Sun, 14 Jan 2018 08:07:39 +1300 Subject: [PATCH 12/13] Use kelvin/mireds correctly for setting iglo white (#11622) * Use kelvin/mireds correctly for setting iglo white * Update requirements_all.txt * Fix line lengths --- homeassistant/components/light/iglo.py | 19 ++++++++++++------- requirements_all.txt | 2 +- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/light/iglo.py b/homeassistant/components/light/iglo.py index eaf783b13ca..11366ffc45c 100644 --- a/homeassistant/components/light/iglo.py +++ b/homeassistant/components/light/iglo.py @@ -5,6 +5,7 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/light.iglo/ """ import logging +import math import voluptuous as vol @@ -16,8 +17,9 @@ from homeassistant.components.light import ( ) import homeassistant.helpers.config_validation as cv +import homeassistant.util.color as color_util -REQUIREMENTS = ['iglo==1.0.0'] +REQUIREMENTS = ['iglo==1.1.3'] _LOGGER = logging.getLogger(__name__) @@ -65,17 +67,19 @@ class IGloLamp(Light): @property def color_temp(self): """Return the color temperature.""" - return self._color_temp + return color_util.color_temperature_kelvin_to_mired(self._color_temp) @property def min_mireds(self): """Return the coldest color_temp that this light supports.""" - return 1 + return math.ceil(color_util.color_temperature_kelvin_to_mired( + self._lamp.max_kelvin)) @property def max_mireds(self): """Return the warmest color_temp that this light supports.""" - return 255 + return math.ceil(color_util.color_temperature_kelvin_to_mired( + self._lamp.min_kelvin)) @property def rgb_color(self): @@ -107,8 +111,9 @@ class IGloLamp(Light): return if ATTR_COLOR_TEMP in kwargs: - color_temp = 255 - kwargs[ATTR_COLOR_TEMP] - self._lamp.white(color_temp) + kelvin = int(color_util.color_temperature_mired_to_kelvin( + kwargs[ATTR_COLOR_TEMP])) + self._lamp.white(kelvin) return def turn_off(self, **kwargs): @@ -121,4 +126,4 @@ class IGloLamp(Light): self._on = state['on'] self._brightness = state['brightness'] self._rgb = state['rgb'] - self._color_temp = 255 - state['white'] + self._color_temp = state['white'] diff --git a/requirements_all.txt b/requirements_all.txt index c949847ec86..2ef38c37aa7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -396,7 +396,7 @@ https://github.com/wokar/pylgnetcast/archive/v0.2.0.zip#pylgnetcast==0.2.0 # i2csense==0.0.4 # homeassistant.components.light.iglo -iglo==1.0.0 +iglo==1.1.3 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From 9b15cfa5a5fd113dc855e21f8b09aa9ae7eadc51 Mon Sep 17 00:00:00 2001 From: Alok Saboo Date: Sat, 13 Jan 2018 14:00:04 -0500 Subject: [PATCH 13/13] Update Pyarlo to 0.1.2 (#11626) --- homeassistant/components/arlo.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/arlo.py b/homeassistant/components/arlo.py index a78b334de0b..a928ed108c9 100644 --- a/homeassistant/components/arlo.py +++ b/homeassistant/components/arlo.py @@ -12,7 +12,7 @@ from requests.exceptions import HTTPError, ConnectTimeout from homeassistant.helpers import config_validation as cv from homeassistant.const import CONF_USERNAME, CONF_PASSWORD -REQUIREMENTS = ['pyarlo==0.1.0'] +REQUIREMENTS = ['pyarlo==0.1.2'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 2ef38c37aa7..820d3894a39 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -640,7 +640,7 @@ pyairvisual==1.0.0 pyalarmdotcom==0.3.0 # homeassistant.components.arlo -pyarlo==0.1.0 +pyarlo==0.1.2 # homeassistant.components.notify.xmpp pyasn1-modules==0.1.5