Esempio n. 1
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()
Esempio n. 2
0
    async def add_custom_doorbell_message(self, message: str) -> None:
        """Adds custom doorbell message"""

        if len(message) > 30:
            raise BadRequest("Message length over 30 characters")

        if message in self.doorbell_settings.custom_messages:
            raise BadRequest("Custom doorbell message already exists")

        self.doorbell_settings.custom_messages.append(DoorbellText(message))
        await self.save_device()
        self.update_all_messages()
Esempio n. 3
0
    def __init__(self,
                 camera: Camera,
                 content_url: str,
                 ffmpeg_path: Optional[Path] = None):
        if not camera.feature_flags.has_speaker:
            raise BadRequest("Camera does not have a speaker for talkback")

        input_args = self.get_args_from_url(content_url)
        if len(input_args) > 0:
            input_args += " "

        # from Android app
        bitrate = 48000
        # 8000 seems to result in best quality without overloading the camera
        udp_bitrate = bitrate + 8000

        # vn = no video
        # acodec = audio codec to encode output in (aac)
        # ac = number of output channels (1)
        # ar = output sampling rate (22050)
        # b:a = set bit rate of output audio
        cmd = (
            "-loglevel info -hide_banner "
            f"{input_args}-i {content_url} -vn "
            f"-acodec {camera.talkback_settings.type_fmt} -ac {camera.talkback_settings.channels} "
            f"-ar {camera.talkback_settings.sampling_rate} -b:a {bitrate} -map 0:a "
            f'-f adts "udp://{camera.host}:{camera.talkback_settings.bind_port}?bitrate={udp_bitrate}"'
        )

        super().__init__(cmd, ffmpeg_path)
Esempio n. 4
0
    async def save_device(self, force_emit: bool = False) -> None:
        """
        Generates a diff for unsaved changed on the device and sends them back to UFP

        USE WITH CAUTION, updates _all_ fields for the current object that have been changed.
        May have unexpected side effects.

        Tested updates have been added a methods on applicable devices.

        Args:

        * `force_emit`: Emit a fake UFP WS message. Should only be use for when UFP does not properly emit a WS message
        """

        if self.model is None:
            raise BadRequest("Unknown model type")

        new_data = self.dict(exclude=self._get_excluded_changed_fields())
        updated = self.unifi_dict(data=self.get_changed())

        # do not patch when there are no updates
        if updated == {}:
            return

        await self._api_update(updated)
        self._initial_data = new_data

        if not force_emit:
            return

        await self.emit_message(updated)
Esempio n. 5
0
    def create_talkback_stream(
            self,
            content_url: str,
            ffmpeg_path: Optional[Path] = None) -> TalkbackStream:
        """
        Creates a subprocess to play audio to a camera through its speaker.

        Requires ffmpeg to use.

        Args:

        * `content_url`: Either a URL accessible by python or a path to a file (ffmepg's `-i` parameter)
        * `ffmpeg_path`: Optional path to ffmpeg binary

        Use either `await stream.run_until_complete()` or `await stream.start()` to start subprocess command
        after getting the stream.

        `.play_audio()` is a helper that wraps this method and automatically runs the subprocess as well
        """

        if self.talkback_stream is not None and self.talkback_stream.is_running:
            raise BadRequest("Camera is already playing audio")

        self.talkback_stream = TalkbackStream(self, content_url, ffmpeg_path)
        return self.talkback_stream
Esempio n. 6
0
    async def set_light_settings(
        self,
        mode: LightModeType,
        enable_at: Optional[LightModeEnableType] = None,
        duration: Optional[timedelta] = None,
        sensitivity: Optional[PercentInt] = None,
    ) -> None:
        """Updates various Light settings.

        Args:

        * `mode`: Light trigger mode
        * `enable_at`: Then the light automatically turns on by itself
        * `duration`: How long the light should remain on after motion, must be timedelta between 15s and 900s
        * `sensitivity`: PIR Motion sensitivity
        """

        self.light_mode_settings.mode = mode
        if enable_at is not None:
            self.light_mode_settings.enable_at = enable_at
        if duration is not None:
            if duration.total_seconds() < 15 or duration.total_seconds() > 900:
                raise BadRequest("Duration outside of 15s to 900s range")

            self.light_device_settings.pir_duration = duration
        if sensitivity is not None:
            self.light_device_settings.pir_sensitivity = sensitivity

        await self.save_device()
Esempio n. 7
0
    async def get_video(self, channel_index: int = 0) -> Optional[bytes]:
        """Get the MP4 video clip for this given event

        Args:

        * `channel_index`: index of `CameraChannel` on the camera to use to retrieve video from

        Will raise an exception if event does not have a camera, end time or the channel index is wrong.
        """

        if self.camera is None:
            raise BadRequest("Event does not have a camera")
        if self.end is None:
            raise BadRequest("Event is ongoing")

        return await self.api.get_camera_video(self.camera.id, self.start, self.end, channel_index)
Esempio n. 8
0
    async def emit_message(self, updated: Dict[str, Any]) -> None:
        """Emites fake WS message for ProtectApiClient to process."""

        if updated == {}:
            _LOGGER.debug("Event ping callback started for %s", self.id)

        if self.model is None:
            raise BadRequest("Unknown model type")

        header = WSPacketFrameHeader(
            packet_type=1,
            payload_format=ProtectWSPayloadFormat.JSON.value,
            deflated=0,
            unknown=1,
            payload_size=1)

        action_frame = WSJSONPacketFrame()
        action_frame.header = header
        action_frame.data = {
            "action": "update",
            "newUpdateId": None,
            "modelKey": self.model.value,
            "id": self.id
        }

        data_frame = WSJSONPacketFrame()
        data_frame.header = header
        data_frame.data = updated

        message = self.api.bootstrap.process_ws_packet(
            WSPacket(action_frame.packed + data_frame.packed))
        if message is not None:
            self.api.emit_message(message)
Esempio n. 9
0
    async def set_speaker_volume(self, level: PercentInt) -> None:
        """Sets the speaker sensitivity level on camera"""

        if not self.feature_flags.has_speaker:
            raise BadRequest("Camera does not have speaker")

        self.speaker_settings.volume = level
        await self.save_device()
Esempio n. 10
0
    async def set_mic_volume(self, level: PercentInt) -> None:
        """Sets the mic sensitivity level on camera"""

        if not self.feature_flags.has_mic:
            raise BadRequest("Camera does not have mic")

        self.mic_volume = level
        await self.save_device()
Esempio n. 11
0
    async def set_wdr_level(self, level: WDRLevel) -> None:
        """Sets WDR (Wide Dynamic Range) on camera"""

        if self.feature_flags.has_hdr:
            raise BadRequest("Cannot set WDR on cameras with HDR")

        self.isp_settings.wdr = level
        await self.save_device()
Esempio n. 12
0
    async def set_video_mode(self, mode: VideoMode) -> None:
        """Sets video mode on camera"""

        if mode not in self.feature_flags.video_modes:
            raise BadRequest(f"Camera does not have {mode}")

        self.video_mode = mode
        await self.save_device()
Esempio n. 13
0
    async def set_hdr(self, enabled: bool) -> None:
        """Sets HDR (High Dynamic Range) on camera"""

        if not self.feature_flags.has_hdr:
            raise BadRequest("Camera does not have HDR")

        self.hdr_mode = enabled
        await self.save_device()
Esempio n. 14
0
    async def set_ir_led_model(self, mode: IRLEDMode) -> None:
        """Sets IR LED mode on camera"""

        if not self.feature_flags.has_led_ir:
            raise BadRequest("Camera does not have an LED IR")

        self.isp_settings.ir_led_mode = mode
        await self.save_device()
Esempio n. 15
0
    async def set_duration(self, duration: timedelta) -> None:
        """Sets motion sensitivity"""

        if duration.total_seconds() < 15 or duration.total_seconds() > 900:
            raise BadRequest("Duration outside of 15s to 900s range")

        self.light_device_settings.pir_duration = duration
        await self.save_device()
Esempio n. 16
0
    async def set_camera_zoom(self, level: PercentInt) -> None:
        """Sets zoom level for camera"""

        if not self.feature_flags.can_optical_zoom:
            raise BadRequest("Camera cannot optical zoom")

        self.isp_settings.zoom_position = level
        await self.save_device()
Esempio n. 17
0
    async def set_chime_duration(self, duration: ChimeDuration) -> None:
        """Sets chime duration for doorbell. Requires camera to be a doorbell"""

        if not self.feature_flags.has_chime:
            raise BadRequest("Camera does not have a chime")

        self.chime_duration = duration
        await self.save_device()
Esempio n. 18
0
    async def set_status_light(self, enabled: bool) -> None:
        """Sets status indicicator light on camera"""

        if not self.feature_flags.has_led_status:
            raise BadRequest("Camera does not have status light")

        self.led_settings.is_enabled = enabled
        self.led_settings.blink_rate = 0
        await self.save_device()
Esempio n. 19
0
    async def remove_custom_doorbell_message(self, message: str) -> None:
        """Removes custom doorbell message"""

        if message not in self.doorbell_settings.custom_messages:
            raise BadRequest("Custom doorbell message does not exists")

        self.doorbell_settings.custom_messages.remove(DoorbellText(message))
        await self.save_device()
        self.update_all_messages()
Esempio n. 20
0
    def api(self) -> ProtectApiClient:
        """
        ProtectApiClient that the UFP object was created with. If no API Client was passed in time of
        creation, will raise `BadRequest`
        """
        if self._api is None:
            raise BadRequest("API Client not initialized")

        return self._api
Esempio n. 21
0
    async def save_device(self, force_emit: bool = False) -> None:
        """
        Generates a diff for unsaved changed on the device and sends them back to UFP

        USE WITH CAUTION, updates _all_ fields for the current object that have been changed.
        May have unexpected side effects.

        Tested updates have been added a methods on applicable devices.

        Args:

        * `force_emit`: Emit a fake UFP WS message. Should only be use for when UFP does not properly emit a WS message
        """

        if self.model is None:
            raise BadRequest("Unknown model type")

        new_data = self.dict(exclude=self._get_excluded_changed_fields())
        updated = self.unifi_dict(data=self.get_changed())

        # do not patch when there are no updates
        if updated == {}:
            return

        await self._api_update(updated)
        self._initial_data = new_data

        if not force_emit:
            return

        header = WSPacketFrameHeader(
            packet_type=1,
            payload_format=ProtectWSPayloadFormat.JSON.value,
            deflated=0,
            unknown=1,
            payload_size=1)

        action_frame = WSJSONPacketFrame()
        action_frame.header = header
        action_frame.data = {
            "action": "update",
            "newUpdateId": None,
            "modelKey": self.model.value,
            "id": self.id
        }

        data_frame = WSJSONPacketFrame()
        data_frame.header = header
        data_frame.data = updated

        message = self.api.bootstrap.process_ws_packet(
            WSPacket(action_frame.packed + data_frame.packed))
        if message is not None:
            self.api.emit_message(message)
Esempio n. 22
0
    def __init__(self,
                 camera: Camera,
                 content_url: str,
                 ffmpeg_path: Optional[Path] = None):
        if not camera.feature_flags.has_speaker:
            raise BadRequest("Camera does not have a speaker for talkback")

        input_args = self.get_args_from_url(content_url)
        if len(input_args) > 0:
            input_args += " "

        cmd = f"-loglevel info -hide_banner {input_args}-i {content_url} -vn -acodec {camera.talkback_settings.type_fmt} -ac {camera.talkback_settings.channels} -ar {camera.talkback_settings.sampling_rate} -bits_per_raw_sample {camera.talkback_settings.bits_per_sample} -map 0:a -aq {camera.talkback_settings.quality} -f adts udp://{camera.host}:{camera.talkback_settings.bind_port}"

        super().__init__(cmd, ffmpeg_path)
Esempio n. 23
0
    async def get_smart_detect_track(self) -> SmartDetectTrack:
        """
        Gets smart detect track for given smart detect event.

        If event is not a smart detect event, it will raise a `BadRequest`
        """

        if self.type != EventType.SMART_DETECT:
            raise BadRequest("Not a smart detect event")

        if self._smart_detect_track is None:
            self._smart_detect_track = await self.api.get_event_smart_detect_track(self.id)

        return self._smart_detect_track
Esempio n. 24
0
    async def set_liveview(self, liveview: Liveview) -> None:
        """
        Sets the liveview current set for the viewer

        Args:

        * `liveview`: The liveview you want to set
        """

        if self._api is not None:
            if liveview.id not in self._api.bootstrap.liveviews:
                raise BadRequest("Unknown liveview")

        self.liveview_id = liveview.id
        # UniFi Protect bug: changing the liveview does _not_ emit a WS message
        await self.save_device(force_emit=True)
Esempio n. 25
0
    async def get_smart_detect_zones(self) -> Dict[int, CameraZone]:
        """Gets the triggering zones for the smart detection"""

        if self.camera is None:
            raise BadRequest("No camera on event")

        if self._smart_detect_zones is None:
            smart_track = await self.get_smart_detect_track()

            ids: Set[int] = set()
            for item in smart_track.payload:
                ids = ids | set(item.zone_ids)

            self._smart_detect_zones = {z.id: z for z in self.camera.smart_detect_zones if z.id in ids}

        return self._smart_detect_zones
Esempio n. 26
0
    async def set_privacy(
            self,
            enabled: bool,
            mic_level: Optional[PercentInt] = None,
            recording_mode: Optional[RecordingMode] = None) -> None:
        """Adds/removes a privacy zone that blacks out the whole camera"""

        if not self.feature_flags.has_privacy_mask:
            raise BadRequest("Camera does not allow privacy zones")

        if enabled:
            self.add_privacy_zone()
        else:
            self.remove_privacy_zone()

        if mic_level is not None:
            self.mic_volume = mic_level

        if recording_mode is not None:
            self.recording_settings.mode = recording_mode

        await self.save_device()
Esempio n. 27
0
    def bootstrap(self) -> Bootstrap:
        if self._bootstrap is None:
            raise BadRequest("Client not initalized, run `update` first")

        return self._bootstrap