From 141d44daa77fe5be84555e7dcddfb455b4d9e066 Mon Sep 17 00:00:00 2001 From: RaHehl <7577984+RaHehl@users.noreply.github.com> Date: Mon, 16 Dec 2024 20:48:01 +0100 Subject: [PATCH 01/10] feat: add AiPort support --- src/uiprotect/api.py | 18 +- src/uiprotect/data/__init__.py | 2 + src/uiprotect/data/bootstrap.py | 2 + src/uiprotect/data/convert.py | 2 + src/uiprotect/data/devices.py | 12 + src/uiprotect/data/types.py | 2 + src/uiprotect/test_util/__init__.py | 17 + tests/conftest.py | 2 +- tests/sample_data/sample_bootstrap.json | 673 ++++++++++++++++++++++++ tests/sample_data/sample_camera.json | 1 + 10 files changed, 729 insertions(+), 2 deletions(-) diff --git a/src/uiprotect/api.py b/src/uiprotect/api.py index d7532cb1..07f8156a 100644 --- a/src/uiprotect/api.py +++ b/src/uiprotect/api.py @@ -57,7 +57,7 @@ create_from_unifi_dict, ) from .data.base import ProtectModelWithId -from .data.devices import Chime +from .data.devices import AiPort, Chime from .data.types import IteratorCallback, ProgressCallback from .exceptions import BadRequest, NotAuthorized, NvrError from .utils import ( @@ -1268,6 +1268,14 @@ async def get_chimes(self) -> list[Chime]: """ return cast(list[Chime], await self.get_devices(ModelType.CHIME, Chime)) + async def get_aiports(self) -> list[AiPort]: + """ + Gets the list of aiports straight from the NVR. + + The websocket is connected and running, you likely just want to use `self.bootstrap.aiports` + """ + return cast(list[AiPort], await self.get_devices(ModelType.AIPORT, AiPort)) + async def get_viewers(self) -> list[Viewer]: """ Gets the list of viewers straight from the NVR. @@ -1386,6 +1394,14 @@ async def get_chime(self, device_id: str) -> Chime: """ return cast(Chime, await self.get_device(ModelType.CHIME, device_id, Chime)) + async def get_aiport(self, device_id: str) -> AiPort: + """ + Gets a AiPort straight from the NVR. + + The websocket is connected and running, you likely just want to use `self.bootstrap.aiport[device_id]` + """ + return cast(AiPort, await self.get_device(ModelType.AIPORT, device_id, AiPort)) + async def get_viewer(self, device_id: str) -> Viewer: """ Gets a viewer straight from the NVR. diff --git a/src/uiprotect/data/__init__.py b/src/uiprotect/data/__init__.py index 216a5bf7..6171d47d 100644 --- a/src/uiprotect/data/__init__.py +++ b/src/uiprotect/data/__init__.py @@ -10,6 +10,7 @@ from .bootstrap import Bootstrap from .convert import create_from_unifi_dict from .devices import ( + AiPort, Bridge, Camera, CameraChannel, @@ -85,6 +86,7 @@ "DEFAULT_TYPE", "NVR", "WS_HEADER_SIZE", + "AiPort", "AnalyticsOption", "AudioStyle", "Bootstrap", diff --git a/src/uiprotect/data/bootstrap.py b/src/uiprotect/data/bootstrap.py index 2f76bd8f..ea5c3e16 100644 --- a/src/uiprotect/data/bootstrap.py +++ b/src/uiprotect/data/bootstrap.py @@ -23,6 +23,7 @@ ) from .convert import MODEL_TO_CLASS, create_from_unifi_dict from .devices import ( + AiPort, Bridge, Camera, Chime, @@ -181,6 +182,7 @@ class Bootstrap(ProtectBaseObject): sensors: dict[str, Sensor] doorlocks: dict[str, Doorlock] chimes: dict[str, Chime] + aiports: dict[str, AiPort] last_update_id: str # TODO: diff --git a/src/uiprotect/data/convert.py b/src/uiprotect/data/convert.py index 050392ef..fa330a64 100644 --- a/src/uiprotect/data/convert.py +++ b/src/uiprotect/data/convert.py @@ -8,6 +8,7 @@ from ..exceptions import DataDecodeError from .devices import ( + AiPort, Bridge, Camera, Chime, @@ -40,6 +41,7 @@ ModelType.SENSOR: Sensor, ModelType.DOORLOCK: Doorlock, ModelType.CHIME: Chime, + ModelType.AIPORT: AiPort, ModelType.KEYRING: Keyring, ModelType.ULP_USER: UlpUser, } diff --git a/src/uiprotect/data/devices.py b/src/uiprotect/data/devices.py index 93b378c0..28871b31 100644 --- a/src/uiprotect/data/devices.py +++ b/src/uiprotect/data/devices.py @@ -976,6 +976,8 @@ class Camera(ProtectMotionDeviceModel): audio_settings: CameraAudioSettings | None = None # requires 5.0.33+ is_third_party_camera: bool | None = None + # requires 5.1.78+ + is_paired_with_ai_port: bool | None = None # TODO: used for adopting # apMac read only # apRssi read only @@ -3382,3 +3384,13 @@ def callback() -> None: raise BadRequest("Camera %s is not paired with chime", camera.id) await self.queue_update(callback) + +class AiPort(Camera): + paired_cameras: list[str] + + @property + def paired_cameras(self) -> list[Camera]: + """Paired Cameras for AiPort""" + if len(self.paired_cameras) == 0: + return [] + return [self._api.bootstrap.cameras[c] for c in self.paired_cameras] diff --git a/src/uiprotect/data/types.py b/src/uiprotect/data/types.py index 097c6796..d24be375 100644 --- a/src/uiprotect/data/types.py +++ b/src/uiprotect/data/types.py @@ -124,6 +124,7 @@ class ModelType(str, UnknownValuesEnumMixin, enum.Enum): DOORLOCK = "doorlock" SCHEDULE = "schedule" CHIME = "chime" + AIPORT = "aiport" DEVICE_GROUP = "deviceGroup" RECORDING_SCHEDULE = "recordingSchedule" ULP_USER = "ulpUser" @@ -173,6 +174,7 @@ def _bootstrap_model_types(cls) -> tuple[ModelType, ...]: ModelType.SENSOR, ModelType.DOORLOCK, ModelType.CHIME, + ModelType.AIPORT, ) @classmethod diff --git a/src/uiprotect/test_util/__init__.py b/src/uiprotect/test_util/__init__.py index af37d872..eb4efedf 100644 --- a/src/uiprotect/test_util/__init__.py +++ b/src/uiprotect/test_util/__init__.py @@ -131,6 +131,7 @@ async def async_generate(self, close_session: bool = True) -> None: "sensor": len(bootstrap["sensors"]), "doorlock": len(bootstrap["doorlocks"]), "chime": len(bootstrap["chimes"]), + "aiport": len(bootstrap["aiports"]), } self.log("Generating event data...") @@ -283,6 +284,7 @@ async def generate_device_data( self.generate_sensor_data(), self.generate_lock_data(), self.generate_chime_data(), + self.generate_aiport_data(), self.generate_bridge_data(), self.generate_liveview_data(), ) @@ -469,6 +471,21 @@ async def generate_chime_data(self) -> None: obj = await self.client.api_request_obj(f"chimes/{device_id}") await self.write_json_file("sample_chime", obj) + async def generate_aiport_data(self) -> None: + objs = await self.client.api_request_list("aiports") + device_id: str | None = None + for obj_dict in objs: + device_id = obj_dict["id"] + if is_online(obj_dict): + break + + if device_id is None: + self.log("No aiport found. Skipping aiport endpoints...") + return + + obj = await self.client.api_request_obj(f"aiports/{device_id}") + await self.write_json_file("sample_aiport", obj) + async def generate_bridge_data(self) -> None: objs = await self.client.api_request_list("bridges") device_id: str | None = None diff --git a/tests/conftest.py b/tests/conftest.py index b272cc0a..02446a29 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -781,7 +781,7 @@ def compare_objs(obj_type, expected, actual): actual = deepcopy(actual) # TODO: fields not supported yet - if obj_type == ModelType.CAMERA.value: + if obj_type in (ModelType.CAMERA.value, ModelType.AIPORT.value): # fields does not always exist (G4 Instant) expected.pop("apMac", None) # field no longer exists on newer cameras diff --git a/tests/sample_data/sample_bootstrap.json b/tests/sample_data/sample_bootstrap.json index 55323b87..5bc7a687 100644 --- a/tests/sample_data/sample_bootstrap.json +++ b/tests/sample_data/sample_bootstrap.json @@ -488,6 +488,7 @@ "canManage": false, "isManaged": true, "marketName": "G4 Dome", + "isPairedWithAiPort": false, "modelKey": "camera" }, { @@ -1041,6 +1042,7 @@ "canManage": false, "isManaged": true, "marketName": "G4 Doorbell", + "isPairedWithAiPort": false, "modelKey": "camera" }, { @@ -1529,6 +1531,7 @@ "canManage": false, "isManaged": true, "marketName": "G4 Bullet", + "isPairedWithAiPort": false, "modelKey": "camera" }, { @@ -2110,6 +2113,7 @@ "canManage": false, "isManaged": true, "marketName": "G4 Doorbell Pro", + "isPairedWithAiPort": true, "modelKey": "camera" }, { @@ -2608,6 +2612,7 @@ "canManage": false, "isManaged": true, "marketName": "AI Bullet", + "isPairedWithAiPort": false, "modelKey": "camera" }, { @@ -3118,6 +3123,7 @@ "canManage": false, "isManaged": true, "marketName": "G4 Doorbell", + "isPairedWithAiPort": false, "modelKey": "camera" }, { @@ -3618,6 +3624,7 @@ "canManage": false, "isManaged": true, "marketName": "G4 Pro", + "isPairedWithAiPort": false, "modelKey": "camera" }, { @@ -4118,6 +4125,7 @@ "canManage": false, "isManaged": true, "marketName": "G4 Pro", + "isPairedWithAiPort": false, "modelKey": "camera" }, { @@ -4626,6 +4634,7 @@ "canManage": false, "isManaged": true, "marketName": "G4 Pro", + "isPairedWithAiPort": false, "modelKey": "camera" }, { @@ -5130,6 +5139,7 @@ "canManage": false, "isManaged": true, "marketName": "G4 Pro", + "isPairedWithAiPort": false, "modelKey": "camera" }, { @@ -5615,6 +5625,7 @@ "canManage": false, "isManaged": true, "marketName": "G4 Instant", + "isPairedWithAiPort": false, "modelKey": "camera" } ], @@ -7264,5 +7275,667 @@ "marketName": "UP Chime", "modelKey": "chime" } + ], + "aiports": [ + { + "isDeleting": false, + "mac": "942A6FERF45E", + "host": "192.168.144.142", + "connectionHost": "192.168.144.141", + "type": "UVC AI Port", + "name": "AI Port", + "upSince": 1734358286292, + "uptime": 5711, + "lastSeen": 1734363997292, + "connectedSince": 1734358304453, + "state": "CONNECTED", + "lastDisconnect": 1734358270257, + "hardwareRevision": "9", + "firmwareVersion": "4.73.7", + "latestFirmwareVersion": null, + "firmwareBuild": "02c7590.241202.29", + "isUpdating": false, + "isDownloadingFW": false, + "fwUpdateState": "upToDate", + "isAdopting": false, + "isRestoring": false, + "isAdopted": true, + "isAdoptedByOther": false, + "isProvisioned": true, + "isRebooting": false, + "isSshEnabled": false, + "canAdopt": false, + "isAttemptingToConnect": false, + "uplinkDevice": null, + "guid": "d856a941-980d-4002-822c-6cc419266f9a", + "anonymousDeviceId": "08752e43-0d7f-4340-b2fe-9d2a5e89ba2b", + "lastMotion": null, + "micVolume": 100, + "isMicEnabled": true, + "isRecording": false, + "isWirelessUplinkEnabled": true, + "isMotionDetected": false, + "isSmartDetected": false, + "phyRate": null, + "hdrMode": true, + "videoMode": "default", + "isProbingForWifi": false, + "apMac": null, + "apRssi": null, + "apMgmtIp": null, + "elementInfo": null, + "chimeDuration": 0, + "isDark": false, + "lastPrivacyZonePositionId": null, + "lastRing": null, + "isLiveHeatmapEnabled": false, + "eventStats": { + "motion": { + "today": 2, + "average": 0, + "lastDays": [], + "recentHours": [ + null + ] + }, + "smart": { + "today": 0, + "average": 0, + "lastDays": [] + } + }, + "videoReconfigurationInProgress": false, + "voltage": null, + "useGlobal": false, + "isPoorNetwork": false, + "stopStreamLevel": null, + "isWaterproofCaseAttached": false, + "userConfiguredAp": false, + "hasRecordings": false, + "isThirdPartyCamera": false, + "isPairedWithAiPort": false, + "wiredConnectionState": { + "phyRate": null + }, + "wifiConnectionState": { + "channel": null, + "frequency": null, + "phyRate": null, + "txRate": null, + "signalQuality": null, + "ssid": null, + "bssid": null, + "apName": null, + "experience": null, + "signalStrength": null, + "connectivity": null + }, + "channels": [ + { + "id": 0, + "videoId": "video1", + "name": "High", + "enabled": true, + "isRtspEnabled": false, + "rtspAlias": null, + "width": 3840, + "height": 2160, + "fps": 24, + "bitrate": 8000000, + "minBitrate": 2000000, + "maxBitrate": 8000000, + "minClientAdaptiveBitRate": 0, + "minMotionAdaptiveBitRate": 2000000, + "fpsValues": [ + 1, + 3, + 5, + 8, + 10, + 12, + 15, + 16, + 18, + 20, + 22, + 24 + ], + "idrInterval": 5, + "autoFps": true, + "autoBitrate": true + }, + { + "id": 1, + "videoId": "video2", + "name": "Medium", + "enabled": true, + "isRtspEnabled": false, + "rtspAlias": null, + "width": 1280, + "height": 720, + "fps": 24, + "bitrate": 2000000, + "minBitrate": 750000, + "maxBitrate": 8000000, + "minClientAdaptiveBitRate": 150000, + "minMotionAdaptiveBitRate": 750000, + "fpsValues": [ + 1, + 3, + 5, + 8, + 10, + 12, + 15, + 16, + 18, + 20, + 22, + 24 + ], + "idrInterval": 5, + "autoFps": true, + "autoBitrate": true + }, + { + "id": 2, + "videoId": "video3", + "name": "Low", + "enabled": true, + "isRtspEnabled": false, + "rtspAlias": null, + "width": 640, + "height": 360, + "fps": 24, + "bitrate": 900000, + "minBitrate": 300000, + "maxBitrate": 8000000, + "minClientAdaptiveBitRate": 0, + "minMotionAdaptiveBitRate": 300000, + "fpsValues": [ + 1, + 3, + 5, + 8, + 10, + 12, + 15, + 16, + 18, + 20, + 22, + 24 + ], + "idrInterval": 5, + "autoFps": true, + "autoBitrate": true + } + ], + "ispSettings": { + "aeMode": "auto", + "irLedMode": "auto", + "irLedLevel": 255, + "wdr": 1, + "icrSensitivity": 0, + "icrSwitchMode": "sensitivity", + "icrCustomValue": 2, + "brightness": 50, + "contrast": 50, + "hue": 50, + "saturation": 50, + "sharpness": 50, + "denoise": 50, + "isColorNightVisionEnabled": false, + "spotlightDuration": 15, + "isFlippedVertical": false, + "isFlippedHorizontal": false, + "isAutoRotateEnabled": false, + "isLdcEnabled": true, + "is3dnrEnabled": true, + "isExternalIrEnabled": false, + "isAggressiveAntiFlickerEnabled": false, + "isPauseMotionEnabled": false, + "dZoomCenterX": 50, + "dZoomCenterY": 50, + "dZoomScale": 0, + "dZoomStreamId": 4, + "focusMode": "ztrig", + "focusPosition": 0, + "touchFocusX": 1001, + "touchFocusY": 1001, + "zoomPosition": 0, + "mountPosition": null, + "hdrMode": "normal" + }, + "audioSettings": { + "style": [ + "nature" + ] + }, + "talkbackSettings": { + "typeFmt": "aac", + "typeIn": "serverudp", + "bindAddr": "0.0.0.0", + "bindPort": 7004, + "filterAddr": null, + "filterPort": null, + "channels": 1, + "samplingRate": 22050, + "bitsPerSample": 16, + "quality": 100 + }, + "osdSettings": { + "isNameEnabled": false, + "isDateEnabled": false, + "isLogoEnabled": true, + "isDebugEnabled": false + }, + "ledSettings": { + "isEnabled": true, + "blinkRate": 0 + }, + "speakerSettings": { + "isEnabled": true, + "areSystemSoundsEnabled": false, + "volume": 80 + }, + "recordingSettings": { + "prePaddingSecs": 2, + "postPaddingSecs": 2, + "smartDetectPrePaddingSecs": 2, + "smartDetectPostPaddingSecs": 2, + "minMotionEventTrigger": 1000, + "endMotionEventDelay": 3000, + "suppressIlluminationSurge": false, + "mode": "always", + "inScheduleMode": "always", + "outScheduleMode": "never", + "geofencing": "off", + "retentionDurationMs": null, + "motionAlgorithm": "enhanced", + "enableMotionDetection": true, + "useNewMotionAlgorithm": true + }, + "smartDetectSettings": { + "objectTypes": [ + "person", + "vehicle", + "package", + "animal", + "face", + "licensePlate" + ], + "autoTrackingObjectTypes": [], + "audioTypes": [ + "alrmSmoke", + "alrmCmonx", + "alrmSiren", + "alrmBabyCry", + "alrmSpeak", + "alrmBurglar", + "alrmCarHorn", + "alrmBark", + "alrmGlassBreak" + ] + }, + "recordingSchedulesV2": [], + "motionZones": [ + { + "id": 1, + "name": "Default", + "color": "#AB46BC", + "points": [ + [ + 0.249, + 0 + ], + [ + 0.517, + 0 + ], + [ + 0.525, + 0.62 + ], + [ + 0.7, + 0.622 + ], + [ + 1, + 0.689 + ], + [ + 1, + 1 + ], + [ + 0, + 1 + ], + [ + 0.205, + 0.611 + ] + ], + "sensitivity": 50, + "isTriggerLightEnabled": true + } + ], + "privacyZones": [], + "smartDetectZones": [ + { + "id": 2, + "name": "Default", + "color": "#AB46BC", + "points": [ + [ + 0.229, + 0 + ], + [ + 0.524, + 0 + ], + [ + 0.526, + 0.602 + ], + [ + 0.692, + 0.6 + ], + [ + 1, + 0.667 + ], + [ + 1, + 1 + ], + [ + 0, + 1 + ], + [ + 0.201, + 0.62 + ] + ], + "sensitivity": 50, + "objectTypes": [ + "animal", + "face", + "vehicle", + "person" + ], + "isTriggerLightEnabled": true, + "source": "unifi-protect", + "triggerAccessTypes": [], + "enableAccessLPOnlyMode": false + } + ], + "smartDetectLines": [ + { + "id": 3, + "name": "New Crossing Line", + "color": "#46C7FD", + "points": [ + [ + 0.69, + 0.609 + ], + [ + 0.53, + 0.609 + ] + ], + "sensitivity": 50, + "objectTypes": [ + "person", + "vehicle", + "animal" + ], + "isTriggerLightEnabled": false, + "direction": "BA", + "isTargetCounting": false, + "plan": { + "autoReset": "none", + "time": null, + "weekDay": null, + "date": null + } + }, + { + "id": 4, + "name": "New Crossing Line", + "color": "#61E066", + "points": [ + [ + 1, + 0.685 + ], + [ + 0.69, + 0.609 + ] + ], + "sensitivity": 50, + "objectTypes": [ + "person", + "vehicle" + ], + "isTriggerLightEnabled": false, + "direction": "BA", + "isTargetCounting": false, + "plan": { + "autoReset": "none", + "time": null, + "weekDay": null, + "date": null + } + } + ], + "stats": { + "rxBytes": 0, + "txBytes": 0, + "wifi": { + "channel": null, + "frequency": null, + "linkSpeedMbps": null, + "signalQuality": 50, + "signalStrength": 0 + }, + "video": { + "recordingStart": null, + "recordingEnd": null, + "recordingStartLQ": null, + "recordingEndLQ": null, + "timelapseStart": null, + "timelapseEnd": null, + "timelapseStartLQ": null, + "timelapseEndLQ": null + }, + "wifiQuality": 50, + "wifiStrength": 0, + "storage": {} + }, + "featureFlags": { + "canAdjustIrLedLevel": false, + "canMagicZoom": false, + "canOpticalZoom": false, + "canTouchFocus": false, + "hasAccelerometer": false, + "hasVerticalFlip": true, + "hasAec": false, + "hasBluetooth": false, + "hasChime": false, + "hasExternalIr": false, + "hasIcrSensitivity": true, + "hasInfrared": true, + "hasLdc": false, + "hasLedIr": false, + "hasLedStatus": true, + "hasLineIn": false, + "hasMic": true, + "hasPrivacyMask": true, + "hasRtc": false, + "hasSdCard": false, + "hasSpeaker": false, + "hasWifi": false, + "hasHdr": false, + "hasAutoICROnly": false, + "videoModes": [ + "default" + ], + "videoModeMaxFps": [ + 30 + ], + "hasMotionZones": true, + "hasLcdScreen": false, + "hasFingerprintSensor": false, + "mountPositions": [], + "smartDetectTypes": [ + "person", + "vehicle", + "animal", + "face", + "licensePlate", + "package" + ], + "smartDetectAudioTypes": [ + "smoke_cmonx", + "alrmSmoke", + "alrmCmonx", + "alrmSiren", + "alrmBabyCry", + "alrmSpeak", + "alrmBurglar", + "alrmCarHorn", + "alrmBark", + "alrmGlassBreak" + ], + "supportNfc": false, + "lensType": null, + "lensModel": null, + "motionAlgorithms": [ + "enhanced" + ], + "hasSquareEventThumbnail": true, + "hasPackageCamera": false, + "audio": [], + "audioCodecs": [ + "aac" + ], + "audioStyle": [], + "isDoorbell": false, + "isPtz": false, + "hasColorLcdScreen": false, + "hasLiveviewTracking": true, + "hasLineCrossing": true, + "hasLineCrossingCounting": true, + "hasFlash": false, + "flashRange": null, + "privacyMaskCapability": { + "maxMasks": 16, + "rectangleOnly": false + }, + "focus": { + "steps": { + "max": null, + "min": null, + "step": null + }, + "degrees": { + "max": null, + "min": null, + "step": null + } + }, + "pan": { + "steps": { + "max": null, + "min": null, + "step": null + }, + "degrees": { + "max": null, + "min": null, + "step": null + } + }, + "tilt": { + "steps": { + "max": null, + "min": null, + "step": null + }, + "degrees": { + "max": null, + "min": null, + "step": null + } + }, + "zoom": { + "ratio": 1, + "steps": { + "max": null, + "min": null, + "step": null + }, + "degrees": { + "max": null, + "min": null, + "step": null + } + }, + "hotplug": { + "audio": null, + "video": null, + "standaloneAdoption": false, + "extender": { + "isAttached": false, + "hasFlash": null, + "flashRange": null, + "hasIR": null, + "hasRadar": false + } + }, + "hasSmartDetect": true + }, + "lcdMessage": {}, + "lenses": [], + "streamSharing": { + "enabled": false, + "token": null, + "shareLink": null, + "expires": null, + "sharedByUserId": null, + "sharedByUser": null, + "maxStreams": null + }, + "homekitSettings": { + "talkbackSettingsActive": false, + "streamInProgress": false, + "microphoneMuted": false, + "speakerMuted": false + }, + "pairedCameras": [ + "0777b5d342302079dc6b793d" + ], + "id": "675erf3d000c9787e7415re2", + "nvrMac": "4B8290F6D7A3", + "isConnected": true, + "platform": "all", + "hasSpeaker": false, + "hasWifi": false, + "audioBitrate": 64000, + "canManage": false, + "isManaged": true, + "marketName": "AI Port", + "is4K": false, + "is2K": false, + "modelKey": "aiport" + } ] } diff --git a/tests/sample_data/sample_camera.json b/tests/sample_data/sample_camera.json index 4f0a2626..78597465 100644 --- a/tests/sample_data/sample_camera.json +++ b/tests/sample_data/sample_camera.json @@ -580,5 +580,6 @@ "isManaged": true, "marketName": "G4 Doorbell Pro", "modelKey": "camera", + "isPairedWithAiPort": false, "guid": "00000000-0000-00 0- 000-000000000000" } From a700e9a8aae3f044e83c79186ac0fd509be9be77 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 16 Dec 2024 19:49:07 +0000 Subject: [PATCH 02/10] chore(pre-commit.ci): auto fixes --- src/uiprotect/data/devices.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/uiprotect/data/devices.py b/src/uiprotect/data/devices.py index 28871b31..c9df29c3 100644 --- a/src/uiprotect/data/devices.py +++ b/src/uiprotect/data/devices.py @@ -3385,6 +3385,7 @@ def callback() -> None: await self.queue_update(callback) + class AiPort(Camera): paired_cameras: list[str] From 1c9e6b546220cf0b442e4aa354761923da119c3e Mon Sep 17 00:00:00 2001 From: RaHehl <7577984+RaHehl@users.noreply.github.com> Date: Tue, 17 Dec 2024 22:22:33 +0100 Subject: [PATCH 03/10] feat: add AiPort related tests --- src/uiprotect/data/devices.py | 7 - tests/conftest.py | 26 + tests/sample_data/sample_aiport.json | 757 +++++++++++++++++++++++++++ tests/test_api.py | 57 ++ 4 files changed, 840 insertions(+), 7 deletions(-) create mode 100644 tests/sample_data/sample_aiport.json diff --git a/src/uiprotect/data/devices.py b/src/uiprotect/data/devices.py index c9df29c3..31f98c76 100644 --- a/src/uiprotect/data/devices.py +++ b/src/uiprotect/data/devices.py @@ -3388,10 +3388,3 @@ def callback() -> None: class AiPort(Camera): paired_cameras: list[str] - - @property - def paired_cameras(self) -> list[Camera]: - """Paired Cameras for AiPort""" - if len(self.paired_cameras) == 0: - return [] - return [self._api.bootstrap.cameras[c] for c in self.paired_cameras] diff --git a/tests/conftest.py b/tests/conftest.py index 02446a29..40b05f9a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -53,6 +53,7 @@ TEST_LIVEVIEW_EXISTS = (SAMPLE_DATA_DIRECTORY / "sample_liveview.json").exists() TEST_DOORLOCK_EXISTS = (SAMPLE_DATA_DIRECTORY / "sample_doorlock.json").exists() TEST_CHIME_EXISTS = (SAMPLE_DATA_DIRECTORY / "sample_chime.json").exists() +TEST_AIPORT_EXISTS = (SAMPLE_DATA_DIRECTORY / "sample_aiport.json").exists() ANY_NONE = [[None], None, []] @@ -88,6 +89,14 @@ def read_camera_json_file(): return camera +def read_aiport_json_file(): + # tests expect global recording settings to be off + aiport = read_json_file("sample_aiport") + if aiport.get("useGlobal"): + aiport["useGlobal"] = False + + return aiport + def get_now(): return datetime.fromisoformat(CONSTANTS["time"]).replace(microsecond=0) @@ -149,6 +158,8 @@ async def mock_api_request(url: str, *args, **kwargs): return [read_json_file("sample_doorlock")] if url == "chimes": return [read_json_file("sample_chime")] + if url == "aiports": + return [read_json_file("sample_aiport")] if url.endswith("ptz/preset"): return { "id": "test-id", @@ -187,6 +198,8 @@ async def mock_api_request(url: str, *args, **kwargs): return read_json_file("sample_doorlock") if url.startswith("chimes"): return read_json_file("sample_chime") + if url.startswith("aiports"): + return read_json_file("sample_aiport") if "smartDetectTrack" in url: return read_json_file("sample_event_smart_track") @@ -457,6 +470,13 @@ async def chime_obj_fixture(protect_client: ProtectApiClient): return next(iter(protect_client.bootstrap.chimes.values())) +@pytest_asyncio.fixture(name="aiport_obj") +async def aiport_obj_fixture(protect_client: ProtectApiClient): + if not TEST_AIPORT_EXISTS: + return None + + return next(iter(protect_client.bootstrap.aiports.values())) + @pytest_asyncio.fixture async def liveview_obj(protect_client: ProtectApiClient): if not TEST_LIVEVIEW_EXISTS: @@ -501,6 +521,12 @@ def camera(): return read_camera_json_file() +@pytest.fixture() +def aiport(): + if not TEST_CAMERA_EXISTS: + return None + + return read_aiport_json_file() @pytest.fixture() def sensor(): diff --git a/tests/sample_data/sample_aiport.json b/tests/sample_data/sample_aiport.json new file mode 100644 index 00000000..002a533c --- /dev/null +++ b/tests/sample_data/sample_aiport.json @@ -0,0 +1,757 @@ +{ + "isDeleting": false, + "mac": "942A6EGCA31F", + "host": "192.168.7.217", + "connectionHost": "192.168.7.8", + "type": "UVC AI Port", + "sysid": "0xa5f1", + "name": "AI Port", + "upSince": 1734402814930, + "uptime": 66493, + "lastSeen": 1734469307930, + "connectedSince": 1734429720103, + "state": "CONNECTED", + "lastDisconnect": 1734429714493, + "hardwareRevision": "9", + "firmwareVersion": "4.73.8", + "latestFirmwareVersion": null, + "firmwareBuild": "bc916ee.241211.844", + "isUpdating": false, + "isDownloadingFW": false, + "fwUpdateState": "upToDate", + "isAdopting": false, + "isRestoring": false, + "isAdopted": true, + "isAdoptedByOther": false, + "isProvisioned": true, + "isRebooting": false, + "isSshEnabled": false, + "canAdopt": false, + "isAttemptingToConnect": false, + "uplinkDevice": null, + "guid": "f3d3739b-743d-46df-9209-a1d31595a893", + "anonymousDeviceId": "4a547994-8d51-4a57-9e69-880c5f699c04", + "lastMotion": null, + "micVolume": 100, + "isMicEnabled": true, + "isRecording": false, + "isWirelessUplinkEnabled": true, + "isMotionDetected": false, + "isSmartDetected": false, + "phyRate": null, + "hdrMode": true, + "videoMode": "default", + "isProbingForWifi": false, + "apMac": null, + "apRssi": null, + "apMgmtIp": null, + "elementInfo": null, + "chimeDuration": 0, + "isDark": false, + "lastPrivacyZonePositionId": null, + "lastRing": null, + "isLiveHeatmapEnabled": false, + "eventStats": { + "motion": { + "today": 0, + "average": 0, + "lastDays": [], + "recentHours": [] + }, + "smart": { + "today": 0, + "average": 0, + "lastDays": [] + } + }, + "videoReconfigurationInProgress": false, + "voltage": null, + "activePatrolSlot": null, + "useGlobal": false, + "hubMac": null, + "isPoorNetwork": false, + "stopStreamLevel": null, + "downScaleMode": 0, + "isExtenderInstalledEver": false, + "isWaterproofCaseAttached": false, + "isMissingRecordingDetected": false, + "userConfiguredAp": false, + "hasRecordings": false, + "videoCodec": "h264", + "videoCodecState": 0, + "videoCodecSwitchingSince": null, + "enableNfc": false, + "isThirdPartyCamera": false, + "isPairedWithAiPort": false, + "streamingChannels": [], + "ptzControlEnabled": true, + "isAudioIncluded": true, + "isStreaming": true, + "wiredConnectionState": { + "phyRate": null + }, + "wifiConnectionState": { + "channel": null, + "frequency": null, + "phyRate": null, + "txRate": null, + "signalQuality": null, + "ssid": null, + "bssid": null, + "apName": null, + "experience": null, + "signalStrength": null, + "connectivity": null + }, + "channels": [ + { + "id": 0, + "videoId": "video1", + "name": "High", + "enabled": true, + "isRtspEnabled": false, + "rtspAlias": null, + "width": 3840, + "height": 2160, + "fps": 24, + "bitrate": 8000000, + "minBitrate": 2000000, + "maxBitrate": 8000000, + "minClientAdaptiveBitRate": 0, + "minMotionAdaptiveBitRate": 2000000, + "fpsValues": [ + 1, + 2, + 3, + 4, + 5, + 6, + 8, + 9, + 10, + 12, + 15, + 16, + 18, + 20, + 24 + ], + "idrInterval": 5, + "autoFps": true, + "autoBitrate": true + }, + { + "id": 1, + "videoId": "video2", + "name": "Medium", + "enabled": true, + "isRtspEnabled": false, + "rtspAlias": null, + "width": 1280, + "height": 720, + "fps": 24, + "bitrate": 2000000, + "minBitrate": 750000, + "maxBitrate": 8000000, + "minClientAdaptiveBitRate": 150000, + "minMotionAdaptiveBitRate": 750000, + "fpsValues": [ + 1, + 2, + 3, + 4, + 5, + 6, + 8, + 9, + 10, + 12, + 15, + 16, + 18, + 20, + 24 + ], + "idrInterval": 5, + "autoFps": true, + "autoBitrate": true + }, + { + "id": 2, + "videoId": "video3", + "name": "Low", + "enabled": true, + "isRtspEnabled": false, + "rtspAlias": null, + "width": 640, + "height": 360, + "fps": 24, + "bitrate": 900000, + "minBitrate": 300000, + "maxBitrate": 8000000, + "minClientAdaptiveBitRate": 0, + "minMotionAdaptiveBitRate": 300000, + "fpsValues": [ + 1, + 2, + 3, + 4, + 5, + 6, + 8, + 9, + 10, + 12, + 15, + 16, + 18, + 20, + 24 + ], + "idrInterval": 5, + "autoFps": true, + "autoBitrate": true + } + ], + "ispSettings": { + "aeMode": "auto", + "irLedMode": "auto", + "irLedLevel": 255, + "wdr": 1, + "icrSensitivity": 0, + "icrSwitchMode": "sensitivity", + "icrCustomValue": 2, + "brightness": 50, + "contrast": 50, + "hue": 50, + "saturation": 50, + "sharpness": 50, + "denoise": 50, + "isColorNightVisionEnabled": false, + "spotlightDuration": 15, + "isFlippedVertical": false, + "isFlippedHorizontal": false, + "isAutoRotateEnabled": false, + "isLdcEnabled": true, + "is3dnrEnabled": true, + "isExternalIrEnabled": false, + "isAggressiveAntiFlickerEnabled": false, + "isPauseMotionEnabled": false, + "dZoomCenterX": 50, + "dZoomCenterY": 50, + "dZoomScale": 0, + "dZoomStreamId": 4, + "focusMode": "ztrig", + "focusPosition": 0, + "touchFocusX": 1001, + "touchFocusY": 1001, + "zoomPosition": 0, + "mountPosition": null, + "hdrMode": "normal", + "sceneMode": "lprNoneReflex" + }, + "audioSettings": { + "style": [ + "nature" + ] + }, + "talkbackSettings": { + "typeFmt": "aac", + "typeIn": "serverudp", + "bindAddr": "0.0.0.0", + "bindPort": 7004, + "filterAddr": null, + "filterPort": null, + "channels": 1, + "samplingRate": 22050, + "bitsPerSample": 16, + "quality": 100 + }, + "osdSettings": { + "isNameEnabled": false, + "isDateEnabled": false, + "isLogoEnabled": true, + "isDebugEnabled": false + }, + "ledSettings": { + "isEnabled": true, + "blinkRate": 0 + }, + "speakerSettings": { + "isEnabled": true, + "areSystemSoundsEnabled": false, + "volume": 80, + "ringVolume": 80 + }, + "recordingSettings": { + "prePaddingSecs": 2, + "postPaddingSecs": 2, + "smartDetectPrePaddingSecs": 2, + "smartDetectPostPaddingSecs": 2, + "minMotionEventTrigger": 1000, + "endMotionEventDelay": 3000, + "suppressIlluminationSurge": false, + "mode": "always", + "inScheduleMode": "always", + "outScheduleMode": "never", + "geofencing": "off", + "retentionDurationMs": null, + "retentionDurationLQMs": null, + "motionAlgorithm": "enhanced", + "enableMotionDetection": true, + "useNewMotionAlgorithm": true + }, + "smartDetectSettings": { + "objectTypes": [ + "person", + "vehicle", + "animal", + "package", + "face", + "licensePlate" + ], + "autoTrackingObjectTypes": [], + "autoTrackingWithZoom": true, + "audioTypes": [], + "detectionRange": { + "max": null, + "min": null + } + }, + "recordingSchedulesV2": [], + "motionZones": [ + { + "id": 1, + "name": "Default", + "color": "#AB46BC", + "points": [ + [ + 0.204, + 0 + ], + [ + 0.546, + 0 + ], + [ + 0.525, + 0.631 + ], + [ + 0.625, + 0.63 + ], + [ + 1, + 0.704 + ], + [ + 1, + 1 + ], + [ + 0.257, + 1 + ] + ], + "sensitivity": 50, + "isTriggerLightEnabled": true + } + ], + "privacyZones": [], + "smartDetectZones": [ + { + "id": 1, + "name": "Default", + "color": "#AB46BC", + "points": [ + [ + 0.204, + 0 + ], + [ + 0.533, + 0 + ], + [ + 0.528, + 0.626 + ], + [ + 0.696, + 0.613 + ], + [ + 1, + 0.711 + ], + [ + 1, + 1 + ], + [ + 0.251, + 1 + ] + ], + "sensitivity": 50, + "objectTypes": [ + "person", + "vehicle", + "animal", + "face", + "licensePlate", + "package" + ], + "isTriggerLightEnabled": true, + "source": "unifi-protect", + "triggerAccessTypes": [], + "enableAccessLPOnlyMode": false + } + ], + "smartDetectLines": [ + { + "id": 3, + "name": "New Crossing Line", + "color": "#46C7FD", + "points": [ + [ + 0.69, + 0.609 + ], + [ + 0.53, + 0.609 + ] + ], + "sensitivity": 50, + "objectTypes": [ + "person", + "vehicle", + "animal" + ], + "isTriggerLightEnabled": false, + "direction": "BA", + "isTargetCounting": false, + "plan": { + "autoReset": "none", + "time": null, + "weekDay": null, + "date": null + } + }, + { + "id": 4, + "name": "New Crossing Line", + "color": "#61E066", + "points": [ + [ + 1, + 0.685 + ], + [ + 0.69, + 0.609 + ] + ], + "sensitivity": 50, + "objectTypes": [ + "person", + "vehicle" + ], + "isTriggerLightEnabled": false, + "direction": "BA", + "isTargetCounting": false, + "plan": { + "autoReset": "none", + "time": null, + "weekDay": null, + "date": null + } + } + ], + "smartDetectLoiterZones": [], + "stats": { + "rxBytes": 0, + "txBytes": 0, + "wifi": { + "channel": null, + "frequency": null, + "linkSpeedMbps": null, + "signalQuality": 50, + "signalStrength": 0 + }, + "video": { + "recordingStart": null, + "recordingEnd": null, + "recordingStartLQ": null, + "recordingEndLQ": null, + "timelapseStart": null, + "timelapseEnd": null, + "timelapseStartLQ": null, + "timelapseEndLQ": null + }, + "storage": { + "used": null, + "rate": null, + "channelStorage": {} + }, + "sdCard": { + "state": "unmounted", + "health": null, + "mounts": [], + "serial": null, + "size": null, + "healthStatus": "insufficient_size", + "usedSize": 0 + }, + "edgeRecording": { + "recordStreamNumber": null, + "recordMode": "smartDetect" + }, + "wifiQuality": 50, + "wifiStrength": 0, + "sdCardStorageCapacityMs": null + }, + "featureFlags": { + "canAdjustIrLedLevel": false, + "maxScaleDownLevel": 0, + "canMagicZoom": false, + "canOpticalZoom": false, + "canTouchFocus": false, + "hasAccelerometer": false, + "hasVerticalFlip": true, + "hasAec": false, + "hasBluetooth": false, + "hasChime": false, + "hasExternalIr": false, + "hasIcrSensitivity": true, + "hasInfrared": true, + "hasLdc": false, + "hasLedIr": false, + "hasLedStatus": true, + "hasLineIn": false, + "hasMic": true, + "hasPrivacyMask": true, + "hasRtc": false, + "hasSdCard": false, + "hasSpeaker": false, + "hasWifi": false, + "hasHdr": false, + "hasAutoICROnly": false, + "videoModes": [ + "default" + ], + "videoModeMaxFps": [ + 30 + ], + "hasMotionZones": true, + "hasLcdScreen": false, + "hasFingerprintSensor": false, + "mountPositions": [], + "smartDetectTypes": [ + "person", + "vehicle", + "animal", + "face", + "licensePlate", + "package" + ], + "smartDetectAudioTypes": [ + "smoke_cmonx", + "alrmSmoke", + "alrmCmonx", + "alrmSiren", + "alrmBabyCry", + "alrmSpeak", + "alrmBurglar", + "alrmCarHorn", + "alrmBark", + "alrmGlassBreak" + ], + "supportDoorAccessConfig": false, + "supportNfc": false, + "supportLpDetectionWithoutVehicle": false, + "lensType": null, + "lensModel": null, + "motionAlgorithms": [ + "enhanced" + ], + "hasSquareEventThumbnail": true, + "hasPackageCamera": false, + "audio": [], + "audioCodecs": [ + "aac" + ], + "videoCodecs": [ + "h264", + "h265", + "mjpg" + ], + "audioStyle": [], + "isDoorbell": false, + "isPtz": false, + "hasColorLcdScreen": false, + "hasLiveviewTracking": true, + "hasLineCrossing": true, + "hasLineCrossingCounting": true, + "hasFlash": false, + "flashRange": null, + "hasLuxCheck": false, + "presetTour": false, + "hasEdgeRecording": false, + "hasLprReflex": false, + "privacyMaskCapability": { + "maxMasks": 16, + "rectangleOnly": false + }, + "focus": { + "steps": { + "max": null, + "min": null, + "step": null + }, + "degrees": { + "max": null, + "min": null, + "step": null + } + }, + "pan": { + "steps": { + "max": null, + "min": null, + "step": null + }, + "degrees": { + "max": null, + "min": null, + "step": null + } + }, + "tilt": { + "steps": { + "max": null, + "min": null, + "step": null + }, + "degrees": { + "max": null, + "min": null, + "step": null + } + }, + "zoom": { + "ratio": 1, + "steps": { + "max": null, + "min": null, + "step": null + }, + "degrees": { + "max": null, + "min": null, + "step": null + } + }, + "hotplug": { + "audio": null, + "video": null, + "standaloneAdoption": false, + "extender": { + "isAttached": false, + "hasFlash": null, + "flashRange": null, + "hasIR": null, + "hasRadar": false, + "radarRangeMax": null, + "radarRangeMin": null + } + }, + "hasSmartDetect": true + }, + "tiltLimitsOfPrivacyZones": { + "side": "bottom", + "limit": 0 + }, + "lcdMessage": {}, + "lenses": [], + "streamSharing": { + "enabled": false, + "token": null, + "shareLink": null, + "expires": null, + "sharedByUserId": null, + "sharedByUser": null, + "maxStreams": null + }, + "homekitSettings": { + "talkbackSettingsActive": false, + "streamInProgress": false, + "microphoneMuted": false, + "speakerMuted": false + }, + "shortcuts": [], + "alarms": { + "lensThermal": 0, + "tiltThermal": 0, + "panTiltMotorFaults": [], + "autoTrackingThermalThresholdReached": false, + "lensThermalThresholdReached": false, + "motorOverheated": false + }, + "extendedAiFeatures": { + "smartDetectTypes": [] + }, + "thirdPartyCameraInfo": { + "port": null, + "rtspUrl": null, + "rtspUrlLQ": null, + "snapshotUrl": null + }, + "fingerprintSettings": { + "enable": false, + "enablePrintLatency": false, + "mode": "identify", + "reportFingerTouch": false, + "reportCaptureComplete": false + }, + "fingerprintState": { + "fingerprintId": null, + "status": null, + "progress": null, + "total": 0, + "free": 0 + }, + "nfcSettings": { + "enableNfc": false, + "supportThirdPartyCard": false + }, + "nfcState": { + "lastSeen": null, + "mode": "disabled", + "cardId": null, + "isUACard": false + }, + "pairedCameras": [ + "663d0aa400918803e4396735" + ], + "id": "6760947e02b4830659365930", + "nvrMac": "F9FC431B07EC", + "displayName": "AI Port", + "isConnected": true, + "platform": "all", + "hasSpeaker": false, + "hasWifi": false, + "audioBitrate": 64000, + "canManage": false, + "isManaged": true, + "marketName": "AI Port", + "is4K": false, + "is2K": false, + "currentResolution": "HD", + "supportedScalingResolutions": [ + "HD" + ], + "modelKey": "aiport" +} diff --git a/tests/test_api.py b/tests/test_api.py index effd614b..413b45e0 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -903,3 +903,60 @@ async def test_get_event_smart_detect_track(protect_client: ProtectApiClient): require_auth=True, raise_exception=True, ) + + +@pytest.mark.skipif(not TEST_CAMERA_EXISTS, reason="Missing testdata") +@pytest.mark.asyncio() +async def test_get_aiport(protect_client: ProtectApiClient, aiport): + obj = create_from_unifi_dict(aiport) + + assert_equal_dump(obj, await protect_client.get_aiport("test_id")) + +@pytest.mark.skipif(not TEST_CAMERA_EXISTS, reason="Missing testdata") +@pytest.mark.asyncio() +async def test_get_aiport_not_adopted(protect_client: ProtectApiClient, aiport): + aiport["isAdopted"] = False + protect_client.api_request_obj = AsyncMock(return_value=aiport) + + with pytest.raises(NvrError): + await protect_client.get_aiport("test_id") + + +@pytest.mark.skipif(not TEST_CAMERA_EXISTS, reason="Missing testdata") +@pytest.mark.asyncio() +async def test_get_aiport_not_adopted_enabled(protect_client: ProtectApiClient, aiport): + aiport["isAdopted"] = False + protect_client.ignore_unadopted = False + protect_client.api_request_obj = AsyncMock(return_value=aiport) + + obj = create_from_unifi_dict(aiport) + assert_equal_dump(obj, await protect_client.get_aiport("test_id")) + + +@pytest.mark.skipif(not TEST_CAMERA_EXISTS, reason="Missing testdata") +@pytest.mark.asyncio() +async def test_get_chime(protect_client: ProtectApiClient, chime): + obj = create_from_unifi_dict(chime) + + assert_equal_dump(obj, await protect_client.get_chime("test_id")) + + +@pytest.mark.skipif(not TEST_CAMERA_EXISTS, reason="Missing testdata") +@pytest.mark.asyncio() +async def test_get_chime_not_adopted(protect_client: ProtectApiClient, chime): + chime["isAdopted"] = False + protect_client.api_request_obj = AsyncMock(return_value=chime) + + with pytest.raises(NvrError): + await protect_client.get_chime("test_id") + + +@pytest.mark.skipif(not TEST_CAMERA_EXISTS, reason="Missing testdata") +@pytest.mark.asyncio() +async def test_get_chime_not_adopted_enabled(protect_client: ProtectApiClient, chime): + chime["isAdopted"] = False + protect_client.ignore_unadopted = False + protect_client.api_request_obj = AsyncMock(return_value=chime) + + obj = create_from_unifi_dict(chime) + assert_equal_dump(obj, await protect_client.get_chime("test_id")) From 8adf09614b34c5ff0ae4022f03dac2af760e7f88 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 17 Dec 2024 21:22:44 +0000 Subject: [PATCH 04/10] chore(pre-commit.ci): auto fixes --- tests/conftest.py | 4 ++++ tests/test_api.py | 1 + 2 files changed, 5 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index 40b05f9a..233502e9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -89,6 +89,7 @@ def read_camera_json_file(): return camera + def read_aiport_json_file(): # tests expect global recording settings to be off aiport = read_json_file("sample_aiport") @@ -477,6 +478,7 @@ async def aiport_obj_fixture(protect_client: ProtectApiClient): return next(iter(protect_client.bootstrap.aiports.values())) + @pytest_asyncio.fixture async def liveview_obj(protect_client: ProtectApiClient): if not TEST_LIVEVIEW_EXISTS: @@ -521,6 +523,7 @@ def camera(): return read_camera_json_file() + @pytest.fixture() def aiport(): if not TEST_CAMERA_EXISTS: @@ -528,6 +531,7 @@ def aiport(): return read_aiport_json_file() + @pytest.fixture() def sensor(): if not TEST_SENSOR_EXISTS: diff --git a/tests/test_api.py b/tests/test_api.py index 413b45e0..05e16cfb 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -912,6 +912,7 @@ async def test_get_aiport(protect_client: ProtectApiClient, aiport): assert_equal_dump(obj, await protect_client.get_aiport("test_id")) + @pytest.mark.skipif(not TEST_CAMERA_EXISTS, reason="Missing testdata") @pytest.mark.asyncio() async def test_get_aiport_not_adopted(protect_client: ProtectApiClient, aiport): From 02ffb65b703d401c6b501bdd657eec080d51da19 Mon Sep 17 00:00:00 2001 From: RaHehl <7577984+RaHehl@users.noreply.github.com> Date: Tue, 17 Dec 2024 23:32:29 +0100 Subject: [PATCH 05/10] feat: add AiPort CLI commands --- src/uiprotect/cli/__init__.py | 2 ++ src/uiprotect/cli/aiports.py | 58 +++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 src/uiprotect/cli/aiports.py diff --git a/src/uiprotect/cli/__init__.py b/src/uiprotect/cli/__init__.py index 7c4f249d..1e1bdad8 100644 --- a/src/uiprotect/cli/__init__.py +++ b/src/uiprotect/cli/__init__.py @@ -17,6 +17,7 @@ from ..test_util import SampleDataGenerator from ..utils import RELEASE_CACHE, get_local_timezone, run_async from ..utils import profile_ws as profile_ws_job +from .aiports import app as aiports_app from .base import CliContext, OutputFormatEnum from .cameras import app as camera_app from .chimes import app as chime_app @@ -128,6 +129,7 @@ app.add_typer(light_app, name="lights") app.add_typer(sensor_app, name="sensors") app.add_typer(viewer_app, name="viewers") +app.add_typer(aiports_app, name="aiports") if backup_app is not None: app.add_typer(backup_app, name="backup") diff --git a/src/uiprotect/cli/aiports.py b/src/uiprotect/cli/aiports.py new file mode 100644 index 00000000..a7ee85d8 --- /dev/null +++ b/src/uiprotect/cli/aiports.py @@ -0,0 +1,58 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Optional + +import typer + +from ..api import ProtectApiClient +from ..cli import base +from ..data import AiPort + +app = typer.Typer(rich_markup_mode="rich") + +ARG_DEVICE_ID = typer.Argument(None, help="ID of chime to select for subcommands") + + + +@dataclass +class AiPortContext(base.CliContext): + devices: dict[str, AiPort] + device: AiPort | None = None + + +ALL_COMMANDS, DEVICE_COMMANDS = base.init_common_commands(app) + + +@app.callback(invoke_without_command=True) +def main(ctx: typer.Context, device_id: Optional[str] = ARG_DEVICE_ID) -> None: + """ + AiPort device CLI. + + Returns full list of AiPorts without any arguments passed. + """ + protect: ProtectApiClient = ctx.obj.protect + context = AiPortContext( + protect=ctx.obj.protect, + device=None, + devices=protect.bootstrap.aiports, + output_format=ctx.obj.output_format, + ) + ctx.obj = context + + if device_id is not None and device_id not in ALL_COMMANDS: + if (device := protect.bootstrap.aiports.get(device_id)) is None: + typer.secho("Invalid aiport ID", fg="red") + raise typer.Exit(1) + ctx.obj.device = device + + if not ctx.invoked_subcommand: + if device_id in ALL_COMMANDS: + ctx.invoke(ALL_COMMANDS[device_id], ctx) + return + + if ctx.obj.device is not None: + base.print_unifi_obj(ctx.obj.device, ctx.obj.output_format) + return + + base.print_unifi_dict(ctx.obj.devices) From ee47bf50f55653c01d88480a59b8ad7030e0921e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 17 Dec 2024 22:32:40 +0000 Subject: [PATCH 06/10] chore(pre-commit.ci): auto fixes --- src/uiprotect/cli/aiports.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/uiprotect/cli/aiports.py b/src/uiprotect/cli/aiports.py index a7ee85d8..068595eb 100644 --- a/src/uiprotect/cli/aiports.py +++ b/src/uiprotect/cli/aiports.py @@ -14,7 +14,6 @@ ARG_DEVICE_ID = typer.Argument(None, help="ID of chime to select for subcommands") - @dataclass class AiPortContext(base.CliContext): devices: dict[str, AiPort] From c26d2b34372fd6f10a993c3fd497f94a9d91b26e Mon Sep 17 00:00:00 2001 From: RaHehl <7577984+RaHehl@users.noreply.github.com> Date: Tue, 17 Dec 2024 23:52:53 +0100 Subject: [PATCH 07/10] feat(tests): add aiports fixture and test for get_aiports API --- tests/conftest.py | 8 ++++++++ tests/test_api.py | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index 233502e9..a14568e5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -580,6 +580,14 @@ def viewports(): return [read_json_file("sample_viewport")] +@pytest.fixture() +def aiports(): + if not TEST_AIPORT_EXISTS: + return [] + + return [read_json_file("sample_aiport")] + + @pytest.fixture() def lights(): if not TEST_LIGHT_EXISTS: diff --git a/tests/test_api.py b/tests/test_api.py index 05e16cfb..0fcfb0fc 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -961,3 +961,11 @@ async def test_get_chime_not_adopted_enabled(protect_client: ProtectApiClient, c obj = create_from_unifi_dict(chime) assert_equal_dump(obj, await protect_client.get_chime("test_id")) + +@pytest.mark.skipif(not TEST_CAMERA_EXISTS, reason="Missing testdata") +@pytest.mark.asyncio() +async def test_get_aiports(protect_client: ProtectApiClient, aiports): + objs = [create_from_unifi_dict(d) for d in aiports] + + assert_equal_dump(objs, await protect_client.get_aiports()) + From 46d83f8c6bbb22fe20b35cab5e92945cbfc15f0a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 17 Dec 2024 22:53:03 +0000 Subject: [PATCH 08/10] chore(pre-commit.ci): auto fixes --- tests/test_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_api.py b/tests/test_api.py index 0fcfb0fc..4c867fbe 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -962,10 +962,10 @@ async def test_get_chime_not_adopted_enabled(protect_client: ProtectApiClient, c obj = create_from_unifi_dict(chime) assert_equal_dump(obj, await protect_client.get_chime("test_id")) + @pytest.mark.skipif(not TEST_CAMERA_EXISTS, reason="Missing testdata") @pytest.mark.asyncio() async def test_get_aiports(protect_client: ProtectApiClient, aiports): objs = [create_from_unifi_dict(d) for d in aiports] assert_equal_dump(objs, await protect_client.get_aiports()) - From f130d776aba66ed5a3887057eb63c0a6ce3c7cd3 Mon Sep 17 00:00:00 2001 From: RaHehl <7577984+RaHehl@users.noreply.github.com> Date: Tue, 17 Dec 2024 23:54:14 +0100 Subject: [PATCH 09/10] fix(cli): update help text for device ID argument in AiPort CLI --- src/uiprotect/cli/aiports.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/uiprotect/cli/aiports.py b/src/uiprotect/cli/aiports.py index 068595eb..4c52fddd 100644 --- a/src/uiprotect/cli/aiports.py +++ b/src/uiprotect/cli/aiports.py @@ -11,7 +11,7 @@ app = typer.Typer(rich_markup_mode="rich") -ARG_DEVICE_ID = typer.Argument(None, help="ID of chime to select for subcommands") +ARG_DEVICE_ID = typer.Argument(None, help="ID of AiPort device to select for subcommands") @dataclass From 3725574be318d8ed48b93a6a2b64ab201eaaf986 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 17 Dec 2024 22:54:23 +0000 Subject: [PATCH 10/10] chore(pre-commit.ci): auto fixes --- src/uiprotect/cli/aiports.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/uiprotect/cli/aiports.py b/src/uiprotect/cli/aiports.py index 4c52fddd..8098b6c7 100644 --- a/src/uiprotect/cli/aiports.py +++ b/src/uiprotect/cli/aiports.py @@ -11,7 +11,9 @@ app = typer.Typer(rich_markup_mode="rich") -ARG_DEVICE_ID = typer.Argument(None, help="ID of AiPort device to select for subcommands") +ARG_DEVICE_ID = typer.Argument( + None, help="ID of AiPort device to select for subcommands" +) @dataclass