From 68bbc59d1720246e7dbca0d7f90be6ae0dfecb02 Mon Sep 17 00:00:00 2001 From: Brian Goldstein Date: Mon, 2 Sep 2024 20:31:29 -0700 Subject: [PATCH 1/7] Update cli.py --- ring_doorbell/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ring_doorbell/cli.py b/ring_doorbell/cli.py index 59365c4..ea3fe33 100644 --- a/ring_doorbell/cli.py +++ b/ring_doorbell/cli.py @@ -295,7 +295,7 @@ async def list_command(ring: Ring) -> None: help="Name of the ring device", ) async def motion_detection(ctx, ring: Ring, device_name, turn_on, turn_off): - """Display ring devices.""" + """Get and change the motion detecton status of a device.""" device = ring.get_device_by_name(device_name) if not device: From dec0c575a6861694d53907559250a61c04084e84 Mon Sep 17 00:00:00 2001 From: Brian Goldstein Date: Mon, 2 Sep 2024 21:37:50 -0700 Subject: [PATCH 2/7] adding in-home chime support to cli --- ring_doorbell/cli.py | 78 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/ring_doorbell/cli.py b/ring_doorbell/cli.py index ea3fe33..527ec83 100644 --- a/ring_doorbell/cli.py +++ b/ring_doorbell/cli.py @@ -653,6 +653,84 @@ async def _get_events(device, max_count): return None +@cli.command() +@pass_ring +@click.pass_context +@click.option( + "--device-name", + "-dn", + required=True, + default=None, + help="Name of the ring device", +) +@click.option( + "--type", + "-t", + default=None, + required=False, + type=click.Choice(["Mechanical", "Digital", "Not Present"], case_sensitive=False), + help="Set the in-home chime type.", +) +@click.option( + "--enabled", + "-e", + default=None, + required=False, + type=click.Choice(["True", "False"], case_sensitive=False), + help="Enable or disable the in-home chime.", +) +@click.option( + "--duration", + "-d", + default=None, + required=False, + type=click.Choice([str(i) for i in range(11)]), + help="Set the in-home chime duration, in seconds.", +) +async def in_home_chime(ctx, ring: Ring, type, enabled, duration, device_name): + """View and manage the Doorbell in-home chime. To see the current in-home chime status of a device, only pass the device name.""" + device = ring.get_device_by_name(device_name) + + if not device: + echo( + f"No device with name {device_name} found." + + " List of found device names (kind) is:" + ) + return await ctx.invoke(list_command) + if device.family != "doorbots": + echo(f"{device_name} is not a doorbell, no in-home chime settings available") + return None + if not type and not enabled and not duration: + echo("Name: %s" % device.name) + echo("ID: %s" % device.id) + echo("Type: %s" % device.existing_doorbell_type) + echo("Enabled: %s" % device.existing_doorbell_type_enabled) + echo("Duration: %s" % device.existing_doorbell_type_duration) + return None + if type: + if type and type.lower() == "mechanical": + await device.async_set_existing_doorbell_type(0) + echo(f"{device_name}'s in-home chime type has been set to {type}") + elif type and type.lower() == "digital": + await device.async_set_existing_doorbell_type(1) + echo(f"{device_name}'s in-home chime type has been set to {type}") + elif type and type.lower() == "not present": + await device.async_set_existing_doorbell_type(2) + echo(f"{device_name}'s in-home chime type has been set to {type}") + if enabled: + if enabled and enabled.lower() == "true": + await device.async_set_existing_doorbell_type_enabled(True) + echo(f"{device_name}'s in-home chime has been enabled") + elif enabled and enabled.lower() == "false": + await device.async_set_existing_doorbell_type_enabled(False) + echo(f"{device_name}'s in-home chime has been disabled") + if duration: + await device.async_set_existing_doorbell_type_duration(int(duration)) + echo( + f"{device_name}'s in-home chime duration has been set to {duration} seconds" + ) + + async def ainput(string: str): loop = asyncio.get_event_loop() await loop.run_in_executor(None, lambda s=string: sys.stdout.write(s + " ")) # type: ignore[misc] From 00116b34c674d36450238fad8879332aea1c4398 Mon Sep 17 00:00:00 2001 From: Brian Goldstein Date: Mon, 2 Sep 2024 21:41:33 -0700 Subject: [PATCH 3/7] Update README.rst --- README.rst | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 4cb15b5..2027943 100644 --- a/README.rst +++ b/README.rst @@ -97,11 +97,18 @@ The CLI is work in progress and currently has the following commands: $ ring-doorbell dings +#. See or manage your doorbell in-home chine settings:: + + $ ring-doorbell in-home-chime --device-name "Front Door" + $ ring-doorbell in-home-chime --device-name "Front Door" --type Mechanical + $ ring-doorbell in-home-chime --device-name "Front Door" --enabled True + $ ring-doorbell in-home-chime --device-name "Front Door" --type Mechanical --enabled True + #. Query a ring api url directly:: $ ring-doorbell raw-query --url /clients_api/dings/active -#. Run ``ring-doorbell --help`` or ``ring-doorbell videos --help`` for full options +#. Run ``ring-doorbell --help`` or ``ring-doorbell --help`` for full options Using the API ------------- From 8e6dfef4d116ecc04407881f6a50b41b39ac5ac7 Mon Sep 17 00:00:00 2001 From: Brian Goldstein Date: Tue, 3 Sep 2024 06:21:27 -0700 Subject: [PATCH 4/7] add in-home-chime tests to test_cli.py --- tests/test_cli.py | 97 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) diff --git a/tests/test_cli.py b/tests/test_cli.py index 5cea069..3d6728a 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -15,6 +15,7 @@ _event_handler, cli, devices_command, + in_home_chime, list_command, listen, motion_detection, @@ -272,3 +273,99 @@ async def test_listen_event_handler(mocker, auth): "Currently active count = 1" ) echomock.assert_called_with(exp) + + +async def test_in_home_chime(ring, aioresponses_mock, devices_fixture): + runner = CliRunner() + with runner.isolated_filesystem(): + # Gets in-home chime details for a doorbell + res = await runner.invoke( + in_home_chime, + ["--device-name", "Front Door"], + obj=ring, + ) + expected = ( + "Name: Front Door\n" + "ID: 987652\n" + "Type: Mechanical\n" + "Enabled: True\n" + "Duration: None\n" + ) + assert res.exit_code == 0 + assert expected in res.output + + # Turns off the in-home chime + res = await runner.invoke( + in_home_chime, + ["--device-name", "Front Door", "--enabled", "False"], + obj=ring, + ) + expected = "Front Door's in-home chime has been disabled" + assert res.exit_code == 0 + assert expected in res.output + + # Turns on the in-home chime and sets type to Mechanical + res = await runner.invoke( + in_home_chime, + [ + "--device-name", + "Front Door", + "--enabled", + "True", + "--type", + "Mechanical", + ], + obj=ring, + ) + expected = ( + "Front Door's in-home chime type has been set to Mechanical\n" + "Front Door's in-home chime has been enabled\n" + ) + assert res.exit_code == 0 + assert expected in res.output + + # Sets type to Digital and changes the duration + res = await runner.invoke( + in_home_chime, + ["--device-name", "Front Door", "--type", "Digital", "--duration", "5"], + obj=ring, + ) + expected = ( + "Front Door's in-home chime type has been set to Digital\n" + "Front Door's in-home chime duration has been set to 5 seconds\n" + ) + assert res.exit_code == 0 + assert expected in res.output + + # Turns on the in-home chime, sets type to Digital, and changes the duration + res = await runner.invoke( + in_home_chime, + [ + "--device-name", + "Front Door", + "--enabled", + "True", + "--type", + "Digital", + "--duration", + "5", + ], + obj=ring, + ) + expected = ( + "Front Door's in-home chime type has been set to Digital\n" + "Front Door's in-home chime has been enabled\n" + "Front Door's in-home chime duration has been set to 5 seconds\n" + ) + assert res.exit_code == 0 + assert expected in res.output + + # Runs in-home-chime against a device that doesn't have an in-home chime + res = await runner.invoke( + in_home_chime, + ["--device-name", "Front"], + obj=ring, + ) + expected = "Front is not a doorbell, no in-home chime settings available" + assert res.exit_code == 0 + assert expected in res.output From 1e914510a2498bef0c3ad627f337535fa83924eb Mon Sep 17 00:00:00 2001 From: Brian Goldstein Date: Tue, 3 Sep 2024 06:22:49 -0700 Subject: [PATCH 5/7] Update README.rst --- README.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/README.rst b/README.rst index 2027943..05d2c07 100644 --- a/README.rst +++ b/README.rst @@ -103,6 +103,7 @@ The CLI is work in progress and currently has the following commands: $ ring-doorbell in-home-chime --device-name "Front Door" --type Mechanical $ ring-doorbell in-home-chime --device-name "Front Door" --enabled True $ ring-doorbell in-home-chime --device-name "Front Door" --type Mechanical --enabled True + $ ring-doorbell in-home-chime --device-name "Front Door" --type Digital --enabled True --duration 5 #. Query a ring api url directly:: From 3358d79c1109cf825fd6b0cc0e7e37c2f258026e Mon Sep 17 00:00:00 2001 From: Steven B <51370195+sdb9696@users.noreply.github.com> Date: Thu, 12 Sep 2024 15:09:38 +0100 Subject: [PATCH 6/7] Update to use arguments --- ring_doorbell/cli.py | 179 ++++++++++++++++++++++++++++--------------- tests/test_cli.py | 52 ++++--------- 2 files changed, 135 insertions(+), 96 deletions(-) diff --git a/ring_doorbell/cli.py b/ring_doorbell/cli.py index 527ec83..74ac595 100644 --- a/ring_doorbell/cli.py +++ b/ring_doorbell/cli.py @@ -27,7 +27,13 @@ RingEvent, RingGeneric, ) -from ring_doorbell.const import CLI_TOKEN_FILE, GCM_TOKEN_FILE, PACKAGE_NAME, USER_AGENT +from ring_doorbell.const import ( + CLI_TOKEN_FILE, + GCM_TOKEN_FILE, + PACKAGE_NAME, + USER_AGENT, + DOORBELL_EXISTING_TYPE, +) from ring_doorbell.listen import can_listen @@ -40,9 +46,16 @@ def _bar() -> None: echo("---------------------------------") +def error(msg: str): + """Print an error and exit.""" + echo(msg) + sys.exit(1) + + click.anyio_backend = "asyncio" # type: ignore[attr-defined] pass_ring = click.make_pass_decorator(Ring) +pass_doorbell = click.make_pass_decorator(RingDoorBell) cache_file = Path(CLI_TOKEN_FILE) gcm_cache_file = Path(GCM_TOKEN_FILE) @@ -58,6 +71,9 @@ def CatchAllExceptions(cls): def _handle_exception(debug, exc): if isinstance(exc, click.ClickException): raise + # Handle exit request from click. + if isinstance(exc, click.exceptions.Exit): + sys.exit(exc.exit_code) echo(f"Raised error: {exc}") if debug: raise @@ -219,6 +235,11 @@ async def _get_ring(username, password, do_update_data, user_agent=USER_AGENT): @click.pass_context async def cli(ctx, username, password, debug, user_agent): """Command line function.""" + # no need to perform any checks if we are just displaying the help + if "--help" in sys.argv: + # Context object is required to avoid crashing on sub-groups + ctx.obj = Ring(None) + return _header() logging.basicConfig() @@ -653,82 +674,120 @@ async def _get_events(device, max_count): return None -@cli.command() +@cli.group(invoke_without_command=True) @pass_ring @click.pass_context @click.option( "--device-name", "-dn", - required=True, - default=None, - help="Name of the ring device", -) -@click.option( - "--type", - "-t", - default=None, required=False, - type=click.Choice(["Mechanical", "Digital", "Not Present"], case_sensitive=False), - help="Set the in-home chime type.", -) -@click.option( - "--enabled", - "-e", default=None, - required=False, - type=click.Choice(["True", "False"], case_sensitive=False), - help="Enable or disable the in-home chime.", -) -@click.option( - "--duration", - "-d", - default=None, - required=False, - type=click.Choice([str(i) for i in range(11)]), - help="Set the in-home chime duration, in seconds.", + help="Name of the ring device", ) -async def in_home_chime(ctx, ring: Ring, type, enabled, duration, device_name): +async def in_home_chime(ctx, ring: Ring, device_name): """View and manage the Doorbell in-home chime. To see the current in-home chime status of a device, only pass the device name.""" - device = ring.get_device_by_name(device_name) + if "--help" in sys.argv: + return + if not device_name: + found = len(ring.devices().doorbells) + if found == 1: + device = ring.devices().doorbells[0] + elif found == 0: + error("No doorbells found") + else: + error( + f"There are {found} doorbells, you need to pass the --device-name option." + ) + elif dev := ring.get_device_by_name(device_name): + if dev.family in {"doorbots", "authorized_doorbots"} and isinstance( + dev, RingDoorBell + ): + device = dev + else: + error( + f"{device_name} is not a doorbell, no in-home chime settings available" + ) + else: + error(f"Cannot find a doorbell with name {device_name}") - if not device: - echo( - f"No device with name {device_name} found." - + " List of found device names (kind) is:" - ) - return await ctx.invoke(list_command) - if device.family != "doorbots": - echo(f"{device_name} is not a doorbell, no in-home chime settings available") - return None - if not type and not enabled and not duration: + if ctx.invoked_subcommand is None: echo("Name: %s" % device.name) echo("ID: %s" % device.id) echo("Type: %s" % device.existing_doorbell_type) echo("Enabled: %s" % device.existing_doorbell_type_enabled) echo("Duration: %s" % device.existing_doorbell_type_duration) return None - if type: - if type and type.lower() == "mechanical": - await device.async_set_existing_doorbell_type(0) - echo(f"{device_name}'s in-home chime type has been set to {type}") - elif type and type.lower() == "digital": - await device.async_set_existing_doorbell_type(1) - echo(f"{device_name}'s in-home chime type has been set to {type}") - elif type and type.lower() == "not present": - await device.async_set_existing_doorbell_type(2) - echo(f"{device_name}'s in-home chime type has been set to {type}") - if enabled: - if enabled and enabled.lower() == "true": - await device.async_set_existing_doorbell_type_enabled(True) - echo(f"{device_name}'s in-home chime has been enabled") - elif enabled and enabled.lower() == "false": - await device.async_set_existing_doorbell_type_enabled(False) - echo(f"{device_name}'s in-home chime has been disabled") - if duration: - await device.async_set_existing_doorbell_type_duration(int(duration)) - echo( - f"{device_name}'s in-home chime duration has been set to {duration} seconds" + + ctx.obj = device + + +@in_home_chime.command() +@pass_doorbell +@click.pass_context +@click.argument( + "new_type", + type=click.Choice(list(DOORBELL_EXISTING_TYPE.values()), case_sensitive=False), + default=None, + required=False, +) +async def type(ctx, device: RingDoorBell, new_type): + """Get/set the type of In-home chime.""" + if new_type is None: + echo(device.existing_doorbell_type) + return + + if device.family == "authorized_doorbots": + exit( + f"{device.name} is a shared device and you do not have permission to update this value" + ) + new_type_int = next(k for k, v in DOORBELL_EXISTING_TYPE.items() if v == new_type) + + await device.async_set_existing_doorbell_type(new_type_int) + echo(f"{device.name}'s in-home chime type has been set to {new_type}") + + +@in_home_chime.command() +@pass_doorbell +@click.pass_context +@click.argument("enable", type=click.BOOL, default=None, required=False) +async def enabled(ctx, device: RingDoorBell, enable: bool | None): + """Gets/sets the in-home chime enabled status. + + ENABLE: 1/0, true/false, t/f, yes/no, y/n, and on/off + """ + if enable is None: + echo(device.existing_doorbell_type_enabled) + return + + if device.family == "authorized_doorbots": + exit( + f"{device.name} is a shared device and you do not have permission to update this value" + ) + await device.async_set_existing_doorbell_type_enabled(enable) + echo( + f"{device.name}'s in-home chime has been {'enabled' if enable else 'disabled'}" + ) + + +@in_home_chime.command() +@pass_doorbell +@click.pass_context +@click.argument("duration", type=click.IntRange(0, 100), default=None, required=False) +async def duration(ctx, device: RingDoorBell, duration: int | None): + """Gets/sets the in-home chime duration. + + DURATION: Value between 0 and 100 + """ + if duration is None: + echo(device.existing_doorbell_type_duration) + return + + if device.family == "authorized_doorbots": + exit( + f"{device.name} is a shared device and you do not have permission to update this value" ) + await device.async_set_existing_doorbell_type_duration(int(duration)) + echo(f"{device.name}'s in-home chime duration has been set to {duration} seconds") async def ainput(string: str): diff --git a/tests/test_cli.py b/tests/test_cli.py index 3d6728a..153f4de 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -284,20 +284,21 @@ async def test_in_home_chime(ring, aioresponses_mock, devices_fixture): ["--device-name", "Front Door"], obj=ring, ) - expected = ( + expected_show = ( "Name: Front Door\n" "ID: 987652\n" "Type: Mechanical\n" "Enabled: True\n" "Duration: None\n" ) + expected = expected_show assert res.exit_code == 0 assert expected in res.output # Turns off the in-home chime res = await runner.invoke( in_home_chime, - ["--device-name", "Front Door", "--enabled", "False"], + ["--device-name", "Front Door", "enabled", "False"], obj=ring, ) expected = "Front Door's in-home chime has been disabled" @@ -310,62 +311,41 @@ async def test_in_home_chime(ring, aioresponses_mock, devices_fixture): [ "--device-name", "Front Door", - "--enabled", - "True", - "--type", - "Mechanical", + "type", + "mechanical", ], obj=ring, ) - expected = ( - "Front Door's in-home chime type has been set to Mechanical\n" - "Front Door's in-home chime has been enabled\n" - ) + expected = "Front Door's in-home chime type has been set to Mechanical\n" assert res.exit_code == 0 assert expected in res.output # Sets type to Digital and changes the duration res = await runner.invoke( in_home_chime, - ["--device-name", "Front Door", "--type", "Digital", "--duration", "5"], + ["--device-name", "Front Door", "duration", "5"], obj=ring, ) - expected = ( - "Front Door's in-home chime type has been set to Digital\n" - "Front Door's in-home chime duration has been set to 5 seconds\n" - ) + expected = "Front Door's in-home chime duration has been set to 5 seconds\n" assert res.exit_code == 0 assert expected in res.output - # Turns on the in-home chime, sets type to Digital, and changes the duration + # Runs in-home-chime against a device that doesn't have an in-home chime res = await runner.invoke( in_home_chime, - [ - "--device-name", - "Front Door", - "--enabled", - "True", - "--type", - "Digital", - "--duration", - "5", - ], + ["--device-name", "Front"], obj=ring, ) - expected = ( - "Front Door's in-home chime type has been set to Digital\n" - "Front Door's in-home chime has been enabled\n" - "Front Door's in-home chime duration has been set to 5 seconds\n" - ) - assert res.exit_code == 0 + expected = "Front is not a doorbell, no in-home chime settings available" + assert res.exit_code == 1 assert expected in res.output - # Runs in-home-chime against a device that doesn't have an in-home chime + # Runs in-home-chime with no parameters and error as more than one doorbot. res = await runner.invoke( in_home_chime, - ["--device-name", "Front"], + [], obj=ring, ) - expected = "Front is not a doorbell, no in-home chime settings available" - assert res.exit_code == 0 + expected = "There are 2 doorbells, you need to pass the --device-name option" + assert res.exit_code == 1 assert expected in res.output From 361eff59897fc28a11c719b069f57065cb3b29c4 Mon Sep 17 00:00:00 2001 From: Steven B <51370195+sdb9696@users.noreply.github.com> Date: Thu, 19 Sep 2024 08:46:48 +0100 Subject: [PATCH 7/7] Update readme --- README.rst | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/README.rst b/README.rst index 05d2c07..4c07912 100644 --- a/README.rst +++ b/README.rst @@ -100,10 +100,9 @@ The CLI is work in progress and currently has the following commands: #. See or manage your doorbell in-home chine settings:: $ ring-doorbell in-home-chime --device-name "Front Door" - $ ring-doorbell in-home-chime --device-name "Front Door" --type Mechanical - $ ring-doorbell in-home-chime --device-name "Front Door" --enabled True - $ ring-doorbell in-home-chime --device-name "Front Door" --type Mechanical --enabled True - $ ring-doorbell in-home-chime --device-name "Front Door" --type Digital --enabled True --duration 5 + $ ring-doorbell in-home-chime --device-name "Front Door" type Mechanical + $ ring-doorbell in-home-chime --device-name "Front Door" enabled True + $ ring-doorbell in-home-chime --device-name "Front Door" duration 5 #. Query a ring api url directly::