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:
committed by
GitHub
parent
a5128c2148
commit
3e01085c91
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user