Beispiel #1
0
async def test_ws_emit_alarm_callback(
    mock_now, protect_client_no_debug: ProtectApiClient, now: datetime, sensor, packet: WSPacket
):
    mock_now.return_value = now
    protect_client = protect_client_no_debug
    protect_client.emit_message = Mock()  # type: ignore

    obj = protect_client.bootstrap.sensors[sensor["id"]]

    expected_updated_id = "0441ecc6-f0fa-4b19-b071-7987c143138a"

    action_frame: WSJSONPacketFrame = packet.action_frame  # type: ignore
    action_frame.data = {
        "action": "update",
        "newUpdateId": expected_updated_id,
        "modelKey": "sensor",
        "id": sensor["id"],
    }

    data_frame: WSJSONPacketFrame = packet.data_frame  # type: ignore
    data_frame.data = {"alarmTriggeredAt": to_js_time(now)}

    msg = MagicMock()
    msg.data = packet.pack_frames()

    assert not obj.is_alarm_detected
    with patch("pyunifiprotect.data.bootstrap.utc_now", mock_now):
        protect_client._process_ws_message(msg)
    assert obj.is_alarm_detected
    mock_now.return_value = utc_now() + EVENT_PING_INTERVAL
    assert not obj.is_alarm_detected

    protect_client.emit_message.assert_called_once()
Beispiel #2
0
    async def set_lcd_text(
        self,
        text_type: Optional[DoorbellMessageType],
        text: Optional[str] = None,
        reset_at: Union[None, datetime, DEFAULT_TYPE] = None,
    ) -> None:
        """Sets doorbell LCD text. Requires camera to be doorbell"""

        if not self.feature_flags.has_lcd_screen:
            raise BadRequest("Camera does not have an LCD screen")

        if text_type is None:
            self.lcd_message = None
            # UniFi Protect bug: clearing LCD text message does _not_ emit a WS message
            await self.save_device(force_emit=True)
            return

        if text_type != DoorbellMessageType.CUSTOM_MESSAGE:
            if text is not None:
                raise BadRequest(
                    "Can only set text if text_type is CUSTOM_MESSAGE")
            text = text_type.value.replace("_", " ")

        if reset_at == DEFAULT:
            reset_at = utc_now(
            ) + self.api.bootstrap.nvr.doorbell_settings.default_message_reset_timeout

        self.lcd_message = LCDMessage(api=self._api,
                                      type=text_type,
                                      text=text,
                                      reset_at=reset_at)
        await self.save_device()
Beispiel #3
0
    def _process_device_update(
            self, packet: WSPacket, action: Dict[str, Any], data: Dict[str,
                                                                       Any],
            ignore_stats: bool) -> Optional[WSSubscriptionMessage]:
        model_type = action["modelKey"]

        data = _remove_stats_keys(data, ignore_stats)
        # nothing left to process
        if len(data) == 0:
            self._create_stat(packet, [], True)
            return None

        key = model_type + "s"
        devices = getattr(self, key)
        if action["id"] in devices:
            obj: ProtectModelWithId = devices[action["id"]]
            data = obj.unifi_dict_to_dict(data)
            old_obj = obj.copy()
            obj = obj.update_from_dict(deepcopy(data))
            now = utc_now()

            if isinstance(obj, Event):
                self.process_event(obj)
            elif isinstance(obj, Camera):
                if "last_ring" in data and obj.last_ring:
                    is_recent = obj.last_ring + RECENT_EVENT_MAX >= now
                    _LOGGER.debug("last_ring for %s (%s)", obj.id, is_recent)
                    if is_recent:
                        obj.set_ring_timeout()
            elif isinstance(obj, Sensor):
                if "alarm_triggered_at" in data and obj.alarm_triggered_at:
                    is_recent = obj.alarm_triggered_at + RECENT_EVENT_MAX >= now
                    _LOGGER.debug("alarm_triggered_at for %s (%s)", obj.id,
                                  is_recent)
                    if is_recent:
                        obj.set_alarm_timeout()
                elif "tampering_detected_at" in data and obj.tampering_detected_at:
                    is_recent = obj.tampering_detected_at + RECENT_EVENT_MAX >= now
                    _LOGGER.debug("tampering_detected_at for %s (%s)", obj.id,
                                  is_recent)
                    if is_recent:
                        obj.set_tampering_timeout()

            devices[action["id"]] = obj

            self._create_stat(packet, list(data.keys()), False)
            return WSSubscriptionMessage(
                action=WSAction.UPDATE,
                new_update_id=self.last_update_id,
                changed_data=data,
                new_obj=obj,
                old_obj=old_obj,
            )

        # ignore updates to events that phase out
        if model_type != ModelType.EVENT.value:
            _LOGGER.debug("Unexpected %s: %s", key, action["id"])
        return None
Beispiel #4
0
    def update_from_dict(self, data: Dict[str, Any]) -> Camera:
        # a message in the past is actually a singal to wipe the message
        reset_at = data.get("lcd_message", {}).get("reset_at")
        if reset_at is not None:
            reset_at = from_js_time(reset_at)
            if utc_now() > reset_at:
                data["lcd_message"] = None

        return super().update_from_dict(data)
Beispiel #5
0
    async def async_set_doorbell_lcd_message(self, message: str, duration: str) -> None:
        """Set LCD Message on Doorbell display."""

        reset_at = None
        if duration.isnumeric():
            reset_at = utc_now() + timedelta(minutes=int(duration))

        await self.device.set_lcd_text(
            DoorbellMessageType.CUSTOM_MESSAGE, message, reset_at=reset_at
        )
Beispiel #6
0
    async def async_set_doorbell_message(self, message: str,
                                         duration: str) -> None:
        """Set LCD Message on Doorbell display."""

        if self.entity_description.key != _KEY_DOORBELL_TEXT:
            raise HomeAssistantError("Not a doorbell text select entity")

        assert isinstance(self.device, Camera)
        reset_at = None
        if duration.isnumeric():
            reset_at = utc_now() + timedelta(minutes=int(duration))

        await self.device.set_lcd_text(DoorbellMessageType.CUSTOM_MESSAGE,
                                       message,
                                       reset_at=reset_at)
Beispiel #7
0
    async def get_events_raw(
        self,
        start: Optional[datetime] = None,
        end: Optional[datetime] = None,
        limit: Optional[int] = None,
        camera_ids: Optional[List[str]] = None,
    ) -> List[Dict[str, Any]]:
        """
        Get list of events from Protect

        Args:

        * `start`: start time for events
        * `end`: end time for events
        * `limit`: max number of events to return
        * `camera_ids`: list of Cameras to get events for

        If `limit`, `start` and `end` are not provided, it will default to all events in the last 24 hours.

        If `start` is provided, then `end` or `limit` must be provided. If `end` is provided, then `start` or
        `limit` must be provided. Otherwise, you will get a 400 error from Unifi Protect

        Providing a list of Camera IDs will not prevent non-camera events from returning.
        """

        # if no parameters are passed in, default to all events from last 24 hours
        if limit is None and start is None and end is None:
            end = utc_now() + timedelta(seconds=10)
            start = end - timedelta(hours=24)

        params: Dict[str, Any] = {}
        if limit is not None:
            params["limit"] = limit

        if start is not None:
            params["start"] = to_js_time(start)

        if end is not None:
            params["end"] = to_js_time(end)

        if camera_ids is not None:
            params["cameras"] = ",".join(camera_ids)

        return await self.api_request_list("events", params=params)
Beispiel #8
0
    async def update(self, force: bool = False) -> Optional[Bootstrap]:
        """
        Updates the state of devices, initalizes `.bootstrap` and
        connects to UFP Websocket for real time updates

        You can use the various other `get_` methods if you need one off data from UFP
        """

        now = time.monotonic()
        now_dt = utc_now()
        max_event_dt = now_dt - timedelta(hours=24)
        if force:
            self._last_update = NEVER_RAN
            self._last_update_dt = max_event_dt

        bootstrap_updated = False
        if self._bootstrap is None or now - self._last_update > DEVICE_UPDATE_INTERVAL:
            bootstrap_updated = True
            self._last_update = now
            self._last_update_dt = now_dt
            self._bootstrap = await self.get_bootstrap()

        await self.async_connect_ws(force)
        active_ws = self.check_ws()
        if not bootstrap_updated and active_ws:
            # If the websocket is connected/connecting
            # we do not need to get events
            _LOGGER.debug("Skipping update since websocket is active")
            return None

        events = await self.get_events(start=self._last_update_dt
                                       or max_event_dt,
                                       end=now_dt)
        for event in events:
            self.bootstrap.process_event(event)

        self._last_update = now
        self._last_update_dt = now_dt
        return self._bootstrap
    def _async_update_device_from_protect(self) -> None:
        super()._async_update_device_from_protect()

        if self.entity_description.ufp_value is None:
            return

        if self.entity_description.key == _KEY_DOORBELL:
            last_ring = get_nested_attr(self.device,
                                        self.entity_description.ufp_value)
            now = utc_now()

            is_ringing = (False if last_ring is None else
                          (now - last_ring) < RING_INTERVAL)
            if is_ringing:
                if self._doorbell_callback is not None:
                    self._doorbell_callback.cancel()
                self._doorbell_callback = asyncio.ensure_future(
                    self._async_wait_for_doorbell(last_ring + RING_INTERVAL))
            self._attr_is_on = is_ringing
        else:
            self._attr_is_on = get_nested_attr(
                self.device, self.entity_description.ufp_value)
Beispiel #10
0
    async def get_camera_snapshot(
        self,
        camera_id: str,
        width: Optional[int] = None,
        height: Optional[int] = None,
    ) -> Optional[bytes]:
        """Gets a snapshot from a camera"""

        dt = utc_now()  # ts is only used as a cache buster
        params = {
            "ts": to_js_time(dt),
            "force": "true",
        }

        if width is not None:
            params.update({"w": width})

        if height is not None:
            params.update({"h": height})

        return await self.api_request_raw(f"cameras/{camera_id}/snapshot",
                                          params=params,
                                          raise_exception=False)
Beispiel #11
0
    def get_changed(self) -> Dict[str, Any]:
        updated = super().get_changed()

        if "lcd_message" in updated:
            lcd_message = updated["lcd_message"]
            # to "clear" LCD message, set reset_at to a time in the past
            if lcd_message is None:
                updated["lcd_message"] = {
                    "reset_at": utc_now() - timedelta(seconds=10)
                }
            # otherwise, pass full LCD message to prevent issues
            elif self.lcd_message is not None:
                updated["lcd_message"] = self.lcd_message.dict()

            # if reset_at is not passed in, it will default to reset in 1 minute
            if lcd_message is not None and "reset_at" not in lcd_message:
                if self.lcd_message is None:
                    updated["lcd_message"]["reset_at"] = None
                else:
                    updated["lcd_message"][
                        "reset_at"] = self.lcd_message.reset_at

        return updated
 async def _async_wait_for_doorbell(self, end_time: datetime) -> None:
     _LOGGER.debug("Doorbell callback started")
     while utc_now() < end_time:
         await asyncio.sleep(1)
     _LOGGER.debug("Doorbell callback ended")
     self._async_updated_event()