uiprotect 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of uiprotect might be problematic. Click here for more details.

@@ -0,0 +1,180 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Optional
5
+
6
+ import typer
7
+
8
+ from uiprotect.api import ProtectApiClient
9
+ from uiprotect.cli import base
10
+ from uiprotect.data import Chime
11
+
12
+ app = typer.Typer(rich_markup_mode="rich")
13
+
14
+ ARG_DEVICE_ID = typer.Argument(None, help="ID of chime to select for subcommands")
15
+ ARG_REPEAT = typer.Argument(..., help="Repeat times count", min=1, max=6)
16
+ ARG_VOLUME = typer.Argument(..., help="Volume", min=1, max=100)
17
+
18
+
19
+ @dataclass
20
+ class ChimeContext(base.CliContext):
21
+ devices: dict[str, Chime]
22
+ device: Chime | None = None
23
+
24
+
25
+ ALL_COMMANDS, DEVICE_COMMANDS = base.init_common_commands(app)
26
+
27
+
28
+ @app.callback(invoke_without_command=True)
29
+ def main(ctx: typer.Context, device_id: Optional[str] = ARG_DEVICE_ID) -> None:
30
+ """
31
+ Chime device CLI.
32
+
33
+ Returns full list of Chimes without any arguments passed.
34
+ """
35
+ protect: ProtectApiClient = ctx.obj.protect
36
+ context = ChimeContext(
37
+ protect=ctx.obj.protect,
38
+ device=None,
39
+ devices=protect.bootstrap.chimes,
40
+ output_format=ctx.obj.output_format,
41
+ )
42
+ ctx.obj = context
43
+
44
+ if device_id is not None and device_id not in ALL_COMMANDS:
45
+ if (device := protect.bootstrap.chimes.get(device_id)) is None:
46
+ typer.secho("Invalid chime ID", fg="red")
47
+ raise typer.Exit(1)
48
+ ctx.obj.device = device
49
+
50
+ if not ctx.invoked_subcommand:
51
+ if device_id in ALL_COMMANDS:
52
+ ctx.invoke(ALL_COMMANDS[device_id], ctx)
53
+ return
54
+
55
+ if ctx.obj.device is not None:
56
+ base.print_unifi_obj(ctx.obj.device, ctx.obj.output_format)
57
+ return
58
+
59
+ base.print_unifi_dict(ctx.obj.devices)
60
+
61
+
62
+ @app.command()
63
+ def cameras(
64
+ ctx: typer.Context,
65
+ camera_ids: list[str] = typer.Argument(
66
+ None,
67
+ help="Set to [] to empty list of cameras",
68
+ ),
69
+ add: bool = typer.Option(False, "-a", "--add", help="Add cameras instead of set"),
70
+ remove: bool = typer.Option(
71
+ False,
72
+ "-r",
73
+ "--remove",
74
+ help="Remove cameras instead of set",
75
+ ),
76
+ ) -> None:
77
+ """Returns or sets paired doorbells for the chime."""
78
+ base.require_device_id(ctx)
79
+ obj: Chime = ctx.obj.device
80
+
81
+ if add and remove:
82
+ typer.secho("Add and remove are mutally exclusive", fg="red")
83
+ raise typer.Exit(1)
84
+
85
+ if len(camera_ids) == 0:
86
+ base.print_unifi_list(obj.cameras)
87
+ return
88
+
89
+ protect: ProtectApiClient = ctx.obj.protect
90
+
91
+ if len(camera_ids) == 1 and camera_ids[0] == "[]":
92
+ camera_ids = []
93
+
94
+ for camera_id in camera_ids:
95
+ if (camera := protect.bootstrap.cameras.get(camera_id)) is None:
96
+ typer.secho(f"Invalid camera ID: {camera_id}", fg="red")
97
+ raise typer.Exit(1)
98
+
99
+ if not camera.feature_flags.is_doorbell:
100
+ typer.secho(f"Camera is not a doorbell: {camera_id}", fg="red")
101
+ raise typer.Exit(1)
102
+
103
+ if add:
104
+ camera_ids = list(set(obj.camera_ids) | set(camera_ids))
105
+ elif remove:
106
+ camera_ids = list(set(obj.camera_ids) - set(camera_ids))
107
+
108
+ data_before_changes = obj.dict_with_excludes()
109
+ obj.camera_ids = camera_ids
110
+ base.run(ctx, obj.save_device(data_before_changes))
111
+
112
+
113
+ @app.command()
114
+ def set_volume(
115
+ ctx: typer.Context,
116
+ value: int = ARG_VOLUME,
117
+ camera_id: Optional[str] = typer.Option(
118
+ None,
119
+ "-c",
120
+ "--camera",
121
+ help="Camera ID to apply volume to",
122
+ ),
123
+ ) -> None:
124
+ """Set volume level for chime rings."""
125
+ base.require_device_id(ctx)
126
+ obj: Chime = ctx.obj.device
127
+ if camera_id is None:
128
+ base.run(ctx, obj.set_volume(value))
129
+ else:
130
+ protect: ProtectApiClient = ctx.obj.protect
131
+ camera = protect.bootstrap.cameras.get(camera_id)
132
+ if camera is None:
133
+ typer.secho(f"Invalid camera ID: {camera_id}", fg="red")
134
+ raise typer.Exit(1)
135
+ base.run(ctx, obj.set_volume_for_camera(camera, value))
136
+
137
+
138
+ @app.command()
139
+ def play(
140
+ ctx: typer.Context,
141
+ volume: Optional[int] = typer.Option(None, "-v", "--volume", min=1, max=100),
142
+ repeat_times: Optional[int] = typer.Option(None, "-r", "--repeat", min=1, max=6),
143
+ ) -> None:
144
+ """Plays chime tone."""
145
+ base.require_device_id(ctx)
146
+ obj: Chime = ctx.obj.device
147
+ base.run(ctx, obj.play(volume=volume, repeat_times=repeat_times))
148
+
149
+
150
+ @app.command()
151
+ def play_buzzer(ctx: typer.Context) -> None:
152
+ """Plays chime buzzer."""
153
+ base.require_device_id(ctx)
154
+ obj: Chime = ctx.obj.device
155
+ base.run(ctx, obj.play_buzzer())
156
+
157
+
158
+ @app.command()
159
+ def set_repeat_times(
160
+ ctx: typer.Context,
161
+ value: int = ARG_REPEAT,
162
+ camera_id: Optional[str] = typer.Option(
163
+ None,
164
+ "-c",
165
+ "--camera",
166
+ help="Camera ID to apply repeat times to",
167
+ ),
168
+ ) -> None:
169
+ """Set number of times for a chime to repeat when doorbell is rang."""
170
+ base.require_device_id(ctx)
171
+ obj: Chime = ctx.obj.device
172
+ if camera_id is None:
173
+ base.run(ctx, obj.set_repeat_times(value))
174
+ else:
175
+ protect: ProtectApiClient = ctx.obj.protect
176
+ camera = protect.bootstrap.cameras.get(camera_id)
177
+ if camera is None:
178
+ typer.secho(f"Invalid camera ID: {camera_id}", fg="red")
179
+ raise typer.Exit(1)
180
+ base.run(ctx, obj.set_repeat_times_for_camera(camera, value))
@@ -0,0 +1,125 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from datetime import timedelta
5
+ from typing import Optional
6
+
7
+ import typer
8
+
9
+ from uiprotect.api import ProtectApiClient
10
+ from uiprotect.cli import base
11
+ from uiprotect.data import Doorlock
12
+
13
+ app = typer.Typer(rich_markup_mode="rich")
14
+
15
+ ARG_DEVICE_ID = typer.Argument(None, help="ID of doorlock to select for subcommands")
16
+
17
+
18
+ @dataclass
19
+ class DoorlockContext(base.CliContext):
20
+ devices: dict[str, Doorlock]
21
+ device: Doorlock | None = None
22
+
23
+
24
+ ALL_COMMANDS, DEVICE_COMMANDS = base.init_common_commands(app)
25
+
26
+
27
+ @app.callback(invoke_without_command=True)
28
+ def main(ctx: typer.Context, device_id: Optional[str] = ARG_DEVICE_ID) -> None:
29
+ """
30
+ Doorlock device CLI.
31
+
32
+ Returns full list of Doorlocks without any arguments passed.
33
+ """
34
+ protect: ProtectApiClient = ctx.obj.protect
35
+ context = DoorlockContext(
36
+ protect=ctx.obj.protect,
37
+ device=None,
38
+ devices=protect.bootstrap.doorlocks,
39
+ output_format=ctx.obj.output_format,
40
+ )
41
+ ctx.obj = context
42
+
43
+ if device_id is not None and device_id not in ALL_COMMANDS:
44
+ if (device := protect.bootstrap.doorlocks.get(device_id)) is None:
45
+ typer.secho("Invalid doorlock ID", fg="red")
46
+ raise typer.Exit(1)
47
+ ctx.obj.device = device
48
+
49
+ if not ctx.invoked_subcommand:
50
+ if device_id in ALL_COMMANDS:
51
+ ctx.invoke(ALL_COMMANDS[device_id], ctx)
52
+ return
53
+
54
+ if ctx.obj.device is not None:
55
+ base.print_unifi_obj(ctx.obj.device, ctx.obj.output_format)
56
+ return
57
+
58
+ base.print_unifi_dict(ctx.obj.devices)
59
+
60
+
61
+ @app.command()
62
+ def camera(ctx: typer.Context, camera_id: Optional[str] = typer.Argument(None)) -> None:
63
+ """Returns or sets tha paired camera for a doorlock."""
64
+ base.require_device_id(ctx)
65
+ obj: Doorlock = ctx.obj.device
66
+
67
+ if camera_id is None:
68
+ base.print_unifi_obj(obj.camera, ctx.obj.output_format)
69
+ else:
70
+ protect: ProtectApiClient = ctx.obj.protect
71
+ if (camera_obj := protect.bootstrap.cameras.get(camera_id)) is None:
72
+ typer.secho("Invalid camera ID")
73
+ raise typer.Exit(1)
74
+ base.run(ctx, obj.set_paired_camera(camera_obj))
75
+
76
+
77
+ @app.command()
78
+ def set_status_light(ctx: typer.Context, enabled: bool) -> None:
79
+ """Sets status light for the lock."""
80
+ base.require_device_id(ctx)
81
+ obj: Doorlock = ctx.obj.device
82
+
83
+ base.run(ctx, obj.set_status_light(enabled))
84
+
85
+
86
+ @app.command()
87
+ def set_auto_close_time(
88
+ ctx: typer.Context,
89
+ duration: int = typer.Argument(..., min=0, max=3600),
90
+ ) -> None:
91
+ """Sets auto-close time for the lock (in seconds). 0 = disabled."""
92
+ base.require_device_id(ctx)
93
+ obj: Doorlock = ctx.obj.device
94
+
95
+ base.run(ctx, obj.set_auto_close_time(timedelta(seconds=duration)))
96
+
97
+
98
+ @app.command()
99
+ def unlock(ctx: typer.Context) -> None:
100
+ """Unlocks the lock."""
101
+ base.require_device_id(ctx)
102
+ obj: Doorlock = ctx.obj.device
103
+ base.run(ctx, obj.open_lock())
104
+
105
+
106
+ @app.command()
107
+ def lock(ctx: typer.Context) -> None:
108
+ """Locks the lock."""
109
+ base.require_device_id(ctx)
110
+ obj: Doorlock = ctx.obj.device
111
+ base.run(ctx, obj.close_lock())
112
+
113
+
114
+ @app.command()
115
+ def calibrate(ctx: typer.Context, force: bool = base.OPTION_FORCE) -> None:
116
+ """
117
+ Calibrate the doorlock.
118
+
119
+ Door must be open and lock unlocked.
120
+ """
121
+ base.require_device_id(ctx)
122
+ obj: Doorlock = ctx.obj.device
123
+
124
+ if force or typer.confirm("Is the door open and unlocked?"):
125
+ base.run(ctx, obj.calibrate())
@@ -0,0 +1,258 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Callable
4
+ from dataclasses import dataclass
5
+ from datetime import datetime
6
+ from pathlib import Path
7
+ from typing import Optional
8
+
9
+ import typer
10
+ from rich.progress import Progress
11
+
12
+ from uiprotect import data as d
13
+ from uiprotect.api import ProtectApiClient
14
+ from uiprotect.cli import base
15
+ from uiprotect.exceptions import NvrError
16
+ from uiprotect.utils import local_datetime
17
+
18
+ app = typer.Typer(rich_markup_mode="rich")
19
+
20
+ ARG_EVENT_ID = typer.Argument(None, help="ID of camera to select for subcommands")
21
+ OPTION_START = typer.Option(None, "-s", "--start")
22
+ OPTION_END = typer.Option(None, "-e", "--end")
23
+ OPTION_LIMIT = typer.Option(None, "-l", "--limit")
24
+ OPTION_OFFSET = typer.Option(None, "-o", "--offet")
25
+ OPTION_TYPES = typer.Option(None, "-t", "--type")
26
+ OPTION_SMART_TYPES = typer.Option(
27
+ None,
28
+ "-d",
29
+ "--smart-detect",
30
+ help="If provided, will only return smartDetectZone events",
31
+ )
32
+
33
+
34
+ @dataclass
35
+ class EventContext(base.CliContext):
36
+ events: dict[str, d.Event] | None = None
37
+ event: d.Event | None = None
38
+
39
+
40
+ ALL_COMMANDS: dict[str, Callable[..., None]] = {}
41
+
42
+
43
+ @app.callback(invoke_without_command=True)
44
+ def main(
45
+ ctx: typer.Context,
46
+ event_id: Optional[str] = ARG_EVENT_ID,
47
+ start: Optional[datetime] = OPTION_START,
48
+ end: Optional[datetime] = OPTION_END,
49
+ limit: Optional[int] = OPTION_LIMIT,
50
+ offset: Optional[int] = OPTION_OFFSET,
51
+ types: Optional[list[d.EventType]] = OPTION_TYPES,
52
+ smart_types: Optional[list[d.SmartDetectObjectType]] = OPTION_SMART_TYPES,
53
+ ) -> None:
54
+ """
55
+ Events CLI.
56
+
57
+ Returns list of events from the last 24 hours without any arguments passed.
58
+ """
59
+ protect: ProtectApiClient = ctx.obj.protect
60
+ context = EventContext(
61
+ protect=ctx.obj.protect,
62
+ event=None,
63
+ events=None,
64
+ output_format=ctx.obj.output_format,
65
+ )
66
+ ctx.obj = context
67
+
68
+ if event_id is not None and event_id not in ALL_COMMANDS:
69
+ try:
70
+ ctx.obj.event = base.run(ctx, protect.get_event(event_id))
71
+ except NvrError as err:
72
+ typer.secho("Invalid event ID", fg="red")
73
+ raise typer.Exit(1) from err
74
+
75
+ if not ctx.invoked_subcommand:
76
+ if ctx.obj.event is not None:
77
+ base.print_unifi_obj(ctx.obj.event, ctx.obj.output_format)
78
+ return
79
+
80
+ if types is not None and len(types) == 0:
81
+ types = None
82
+ if smart_types is not None and len(smart_types) == 0:
83
+ smart_types = None
84
+ events = base.run(
85
+ ctx,
86
+ protect.get_events(
87
+ start=start,
88
+ end=end,
89
+ limit=limit,
90
+ offset=offset,
91
+ types=types,
92
+ smart_detect_types=smart_types,
93
+ ),
94
+ )
95
+ ctx.obj.events = {}
96
+ for event in events:
97
+ ctx.obj.events[event.id] = event
98
+
99
+ if event_id in ALL_COMMANDS:
100
+ ctx.invoke(ALL_COMMANDS[event_id], ctx)
101
+ return
102
+
103
+ base.print_unifi_dict(ctx.obj.events)
104
+
105
+
106
+ def require_event_id(ctx: typer.Context) -> None:
107
+ """Requires event ID in context"""
108
+ if ctx.obj.event is None:
109
+ typer.secho("Requires a valid event ID to be selected")
110
+ raise typer.Exit(1)
111
+
112
+
113
+ def require_no_event_id(ctx: typer.Context) -> None:
114
+ """Requires no device ID in context"""
115
+ if ctx.obj.event is not None or ctx.obj.events is None:
116
+ typer.secho("Requires no event ID to be selected")
117
+ raise typer.Exit(1)
118
+
119
+
120
+ @app.command()
121
+ def list_ids(ctx: typer.Context) -> None:
122
+ """
123
+ Prints list of "id type timestamp" for each event.
124
+
125
+ Timestamps dispalyed in your locale timezone. If it is not configured
126
+ correctly, it will default to UTC. You can override your timezone with
127
+ the TZ environment variable.
128
+ """
129
+ require_no_event_id(ctx)
130
+ objs: dict[str, d.Event] = ctx.obj.events
131
+ to_print: list[tuple[str, str, datetime]] = []
132
+ longest_event = 0
133
+ for obj in objs.values():
134
+ event_type = obj.type.value
135
+ if event_type in {
136
+ d.EventType.SMART_DETECT.value,
137
+ d.EventType.SMART_DETECT_LINE.value,
138
+ }:
139
+ event_type = f"{event_type}[{','.join(obj.smart_detect_types)}]"
140
+ longest_event = max(len(event_type), longest_event)
141
+ dt = obj.timestamp or obj.start
142
+ dt = local_datetime(dt)
143
+
144
+ to_print.append((obj.id, event_type, dt))
145
+
146
+ if ctx.obj.output_format == base.OutputFormatEnum.JSON:
147
+ base.json_output(to_print)
148
+ else:
149
+ for item in to_print:
150
+ typer.echo(f"{item[0]}\t{item[1]:{longest_event}}\t{item[2]}")
151
+
152
+
153
+ ALL_COMMANDS["list-ids"] = list_ids
154
+
155
+
156
+ @app.command()
157
+ def save_thumbnail(
158
+ ctx: typer.Context,
159
+ output_path: Path = typer.Argument(..., help="JPEG format"),
160
+ ) -> None:
161
+ """
162
+ Saves thumbnail for event.
163
+
164
+ Only for ring, motion and smartDetectZone events.
165
+ """
166
+ require_event_id(ctx)
167
+ event: d.Event = ctx.obj.event
168
+
169
+ thumbnail = base.run(ctx, event.get_thumbnail())
170
+ if thumbnail is None:
171
+ typer.secho("Could not get thumbnail", fg="red")
172
+ raise typer.Exit(1)
173
+
174
+ Path(output_path).write_bytes(thumbnail)
175
+
176
+
177
+ @app.command()
178
+ def save_animated_thumbnail(
179
+ ctx: typer.Context,
180
+ output_path: Path = typer.Argument(..., help="GIF format"),
181
+ ) -> None:
182
+ """
183
+ Saves animated thumbnail for event.
184
+
185
+ Only for ring, motion and smartDetectZone events.
186
+ """
187
+ require_event_id(ctx)
188
+ event: d.Event = ctx.obj.event
189
+
190
+ thumbnail = base.run(ctx, event.get_animated_thumbnail())
191
+ if thumbnail is None:
192
+ typer.secho("Could not get thumbnail", fg="red")
193
+ raise typer.Exit(1)
194
+
195
+ Path(output_path).write_bytes(thumbnail)
196
+
197
+
198
+ @app.command()
199
+ def save_heatmap(
200
+ ctx: typer.Context,
201
+ output_path: Path = typer.Argument(..., help="PNG format"),
202
+ ) -> None:
203
+ """
204
+ Saves heatmap for event.
205
+
206
+ Only motion events have heatmaps.
207
+ """
208
+ require_event_id(ctx)
209
+ event: d.Event = ctx.obj.event
210
+
211
+ heatmap = base.run(ctx, event.get_heatmap())
212
+ if heatmap is None:
213
+ typer.secho("Could not get heatmap", fg="red")
214
+ raise typer.Exit(1)
215
+
216
+ Path(output_path).write_bytes(heatmap)
217
+
218
+
219
+ @app.command()
220
+ def save_video(
221
+ ctx: typer.Context,
222
+ output_path: Path = typer.Argument(..., help="MP4 format"),
223
+ channel: int = typer.Option(
224
+ 0,
225
+ "-c",
226
+ "--channel",
227
+ min=0,
228
+ max=3,
229
+ help="0 = High, 1 = Medium, 2 = Low, 3 = Package",
230
+ ),
231
+ ) -> None:
232
+ """
233
+ Exports video for event.
234
+
235
+ Only for ring, motion and smartDetectZone events.
236
+ """
237
+ require_event_id(ctx)
238
+ event: d.Event = ctx.obj.event
239
+
240
+ with Progress() as pb:
241
+ task_id = pb.add_task("(1/2) Exporting", total=100)
242
+
243
+ async def callback(step: int, current: int, total: int) -> None:
244
+ pb.update(
245
+ task_id,
246
+ total=total,
247
+ completed=current,
248
+ description="(2/2) Downloading",
249
+ )
250
+
251
+ base.run(
252
+ ctx,
253
+ event.get_video(
254
+ channel,
255
+ output_file=output_path,
256
+ progress_callback=callback,
257
+ ),
258
+ )
@@ -0,0 +1,119 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from datetime import timedelta
5
+ from typing import Optional
6
+
7
+ import typer
8
+
9
+ from uiprotect.api import ProtectApiClient
10
+ from uiprotect.cli import base
11
+ from uiprotect.data import Light
12
+
13
+ app = typer.Typer(rich_markup_mode="rich")
14
+
15
+ ARG_DEVICE_ID = typer.Argument(None, help="ID of light to select for subcommands")
16
+
17
+
18
+ @dataclass
19
+ class LightContext(base.CliContext):
20
+ devices: dict[str, Light]
21
+ device: Light | None = None
22
+
23
+
24
+ ALL_COMMANDS, DEVICE_COMMANDS = base.init_common_commands(app)
25
+
26
+
27
+ @app.callback(invoke_without_command=True)
28
+ def main(ctx: typer.Context, device_id: Optional[str] = ARG_DEVICE_ID) -> None:
29
+ """
30
+ Lights device CLI.
31
+
32
+ Returns full list of Viewers without any arguments passed.
33
+ """
34
+ protect: ProtectApiClient = ctx.obj.protect
35
+ context = LightContext(
36
+ protect=ctx.obj.protect,
37
+ device=None,
38
+ devices=protect.bootstrap.lights,
39
+ output_format=ctx.obj.output_format,
40
+ )
41
+ ctx.obj = context
42
+
43
+ if device_id is not None and device_id not in ALL_COMMANDS:
44
+ if (device := protect.bootstrap.lights.get(device_id)) is None:
45
+ typer.secho("Invalid light ID", fg="red")
46
+ raise typer.Exit(1)
47
+ ctx.obj.device = device
48
+
49
+ if not ctx.invoked_subcommand:
50
+ if device_id in ALL_COMMANDS:
51
+ ctx.invoke(ALL_COMMANDS[device_id], ctx)
52
+ return
53
+
54
+ if ctx.obj.device is not None:
55
+ base.print_unifi_obj(ctx.obj.device, ctx.obj.output_format)
56
+ return
57
+
58
+ base.print_unifi_dict(ctx.obj.devices)
59
+
60
+
61
+ @app.command()
62
+ def camera(ctx: typer.Context, camera_id: Optional[str] = typer.Argument(None)) -> None:
63
+ """Returns or sets tha paired camera for a light."""
64
+ base.require_device_id(ctx)
65
+ obj: Light = ctx.obj.device
66
+
67
+ if camera_id is None:
68
+ base.print_unifi_obj(obj.camera, ctx.obj.output_format)
69
+ else:
70
+ protect: ProtectApiClient = ctx.obj.protect
71
+ if (camera_obj := protect.bootstrap.cameras.get(camera_id)) is None:
72
+ typer.secho("Invalid camera ID")
73
+ raise typer.Exit(1)
74
+ base.run(ctx, obj.set_paired_camera(camera_obj))
75
+
76
+
77
+ @app.command()
78
+ def set_status_light(ctx: typer.Context, enabled: bool) -> None:
79
+ """Sets status light for light device."""
80
+ base.require_device_id(ctx)
81
+ obj: Light = ctx.obj.device
82
+
83
+ base.run(ctx, obj.set_status_light(enabled))
84
+
85
+
86
+ @app.command()
87
+ def set_led_level(
88
+ ctx: typer.Context,
89
+ led_level: int = typer.Argument(..., min=1, max=6),
90
+ ) -> None:
91
+ """Sets brightness of LED on light."""
92
+ base.require_device_id(ctx)
93
+ obj: Light = ctx.obj.device
94
+
95
+ base.run(ctx, obj.set_led_level(led_level))
96
+
97
+
98
+ @app.command()
99
+ def set_sensitivity(
100
+ ctx: typer.Context,
101
+ sensitivity: int = typer.Argument(..., min=0, max=100),
102
+ ) -> None:
103
+ """Sets motion sensitivity for the light."""
104
+ base.require_device_id(ctx)
105
+ obj: Light = ctx.obj.device
106
+
107
+ base.run(ctx, obj.set_sensitivity(sensitivity))
108
+
109
+
110
+ @app.command()
111
+ def set_duration(
112
+ ctx: typer.Context,
113
+ duration: int = typer.Argument(..., min=15, max=900),
114
+ ) -> None:
115
+ """Sets timeout duration (in seconds) for light."""
116
+ base.require_device_id(ctx)
117
+ obj: Light = ctx.obj.device
118
+
119
+ base.run(ctx, obj.set_duration(timedelta(seconds=duration)))