From a74d1d23ced4691a9be0c24ef387ab592068aa11 Mon Sep 17 00:00:00 2001 From: Steven B <51370195+sdb9696@users.noreply.github.com> Date: Fri, 26 Jan 2024 15:40:42 +0000 Subject: [PATCH 1/5] Fix coverage over-reporting by uploading xml report (#333) * Fix coverage by uploading xml report * Add coverage rc to pyproject.toml --- .github/workflows/ci.yml | 3 +++ pyproject.toml | 4 ++++ tox.ini | 2 +- 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f571e04..7ea7188 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -109,4 +109,7 @@ jobs: TOXENV: ${{ steps.toxenv.outputs.toxenv }} - name: Coveralls GitHub Action uses: coverallsapp/github-action@v2.2.3 + with: + file: coverage.xml + debug: true if: ${{ success() && matrix.python-version == '3.12' }} diff --git a/pyproject.toml b/pyproject.toml index f62ec6c..03c123f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -96,6 +96,10 @@ profile = "black" known_first_party = "ring_doorbell" known_third_party = ["asyncclick", "pytest"] +[tool.coverage.run] +source = ["ring_doorbell"] +branch = true + [build-system] requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" diff --git a/tox.ini b/tox.ini index 0f6bf4f..eafe389 100644 --- a/tox.ini +++ b/tox.ini @@ -8,7 +8,7 @@ allowlist_externals = poetry commands_pre = poetry install --no-root --sync --extras listen commands = - poetry run pytest tests/ --cov ring_doorbell --cov-report term-missing + poetry run pytest tests/ --cov=ring_doorbell --cov-report=xml --cov-report=term-missing [testenv:lint] allowlist_externals = poetry From 905b6c3bc8e6df508a6b50e0a668c6eb57f0c17a Mon Sep 17 00:00:00 2001 From: cosimomeli Date: Fri, 26 Jan 2024 16:42:25 +0100 Subject: [PATCH 2/5] Updated Intercom Support (2024) (#330) * brought rautsch branch up to speed with main and added more configurability to intercom object (volumes, refractory period for ring detection) * cleaning up per tox * addressing lint issues * tweak to match new id naming convention across other devices * First step of Other tests * Add open_door test * Rename Other and format code * Remove unsued import * Replace monkey patch with mocker.patch * Remove useless catch on query --------- Co-authored-by: Andrew Bogaard Co-authored-by: Andrew Bogaard --- ring_doorbell/__init__.py | 2 + ring_doorbell/auth.py | 2 +- ring_doorbell/cli.py | 3 + ring_doorbell/const.py | 18 + ring_doorbell/other.py | 283 +++++++++++++ ring_doorbell/ring.py | 3 + tests/conftest.py | 29 ++ tests/fixtures/ring_devices.json | 91 ++++- tests/fixtures/ring_intercom_settings.json | 439 +++++++++++++++++++++ tests/fixtures/ring_intercom_users.json | 34 ++ tests/test_other.py | 89 +++++ tests/test_ring.py | 10 +- 12 files changed, 991 insertions(+), 12 deletions(-) create mode 100644 ring_doorbell/other.py create mode 100644 tests/fixtures/ring_intercom_settings.json create mode 100644 tests/fixtures/ring_intercom_users.json create mode 100644 tests/test_other.py diff --git a/ring_doorbell/__init__.py b/ring_doorbell/__init__.py index 450369a..d5cb467 100644 --- a/ring_doorbell/__init__.py +++ b/ring_doorbell/__init__.py @@ -15,6 +15,7 @@ ) from ring_doorbell.generic import RingGeneric from ring_doorbell.group import RingLightGroup +from ring_doorbell.other import RingOther from ring_doorbell.ring import Ring from ring_doorbell.stickup_cam import RingStickUpCam @@ -25,6 +26,7 @@ "RingStickUpCam", "RingLightGroup", "RingDoorBell", + "RingOther", "RingEvent", "RingError", "AuthenticationError", diff --git a/ring_doorbell/auth.py b/ring_doorbell/auth.py index 7f1e770..f9f2c5b 100644 --- a/ring_doorbell/auth.py +++ b/ring_doorbell/auth.py @@ -128,7 +128,7 @@ def query( "timeout": timeout, } - if method == "POST": + if method in ["POST", "PUT"]: if json is not None: kwargs["json"] = json kwargs["headers"]["Content-Type"] = "application/json" diff --git a/ring_doorbell/cli.py b/ring_doorbell/cli.py index 237e370..979ec24 100644 --- a/ring_doorbell/cli.py +++ b/ring_doorbell/cli.py @@ -208,6 +208,7 @@ async def list_command(ring: Ring): doorbells = devices["doorbots"] chimes = devices["chimes"] stickup_cams = devices["stickup_cams"] + other = devices["other"] for device in doorbells: echo(device) @@ -215,6 +216,8 @@ async def list_command(ring: Ring): echo(device) for device in stickup_cams: echo(device) + for device in other: + echo(device) @cli.command() diff --git a/ring_doorbell/const.py b/ring_doorbell/const.py index 6968f77..42bf9ee 100644 --- a/ring_doorbell/const.py +++ b/ring_doorbell/const.py @@ -43,6 +43,8 @@ class OAuth: PERSIST_TOKEN_ENDPOINT = "/clients_api/device" SUBSCRIPTION_ENDPOINT = "/clients_api/device" GROUPS_ENDPOINT = "/groups/v1/locations/{0}/groups" +LOCATIONS_HISTORY_ENDPOINT = "/evm/v2/history/locations/{0}" +LOCATIONS_ENDPOINT = "/clients_api/locations/{0}" HEALTH_DOORBELL_ENDPOINT = DOORBELLS_ENDPOINT + "/health" HEALTH_CHIMES_ENDPOINT = CHIMES_ENDPOINT + "/health" @@ -60,6 +62,11 @@ class OAuth: URL_RECORDING_SHARE_PLAY = "/clients_api/dings/{0}/share/play" GROUP_DEVICES_ENDPOINT = GROUPS_ENDPOINT + "/{1}/devices" SETTINGS_ENDPOINT = "/devices/v1/devices/{0}/settings" +URL_INTERCOM_HISTORY = LOCATIONS_HISTORY_ENDPOINT + "?ringtercom" +INTERCOM_OPEN_ENDPOINT = "/commands/v1/devices/{0}/device_rpc" +INTERCOM_INVITATIONS_ENDPOINT = LOCATIONS_ENDPOINT + "/invitations" +INTERCOM_INVITATIONS_DELETE_ENDPOINT = LOCATIONS_ENDPOINT + "/invitations/{1}" +INTERCOM_ALLOWED_USERS = LOCATIONS_ENDPOINT + "/users" # chime test sound kinds KIND_DING = "ding" @@ -73,6 +80,15 @@ class OAuth: DOORBELL_VOL_MIN = 0 DOORBELL_VOL_MAX = 11 +MIC_VOL_MIN = 0 +MIC_VOL_MAX = 11 + +VOICE_VOL_MIN = 0 +VOICE_VOL_MAX = 11 + +OTHER_DOORBELL_VOL_MIN = 0 +OTHER_DOORBELL_VOL_MAX = 8 + DOORBELL_EXISTING_TYPE = {0: "Mechanical", 1: "Digital", 2: "Not Present"} SIREN_DURATION_MIN = 0 @@ -110,6 +126,8 @@ class OAuth: STICKUP_CAM_GEN3_KINDS = ["cocoa_camera"] BEAM_KINDS = ["beams_ct200_transformer"] +INTERCOM_KINDS = ["intercom_handset_audio"] + # error strings MSG_BOOLEAN_REQUIRED = "Boolean value is required." MSG_EXISTING_TYPE = "Integer value where {0}.".format(DOORBELL_EXISTING_TYPE) diff --git a/ring_doorbell/other.py b/ring_doorbell/other.py new file mode 100644 index 0000000..9866838 --- /dev/null +++ b/ring_doorbell/other.py @@ -0,0 +1,283 @@ +# coding: utf-8 +# vim:sw=4:ts=4:et: +"""Python Ring Other (Intercom) wrapper.""" +import json +import logging +import uuid + +from ring_doorbell.const import ( + DOORBELLS_ENDPOINT, + HEALTH_DOORBELL_ENDPOINT, + INTERCOM_ALLOWED_USERS, + INTERCOM_INVITATIONS_DELETE_ENDPOINT, + INTERCOM_INVITATIONS_ENDPOINT, + INTERCOM_KINDS, + INTERCOM_OPEN_ENDPOINT, + MIC_VOL_MAX, + MIC_VOL_MIN, + MSG_VOL_OUTBOUND, + OTHER_DOORBELL_VOL_MAX, + OTHER_DOORBELL_VOL_MIN, + SETTINGS_ENDPOINT, + VOICE_VOL_MAX, + VOICE_VOL_MIN, +) +from ring_doorbell.generic import RingGeneric + +_LOGGER = logging.getLogger(__name__) + + +class RingOther(RingGeneric): + """Implementation for Ring Intercom.""" + + def __init__(self, ring, device_api_id, shared=False): + super().__init__(ring, device_api_id) + self.shared = shared + + @property + def family(self): + """Return Ring device family type.""" + return "other" + + def update_health_data(self): + """Update health attrs.""" + self._health_attrs = ( + self._ring.query(HEALTH_DOORBELL_ENDPOINT.format(self.device_api_id)) + .json() + .get("device_health", {}) + ) + + @property + def model(self): + """Return Ring device model name.""" + if self.kind in INTERCOM_KINDS: + return "Intercom" + return None + + def has_capability(self, capability): + """Return if device has specific capability.""" + if capability == "open": + return self.kind in INTERCOM_KINDS + return False + + @property + def battery_life(self): + """Return battery life.""" + if self.kind in INTERCOM_KINDS: + if self._attrs.get("battery_life") is None: + return None + + value = int(self._attrs.get("battery_life", 0)) + if value and value > 100: + value = 100 + + return value + return None + + @property + def subscribed(self): + """Return if is online.""" + if self.kind in INTERCOM_KINDS: + result = self._attrs.get("subscribed") + if result is None: + return False + return True + return None + + @property + def has_subscription(self): + """Return boolean if the account has subscription.""" + if self.kind in INTERCOM_KINDS: + return self._attrs.get("features").get("show_recordings") + return None + + @property + def unlock_duration(self): + """Return time unlock switch is held closed""" + json.loads( + self._attrs.get("settings").get("intercom_settings").get("config") + ).get("analog", {}).get("unlock_duration") + + @property + def doorbell_volume(self): + """Return doorbell volume.""" + if self.kind in INTERCOM_KINDS: + return self._attrs.get("settings").get("doorbell_volume") + return None + + @doorbell_volume.setter + def doorbell_volume(self, value): + if not ( + (isinstance(value, int)) + and (OTHER_DOORBELL_VOL_MIN <= value <= OTHER_DOORBELL_VOL_MAX) + ): + _LOGGER.error( + "%s", + MSG_VOL_OUTBOUND.format(OTHER_DOORBELL_VOL_MIN, OTHER_DOORBELL_VOL_MAX), + ) + return False + + params = { + "doorbot[settings][doorbell_volume]": str(value), + } + url = DOORBELLS_ENDPOINT.format(self.device_api_id) + self._ring.query(url, extra_params=params, method="PUT") + self._ring.update_devices() + return True + + @property + def keep_alive_auto(self): + if self.kind in INTERCOM_KINDS: + return self._attrs.get("settings").get("keep_alive_auto") + return None + + @keep_alive_auto.setter + def keep_alive_auto(self, value): + url = SETTINGS_ENDPOINT.format(self.device_api_id) + payload = {"keep_alive_settings": {"keep_alive_auto": value}} + + self._ring.query(url, method="PATCH", json=payload) + self._ring.update_devices() + return True + + @property + def mic_volume(self): + """Return mic volume.""" + if self.kind in INTERCOM_KINDS: + return self._attrs.get("settings").get("mic_volume") + return None + + @mic_volume.setter + def mic_volume(self, value): + if not ((isinstance(value, int)) and (MIC_VOL_MIN <= value <= MIC_VOL_MAX)): + _LOGGER.error("%s", MSG_VOL_OUTBOUND.format(MIC_VOL_MIN, MIC_VOL_MAX)) + return False + + url = SETTINGS_ENDPOINT.format(self.device_api_id) + payload = {"volume_settings": {"mic_volume": value}} + + self._ring.query(url, method="PATCH", json=payload) + self._ring.update_devices() + return True + + @property + def voice_volume(self): + """Return voice volume.""" + if self.kind in INTERCOM_KINDS: + return self._attrs.get("settings").get("voice_volume") + return None + + @voice_volume.setter + def voice_volume(self, value): + if not ((isinstance(value, int)) and (VOICE_VOL_MIN <= value <= VOICE_VOL_MAX)): + _LOGGER.error("%s", MSG_VOL_OUTBOUND.format(VOICE_VOL_MIN, VOICE_VOL_MAX)) + return False + + url = SETTINGS_ENDPOINT.format(self.device_api_id) + payload = {"volume_settings": {"voice_volume": value}} + + self._ring.query(url, method="PATCH", json=payload) + self._ring.update_devices() + return True + + @property + def clip_length_max(self): + # this value sets an effective refractory period on consecutive rigns + # eg if set to default value of 60, rings occuring with 60 seconds of + # first will not be detected + + url = SETTINGS_ENDPOINT.format(self.device_api_id) + + return ( + self._ring.query(url, method="GET") + .json() + .get("video_settings") + .get("clip_length_max") + ) + + @clip_length_max.setter + def clip_length_max(self, value): + url = SETTINGS_ENDPOINT.format(self.device_api_id) + payload = {"video_settings": {"clip_length_max": value}} + self._ring.query(url, method="PATCH", json=payload) + self._ring.update_devices() + return True + + @property + def connection_status(self): + """Return connection status.""" + if self.kind in INTERCOM_KINDS: + return self._attrs.get("alerts").get("connection") + return None + + @property + def location_id(self): + """Return location id.""" + if self.kind in INTERCOM_KINDS: + return self._attrs.get("location_id", None) + return None + + @property + def allowed_users(self): + """Return list of users allowed or invited to access""" + if self.kind in INTERCOM_KINDS: + url = INTERCOM_ALLOWED_USERS.format(self.location_id) + return self._ring.query(url, method="GET").json() + + return None + + def open_door(self, user_id=-1): + """Open the door""" + + if self.kind in INTERCOM_KINDS: + url = INTERCOM_OPEN_ENDPOINT.format(self.device_api_id) + request_id = str(uuid.uuid4()) + # request_timestamp = int(time.time() * 1000) + payload = { + "command_name": "device_rpc", + "request": { + "id": request_id, + "jsonrpc": "2.0", + "method": "unlock_door", + "params": { + # "command_timeout": 5, + "door_id": 0, + # "issue_time": request_timestamp, + "user_id": user_id, + }, + }, + } + + response = self._ring.query(url, method="PUT", json=payload).json() + self._ring.update_devices() + if response.get("result", {}).get("code", -1) == 0: + return True + + return False + + def invite_access(self, email): + """Invite user""" + + if self.kind in INTERCOM_KINDS: + url = INTERCOM_INVITATIONS_ENDPOINT.format(self.location_id) + payload = { + "invitation": { + "doorbot_ids": [self.device_api_id], + "invited_email": email, + "group_ids": [], + } + } + self._ring.query(url, method="POST", json=payload) + return True + + return False + + def remove_access(self, user_id): + """Remove user access or invitation""" + + if self.kind in INTERCOM_KINDS: + url = INTERCOM_INVITATIONS_DELETE_ENDPOINT.format(self.location_id, user_id) + self._ring.query(url, method="DELETE") + return True + + return False diff --git a/ring_doorbell/ring.py b/ring_doorbell/ring.py index 8200e10..2f62491 100644 --- a/ring_doorbell/ring.py +++ b/ring_doorbell/ring.py @@ -9,6 +9,7 @@ from ring_doorbell.chime import RingChime from ring_doorbell.doorbot import RingDoorBell from ring_doorbell.group import RingLightGroup +from ring_doorbell.other import RingOther from ring_doorbell.stickup_cam import RingStickUpCam from .const import ( @@ -30,6 +31,7 @@ "authorized_doorbots": lambda ring, description: RingDoorBell( ring, description, shared=True ), + "other": RingOther, } @@ -181,6 +183,7 @@ def get_device_list(self): + devices["authorized_doorbots"] + devices["stickup_cams"] + devices["chimes"] + + devices["other"] ) def get_device_by_name(self, device_name): diff --git a/tests/conftest.py b/tests/conftest.py index 8c1ba26..088d223 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -139,4 +139,33 @@ def requests_mock_fixture(): status_code=204, content=b"", ) + mock.put( + "https://api.ring.com/clients_api/doorbots/185036587", + status_code=204, + content=b"", + ) + mock.get( + "https://api.ring.com/devices/v1/devices/185036587/settings", + text=load_fixture("ring_intercom_settings.json"), + ) + mock.get( + "https://api.ring.com/clients_api/locations/mock-location-id/users", + text=load_fixture("ring_intercom_users.json"), + ) + mock.post( + "https://api.ring.com/clients_api/locations/mock-location-id/invitations", + text="ok", + ) + mock.delete( + ( + "https://api.ring.com/clients_api/locations/" + "mock-location-id/invitations/123456789" + ), + text="ok", + ) + requestid = "44529542-3ed7-41da-807e-c170a01bac1d" + mock.put( + "https://api.ring.com/commands/v1/devices/185036587/device_rpc", + text='{"result": {"code": 0}, "id": "' + requestid + '", "jsonrpc": "2.0"}', + ) yield mock diff --git a/tests/fixtures/ring_devices.json b/tests/fixtures/ring_devices.json index c72b8c6..9ff917e 100644 --- a/tests/fixtures/ring_devices.json +++ b/tests/fixtures/ring_devices.json @@ -265,5 +265,92 @@ "stolen": false, "subscribed": true, "subscribed_motions": true, - "time_zone": "America/New_York"}] -} + "time_zone": "America/New_York" }], + "other": [ + { + "id": 185036587, + "kind": "intercom_handset_audio", + "description": "Ingress", + "location_id": "mock-location-id", + "schema_id": null, + "is_sidewalk_gateway": false, + "created_at": "2023-12-01T18:05:25Z", + "deactivated_at": null, + "owner": { + "id": 762490876, + "first_name": "", + "last_name": "", + "email": "" + }, + "device_id": "124ba1b3fe1a", + "time_zone": "Europe/Rome", + "firmware_version": "Up to Date", + "owned": true, + "ring_net_id": null, + "settings": { + "features_confirmed": 5, + "show_recordings": true, + "recording_ttl": 180, + "recording_enabled": false, + "keep_alive": null, + "keep_alive_auto": 45.0, + "doorbell_volume": 8, + "enable_chime": 1, + "theft_alarm_enable": 0, + "use_cached_domain": 1, + "use_server_ip": 0, + "server_domain": "fw.ring.com", + "server_ip": null, + "enable_log": 1, + "forced_keep_alive": null, + "mic_volume": 11, + "chime_settings": { + "enable": true, + "type": 2, + "duration": 10 + }, + "intercom_settings": { + "ring_to_open": false, + "predecessor": "{\"make\":\"Comelit\",\"model\":\"2738W\",\"wires\":2}", + "config": "{\"intercom_type\": 2, \"number_of_wires\": 2, \"autounlock_enabled\": false, \"speaker_gain\": [-49, -35, -25, -21, -16, -9, -6, -3, 0, 3, 6, 9], \"digital\": {\"audio_amp\": 0, \"chg_en\": false, \"fast_chg\": false, \"bypass\": false, \"idle_lvl\": 32, \"ext_audio\": false, \"ext_audio_term\": 0, \"off_hk_tm\": 0, \"unlk_ka\": false, \"unlock\": {\"cap_tm\": 1000, \"rpl_tm\": 1500, \"gain\": 1000, \"cmp_thr\": 4000, \"lvl\": 25000, \"thr\": 30, \"thr2\": 0, \"offln\": false, \"ac\": true, \"bias\": \"h\", \"tx_pin\": \"TXD2\", \"ack\": 0, \"prot\": \"Comelit_SB2\", \"ask\": {\"f\": 25000, \"b\": 333}}, \"ring\": {\"cap_tm\": 40, \"rpl_tm\": 200, \"gain\": 2000, \"cmp_thr\": 4500, \"lvl\": 28000, \"thr\": 30, \"thr2\": 0, \"offln\": false, \"ac\": true, \"bias\": \"m\", \"tx_pin\": \"TXD2\", \"ack\": 0, \"prot\": \"Comelit_SB2\", \"ask\": {\"f\": 25000, \"b\": 333}}, \"hook_off\": {\"cap_tm\": 1000, \"rpl_tm\": 1500, \"gain\": 1000, \"cmp_thr\": 4000, \"lvl\": 25000, \"thr\": 30, \"thr2\": 0, \"offln\": false, \"ac\": true, \"bias\": \"h\", \"tx_pin\": \"TXD2\", \"ack\": 0, \"prot\": \"Comelit_SB2\", \"ask\": {\"f\": 25000, \"b\": 333}}, \"hook_on\": {\"cap_tm\": 1000, \"rpl_tm\": 1500, \"gain\": 1000, \"cmp_thr\": 4000, \"lvl\": 25000, \"thr\": 30, \"thr2\": 0, \"offln\": false, \"ac\": true, \"bias\": \"h\", \"tx_pin\": \"TXD2\", \"ack\": 0, \"prot\": \"Comelit_SB2\", \"ask\": {\"f\": 25000, \"b\": 333}}}}", + "intercom_type": "DF", + "replication": 1, + "unlock_mode": 0 + }, + "voice_volume": 11 + }, + "alerts": { + "connection": "online", + "ota_status": "timeout" + }, + "function": { + "name": null + }, + "subscribed": false, + "battery_life": "52", + "features": { + "cfes_eligible": false, + "motion_zone_recommendation": false, + "motions_enabled": true, + "show_recordings": true, + "show_vod_settings": true, + "rich_notifications_eligible": false, + "show_offline_motion_events": false, + "sheila_camera_eligible": null, + "sheila_camera_processing_eligible": null, + "dynamic_network_switching_eligible": false, + "chime_auto_detect_capable": false, + "missing_key_delivery_address": false, + "show_24x7_lite": false, + "recording_24x7_eligible": null + }, + "metadata": { + "ethernet": false, + "legacy_fw_migrated": true, + "imported_from_amazon": false, + "is_sidewalk_gateway": false, + "key_access_point_associated": true + } + } + ] +} \ No newline at end of file diff --git a/tests/fixtures/ring_intercom_settings.json b/tests/fixtures/ring_intercom_settings.json new file mode 100644 index 0000000..c66220e --- /dev/null +++ b/tests/fixtures/ring_intercom_settings.json @@ -0,0 +1,439 @@ +{ + "type": "intercom_handset_audio", + "advanced_motion_settings": { + "zone_1": { + "name": "Default Zone", + "state": 2, + "vertex1": { + "x": 0, + "y": 0.4 + }, + "vertex2": { + "x": 0.333333, + "y": 0.4 + }, + "vertex3": { + "x": 0.666666, + "y": 0.4 + }, + "vertex4": { + "x": 1, + "y": 0.4 + }, + "vertex5": { + "x": 1, + "y": 1 + }, + "vertex6": { + "x": 0.666666, + "y": 1 + }, + "vertex7": { + "x": 0.333333, + "y": 1 + }, + "vertex8": { + "x": 0, + "y": 1 + } + }, + "zone_2": { + "name": "Zone 2", + "state": 0, + "vertex1": { + "x": 0, + "y": 0 + }, + "vertex2": { + "x": 0, + "y": 0 + }, + "vertex3": { + "x": 0, + "y": 0 + }, + "vertex4": { + "x": 0, + "y": 0 + }, + "vertex5": { + "x": 0, + "y": 0 + }, + "vertex6": { + "x": 0, + "y": 0 + }, + "vertex7": { + "x": 0, + "y": 0 + }, + "vertex8": { + "x": 0, + "y": 0 + } + }, + "zone_3": { + "name": "Zone 3", + "state": 0, + "vertex1": { + "x": 0, + "y": 0 + }, + "vertex2": { + "x": 0, + "y": 0 + }, + "vertex3": { + "x": 0, + "y": 0 + }, + "vertex4": { + "x": 0, + "y": 0 + }, + "vertex5": { + "x": 0, + "y": 0 + }, + "vertex6": { + "x": 0, + "y": 0 + }, + "vertex7": { + "x": 0, + "y": 0 + }, + "vertex8": { + "x": 0, + "y": 0 + } + } + }, + "backend_settings": { + "live_view_preset_profile": "middle", + "motion_snooze_preset_profile": "low", + "enable_rich_notifications": false, + "terms_of_service_accepted": { + "autoreply": false, + "concierge": false + }, + "paid_features": { + "alexa_concierge": true, + "cv_triggers": true, + "human": true, + "loitering": true, + "motion": true, + "other_motion": true, + "package_delivery": true, + "package_pickup": true, + "sheila_cv": true, + "sheila_recording": true + }, + "features_confirmed": 5 + }, + "chime_settings": { + "enable": true, + "type": 2, + "duration": 10 + }, + "motion_settings": { + "motion_detection_enabled": true, + "advanced_motion_detection_enabled": true, + "advanced_motion_detection_mode": "edge", + "advanced_motion_detection_human_only_mode": false, + "advanced_motion_detection_loitering_mode": false, + "advanced_motion_zones_enabled": true, + "advanced_motion_zones_type": "8vertices", + "advanced_pir_motion_zones": { + "zone1_sensitivity": 5, + "zone2_sensitivity": 5, + "zone3_sensitivity": 5, + "zone4_sensitivity": 5, + "zone5_sensitivity": 5, + "zone6_sensitivity": 5 + }, + "loitering_threshold": 10, + "enable_recording": true, + "end_detection": 20, + "advanced_motion_recording_human_mode": false, + "advanced_motion_glance_enabled": false, + "zone_settings_v2_enabled": true, + "motion_snooze_profile": [ + 1, + 5, + 15 + ] + }, + "pir_settings": { + "sensitivity_1": 5, + "sensitivity_2": 5, + "sensitivity_3": 5, + "zone_enable": 31, + "zone_mask": 0 + }, + "stream_settings": { + "profile": 2, + "active_streaming_profile": "rms", + "streaming_profiles": { + "freeswitch": {}, + "rms": { + "host": "rms-eu-west-1.rapi.us-east-1.prod.client.cap.ring.devices.a2z.com", + "port": 443 + } + } + }, + "video_settings": { + "exposure_control": 2, + "night_color_enable": false, + "hdr_enable": false, + "clip_length_max": 60, + "clip_length_min": 10, + "ae_mode": 0, + "ignore_zones": { + "zone1": { + "name": "undefined", + "state": 0, + "vertex1": { + "x": 0, + "y": 0 + }, + "vertex2": { + "x": 0, + "y": 0 + } + }, + "zone2": { + "name": "undefined", + "state": 0, + "vertex1": { + "x": 0, + "y": 0 + }, + "vertex2": { + "x": 0, + "y": 0 + } + }, + "zone3": { + "name": "undefined", + "state": 0, + "vertex1": { + "x": 0, + "y": 0 + }, + "vertex2": { + "x": 0, + "y": 0 + } + }, + "zone4": { + "name": "undefined", + "state": 0, + "vertex1": { + "x": 0, + "y": 0 + }, + "vertex2": { + "x": 0, + "y": 0 + } + } + }, + "encryption_enabled": false, + "encryption_method": 1 + }, + "vod_settings": { + "enable": true, + "toggled_at": "2016-08-01T00:00:00+00:00", + "use_cached_vod_domain": false + }, + "volume_settings": { + "doorbell_volume": 6, + "mic_volume": 11, + "voice_volume": 11 + }, + "general_settings": { + "enable_audio_recording": true, + "lite_24x7_enabled": false, + "offline_motion_event_enabled": false, + "lite_24x7_subscribed": true, + "offline_motion_event_subscribed": false, + "firmwares_locked": false, + "utc_offset": "+01:00", + "theft_alarm_enable": false, + "wrapup_domain": "wu.ring.com", + "use_wrapup_domain": false, + "data_collection_enabled": false, + "log_selected_sink": 0, + "country_code": "IT" + }, + "snapshot_settings": { + "frequency_secs": 3600, + "lite_24x7_resolution_p": 360, + "ome_resolution_p": 360, + "max_upload_kb": 5000, + "frequency_after_secs": 2, + "period_after_secs": 30, + "close_container": 1 + }, + "client_device_settings": { + "ringtones_enabled": false, + "people_only_enabled": false, + "advanced_motion_enabled": false, + "motion_message_enabled": false, + "shadow_correction_enabled": false, + "night_vision_enabled": false, + "light_schedule_enabled": false, + "rich_notifications_eligible": false, + "show_24x7_lite": false, + "show_offline_motion_events": false, + "cfes_eligible": false, + "show_radar_data": false, + "motion_zone_recommendation": false, + "ptz_setup_complete": false, + "local_playback_enabled": false, + "dynamic_network_switching_eligible": false, + "missing_key_delivery_address": false + }, + "light_snooze_settings": { + "duration": 0 + }, + "cv_settings": { + "detection_types": { + "human": { + "enabled": false, + "mode": "none", + "notification": false + }, + "loitering": { + "enabled": false, + "mode": "none", + "notification": false + }, + "motion": { + "enabled": true, + "mode": "edge", + "notification": true + }, + "moving_vehicle": { + "enabled": false, + "mode": "none", + "notification": false + }, + "nearby_pom": { + "enabled": false, + "mode": "none", + "notification": false + }, + "other_motion": { + "enabled": false, + "mode": "none", + "notification": false + }, + "package_delivery": { + "enabled": false, + "mode": "none", + "notification": false + }, + "package_pickup": { + "enabled": false, + "mode": "none", + "notification": false + } + }, + "threshold": { + "loitering": 10, + "package_delivery": 2 + } + }, + "concierge_settings": { + "mode": "disabled", + "alexa_settings": { + "delay_ms": 10000 + }, + "autoreply_settings": { + "delay_ms": 10000 + } + }, + "schedule_settings": {}, + "intercom_settings": { + "predecessor": "{\"make\":\"Comelit\",\"model\":\"2738W\",\"wires\":2}", + "config": "{\"intercom_type\": 2, \"number_of_wires\": 2, \"autounlock_enabled\": false, \"speaker_gain\": [-49, -35, -25, -21, -16, -9, -6, -3, 0, 3, 6, 9], \"digital\": {\"audio_amp\": 0, \"chg_en\": false, \"fast_chg\": false, \"bypass\": false, \"idle_lvl\": 32, \"ext_audio\": false, \"ext_audio_term\": 0, \"off_hk_tm\": 0, \"unlk_ka\": false, \"unlock\": {\"cap_tm\": 1000, \"rpl_tm\": 1500, \"gain\": 1000, \"cmp_thr\": 4000, \"lvl\": 25000, \"thr\": 30, \"thr2\": 0, \"offln\": false, \"ac\": true, \"bias\": \"h\", \"tx_pin\": \"TXD2\", \"ack\": 0, \"prot\": \"Comelit_SB2\", \"ask\": {\"f\": 25000, \"b\": 333}}, \"ring\": {\"cap_tm\": 40, \"rpl_tm\": 200, \"gain\": 2000, \"cmp_thr\": 4500, \"lvl\": 28000, \"thr\": 30, \"thr2\": 0, \"offln\": false, \"ac\": true, \"bias\": \"m\", \"tx_pin\": \"TXD2\", \"ack\": 0, \"prot\": \"Comelit_SB2\", \"ask\": {\"f\": 25000, \"b\": 333}}, \"hook_off\": {\"cap_tm\": 1000, \"rpl_tm\": 1500, \"gain\": 1000, \"cmp_thr\": 4000, \"lvl\": 25000, \"thr\": 30, \"thr2\": 0, \"offln\": false, \"ac\": true, \"bias\": \"h\", \"tx_pin\": \"TXD2\", \"ack\": 0, \"prot\": \"Comelit_SB2\", \"ask\": {\"f\": 25000, \"b\": 333}}, \"hook_on\": {\"cap_tm\": 1000, \"rpl_tm\": 1500, \"gain\": 1000, \"cmp_thr\": 4000, \"lvl\": 25000, \"thr\": 30, \"thr2\": 0, \"offln\": false, \"ac\": true, \"bias\": \"h\", \"tx_pin\": \"TXD2\", \"ack\": 0, \"prot\": \"Comelit_SB2\", \"ask\": {\"f\": 25000, \"b\": 333}}}}", + "intercom_type": "DF", + "ring_to_open": false, + "unlock_mode": 0, + "replication": 1 + }, + "sheila_settings": { + "cv_processing_enabled": false, + "local_storage_enabled": false + }, + "keep_alive_settings": { + "keep_alive_auto": 45 + }, + "lite_24x7": { + "mode": "cloud", + "mode_properties": { + "sheila": {} + } + }, + "zone_settings": { + "motion": [ + { + "id": "718bd4c3-a4e4-4460-8118-90098d5f237a", + "name": "Default Zone", + "state": "enabled", + "properties": { + "detection_types": [ + "motion" + ] + }, + "vertices": [ + { + "x": 0, + "y": 0.4 + }, + { + "x": 0.333333, + "y": 0.4 + }, + { + "x": 0.666666, + "y": 0.4 + }, + { + "x": 1, + "y": 0.4 + }, + { + "x": 1, + "y": 1 + }, + { + "x": 0.666666, + "y": 1 + }, + { + "x": 0.333333, + "y": 1 + }, + { + "x": 0, + "y": 1 + } + ] + } + ] + }, + "ptz_settings": {}, + "thermometer_settings": {}, + "auth_settings": { + "fallback_enabled": false, + "protocol": "basic", + "retry_interval": 3600 + }, + "attestation_settings": { + "rda_enabled": true + } +} diff --git a/tests/fixtures/ring_intercom_users.json b/tests/fixtures/ring_intercom_users.json new file mode 100644 index 0000000..37cdbc7 --- /dev/null +++ b/tests/fixtures/ring_intercom_users.json @@ -0,0 +1,34 @@ +[ + { + "id": 115201490, + "verified": true, + "first_name": "John", + "last_name": "Carter", + "email": "john@cart.er", + "object_type": "user", + "devices": [ + { + "id": 185036587, + "role": "owner", + "device_type": "intercom_handset_audio", + "permissions": null + } + ] + }, + { + "id": 194872097, + "verified": true, + "first_name": "Bob", + "last_name": "Meloni", + "email": "bob@melo.ni", + "object_type": "user", + "devices": [ + { + "id": 185036587, + "role": "shared_user", + "device_type": "intercom_handset_audio", + "permissions": null + } + ] + } +] diff --git a/tests/test_other.py b/tests/test_other.py new file mode 100644 index 0000000..4fb52fa --- /dev/null +++ b/tests/test_other.py @@ -0,0 +1,89 @@ +"""The tests for the Ring platform.""" + + +def test_other_attributes(ring): + """Test the Ring Other class and methods.""" + dev = ring.devices()["other"][0] + + assert dev.id != 99999 + assert dev.device_id == "124ba1b3fe1a" + assert dev.kind == "intercom_handset_audio" + assert dev.model == "Intercom" + assert dev.location_id == "mock-location-id" + assert dev.has_capability("battery") is False + assert dev.has_capability("open") is True + assert dev.timezone == "Europe/Rome" + assert dev.battery_life == 52 + assert dev.doorbell_volume == 8 + assert dev.mic_volume == 11 + assert dev.clip_length_max == 60 + assert dev.connection_status == "online" + assert len(dev.allowed_users) == 2 + assert dev.subscribed is True + assert dev.has_subscription is True + assert dev.unlock_duration is None + assert dev.keep_alive_auto == 45.0 + + dev.update_health_data() + assert dev.wifi_name == "ring_mock_wifi" + assert dev.wifi_signal_category == "good" + assert dev.wifi_signal_strength != 100 + + +def test_other_controls(ring, requests_mock): + dev = ring.devices()["other"][0] + + dev.doorbell_volume = 6 + history = list(filter(lambda x: x.method == "PUT", requests_mock.request_history)) + assert history[0].path == "/clients_api/doorbots/185036587" + assert history[0].query == "doorbot%5bsettings%5d%5bdoorbell_volume%5d=6" + + dev.mic_volume = 10 + dev.voice_volume = 9 + dev.clip_length_max = 30 + dev.keep_alive_auto = 32.2 + history = list(filter(lambda x: x.method == "PATCH", requests_mock.request_history)) + assert history[0].path == "/devices/v1/devices/185036587/settings" + assert history[0].text == '{"volume_settings": {"mic_volume": 10}}' + assert history[1].path == "/devices/v1/devices/185036587/settings" + assert history[1].text == '{"volume_settings": {"voice_volume": 9}}' + assert history[2].path == "/devices/v1/devices/185036587/settings" + assert history[2].text == '{"video_settings": {"clip_length_max": 30}}' + assert history[3].path == "/devices/v1/devices/185036587/settings" + assert history[3].text == '{"keep_alive_settings": {"keep_alive_auto": 32.2}}' + + +def test_other_invitations(ring, requests_mock): + dev = ring.devices()["other"][0] + + dev.invite_access("test@example.com") + history = list(filter(lambda x: x.method == "POST", requests_mock.request_history)) + assert history[2].path == "/clients_api/locations/mock-location-id/invitations" + assert history[2].text == ( + '{"invitation": {"doorbot_ids": [185036587],' + ' "invited_email": "test@example.com", "group_ids": []}}' + ) + + dev.remove_access(123456789) + history = list( + filter(lambda x: x.method == "DELETE", requests_mock.request_history) + ) + assert ( + history[0].path + == "/clients_api/locations/mock-location-id/invitations/123456789" + ) + + +def test_other_open_door(ring, requests_mock, mocker): + dev = ring.devices()["other"][0] + + mocker.patch("uuid.uuid4", return_value="987654321") + + dev.open_door(15) + history = list(filter(lambda x: x.method == "PUT", requests_mock.request_history)) + assert history[0].path == "/commands/v1/devices/185036587/device_rpc" + assert history[0].text == ( + '{"command_name": "device_rpc", "request": ' + '{"id": "987654321", "jsonrpc": "2.0", "method": "unlock_door", "params": ' + '{"door_id": 0, "user_id": 15}}}' + ) diff --git a/tests/test_ring.py b/tests/test_ring.py index 4633d00..233e676 100644 --- a/tests/test_ring.py +++ b/tests/test_ring.py @@ -1,13 +1,4 @@ """The tests for the Ring platform.""" -import json - -import pytest -import requests_mock - -from ring_doorbell import Auth, Ring -from ring_doorbell.doorbot import RingDoorBell -from ring_doorbell.listen import can_listen -from tests.conftest import load_fixture def test_basic_attributes(ring): @@ -17,6 +8,7 @@ def test_basic_attributes(ring): assert len(data["doorbots"]) == 1 assert len(data["authorized_doorbots"]) == 1 assert len(data["stickup_cams"]) == 1 + assert len(data["other"]) == 1 def test_chime_attributes(ring): From fae00a03050bb2b65dd65c254b1db0c309e2478a Mon Sep 17 00:00:00 2001 From: sdb9696 Date: Tue, 30 Jan 2024 16:04:21 +0000 Subject: [PATCH 3/5] Use sphinx changelog generator --- docs/source/changelog.rst | 6 ++++++ docs/source/conf.py | 3 +++ 2 files changed, 9 insertions(+) diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 6f90edd..bc28082 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -1 +1,7 @@ +.. changelog:: + :changelog-url: https://python-ring-doorbell.readthedocs.io/changelog.html + :github: https://github.com/tchellomello/python-ring-doorbell/releases/ + :pypi: https://pypi.org/project/ring-doorbell/ + + .. include:: ../../CHANGELOG.rst \ No newline at end of file diff --git a/docs/source/conf.py b/docs/source/conf.py index d420ff3..92a3a84 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -2,6 +2,7 @@ # # For the full list of built-in configuration values, see the documentation: # https://www.sphinx-doc.org/en/master/usage/configuration.html +import os from importlib.metadata import version as _version # -- Project information ----------------------------------------------------- @@ -21,6 +22,7 @@ "sphinx.ext.coverage", "sphinx.ext.viewcode", "sphinx.ext.todo", + "sphinx_github_changelog", ] templates_path = ["_templates"] @@ -31,3 +33,4 @@ html_theme = "sphinx_rtd_theme" html_static_path = ["_static"] +sphinx_github_changelog_token = os.environ.get("CHANGELOG_GITHUB_TOKEN") \ No newline at end of file From 9126605be67eb4300c2503da8b98df725a59915d Mon Sep 17 00:00:00 2001 From: sdb9696 Date: Tue, 30 Jan 2024 16:11:43 +0000 Subject: [PATCH 4/5] Fix docs dependency --- poetry.lock | 40 ++++++++++++++++++++-------------------- pyproject.toml | 4 ++-- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/poetry.lock b/poetry.lock index 91c78ef..fc97ddc 100644 --- a/poetry.lock +++ b/poetry.lock @@ -4,7 +4,7 @@ name = "alabaster" version = "0.7.13" description = "A configurable sidebar-enabled Sphinx theme" -optional = false +optional = true python-versions = ">=3.6" files = [ {file = "alabaster-0.7.13-py3-none-any.whl", hash = "sha256:1ee19aca801bbabb5ba3f5f258e4422dfa86f82f3e9cefb0859b283cdd7f62a3"}, @@ -64,7 +64,7 @@ colorama = {version = "*", markers = "platform_system == \"Windows\""} name = "babel" version = "2.13.0" description = "Internationalization utilities" -optional = false +optional = true python-versions = ">=3.7" files = [ {file = "Babel-2.13.0-py3-none-any.whl", hash = "sha256:fbfcae1575ff78e26c7449136f1abbefc3c13ce542eeb13d43d50d8b047216ec"}, @@ -492,7 +492,7 @@ files = [ name = "docutils" version = "0.18.1" description = "Docutils -- Python Documentation Utilities" -optional = false +optional = true python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [ {file = "docutils-0.18.1-py2.py3-none-any.whl", hash = "sha256:23010f129180089fbcd3bc08cfefccb3b890b0050e1ca00c867036e9d161b98c"}, @@ -607,7 +607,7 @@ files = [ name = "imagesize" version = "1.4.1" description = "Getting image size from png/jpeg/jpeg2000/gif file" -optional = false +optional = true python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ {file = "imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b"}, @@ -618,7 +618,7 @@ files = [ name = "importlib-metadata" version = "6.8.0" description = "Read metadata from Python packages" -optional = false +optional = true python-versions = ">=3.8" files = [ {file = "importlib_metadata-6.8.0-py3-none-any.whl", hash = "sha256:3ebb78df84a805d7698245025b975d9d67053cd94c79245ba4b3eb694abe68bb"}, @@ -665,7 +665,7 @@ requirements-deprecated-finder = ["pip-api", "pipreqs"] name = "jinja2" version = "3.1.2" description = "A very fast and expressive template engine." -optional = false +optional = true python-versions = ">=3.7" files = [ {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, @@ -682,7 +682,7 @@ i18n = ["Babel (>=2.7)"] name = "markupsafe" version = "2.1.3" description = "Safely add untrusted strings to HTML/XML markup." -optional = false +optional = true python-versions = ">=3.7" files = [ {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa"}, @@ -944,7 +944,7 @@ files = [ name = "pygments" version = "2.16.1" description = "Pygments is a syntax highlighting package written in Python." -optional = false +optional = true python-versions = ">=3.7" files = [ {file = "Pygments-2.16.1-py3-none-any.whl", hash = "sha256:13fc09fa63bc8d8671a6d247e1eb303c4b343eaee81d861f3404db2935653692"}, @@ -1262,7 +1262,7 @@ files = [ name = "snowballstemmer" version = "2.2.0" description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." -optional = false +optional = true python-versions = "*" files = [ {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, @@ -1273,7 +1273,7 @@ files = [ name = "sphinx" version = "7.1.2" description = "Python documentation generator" -optional = false +optional = true python-versions = ">=3.8" files = [ {file = "sphinx-7.1.2-py3-none-any.whl", hash = "sha256:d170a81825b2fcacb6dfd5a0d7f578a053e45d3f2b153fecc948c37344eb4cbe"}, @@ -1308,7 +1308,7 @@ test = ["cython", "filelock", "html5lib", "pytest (>=4.6)"] name = "sphinx-github-changelog" version = "1.2.1" description = "Build a sphinx changelog from GitHub Releases" -optional = false +optional = true python-versions = ">=3.7,<4.0" files = [ {file = "sphinx_github_changelog-1.2.1-py3-none-any.whl", hash = "sha256:27b8906220c8010f116b61c90980f84d12f9446fb144f7c575c124e9c92e6c46"}, @@ -1343,7 +1343,7 @@ dev = ["bump2version", "sphinxcontrib-httpdomain", "transifex-client", "wheel"] name = "sphinxcontrib-applehelp" version = "1.0.4" description = "sphinxcontrib-applehelp is a Sphinx extension which outputs Apple help books" -optional = false +optional = true python-versions = ">=3.8" files = [ {file = "sphinxcontrib-applehelp-1.0.4.tar.gz", hash = "sha256:828f867945bbe39817c210a1abfd1bc4895c8b73fcaade56d45357a348a07d7e"}, @@ -1358,7 +1358,7 @@ test = ["pytest"] name = "sphinxcontrib-devhelp" version = "1.0.2" description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp document." -optional = false +optional = true python-versions = ">=3.5" files = [ {file = "sphinxcontrib-devhelp-1.0.2.tar.gz", hash = "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4"}, @@ -1373,7 +1373,7 @@ test = ["pytest"] name = "sphinxcontrib-htmlhelp" version = "2.0.1" description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" -optional = false +optional = true python-versions = ">=3.8" files = [ {file = "sphinxcontrib-htmlhelp-2.0.1.tar.gz", hash = "sha256:0cbdd302815330058422b98a113195c9249825d681e18f11e8b1f78a2f11efff"}, @@ -1402,7 +1402,7 @@ Sphinx = ">=1.8" name = "sphinxcontrib-jsmath" version = "1.0.1" description = "A sphinx extension which renders display math in HTML via JavaScript" -optional = false +optional = true python-versions = ">=3.5" files = [ {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, @@ -1416,7 +1416,7 @@ test = ["flake8", "mypy", "pytest"] name = "sphinxcontrib-qthelp" version = "1.0.3" description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp document." -optional = false +optional = true python-versions = ">=3.5" files = [ {file = "sphinxcontrib-qthelp-1.0.3.tar.gz", hash = "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72"}, @@ -1431,7 +1431,7 @@ test = ["pytest"] name = "sphinxcontrib-serializinghtml" version = "1.1.5" description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)." -optional = false +optional = true python-versions = ">=3.5" files = [ {file = "sphinxcontrib-serializinghtml-1.1.5.tar.gz", hash = "sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952"}, @@ -1543,7 +1543,7 @@ test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess name = "zipp" version = "3.17.0" description = "Backport of pathlib-compatible object wrapper for zip files" -optional = false +optional = true python-versions = ">=3.8" files = [ {file = "zipp-3.17.0-py3-none-any.whl", hash = "sha256:0e923e726174922dce09c53c59ad483ff7bbb8e572e00c7f7c46b88556409f31"}, @@ -1555,10 +1555,10 @@ docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.link testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy (>=0.9.1)", "pytest-ruff"] [extras] -docs = ["sphinx", "sphinx-rtd-theme"] +docs = ["sphinx", "sphinx-github-changelog", "sphinx-rtd-theme"] listen = ["firebase-messaging"] [metadata] lock-version = "2.0" python-versions = "^3.8" -content-hash = "2c15ebc01451e34e44e63172a82ff0cda9d6007850e553fc06166b62c0578c9b" +content-hash = "5a0289d7837829f26da51ae589718313616bc10558e8b58921f3f8dae1f25e32" diff --git a/pyproject.toml b/pyproject.toml index 03c123f..a32cd9b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,6 +48,7 @@ asyncclick = ">=8" anyio = "*" # see https://github.com/python-trio/asyncclick/issues/18 sphinx = {version = "<7.2.6", optional = true} sphinx-rtd-theme = {version = "^1.3.0", optional = true} +sphinx-github-changelog = {version = "^1.2.1", optional = true} firebase-messaging = {version = "^0.2.0", optional = true} [tool.poetry.group.dev.dependencies] @@ -58,7 +59,6 @@ pylint = "*" pytest = "*" pytest-cov = "*" requests-mock = "*" -sphinx-github-changelog = "^1.2.1" tox = "*" pytest-asyncio = "*" pytest-mock = "*" @@ -66,7 +66,7 @@ black = "*" pytest-socket = "^0.6.0" [tool.poetry.extras] -docs = ["sphinx", "sphinx-rtd-theme"] +docs = ["sphinx", "sphinx-rtd-theme", "sphinx-github-changelog"] listen = ["firebase-messaging"] [tool.pytest.ini_options] From 4ae48ba55abd7e4756f25a46b951c6c7a6312882 Mon Sep 17 00:00:00 2001 From: sdb9696 Date: Tue, 30 Jan 2024 17:13:56 +0000 Subject: [PATCH 5/5] Update changelog generator docs --- CHANGELOG.rst | 779 +------------------------------------- CONTRIBUTING.rst | 17 + docs/source/changelog.rst | 13 +- docs/source/conf.py | 1 + docs/source/index.rst | 3 + 5 files changed, 29 insertions(+), 784 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index fed19fd..015204b 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,781 +2,4 @@ Changelog ========= -0.7.4 -===== - -Released on 2023-09-27 - `GitHub `__ - `PyPI `__ - -.. raw:: html - - -

What's Changed

-
    -
  • Fix and update cli (#288) @sdb9696
  • -
  • Update to pyproject.toml, poetry, and update docs to use yaml config (#287) @sdb9696
  • -
- - - -0.7.3 -===== - -Released on 2023-09-11 - `GitHub `__ - `PyPI `__ - -.. raw:: html - - -

What's Changed

-
    -
  • Add motion detection enabled switch (#282) @sdb9696
  • -
  • Fix ci to use up to date python versions and include pre-commit-config (#281) @sdb9696
  • -
  • Add support for Floodlight Cam Pro (#280) @twasilczyk
  • -
- - - - -0.7.2 -===== - -Released on 2021-12-18 - `GitHub `__ - `PyPI `__ - -.. raw:: html - - -

What's Changed

-
    -
  • Recognize cocoa_floodlight as a floodlight kind (#255) @mwren
  • -
- - - - -0.7.1 -===== - -Released on 2021-08-26 - `GitHub `__ - `PyPI `__ - -.. raw:: html - - -

What's Changed

-
    -
  • fix memory growth when calling url_recording (#253) @prwood80
  • -
- - - - -0.7.0 -===== - -Released on 2021-02-05 - `GitHub `__ - `PyPI `__ - -.. raw:: html - - -

New features

-
    -
  • #231 Support for light groups (and thus Transformers indirectly) (thanks @decompil3d!)
  • -
-

Fixes

-
    -
  • #196 Fix snapshot functionality (thanks @dshokouhi!)
  • -
  • #225 Fix live stream functionality (thanks @JoeDaddy7105!)
  • -
  • #228 Avoid multiple clients in list by maintaining consistent hardware ID (thanks @riptidewave93!)
  • -
  • #185 Return None instead of 0 for battery level when a device is not battery powered (thanks @balloob!)
  • -
  • #218 Fix snapshot again and add download option (thanks @kvntng17!)
  • -
-

Misc

- - - - - -0.6.2 -===== - -Released on 2020-11-21 - `GitHub `__ - `PyPI `__ - -.. raw:: html - - - - - - - -0.6.1 -===== - -Released on 2020-09-28 - `GitHub `__ - `PyPI `__ - -.. raw:: html - - -

Relax requirements version pinning - 59ae9b1

- - - - -0.6.0 -===== - -Released on 2020-01-14 - `GitHub `__ - `PyPI `__ - -.. raw:: html - - -

Major breaking change

-

Ring APIs offer 1 endpoint with all device info. 1 with all health for doorbells etc. The API used to make a request from each device to the "all device" endpoint and fetch its own data.

-

With the new approach we now just fetch the data once and each device will fetch that data. This significantly reduces the number of requests.

-

See updated test.py on usage.

-

Changes:

-
    -
  • Pass a user agent to the auth class to identify your project (at request from Ring)
  • -
  • For most updates, just call ring.update_all(). If you want health data (wifi stuff), call device.update_health_data() on each device
  • -
  • Renamed device.id -> device.device_id, device.account_id -> device.id to follow API naming.
  • -
  • Call ring.update_all() at least once before querying for devices
  • -
  • Querying devices now is a function ring.devices() instead of property ring.devices
  • -
  • Removed ring.chimes, ring.doorbells, ring.stickup_cams
  • -
  • Cleaned up tests with pytest fixtures
  • -
  • Run Black on code to silence hound.
  • -
- - - - -0.5.0 -===== - -Released on 2020-01-12 - `GitHub `__ - `PyPI `__ - -.. raw:: html - - -

Breaking Change

-

The Auth class no longer takes an otp_callback but now takes an otp_code. It raises MissingTokenError if otp_code is required. See the updated example. This prevents duplicate SMS messages. Thanks to @steve-gombos

-

Timeout has been increased from 5 to 10 seconds to give requests a bit more time (by @cyberjunky)

- - - - -0.4.0 -===== - -Released on 2020-01-11 - `GitHub `__ - `PyPI `__ - -.. raw:: html - - -

Major breaking change.

-

This release is a major breaking change to clean up the auth and follow proper OAuth2. Big thanks to @steve-gombos for this.

-

All authentication is now done inside Auth. The first time you need username, password and optionally an 2-factor auth callback function. After that you have a token and that can be used.

-

The old cache file is no longer in use and can be removed.

-

Example usage (also available as test.py):

-
import json
-      from pathlib import Path
-      
-      from ring_doorbell import Ring, Auth
-      
-      
-      cache_file = Path('test_token.cache')
-      
-      
-      def token_updated(token):
-          cache_file.write_text(json.dumps(token))
-      
-      
-      def otp_callback():
-          auth_code = input("2FA code: ")
-          return auth_code
-      
-      
-      def main():
-          if cache_file.is_file():
-              auth = Auth(json.loads(cache_file.read_text()), token_updated)
-          else:
-              username = input("Username: ")
-              password = input("Password: ")
-              auth = Auth(None, token_updated)
-              auth.fetch_token(username, password, otp_callback)
-      
-          ring = Ring(auth)
-          print(ring.devices)
-      
-      
-      if __name__ == '__main__':
-          main()
- - - - -Version 0.2.9 -============= - -Released on 2020-01-03 - `GitHub `__ - `PyPI `__ - -.. raw:: html - - -
    -
  • Fixed Compatibility with Python 2 (old-school typing syntax in docstrings); fix for OAuth.SCOPE - @ZachBenz #163
  • -
  • Implemented timeouts for HTTP requests methods - @tchellomello #165
  • -
  • Use auth expires_in to refresh oauth tokens. - @jeromelaban #167
  • -
  • Fixed logic and simplified module imports - @tchellomello #168
  • -
- - - - -Version 0.2.8 -============= - -Released on 2019-12-27 - `GitHub `__ - `PyPI `__ - -.. raw:: html - - -

Quick fix to make sure the requests-oauthlib gets installed. Made requirements.txt and setup.py consistent. @tchellomello - #158

- - - - -Version 0.2.6 -============= - -Released on 2019-12-27 - `GitHub `__ - `PyPI `__ - -.. raw:: html - - -

This release includes a properly OAuth2 handle implemented by @steve-gombos. Many thanks for all involved to make this happen!

-

Fix for Issue #146 #149 - @ZachBenz
- Fix R1705: Unnecessary elif after return (no-else-return) #151 - @xernaj
- OAuth Fixes #152 - @steve-gombos

- - - - -Version 0.2.5 -============= - -Released on 2019-12-20 - `GitHub `__ - `PyPI `__ - -.. raw:: html - - -

@dshokouhi - Add a couple of device kinds #137
- @xernaj - Fix/oauth fail due to blocked user agent #143

-

Many thanks for your efforts and help!!

- - - - -Version 0.2.3 -============= - -Released on 2019-03-05 - `GitHub `__ - `PyPI `__ - -.. raw:: html - - - -

Many thanks to @MorganBulkeley @steveww @jsetton
- You guys rock!!

- - - - -Version 0.2.2 -============= - -Released on 2018-10-29 - `GitHub `__ - `PyPI `__ - -.. raw:: html - - -
    -
  • Support for Spotlight Battery cameras with multiple battery bays #106 (@evanjd)
  • -
-

Many thanks for your first contribution @evanjd!!

- - - - -Version 0.2.1 -============= - -Released on 2018-06-15 - `GitHub `__ - `PyPI `__ - -.. raw:: html - - -
    -
  • Updated Ring authentication method to oauth base
  • -
-

Many thanks to @davglass for reporting this issue and for @rbrtio, @vickyg3, @cathcartd, and @dshokouhi for testing the fix.

- - - - -Version 0.2.0 -============= - -Released on 2018-05-16 - `GitHub `__ - `PyPI `__ - -.. raw:: html - - -

Changelog:

-

@andrewkress - only save token to disk if reuse session is true #81
- @tchellomello - Getting TypeError #86

-

Many thanks to @andrewkress for your contribution!

- - - - -Version 0.1.9 -============= - -Released on 2017-11-29 - `GitHub `__ - `PyPI `__ - -.. raw:: html - - -
    -
  • Added generic method update() on the top level Ring method that refreshes the attributes for all linked devices. #75
  • -
- - - - -Version 0.1.8 -============= - -Released on 2017-11-22 - `GitHub `__ - `PyPI `__ - -.. raw:: html - - -
    -
  • Added the ability to check if account has the Ring subscription active since it is now enforced by Ring in order to use the methods recording_download() and recording_url() #71
  • -
-

Many thanks to @arsaboo for your help on testing this.

- - - - -Version 0.1.7 -============= - -Released on 2017-11-14 - `GitHub `__ - `PyPI `__ - -.. raw:: html - - -
    -
  • Fixes some bugs and enhancements
  • -
  • Add ability to inform attribute older_than to the history() method. Thanks to @troopermax #63
  • -
  • Introduced ring_cli.py script
  • -
-
scripts/ringcli.py -u foo -p bar --count
-      ---------------------------------
-      Ring CLI
-      ---------------------------------
-      	Counting videos linked on your Ring account.
-      	This may take some time....
-      
-      	Total videos: 384
-      	Ding triggered: 32
-      	Motion triggered: 340
-      	On-Demand triggered: 12
-      
-      ======================
-      
-      ---------------------------------
-      Ring CLI
-      ---------------------------------
-      scripts/ringcli.py -u foo -p bar --count
-      	1/384 Downloading 2017-11-14_00.57.16+00.00_motion_answered_64880679462719.mp4
-      	2/384 Downloading 2017-11-13_21.32.23+00.00_motion_not_answered_64880151491.mp4
-      ======================
-      
-      scripts/ringcli.py --help
-      usage: ringcli.py [-h] [-u USERNAME] [-p PASSWORD] [--count] [--download-all]
-      
-      Ring Doorbell
-      
-      optional arguments:
-        -h, --help            show this help message and exit
-        -u USERNAME, --username USERNAME
-                              username for Ring account
-        -p PASSWORD, --password PASSWORD
-                              username for Ring account
-        --count               count the number of videos on your Ring account
-        --download-all        download all videos on your Ring account
-      
-      https://github.com/tchellomello/python-ring-doorbell
- - - - -Version 0.1.6 -============= - -Released on 2017-10-19 - `GitHub `__ - `PyPI `__ - -.. raw:: html - - -
    -
  • Introduces support to floodlight lights and siren support. Many thanks to @jsetton for this addition
  • -
- - - - -Version 0.1.5 -============= - -Released on 2017-10-17 - `GitHub `__ - `PyPI `__ - -.. raw:: html - - -
    -
  • Added support to stickup and floodlight cameras. @jlippold
  • -
  • Allow history() to return exact number of events of a given kind
  • -
  • Code refactored
  • -
  • Added support to report wifi status. Thanks to @keeth
  • -
  • Added support to play test sounds @vickyg3
  • -
-

Many thanks to the community and special thanks to @keeth @vickyg3 @jlippold

- - - - -v0.1.4 -====== - -Released on 2017-04-30 - `GitHub `__ - `PyPI `__ - -.. raw:: html - - - - - - - -v0.1.3 -====== - -Released on 2017-03-31 - `GitHub `__ - `PyPI `__ - -.. raw:: html - - - - - - - -v0.1.2 -====== - -Released on 2017-03-20 - `GitHub `__ - `PyPI `__ - -.. raw:: html - - -

v0.1.2

- - - - -0.1.1 -===== - -Released on 2017-03-09 - `GitHub `__ - `PyPI `__ - -.. raw:: html - - - - - - - -0.1.0 -===== - -Released on 2017-02-25 - `GitHub `__ - `PyPI `__ - -.. raw:: html - - -

BREAK CHANGES
- The code was refactored to allow to manipulate the objects in a better way.

-
In [1]: from ring_doorbell import Ring
-      In [2]: myring = Ring('user@email.com', 'password')
-      
-      In [3]: myring.devices
-      Out[3]: 
-      {'chimes': [<RingChime: Downstairs>],
-       'doorbells': [<RingDoorBell: Front Door>]}
-      
-      In [4]: myring.chimes
-      Out[4]: [<RingChime: Downstairs>]
-      
-      In [5]: myring.doorbells
-      Out[5]: [<RingDoorBell: Front Door>]
-      
-      In [6]: mychime = myring.chimes[0]
-      
-      In [7]: mychime.
-               mychime.account_id         mychime.firmware           mychime.linked_tree        mychime.subscribed_motions 
-               mychime.address            mychime.id                 mychime.longitude          mychime.timezone           
-               mychime.debug              mychime.kind               mychime.name               mychime.update             
-               mychime.family             mychime.latitude           mychime.subscribed         mychime.volume  
-      
-      In [7]: mychime.volume
-      Out[7]: 5
-      
-      #updating volume
-      In [8]: mychime.volume = 200
-      Must be within the 0-10.
-      
-      In [9]: mychime.volume = 4
-      
-      In [10]: mychime.volume
-      Out[10]: 4
-      
-      # DoorBells 
-      In [11]: mydoorbell = myring.doorbells[0]
-      
-      In [12]: mydoorbell.
-                           mydoorbell.account_id                      mydoorbell.kind                            
-                           mydoorbell.address                         mydoorbell.last_recording_id               
-                           mydoorbell.battery_life                    mydoorbell.latitude                        
-                           mydoorbell.check_activity                  mydoorbell.live_streaming_json             
-                           mydoorbell.debug                           mydoorbell.longitude                       
-                           mydoorbell.existing_doorbell_type          mydoorbell.name                            
-                           mydoorbell.existing_doorbell_type_duration mydoorbell.recording_download              
-                           mydoorbell.existing_doorbell_type_enabled  mydoorbell.recording_url                   
-                           mydoorbell.family                          mydoorbell.timezone                        
-                           mydoorbell.firmware                        mydoorbell.update                          
-                           mydoorbell.history                         mydoorbell.volume                          
-                           mydoorbell.id                                                                
-      
-      In [12]: mydoorbell.last_recording_id
-      Out[12]: 2222222221
-      
-      In [14]: mydoorbell.existing_doorbell_type
-      Out[14]: 'Mechanical'
-      
-      In [15]: mydoorbell.existing_doorbell_type_enabled
-      Out[15]: True
-      
-      In [16]: mydoorbell.existing_doorbell_type_enabled = False
-      
-      In [17]: mydoorbell.existing_doorbell_type_enabled
-      Out[17]: False
- - - - -0.0.4 -===== - -Released on 2017-02-15 - `GitHub `__ - `PyPI `__ - -.. raw:: html - - -
    -
  • Allow to filter history per doorbell or globally.
  • -
- - - - -0.0.3 -===== - -Released on 2017-02-15 - `GitHub `__ - `PyPI `__ - -.. raw:: html - - - - - - - -0.0.2 -===== - -Released on 2017-02-15 - `GitHub `__ - `PyPI `__ - -.. raw:: html - - - - - - - -0.0.1 -===== - -Released on 2017-02-15 - `GitHub `__ - `PyPI `__ - -.. raw:: html - - - - - +An up to date dynamic changelog can found on `readthedocs `_. diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 293488f..75f27e0 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -138,3 +138,20 @@ the install script above or pipx. See `poetry documentation `_ for more info +Documentation +^^^^^^^^^^^^^ + +To build the docs install with the docs extra:: + + $ poetry install --extras docs + +Then generate a `Github access token `_ +(no permissions are needed) and export it as follows:: + + $ export CHANGELOG_GITHUB_TOKEN="«your-40-digit-github-token»" + +Then build:: + + $ make -C html + +You can add the token to your shell profile to avoid having to export it each time. (e.g., .env, ~/.bash_profile, ~/.bashrc, ~/.zshrc, etc) \ No newline at end of file diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index bc28082..98a849e 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -1,7 +1,8 @@ -.. changelog:: - :changelog-url: https://python-ring-doorbell.readthedocs.io/changelog.html - :github: https://github.com/tchellomello/python-ring-doorbell/releases/ - :pypi: https://pypi.org/project/ring-doorbell/ - +========= +Changelog +========= -.. include:: ../../CHANGELOG.rst \ No newline at end of file +.. changelog:: + :changelog-url: https://python-ring-doorbell.readthedocs.io/changelog.html + :github: https://github.com/tchellomello/python-ring-doorbell/releases/ + :pypi: https://pypi.org/project/ring-doorbell/ diff --git a/docs/source/conf.py b/docs/source/conf.py index 92a3a84..12ec684 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -33,4 +33,5 @@ html_theme = "sphinx_rtd_theme" html_static_path = ["_static"] +master_doc = "index" sphinx_github_changelog_token = os.environ.get("CHANGELOG_GITHUB_TOKEN") \ No newline at end of file diff --git a/docs/source/index.rst b/docs/source/index.rst index 7c4f04e..cd55e8d 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -6,6 +6,7 @@ Welcome to python-ring-doorbell's documentation! ================================================ + .. include:: ../../README.rst .. toctree:: @@ -17,3 +18,5 @@ Welcome to python-ring-doorbell's documentation! contributing changelog + +