From 3492545ec11b6cddc696ec037d553a4afdac5f48 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 20 May 2017 22:34:53 -0700 Subject: [PATCH 1/6] Update state automation to work with new and deleted state changes --- homeassistant/components/automation/state.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/automation/state.py b/homeassistant/components/automation/state.py index 9c12a37f9b8..90a8f2adce4 100644 --- a/homeassistant/components/automation/state.py +++ b/homeassistant/components/automation/state.py @@ -80,7 +80,8 @@ def async_trigger(hass, config, action): return # If only state attributes changed, ignore this event - if from_s.last_changed == to_s.last_changed: + if (from_s is not None and to_s is not None and + from_s.last_changed == to_s.last_changed): return @callback From d6f43ba839c28bbfe915de4890d8f11d4821b249 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 20 May 2017 22:34:59 -0700 Subject: [PATCH 2/6] Version bump to 0.45.1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 65a8eb070c6..5fe9a9bedf6 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 45 -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 a9926e355ffbd328fc23f41ca547ba4bdbcda047 Mon Sep 17 00:00:00 2001 From: Eugenio Panadero Date: Mon, 22 May 2017 02:02:22 +0200 Subject: [PATCH 3/6] Fix telegram chats (#7689) * bugfix for Telegram chat_ids - Negative `chat_id`s for groups. - Include `chat_id` in event data. - Handle KeyError when receiving other types of messages, as `new_chat_member` ones, and send them as text. * unused import * fix double quote style, fix boolean expr, change warning msg * mistake * some more fixes - fix if condition for msg bad fields. - return True for a correct but not allowed or not recognized message: if not, the message arrives continuously. - Allow to receive messages from unauthorized users if they come from authorized groups. * support for `edited_message`s - They come as normal messages, except for the 'edited_message' field instead of 'message'. --- homeassistant/components/notify/telegram.py | 3 +- .../components/telegram_bot/__init__.py | 97 +++++++++++-------- 2 files changed, 59 insertions(+), 41 deletions(-) diff --git a/homeassistant/components/notify/telegram.py b/homeassistant/components/notify/telegram.py index 1bc2baa632e..fb453263dd8 100644 --- a/homeassistant/components/notify/telegram.py +++ b/homeassistant/components/notify/telegram.py @@ -8,7 +8,6 @@ import logging import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.notify import ( ATTR_MESSAGE, ATTR_TITLE, ATTR_DATA, ATTR_TARGET, PLATFORM_SCHEMA, BaseNotificationService) @@ -27,7 +26,7 @@ ATTR_DOCUMENT = 'document' CONF_CHAT_ID = 'chat_id' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_CHAT_ID): cv.positive_int, + vol.Required(CONF_CHAT_ID): vol.Coerce(int), }) diff --git a/homeassistant/components/telegram_bot/__init__.py b/homeassistant/components/telegram_bot/__init__.py index 235217d1942..fdc9d16677c 100644 --- a/homeassistant/components/telegram_bot/__init__.py +++ b/homeassistant/components/telegram_bot/__init__.py @@ -38,6 +38,7 @@ ATTR_COMMAND = 'command' ATTR_USER_ID = 'user_id' ATTR_ARGS = 'args' ATTR_MSG = 'message' +ATTR_EDITED_MSG = 'edited_message' ATTR_CHAT_INSTANCE = 'chat_instance' ATTR_CHAT_ID = 'chat_id' ATTR_MSGID = 'id' @@ -76,7 +77,7 @@ CONFIG_SCHEMA = vol.Schema({ vol.Required(CONF_PLATFORM): cv.string, vol.Required(CONF_API_KEY): cv.string, vol.Required(CONF_ALLOWED_CHAT_IDS): - vol.All(cv.ensure_list, [cv.positive_int]), + vol.All(cv.ensure_list, [vol.Coerce(int)]), vol.Optional(ATTR_PARSER, default=PARSER_MD): cv.string, vol.Optional(CONF_TRUSTED_NETWORKS, default=DEFAULT_TRUSTED_NETWORKS): vol.All(cv.ensure_list, [ip_network]) @@ -84,7 +85,7 @@ CONFIG_SCHEMA = vol.Schema({ }, extra=vol.ALLOW_EXTRA) BASE_SERVICE_SCHEMA = vol.Schema({ - vol.Optional(ATTR_TARGET): vol.All(cv.ensure_list, [cv.positive_int]), + vol.Optional(ATTR_TARGET): vol.All(cv.ensure_list, [vol.Coerce(int)]), vol.Optional(ATTR_PARSER): cv.string, vol.Optional(ATTR_DISABLE_NOTIF): cv.boolean, vol.Optional(ATTR_DISABLE_WEB_PREV): cv.boolean, @@ -113,19 +114,19 @@ SERVICE_SCHEMA_SEND_LOCATION = BASE_SERVICE_SCHEMA.extend({ SERVICE_EDIT_MESSAGE = 'edit_message' SERVICE_SCHEMA_EDIT_MESSAGE = SERVICE_SCHEMA_SEND_MESSAGE.extend({ vol.Required(ATTR_MESSAGEID): vol.Any(cv.positive_int, cv.string), - vol.Required(ATTR_CHAT_ID): cv.positive_int, + vol.Required(ATTR_CHAT_ID): vol.Coerce(int), }) SERVICE_EDIT_CAPTION = 'edit_caption' SERVICE_SCHEMA_EDIT_CAPTION = vol.Schema({ vol.Required(ATTR_MESSAGEID): vol.Any(cv.positive_int, cv.string), - vol.Required(ATTR_CHAT_ID): cv.positive_int, + vol.Required(ATTR_CHAT_ID): vol.Coerce(int), vol.Required(ATTR_CAPTION): cv.string, vol.Optional(ATTR_KEYBOARD_INLINE): cv.ensure_list, }, extra=vol.ALLOW_EXTRA) SERVICE_EDIT_REPLYMARKUP = 'edit_replymarkup' SERVICE_SCHEMA_EDIT_REPLYMARKUP = vol.Schema({ vol.Required(ATTR_MESSAGEID): vol.Any(cv.positive_int, cv.string), - vol.Required(ATTR_CHAT_ID): cv.positive_int, + vol.Required(ATTR_CHAT_ID): vol.Coerce(int), vol.Required(ATTR_KEYBOARD_INLINE): cv.ensure_list, }, extra=vol.ALLOW_EXTRA) SERVICE_ANSWER_CALLBACK_QUERY = 'answer_callback_query' @@ -198,7 +199,7 @@ def async_setup(hass, config): return except Exception: # pylint: disable=broad-except - _LOGGER.exception('Error setting up platform %s', p_type) + _LOGGER.exception("Error setting up platform %s", p_type) return notify_service = TelegramNotificationService( @@ -221,7 +222,7 @@ def async_setup(hass, config): kwargs = dict(service.data) _render_template_attr(kwargs, ATTR_MESSAGE) _render_template_attr(kwargs, ATTR_TITLE) - _LOGGER.debug('NEW telegram_message "%s": %s', msgtype, kwargs) + _LOGGER.debug("NEW telegram_message %s: %s", msgtype, kwargs) if msgtype == SERVICE_SEND_MESSAGE: yield from hass.async_add_job( @@ -300,7 +301,7 @@ class TelegramNotificationService: if isinstance(target, int): if target in self.allowed_chat_ids: return [target] - _LOGGER.warning('BAD TARGET "%s", using default: %s', + _LOGGER.warning("BAD TARGET %s, using default: %s", target, self._default_user) else: try: @@ -308,9 +309,9 @@ class TelegramNotificationService: if int(t) in self.allowed_chat_ids] if len(chat_ids) > 0: return chat_ids - _LOGGER.warning('ALL BAD TARGETS: "%s"', target) + _LOGGER.warning("ALL BAD TARGETS: %s", target) except (ValueError, TypeError): - _LOGGER.warning('BAD TARGET DATA "%s", using default: %s', + _LOGGER.warning("BAD TARGET DATA %s, using default: %s", target, self._default_user) return [self._default_user] @@ -378,10 +379,10 @@ class TelegramNotificationService: if not isinstance(out, bool) and hasattr(out, ATTR_MESSAGEID): chat_id = out.chat_id self._last_message_id[chat_id] = out[ATTR_MESSAGEID] - _LOGGER.debug('LAST MSG ID: %s (from chat_id %s)', + _LOGGER.debug("LAST MSG ID: %s (from chat_id %s)", self._last_message_id, chat_id) elif not isinstance(out, bool): - _LOGGER.warning('UPDATE LAST MSG??: out_type:%s, out=%s', + _LOGGER.warning("UPDATE LAST MSG??: out_type:%s, out=%s", type(out), out) return out except TelegramError: @@ -393,7 +394,7 @@ class TelegramNotificationService: text = '{}\n{}'.format(title, message) if title else message params = self._get_msg_kwargs(kwargs) for chat_id in self._get_target_chat_ids(target): - _LOGGER.debug('send_message in chat_id %s with params: %s', + _LOGGER.debug("send_message in chat_id %s with params: %s", chat_id, params) self._send_msg(self.bot.sendMessage, "Error sending message", @@ -404,13 +405,13 @@ class TelegramNotificationService: chat_id = self._get_target_chat_ids(chat_id)[0] message_id, inline_message_id = self._get_msg_ids(kwargs, chat_id) params = self._get_msg_kwargs(kwargs) - _LOGGER.debug('edit_message %s in chat_id %s with params: %s', + _LOGGER.debug("edit_message %s in chat_id %s with params: %s", message_id or inline_message_id, chat_id, params) if type_edit == SERVICE_EDIT_MESSAGE: message = kwargs.get(ATTR_MESSAGE) title = kwargs.get(ATTR_TITLE) text = '{}\n{}'.format(title, message) if title else message - _LOGGER.debug('editing message w/id %s.', + _LOGGER.debug("editing message w/id %s.", message_id or inline_message_id) return self._send_msg(self.bot.editMessageText, "Error editing text message", @@ -432,7 +433,7 @@ class TelegramNotificationService: show_alert=False, **kwargs): """Answer a callback originated with a press in an inline keyboard.""" params = self._get_msg_kwargs(kwargs) - _LOGGER.debug('answer_callback_query w/callback_id %s: %s, alert: %s.', + _LOGGER.debug("answer_callback_query w/callback_id %s: %s, alert: %s.", callback_query_id, message, show_alert) self._send_msg(self.bot.answerCallbackQuery, "Error sending answer callback query", @@ -451,7 +452,7 @@ class TelegramNotificationService: caption = kwargs.get(ATTR_CAPTION) func_send = self.bot.sendPhoto if is_photo else self.bot.sendDocument for chat_id in self._get_target_chat_ids(target): - _LOGGER.debug('send file %s to chat_id %s. Caption: %s.', + _LOGGER.debug("send file %s to chat_id %s. Caption: %s.", file, chat_id, caption) self._send_msg(func_send, "Error sending file", chat_id, file, caption=caption, **params) @@ -462,7 +463,7 @@ class TelegramNotificationService: longitude = float(longitude) params = self._get_msg_kwargs(kwargs) for chat_id in self._get_target_chat_ids(target): - _LOGGER.debug('send location %s/%s to chat_id %s.', + _LOGGER.debug("send location %s/%s to chat_id %s.", latitude, longitude, chat_id) self._send_msg(self.bot.sendLocation, "Error sending location", @@ -479,36 +480,54 @@ class BaseTelegramBotEntity: self.hass = hass def _get_message_data(self, msg_data): - if (not msg_data or - ('text' not in msg_data and 'data' not in msg_data) or - 'from' not in msg_data or - msg_data['from'].get('id') not in self.allowed_chat_ids): + """Return boolean msg_data_is_ok and dict msg_data.""" + if not msg_data: + return False, None + bad_fields = ('text' not in msg_data and + 'data' not in msg_data and + 'chat' not in msg_data) + if bad_fields or 'from' not in msg_data: # Message is not correct. _LOGGER.error("Incoming message does not have required data (%s)", msg_data) - return None + return False, None + if msg_data['from'].get('id') not in self.allowed_chat_ids \ + or msg_data['chat'].get('id') not in self.allowed_chat_ids: + # Origin is not allowed. + _LOGGER.error("Incoming message is not allowed (%s)", msg_data) + return True, None - return { + return True, { ATTR_USER_ID: msg_data['from']['id'], + ATTR_CHAT_ID: msg_data['chat']['id'], ATTR_FROM_FIRST: msg_data['from']['first_name'], ATTR_FROM_LAST: msg_data['from']['last_name'] } def process_message(self, data): """Check for basic message rules and fire an event if message is ok.""" - if ATTR_MSG in data: + if ATTR_MSG in data or ATTR_EDITED_MSG in data: event = EVENT_TELEGRAM_COMMAND - data = data.get(ATTR_MSG) - event_data = self._get_message_data(data) - if event_data is None: - return False - - if data[ATTR_TEXT][0] == '/': - pieces = data[ATTR_TEXT].split(' ') - event_data[ATTR_COMMAND] = pieces[0] - event_data[ATTR_ARGS] = pieces[1:] + if ATTR_MSG in data: + data = data.get(ATTR_MSG) else: - event_data[ATTR_TEXT] = data[ATTR_TEXT] + data = data.get(ATTR_EDITED_MSG) + message_ok, event_data = self._get_message_data(data) + if event_data is None: + return message_ok + + if 'text' in data: + if data['text'][0] == '/': + pieces = data['text'].split(' ') + event_data[ATTR_COMMAND] = pieces[0] + event_data[ATTR_ARGS] = pieces[1:] + else: + event_data[ATTR_TEXT] = data['text'] + event = EVENT_TELEGRAM_TEXT + else: + # Some other thing... + _LOGGER.warning("Message without text data received: %s", data) + event_data[ATTR_TEXT] = str(data) event = EVENT_TELEGRAM_TEXT self.hass.bus.async_fire(event, event_data) @@ -516,9 +535,9 @@ class BaseTelegramBotEntity: elif ATTR_CALLBACK_QUERY in data: event = EVENT_TELEGRAM_CALLBACK data = data.get(ATTR_CALLBACK_QUERY) - event_data = self._get_message_data(data) + message_ok, event_data = self._get_message_data(data) if event_data is None: - return False + return message_ok event_data[ATTR_DATA] = data[ATTR_DATA] event_data[ATTR_MSG] = data[ATTR_MSG] @@ -529,5 +548,5 @@ class BaseTelegramBotEntity: return True else: # Some other thing... - _LOGGER.warning('SOME OTHER THING RECEIVED --> "%s"', data) - return False + _LOGGER.warning("SOME OTHER THING RECEIVED --> %s", data) + return True From 3fb691ead66f4a5a57fa3acee34d07696be9542b Mon Sep 17 00:00:00 2001 From: cgtobi Date: Mon, 22 May 2017 02:05:04 +0200 Subject: [PATCH 4/6] Fix playback control of web streams (#7683) Web streams can't be paused and resumed later. That's why volumio stops them instead of pausing them. --- homeassistant/components/media_player/volumio.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/media_player/volumio.py b/homeassistant/components/media_player/volumio.py index 9bf0351d200..ade49b8116e 100755 --- a/homeassistant/components/media_player/volumio.py +++ b/homeassistant/components/media_player/volumio.py @@ -190,6 +190,8 @@ class Volumio(MediaPlayerDevice): def async_media_pause(self): """Send media_pause command to media player.""" + if self._state['trackType'] == 'webradio': + return self.send_volumio_msg('commands', params={'cmd': 'stop'}) return self.send_volumio_msg('commands', params={'cmd': 'pause'}) def async_set_volume_level(self, volume): From dc4b0695b5edc799cc6a242b296b69da9917e518 Mon Sep 17 00:00:00 2001 From: tobygray Date: Mon, 22 May 2017 01:26:05 +0100 Subject: [PATCH 5/6] device_tracker.ubus: Handle empty results (#7673) If OpenWRT isn't running the DHCP server then some OpenWRT hardware, such as TP-Link TL-WDR3600 v1, can't determine the host corresponding to an associated wifi client. This change handles that by returning None when the request has no data in the result. --- homeassistant/components/device_tracker/ubus.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) mode change 100644 => 100755 homeassistant/components/device_tracker/ubus.py diff --git a/homeassistant/components/device_tracker/ubus.py b/homeassistant/components/device_tracker/ubus.py old mode 100644 new mode 100755 index 31c7d32c4c1..b1d5aa499b5 --- a/homeassistant/components/device_tracker/ubus.py +++ b/homeassistant/components/device_tracker/ubus.py @@ -144,7 +144,10 @@ def _req_json_rpc(url, session_id, rpcmethod, subsystem, method, **params): response = res.json() if rpcmethod == "call": - return response["result"][1] + try: + return response["result"][1] + except IndexError: + return else: return response["result"] From cdc8628e5ac3ef9e5bac7c29d34a7aa455206216 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 22 May 2017 11:00:02 -0700 Subject: [PATCH 6/6] Allow fetching hass.io panel without auth (#7714) --- homeassistant/components/hassio.py | 7 +++- tests/components/test_hassio.py | 59 ++++++++++++++++++++++++++---- 2 files changed, 56 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/hassio.py b/homeassistant/components/hassio.py index ed7e13a2969..e33a387eada 100644 --- a/homeassistant/components/hassio.py +++ b/homeassistant/components/hassio.py @@ -16,7 +16,7 @@ from aiohttp.hdrs import CONTENT_TYPE import async_timeout from homeassistant.const import CONTENT_TYPE_TEXT_PLAIN -from homeassistant.components.http import HomeAssistantView +from homeassistant.components.http import HomeAssistantView, KEY_AUTHENTICATED from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.components.frontend import register_built_in_panel @@ -139,7 +139,7 @@ class HassIOView(HomeAssistantView): name = "api:hassio" url = "/api/hassio/{path:.+}" - requires_auth = True + requires_auth = False def __init__(self, hassio): """Initialize a hassio base view.""" @@ -148,6 +148,9 @@ class HassIOView(HomeAssistantView): @asyncio.coroutine def _handle(self, request, path): """Route data to hassio.""" + if path != 'panel' and not request[KEY_AUTHENTICATED]: + return web.Response(status=401) + client = yield from self.hassio.command_proxy(path, request) data = yield from client.read() diff --git a/tests/components/test_hassio.py b/tests/components/test_hassio.py index 658e78b4523..eb0754fdc0a 100644 --- a/tests/components/test_hassio.py +++ b/tests/components/test_hassio.py @@ -5,9 +5,12 @@ from unittest.mock import patch, Mock, MagicMock import pytest +from homeassistant.const import HTTP_HEADER_HA_AUTH from homeassistant.setup import async_setup_component -from tests.common import mock_coro, mock_http_component_app +from tests.common import mock_coro + +API_PASSWORD = 'pass1234' @pytest.fixture @@ -22,10 +25,12 @@ def hassio_env(): @pytest.fixture def hassio_client(hassio_env, hass, test_client): """Create mock hassio http client.""" - app = mock_http_component_app(hass) - hass.loop.run_until_complete(async_setup_component(hass, 'hassio', {})) - hass.http.views['api:hassio'].register(app.router) - yield hass.loop.run_until_complete(test_client(app)) + hass.loop.run_until_complete(async_setup_component(hass, 'hassio', { + 'http': { + 'api_password': API_PASSWORD + } + })) + yield hass.loop.run_until_complete(test_client(hass.http.app)) @asyncio.coroutine @@ -56,7 +61,40 @@ def test_forward_request(hassio_client): Mock(return_value=mock_coro(response))), \ patch('homeassistant.components.hassio._create_response') as mresp: mresp.return_value = 'response' - resp = yield from hassio_client.post('/api/hassio/beer') + resp = yield from hassio_client.post('/api/hassio/beer', headers={ + HTTP_HEADER_HA_AUTH: API_PASSWORD + }) + + # Check we got right response + assert resp.status == 200 + body = yield from resp.text() + assert body == 'response' + + # Check we forwarded command + assert len(mresp.mock_calls) == 1 + assert mresp.mock_calls[0][1] == (response, 'data') + + +@asyncio.coroutine +def test_auth_required_forward_request(hassio_client): + """Test auth required for normal request.""" + resp = yield from hassio_client.post('/api/hassio/beer') + + # Check we got right response + assert resp.status == 401 + + +@asyncio.coroutine +def test_forward_request_no_auth_for_panel(hassio_client): + """Test no auth needed for .""" + response = MagicMock() + response.read.return_value = mock_coro('data') + + with patch('homeassistant.components.hassio.HassIO.command_proxy', + Mock(return_value=mock_coro(response))), \ + patch('homeassistant.components.hassio._create_response') as mresp: + mresp.return_value = 'response' + resp = yield from hassio_client.get('/api/hassio/panel') # Check we got right response assert resp.status == 200 @@ -79,7 +117,9 @@ def test_forward_log_request(hassio_client): patch('homeassistant.components.hassio.' '_create_response_log') as mresp: mresp.return_value = 'response' - resp = yield from hassio_client.get('/api/hassio/beer/logs') + resp = yield from hassio_client.get('/api/hassio/beer/logs', headers={ + HTTP_HEADER_HA_AUTH: API_PASSWORD + }) # Check we got right response assert resp.status == 200 @@ -96,5 +136,8 @@ def test_bad_gateway_when_cannot_find_supervisor(hassio_client): """Test we get a bad gateway error if we can't find supervisor.""" with patch('homeassistant.components.hassio.async_timeout.timeout', side_effect=asyncio.TimeoutError): - resp = yield from hassio_client.get('/api/hassio/addons/test/info') + resp = yield from hassio_client.get( + '/api/hassio/addons/test/info', headers={ + HTTP_HEADER_HA_AUTH: API_PASSWORD + }) assert resp.status == 502