Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6bb95f6b58 | |||
| c90219ad2e | |||
| 0e7a2f163c | |||
| 536356ceec | |||
| 984af45bb2 | |||
| eab575e65d | |||
| e7a17b710d | |||
| a267df2abb | |||
| 9e56283eaf |
@@ -79,7 +79,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
)
|
||||
)
|
||||
|
||||
add_entities(sensors, True)
|
||||
add_entities(sensors, True)
|
||||
|
||||
|
||||
def get_opening_type(zone):
|
||||
|
||||
@@ -65,12 +65,25 @@ class HassIOIngress(HomeAssistantView):
|
||||
post = _handle
|
||||
put = _handle
|
||||
delete = _handle
|
||||
patch = _handle
|
||||
options = _handle
|
||||
|
||||
async def _handle_websocket(
|
||||
self, request: web.Request, token: str, path: str
|
||||
) -> web.WebSocketResponse:
|
||||
"""Ingress route for websocket."""
|
||||
ws_server = web.WebSocketResponse()
|
||||
if hdrs.SEC_WEBSOCKET_PROTOCOL in request.headers:
|
||||
req_protocols = [
|
||||
str(proto.strip())
|
||||
for proto in
|
||||
request.headers[hdrs.SEC_WEBSOCKET_PROTOCOL].split(",")
|
||||
]
|
||||
else:
|
||||
req_protocols = ()
|
||||
|
||||
ws_server = web.WebSocketResponse(
|
||||
protocols=req_protocols, autoclose=False, autoping=False
|
||||
)
|
||||
await ws_server.prepare(request)
|
||||
|
||||
# Preparing
|
||||
@@ -83,7 +96,8 @@ class HassIOIngress(HomeAssistantView):
|
||||
|
||||
# Start proxy
|
||||
async with self._websession.ws_connect(
|
||||
url, headers=source_header
|
||||
url, headers=source_header, protocols=req_protocols,
|
||||
autoclose=False, autoping=False,
|
||||
) as ws_client:
|
||||
# Proxy requests
|
||||
await asyncio.wait(
|
||||
@@ -197,22 +211,25 @@ def _is_websocket(request: web.Request) -> bool:
|
||||
"""Return True if request is a websocket."""
|
||||
headers = request.headers
|
||||
|
||||
if headers.get(hdrs.CONNECTION) == "Upgrade" and \
|
||||
headers.get(hdrs.UPGRADE) == "websocket":
|
||||
if "upgrade" in headers.get(hdrs.CONNECTION, "").lower() and \
|
||||
headers.get(hdrs.UPGRADE, "").lower() == "websocket":
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
async def _websocket_forward(ws_from, ws_to):
|
||||
"""Handle websocket message directly."""
|
||||
async for msg in ws_from:
|
||||
if msg.type == aiohttp.WSMsgType.TEXT:
|
||||
await ws_to.send_str(msg.data)
|
||||
elif msg.type == aiohttp.WSMsgType.BINARY:
|
||||
await ws_to.send_bytes(msg.data)
|
||||
elif msg.type == aiohttp.WSMsgType.PING:
|
||||
await ws_to.ping()
|
||||
elif msg.type == aiohttp.WSMsgType.PONG:
|
||||
await ws_to.pong()
|
||||
elif ws_to.closed:
|
||||
await ws_to.close(code=ws_to.close_code, message=msg.extra)
|
||||
try:
|
||||
async for msg in ws_from:
|
||||
if msg.type == aiohttp.WSMsgType.TEXT:
|
||||
await ws_to.send_str(msg.data)
|
||||
elif msg.type == aiohttp.WSMsgType.BINARY:
|
||||
await ws_to.send_bytes(msg.data)
|
||||
elif msg.type == aiohttp.WSMsgType.PING:
|
||||
await ws_to.ping()
|
||||
elif msg.type == aiohttp.WSMsgType.PONG:
|
||||
await ws_to.pong()
|
||||
elif ws_to.closed:
|
||||
await ws_to.close(code=ws_to.close_code, message=msg.extra)
|
||||
except RuntimeError:
|
||||
_LOGGER.debug("Ingress Websocket runtime error")
|
||||
|
||||
@@ -66,7 +66,8 @@ class HomeAssistantView:
|
||||
urls = [self.url] + self.extra_urls
|
||||
routes = []
|
||||
|
||||
for method in ('get', 'post', 'delete', 'put'):
|
||||
for method in ('get', 'post', 'delete', 'put', 'patch', 'head',
|
||||
'options'):
|
||||
handler = getattr(self, method, None)
|
||||
|
||||
if not handler:
|
||||
|
||||
@@ -8,9 +8,10 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
|
||||
from .const import (ATTR_SENSOR_STATE,
|
||||
ATTR_SENSOR_TYPE_BINARY_SENSOR as ENTITY_TYPE,
|
||||
ATTR_SENSOR_UNIQUE_ID,
|
||||
DATA_DEVICES, DOMAIN)
|
||||
|
||||
from .entity import MobileAppEntity
|
||||
from .entity import MobileAppEntity, sensor_id
|
||||
|
||||
DEPENDENCIES = ['mobile_app']
|
||||
|
||||
@@ -36,6 +37,16 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
if data[CONF_WEBHOOK_ID] != webhook_id:
|
||||
return
|
||||
|
||||
unique_id = sensor_id(data[CONF_WEBHOOK_ID],
|
||||
data[ATTR_SENSOR_UNIQUE_ID])
|
||||
|
||||
entity = hass.data[DOMAIN][ENTITY_TYPE][unique_id]
|
||||
|
||||
if 'added' in entity:
|
||||
return
|
||||
|
||||
entity['added'] = True
|
||||
|
||||
device = hass.data[DOMAIN][DATA_DEVICES][data[CONF_WEBHOOK_ID]]
|
||||
|
||||
async_add_entities([MobileAppBinarySensor(data, device, config_entry)])
|
||||
|
||||
@@ -13,6 +13,11 @@ from .const import (ATTR_DEVICE_ID, ATTR_DEVICE_NAME, ATTR_MANUFACTURER,
|
||||
DOMAIN, SIGNAL_SENSOR_UPDATE)
|
||||
|
||||
|
||||
def sensor_id(webhook_id, unique_id):
|
||||
"""Return a unique sensor ID."""
|
||||
return "{}_{}".format(webhook_id, unique_id)
|
||||
|
||||
|
||||
class MobileAppEntity(Entity):
|
||||
"""Representation of an mobile app entity."""
|
||||
|
||||
@@ -22,8 +27,8 @@ class MobileAppEntity(Entity):
|
||||
self._device = device
|
||||
self._entry = entry
|
||||
self._registration = entry.data
|
||||
self._sensor_id = "{}_{}".format(self._registration[CONF_WEBHOOK_ID],
|
||||
config[ATTR_SENSOR_UNIQUE_ID])
|
||||
self._sensor_id = sensor_id(self._registration[CONF_WEBHOOK_ID],
|
||||
config[ATTR_SENSOR_UNIQUE_ID])
|
||||
self._entity_type = config[ATTR_SENSOR_TYPE]
|
||||
self.unsub_dispatcher = None
|
||||
|
||||
@@ -94,5 +99,10 @@ class MobileAppEntity(Entity):
|
||||
@callback
|
||||
def _handle_update(self, data):
|
||||
"""Handle async event updates."""
|
||||
incoming_id = sensor_id(data[CONF_WEBHOOK_ID],
|
||||
data[ATTR_SENSOR_UNIQUE_ID])
|
||||
if incoming_id != self._sensor_id:
|
||||
return
|
||||
|
||||
self._config = data
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@@ -7,9 +7,10 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
|
||||
from .const import (ATTR_SENSOR_STATE,
|
||||
ATTR_SENSOR_TYPE_SENSOR as ENTITY_TYPE,
|
||||
ATTR_SENSOR_UOM, DATA_DEVICES, DOMAIN)
|
||||
ATTR_SENSOR_UNIQUE_ID, ATTR_SENSOR_UOM, DATA_DEVICES,
|
||||
DOMAIN)
|
||||
|
||||
from .entity import MobileAppEntity
|
||||
from .entity import MobileAppEntity, sensor_id
|
||||
|
||||
DEPENDENCIES = ['mobile_app']
|
||||
|
||||
@@ -35,6 +36,16 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
if data[CONF_WEBHOOK_ID] != webhook_id:
|
||||
return
|
||||
|
||||
unique_id = sensor_id(data[CONF_WEBHOOK_ID],
|
||||
data[ATTR_SENSOR_UNIQUE_ID])
|
||||
|
||||
entity = hass.data[DOMAIN][ENTITY_TYPE][unique_id]
|
||||
|
||||
if 'added' in entity:
|
||||
return
|
||||
|
||||
entity['added'] = True
|
||||
|
||||
device = hass.data[DOMAIN][DATA_DEVICES][data[CONF_WEBHOOK_ID]]
|
||||
|
||||
async_add_entities([MobileAppSensor(data, device, config_entry)])
|
||||
|
||||
@@ -4,6 +4,8 @@ import logging
|
||||
from aiohttp.web import HTTPBadRequest, Response, Request
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.cloud import (async_remote_ui_url,
|
||||
CloudNotAvailable)
|
||||
from homeassistant.components.device_tracker import (ATTR_ATTRIBUTES,
|
||||
ATTR_DEV_ID,
|
||||
DOMAIN as DT_DOMAIN,
|
||||
@@ -31,14 +33,15 @@ from .const import (ATTR_ALTITUDE, ATTR_BATTERY, ATTR_COURSE, ATTR_DEVICE_ID,
|
||||
ATTR_TEMPLATE_VARIABLES, ATTR_VERTICAL_ACCURACY,
|
||||
ATTR_WEBHOOK_DATA, ATTR_WEBHOOK_ENCRYPTED,
|
||||
ATTR_WEBHOOK_ENCRYPTED_DATA, ATTR_WEBHOOK_TYPE,
|
||||
CONF_SECRET, DATA_CONFIG_ENTRIES, DATA_DELETED_IDS,
|
||||
DATA_STORE, DOMAIN, ERR_ENCRYPTION_REQUIRED,
|
||||
ERR_SENSOR_DUPLICATE_UNIQUE_ID, ERR_SENSOR_NOT_REGISTERED,
|
||||
SIGNAL_SENSOR_UPDATE, WEBHOOK_PAYLOAD_SCHEMA,
|
||||
WEBHOOK_SCHEMAS, WEBHOOK_TYPES, WEBHOOK_TYPE_CALL_SERVICE,
|
||||
WEBHOOK_TYPE_FIRE_EVENT, WEBHOOK_TYPE_GET_CONFIG,
|
||||
WEBHOOK_TYPE_GET_ZONES, WEBHOOK_TYPE_REGISTER_SENSOR,
|
||||
WEBHOOK_TYPE_RENDER_TEMPLATE, WEBHOOK_TYPE_UPDATE_LOCATION,
|
||||
CONF_CLOUDHOOK_URL, CONF_REMOTE_UI_URL, CONF_SECRET,
|
||||
DATA_CONFIG_ENTRIES, DATA_DELETED_IDS, DATA_STORE, DOMAIN,
|
||||
ERR_ENCRYPTION_REQUIRED, ERR_SENSOR_DUPLICATE_UNIQUE_ID,
|
||||
ERR_SENSOR_NOT_REGISTERED, SIGNAL_SENSOR_UPDATE,
|
||||
WEBHOOK_PAYLOAD_SCHEMA, WEBHOOK_SCHEMAS, WEBHOOK_TYPES,
|
||||
WEBHOOK_TYPE_CALL_SERVICE, WEBHOOK_TYPE_FIRE_EVENT,
|
||||
WEBHOOK_TYPE_GET_CONFIG, WEBHOOK_TYPE_GET_ZONES,
|
||||
WEBHOOK_TYPE_REGISTER_SENSOR, WEBHOOK_TYPE_RENDER_TEMPLATE,
|
||||
WEBHOOK_TYPE_UPDATE_LOCATION,
|
||||
WEBHOOK_TYPE_UPDATE_REGISTRATION,
|
||||
WEBHOOK_TYPE_UPDATE_SENSOR_STATES)
|
||||
|
||||
@@ -96,6 +99,9 @@ async def handle_webhook(hass: HomeAssistantType, webhook_id: str,
|
||||
|
||||
data = webhook_payload
|
||||
|
||||
_LOGGER.debug("Received webhook payload for type %s: %s", webhook_type,
|
||||
data)
|
||||
|
||||
if webhook_type in WEBHOOK_SCHEMAS:
|
||||
try:
|
||||
data = WEBHOOK_SCHEMAS[webhook_type](webhook_payload)
|
||||
@@ -286,7 +292,7 @@ async def handle_webhook(hass: HomeAssistantType, webhook_id: str,
|
||||
|
||||
hass_config = hass.config.as_dict()
|
||||
|
||||
return webhook_response({
|
||||
resp = {
|
||||
'latitude': hass_config['latitude'],
|
||||
'longitude': hass_config['longitude'],
|
||||
'elevation': hass_config['elevation'],
|
||||
@@ -296,4 +302,15 @@ async def handle_webhook(hass: HomeAssistantType, webhook_id: str,
|
||||
'components': hass_config['components'],
|
||||
'version': hass_config['version'],
|
||||
'theme_color': MANIFEST_JSON['theme_color'],
|
||||
}, registration=registration, headers=headers)
|
||||
}
|
||||
|
||||
if CONF_CLOUDHOOK_URL in registration:
|
||||
resp[CONF_CLOUDHOOK_URL] = registration[CONF_CLOUDHOOK_URL]
|
||||
|
||||
try:
|
||||
resp[CONF_REMOTE_UI_URL] = async_remote_ui_url(hass)
|
||||
except CloudNotAvailable:
|
||||
pass
|
||||
|
||||
return webhook_response(resp, registration=registration,
|
||||
headers=headers)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"""Constants used by Home Assistant components."""
|
||||
MAJOR_VERSION = 0
|
||||
MINOR_VERSION = 91
|
||||
PATCH_VERSION = '2'
|
||||
PATCH_VERSION = '4'
|
||||
__short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION)
|
||||
__version__ = '{}.{}'.format(__short_version__, PATCH_VERSION)
|
||||
REQUIRED_PYTHON_VER = (3, 5, 3)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""The tests for the hassio component."""
|
||||
|
||||
from aiohttp.hdrs import X_FORWARDED_FOR, X_FORWARDED_HOST, X_FORWARDED_PROTO
|
||||
from aiohttp.client_exceptions import WSServerHandshakeError
|
||||
import pytest
|
||||
|
||||
|
||||
@@ -137,6 +136,72 @@ async def test_ingress_request_delete(
|
||||
assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_PROTO]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'build_type', [
|
||||
("a3_vl", "test/beer/ping?index=1"), ("core", "index.html"),
|
||||
("local", "panel/config"), ("jk_921", "editor.php?idx=3&ping=5"),
|
||||
("fsadjf10312", "")
|
||||
])
|
||||
async def test_ingress_request_patch(
|
||||
hassio_client, build_type, aioclient_mock):
|
||||
"""Test no auth needed for ."""
|
||||
aioclient_mock.patch("http://127.0.0.1/ingress/{}/{}".format(
|
||||
build_type[0], build_type[1]), text="test")
|
||||
|
||||
resp = await hassio_client.patch(
|
||||
'/api/hassio_ingress/{}/{}'.format(build_type[0], build_type[1]),
|
||||
headers={"X-Test-Header": "beer"}
|
||||
)
|
||||
|
||||
# Check we got right response
|
||||
assert resp.status == 200
|
||||
body = await resp.text()
|
||||
assert body == "test"
|
||||
|
||||
# Check we forwarded command
|
||||
assert len(aioclient_mock.mock_calls) == 1
|
||||
assert aioclient_mock.mock_calls[-1][3]["X-Hassio-Key"] == "123456"
|
||||
assert aioclient_mock.mock_calls[-1][3]["X-Ingress-Path"] == \
|
||||
"/api/hassio_ingress/{}".format(build_type[0])
|
||||
assert aioclient_mock.mock_calls[-1][3]["X-Test-Header"] == "beer"
|
||||
assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_FOR]
|
||||
assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_HOST]
|
||||
assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_PROTO]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'build_type', [
|
||||
("a3_vl", "test/beer/ping?index=1"), ("core", "index.html"),
|
||||
("local", "panel/config"), ("jk_921", "editor.php?idx=3&ping=5"),
|
||||
("fsadjf10312", "")
|
||||
])
|
||||
async def test_ingress_request_options(
|
||||
hassio_client, build_type, aioclient_mock):
|
||||
"""Test no auth needed for ."""
|
||||
aioclient_mock.options("http://127.0.0.1/ingress/{}/{}".format(
|
||||
build_type[0], build_type[1]), text="test")
|
||||
|
||||
resp = await hassio_client.options(
|
||||
'/api/hassio_ingress/{}/{}'.format(build_type[0], build_type[1]),
|
||||
headers={"X-Test-Header": "beer"}
|
||||
)
|
||||
|
||||
# Check we got right response
|
||||
assert resp.status == 200
|
||||
body = await resp.text()
|
||||
assert body == "test"
|
||||
|
||||
# Check we forwarded command
|
||||
assert len(aioclient_mock.mock_calls) == 1
|
||||
assert aioclient_mock.mock_calls[-1][3]["X-Hassio-Key"] == "123456"
|
||||
assert aioclient_mock.mock_calls[-1][3]["X-Ingress-Path"] == \
|
||||
"/api/hassio_ingress/{}".format(build_type[0])
|
||||
assert aioclient_mock.mock_calls[-1][3]["X-Test-Header"] == "beer"
|
||||
assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_FOR]
|
||||
assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_HOST]
|
||||
assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_PROTO]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'build_type', [
|
||||
("a3_vl", "test/beer/ws"), ("core", "ws.php"),
|
||||
@@ -150,11 +215,10 @@ async def test_ingress_websocket(
|
||||
build_type[0], build_type[1]))
|
||||
|
||||
# Ignore error because we can setup a full IO infrastructure
|
||||
with pytest.raises(WSServerHandshakeError):
|
||||
await hassio_client.ws_connect(
|
||||
'/api/hassio_ingress/{}/{}'.format(build_type[0], build_type[1]),
|
||||
headers={"X-Test-Header": "beer"}
|
||||
)
|
||||
await hassio_client.ws_connect(
|
||||
'/api/hassio_ingress/{}/{}'.format(build_type[0], build_type[1]),
|
||||
headers={"X-Test-Header": "beer"}
|
||||
)
|
||||
|
||||
# Check we forwarded command
|
||||
assert len(aioclient_mock.mock_calls) == 1
|
||||
|
||||
@@ -82,6 +82,10 @@ class AiohttpClientMocker:
|
||||
"""Register a mock options request."""
|
||||
self.request('options', *args, **kwargs)
|
||||
|
||||
def patch(self, *args, **kwargs):
|
||||
"""Register a mock patch request."""
|
||||
self.request('patch', *args, **kwargs)
|
||||
|
||||
@property
|
||||
def call_count(self):
|
||||
"""Return the number of requests made."""
|
||||
@@ -102,7 +106,7 @@ class AiohttpClientMocker:
|
||||
|
||||
async def match_request(self, method, url, *, data=None, auth=None,
|
||||
params=None, headers=None, allow_redirects=None,
|
||||
timeout=None, json=None, cookies=None):
|
||||
timeout=None, json=None, cookies=None, **kwargs):
|
||||
"""Match a request against pre-registered requests."""
|
||||
data = data or json
|
||||
url = URL(url)
|
||||
|
||||
Reference in New Issue
Block a user