mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 05:07:41 +00:00
Add repair for UniFi Protect if RTSP is disabled on camera (#114088)
Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
parent
a5128c2148
commit
3e01085c91
@ -18,8 +18,10 @@ from pyunifiprotect.data import (
|
||||
from homeassistant.components.camera import Camera, CameraEntityFeature
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import issue_registry as ir
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity
|
||||
|
||||
from .const import (
|
||||
ATTR_BITRATE,
|
||||
@ -33,12 +35,40 @@ from .const import (
|
||||
)
|
||||
from .data import ProtectData
|
||||
from .entity import ProtectDeviceEntity
|
||||
from .utils import async_dispatch_id as _ufpd
|
||||
from .utils import async_dispatch_id as _ufpd, get_camera_base_name
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_camera_channels(
|
||||
@callback
|
||||
def _create_rtsp_repair(
|
||||
hass: HomeAssistant, entry: ConfigEntry, data: ProtectData, camera: UFPCamera
|
||||
) -> None:
|
||||
edit_key = "readonly"
|
||||
if camera.can_write(data.api.bootstrap.auth_user):
|
||||
edit_key = "writable"
|
||||
|
||||
translation_key = f"rtsp_disabled_{edit_key}"
|
||||
issue_key = f"rtsp_disabled_{camera.id}"
|
||||
|
||||
ir.async_create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
issue_key,
|
||||
is_fixable=True,
|
||||
is_persistent=False,
|
||||
learn_more_url="https://www.home-assistant.io/integrations/unifiprotect/#camera-streams",
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key=translation_key,
|
||||
translation_placeholders={"camera": camera.display_name},
|
||||
data={"entry_id": entry.entry_id, "camera_id": camera.id},
|
||||
)
|
||||
|
||||
|
||||
@callback
|
||||
def _get_camera_channels(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
data: ProtectData,
|
||||
ufp_device: UFPCamera | None = None,
|
||||
) -> Generator[tuple[UFPCamera, CameraChannel, bool], None, None]:
|
||||
@ -70,15 +100,23 @@ def get_camera_channels(
|
||||
|
||||
# no RTSP enabled use first channel with no stream
|
||||
if is_default:
|
||||
_create_rtsp_repair(hass, entry, data, camera)
|
||||
yield camera, camera.channels[0], True
|
||||
else:
|
||||
ir.async_delete_issue(hass, DOMAIN, f"rtsp_disabled_{camera.id}")
|
||||
|
||||
|
||||
def _async_camera_entities(
|
||||
data: ProtectData, ufp_device: UFPCamera | None = None
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
data: ProtectData,
|
||||
ufp_device: UFPCamera | None = None,
|
||||
) -> list[ProtectDeviceEntity]:
|
||||
disable_stream = data.disable_stream
|
||||
entities: list[ProtectDeviceEntity] = []
|
||||
for camera, channel, is_default in get_camera_channels(data, ufp_device):
|
||||
for camera, channel, is_default in _get_camera_channels(
|
||||
hass, entry, data, ufp_device
|
||||
):
|
||||
# do not enable streaming for package camera
|
||||
# 2 FPS causes a lot of buferring
|
||||
entities.append(
|
||||
@ -119,7 +157,7 @@ async def async_setup_entry(
|
||||
if not isinstance(device, UFPCamera):
|
||||
return # type: ignore[unreachable]
|
||||
|
||||
entities = _async_camera_entities(data, ufp_device=device)
|
||||
entities = _async_camera_entities(hass, entry, data, ufp_device=device)
|
||||
async_add_entities(entities)
|
||||
|
||||
entry.async_on_unload(
|
||||
@ -129,7 +167,7 @@ async def async_setup_entry(
|
||||
async_dispatcher_connect(hass, _ufpd(entry, DISPATCH_CHANNELS), _add_new_device)
|
||||
)
|
||||
|
||||
entities = _async_camera_entities(data)
|
||||
entities = _async_camera_entities(hass, entry, data)
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
@ -155,12 +193,13 @@ class ProtectCamera(ProtectDeviceEntity, Camera):
|
||||
super().__init__(data, camera)
|
||||
device = self.device
|
||||
|
||||
camera_name = get_camera_base_name(channel)
|
||||
if self._secure:
|
||||
self._attr_unique_id = f"{device.mac}_{channel.id}"
|
||||
self._attr_name = f"{device.display_name} {channel.name}"
|
||||
self._attr_name = f"{device.display_name} {camera_name}"
|
||||
else:
|
||||
self._attr_unique_id = f"{device.mac}_{channel.id}_insecure"
|
||||
self._attr_name = f"{device.display_name} {channel.name} Insecure"
|
||||
self._attr_name = f"{device.display_name} {camera_name} (Insecure)"
|
||||
# only the default (first) channel is enabled by default
|
||||
self._attr_entity_registry_enabled_default = is_default and secure
|
||||
|
||||
|
@ -2,10 +2,10 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import cast
|
||||
|
||||
from pyunifiprotect import ProtectApiClient
|
||||
from pyunifiprotect.data import Bootstrap, Camera, ModelType
|
||||
from pyunifiprotect.data.types import FirmwareReleaseChannel
|
||||
import voluptuous as vol
|
||||
|
||||
@ -18,8 +18,6 @@ from homeassistant.helpers.issue_registry import async_get as async_get_issue_re
|
||||
from .const import CONF_ALLOW_EA
|
||||
from .utils import async_create_api_client
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ProtectRepair(RepairsFlow):
|
||||
"""Handler for an issue fixing flow."""
|
||||
@ -27,7 +25,7 @@ class ProtectRepair(RepairsFlow):
|
||||
_api: ProtectApiClient
|
||||
_entry: ConfigEntry
|
||||
|
||||
def __init__(self, api: ProtectApiClient, entry: ConfigEntry) -> None:
|
||||
def __init__(self, *, api: ProtectApiClient, entry: ConfigEntry) -> None:
|
||||
"""Create flow."""
|
||||
|
||||
self._api = api
|
||||
@ -46,7 +44,7 @@ class ProtectRepair(RepairsFlow):
|
||||
return description_placeholders
|
||||
|
||||
|
||||
class EAConfirm(ProtectRepair):
|
||||
class EAConfirmRepair(ProtectRepair):
|
||||
"""Handler for an issue fixing flow."""
|
||||
|
||||
async def async_step_init(
|
||||
@ -92,7 +90,7 @@ class EAConfirm(ProtectRepair):
|
||||
)
|
||||
|
||||
|
||||
class CloudAccount(ProtectRepair):
|
||||
class CloudAccountRepair(ProtectRepair):
|
||||
"""Handler for an issue fixing flow."""
|
||||
|
||||
async def async_step_init(
|
||||
@ -119,6 +117,108 @@ class CloudAccount(ProtectRepair):
|
||||
return self.async_create_entry(data={})
|
||||
|
||||
|
||||
class RTSPRepair(ProtectRepair):
|
||||
"""Handler for an issue fixing flow."""
|
||||
|
||||
_camera_id: str
|
||||
_camera: Camera | None
|
||||
_bootstrap: Bootstrap | None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
api: ProtectApiClient,
|
||||
entry: ConfigEntry,
|
||||
camera_id: str,
|
||||
) -> None:
|
||||
"""Create flow."""
|
||||
|
||||
super().__init__(api=api, entry=entry)
|
||||
self._camera_id = camera_id
|
||||
self._bootstrap = None
|
||||
self._camera = None
|
||||
|
||||
@callback
|
||||
def _async_get_placeholders(self) -> dict[str, str]:
|
||||
description_placeholders = super()._async_get_placeholders()
|
||||
if self._camera is not None:
|
||||
description_placeholders["camera"] = self._camera.display_name
|
||||
|
||||
return description_placeholders
|
||||
|
||||
async def _get_boostrap(self) -> Bootstrap:
|
||||
if self._bootstrap is None:
|
||||
self._bootstrap = await self._api.get_bootstrap()
|
||||
|
||||
return self._bootstrap
|
||||
|
||||
async def _get_camera(self) -> Camera:
|
||||
if self._camera is None:
|
||||
bootstrap = await self._get_boostrap()
|
||||
self._camera = bootstrap.cameras.get(self._camera_id)
|
||||
assert self._camera is not None
|
||||
return self._camera
|
||||
|
||||
async def _enable_rtsp(self) -> None:
|
||||
camera = await self._get_camera()
|
||||
bootstrap = await self._get_boostrap()
|
||||
user = bootstrap.users.get(bootstrap.auth_user_id)
|
||||
if not user or not camera.can_write(user):
|
||||
return
|
||||
|
||||
channel = camera.channels[0]
|
||||
channel.is_rtsp_enabled = True
|
||||
await self._api.update_device(
|
||||
ModelType.CAMERA, camera.id, {"channels": camera.unifi_dict()["channels"]}
|
||||
)
|
||||
|
||||
async def async_step_init(
|
||||
self, user_input: dict[str, str] | None = None
|
||||
) -> data_entry_flow.FlowResult:
|
||||
"""Handle the first step of a fix flow."""
|
||||
|
||||
return await self.async_step_start()
|
||||
|
||||
async def async_step_start(
|
||||
self, user_input: dict[str, str] | None = None
|
||||
) -> data_entry_flow.FlowResult:
|
||||
"""Handle the first step of a fix flow."""
|
||||
|
||||
if user_input is None:
|
||||
# make sure camera object is loaded for placeholders
|
||||
await self._get_camera()
|
||||
placeholders = self._async_get_placeholders()
|
||||
return self.async_show_form(
|
||||
step_id="start",
|
||||
data_schema=vol.Schema({}),
|
||||
description_placeholders=placeholders,
|
||||
)
|
||||
|
||||
updated_camera = await self._api.get_camera(self._camera_id)
|
||||
if not any(c.is_rtsp_enabled for c in updated_camera.channels):
|
||||
await self._enable_rtsp()
|
||||
|
||||
updated_camera = await self._api.get_camera(self._camera_id)
|
||||
if any(c.is_rtsp_enabled for c in updated_camera.channels):
|
||||
await self.hass.config_entries.async_reload(self._entry.entry_id)
|
||||
return self.async_create_entry(data={})
|
||||
return await self.async_step_confirm()
|
||||
|
||||
async def async_step_confirm(
|
||||
self, user_input: dict[str, str] | None = None
|
||||
) -> data_entry_flow.FlowResult:
|
||||
"""Handle the confirm step of a fix flow."""
|
||||
if user_input is not None:
|
||||
return self.async_create_entry(data={})
|
||||
|
||||
placeholders = self._async_get_placeholders()
|
||||
return self.async_show_form(
|
||||
step_id="confirm",
|
||||
data_schema=vol.Schema({}),
|
||||
description_placeholders=placeholders,
|
||||
)
|
||||
|
||||
|
||||
async def async_create_fix_flow(
|
||||
hass: HomeAssistant,
|
||||
issue_id: str,
|
||||
@ -129,10 +229,19 @@ async def async_create_fix_flow(
|
||||
entry_id = cast(str, data["entry_id"])
|
||||
if (entry := hass.config_entries.async_get_entry(entry_id)) is not None:
|
||||
api = async_create_api_client(hass, entry)
|
||||
return EAConfirm(api, entry)
|
||||
return EAConfirmRepair(api=api, entry=entry)
|
||||
|
||||
elif data is not None and issue_id == "cloud_user":
|
||||
entry_id = cast(str, data["entry_id"])
|
||||
if (entry := hass.config_entries.async_get_entry(entry_id)) is not None:
|
||||
api = async_create_api_client(hass, entry)
|
||||
return CloudAccount(api, entry)
|
||||
return CloudAccountRepair(api=api, entry=entry)
|
||||
|
||||
elif data is not None and issue_id.startswith("rtsp_disabled_"):
|
||||
entry_id = cast(str, data["entry_id"])
|
||||
camera_id = cast(str, data["camera_id"])
|
||||
if (entry := hass.config_entries.async_get_entry(entry_id)) is not None:
|
||||
api = async_create_api_client(hass, entry)
|
||||
return RTSPRepair(api=api, entry=entry, camera_id=camera_id)
|
||||
|
||||
return ConfirmRepairFlow()
|
||||
|
@ -91,6 +91,36 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"rtsp_disabled_readonly": {
|
||||
"title": "RTSPS is disabled on camera {camera}",
|
||||
"fix_flow": {
|
||||
"step": {
|
||||
"start": {
|
||||
"title": "RTSPS is disabled on camera {camera}",
|
||||
"description": "RTSPS is disabled on the camera {camera}. RTSPS is required to be able to live stream your camera within Home Assistant. If you do not enable RTSPS, it may create an additional load on your UniFi Protect NVR, as any live video players will default to rapidly pulling snapshots from the camera.\n\nPlease [enable RTSPS]({learn_more}) on the camera and then come back and confirm this repair."
|
||||
},
|
||||
"confirm": {
|
||||
"title": "[%key:component::unifiprotect::issues::rtsp_disabled_readonly::fix_flow::step::start::title%]",
|
||||
"description": "Are you sure you want to leave RTSPS disabled for {camera}?"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"rtsp_disabled_writable": {
|
||||
"title": "RTSPS is disabled on camera {camera}",
|
||||
"fix_flow": {
|
||||
"step": {
|
||||
"start": {
|
||||
"title": "[%key:component::unifiprotect::issues::rtsp_disabled_readonly::fix_flow::step::start::title%]",
|
||||
"description": "RTSPS is disabled on the camera {camera}. RTSPS is required to live stream your camera within Home Assistant. If you do not enable RTSPS, it may create an additional load on your UniFi Protect NVR as any live video players will default to rapidly pulling snapshots from the camera.\n\nYou may manually [enable RTSPS]({learn_more}) on your selected camera quality channel or Home Assistant can automatically enable the highest quality channel for you. Confirm this repair once you have enabled the RTSPS channel or if you want Home Assistant to enable the highest quality automatically."
|
||||
},
|
||||
"confirm": {
|
||||
"title": "[%key:component::unifiprotect::issues::rtsp_disabled_readonly::fix_flow::step::start::title%]",
|
||||
"description": "[%key:component::unifiprotect::issues::rtsp_disabled_readonly::fix_flow::step::confirm::description%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"deprecate_hdr_switch": {
|
||||
"title": "HDR Mode Switch Deprecated",
|
||||
"description": "UniFi Protect v3 added a new state for HDR (auto). As a result, the HDR Mode Switch has been replaced with an HDR Mode Select, and it is deprecated.\n\nBelow are the detected automations or scripts that use one or more of the deprecated entities:\n{items}\nThe above list may be incomplete and it does not include any template usages inside of dashboards. Please update any templates, automations or scripts accordingly."
|
||||
|
@ -13,6 +13,7 @@ from aiohttp import CookieJar
|
||||
from pyunifiprotect import ProtectApiClient
|
||||
from pyunifiprotect.data import (
|
||||
Bootstrap,
|
||||
CameraChannel,
|
||||
Light,
|
||||
LightModeEnableType,
|
||||
LightModeType,
|
||||
@ -146,3 +147,14 @@ def async_create_api_client(
|
||||
ignore_unadopted=False,
|
||||
cache_dir=Path(hass.config.path(STORAGE_DIR, "unifiprotect_cache")),
|
||||
)
|
||||
|
||||
|
||||
@callback
|
||||
def get_camera_base_name(channel: CameraChannel) -> str:
|
||||
"""Get base name for cameras channel."""
|
||||
|
||||
camera_name = channel.name
|
||||
if channel.name != "Package Camera":
|
||||
camera_name = f"{channel.name} Resolution Channel"
|
||||
|
||||
return camera_name
|
||||
|
@ -21,6 +21,7 @@ from homeassistant.components.unifiprotect.const import (
|
||||
DEFAULT_ATTRIBUTION,
|
||||
DEFAULT_SCAN_INTERVAL,
|
||||
)
|
||||
from homeassistant.components.unifiprotect.utils import get_camera_base_name
|
||||
from homeassistant.const import (
|
||||
ATTR_ATTRIBUTION,
|
||||
ATTR_ENTITY_ID,
|
||||
@ -51,7 +52,8 @@ def validate_default_camera_entity(
|
||||
|
||||
channel = camera_obj.channels[channel_id]
|
||||
|
||||
entity_name = f"{camera_obj.name} {channel.name}"
|
||||
camera_name = get_camera_base_name(channel)
|
||||
entity_name = f"{camera_obj.name} {camera_name}"
|
||||
unique_id = f"{camera_obj.mac}_{channel.id}"
|
||||
entity_id = f"camera.{entity_name.replace(' ', '_').lower()}"
|
||||
|
||||
@ -73,7 +75,7 @@ def validate_rtsps_camera_entity(
|
||||
|
||||
channel = camera_obj.channels[channel_id]
|
||||
|
||||
entity_name = f"{camera_obj.name} {channel.name}"
|
||||
entity_name = f"{camera_obj.name} {channel.name} Resolution Channel"
|
||||
unique_id = f"{camera_obj.mac}_{channel.id}"
|
||||
entity_id = f"camera.{entity_name.replace(' ', '_').lower()}"
|
||||
|
||||
@ -95,9 +97,9 @@ def validate_rtsp_camera_entity(
|
||||
|
||||
channel = camera_obj.channels[channel_id]
|
||||
|
||||
entity_name = f"{camera_obj.name} {channel.name} Insecure"
|
||||
entity_name = f"{camera_obj.name} {channel.name} Resolution Channel (Insecure)"
|
||||
unique_id = f"{camera_obj.mac}_{channel.id}_insecure"
|
||||
entity_id = f"camera.{entity_name.replace(' ', '_').lower()}"
|
||||
entity_id = f"camera.{entity_name.replace(' ', '_').replace('(', '').replace(')', '').lower()}"
|
||||
|
||||
entity_registry = er.async_get(hass)
|
||||
entity = entity_registry.async_get(entity_id)
|
||||
@ -314,7 +316,7 @@ async def test_camera_image(
|
||||
|
||||
ufp.api.get_camera_snapshot = AsyncMock()
|
||||
|
||||
await async_get_image(hass, "camera.test_camera_high")
|
||||
await async_get_image(hass, "camera.test_camera_high_resolution_channel")
|
||||
ufp.api.get_camera_snapshot.assert_called_once()
|
||||
|
||||
|
||||
@ -339,7 +341,7 @@ async def test_camera_generic_update(
|
||||
|
||||
await init_entry(hass, ufp, [camera])
|
||||
assert_entity_counts(hass, Platform.CAMERA, 2, 1)
|
||||
entity_id = "camera.test_camera_high"
|
||||
entity_id = "camera.test_camera_high_resolution_channel"
|
||||
|
||||
assert await async_setup_component(hass, "homeassistant", {})
|
||||
|
||||
@ -365,7 +367,7 @@ async def test_camera_interval_update(
|
||||
|
||||
await init_entry(hass, ufp, [camera])
|
||||
assert_entity_counts(hass, Platform.CAMERA, 2, 1)
|
||||
entity_id = "camera.test_camera_high"
|
||||
entity_id = "camera.test_camera_high_resolution_channel"
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state and state.state == "idle"
|
||||
@ -388,7 +390,7 @@ async def test_camera_bad_interval_update(
|
||||
|
||||
await init_entry(hass, ufp, [camera])
|
||||
assert_entity_counts(hass, Platform.CAMERA, 2, 1)
|
||||
entity_id = "camera.test_camera_high"
|
||||
entity_id = "camera.test_camera_high_resolution_channel"
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state and state.state == "idle"
|
||||
@ -415,7 +417,7 @@ async def test_camera_ws_update(
|
||||
|
||||
await init_entry(hass, ufp, [camera])
|
||||
assert_entity_counts(hass, Platform.CAMERA, 2, 1)
|
||||
entity_id = "camera.test_camera_high"
|
||||
entity_id = "camera.test_camera_high_resolution_channel"
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state and state.state == "idle"
|
||||
@ -450,7 +452,7 @@ async def test_camera_ws_update_offline(
|
||||
|
||||
await init_entry(hass, ufp, [camera])
|
||||
assert_entity_counts(hass, Platform.CAMERA, 2, 1)
|
||||
entity_id = "camera.test_camera_high"
|
||||
entity_id = "camera.test_camera_high_resolution_channel"
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state and state.state == "idle"
|
||||
@ -492,7 +494,7 @@ async def test_camera_enable_motion(
|
||||
|
||||
await init_entry(hass, ufp, [camera])
|
||||
assert_entity_counts(hass, Platform.CAMERA, 2, 1)
|
||||
entity_id = "camera.test_camera_high"
|
||||
entity_id = "camera.test_camera_high_resolution_channel"
|
||||
|
||||
camera.__fields__["set_motion_detection"] = Mock(final=False)
|
||||
camera.set_motion_detection = AsyncMock()
|
||||
@ -514,7 +516,7 @@ async def test_camera_disable_motion(
|
||||
|
||||
await init_entry(hass, ufp, [camera])
|
||||
assert_entity_counts(hass, Platform.CAMERA, 2, 1)
|
||||
entity_id = "camera.test_camera_high"
|
||||
entity_id = "camera.test_camera_high_resolution_channel"
|
||||
|
||||
camera.__fields__["set_motion_detection"] = Mock(final=False)
|
||||
camera.set_motion_detection = AsyncMock()
|
||||
|
@ -362,7 +362,8 @@ async def test_browse_media_camera(
|
||||
|
||||
entity_registry = er.async_get(hass)
|
||||
entity_registry.async_update_entity(
|
||||
"camera.test_camera_high", disabled_by=er.RegistryEntryDisabler("user")
|
||||
"camera.test_camera_high_resolution_channel",
|
||||
disabled_by=er.RegistryEntryDisabler("user"),
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
@ -2,11 +2,11 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from copy import copy
|
||||
from copy import copy, deepcopy
|
||||
from http import HTTPStatus
|
||||
from unittest.mock import Mock
|
||||
from unittest.mock import AsyncMock, Mock
|
||||
|
||||
from pyunifiprotect.data import CloudAccount, Version
|
||||
from pyunifiprotect.data import Camera, CloudAccount, ModelType, Version
|
||||
|
||||
from homeassistant.components.repairs.issue_handler import (
|
||||
async_process_repairs_platforms,
|
||||
@ -192,3 +192,168 @@ async def test_cloud_user_fix(
|
||||
assert data["type"] == "create_entry"
|
||||
await hass.async_block_till_done()
|
||||
assert any(ufp.entry.async_get_active_flows(hass, {SOURCE_REAUTH}))
|
||||
|
||||
|
||||
async def test_rtsp_read_only_ignore(
|
||||
hass: HomeAssistant,
|
||||
ufp: MockUFPFixture,
|
||||
doorbell: Camera,
|
||||
hass_client: ClientSessionGenerator,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
) -> None:
|
||||
"""Test RTSP disabled warning if camera is read-only and it is ignored."""
|
||||
|
||||
for channel in doorbell.channels:
|
||||
channel.is_rtsp_enabled = False
|
||||
for user in ufp.api.bootstrap.users.values():
|
||||
user.all_permissions = []
|
||||
|
||||
ufp.api.get_camera = AsyncMock(return_value=doorbell)
|
||||
|
||||
await init_entry(hass, ufp, [doorbell])
|
||||
await async_process_repairs_platforms(hass)
|
||||
ws_client = await hass_ws_client(hass)
|
||||
client = await hass_client()
|
||||
|
||||
issue_id = f"rtsp_disabled_{doorbell.id}"
|
||||
|
||||
await ws_client.send_json({"id": 1, "type": "repairs/list_issues"})
|
||||
msg = await ws_client.receive_json()
|
||||
|
||||
assert msg["success"]
|
||||
assert len(msg["result"]["issues"]) > 0
|
||||
issue = None
|
||||
for i in msg["result"]["issues"]:
|
||||
if i["issue_id"] == issue_id:
|
||||
issue = i
|
||||
assert issue is not None
|
||||
|
||||
url = RepairsFlowIndexView.url
|
||||
resp = await client.post(url, json={"handler": DOMAIN, "issue_id": issue_id})
|
||||
assert resp.status == HTTPStatus.OK
|
||||
data = await resp.json()
|
||||
|
||||
flow_id = data["flow_id"]
|
||||
assert data["step_id"] == "start"
|
||||
|
||||
url = RepairsFlowResourceView.url.format(flow_id=flow_id)
|
||||
resp = await client.post(url)
|
||||
assert resp.status == HTTPStatus.OK
|
||||
data = await resp.json()
|
||||
|
||||
flow_id = data["flow_id"]
|
||||
assert data["step_id"] == "confirm"
|
||||
|
||||
url = RepairsFlowResourceView.url.format(flow_id=flow_id)
|
||||
resp = await client.post(url)
|
||||
assert resp.status == HTTPStatus.OK
|
||||
data = await resp.json()
|
||||
|
||||
assert data["type"] == "create_entry"
|
||||
|
||||
|
||||
async def test_rtsp_read_only_fix(
|
||||
hass: HomeAssistant,
|
||||
ufp: MockUFPFixture,
|
||||
doorbell: Camera,
|
||||
hass_client: ClientSessionGenerator,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
) -> None:
|
||||
"""Test RTSP disabled warning if camera is read-only and it is fixed."""
|
||||
|
||||
for channel in doorbell.channels:
|
||||
channel.is_rtsp_enabled = False
|
||||
for user in ufp.api.bootstrap.users.values():
|
||||
user.all_permissions = []
|
||||
|
||||
await init_entry(hass, ufp, [doorbell])
|
||||
await async_process_repairs_platforms(hass)
|
||||
ws_client = await hass_ws_client(hass)
|
||||
client = await hass_client()
|
||||
|
||||
new_doorbell = deepcopy(doorbell)
|
||||
new_doorbell.channels[1].is_rtsp_enabled = True
|
||||
ufp.api.get_camera = AsyncMock(return_value=new_doorbell)
|
||||
issue_id = f"rtsp_disabled_{doorbell.id}"
|
||||
|
||||
await ws_client.send_json({"id": 1, "type": "repairs/list_issues"})
|
||||
msg = await ws_client.receive_json()
|
||||
|
||||
assert msg["success"]
|
||||
assert len(msg["result"]["issues"]) > 0
|
||||
issue = None
|
||||
for i in msg["result"]["issues"]:
|
||||
if i["issue_id"] == issue_id:
|
||||
issue = i
|
||||
assert issue is not None
|
||||
|
||||
url = RepairsFlowIndexView.url
|
||||
resp = await client.post(url, json={"handler": DOMAIN, "issue_id": issue_id})
|
||||
assert resp.status == HTTPStatus.OK
|
||||
data = await resp.json()
|
||||
|
||||
flow_id = data["flow_id"]
|
||||
assert data["step_id"] == "start"
|
||||
|
||||
url = RepairsFlowResourceView.url.format(flow_id=flow_id)
|
||||
resp = await client.post(url)
|
||||
assert resp.status == HTTPStatus.OK
|
||||
data = await resp.json()
|
||||
|
||||
assert data["type"] == "create_entry"
|
||||
|
||||
|
||||
async def test_rtsp_writable_fix(
|
||||
hass: HomeAssistant,
|
||||
ufp: MockUFPFixture,
|
||||
doorbell: Camera,
|
||||
hass_client: ClientSessionGenerator,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
) -> None:
|
||||
"""Test RTSP disabled warning if camera is writable and it is ignored."""
|
||||
|
||||
for channel in doorbell.channels:
|
||||
channel.is_rtsp_enabled = False
|
||||
|
||||
await init_entry(hass, ufp, [doorbell])
|
||||
await async_process_repairs_platforms(hass)
|
||||
ws_client = await hass_ws_client(hass)
|
||||
client = await hass_client()
|
||||
|
||||
new_doorbell = deepcopy(doorbell)
|
||||
new_doorbell.channels[0].is_rtsp_enabled = True
|
||||
ufp.api.get_camera = AsyncMock(side_effect=[doorbell, new_doorbell])
|
||||
ufp.api.update_device = AsyncMock()
|
||||
issue_id = f"rtsp_disabled_{doorbell.id}"
|
||||
|
||||
await ws_client.send_json({"id": 1, "type": "repairs/list_issues"})
|
||||
msg = await ws_client.receive_json()
|
||||
|
||||
assert msg["success"]
|
||||
assert len(msg["result"]["issues"]) > 0
|
||||
issue = None
|
||||
for i in msg["result"]["issues"]:
|
||||
if i["issue_id"] == issue_id:
|
||||
issue = i
|
||||
assert issue is not None
|
||||
|
||||
url = RepairsFlowIndexView.url
|
||||
resp = await client.post(url, json={"handler": DOMAIN, "issue_id": issue_id})
|
||||
assert resp.status == HTTPStatus.OK
|
||||
data = await resp.json()
|
||||
|
||||
flow_id = data["flow_id"]
|
||||
assert data["step_id"] == "start"
|
||||
|
||||
url = RepairsFlowResourceView.url.format(flow_id=flow_id)
|
||||
resp = await client.post(url)
|
||||
assert resp.status == HTTPStatus.OK
|
||||
data = await resp.json()
|
||||
|
||||
assert data["type"] == "create_entry"
|
||||
|
||||
channels = doorbell.unifi_dict()["channels"]
|
||||
channels[0]["isRtspEnabled"] = True
|
||||
ufp.api.update_device.assert_called_with(
|
||||
ModelType.CAMERA, doorbell.id, {"channels": channels}
|
||||
)
|
||||
|
@ -483,7 +483,7 @@ async def test_video_entity_id(
|
||||
)
|
||||
|
||||
url = async_generate_event_video_url(event)
|
||||
url = url.replace(camera.id, "camera.test_camera_high")
|
||||
url = url.replace(camera.id, "camera.test_camera_high_resolution_channel")
|
||||
|
||||
http_client = await hass_client()
|
||||
response = cast(ClientResponse, await http_client.get(url))
|
||||
|
Loading…
x
Reference in New Issue
Block a user