diff --git a/homeassistant/components/automation/state.py b/homeassistant/components/automation/state.py index 9c12a37f9b8ffc..90a8f2adce4718 100644 --- a/homeassistant/components/automation/state.py +++ b/homeassistant/components/automation/state.py @@ -80,7 +80,8 @@ def call_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 diff --git a/homeassistant/components/device_tracker/ubus.py b/homeassistant/components/device_tracker/ubus.py old mode 100644 new mode 100755 index 31c7d32c4c1357..b1d5aa499b5802 --- 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"] diff --git a/homeassistant/components/hassio.py b/homeassistant/components/hassio.py index ed7e13a2969ed4..e33a387eadab97 100644 --- a/homeassistant/components/hassio.py +++ b/homeassistant/components/hassio.py @@ -16,7 +16,7 @@ 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 @@ def __init__(self, hassio): @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/homeassistant/components/media_player/volumio.py b/homeassistant/components/media_player/volumio.py index 9bf0351d200be1..ade49b8116e4b3 100755 --- a/homeassistant/components/media_player/volumio.py +++ b/homeassistant/components/media_player/volumio.py @@ -190,6 +190,8 @@ def async_media_play(self): 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): diff --git a/homeassistant/components/notify/html5.py b/homeassistant/components/notify/html5.py index 52d2deedcd493e..1cd22edc94e89f 100644 --- a/homeassistant/components/notify/html5.py +++ b/homeassistant/components/notify/html5.py @@ -115,7 +115,7 @@ def get_service(hass, config, discovery_info=None): add_manifest_json_key( ATTR_GCM_SENDER_ID, config.get(ATTR_GCM_SENDER_ID)) - return HTML5NotificationService(gcm_api_key, registrations) + return HTML5NotificationService(gcm_api_key, registrations, json_path) def _load_config(filename): @@ -327,10 +327,11 @@ def post(self, request): class HTML5NotificationService(BaseNotificationService): """Implement the notification service for HTML5.""" - def __init__(self, gcm_key, registrations): + def __init__(self, gcm_key, registrations, json_path): """Initialize the service.""" self._gcm_key = gcm_key self.registrations = registrations + self.registrations_json_path = json_path @property def targets(self): @@ -383,7 +384,7 @@ def send_message(self, message="", **kwargs): if not targets: targets = self.registrations.keys() - for target in targets: + for target in list(targets): info = self.registrations.get(target) if info is None: _LOGGER.error("%s is not a valid HTML5 push notification" @@ -399,5 +400,15 @@ def send_message(self, message="", **kwargs): jwt_token = jwt.encode(jwt_claims, jwt_secret).decode('utf-8') payload[ATTR_DATA][ATTR_JWT] = jwt_token - WebPusher(info[ATTR_SUBSCRIPTION]).send( + response = WebPusher(info[ATTR_SUBSCRIPTION]).send( json.dumps(payload), gcm_key=self._gcm_key, ttl='86400') + + if (response.status_code == 410): + _LOGGER.info("Notification channel has expired") + reg = self.registrations.pop(target) + if not _save_config(self.registrations_json_path, + self.registrations): + self.registrations[target] = reg + _LOGGER.error("Error saving registration.") + else: + _LOGGER.info("Configuration saved") diff --git a/homeassistant/components/notify/telegram.py b/homeassistant/components/notify/telegram.py index 1bc2baa632e6d2..fb453263dd8542 100644 --- a/homeassistant/components/notify/telegram.py +++ b/homeassistant/components/notify/telegram.py @@ -8,7 +8,6 @@ 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 @@ 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 235217d1942ade..fdc9d16677cc21 100644 --- a/homeassistant/components/telegram_bot/__init__.py +++ b/homeassistant/components/telegram_bot/__init__.py @@ -38,6 +38,7 @@ 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 @@ 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 @@ }, 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_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_platform(p_type, p_config=None, discovery_info=None): 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 _render_template_attr(data, attribute): 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 @@ def _get_target_chat_ids(self, target): 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 @@ def _get_target_chat_ids(self, target): 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 @@ def _send_msg(self, func_send, msg_error, *args_rep, **kwargs_rep): 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 @@ def send_message(self, message="", target=None, **kwargs): 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 @@ def edit_message(self, type_edit, chat_id=None, **kwargs): 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 @@ def answer_callback_query(self, message, callback_query_id, 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 @@ def send_file(self, is_photo=True, target=None, **kwargs): 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 @@ def send_location(self, latitude, longitude, target=None, **kwargs): 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 @@ def __init__(self, hass, allowed_chat_ids): 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 { + 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 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 ATTR_MSG in data: + data = data.get(ATTR_MSG) + else: + data = data.get(ATTR_EDITED_MSG) + message_ok, 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:] + 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: - event_data[ATTR_TEXT] = data[ATTR_TEXT] + # 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 @@ def process_message(self, data): 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 @@ def process_message(self, data): 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 diff --git a/homeassistant/const.py b/homeassistant/const.py index 65a8eb070c6369..5fe9a9bedf6f93 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) diff --git a/tests/components/notify/test_html5.py b/tests/components/notify/test_html5.py index 8d27a11e0943de..652a70b86840a9 100644 --- a/tests/components/notify/test_html5.py +++ b/tests/components/notify/test_html5.py @@ -86,10 +86,15 @@ def test_sending_message(self, mock_wp): service.send_message('Hello', target=['device', 'non_existing'], data={'icon': 'beer.png'}) - assert len(mock_wp.mock_calls) == 2 + print(mock_wp.mock_calls) + + assert len(mock_wp.mock_calls) == 3 + # WebPusher constructor assert mock_wp.mock_calls[0][1][0] == SUBSCRIPTION_1['subscription'] + # Third mock_call checks the status_code of the response. + assert mock_wp.mock_calls[2][0] == '().send().status_code.__eq__' # Call to send payload = json.loads(mock_wp.mock_calls[1][1][0]) @@ -376,11 +381,13 @@ def test_callback_view_with_jwt(self, loop, test_client): service.send_message('Hello', target=['device'], data={'icon': 'beer.png'}) - assert len(mock_wp.mock_calls) == 2 + assert len(mock_wp.mock_calls) == 3 # WebPusher constructor assert mock_wp.mock_calls[0][1][0] == \ SUBSCRIPTION_1['subscription'] + # Third mock_call checks the status_code of the response. + assert mock_wp.mock_calls[2][0] == '().send().status_code.__eq__' # Call to send push_payload = json.loads(mock_wp.mock_calls[1][1][0]) diff --git a/tests/components/test_hassio.py b/tests/components/test_hassio.py index 658e78b45233cc..eb0754fdc0abf4 100644 --- a/tests/components/test_hassio.py +++ b/tests/components/test_hassio.py @@ -5,9 +5,12 @@ 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