diff --git a/homeassistant/components/blink/__init__.py b/homeassistant/components/blink/__init__.py index 668a7f99c02..b94a77fbf18 100644 --- a/homeassistant/components/blink/__init__.py +++ b/homeassistant/components/blink/__init__.py @@ -8,7 +8,13 @@ import voluptuous as vol from homeassistant.components import persistent_notification from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry -from homeassistant.const import CONF_FILENAME, CONF_NAME, CONF_PIN, CONF_SCAN_INTERVAL +from homeassistant.const import ( + CONF_FILE_PATH, + CONF_FILENAME, + CONF_NAME, + CONF_PIN, + CONF_SCAN_INTERVAL, +) from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import config_validation as cv @@ -18,6 +24,7 @@ from .const import ( DOMAIN, PLATFORMS, SERVICE_REFRESH, + SERVICE_SAVE_RECENT_CLIPS, SERVICE_SAVE_VIDEO, SERVICE_SEND_PIN, ) @@ -28,6 +35,9 @@ SERVICE_SAVE_VIDEO_SCHEMA = vol.Schema( {vol.Required(CONF_NAME): cv.string, vol.Required(CONF_FILENAME): cv.string} ) SERVICE_SEND_PIN_SCHEMA = vol.Schema({vol.Optional(CONF_PIN): cv.string}) +SERVICE_SAVE_RECENT_CLIPS_SCHEMA = vol.Schema( + {vol.Required(CONF_NAME): cv.string, vol.Required(CONF_FILE_PATH): cv.string} +) def _blink_startup_wrapper(hass: HomeAssistant, entry: ConfigEntry) -> Blink: @@ -100,6 +110,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Call save video service handler.""" await async_handle_save_video_service(hass, entry, call) + async def async_save_recent_clips(call): + """Call save recent clips service handler.""" + await async_handle_save_recent_clips_service(hass, entry, call) + def send_pin(call): """Call blink to send new pin.""" pin = call.data[CONF_PIN] @@ -112,6 +126,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.services.async_register( DOMAIN, SERVICE_SAVE_VIDEO, async_save_video, schema=SERVICE_SAVE_VIDEO_SCHEMA ) + hass.services.async_register( + DOMAIN, + SERVICE_SAVE_RECENT_CLIPS, + async_save_recent_clips, + schema=SERVICE_SAVE_RECENT_CLIPS_SCHEMA, + ) hass.services.async_register( DOMAIN, SERVICE_SEND_PIN, send_pin, schema=SERVICE_SEND_PIN_SCHEMA ) @@ -164,13 +184,33 @@ async def async_handle_save_video_service(hass, entry, call): _LOGGER.error("Can't write %s, no access to path!", video_path) return - def _write_video(camera_name, video_path): + def _write_video(name, file_path): """Call video write.""" all_cameras = hass.data[DOMAIN][entry.entry_id].cameras - if camera_name in all_cameras: - all_cameras[camera_name].video_to_file(video_path) + if name in all_cameras: + all_cameras[name].video_to_file(file_path) try: await hass.async_add_executor_job(_write_video, camera_name, video_path) except OSError as err: _LOGGER.error("Can't write image to file: %s", err) + + +async def async_handle_save_recent_clips_service(hass, entry, call): + """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): + _LOGGER.error("Can't write to directory %s, no access to path!", clips_dir) + return + + def _save_recent_clips(name, output_dir): + """Call save recent clips.""" + all_cameras = hass.data[DOMAIN][entry.entry_id].cameras + if name in all_cameras: + all_cameras[name].save_recent_clips(output_dir=output_dir) + + try: + await hass.async_add_executor_job(_save_recent_clips, camera_name, clips_dir) + except OSError as err: + _LOGGER.error("Can't write recent clips to directory: %s", err) diff --git a/homeassistant/components/blink/alarm_control_panel.py b/homeassistant/components/blink/alarm_control_panel.py index 22a142ff44c..64463df723a 100644 --- a/homeassistant/components/blink/alarm_control_panel.py +++ b/homeassistant/components/blink/alarm_control_panel.py @@ -55,8 +55,15 @@ class BlinkSyncModule(AlarmControlPanelEntity): def update(self) -> None: """Update the state of the device.""" - _LOGGER.debug("Updating Blink Alarm Control Panel %s", self._name) - self.data.refresh() + if self.data.check_if_ok_to_update(): + _LOGGER.debug( + "Initiating a blink.refresh() from BlinkSyncModule('%s') (%s)", + self._name, + self.data, + ) + self.data.refresh() + _LOGGER.info("Updating State of Blink Alarm Control Panel '%s'", self._name) + self._attr_state = ( STATE_ALARM_ARMED_AWAY if self.sync.arm else STATE_ALARM_DISARMED ) diff --git a/homeassistant/components/blink/binary_sensor.py b/homeassistant/components/blink/binary_sensor.py index 9454daa85ec..5a50d3f8c93 100644 --- a/homeassistant/components/blink/binary_sensor.py +++ b/homeassistant/components/blink/binary_sensor.py @@ -1,6 +1,8 @@ """Support for Blink system camera control.""" from __future__ import annotations +import logging + from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, BinarySensorEntity, @@ -20,6 +22,8 @@ from .const import ( TYPE_MOTION_DETECTED, ) +_LOGGER = logging.getLogger(__name__) + BINARY_SENSORS_TYPES: tuple[BinarySensorEntityDescription, ...] = ( BinarySensorEntityDescription( key=TYPE_BATTERY, @@ -74,8 +78,13 @@ class BlinkBinarySensor(BinarySensorEntity): def update(self) -> None: """Update sensor state.""" - self.data.refresh() state = self._camera.attributes[self.entity_description.key] + _LOGGER.debug( + "'%s' %s = %s", + self._camera.attributes["name"], + self.entity_description.key, + state, + ) if self.entity_description.key == TYPE_BATTERY: state = state != "ok" self._attr_is_on = state diff --git a/homeassistant/components/blink/const.py b/homeassistant/components/blink/const.py index 8986782031f..d58920562f4 100644 --- a/homeassistant/components/blink/const.py +++ b/homeassistant/components/blink/const.py @@ -23,6 +23,7 @@ TYPE_WIFI_STRENGTH = "wifi_strength" SERVICE_REFRESH = "blink_update" SERVICE_TRIGGER = "trigger_camera" SERVICE_SAVE_VIDEO = "save_video" +SERVICE_SAVE_RECENT_CLIPS = "save_recent_clips" SERVICE_SEND_PIN = "send_pin" PLATFORMS = [ diff --git a/homeassistant/components/blink/sensor.py b/homeassistant/components/blink/sensor.py index c051fef98f4..eae45394534 100644 --- a/homeassistant/components/blink/sensor.py +++ b/homeassistant/components/blink/sensor.py @@ -78,9 +78,14 @@ class BlinkSensor(SensorEntity): def update(self) -> None: """Retrieve sensor data from the camera.""" - self.data.refresh() try: self._attr_native_value = self._camera.attributes[self._sensor_key] + _LOGGER.debug( + "'%s' %s = %s", + self._camera.attributes["name"], + self._sensor_key, + self._attr_native_value, + ) except KeyError: self._attr_native_value = None _LOGGER.error( diff --git a/homeassistant/components/blink/services.yaml b/homeassistant/components/blink/services.yaml index 89af4799c85..3d51ba2f7bb 100644 --- a/homeassistant/components/blink/services.yaml +++ b/homeassistant/components/blink/services.yaml @@ -25,12 +25,31 @@ save_video: text: filename: name: File name - description: Filename to writable path (directory may need to be included in whitelist_dirs in config) + description: Filename to writable path (directory may need to be included in allowlist_external_dirs in config) required: true example: "/tmp/video.mp4" selector: text: +save_recent_clips: + name: Save recent clips + description: 'Save all recent video clips to local directory with file pattern "%Y%m%d_%H%M%S_{name}.mp4"' + fields: + name: + name: Name + description: Name of camera to grab recent clips from. + required: true + example: "Living Room" + selector: + text: + file_path: + name: Output directory + description: Directory name of writable path (directory may need to be included in allowlist_external_dirs in config) + required: true + example: "/tmp" + selector: + text: + send_pin: name: Send pin description: Send a new PIN to blink for 2FA.