ring-sandbox 0.1.0__tar.gz

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.
Files changed (26) hide show
  1. ring_sandbox-0.1.0/.gitignore +11 -0
  2. ring_sandbox-0.1.0/LICENSE +21 -0
  3. ring_sandbox-0.1.0/PKG-INFO +156 -0
  4. ring_sandbox-0.1.0/README.md +122 -0
  5. ring_sandbox-0.1.0/examples/late_arrival.yml +36 -0
  6. ring_sandbox-0.1.0/examples/partial_blackout.yml +32 -0
  7. ring_sandbox-0.1.0/examples/visitor_not_worker.yml +31 -0
  8. ring_sandbox-0.1.0/fixtures/README.md +1 -0
  9. ring_sandbox-0.1.0/fixtures/devices.json +181 -0
  10. ring_sandbox-0.1.0/fixtures/history-on-demand.ava1.ring.device.DCT27DEDP2FDDRTDVIE62YYMBEDQN3NX5BIT44JHC2J6KXZG3J4NZE7LXTU67NPGY7NJ6I3AWQS3532PMPUZ3QCFJDXNDR6O.json +82 -0
  11. ring_sandbox-0.1.0/fixtures/history.ava1.ring.device.DCT27DEDP2FDDRTDVIE62YYMBEDQN3NX5BIT44JHC2J6KXZG3J4NZE7LXTU67NPGY7NJ6I3AWQS3532PMPUZ3QCFJDXNDR6O.json +3 -0
  12. ring_sandbox-0.1.0/pyproject.toml +63 -0
  13. ring_sandbox-0.1.0/requirements-dev.txt +29 -0
  14. ring_sandbox-0.1.0/src/ring_sandbox/__init__.py +38 -0
  15. ring_sandbox-0.1.0/src/ring_sandbox/cli.py +188 -0
  16. ring_sandbox-0.1.0/src/ring_sandbox/client.py +650 -0
  17. ring_sandbox-0.1.0/src/ring_sandbox/emulator.py +494 -0
  18. ring_sandbox-0.1.0/src/ring_sandbox/models.py +376 -0
  19. ring_sandbox-0.1.0/src/ring_sandbox/pytest_plugin.py +67 -0
  20. ring_sandbox-0.1.0/src/ring_sandbox/scenarios.py +154 -0
  21. ring_sandbox-0.1.0/src/ring_sandbox/webhooks.py +107 -0
  22. ring_sandbox-0.1.0/src/ring_sandbox/world.py +487 -0
  23. ring_sandbox-0.1.0/tests/test_chaos.py +139 -0
  24. ring_sandbox-0.1.0/tests/test_client_emulator.py +198 -0
  25. ring_sandbox-0.1.0/tests/test_http_boundaries.py +185 -0
  26. ring_sandbox-0.1.0/tests/test_scenarios_live.py +120 -0
@@ -0,0 +1,11 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.pyc
4
+ *.egg-info/
5
+ dist/
6
+ build/
7
+ .pytest_cache/
8
+ .ruff_cache/
9
+ .env
10
+ .env.*
11
+ fixtures/me.json
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Joseph Mayo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,156 @@
1
+ Metadata-Version: 2.5
2
+ Name: ring-sandbox
3
+ Version: 0.1.0
4
+ Summary: Typed Python client and offline emulator for the Ring Partner API (api.amazonvision.com)
5
+ Project-URL: Homepage, https://github.com/josepha-mayo/ring-sandbox
6
+ Project-URL: Documentation, https://developer.amazon.com/docs/ring/api-documentation.html
7
+ Author: Joseph Mayo
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Keywords: amazon,amazonvision,emulator,ring,sandbox,webhooks
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Framework :: Pytest
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Topic :: Software Development :: Testing
17
+ Requires-Python: >=3.11
18
+ Requires-Dist: httpx>=0.28
19
+ Requires-Dist: pydantic>=2.7
20
+ Provides-Extra: dev
21
+ Requires-Dist: fastapi>=0.115; extra == 'dev'
22
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
23
+ Requires-Dist: pytest>=8; extra == 'dev'
24
+ Requires-Dist: python-multipart>=0.0.9; extra == 'dev'
25
+ Requires-Dist: pyyaml>=6.0; extra == 'dev'
26
+ Requires-Dist: ruff>=0.6; extra == 'dev'
27
+ Requires-Dist: uvicorn>=0.30; extra == 'dev'
28
+ Provides-Extra: server
29
+ Requires-Dist: fastapi>=0.115; extra == 'server'
30
+ Requires-Dist: python-multipart>=0.0.9; extra == 'server'
31
+ Requires-Dist: pyyaml>=6.0; extra == 'server'
32
+ Requires-Dist: uvicorn>=0.30; extra == 'server'
33
+ Description-Content-Type: text/markdown
34
+
35
+ # ring-sandbox
36
+
37
+ Typed Python client **and** offline emulator for the [Ring Partner API](https://developer.amazon.com/docs/ring/api-documentation.html) (`api.amazonvision.com`).
38
+
39
+ Ring ships no SDK and no local simulator. Testing a partner integration today means a real device, a 30-minute Playground token, or hand-rolled mocks. `ring-sandbox` gives you:
40
+
41
+ - **`RingClient`** – a small, typed, synchronous client over `httpx` covering users, device discovery (with `?include=` side-loading), status, capabilities, configurations, location, event history (auto-pagination), image snapshots (303 redirect flow), media clips (200/206/416 semantics), and chime playback.
42
+ - **Webhook helpers** – HMAC-SHA256 `X-Signature` signing/verification over raw bytes, v1.1 payload construction, and parsing into a `WebhookEvent`.
43
+ - **Emulator** – a FastAPI app that speaks the same JSON:API shapes at `/v1/...`, plus a `/_sandbox` control plane to inject events, register webhook targets (the emulator signs and delivers them), add devices (doorbells, cameras, chimes, Early Access sensors), and reset.
44
+ - **Scenarios** – scripted event sequences (`delivery`, `home_aide_visit`, `short_visit`, `no_show`, `device_flap`, or your own YAML), replayable in real time, time-compressed, or back-dated into history.
45
+ - **pytest plugin** – `ring_client`, `ring_control`, `ring_world` fixtures that run the emulator in-process with no sockets.
46
+ - **Recorder** – `ring-sandbox record --token ...` snapshots real Playground/production responses into JSON fixtures.
47
+
48
+ ## Install
49
+
50
+ ```bash
51
+ pip install "ring-sandbox[server]" # client + emulator + CLI
52
+ pip install ring-sandbox # client only
53
+
54
+ # development: pinned, CI-tested dependency set
55
+ pip install -r requirements-dev.txt -e ".[dev]"
56
+ ```
57
+
58
+ ## Client
59
+
60
+ ```python
61
+ from ring_sandbox import RingClient
62
+
63
+ with RingClient(token) as ring: # production by default
64
+ for d in ring.devices(include=["status", "capabilities"]):
65
+ print(d.name, d.online, d.capabilities.is_camera)
66
+
67
+ cam = next(d for d in ring.devices(include=["capabilities"]) if d.capabilities.is_camera)
68
+ for ev in ring.events(cam.id, event_types=["motion.human", "ding"]):
69
+ print(ev.attributes.started_at, ev.attributes.event_type)
70
+
71
+ snap = ring.snapshot_latest(cam.id, start=some_datetime) # follows the 303 to the pre-signed URL
72
+ clip = ring.clip(cam.id, timestamp=some_datetime, duration_ms=10_000)
73
+ if clip.partial: print("only", clip.actual_length_ms, "ms available")
74
+ ```
75
+
76
+ Media endpoints 303-redirect to a pre-signed URL on another host. Redirects are followed
77
+ manually: bearer credentials and cookies are never forwarded, the response body is capped by
78
+ `max_media_bytes`, and off-origin targets must be allowlisted — pass
79
+ `media_origins=["https://media-host.example", "*.amazonaws.com"]` when talking to real Ring.
80
+ JSON endpoints never follow redirects; device ids are treated as opaque single path segments.
81
+
82
+ Point it at the emulator with `RingClient(token, base_url="http://127.0.0.1:8787")`.
83
+
84
+ ## Emulator
85
+
86
+ ```bash
87
+ ring-sandbox serve --port 8787
88
+ ring-sandbox webhook http://localhost:8000/webhooks/ring --key my-hmac-key
89
+ ring-sandbox play delivery --speed 5 # courier: vehicle -> human -> ding -> package -> vehicle
90
+ ring-sandbox play home_aide_visit --backdate # 90-minute visit written straight into history
91
+ ring-sandbox play examples/late_arrival.yml # your own scenario: name, description, steps
92
+ ring-sandbox inject --type motion_detected --sub-type human
93
+ ```
94
+
95
+ Custom scenarios are plain YAML — `steps` entries take `offset_s`, `type`, optional `sub_type`, `device` (id or name), and `duration_ms`. See `examples/` for documented files: `late_arrival.yml` (aide shows 25 min late), `partial_blackout.yml` (camera dies mid-visit — departure unobserved), `visitor_not_worker.yml` (courier activity in the aide's window).
96
+
97
+ Interactive docs at `http://127.0.0.1:8787/_sandbox/docs`. Drop real `default.jpg` / `default.mp4` (or `<device_id>.jpg`) in a folder and pass `--media-dir` to serve real media instead of placeholders.
98
+
99
+ ### Fidelity notes
100
+
101
+ The emulator reproduces the parts of the API that bite integrators:
102
+
103
+ | Behaviour | Emulated |
104
+ |---|---|
105
+ | JSON:API compound documents via `?include=` | yes |
106
+ | History newest-first, `page[key]` cursor, dotted `event_types` filters | yes |
107
+ | `is_third_party_reviewed` flips after media access | yes |
108
+ | Snapshot `303 See Other` to a pre-signed URL | yes |
109
+ | Clip `206 Partial` + `X-Media-Length`, `416 TIMESTAMP_NOT_FOUND` when idle | yes |
110
+ | Chime playback restricted to the app's two audio slots | yes |
111
+ | Sensor `faulted` semantics, `255` battery sentinel on mains devices | yes |
112
+ | Webhook v1.1 payloads with `sub_type`, `component_ids`, HMAC `X-Signature` | yes |
113
+ | OAuth / account linking / nonce flow | no (use any bearer token, or `--token` to pin one) |
114
+ | WHEP / RTSP live video | no |
115
+
116
+ ## Webhooks
117
+
118
+ ```python
119
+ from ring_sandbox import webhooks
120
+
121
+ @app.post("/webhooks/ring")
122
+ async def ring_hook(request: Request):
123
+ raw = await request.body() # raw bytes, never re-serialized JSON
124
+ ev = webhooks.parse(raw, signing_key=KEY, signature=request.headers.get("X-Signature"))
125
+ if ev.event_type == "motion_detected" and ev.sub_type == "human": ...
126
+ ```
127
+
128
+ ## Chaos fault injection
129
+
130
+ `ring-sandbox serve --chaos storm` runs the emulator with a fault-injection profile:
131
+ webhook deliveries can be duplicated, dropped, or delayed with jitter, and history/media
132
+ endpoints can return transient 500s. Presets: `delivery` (dup/drop/delay only), `flaky`
133
+ (endpoint failures only), `storm` (both). A custom profile is a key=value list:
134
+ `--chaos drop=0.2,duplicate=0.4,jitter_ms=1500`. `--chaos-seed N` makes the fault stream
135
+ deterministic for reproducible runs.
136
+
137
+ Every injected fault is recorded — `GET /_sandbox/chaos` returns the active profile plus
138
+ the actions taken so far (`webhook.dropped`, `webhook.duplicated`, `webhook.delayed` with
139
+ the applied ms). `POST /_sandbox/chaos` adjusts rates live (`{"drop": 0.5}`) without
140
+ restarting. The point is proving the *receiver*: a correct consumer dedupes re-delivered
141
+ `request_id`s, tolerates out-of-order arrival, and keeps working through flaky polls.
142
+
143
+ ## pytest
144
+
145
+ ```python
146
+ def test_visit_detection(ring_client, ring_control):
147
+ ring_control.post("/_sandbox/events", json={"type": "motion_detected", "sub_type": "human"})
148
+ cam = ring_client.devices()[0]
149
+ assert next(ring_client.events(cam.id)).attributes.event_type == "motion"
150
+ ```
151
+
152
+ ## Status
153
+
154
+ Built during the Amazon Developer Hackathon 2026. Response shapes follow the public documentation; where the docs are ambiguous the emulator follows what the Playground returns (see `fixtures/`). Sensors and chimes are Early Access upstream and may change.
155
+
156
+ MIT licensed.
@@ -0,0 +1,122 @@
1
+ # ring-sandbox
2
+
3
+ Typed Python client **and** offline emulator for the [Ring Partner API](https://developer.amazon.com/docs/ring/api-documentation.html) (`api.amazonvision.com`).
4
+
5
+ Ring ships no SDK and no local simulator. Testing a partner integration today means a real device, a 30-minute Playground token, or hand-rolled mocks. `ring-sandbox` gives you:
6
+
7
+ - **`RingClient`** – a small, typed, synchronous client over `httpx` covering users, device discovery (with `?include=` side-loading), status, capabilities, configurations, location, event history (auto-pagination), image snapshots (303 redirect flow), media clips (200/206/416 semantics), and chime playback.
8
+ - **Webhook helpers** – HMAC-SHA256 `X-Signature` signing/verification over raw bytes, v1.1 payload construction, and parsing into a `WebhookEvent`.
9
+ - **Emulator** – a FastAPI app that speaks the same JSON:API shapes at `/v1/...`, plus a `/_sandbox` control plane to inject events, register webhook targets (the emulator signs and delivers them), add devices (doorbells, cameras, chimes, Early Access sensors), and reset.
10
+ - **Scenarios** – scripted event sequences (`delivery`, `home_aide_visit`, `short_visit`, `no_show`, `device_flap`, or your own YAML), replayable in real time, time-compressed, or back-dated into history.
11
+ - **pytest plugin** – `ring_client`, `ring_control`, `ring_world` fixtures that run the emulator in-process with no sockets.
12
+ - **Recorder** – `ring-sandbox record --token ...` snapshots real Playground/production responses into JSON fixtures.
13
+
14
+ ## Install
15
+
16
+ ```bash
17
+ pip install "ring-sandbox[server]" # client + emulator + CLI
18
+ pip install ring-sandbox # client only
19
+
20
+ # development: pinned, CI-tested dependency set
21
+ pip install -r requirements-dev.txt -e ".[dev]"
22
+ ```
23
+
24
+ ## Client
25
+
26
+ ```python
27
+ from ring_sandbox import RingClient
28
+
29
+ with RingClient(token) as ring: # production by default
30
+ for d in ring.devices(include=["status", "capabilities"]):
31
+ print(d.name, d.online, d.capabilities.is_camera)
32
+
33
+ cam = next(d for d in ring.devices(include=["capabilities"]) if d.capabilities.is_camera)
34
+ for ev in ring.events(cam.id, event_types=["motion.human", "ding"]):
35
+ print(ev.attributes.started_at, ev.attributes.event_type)
36
+
37
+ snap = ring.snapshot_latest(cam.id, start=some_datetime) # follows the 303 to the pre-signed URL
38
+ clip = ring.clip(cam.id, timestamp=some_datetime, duration_ms=10_000)
39
+ if clip.partial: print("only", clip.actual_length_ms, "ms available")
40
+ ```
41
+
42
+ Media endpoints 303-redirect to a pre-signed URL on another host. Redirects are followed
43
+ manually: bearer credentials and cookies are never forwarded, the response body is capped by
44
+ `max_media_bytes`, and off-origin targets must be allowlisted — pass
45
+ `media_origins=["https://media-host.example", "*.amazonaws.com"]` when talking to real Ring.
46
+ JSON endpoints never follow redirects; device ids are treated as opaque single path segments.
47
+
48
+ Point it at the emulator with `RingClient(token, base_url="http://127.0.0.1:8787")`.
49
+
50
+ ## Emulator
51
+
52
+ ```bash
53
+ ring-sandbox serve --port 8787
54
+ ring-sandbox webhook http://localhost:8000/webhooks/ring --key my-hmac-key
55
+ ring-sandbox play delivery --speed 5 # courier: vehicle -> human -> ding -> package -> vehicle
56
+ ring-sandbox play home_aide_visit --backdate # 90-minute visit written straight into history
57
+ ring-sandbox play examples/late_arrival.yml # your own scenario: name, description, steps
58
+ ring-sandbox inject --type motion_detected --sub-type human
59
+ ```
60
+
61
+ Custom scenarios are plain YAML — `steps` entries take `offset_s`, `type`, optional `sub_type`, `device` (id or name), and `duration_ms`. See `examples/` for documented files: `late_arrival.yml` (aide shows 25 min late), `partial_blackout.yml` (camera dies mid-visit — departure unobserved), `visitor_not_worker.yml` (courier activity in the aide's window).
62
+
63
+ Interactive docs at `http://127.0.0.1:8787/_sandbox/docs`. Drop real `default.jpg` / `default.mp4` (or `<device_id>.jpg`) in a folder and pass `--media-dir` to serve real media instead of placeholders.
64
+
65
+ ### Fidelity notes
66
+
67
+ The emulator reproduces the parts of the API that bite integrators:
68
+
69
+ | Behaviour | Emulated |
70
+ |---|---|
71
+ | JSON:API compound documents via `?include=` | yes |
72
+ | History newest-first, `page[key]` cursor, dotted `event_types` filters | yes |
73
+ | `is_third_party_reviewed` flips after media access | yes |
74
+ | Snapshot `303 See Other` to a pre-signed URL | yes |
75
+ | Clip `206 Partial` + `X-Media-Length`, `416 TIMESTAMP_NOT_FOUND` when idle | yes |
76
+ | Chime playback restricted to the app's two audio slots | yes |
77
+ | Sensor `faulted` semantics, `255` battery sentinel on mains devices | yes |
78
+ | Webhook v1.1 payloads with `sub_type`, `component_ids`, HMAC `X-Signature` | yes |
79
+ | OAuth / account linking / nonce flow | no (use any bearer token, or `--token` to pin one) |
80
+ | WHEP / RTSP live video | no |
81
+
82
+ ## Webhooks
83
+
84
+ ```python
85
+ from ring_sandbox import webhooks
86
+
87
+ @app.post("/webhooks/ring")
88
+ async def ring_hook(request: Request):
89
+ raw = await request.body() # raw bytes, never re-serialized JSON
90
+ ev = webhooks.parse(raw, signing_key=KEY, signature=request.headers.get("X-Signature"))
91
+ if ev.event_type == "motion_detected" and ev.sub_type == "human": ...
92
+ ```
93
+
94
+ ## Chaos fault injection
95
+
96
+ `ring-sandbox serve --chaos storm` runs the emulator with a fault-injection profile:
97
+ webhook deliveries can be duplicated, dropped, or delayed with jitter, and history/media
98
+ endpoints can return transient 500s. Presets: `delivery` (dup/drop/delay only), `flaky`
99
+ (endpoint failures only), `storm` (both). A custom profile is a key=value list:
100
+ `--chaos drop=0.2,duplicate=0.4,jitter_ms=1500`. `--chaos-seed N` makes the fault stream
101
+ deterministic for reproducible runs.
102
+
103
+ Every injected fault is recorded — `GET /_sandbox/chaos` returns the active profile plus
104
+ the actions taken so far (`webhook.dropped`, `webhook.duplicated`, `webhook.delayed` with
105
+ the applied ms). `POST /_sandbox/chaos` adjusts rates live (`{"drop": 0.5}`) without
106
+ restarting. The point is proving the *receiver*: a correct consumer dedupes re-delivered
107
+ `request_id`s, tolerates out-of-order arrival, and keeps working through flaky polls.
108
+
109
+ ## pytest
110
+
111
+ ```python
112
+ def test_visit_detection(ring_client, ring_control):
113
+ ring_control.post("/_sandbox/events", json={"type": "motion_detected", "sub_type": "human"})
114
+ cam = ring_client.devices()[0]
115
+ assert next(ring_client.events(cam.id)).attributes.event_type == "motion"
116
+ ```
117
+
118
+ ## Status
119
+
120
+ Built during the Amazon Developer Hackathon 2026. Response shapes follow the public documentation; where the docs are ambiguous the emulator follows what the Playground returns (see `fixtures/`). Sensors and chimes are Early Access upstream and may change.
121
+
122
+ MIT licensed.
@@ -0,0 +1,36 @@
1
+ # Custom scenario for `ring-sandbox play` / `attest replay`.
2
+ #
3
+ # A home-health aide is scheduled for 09:00 but the first observation is at
4
+ # 09:25 — useful for exercising divergence reporting and worker reviews.
5
+ # Field reference (see ring_sandbox.scenarios.Step):
6
+ # offset_s seconds after the scenario's simulated start
7
+ # type webhook event type: motion_detected | button_press |
8
+ # contact_sensor_faulted | contact_sensor_cleared |
9
+ # device_offline | device_online
10
+ # sub_type motion hint: human | vehicle | package (optional)
11
+ # device device id or name; omit for the default doorbell (optional)
12
+ # duration_ms event duration in milliseconds (optional)
13
+
14
+ name: late_arrival
15
+ description: Scheduled at window start; nobody observed until 25 minutes in.
16
+ steps:
17
+ - offset_s: 1500 # 25 min in
18
+ type: motion_detected
19
+ sub_type: human
20
+ - offset_s: 1510
21
+ type: button_press
22
+ - offset_s: 1530
23
+ type: contact_sensor_faulted
24
+ device: Front Door Sensor
25
+ - offset_s: 1550
26
+ type: contact_sensor_cleared
27
+ device: Front Door Sensor
28
+ - offset_s: 4800 # ~80-min visit, departs
29
+ type: contact_sensor_faulted
30
+ device: Front Door Sensor
31
+ - offset_s: 4815
32
+ type: contact_sensor_cleared
33
+ device: Front Door Sensor
34
+ - offset_s: 4820
35
+ type: motion_detected
36
+ sub_type: human
@@ -0,0 +1,32 @@
1
+ # Custom scenario for `ring-sandbox play` / `attest replay`.
2
+ #
3
+ # The aide arrives on time, but the doorbell drops offline 20 minutes in and
4
+ # stays down past the window — the departure is never observed. Useful for
5
+ # exercising honest "departure unconfirmed" records and coverage gaps.
6
+ # Field reference (see ring_sandbox.scenarios.Step):
7
+ # offset_s seconds after the scenario's simulated start
8
+ # type webhook event type: motion_detected | button_press |
9
+ # contact_sensor_faulted | contact_sensor_cleared |
10
+ # device_offline | device_online
11
+ # sub_type motion hint: human | vehicle | package (optional)
12
+ # device device id or name; omit for the default doorbell (optional)
13
+ # duration_ms event duration in milliseconds (optional)
14
+
15
+ name: partial_blackout
16
+ description: On-time arrival, then the camera dies mid-visit — departure unobserved.
17
+ steps:
18
+ - offset_s: 0
19
+ type: motion_detected
20
+ sub_type: human
21
+ - offset_s: 6
22
+ type: button_press
23
+ - offset_s: 20
24
+ type: contact_sensor_faulted
25
+ device: Front Door Sensor
26
+ - offset_s: 35
27
+ type: contact_sensor_cleared
28
+ device: Front Door Sensor
29
+ - offset_s: 1200 # 20 min in — the doorbell drops offline
30
+ type: device_offline
31
+ - offset_s: 7200 # two hours later it recovers, after the window closed
32
+ type: device_online
@@ -0,0 +1,31 @@
1
+ # Custom scenario for `ring-sandbox play` / `attest replay`.
2
+ #
3
+ # Doorstep activity during the aide's scheduled window — but it's a courier:
4
+ # package motion, a quick doorbell press, no door cycle. The record should show
5
+ # observed activity without implying the scheduled worker arrived.
6
+ # Field reference (see ring_sandbox.scenarios.Step):
7
+ # offset_s seconds after the scenario's simulated start
8
+ # type webhook event type: motion_detected | button_press |
9
+ # contact_sensor_faulted | contact_sensor_cleared |
10
+ # device_offline | device_online
11
+ # sub_type motion hint: human | vehicle | package (optional)
12
+ # device device id or name; omit for the default doorbell (optional)
13
+ # duration_ms event duration in milliseconds (optional)
14
+
15
+ name: visitor_not_worker
16
+ description: Courier activity in the aide's window — observed, but not the scheduled worker.
17
+ steps:
18
+ - offset_s: 600 # 10 min into the window a vehicle pulls up
19
+ type: motion_detected
20
+ sub_type: vehicle
21
+ - offset_s: 620
22
+ type: motion_detected
23
+ sub_type: human
24
+ - offset_s: 630
25
+ type: button_press
26
+ - offset_s: 640
27
+ type: motion_detected
28
+ sub_type: package
29
+ - offset_s: 660
30
+ type: motion_detected
31
+ sub_type: vehicle
@@ -0,0 +1 @@
1
+ Real responses recorded from api.amazonvision.com with a Playground token via `ring-sandbox record` on 2026-09-15. Device ids are the Playground's synthetic Doorbell Pro. `me.json` is not committed.
@@ -0,0 +1,181 @@
1
+ {
2
+ "data": [
3
+ {
4
+ "type": "devices",
5
+ "id": "ava1.ring.device.DCT27DEDP2FDDRTDVIE62YYMBEDQN3NX5BIT44JHC2J6KXZG3J4NZE7LXTU67NPGY7NJ6I3AWQS3532PMPUZ3QCFJDXNDR6O",
6
+ "attributes": {
7
+ "image_url": "https://app-content.ring.com/shared/images/devices/square_device_images/DoorbellPro/rvdp_3x.png",
8
+ "name": "Playground Device"
9
+ },
10
+ "relationships": {
11
+ "status": {
12
+ "data": {
13
+ "type": "device-status",
14
+ "id": "ava1.ring.device.status.Z5SWOVK22ZKGIZKCL5YW2OUB7F2ETAFR3P4GHTGYQWR5AFQYDLRIBCUTVXQIJSFDBI52BU3BQYL6SLQRX4P7RRIAN3MKIDLQ"
15
+ },
16
+ "links": {
17
+ "related": "/v1/devices/ava1.ring.device.DCT27DEDP2FDDRTDVIE62YYMBEDQN3NX5BIT44JHC2J6KXZG3J4NZE7LXTU67NPGY7NJ6I3AWQS3532PMPUZ3QCFJDXNDR6O/status"
18
+ }
19
+ },
20
+ "location": {
21
+ "data": {
22
+ "type": "locations",
23
+ "id": "ava1.ring.location.NWMCX4YY3NA25FEBNT2AYFEECHDPUBMR4DGXTMDPOGVGD2YLTIIH5FQ5TJYHQP3Q7HOQQT3ZXBVN4XGUAKEXOOHCM37KE7I64AA57ELYP5D5XDQA7C5E3IP3CA"
24
+ },
25
+ "links": {
26
+ "related": "/v1/devices/ava1.ring.device.DCT27DEDP2FDDRTDVIE62YYMBEDQN3NX5BIT44JHC2J6KXZG3J4NZE7LXTU67NPGY7NJ6I3AWQS3532PMPUZ3QCFJDXNDR6O/location"
27
+ }
28
+ },
29
+ "capabilities": {
30
+ "data": {
31
+ "type": "device-capabilities",
32
+ "id": "ava1.ring.device.capabilities.7PI6SEHHABUDBWLTRS57HDZF3C63I4ME52HJSU6MIKXTQCOQ53GYOJ6PVJV6H7D6RADKFULI5BY3CC4II6YT3K7WW2GJIRUU"
33
+ },
34
+ "links": {
35
+ "related": "/v1/devices/ava1.ring.device.DCT27DEDP2FDDRTDVIE62YYMBEDQN3NX5BIT44JHC2J6KXZG3J4NZE7LXTU67NPGY7NJ6I3AWQS3532PMPUZ3QCFJDXNDR6O/capabilities"
36
+ }
37
+ },
38
+ "configurations": {
39
+ "data": {
40
+ "type": "device-configurations",
41
+ "id": "ava1.ring.device.configurations.WIEQVPAOOPWTGREOVHSMBTMTNVZWYEEBST6L6WALDEVA3J3M5B76TL4MH227OT47326BK3WJASCCIWFX3RTSHMFJ7H3I52Y4ZQYXRVXADUL4PTT6NYF2H7DXHU"
42
+ },
43
+ "links": {
44
+ "related": "/v1/devices/ava1.ring.device.DCT27DEDP2FDDRTDVIE62YYMBEDQN3NX5BIT44JHC2J6KXZG3J4NZE7LXTU67NPGY7NJ6I3AWQS3532PMPUZ3QCFJDXNDR6O/configurations"
45
+ }
46
+ }
47
+ }
48
+ }
49
+ ],
50
+ "included": [
51
+ {
52
+ "type": "locations",
53
+ "id": "ava1.ring.location.NWMCX4YY3NA25FEBNT2AYFEECHDPUBMR4DGXTMDPOGVGD2YLTIIH5FQ5TJYHQP3Q7HOQQT3ZXBVN4XGUAKEXOOHCM37KE7I64AA57ELYP5D5XDQA7C5E3IP3CA",
54
+ "attributes": {
55
+ "state": "CA",
56
+ "country": "US"
57
+ }
58
+ },
59
+ {
60
+ "type": "device-status",
61
+ "id": "ava1.ring.device.status.Z5SWOVK22ZKGIZKCL5YW2OUB7F2ETAFR3P4GHTGYQWR5AFQYDLRIBCUTVXQIJSFDBI52BU3BQYL6SLQRX4P7RRIAN3MKIDLQ",
62
+ "attributes": {
63
+ "audio": {
64
+ "snooze": {
65
+ "active": null,
66
+ "until": null
67
+ }
68
+ },
69
+ "online": true,
70
+ "reported_at": "2026-09-15T12:45:03Z",
71
+ "state": null
72
+ }
73
+ },
74
+ {
75
+ "type": "device-capabilities",
76
+ "id": "ava1.ring.device.capabilities.7PI6SEHHABUDBWLTRS57HDZF3C63I4ME52HJSU6MIKXTQCOQ53GYOJ6PVJV6H7D6RADKFULI5BY3CC4II6YT3K7WW2GJIRUU",
77
+ "attributes": {
78
+ "video": {
79
+ "configurations": [
80
+ "resolution_mode"
81
+ ],
82
+ "codecs": [
83
+ "AVC"
84
+ ],
85
+ "ratio": "16:9",
86
+ "max_resolution": 1080,
87
+ "supported_resolutions": [
88
+ 1080
89
+ ]
90
+ },
91
+ "audio": {
92
+ "customizable_slots": null,
93
+ "supported_actions": null
94
+ },
95
+ "image_enhancements": {
96
+ "configurations": [
97
+ "color_night_vision",
98
+ "privacy_zones",
99
+ "snapshot"
100
+ ]
101
+ },
102
+ "flood_detection": null,
103
+ "tamper_detection": null,
104
+ "glass_break_detection": null,
105
+ "motion_detection": {
106
+ "configurations": [
107
+ "enabled",
108
+ "motion_zones"
109
+ ]
110
+ },
111
+ "smoke_detection": null,
112
+ "freeze_detection": null,
113
+ "co_detection_listener": null,
114
+ "battery_status": null,
115
+ "contact_detection": null
116
+ }
117
+ },
118
+ {
119
+ "type": "device-configurations",
120
+ "id": "ava1.ring.device.configurations.WIEQVPAOOPWTGREOVHSMBTMTNVZWYEEBST6L6WALDEVA3J3M5B76TL4MH227OT47326BK3WJASCCIWFX3RTSHMFJ7H3I52Y4ZQYXRVXADUL4PTT6NYF2H7DXHU",
121
+ "attributes": {
122
+ "motion_detection": {
123
+ "enabled": "on",
124
+ "motion_zones": [
125
+ {
126
+ "id": "bac09fbb-7a79-4eca-80d8-4e05a4ea8d07",
127
+ "vertices": [
128
+ {
129
+ "x": 0,
130
+ "y": 0
131
+ },
132
+ {
133
+ "x": 0.5,
134
+ "y": 0
135
+ },
136
+ {
137
+ "x": 1,
138
+ "y": 0
139
+ },
140
+ {
141
+ "x": 1,
142
+ "y": 0.6
143
+ },
144
+ {
145
+ "x": 1,
146
+ "y": 1
147
+ },
148
+ {
149
+ "x": 0.5,
150
+ "y": 1
151
+ },
152
+ {
153
+ "x": 0,
154
+ "y": 1
155
+ },
156
+ {
157
+ "x": 0,
158
+ "y": 0.6
159
+ }
160
+ ]
161
+ }
162
+ ]
163
+ },
164
+ "audio": {
165
+ "customizable_slots": null,
166
+ "volume": 11
167
+ },
168
+ "image_enhancements": {
169
+ "color_night_vision": "off",
170
+ "hdr": "off",
171
+ "ir_led_night_vision": "off",
172
+ "auto_zoom_track": "off",
173
+ "privacy_zones": []
174
+ }
175
+ }
176
+ }
177
+ ],
178
+ "meta": {
179
+ "time": "2026-09-15T12:47:12Z"
180
+ }
181
+ }