mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 03:07:37 +00:00
mobile_app: Camera Stream Webhook (#36839)
This commit is contained in:
parent
87f236c05c
commit
8541ae0360
@ -72,3 +72,5 @@ ATTR_SENSOR_UOM = "unit_of_measurement"
|
|||||||
|
|
||||||
SIGNAL_SENSOR_UPDATE = f"{DOMAIN}_sensor_update"
|
SIGNAL_SENSOR_UPDATE = f"{DOMAIN}_sensor_update"
|
||||||
SIGNAL_LOCATION_UPDATE = DOMAIN + "_location_update_{}"
|
SIGNAL_LOCATION_UPDATE = DOMAIN + "_location_update_{}"
|
||||||
|
|
||||||
|
ATTR_CAMERA_ENTITY_ID = "camera_entity_id"
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/mobile_app",
|
"documentation": "https://www.home-assistant.io/integrations/mobile_app",
|
||||||
"requirements": ["PyNaCl==1.3.0"],
|
"requirements": ["PyNaCl==1.3.0"],
|
||||||
"dependencies": ["http", "webhook", "person"],
|
"dependencies": ["http", "webhook", "person"],
|
||||||
"after_dependencies": ["cloud"],
|
"after_dependencies": ["cloud", "camera"],
|
||||||
"codeowners": ["@robbiet480"],
|
"codeowners": ["@robbiet480"],
|
||||||
"quality_scale": "internal"
|
"quality_scale": "internal"
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,7 @@ import voluptuous as vol
|
|||||||
from homeassistant.components.binary_sensor import (
|
from homeassistant.components.binary_sensor import (
|
||||||
DEVICE_CLASSES as BINARY_SENSOR_CLASSES,
|
DEVICE_CLASSES as BINARY_SENSOR_CLASSES,
|
||||||
)
|
)
|
||||||
|
from homeassistant.components.camera import SUPPORT_STREAM as CAMERA_SUPPORT_STREAM
|
||||||
from homeassistant.components.device_tracker import (
|
from homeassistant.components.device_tracker import (
|
||||||
ATTR_BATTERY,
|
ATTR_BATTERY,
|
||||||
ATTR_GPS,
|
ATTR_GPS,
|
||||||
@ -29,7 +30,7 @@ from homeassistant.const import (
|
|||||||
HTTP_CREATED,
|
HTTP_CREATED,
|
||||||
)
|
)
|
||||||
from homeassistant.core import EventOrigin
|
from homeassistant.core import EventOrigin
|
||||||
from homeassistant.exceptions import ServiceNotFound, TemplateError
|
from homeassistant.exceptions import HomeAssistantError, ServiceNotFound, TemplateError
|
||||||
from homeassistant.helpers import config_validation as cv, device_registry as dr
|
from homeassistant.helpers import config_validation as cv, device_registry as dr
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||||
from homeassistant.helpers.template import attach
|
from homeassistant.helpers.template import attach
|
||||||
@ -40,6 +41,7 @@ from .const import (
|
|||||||
ATTR_ALTITUDE,
|
ATTR_ALTITUDE,
|
||||||
ATTR_APP_DATA,
|
ATTR_APP_DATA,
|
||||||
ATTR_APP_VERSION,
|
ATTR_APP_VERSION,
|
||||||
|
ATTR_CAMERA_ENTITY_ID,
|
||||||
ATTR_COURSE,
|
ATTR_COURSE,
|
||||||
ATTR_DEVICE_ID,
|
ATTR_DEVICE_ID,
|
||||||
ATTR_DEVICE_NAME,
|
ATTR_DEVICE_NAME,
|
||||||
@ -240,6 +242,32 @@ async def webhook_fire_event(hass, config_entry, data):
|
|||||||
return empty_okay_response()
|
return empty_okay_response()
|
||||||
|
|
||||||
|
|
||||||
|
@WEBHOOK_COMMANDS.register("stream_camera")
|
||||||
|
@validate_schema({vol.Required(ATTR_CAMERA_ENTITY_ID): cv.string})
|
||||||
|
async def webhook_stream_camera(hass, config_entry, data):
|
||||||
|
"""Handle a request to HLS-stream a camera."""
|
||||||
|
camera = hass.states.get(data[ATTR_CAMERA_ENTITY_ID])
|
||||||
|
|
||||||
|
if camera is None:
|
||||||
|
return webhook_response(
|
||||||
|
{"success": False}, registration=config_entry.data, status=HTTP_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
resp = {"mjpeg_path": "/api/camera_proxy_stream/%s" % (camera.entity_id)}
|
||||||
|
|
||||||
|
if camera.attributes["supported_features"] & CAMERA_SUPPORT_STREAM:
|
||||||
|
try:
|
||||||
|
resp["hls_path"] = await hass.components.camera.async_request_stream(
|
||||||
|
camera.entity_id, "hls"
|
||||||
|
)
|
||||||
|
except HomeAssistantError:
|
||||||
|
resp["hls_path"] = None
|
||||||
|
else:
|
||||||
|
resp["hls_path"] = None
|
||||||
|
|
||||||
|
return webhook_response(resp, registration=config_entry.data)
|
||||||
|
|
||||||
|
|
||||||
@WEBHOOK_COMMANDS.register("render_template")
|
@WEBHOOK_COMMANDS.register("render_template")
|
||||||
@validate_schema(
|
@validate_schema(
|
||||||
{
|
{
|
||||||
|
@ -3,14 +3,17 @@ import logging
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from homeassistant.components.camera import SUPPORT_STREAM as CAMERA_SUPPORT_STREAM
|
||||||
from homeassistant.components.mobile_app.const import CONF_SECRET
|
from homeassistant.components.mobile_app.const import CONF_SECRET
|
||||||
from homeassistant.components.zone import DOMAIN as ZONE_DOMAIN
|
from homeassistant.components.zone import DOMAIN as ZONE_DOMAIN
|
||||||
from homeassistant.const import CONF_WEBHOOK_ID
|
from homeassistant.const import CONF_WEBHOOK_ID
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.setup import async_setup_component
|
from homeassistant.setup import async_setup_component
|
||||||
|
|
||||||
from .const import CALL_SERVICE, FIRE_EVENT, REGISTER_CLEARTEXT, RENDER_TEMPLATE, UPDATE
|
from .const import CALL_SERVICE, FIRE_EVENT, REGISTER_CLEARTEXT, RENDER_TEMPLATE, UPDATE
|
||||||
|
|
||||||
|
from tests.async_mock import patch
|
||||||
from tests.common import async_mock_service
|
from tests.common import async_mock_service
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
@ -303,3 +306,103 @@ async def test_webhook_enable_encryption(hass, webhook_client, create_registrati
|
|||||||
decrypted_data = decrypt_payload(key, enc_json["encrypted_data"])
|
decrypted_data = decrypt_payload(key, enc_json["encrypted_data"])
|
||||||
|
|
||||||
assert decrypted_data == {"one": "Hello world"}
|
assert decrypted_data == {"one": "Hello world"}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_webhook_camera_stream_non_existent(
|
||||||
|
hass, create_registrations, webhook_client
|
||||||
|
):
|
||||||
|
"""Test fetching camera stream URLs for a non-existent camera."""
|
||||||
|
webhook_id = create_registrations[1]["webhook_id"]
|
||||||
|
|
||||||
|
resp = await webhook_client.post(
|
||||||
|
f"/api/webhook/{webhook_id}",
|
||||||
|
json={
|
||||||
|
"type": "stream_camera",
|
||||||
|
"data": {"camera_entity_id": "camera.doesnt_exist"},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert resp.status == 400
|
||||||
|
webhook_json = await resp.json()
|
||||||
|
assert webhook_json["success"] is False
|
||||||
|
|
||||||
|
|
||||||
|
async def test_webhook_camera_stream_non_hls(
|
||||||
|
hass, create_registrations, webhook_client
|
||||||
|
):
|
||||||
|
"""Test fetching camera stream URLs for a non-HLS/stream-supporting camera."""
|
||||||
|
hass.states.async_set("camera.non_stream_camera", "idle", {"supported_features": 0})
|
||||||
|
|
||||||
|
webhook_id = create_registrations[1]["webhook_id"]
|
||||||
|
|
||||||
|
resp = await webhook_client.post(
|
||||||
|
f"/api/webhook/{webhook_id}",
|
||||||
|
json={
|
||||||
|
"type": "stream_camera",
|
||||||
|
"data": {"camera_entity_id": "camera.non_stream_camera"},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert resp.status == 200
|
||||||
|
webhook_json = await resp.json()
|
||||||
|
assert webhook_json["hls_path"] is None
|
||||||
|
assert (
|
||||||
|
webhook_json["mjpeg_path"]
|
||||||
|
== "/api/camera_proxy_stream/camera.non_stream_camera"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_webhook_camera_stream_stream_available(
|
||||||
|
hass, create_registrations, webhook_client
|
||||||
|
):
|
||||||
|
"""Test fetching camera stream URLs for an HLS/stream-supporting camera."""
|
||||||
|
hass.states.async_set(
|
||||||
|
"camera.stream_camera", "idle", {"supported_features": CAMERA_SUPPORT_STREAM}
|
||||||
|
)
|
||||||
|
|
||||||
|
webhook_id = create_registrations[1]["webhook_id"]
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.camera.async_request_stream",
|
||||||
|
return_value="/api/streams/some_hls_stream",
|
||||||
|
):
|
||||||
|
resp = await webhook_client.post(
|
||||||
|
f"/api/webhook/{webhook_id}",
|
||||||
|
json={
|
||||||
|
"type": "stream_camera",
|
||||||
|
"data": {"camera_entity_id": "camera.stream_camera"},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert resp.status == 200
|
||||||
|
webhook_json = await resp.json()
|
||||||
|
assert webhook_json["hls_path"] == "/api/streams/some_hls_stream"
|
||||||
|
assert webhook_json["mjpeg_path"] == "/api/camera_proxy_stream/camera.stream_camera"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_webhook_camera_stream_stream_available_but_errors(
|
||||||
|
hass, create_registrations, webhook_client
|
||||||
|
):
|
||||||
|
"""Test fetching camera stream URLs for an HLS/stream-supporting camera but that streaming errors."""
|
||||||
|
hass.states.async_set(
|
||||||
|
"camera.stream_camera", "idle", {"supported_features": CAMERA_SUPPORT_STREAM}
|
||||||
|
)
|
||||||
|
|
||||||
|
webhook_id = create_registrations[1]["webhook_id"]
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.camera.async_request_stream",
|
||||||
|
side_effect=HomeAssistantError(),
|
||||||
|
):
|
||||||
|
resp = await webhook_client.post(
|
||||||
|
f"/api/webhook/{webhook_id}",
|
||||||
|
json={
|
||||||
|
"type": "stream_camera",
|
||||||
|
"data": {"camera_entity_id": "camera.stream_camera"},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert resp.status == 200
|
||||||
|
webhook_json = await resp.json()
|
||||||
|
assert webhook_json["hls_path"] is None
|
||||||
|
assert webhook_json["mjpeg_path"] == "/api/camera_proxy_stream/camera.stream_camera"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user