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:
Christopher Bailey
2024-03-24 23:30:52 -04:00
committed by GitHub
parent a5128c2148
commit 3e01085c91
8 changed files with 391 additions and 33 deletions

View File

@@ -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()

View File

@@ -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()

View File

@@ -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}
)

View File

@@ -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))