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:
parent
1909163c8e
commit
e7e0ae8f6a
6 changed files with 209 additions and 387 deletions
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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`."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
Loading…
Add table
Reference in a new issue