Move services to entity services in blink (#105413)
* Use device name to lookup camera * Fix device registry serial * Move to entity based services * Update tests * Use config_entry Move refresh service out of camera * Use config entry for services * Fix service schema * Add depreciation note * Depreciation note * key error changes deprecated (not depreciated) repair issue * tweak message * deprication v2 * back out update field change * backout update schema changes * Finish rollback on update service * update doc strings * move to 2024.7.0 More verbosity to deprecation messagepull/106598/head
parent
1909163c8e
commit
e7e0ae8f6a
|
@ -8,17 +8,26 @@ import logging
|
|||
from typing import Any
|
||||
|
||||
from requests.exceptions import ChunkedEncodingError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.camera import Camera
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_FILE_PATH, CONF_FILENAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||
from homeassistant.helpers import entity_platform
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DEFAULT_BRAND, DOMAIN, SERVICE_TRIGGER
|
||||
from .const import (
|
||||
DEFAULT_BRAND,
|
||||
DOMAIN,
|
||||
SERVICE_SAVE_RECENT_CLIPS,
|
||||
SERVICE_SAVE_VIDEO,
|
||||
SERVICE_TRIGGER,
|
||||
)
|
||||
from .coordinator import BlinkUpdateCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
@ -43,6 +52,16 @@ async def async_setup_entry(
|
|||
|
||||
platform = entity_platform.async_get_current_platform()
|
||||
platform.async_register_entity_service(SERVICE_TRIGGER, {}, "trigger_camera")
|
||||
platform.async_register_entity_service(
|
||||
SERVICE_SAVE_RECENT_CLIPS,
|
||||
{vol.Required(CONF_FILE_PATH): cv.string},
|
||||
"save_recent_clips",
|
||||
)
|
||||
platform.async_register_entity_service(
|
||||
SERVICE_SAVE_VIDEO,
|
||||
{vol.Required(CONF_FILENAME): cv.string},
|
||||
"save_video",
|
||||
)
|
||||
|
||||
|
||||
class BlinkCamera(CoordinatorEntity[BlinkUpdateCoordinator], Camera):
|
||||
|
@ -64,7 +83,7 @@ class BlinkCamera(CoordinatorEntity[BlinkUpdateCoordinator], Camera):
|
|||
manufacturer=DEFAULT_BRAND,
|
||||
model=camera.camera_type,
|
||||
)
|
||||
_LOGGER.debug("Initialized blink camera %s", self.name)
|
||||
_LOGGER.debug("Initialized blink camera %s", self._camera.name)
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self) -> Mapping[str, Any] | None:
|
||||
|
@ -121,3 +140,39 @@ class BlinkCamera(CoordinatorEntity[BlinkUpdateCoordinator], Camera):
|
|||
except TypeError:
|
||||
_LOGGER.debug("No cached image for %s", self._camera.name)
|
||||
return None
|
||||
|
||||
async def save_recent_clips(self, file_path) -> None:
|
||||
"""Save multiple recent clips to output directory."""
|
||||
if not self.hass.config.is_allowed_path(file_path):
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="no_path",
|
||||
translation_placeholders={"target": file_path},
|
||||
)
|
||||
|
||||
try:
|
||||
await self._camera.save_recent_clips(output_dir=file_path)
|
||||
except OSError as err:
|
||||
raise ServiceValidationError(
|
||||
str(err),
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="cant_write",
|
||||
) from err
|
||||
|
||||
async def save_video(self, filename) -> None:
|
||||
"""Handle save video service calls."""
|
||||
if not self.hass.config.is_allowed_path(filename):
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="no_path",
|
||||
translation_placeholders={"target": filename},
|
||||
)
|
||||
|
||||
try:
|
||||
await self._camera.video_to_file(filename)
|
||||
except OSError as err:
|
||||
raise ServiceValidationError(
|
||||
str(err),
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="cant_write",
|
||||
) from err
|
||||
|
|
|
@ -24,6 +24,7 @@ SERVICE_TRIGGER = "trigger_camera"
|
|||
SERVICE_SAVE_VIDEO = "save_video"
|
||||
SERVICE_SAVE_RECENT_CLIPS = "save_recent_clips"
|
||||
SERVICE_SEND_PIN = "send_pin"
|
||||
ATTR_CONFIG_ENTRY_ID = "config_entry_id"
|
||||
|
||||
PLATFORMS = [
|
||||
Platform.ALARM_CONTROL_PANEL,
|
||||
|
|
|
@ -4,25 +4,16 @@ from __future__ import annotations
|
|||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
|
||||
from homeassistant.const import (
|
||||
ATTR_DEVICE_ID,
|
||||
CONF_FILE_PATH,
|
||||
CONF_FILENAME,
|
||||
CONF_NAME,
|
||||
CONF_PIN,
|
||||
)
|
||||
from homeassistant.const import ATTR_DEVICE_ID, CONF_PIN
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
import homeassistant.helpers.device_registry as dr
|
||||
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
SERVICE_REFRESH,
|
||||
SERVICE_SAVE_RECENT_CLIPS,
|
||||
SERVICE_SAVE_VIDEO,
|
||||
SERVICE_SEND_PIN,
|
||||
from homeassistant.helpers import (
|
||||
config_validation as cv,
|
||||
device_registry as dr,
|
||||
issue_registry as ir,
|
||||
)
|
||||
|
||||
from .const import ATTR_CONFIG_ENTRY_ID, DOMAIN, SERVICE_REFRESH, SERVICE_SEND_PIN
|
||||
from .coordinator import BlinkUpdateCoordinator
|
||||
|
||||
SERVICE_UPDATE_SCHEMA = vol.Schema(
|
||||
|
@ -30,26 +21,12 @@ SERVICE_UPDATE_SCHEMA = vol.Schema(
|
|||
vol.Required(ATTR_DEVICE_ID): vol.All(cv.ensure_list, [cv.string]),
|
||||
}
|
||||
)
|
||||
SERVICE_SAVE_VIDEO_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_DEVICE_ID): vol.All(cv.ensure_list, [cv.string]),
|
||||
vol.Required(CONF_NAME): cv.string,
|
||||
vol.Required(CONF_FILENAME): cv.string,
|
||||
}
|
||||
)
|
||||
SERVICE_SEND_PIN_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_DEVICE_ID): vol.All(cv.ensure_list, [cv.string]),
|
||||
vol.Required(ATTR_CONFIG_ENTRY_ID): vol.All(cv.ensure_list, [cv.string]),
|
||||
vol.Optional(CONF_PIN): cv.string,
|
||||
}
|
||||
)
|
||||
SERVICE_SAVE_RECENT_CLIPS_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_DEVICE_ID): vol.All(cv.ensure_list, [cv.string]),
|
||||
vol.Required(CONF_NAME): cv.string,
|
||||
vol.Required(CONF_FILE_PATH): cv.string,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def setup_services(hass: HomeAssistant) -> None:
|
||||
|
@ -94,57 +71,22 @@ def setup_services(hass: HomeAssistant) -> None:
|
|||
coordinators.append(hass.data[DOMAIN][config_entry.entry_id])
|
||||
return coordinators
|
||||
|
||||
async def async_handle_save_video_service(call: ServiceCall) -> None:
|
||||
"""Handle save video service calls."""
|
||||
camera_name = call.data[CONF_NAME]
|
||||
video_path = call.data[CONF_FILENAME]
|
||||
if not hass.config.is_allowed_path(video_path):
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="no_path",
|
||||
translation_placeholders={"target": video_path},
|
||||
)
|
||||
|
||||
for coordinator in collect_coordinators(call.data[ATTR_DEVICE_ID]):
|
||||
all_cameras = coordinator.api.cameras
|
||||
if camera_name in all_cameras:
|
||||
try:
|
||||
await all_cameras[camera_name].video_to_file(video_path)
|
||||
except OSError as err:
|
||||
raise ServiceValidationError(
|
||||
str(err),
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="cant_write",
|
||||
) from err
|
||||
|
||||
async def async_handle_save_recent_clips_service(call: ServiceCall) -> None:
|
||||
"""Save multiple recent clips to output directory."""
|
||||
camera_name = call.data[CONF_NAME]
|
||||
clips_dir = call.data[CONF_FILE_PATH]
|
||||
if not hass.config.is_allowed_path(clips_dir):
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="no_path",
|
||||
translation_placeholders={"target": clips_dir},
|
||||
)
|
||||
|
||||
for coordinator in collect_coordinators(call.data[ATTR_DEVICE_ID]):
|
||||
all_cameras = coordinator.api.cameras
|
||||
if camera_name in all_cameras:
|
||||
try:
|
||||
await all_cameras[camera_name].save_recent_clips(
|
||||
output_dir=clips_dir
|
||||
)
|
||||
except OSError as err:
|
||||
raise ServiceValidationError(
|
||||
str(err),
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="cant_write",
|
||||
) from err
|
||||
|
||||
async def send_pin(call: ServiceCall):
|
||||
"""Call blink to send new pin."""
|
||||
for coordinator in collect_coordinators(call.data[ATTR_DEVICE_ID]):
|
||||
for entry_id in call.data[ATTR_CONFIG_ENTRY_ID]:
|
||||
if not (config_entry := hass.config_entries.async_get_entry(entry_id)):
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="integration_not_found",
|
||||
translation_placeholders={"target": DOMAIN},
|
||||
)
|
||||
if config_entry.state != ConfigEntryState.LOADED:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="not_loaded",
|
||||
translation_placeholders={"target": config_entry.title},
|
||||
)
|
||||
coordinator = hass.data[DOMAIN][entry_id]
|
||||
await coordinator.api.auth.send_auth_key(
|
||||
coordinator.api,
|
||||
call.data[CONF_PIN],
|
||||
|
@ -152,22 +94,24 @@ def setup_services(hass: HomeAssistant) -> None:
|
|||
|
||||
async def blink_refresh(call: ServiceCall):
|
||||
"""Call blink to refresh info."""
|
||||
ir.async_create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
"service_deprecation",
|
||||
breaks_in_ha_version="2024.7.0",
|
||||
is_fixable=True,
|
||||
is_persistent=True,
|
||||
severity=ir.IssueSeverity.WARNING,
|
||||
translation_key="service_deprecation",
|
||||
)
|
||||
|
||||
for coordinator in collect_coordinators(call.data[ATTR_DEVICE_ID]):
|
||||
await coordinator.api.refresh(force_cache=True)
|
||||
|
||||
# Register all the above services
|
||||
# Refresh service is deprecated and will be removed in 7/2024
|
||||
service_mapping = [
|
||||
(blink_refresh, SERVICE_REFRESH, SERVICE_UPDATE_SCHEMA),
|
||||
(
|
||||
async_handle_save_video_service,
|
||||
SERVICE_SAVE_VIDEO,
|
||||
SERVICE_SAVE_VIDEO_SCHEMA,
|
||||
),
|
||||
(
|
||||
async_handle_save_recent_clips_service,
|
||||
SERVICE_SAVE_RECENT_CLIPS,
|
||||
SERVICE_SAVE_RECENT_CLIPS_SCHEMA,
|
||||
),
|
||||
(send_pin, SERVICE_SEND_PIN, SERVICE_SEND_PIN_SCHEMA),
|
||||
]
|
||||
|
||||
|
|
|
@ -9,25 +9,17 @@ blink_update:
|
|||
integration: blink
|
||||
|
||||
trigger_camera:
|
||||
fields:
|
||||
device_id:
|
||||
required: true
|
||||
selector:
|
||||
device:
|
||||
integration: blink
|
||||
target:
|
||||
entity:
|
||||
integration: blink
|
||||
domain: camera
|
||||
|
||||
save_video:
|
||||
target:
|
||||
entity:
|
||||
integration: blink
|
||||
domain: camera
|
||||
fields:
|
||||
device_id:
|
||||
required: true
|
||||
selector:
|
||||
device:
|
||||
integration: blink
|
||||
name:
|
||||
required: true
|
||||
example: "Living Room"
|
||||
selector:
|
||||
text:
|
||||
filename:
|
||||
required: true
|
||||
example: "/tmp/video.mp4"
|
||||
|
@ -35,17 +27,11 @@ save_video:
|
|||
text:
|
||||
|
||||
save_recent_clips:
|
||||
target:
|
||||
entity:
|
||||
integration: blink
|
||||
domain: camera
|
||||
fields:
|
||||
device_id:
|
||||
required: true
|
||||
selector:
|
||||
device:
|
||||
integration: blink
|
||||
name:
|
||||
required: true
|
||||
example: "Living Room"
|
||||
selector:
|
||||
text:
|
||||
file_path:
|
||||
required: true
|
||||
example: "/tmp"
|
||||
|
@ -54,10 +40,10 @@ save_recent_clips:
|
|||
|
||||
send_pin:
|
||||
fields:
|
||||
device_id:
|
||||
config_entry_id:
|
||||
required: true
|
||||
selector:
|
||||
device:
|
||||
config_entry:
|
||||
integration: blink
|
||||
pin:
|
||||
example: "abc123"
|
||||
|
|
|
@ -67,29 +67,15 @@
|
|||
},
|
||||
"trigger_camera": {
|
||||
"name": "Trigger camera",
|
||||
"description": "Requests camera to take new image.",
|
||||
"fields": {
|
||||
"device_id": {
|
||||
"name": "Device ID",
|
||||
"description": "The Blink device id."
|
||||
}
|
||||
}
|
||||
"description": "Requests camera to take new image."
|
||||
},
|
||||
"save_video": {
|
||||
"name": "Save video",
|
||||
"description": "Saves last recorded video clip to local file.",
|
||||
"fields": {
|
||||
"name": {
|
||||
"name": "[%key:common::config_flow::data::name%]",
|
||||
"description": "Name of camera to grab video from."
|
||||
},
|
||||
"filename": {
|
||||
"name": "File name",
|
||||
"description": "Filename to writable path (directory may need to be included in allowlist_external_dirs in config)."
|
||||
},
|
||||
"device_id": {
|
||||
"name": "Device ID",
|
||||
"description": "The Blink device id."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -97,17 +83,9 @@
|
|||
"name": "Save recent clips",
|
||||
"description": "Saves all recent video clips to local directory with file pattern \"%Y%m%d_%H%M%S_{name}.mp4\".",
|
||||
"fields": {
|
||||
"name": {
|
||||
"name": "[%key:common::config_flow::data::name%]",
|
||||
"description": "Name of camera to grab recent clips from."
|
||||
},
|
||||
"file_path": {
|
||||
"name": "Output directory",
|
||||
"description": "Directory name of writable path (directory may need to be included in allowlist_external_dirs in config)."
|
||||
},
|
||||
"device_id": {
|
||||
"name": "Device ID",
|
||||
"description": "The Blink device id."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -119,19 +97,16 @@
|
|||
"name": "Pin",
|
||||
"description": "PIN received from blink. Leave empty if you only received a verification email."
|
||||
},
|
||||
"device_id": {
|
||||
"name": "Device ID",
|
||||
"description": "The Blink device id."
|
||||
"config_entry_id": {
|
||||
"name": "Integration ID",
|
||||
"description": "The Blink Integration id."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"invalid_device": {
|
||||
"message": "Device '{target}' is not a {domain} device"
|
||||
},
|
||||
"device_not_found": {
|
||||
"message": "Device '{target}' not found in device registry"
|
||||
"integration_not_found": {
|
||||
"message": "Integraion '{target}' not found in registry"
|
||||
},
|
||||
"no_path": {
|
||||
"message": "Can't write to directory {target}, no access to path!"
|
||||
|
@ -142,5 +117,18 @@
|
|||
"not_loaded": {
|
||||
"message": "{target} is not loaded"
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"service_deprecation": {
|
||||
"title": "Blink update service is being removed",
|
||||
"fix_flow": {
|
||||
"step": {
|
||||
"confirm": {
|
||||
"title": "[%key:component::blink::issues::service_deprecation::title%]",
|
||||
"description": "Blink update service is deprecated and will be removed.\nPlease update your automations and scripts to use `Home Assistant Core Integration: Update entity`."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,22 +4,15 @@ from unittest.mock import AsyncMock, MagicMock, Mock
|
|||
import pytest
|
||||
|
||||
from homeassistant.components.blink.const import (
|
||||
ATTR_CONFIG_ENTRY_ID,
|
||||
DOMAIN,
|
||||
SERVICE_REFRESH,
|
||||
SERVICE_SAVE_RECENT_CLIPS,
|
||||
SERVICE_SAVE_VIDEO,
|
||||
SERVICE_SEND_PIN,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import (
|
||||
ATTR_DEVICE_ID,
|
||||
CONF_FILE_PATH,
|
||||
CONF_FILENAME,
|
||||
CONF_NAME,
|
||||
CONF_PIN,
|
||||
)
|
||||
from homeassistant.const import ATTR_DEVICE_ID, CONF_PIN
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
@ -43,7 +36,6 @@ async def test_refresh_service_calls(
|
|||
await hass.async_block_till_done()
|
||||
|
||||
device_entry = device_registry.async_get_device(identifiers={(DOMAIN, "12345")})
|
||||
|
||||
assert device_entry
|
||||
|
||||
assert mock_config_entry.state is ConfigEntryState.LOADED
|
||||
|
@ -67,163 +59,8 @@ async def test_refresh_service_calls(
|
|||
)
|
||||
|
||||
|
||||
async def test_video_service_calls(
|
||||
hass: HomeAssistant,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
mock_blink_api: MagicMock,
|
||||
mock_blink_auth_api: MagicMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test video service calls."""
|
||||
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
assert await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
device_entry = device_registry.async_get_device(identifiers={(DOMAIN, "12345")})
|
||||
|
||||
assert device_entry
|
||||
|
||||
assert mock_config_entry.state is ConfigEntryState.LOADED
|
||||
assert mock_blink_api.refresh.call_count == 1
|
||||
|
||||
with pytest.raises(HomeAssistantError):
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_SAVE_VIDEO,
|
||||
{
|
||||
ATTR_DEVICE_ID: [device_entry.id],
|
||||
CONF_NAME: CAMERA_NAME,
|
||||
CONF_FILENAME: FILENAME,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
hass.config.is_allowed_path = Mock(return_value=True)
|
||||
caplog.clear()
|
||||
mock_blink_api.cameras = {CAMERA_NAME: AsyncMock()}
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_SAVE_VIDEO,
|
||||
{
|
||||
ATTR_DEVICE_ID: [device_entry.id],
|
||||
CONF_NAME: CAMERA_NAME,
|
||||
CONF_FILENAME: FILENAME,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
mock_blink_api.cameras[CAMERA_NAME].video_to_file.assert_awaited_once()
|
||||
|
||||
with pytest.raises(HomeAssistantError):
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_SAVE_VIDEO,
|
||||
{
|
||||
ATTR_DEVICE_ID: ["bad-device_id"],
|
||||
CONF_NAME: CAMERA_NAME,
|
||||
CONF_FILENAME: FILENAME,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
mock_blink_api.cameras[CAMERA_NAME].video_to_file = AsyncMock(side_effect=OSError)
|
||||
|
||||
with pytest.raises(ServiceValidationError):
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_SAVE_VIDEO,
|
||||
{
|
||||
ATTR_DEVICE_ID: [device_entry.id],
|
||||
CONF_NAME: CAMERA_NAME,
|
||||
CONF_FILENAME: FILENAME,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
hass.config.is_allowed_path = Mock(return_value=False)
|
||||
|
||||
|
||||
async def test_picture_service_calls(
|
||||
hass: HomeAssistant,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
mock_blink_api: MagicMock,
|
||||
mock_blink_auth_api: MagicMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test picture servcie calls."""
|
||||
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
assert await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
device_entry = device_registry.async_get_device(identifiers={(DOMAIN, "12345")})
|
||||
|
||||
assert device_entry
|
||||
|
||||
assert mock_config_entry.state is ConfigEntryState.LOADED
|
||||
assert mock_blink_api.refresh.call_count == 1
|
||||
|
||||
with pytest.raises(ServiceValidationError):
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_SAVE_RECENT_CLIPS,
|
||||
{
|
||||
ATTR_DEVICE_ID: [device_entry.id],
|
||||
CONF_NAME: CAMERA_NAME,
|
||||
CONF_FILE_PATH: FILENAME,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
hass.config.is_allowed_path = Mock(return_value=True)
|
||||
mock_blink_api.cameras = {CAMERA_NAME: AsyncMock()}
|
||||
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_SAVE_RECENT_CLIPS,
|
||||
{
|
||||
ATTR_DEVICE_ID: [device_entry.id],
|
||||
CONF_NAME: CAMERA_NAME,
|
||||
CONF_FILE_PATH: FILENAME,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
mock_blink_api.cameras[CAMERA_NAME].save_recent_clips.assert_awaited_once()
|
||||
|
||||
mock_blink_api.cameras[CAMERA_NAME].save_recent_clips = AsyncMock(
|
||||
side_effect=OSError
|
||||
)
|
||||
|
||||
with pytest.raises(ServiceValidationError):
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_SAVE_RECENT_CLIPS,
|
||||
{
|
||||
ATTR_DEVICE_ID: [device_entry.id],
|
||||
CONF_NAME: CAMERA_NAME,
|
||||
CONF_FILE_PATH: FILENAME,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
with pytest.raises(HomeAssistantError):
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_SAVE_RECENT_CLIPS,
|
||||
{
|
||||
ATTR_DEVICE_ID: ["bad-device_id"],
|
||||
CONF_NAME: CAMERA_NAME,
|
||||
CONF_FILE_PATH: FILENAME,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
|
||||
async def test_pin_service_calls(
|
||||
hass: HomeAssistant,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
mock_blink_api: MagicMock,
|
||||
mock_blink_auth_api: MagicMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
|
@ -234,17 +71,13 @@ async def test_pin_service_calls(
|
|||
assert await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
device_entry = device_registry.async_get_device(identifiers={(DOMAIN, "12345")})
|
||||
|
||||
assert device_entry
|
||||
|
||||
assert mock_config_entry.state is ConfigEntryState.LOADED
|
||||
assert mock_blink_api.refresh.call_count == 1
|
||||
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_SEND_PIN,
|
||||
{ATTR_DEVICE_ID: [device_entry.id], CONF_PIN: PIN},
|
||||
{ATTR_CONFIG_ENTRY_ID: [mock_config_entry.entry_id], CONF_PIN: PIN},
|
||||
blocking=True,
|
||||
)
|
||||
assert mock_blink_api.auth.send_auth_key.assert_awaited_once
|
||||
|
@ -253,41 +86,18 @@ async def test_pin_service_calls(
|
|||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_SEND_PIN,
|
||||
{ATTR_DEVICE_ID: ["bad-device_id"], CONF_PIN: PIN},
|
||||
{ATTR_CONFIG_ENTRY_ID: ["bad-config_id"], CONF_PIN: PIN},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("service", "params"),
|
||||
[
|
||||
(SERVICE_SEND_PIN, {CONF_PIN: PIN}),
|
||||
(
|
||||
SERVICE_SAVE_RECENT_CLIPS,
|
||||
{
|
||||
CONF_NAME: CAMERA_NAME,
|
||||
CONF_FILE_PATH: FILENAME,
|
||||
},
|
||||
),
|
||||
(
|
||||
SERVICE_SAVE_VIDEO,
|
||||
{
|
||||
CONF_NAME: CAMERA_NAME,
|
||||
CONF_FILENAME: FILENAME,
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_service_called_with_non_blink_device(
|
||||
async def test_service_pin_called_with_non_blink_device(
|
||||
hass: HomeAssistant,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
mock_blink_api: MagicMock,
|
||||
mock_blink_auth_api: MagicMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
service,
|
||||
params,
|
||||
) -> None:
|
||||
"""Test service calls with non blink device."""
|
||||
"""Test pin service calls with non blink device."""
|
||||
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
assert await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
|
@ -295,11 +105,48 @@ async def test_service_called_with_non_blink_device(
|
|||
|
||||
other_domain = "NotBlink"
|
||||
other_config_id = "555"
|
||||
await hass.config_entries.async_add(
|
||||
MockConfigEntry(
|
||||
title="Not Blink", domain=other_domain, entry_id=other_config_id
|
||||
)
|
||||
other_mock_config_entry = MockConfigEntry(
|
||||
title="Not Blink", domain=other_domain, entry_id=other_config_id
|
||||
)
|
||||
await hass.config_entries.async_add(other_mock_config_entry)
|
||||
|
||||
hass.config.is_allowed_path = Mock(return_value=True)
|
||||
mock_blink_api.cameras = {CAMERA_NAME: AsyncMock()}
|
||||
|
||||
parameters = {
|
||||
ATTR_CONFIG_ENTRY_ID: [other_mock_config_entry.entry_id],
|
||||
CONF_PIN: PIN,
|
||||
}
|
||||
|
||||
with pytest.raises(HomeAssistantError):
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_SEND_PIN,
|
||||
parameters,
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
|
||||
async def test_service_update_called_with_non_blink_device(
|
||||
hass: HomeAssistant,
|
||||
mock_blink_api: MagicMock,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
mock_blink_auth_api: MagicMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test update service calls with non blink device."""
|
||||
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
assert await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
other_domain = "NotBlink"
|
||||
other_config_id = "555"
|
||||
other_mock_config_entry = MockConfigEntry(
|
||||
title="Not Blink", domain=other_domain, entry_id=other_config_id
|
||||
)
|
||||
await hass.config_entries.async_add(other_mock_config_entry)
|
||||
|
||||
device_entry = device_registry.async_get_or_create(
|
||||
config_entry_id=other_config_id,
|
||||
identifiers={
|
||||
|
@ -311,67 +158,68 @@ async def test_service_called_with_non_blink_device(
|
|||
mock_blink_api.cameras = {CAMERA_NAME: AsyncMock()}
|
||||
|
||||
parameters = {ATTR_DEVICE_ID: [device_entry.id]}
|
||||
parameters.update(params)
|
||||
|
||||
with pytest.raises(ServiceValidationError):
|
||||
with pytest.raises(HomeAssistantError):
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
service,
|
||||
SERVICE_REFRESH,
|
||||
parameters,
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("service", "params"),
|
||||
[
|
||||
(SERVICE_SEND_PIN, {CONF_PIN: PIN}),
|
||||
(
|
||||
SERVICE_SAVE_RECENT_CLIPS,
|
||||
{
|
||||
CONF_NAME: CAMERA_NAME,
|
||||
CONF_FILE_PATH: FILENAME,
|
||||
},
|
||||
),
|
||||
(
|
||||
SERVICE_SAVE_VIDEO,
|
||||
{
|
||||
CONF_NAME: CAMERA_NAME,
|
||||
CONF_FILENAME: FILENAME,
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_service_called_with_unloaded_entry(
|
||||
async def test_service_pin_called_with_unloaded_entry(
|
||||
hass: HomeAssistant,
|
||||
mock_blink_api: MagicMock,
|
||||
mock_blink_auth_api: MagicMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test pin service calls with not ready config entry."""
|
||||
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
assert await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
mock_config_entry.state = ConfigEntryState.SETUP_ERROR
|
||||
hass.config.is_allowed_path = Mock(return_value=True)
|
||||
mock_blink_api.cameras = {CAMERA_NAME: AsyncMock()}
|
||||
|
||||
parameters = {ATTR_CONFIG_ENTRY_ID: [mock_config_entry.entry_id], CONF_PIN: PIN}
|
||||
|
||||
with pytest.raises(HomeAssistantError):
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_SEND_PIN,
|
||||
parameters,
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
|
||||
async def test_service_update_called_with_unloaded_entry(
|
||||
hass: HomeAssistant,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
mock_blink_api: MagicMock,
|
||||
mock_blink_auth_api: MagicMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
service,
|
||||
params,
|
||||
) -> None:
|
||||
"""Test service calls with unloaded config entry."""
|
||||
"""Test update service calls with not ready config entry."""
|
||||
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
assert await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
await mock_config_entry.async_unload(hass)
|
||||
|
||||
device_entry = device_registry.async_get_device(identifiers={(DOMAIN, "12345")})
|
||||
|
||||
assert device_entry
|
||||
|
||||
mock_config_entry.state = ConfigEntryState.SETUP_ERROR
|
||||
hass.config.is_allowed_path = Mock(return_value=True)
|
||||
mock_blink_api.cameras = {CAMERA_NAME: AsyncMock()}
|
||||
|
||||
device_entry = device_registry.async_get_device(identifiers={(DOMAIN, "12345")})
|
||||
assert device_entry
|
||||
|
||||
parameters = {ATTR_DEVICE_ID: [device_entry.id]}
|
||||
parameters.update(params)
|
||||
|
||||
with pytest.raises(HomeAssistantError):
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
service,
|
||||
SERVICE_REFRESH,
|
||||
parameters,
|
||||
blocking=True,
|
||||
)
|
||||
|
|
Loading…
Reference in New Issue