"""This component provides support for Reolink IP VoD support.""" from urllib.parse import quote_plus from dataclasses import dataclass import datetime as dt import asyncio import logging import os from dateutil import relativedelta from homeassistant.core import CALLBACK_TYPE, HomeAssistant import homeassistant.util.dt as dt_utils from homeassistant.config_entries import ConfigEntry from homeassistant.components.sensor import DEVICE_CLASS_TIMESTAMP, SensorEntity from .const import ( BASE, DOMAIN, DOMAIN_DATA, LAST_EVENT, THUMBNAIL_EXTENSION, THUMBNAIL_URL, VOD_URL, ) from .entity import ReolinkEntity from .base import ReolinkBase, searchtime_to_datetime from .typings import VoDEvent, VoDEventThumbnail _LOGGER = logging.getLogger(__name__) @asyncio.coroutine async def async_setup_entry(hass, config_entry, async_add_devices): """Set up the Reolink IP Camera switches.""" devices = [] base: ReolinkBase = hass.data[DOMAIN][config_entry.entry_id][BASE] # TODO : add playback (based off of hdd_info) to api capabilities await base.api.get_switch_capabilities() if base.api.hdd_info: devices.append(LastEventSensor(hass, config_entry)) async_add_devices(devices, update_before_add=False) @dataclass class _Attrs: oldest_day: dt.datetime = None most_recent_day: dt.datetime = None last_event: VoDEvent = None class LastEventSensor(ReolinkEntity, SensorEntity): """An implementation of a Reolink IP camera sensor.""" def __init__(self, hass: HomeAssistant, config: ConfigEntry): """Initialize a Reolink camera.""" ReolinkEntity.__init__(self, hass, config) SensorEntity.__init__(self) self._attrs = _Attrs() self._bus_listener: CALLBACK_TYPE = None self._entry_id = config.entry_id async def async_added_to_hass(self) -> None: """Entity created.""" await super().async_added_to_hass() self._bus_listener = self.hass.bus.async_listen( self._base.event_id, self.handle_event ) self._hass.async_add_job(self._update_event_range) async def async_will_remove_from_hass(self): """Entity removed""" if self._bus_listener: self._bus_listener() self._bus_listener = None await super().async_will_remove_from_hass() async def request_refresh(self): """ force an update of the sensor """ await super().request_refresh() self._hass.async_add_job(self._update_event_range) async def async_update(self): """ polling update """ await super().async_update() self._hass.async_add_job(self._update_event_range) async def _update_event_range(self): end = dt_utils.now() start = self._attrs.most_recent_day if not start: start = dt.datetime.combine(end.date().replace(day=1), dt.time.min) if self._base.playback_months > 1: start -= relativedelta.relativedelta( months=int(self._base.playback_months) ) search, _ = await self._base.send_search(start, end, True) if not search or len(search) < 1: return entry = search[0] self._attrs.oldest_day = dt.datetime( entry["year"], entry["mon"], next((i for (i, e) in enumerate(entry["table"], start=1) if e == "1")), tzinfo=end.tzinfo, ) entry = search[-1] start = self._attrs.most_recent_day = dt.datetime( entry["year"], entry["mon"], len(entry["table"]) - next( ( i for (i, e) in enumerate(reversed(entry["table"]), start=0) if e == "1" ) ), tzinfo=end.tzinfo, ) end = dt.datetime.combine(start.date(), dt.time.max, tzinfo=end.tzinfo) _, files = await self._base.send_search(start, end) file = files[-1] if files and len(files) > 0 else None if not file: return end = searchtime_to_datetime(file["EndTime"], start.tzinfo) start = searchtime_to_datetime(file["StartTime"], end.tzinfo) last = self._attrs.last_event = VoDEvent( str(start.timestamp()), start, end - start, file["name"], ) last.url = VOD_URL.format( camera_id=self._entry_id, event_id=quote_plus(file["name"]) ) thumbnail = last.thumbnail = VoDEventThumbnail( THUMBNAIL_URL.format(camera_id=self._entry_id, event_id=last.event_id), path=os.path.join( self._base.thumbnail_path, f"{last.event_id}.{THUMBNAIL_EXTENSION}" ), ) thumbnail.exists = os.path.isfile(thumbnail.path) data: dict = self._hass.data.setdefault(DOMAIN_DATA, {}) data = data.setdefault(self._base.unique_id, {}) data[LAST_EVENT] = last self._state = True self.async_schedule_update_ha_state() async def handle_event(self, event): """Handle incoming event for VoD update""" if not "motion" in event.data: return self._hass.async_add_job(self._update_event_range) @property def unique_id(self): """Return Unique ID string.""" return f"reolink_lastevent_{self._base.unique_id}" @property def name(self): """Return the name of this sensor.""" return f"{self._base.name} Last Event" @property def device_class(self): """Device class of the sensor.""" return DEVICE_CLASS_TIMESTAMP @property def state(self): """Return the state of the sensor.""" if not self._state: return None date = ( self._attrs.last_event.start if self._attrs.last_event and self._attrs.last_event.start else None ) if not date: return None return date.isoformat() @property def icon(self): """Icon of the sensor.""" return "mdi:history" @property def extra_state_attributes(self): """Return the state attributes.""" attrs = super().extra_state_attributes if self._state: if attrs is None: attrs = {} if self._attrs.oldest_day: attrs["oldest_day"] = self._attrs.oldest_day.isoformat() if self._attrs.last_event: if self._attrs.last_event.event_id: attrs["vod_event_id"] = self._attrs.last_event.event_id if self._attrs.last_event.thumbnail: attrs["has_thumbnail"] = ( "true" if self._attrs.last_event.thumbnail.exists else "false" ) attrs["thumbnail_path"] = self._attrs.last_event.thumbnail.path if self._attrs.last_event.duration: attrs["duration"] = str(self._attrs.last_event.duration) return attrs