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 message
This commit is contained in:
mkmer 2023-12-28 13:56:40 -05:00 committed by GitHub
parent 1909163c8e
commit e7e0ae8f6a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 209 additions and 387 deletions

View file

@ -8,17 +8,26 @@ import logging
from typing import Any from typing import Any
from requests.exceptions import ChunkedEncodingError from requests.exceptions import ChunkedEncodingError
import voluptuous as vol
from homeassistant.components.camera import Camera from homeassistant.components.camera import Camera
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_FILE_PATH, CONF_FILENAME
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.helpers import entity_platform from homeassistant.helpers import entity_platform
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity 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 from .coordinator import BlinkUpdateCoordinator
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -43,6 +52,16 @@ async def async_setup_entry(
platform = entity_platform.async_get_current_platform() platform = entity_platform.async_get_current_platform()
platform.async_register_entity_service(SERVICE_TRIGGER, {}, "trigger_camera") 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): class BlinkCamera(CoordinatorEntity[BlinkUpdateCoordinator], Camera):
@ -64,7 +83,7 @@ class BlinkCamera(CoordinatorEntity[BlinkUpdateCoordinator], Camera):
manufacturer=DEFAULT_BRAND, manufacturer=DEFAULT_BRAND,
model=camera.camera_type, model=camera.camera_type,
) )
_LOGGER.debug("Initialized blink camera %s", self.name) _LOGGER.debug("Initialized blink camera %s", self._camera.name)
@property @property
def extra_state_attributes(self) -> Mapping[str, Any] | None: def extra_state_attributes(self) -> Mapping[str, Any] | None:
@ -121,3 +140,39 @@ class BlinkCamera(CoordinatorEntity[BlinkUpdateCoordinator], Camera):
except TypeError: except TypeError:
_LOGGER.debug("No cached image for %s", self._camera.name) _LOGGER.debug("No cached image for %s", self._camera.name)
return None 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

View file

@ -24,6 +24,7 @@ SERVICE_TRIGGER = "trigger_camera"
SERVICE_SAVE_VIDEO = "save_video" SERVICE_SAVE_VIDEO = "save_video"
SERVICE_SAVE_RECENT_CLIPS = "save_recent_clips" SERVICE_SAVE_RECENT_CLIPS = "save_recent_clips"
SERVICE_SEND_PIN = "send_pin" SERVICE_SEND_PIN = "send_pin"
ATTR_CONFIG_ENTRY_ID = "config_entry_id"
PLATFORMS = [ PLATFORMS = [
Platform.ALARM_CONTROL_PANEL, Platform.ALARM_CONTROL_PANEL,

View file

@ -4,25 +4,16 @@ from __future__ import annotations
import voluptuous as vol import voluptuous as vol
from homeassistant.config_entries import ConfigEntry, ConfigEntryState from homeassistant.config_entries import ConfigEntry, ConfigEntryState
from homeassistant.const import ( from homeassistant.const import ATTR_DEVICE_ID, CONF_PIN
ATTR_DEVICE_ID,
CONF_FILE_PATH,
CONF_FILENAME,
CONF_NAME,
CONF_PIN,
)
from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
import homeassistant.helpers.config_validation as cv from homeassistant.helpers import (
import homeassistant.helpers.device_registry as dr config_validation as cv,
device_registry as dr,
from .const import ( issue_registry as ir,
DOMAIN,
SERVICE_REFRESH,
SERVICE_SAVE_RECENT_CLIPS,
SERVICE_SAVE_VIDEO,
SERVICE_SEND_PIN,
) )
from .const import ATTR_CONFIG_ENTRY_ID, DOMAIN, SERVICE_REFRESH, SERVICE_SEND_PIN
from .coordinator import BlinkUpdateCoordinator from .coordinator import BlinkUpdateCoordinator
SERVICE_UPDATE_SCHEMA = vol.Schema( 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]), 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( 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, 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: 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]) coordinators.append(hass.data[DOMAIN][config_entry.entry_id])
return coordinators 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): async def send_pin(call: ServiceCall):
"""Call blink to send new pin.""" """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( await coordinator.api.auth.send_auth_key(
coordinator.api, coordinator.api,
call.data[CONF_PIN], call.data[CONF_PIN],
@ -152,22 +94,24 @@ def setup_services(hass: HomeAssistant) -> None:
async def blink_refresh(call: ServiceCall): async def blink_refresh(call: ServiceCall):
"""Call blink to refresh info.""" """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]): for coordinator in collect_coordinators(call.data[ATTR_DEVICE_ID]):
await coordinator.api.refresh(force_cache=True) await coordinator.api.refresh(force_cache=True)
# Register all the above services # Register all the above services
# Refresh service is deprecated and will be removed in 7/2024
service_mapping = [ service_mapping = [
(blink_refresh, SERVICE_REFRESH, SERVICE_UPDATE_SCHEMA), (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), (send_pin, SERVICE_SEND_PIN, SERVICE_SEND_PIN_SCHEMA),
] ]

View file

@ -9,25 +9,17 @@ blink_update:
integration: blink integration: blink
trigger_camera: trigger_camera:
fields: target:
device_id: entity:
required: true integration: blink
selector: domain: camera
device:
integration: blink
save_video: save_video:
target:
entity:
integration: blink
domain: camera
fields: fields:
device_id:
required: true
selector:
device:
integration: blink
name:
required: true
example: "Living Room"
selector:
text:
filename: filename:
required: true required: true
example: "/tmp/video.mp4" example: "/tmp/video.mp4"
@ -35,17 +27,11 @@ save_video:
text: text:
save_recent_clips: save_recent_clips:
target:
entity:
integration: blink
domain: camera
fields: fields:
device_id:
required: true
selector:
device:
integration: blink
name:
required: true
example: "Living Room"
selector:
text:
file_path: file_path:
required: true required: true
example: "/tmp" example: "/tmp"
@ -54,10 +40,10 @@ save_recent_clips:
send_pin: send_pin:
fields: fields:
device_id: config_entry_id:
required: true required: true
selector: selector:
device: config_entry:
integration: blink integration: blink
pin: pin:
example: "abc123" example: "abc123"

View file

@ -67,29 +67,15 @@
}, },
"trigger_camera": { "trigger_camera": {
"name": "Trigger camera", "name": "Trigger camera",
"description": "Requests camera to take new image.", "description": "Requests camera to take new image."
"fields": {
"device_id": {
"name": "Device ID",
"description": "The Blink device id."
}
}
}, },
"save_video": { "save_video": {
"name": "Save video", "name": "Save video",
"description": "Saves last recorded video clip to local file.", "description": "Saves last recorded video clip to local file.",
"fields": { "fields": {
"name": {
"name": "[%key:common::config_flow::data::name%]",
"description": "Name of camera to grab video from."
},
"filename": { "filename": {
"name": "File name", "name": "File name",
"description": "Filename to writable path (directory may need to be included in allowlist_external_dirs in config)." "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", "name": "Save recent clips",
"description": "Saves all recent video clips to local directory with file pattern \"%Y%m%d_%H%M%S_{name}.mp4\".", "description": "Saves all recent video clips to local directory with file pattern \"%Y%m%d_%H%M%S_{name}.mp4\".",
"fields": { "fields": {
"name": {
"name": "[%key:common::config_flow::data::name%]",
"description": "Name of camera to grab recent clips from."
},
"file_path": { "file_path": {
"name": "Output directory", "name": "Output directory",
"description": "Directory name of writable path (directory may need to be included in allowlist_external_dirs in config)." "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", "name": "Pin",
"description": "PIN received from blink. Leave empty if you only received a verification email." "description": "PIN received from blink. Leave empty if you only received a verification email."
}, },
"device_id": { "config_entry_id": {
"name": "Device ID", "name": "Integration ID",
"description": "The Blink device id." "description": "The Blink Integration id."
} }
} }
} }
}, },
"exceptions": { "exceptions": {
"invalid_device": { "integration_not_found": {
"message": "Device '{target}' is not a {domain} device" "message": "Integraion '{target}' not found in registry"
},
"device_not_found": {
"message": "Device '{target}' not found in device registry"
}, },
"no_path": { "no_path": {
"message": "Can't write to directory {target}, no access to path!" "message": "Can't write to directory {target}, no access to path!"
@ -142,5 +117,18 @@
"not_loaded": { "not_loaded": {
"message": "{target} is 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`."
}
}
}
}
} }
} }

View file

@ -4,22 +4,15 @@ from unittest.mock import AsyncMock, MagicMock, Mock
import pytest import pytest
from homeassistant.components.blink.const import ( from homeassistant.components.blink.const import (
ATTR_CONFIG_ENTRY_ID,
DOMAIN, DOMAIN,
SERVICE_REFRESH, SERVICE_REFRESH,
SERVICE_SAVE_RECENT_CLIPS,
SERVICE_SAVE_VIDEO,
SERVICE_SEND_PIN, SERVICE_SEND_PIN,
) )
from homeassistant.config_entries import ConfigEntryState from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import ( from homeassistant.const import ATTR_DEVICE_ID, CONF_PIN
ATTR_DEVICE_ID,
CONF_FILE_PATH,
CONF_FILENAME,
CONF_NAME,
CONF_PIN,
)
from homeassistant.core import HomeAssistant 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 homeassistant.helpers import device_registry as dr
from tests.common import MockConfigEntry from tests.common import MockConfigEntry
@ -43,7 +36,6 @@ async def test_refresh_service_calls(
await hass.async_block_till_done() await hass.async_block_till_done()
device_entry = device_registry.async_get_device(identifiers={(DOMAIN, "12345")}) device_entry = device_registry.async_get_device(identifiers={(DOMAIN, "12345")})
assert device_entry assert device_entry
assert mock_config_entry.state is ConfigEntryState.LOADED 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( async def test_pin_service_calls(
hass: HomeAssistant, hass: HomeAssistant,
device_registry: dr.DeviceRegistry,
mock_blink_api: MagicMock, mock_blink_api: MagicMock,
mock_blink_auth_api: MagicMock, mock_blink_auth_api: MagicMock,
mock_config_entry: MockConfigEntry, 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) assert await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done() 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_config_entry.state is ConfigEntryState.LOADED
assert mock_blink_api.refresh.call_count == 1 assert mock_blink_api.refresh.call_count == 1
await hass.services.async_call( await hass.services.async_call(
DOMAIN, DOMAIN,
SERVICE_SEND_PIN, 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, blocking=True,
) )
assert mock_blink_api.auth.send_auth_key.assert_awaited_once 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( await hass.services.async_call(
DOMAIN, DOMAIN,
SERVICE_SEND_PIN, SERVICE_SEND_PIN,
{ATTR_DEVICE_ID: ["bad-device_id"], CONF_PIN: PIN}, {ATTR_CONFIG_ENTRY_ID: ["bad-config_id"], CONF_PIN: PIN},
blocking=True, blocking=True,
) )
@pytest.mark.parametrize( async def test_service_pin_called_with_non_blink_device(
("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(
hass: HomeAssistant, hass: HomeAssistant,
device_registry: dr.DeviceRegistry,
mock_blink_api: MagicMock, mock_blink_api: MagicMock,
mock_blink_auth_api: MagicMock, mock_blink_auth_api: MagicMock,
mock_config_entry: MockConfigEntry, mock_config_entry: MockConfigEntry,
service,
params,
) -> None: ) -> None:
"""Test service calls with non blink device.""" """Test pin service calls with non blink device."""
mock_config_entry.add_to_hass(hass) mock_config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(mock_config_entry.entry_id) 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_domain = "NotBlink"
other_config_id = "555" other_config_id = "555"
await hass.config_entries.async_add( other_mock_config_entry = MockConfigEntry(
MockConfigEntry( title="Not Blink", domain=other_domain, entry_id=other_config_id
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( device_entry = device_registry.async_get_or_create(
config_entry_id=other_config_id, config_entry_id=other_config_id,
identifiers={ identifiers={
@ -311,67 +158,68 @@ async def test_service_called_with_non_blink_device(
mock_blink_api.cameras = {CAMERA_NAME: AsyncMock()} mock_blink_api.cameras = {CAMERA_NAME: AsyncMock()}
parameters = {ATTR_DEVICE_ID: [device_entry.id]} parameters = {ATTR_DEVICE_ID: [device_entry.id]}
parameters.update(params)
with pytest.raises(ServiceValidationError): with pytest.raises(HomeAssistantError):
await hass.services.async_call( await hass.services.async_call(
DOMAIN, DOMAIN,
service, SERVICE_REFRESH,
parameters, parameters,
blocking=True, blocking=True,
) )
@pytest.mark.parametrize( async def test_service_pin_called_with_unloaded_entry(
("service", "params"), hass: HomeAssistant,
[ mock_blink_api: MagicMock,
(SERVICE_SEND_PIN, {CONF_PIN: PIN}), mock_blink_auth_api: MagicMock,
( mock_config_entry: MockConfigEntry,
SERVICE_SAVE_RECENT_CLIPS, ) -> None:
{ """Test pin service calls with not ready config entry."""
CONF_NAME: CAMERA_NAME,
CONF_FILE_PATH: FILENAME, 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
SERVICE_SAVE_VIDEO, hass.config.is_allowed_path = Mock(return_value=True)
{ mock_blink_api.cameras = {CAMERA_NAME: AsyncMock()}
CONF_NAME: CAMERA_NAME,
CONF_FILENAME: FILENAME, parameters = {ATTR_CONFIG_ENTRY_ID: [mock_config_entry.entry_id], CONF_PIN: PIN}
},
), with pytest.raises(HomeAssistantError):
], await hass.services.async_call(
) DOMAIN,
async def test_service_called_with_unloaded_entry( SERVICE_SEND_PIN,
parameters,
blocking=True,
)
async def test_service_update_called_with_unloaded_entry(
hass: HomeAssistant, hass: HomeAssistant,
device_registry: dr.DeviceRegistry, device_registry: dr.DeviceRegistry,
mock_blink_api: MagicMock, mock_blink_api: MagicMock,
mock_blink_auth_api: MagicMock, mock_blink_auth_api: MagicMock,
mock_config_entry: MockConfigEntry, mock_config_entry: MockConfigEntry,
service,
params,
) -> None: ) -> None:
"""Test service calls with unloaded config entry.""" """Test update service calls with not ready config entry."""
mock_config_entry.add_to_hass(hass) mock_config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(mock_config_entry.entry_id) assert await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done() 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) hass.config.is_allowed_path = Mock(return_value=True)
mock_blink_api.cameras = {CAMERA_NAME: AsyncMock()} 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 = {ATTR_DEVICE_ID: [device_entry.id]}
parameters.update(params)
with pytest.raises(HomeAssistantError): with pytest.raises(HomeAssistantError):
await hass.services.async_call( await hass.services.async_call(
DOMAIN, DOMAIN,
service, SERVICE_REFRESH,
parameters, parameters,
blocking=True, blocking=True,
) )