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.
- ring_sandbox-0.1.0/.gitignore +11 -0
- ring_sandbox-0.1.0/LICENSE +21 -0
- ring_sandbox-0.1.0/PKG-INFO +156 -0
- ring_sandbox-0.1.0/README.md +122 -0
- ring_sandbox-0.1.0/examples/late_arrival.yml +36 -0
- ring_sandbox-0.1.0/examples/partial_blackout.yml +32 -0
- ring_sandbox-0.1.0/examples/visitor_not_worker.yml +31 -0
- ring_sandbox-0.1.0/fixtures/README.md +1 -0
- ring_sandbox-0.1.0/fixtures/devices.json +181 -0
- ring_sandbox-0.1.0/fixtures/history-on-demand.ava1.ring.device.DCT27DEDP2FDDRTDVIE62YYMBEDQN3NX5BIT44JHC2J6KXZG3J4NZE7LXTU67NPGY7NJ6I3AWQS3532PMPUZ3QCFJDXNDR6O.json +82 -0
- ring_sandbox-0.1.0/fixtures/history.ava1.ring.device.DCT27DEDP2FDDRTDVIE62YYMBEDQN3NX5BIT44JHC2J6KXZG3J4NZE7LXTU67NPGY7NJ6I3AWQS3532PMPUZ3QCFJDXNDR6O.json +3 -0
- ring_sandbox-0.1.0/pyproject.toml +63 -0
- ring_sandbox-0.1.0/requirements-dev.txt +29 -0
- ring_sandbox-0.1.0/src/ring_sandbox/__init__.py +38 -0
- ring_sandbox-0.1.0/src/ring_sandbox/cli.py +188 -0
- ring_sandbox-0.1.0/src/ring_sandbox/client.py +650 -0
- ring_sandbox-0.1.0/src/ring_sandbox/emulator.py +494 -0
- ring_sandbox-0.1.0/src/ring_sandbox/models.py +376 -0
- ring_sandbox-0.1.0/src/ring_sandbox/pytest_plugin.py +67 -0
- ring_sandbox-0.1.0/src/ring_sandbox/scenarios.py +154 -0
- ring_sandbox-0.1.0/src/ring_sandbox/webhooks.py +107 -0
- ring_sandbox-0.1.0/src/ring_sandbox/world.py +487 -0
- ring_sandbox-0.1.0/tests/test_chaos.py +139 -0
- ring_sandbox-0.1.0/tests/test_client_emulator.py +198 -0
- ring_sandbox-0.1.0/tests/test_http_boundaries.py +185 -0
- ring_sandbox-0.1.0/tests/test_scenarios_live.py +120 -0
|
@@ -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
|
+
}
|