From f1faf132388a722aefb2b109930bf4217e9124ca Mon Sep 17 00:00:00 2001 From: danielcolquitt Date: Sun, 2 Aug 2026 12:59:46 +0100 Subject: [PATCH] Add Outdoor Cam Plus stickup cam mapping --- ring_doorbell/const.py | 1 + ring_doorbell/stickup_cam.py | 27 ++++++++++++++++++++++----- tests/test_ring.py | 30 ++++++++++++++++++++++++++++++ 3 files changed, 53 insertions(+), 5 deletions(-) diff --git a/ring_doorbell/const.py b/ring_doorbell/const.py index 79c88ff..3e27d30 100644 --- a/ring_doorbell/const.py +++ b/ring_doorbell/const.py @@ -185,6 +185,7 @@ def from_name(name: str) -> RingCapability: STICKUP_CAM_ELITE_KINDS = ["stickup_cam_elite", "stickup_cam_wired"] STICKUP_CAM_WIRED_KINDS = STICKUP_CAM_ELITE_KINDS # Deprecated STICKUP_CAM_GEN3_KINDS = ["cocoa_camera"] +OUTDOOR_CAM_PLUS_KINDS = ["cocoa_camera_v2"] BEAM_KINDS = ["beams_ct200_transformer"] INTERCOM_KINDS = ["intercom_handset_audio", "intercom_handset_video"] diff --git a/ring_doorbell/stickup_cam.py b/ring_doorbell/stickup_cam.py index 229a1e5..546ddc1 100644 --- a/ring_doorbell/stickup_cam.py +++ b/ring_doorbell/stickup_cam.py @@ -16,6 +16,7 @@ LIGHTS_ENDPOINT, MSG_ALLOWED_VALUES, MSG_VOL_OUTBOUND, + OUTDOOR_CAM_PLUS_KINDS, SIREN_DURATION_MAX, SIREN_DURATION_MIN, SIREN_ENDPOINT, @@ -38,6 +39,12 @@ class RingStickUpCam(RingDoorBell): """Implementation for RingStickUpCam.""" + def _outdoor_cam_plus_has_battery(self) -> bool: + """Return if an Outdoor Cam Plus reports a battery.""" + if "battery_present" in self._attrs.get("health", {}): + return bool(self._attrs["health"]["battery_present"]) + return self.battery_life is not None + @property def family(self) -> str: """Return Ring device family type.""" @@ -58,6 +65,8 @@ def model(self) -> str: # noqa: C901, PLR0911, PLR0912 return "Indoor Cam (2nd Gen)" if self.kind in INDOOR_CAM_PTZ_KINDS: return "Pan-Tilt Indoor Cam" + if self.kind in OUTDOOR_CAM_PLUS_KINDS: + return "Outdoor Cam Plus" if self.kind in SPOTLIGHT_CAM_BATTERY_KINDS: return "Spotlight Cam {}".format( self._attrs.get("ring_cam_setup_flow", "battery").title() @@ -91,11 +100,17 @@ def has_capability(self, capability: RingCapability | str) -> bool: if capability == RingCapability.HISTORY: return True if capability == RingCapability.BATTERY: - return self.kind in ( - SPOTLIGHT_CAM_BATTERY_KINDS - + STICKUP_CAM_KINDS - + STICKUP_CAM_BATTERY_KINDS - + STICKUP_CAM_GEN3_KINDS + return ( + self.kind in OUTDOOR_CAM_PLUS_KINDS + and self._outdoor_cam_plus_has_battery() + ) or ( + self.kind + in ( + SPOTLIGHT_CAM_BATTERY_KINDS + + STICKUP_CAM_KINDS + + STICKUP_CAM_BATTERY_KINDS + + STICKUP_CAM_GEN3_KINDS + ) ) if capability == RingCapability.LIGHT: return self.kind in ( @@ -115,6 +130,7 @@ def has_capability(self, capability: RingCapability | str) -> bool: + INDOOR_CAM_KINDS + INDOOR_CAM_GEN2_KINDS + INDOOR_CAM_PTZ_KINDS + + OUTDOOR_CAM_PLUS_KINDS + SPOTLIGHT_CAM_BATTERY_KINDS + SPOTLIGHT_CAM_WIRED_KINDS + SPOTLIGHT_CAM_PLUS_KINDS @@ -131,6 +147,7 @@ def has_capability(self, capability: RingCapability | str) -> bool: + INDOOR_CAM_KINDS + INDOOR_CAM_GEN2_KINDS + INDOOR_CAM_PTZ_KINDS + + OUTDOOR_CAM_PLUS_KINDS + SPOTLIGHT_CAM_BATTERY_KINDS + SPOTLIGHT_CAM_WIRED_KINDS + SPOTLIGHT_CAM_PLUS_KINDS diff --git a/tests/test_ring.py b/tests/test_ring.py index def8f47..116b5d2 100644 --- a/tests/test_ring.py +++ b/tests/test_ring.py @@ -113,6 +113,36 @@ def test_stickup_cam_attributes(ring): assert dev.siren == 0 +def test_outdoor_cam_plus_battery_attributes(ring): + dev = ring.devices()["stickup_cams"][0] + dev._attrs["kind"] = "cocoa_camera_v2" + dev._attrs["battery_life"] = 76 + dev._attrs["health"] = {"battery_present": True} + + assert dev.model == "Outdoor Cam Plus" + assert dev.has_capability("battery") is True + assert dev.has_capability("light") is False + assert dev.has_capability("history") is True + assert dev.has_capability("motion_detection") is True + assert dev.has_capability("siren") is True + assert dev.has_capability("video") is True + + +def test_outdoor_cam_plus_powered_attributes(ring): + dev = ring.devices()["stickup_cams"][0] + dev._attrs["kind"] = "cocoa_camera_v2" + dev._attrs["battery_life"] = None + dev._attrs["health"] = {"battery_present": False} + + assert dev.model == "Outdoor Cam Plus" + assert dev.has_capability("battery") is False + assert dev.has_capability("light") is False + assert dev.has_capability("history") is True + assert dev.has_capability("motion_detection") is True + assert dev.has_capability("siren") is True + assert dev.has_capability("video") is True + + async def test_stickup_cam_controls(ring, aioresponses_mock): dev = ring.devices()["stickup_cams"][0]