Implement Alexa.CameraStreamController in Alexa (#32772)
* Implement Alexa.CameraStreamController.
* Add dependencies.
* Add camera helpers for image url, and mjpeg url.
* Remove unsupported AlexaPowerController from cameras.
* Refactor camera_stream_url to hass_url
* Declare HLS instead of RTSP.
* Add test for async_get_image_url() and async_get_mpeg_stream_url().
* Sort imports.
* Fix camera.options to camera.stream_options. (#32767)
(cherry picked from commit 9af95e8577
)
* Remove URL configuration option for AlexaCameraStreamController.
* Update test_initialize_camera_stream.
* Optimize camera stream configuration.
* Update Tests for optimized camera stream configuration.
* Sort imports.
* Add check for Stream integration.
* Checks and Balances.
* Remove unnecessary camera helpers.
* Return None instead of empty list for camera_stream_configurations().
This commit is contained in:
parent
369ffe2288
commit
28a2c9c653
7 changed files with 254 additions and 16 deletions
|
@ -98,11 +98,7 @@ async def async_setup(hass, config):
|
||||||
f"send command {data['request']['namespace']}/{data['request']['name']}"
|
f"send command {data['request']['namespace']}/{data['request']['name']}"
|
||||||
)
|
)
|
||||||
|
|
||||||
return {
|
return {"name": "Amazon Alexa", "message": message, "entity_id": entity_id}
|
||||||
"name": "Amazon Alexa",
|
|
||||||
"message": message,
|
|
||||||
"entity_id": entity_id,
|
|
||||||
}
|
|
||||||
|
|
||||||
hass.components.logbook.async_describe_event(
|
hass.components.logbook.async_describe_event(
|
||||||
DOMAIN, EVENT_ALEXA_SMART_HOME, async_describe_logbook_event
|
DOMAIN, EVENT_ALEXA_SMART_HOME, async_describe_logbook_event
|
||||||
|
|
|
@ -169,6 +169,11 @@ class AlexaCapability:
|
||||||
"""Return the supportedOperations object."""
|
"""Return the supportedOperations object."""
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def camera_stream_configurations():
|
||||||
|
"""Applicable only to CameraStreamController."""
|
||||||
|
return None
|
||||||
|
|
||||||
def serialize_discovery(self):
|
def serialize_discovery(self):
|
||||||
"""Serialize according to the Discovery API."""
|
"""Serialize according to the Discovery API."""
|
||||||
result = {"type": "AlexaInterface", "interface": self.name(), "version": "3"}
|
result = {"type": "AlexaInterface", "interface": self.name(), "version": "3"}
|
||||||
|
@ -222,6 +227,10 @@ class AlexaCapability:
|
||||||
if inputs:
|
if inputs:
|
||||||
result["inputs"] = inputs
|
result["inputs"] = inputs
|
||||||
|
|
||||||
|
camera_stream_configurations = self.camera_stream_configurations()
|
||||||
|
if camera_stream_configurations:
|
||||||
|
result["cameraStreamConfigurations"] = camera_stream_configurations
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def serialize_properties(self):
|
def serialize_properties(self):
|
||||||
|
@ -1854,3 +1863,40 @@ class AlexaTimeHoldController(AlexaCapability):
|
||||||
When false, Alexa does not send the Resume directive.
|
When false, Alexa does not send the Resume directive.
|
||||||
"""
|
"""
|
||||||
return {"allowRemoteResume": self._allow_remote_resume}
|
return {"allowRemoteResume": self._allow_remote_resume}
|
||||||
|
|
||||||
|
|
||||||
|
class AlexaCameraStreamController(AlexaCapability):
|
||||||
|
"""Implements Alexa.CameraStreamController.
|
||||||
|
|
||||||
|
https://developer.amazon.com/docs/device-apis/alexa-camerastreamcontroller.html
|
||||||
|
"""
|
||||||
|
|
||||||
|
supported_locales = {
|
||||||
|
"de-DE",
|
||||||
|
"en-AU",
|
||||||
|
"en-CA",
|
||||||
|
"en-GB",
|
||||||
|
"en-IN",
|
||||||
|
"en-US",
|
||||||
|
"es-ES",
|
||||||
|
"fr-FR",
|
||||||
|
"it-IT",
|
||||||
|
"ja-JP",
|
||||||
|
}
|
||||||
|
|
||||||
|
def name(self):
|
||||||
|
"""Return the Alexa API name of this interface."""
|
||||||
|
return "Alexa.CameraStreamController"
|
||||||
|
|
||||||
|
def camera_stream_configurations(self):
|
||||||
|
"""Return cameraStreamConfigurations object."""
|
||||||
|
camera_stream_configurations = [
|
||||||
|
{
|
||||||
|
"protocols": ["HLS"],
|
||||||
|
"resolutions": [{"width": 1280, "height": 720}],
|
||||||
|
"authorizationTypes": ["NONE"],
|
||||||
|
"videoCodecs": ["H264"],
|
||||||
|
"audioCodecs": ["AAC"],
|
||||||
|
}
|
||||||
|
]
|
||||||
|
return camera_stream_configurations
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
"""Alexa entity adapters."""
|
"""Alexa entity adapters."""
|
||||||
|
import logging
|
||||||
from typing import List
|
from typing import List
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
from homeassistant.components import (
|
from homeassistant.components import (
|
||||||
alarm_control_panel,
|
alarm_control_panel,
|
||||||
alert,
|
alert,
|
||||||
automation,
|
automation,
|
||||||
binary_sensor,
|
binary_sensor,
|
||||||
|
camera,
|
||||||
cover,
|
cover,
|
||||||
fan,
|
fan,
|
||||||
group,
|
group,
|
||||||
|
@ -33,11 +36,13 @@ from homeassistant.const import (
|
||||||
TEMP_FAHRENHEIT,
|
TEMP_FAHRENHEIT,
|
||||||
)
|
)
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
|
from homeassistant.helpers import network
|
||||||
from homeassistant.util.decorator import Registry
|
from homeassistant.util.decorator import Registry
|
||||||
|
|
||||||
from .capabilities import (
|
from .capabilities import (
|
||||||
Alexa,
|
Alexa,
|
||||||
AlexaBrightnessController,
|
AlexaBrightnessController,
|
||||||
|
AlexaCameraStreamController,
|
||||||
AlexaChannelController,
|
AlexaChannelController,
|
||||||
AlexaColorController,
|
AlexaColorController,
|
||||||
AlexaColorTemperatureController,
|
AlexaColorTemperatureController,
|
||||||
|
@ -68,6 +73,8 @@ from .capabilities import (
|
||||||
)
|
)
|
||||||
from .const import CONF_DESCRIPTION, CONF_DISPLAY_CATEGORIES
|
from .const import CONF_DESCRIPTION, CONF_DISPLAY_CATEGORIES
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
ENTITY_ADAPTERS = Registry()
|
ENTITY_ADAPTERS = Registry()
|
||||||
|
|
||||||
TRANSLATION_TABLE = dict.fromkeys(map(ord, r"}{\/|\"()[]+~!><*%"), None)
|
TRANSLATION_TABLE = dict.fromkeys(map(ord, r"}{\/|\"()[]+~!><*%"), None)
|
||||||
|
@ -763,3 +770,41 @@ class VacuumCapabilities(AlexaEntity):
|
||||||
|
|
||||||
yield AlexaEndpointHealth(self.hass, self.entity)
|
yield AlexaEndpointHealth(self.hass, self.entity)
|
||||||
yield Alexa(self.hass)
|
yield Alexa(self.hass)
|
||||||
|
|
||||||
|
|
||||||
|
@ENTITY_ADAPTERS.register(camera.DOMAIN)
|
||||||
|
class CameraCapabilities(AlexaEntity):
|
||||||
|
"""Class to represent Camera capabilities."""
|
||||||
|
|
||||||
|
def default_display_categories(self):
|
||||||
|
"""Return the display categories for this entity."""
|
||||||
|
return [DisplayCategory.CAMERA]
|
||||||
|
|
||||||
|
def interfaces(self):
|
||||||
|
"""Yield the supported interfaces."""
|
||||||
|
if self._check_requirements():
|
||||||
|
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||||
|
if supported & camera.SUPPORT_STREAM:
|
||||||
|
yield AlexaCameraStreamController(self.entity)
|
||||||
|
|
||||||
|
yield AlexaEndpointHealth(self.hass, self.entity)
|
||||||
|
yield Alexa(self.hass)
|
||||||
|
|
||||||
|
def _check_requirements(self):
|
||||||
|
"""Check the hass URL for HTTPS scheme and port 443."""
|
||||||
|
if "stream" not in self.hass.config.components:
|
||||||
|
_LOGGER.error(
|
||||||
|
"%s requires stream component for AlexaCameraStreamController",
|
||||||
|
self.entity_id,
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
url = urlparse(network.async_get_external_url(self.hass))
|
||||||
|
if url.scheme != "https" or (url.port is not None and url.port != 443):
|
||||||
|
_LOGGER.error(
|
||||||
|
"%s requires HTTPS support on port 443 for AlexaCameraStreamController",
|
||||||
|
self.entity_id,
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
|
@ -4,6 +4,7 @@ import math
|
||||||
|
|
||||||
from homeassistant import core as ha
|
from homeassistant import core as ha
|
||||||
from homeassistant.components import (
|
from homeassistant.components import (
|
||||||
|
camera,
|
||||||
cover,
|
cover,
|
||||||
fan,
|
fan,
|
||||||
group,
|
group,
|
||||||
|
@ -41,6 +42,7 @@ from homeassistant.const import (
|
||||||
TEMP_CELSIUS,
|
TEMP_CELSIUS,
|
||||||
TEMP_FAHRENHEIT,
|
TEMP_FAHRENHEIT,
|
||||||
)
|
)
|
||||||
|
from homeassistant.helpers import network
|
||||||
import homeassistant.util.color as color_util
|
import homeassistant.util.color as color_util
|
||||||
from homeassistant.util.decorator import Registry
|
from homeassistant.util.decorator import Registry
|
||||||
import homeassistant.util.dt as dt_util
|
import homeassistant.util.dt as dt_util
|
||||||
|
@ -1523,3 +1525,28 @@ async def async_api_resume(hass, config, directive, context):
|
||||||
)
|
)
|
||||||
|
|
||||||
return directive.response()
|
return directive.response()
|
||||||
|
|
||||||
|
|
||||||
|
@HANDLERS.register(("Alexa.CameraStreamController", "InitializeCameraStreams"))
|
||||||
|
async def async_api_initialize_camera_stream(hass, config, directive, context):
|
||||||
|
"""Process a InitializeCameraStreams request."""
|
||||||
|
entity = directive.entity
|
||||||
|
stream_source = await camera.async_request_stream(hass, entity.entity_id, fmt="hls")
|
||||||
|
camera_image = hass.states.get(entity.entity_id).attributes["entity_picture"]
|
||||||
|
external_url = network.async_get_external_url(hass)
|
||||||
|
payload = {
|
||||||
|
"cameraStreams": [
|
||||||
|
{
|
||||||
|
"uri": f"{external_url}{stream_source}",
|
||||||
|
"protocol": "HLS",
|
||||||
|
"resolution": {"width": 1280, "height": 720},
|
||||||
|
"authorizationType": "NONE",
|
||||||
|
"videoCodec": "H264",
|
||||||
|
"audioCodec": "AAC",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"imageUri": f"{external_url}{camera_image}",
|
||||||
|
}
|
||||||
|
return directive.response(
|
||||||
|
name="Response", namespace="Alexa.CameraStreamController", payload=payload
|
||||||
|
)
|
||||||
|
|
|
@ -4,6 +4,6 @@
|
||||||
"documentation": "https://www.home-assistant.io/integrations/alexa",
|
"documentation": "https://www.home-assistant.io/integrations/alexa",
|
||||||
"requirements": [],
|
"requirements": [],
|
||||||
"dependencies": ["http"],
|
"dependencies": ["http"],
|
||||||
"after_dependencies": ["logbook"],
|
"after_dependencies": ["logbook", "camera"],
|
||||||
"codeowners": ["@home-assistant/cloud", "@ochlocracy"]
|
"codeowners": ["@home-assistant/cloud", "@ochlocracy"]
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,7 @@ class MockConfig(config.AbstractConfig):
|
||||||
"binary_sensor.test_contact_forced": {"display_categories": "CONTACT_SENSOR"},
|
"binary_sensor.test_contact_forced": {"display_categories": "CONTACT_SENSOR"},
|
||||||
"binary_sensor.test_motion_forced": {"display_categories": "MOTION_SENSOR"},
|
"binary_sensor.test_motion_forced": {"display_categories": "MOTION_SENSOR"},
|
||||||
"binary_sensor.test_motion_camera_event": {"display_categories": "CAMERA"},
|
"binary_sensor.test_motion_camera_event": {"display_categories": "CAMERA"},
|
||||||
|
"camera.test": {"display_categories": "CAMERA"},
|
||||||
}
|
}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
"""Test for smart home alexa support."""
|
"""Test for smart home alexa support."""
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from homeassistant.components.alexa import messages, smart_home
|
from homeassistant.components.alexa import messages, smart_home
|
||||||
|
import homeassistant.components.camera as camera
|
||||||
from homeassistant.components.media_player.const import (
|
from homeassistant.components.media_player.const import (
|
||||||
SUPPORT_NEXT_TRACK,
|
SUPPORT_NEXT_TRACK,
|
||||||
SUPPORT_PAUSE,
|
SUPPORT_PAUSE,
|
||||||
|
@ -22,6 +25,7 @@ import homeassistant.components.vacuum as vacuum
|
||||||
from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT
|
from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT
|
||||||
from homeassistant.core import Context, callback
|
from homeassistant.core import Context, callback
|
||||||
from homeassistant.helpers import entityfilter
|
from homeassistant.helpers import entityfilter
|
||||||
|
from homeassistant.setup import async_setup_component
|
||||||
|
|
||||||
from . import (
|
from . import (
|
||||||
DEFAULT_CONFIG,
|
DEFAULT_CONFIG,
|
||||||
|
@ -35,7 +39,7 @@ from . import (
|
||||||
reported_properties,
|
reported_properties,
|
||||||
)
|
)
|
||||||
|
|
||||||
from tests.common import async_mock_service
|
from tests.common import async_mock_service, mock_coro
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
@ -48,6 +52,22 @@ def events(hass):
|
||||||
yield events
|
yield events
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_camera(hass):
|
||||||
|
"""Initialize a demo camera platform."""
|
||||||
|
assert hass.loop.run_until_complete(
|
||||||
|
async_setup_component(hass, "camera", {camera.DOMAIN: {"platform": "demo"}})
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_stream(hass):
|
||||||
|
"""Initialize a demo camera platform with streaming."""
|
||||||
|
assert hass.loop.run_until_complete(
|
||||||
|
async_setup_component(hass, "stream", {"stream": {}})
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_create_api_message_defaults(hass):
|
def test_create_api_message_defaults(hass):
|
||||||
"""Create a API message response of a request with defaults."""
|
"""Create a API message response of a request with defaults."""
|
||||||
request = get_new_request("Alexa.PowerController", "TurnOn", "switch#xy")
|
request = get_new_request("Alexa.PowerController", "TurnOn", "switch#xy")
|
||||||
|
@ -3445,11 +3465,11 @@ async def test_vacuum_discovery(hass):
|
||||||
properties.assert_equal("Alexa.PowerController", "powerState", "OFF")
|
properties.assert_equal("Alexa.PowerController", "powerState", "OFF")
|
||||||
|
|
||||||
await assert_request_calls_service(
|
await assert_request_calls_service(
|
||||||
"Alexa.PowerController", "TurnOn", "vacuum#test_1", "vacuum.turn_on", hass,
|
"Alexa.PowerController", "TurnOn", "vacuum#test_1", "vacuum.turn_on", hass
|
||||||
)
|
)
|
||||||
|
|
||||||
await assert_request_calls_service(
|
await assert_request_calls_service(
|
||||||
"Alexa.PowerController", "TurnOff", "vacuum#test_1", "vacuum.turn_off", hass,
|
"Alexa.PowerController", "TurnOff", "vacuum#test_1", "vacuum.turn_off", hass
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -3663,18 +3683,18 @@ async def test_vacuum_discovery_no_turn_on(hass):
|
||||||
appliance = await discovery_test(device, hass)
|
appliance = await discovery_test(device, hass)
|
||||||
|
|
||||||
assert_endpoint_capabilities(
|
assert_endpoint_capabilities(
|
||||||
appliance, "Alexa.PowerController", "Alexa.EndpointHealth", "Alexa",
|
appliance, "Alexa.PowerController", "Alexa.EndpointHealth", "Alexa"
|
||||||
)
|
)
|
||||||
|
|
||||||
properties = await reported_properties(hass, "vacuum#test_5")
|
properties = await reported_properties(hass, "vacuum#test_5")
|
||||||
properties.assert_equal("Alexa.PowerController", "powerState", "ON")
|
properties.assert_equal("Alexa.PowerController", "powerState", "ON")
|
||||||
|
|
||||||
await assert_request_calls_service(
|
await assert_request_calls_service(
|
||||||
"Alexa.PowerController", "TurnOn", "vacuum#test_5", "vacuum.start", hass,
|
"Alexa.PowerController", "TurnOn", "vacuum#test_5", "vacuum.start", hass
|
||||||
)
|
)
|
||||||
|
|
||||||
await assert_request_calls_service(
|
await assert_request_calls_service(
|
||||||
"Alexa.PowerController", "TurnOff", "vacuum#test_5", "vacuum.turn_off", hass,
|
"Alexa.PowerController", "TurnOff", "vacuum#test_5", "vacuum.turn_off", hass
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -3693,11 +3713,11 @@ async def test_vacuum_discovery_no_turn_off(hass):
|
||||||
appliance = await discovery_test(device, hass)
|
appliance = await discovery_test(device, hass)
|
||||||
|
|
||||||
assert_endpoint_capabilities(
|
assert_endpoint_capabilities(
|
||||||
appliance, "Alexa.PowerController", "Alexa.EndpointHealth", "Alexa",
|
appliance, "Alexa.PowerController", "Alexa.EndpointHealth", "Alexa"
|
||||||
)
|
)
|
||||||
|
|
||||||
await assert_request_calls_service(
|
await assert_request_calls_service(
|
||||||
"Alexa.PowerController", "TurnOn", "vacuum#test_6", "vacuum.turn_on", hass,
|
"Alexa.PowerController", "TurnOn", "vacuum#test_6", "vacuum.turn_on", hass
|
||||||
)
|
)
|
||||||
|
|
||||||
await assert_request_calls_service(
|
await assert_request_calls_service(
|
||||||
|
@ -3722,11 +3742,11 @@ async def test_vacuum_discovery_no_turn_on_or_off(hass):
|
||||||
appliance = await discovery_test(device, hass)
|
appliance = await discovery_test(device, hass)
|
||||||
|
|
||||||
assert_endpoint_capabilities(
|
assert_endpoint_capabilities(
|
||||||
appliance, "Alexa.PowerController", "Alexa.EndpointHealth", "Alexa",
|
appliance, "Alexa.PowerController", "Alexa.EndpointHealth", "Alexa"
|
||||||
)
|
)
|
||||||
|
|
||||||
await assert_request_calls_service(
|
await assert_request_calls_service(
|
||||||
"Alexa.PowerController", "TurnOn", "vacuum#test_7", "vacuum.start", hass,
|
"Alexa.PowerController", "TurnOn", "vacuum#test_7", "vacuum.start", hass
|
||||||
)
|
)
|
||||||
|
|
||||||
await assert_request_calls_service(
|
await assert_request_calls_service(
|
||||||
|
@ -3736,3 +3756,106 @@ async def test_vacuum_discovery_no_turn_on_or_off(hass):
|
||||||
"vacuum.return_to_base",
|
"vacuum.return_to_base",
|
||||||
hass,
|
hass,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_camera_discovery(hass, mock_stream):
|
||||||
|
"""Test camera discovery."""
|
||||||
|
device = (
|
||||||
|
"camera.test",
|
||||||
|
"idle",
|
||||||
|
{"friendly_name": "Test camera", "supported_features": 3},
|
||||||
|
)
|
||||||
|
with patch(
|
||||||
|
"homeassistant.helpers.network.async_get_external_url",
|
||||||
|
return_value="https://example.nabu.casa",
|
||||||
|
):
|
||||||
|
appliance = await discovery_test(device, hass)
|
||||||
|
|
||||||
|
capabilities = assert_endpoint_capabilities(
|
||||||
|
appliance, "Alexa.CameraStreamController", "Alexa.EndpointHealth", "Alexa"
|
||||||
|
)
|
||||||
|
|
||||||
|
camera_stream_capability = get_capability(
|
||||||
|
capabilities, "Alexa.CameraStreamController"
|
||||||
|
)
|
||||||
|
configuration = camera_stream_capability["cameraStreamConfigurations"][0]
|
||||||
|
assert "HLS" in configuration["protocols"]
|
||||||
|
assert {"width": 1280, "height": 720} in configuration["resolutions"]
|
||||||
|
assert "NONE" in configuration["authorizationTypes"]
|
||||||
|
assert "H264" in configuration["videoCodecs"]
|
||||||
|
assert "AAC" in configuration["audioCodecs"]
|
||||||
|
|
||||||
|
|
||||||
|
async def test_camera_discovery_without_stream(hass):
|
||||||
|
"""Test camera discovery without stream integration."""
|
||||||
|
device = (
|
||||||
|
"camera.test",
|
||||||
|
"idle",
|
||||||
|
{"friendly_name": "Test camera", "supported_features": 3},
|
||||||
|
)
|
||||||
|
with patch(
|
||||||
|
"homeassistant.helpers.network.async_get_external_url",
|
||||||
|
return_value="https://example.nabu.casa",
|
||||||
|
):
|
||||||
|
appliance = await discovery_test(device, hass)
|
||||||
|
# assert Alexa.CameraStreamController is not yielded.
|
||||||
|
assert_endpoint_capabilities(appliance, "Alexa.EndpointHealth", "Alexa")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"url,result",
|
||||||
|
[
|
||||||
|
("http://nohttpswrongport.org:8123", 2),
|
||||||
|
("https://httpswrongport.org:8123", 2),
|
||||||
|
("http://nohttpsport443.org:443", 2),
|
||||||
|
("tls://nohttpsport443.org:443", 2),
|
||||||
|
("https://correctschemaandport.org:443", 3),
|
||||||
|
("https://correctschemaandport.org", 3),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_camera_hass_urls(hass, mock_stream, url, result):
|
||||||
|
"""Test camera discovery with unsupported urls."""
|
||||||
|
device = (
|
||||||
|
"camera.test",
|
||||||
|
"idle",
|
||||||
|
{"friendly_name": "Test camera", "supported_features": 3},
|
||||||
|
)
|
||||||
|
with patch(
|
||||||
|
"homeassistant.helpers.network.async_get_external_url", return_value=url
|
||||||
|
):
|
||||||
|
appliance = await discovery_test(device, hass)
|
||||||
|
assert len(appliance["capabilities"]) == result
|
||||||
|
|
||||||
|
|
||||||
|
async def test_initialize_camera_stream(hass, mock_camera, mock_stream):
|
||||||
|
"""Test InitializeCameraStreams handler."""
|
||||||
|
request = get_new_request(
|
||||||
|
"Alexa.CameraStreamController", "InitializeCameraStreams", "camera#demo_camera"
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.demo.camera.DemoCamera.stream_source",
|
||||||
|
return_value=mock_coro("rtsp://example.local"),
|
||||||
|
), patch(
|
||||||
|
"homeassistant.helpers.network.async_get_external_url",
|
||||||
|
return_value="https://mycamerastream.test",
|
||||||
|
):
|
||||||
|
msg = await smart_home.async_handle_message(hass, DEFAULT_CONFIG, request)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert "event" in msg
|
||||||
|
response = msg["event"]
|
||||||
|
assert response["header"]["namespace"] == "Alexa.CameraStreamController"
|
||||||
|
assert response["header"]["name"] == "Response"
|
||||||
|
camera_streams = response["payload"]["cameraStreams"]
|
||||||
|
assert "https://mycamerastream.test/api/hls/" in camera_streams[0]["uri"]
|
||||||
|
assert camera_streams[0]["protocol"] == "HLS"
|
||||||
|
assert camera_streams[0]["resolution"]["width"] == 1280
|
||||||
|
assert camera_streams[0]["resolution"]["height"] == 720
|
||||||
|
assert camera_streams[0]["authorizationType"] == "NONE"
|
||||||
|
assert camera_streams[0]["videoCodec"] == "H264"
|
||||||
|
assert camera_streams[0]["audioCodec"] == "AAC"
|
||||||
|
assert (
|
||||||
|
"https://mycamerastream.test/api/camera_proxy/camera.demo_camera?token="
|
||||||
|
in response["payload"]["imageUri"]
|
||||||
|
)
|
||||||
|
|
Loading…
Add table
Reference in a new issue