From 371aa03bcab77736a4a98c1188c0a906443bf9f6 Mon Sep 17 00:00:00 2001 From: Greg Thornton Date: Sat, 11 Sep 2021 21:41:30 -0500 Subject: [PATCH] Add audio support option to HomeKit camera UI config flow (#56107) --- .../components/homekit/config_flow.py | 16 +++ homeassistant/components/homekit/strings.json | 5 +- .../components/homekit/translations/en.json | 3 +- tests/components/homekit/test_config_flow.py | 135 +++++++++++++++++- 4 files changed, 155 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/homekit/config_flow.py b/homeassistant/components/homekit/config_flow.py index 81f439c8954..79fc43fde3a 100644 --- a/homeassistant/components/homekit/config_flow.py +++ b/homeassistant/components/homekit/config_flow.py @@ -41,6 +41,7 @@ from .const import ( CONF_EXCLUDE_ACCESSORY_MODE, CONF_FILTER, CONF_HOMEKIT_MODE, + CONF_SUPPORT_AUDIO, CONF_VIDEO_CODEC, DEFAULT_AUTO_START, DEFAULT_CONFIG_FLOW_PORT, @@ -54,6 +55,7 @@ from .const import ( ) from .util import async_find_next_available_port, state_needs_accessory_mode +CONF_CAMERA_AUDIO = "camera_audio" CONF_CAMERA_COPY = "camera_copy" CONF_INCLUDE_EXCLUDE_MODE = "include_exclude_mode" @@ -362,14 +364,24 @@ class OptionsFlowHandler(config_entries.OptionsFlow): and CONF_VIDEO_CODEC in entity_config[entity_id] ): del entity_config[entity_id][CONF_VIDEO_CODEC] + if entity_id in user_input[CONF_CAMERA_AUDIO]: + entity_config.setdefault(entity_id, {})[CONF_SUPPORT_AUDIO] = True + elif ( + entity_id in entity_config + and CONF_SUPPORT_AUDIO in entity_config[entity_id] + ): + del entity_config[entity_id][CONF_SUPPORT_AUDIO] return await self.async_step_advanced() + cameras_with_audio = [] cameras_with_copy = [] entity_config = self.hk_options.setdefault(CONF_ENTITY_CONFIG, {}) for entity in self.included_cameras: hk_entity_config = entity_config.get(entity, {}) if hk_entity_config.get(CONF_VIDEO_CODEC) == VIDEO_CODEC_COPY: cameras_with_copy.append(entity) + if hk_entity_config.get(CONF_SUPPORT_AUDIO): + cameras_with_audio.append(entity) data_schema = vol.Schema( { @@ -377,6 +389,10 @@ class OptionsFlowHandler(config_entries.OptionsFlow): CONF_CAMERA_COPY, default=cameras_with_copy, ): cv.multi_select(self.included_cameras), + vol.Optional( + CONF_CAMERA_AUDIO, + default=cameras_with_audio, + ): cv.multi_select(self.included_cameras), } ) return self.async_show_form(step_id="cameras", data_schema=data_schema) diff --git a/homeassistant/components/homekit/strings.json b/homeassistant/components/homekit/strings.json index 69cff3bfcc3..ede11aef19c 100644 --- a/homeassistant/components/homekit/strings.json +++ b/homeassistant/components/homekit/strings.json @@ -23,10 +23,11 @@ }, "cameras": { "data": { - "camera_copy": "Cameras that support native H.264 streams" + "camera_copy": "Cameras that support native H.264 streams", + "camera_audio": "Cameras that support audio" }, "description": "Check all cameras that support native H.264 streams. If the camera does not output a H.264 stream, the system will transcode the video to H.264 for HomeKit. Transcoding requires a performant CPU and is unlikely to work on single board computers.", - "title": "Select camera video codec." + "title": "Camera Configuration" }, "advanced": { "data": { diff --git a/homeassistant/components/homekit/translations/en.json b/homeassistant/components/homekit/translations/en.json index 564709cb9c1..b118ec16424 100644 --- a/homeassistant/components/homekit/translations/en.json +++ b/homeassistant/components/homekit/translations/en.json @@ -29,10 +29,11 @@ }, "cameras": { "data": { + "camera_audio": "Cameras that support audio", "camera_copy": "Cameras that support native H.264 streams" }, "description": "Check all cameras that support native H.264 streams. If the camera does not output a H.264 stream, the system will transcode the video to H.264 for HomeKit. Transcoding requires a performant CPU and is unlikely to work on single board computers.", - "title": "Select camera video codec." + "title": "Camera Configuration" }, "include_exclude": { "data": { diff --git a/tests/components/homekit/test_config_flow.py b/tests/components/homekit/test_config_flow.py index fadb4572df3..8d68b8aba73 100644 --- a/tests/components/homekit/test_config_flow.py +++ b/tests/components/homekit/test_config_flow.py @@ -753,7 +753,10 @@ async def test_options_flow_include_mode_with_cameras(hass, mock_get_source_ip): ) assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM assert result2["step_id"] == "cameras" - assert result2["data_schema"]({}) == {"camera_copy": ["camera.native_h264"]} + assert result2["data_schema"]({}) == { + "camera_copy": ["camera.native_h264"], + "camera_audio": [], + } schema = result2["data_schema"].schema assert _get_schema_default(schema, "camera_copy") == ["camera.native_h264"] @@ -776,6 +779,136 @@ async def test_options_flow_include_mode_with_cameras(hass, mock_get_source_ip): } +async def test_options_flow_with_camera_audio(hass, mock_get_source_ip): + """Test config flow options with cameras that support audio.""" + + config_entry = _mock_config_entry_with_options_populated() + config_entry.add_to_hass(hass) + + hass.states.async_set("climate.old", "off") + hass.states.async_set("camera.audio", "off") + hass.states.async_set("camera.no_audio", "off") + hass.states.async_set("camera.excluded", "off") + + await hass.async_block_till_done() + + result = await hass.config_entries.options.async_init( + config_entry.entry_id, context={"show_advanced_options": False} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={"domains": ["fan", "vacuum", "climate", "camera"]}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "include_exclude" + + result2 = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + "entities": ["camera.audio", "camera.no_audio"], + "include_exclude_mode": "include", + }, + ) + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["step_id"] == "cameras" + + result3 = await hass.config_entries.options.async_configure( + result2["flow_id"], + user_input={"camera_audio": ["camera.audio"]}, + ) + + assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert config_entry.options == { + "auto_start": True, + "mode": "bridge", + "filter": { + "exclude_domains": [], + "exclude_entities": [], + "include_domains": ["fan", "vacuum", "climate"], + "include_entities": ["camera.audio", "camera.no_audio"], + }, + "entity_config": {"camera.audio": {"support_audio": True}}, + } + + # Now run though again and verify we can turn off audio + + result = await hass.config_entries.options.async_init( + config_entry.entry_id, context={"show_advanced_options": False} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "init" + assert result["data_schema"]({}) == { + "domains": ["fan", "vacuum", "climate", "camera"], + "mode": "bridge", + } + schema = result["data_schema"].schema + assert _get_schema_default(schema, "domains") == [ + "fan", + "vacuum", + "climate", + "camera", + ] + assert _get_schema_default(schema, "mode") == "bridge" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={"domains": ["fan", "vacuum", "climate", "camera"]}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "include_exclude" + assert result["data_schema"]({}) == { + "entities": ["camera.audio", "camera.no_audio"], + "include_exclude_mode": "include", + } + schema = result["data_schema"].schema + assert _get_schema_default(schema, "entities") == [ + "camera.audio", + "camera.no_audio", + ] + assert _get_schema_default(schema, "include_exclude_mode") == "include" + + result2 = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + "entities": ["climate.old", "camera.excluded"], + "include_exclude_mode": "exclude", + }, + ) + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["step_id"] == "cameras" + assert result2["data_schema"]({}) == { + "camera_copy": [], + "camera_audio": ["camera.audio"], + } + schema = result2["data_schema"].schema + assert _get_schema_default(schema, "camera_audio") == ["camera.audio"] + + result3 = await hass.config_entries.options.async_configure( + result2["flow_id"], + user_input={"camera_audio": []}, + ) + + assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert config_entry.options == { + "auto_start": True, + "entity_config": {"camera.audio": {}}, + "filter": { + "exclude_domains": [], + "exclude_entities": ["climate.old", "camera.excluded"], + "include_domains": ["fan", "vacuum", "climate", "camera"], + "include_entities": [], + }, + "mode": "bridge", + } + + async def test_options_flow_blocked_when_from_yaml(hass, mock_get_source_ip): """Test config flow options."""