een-api-toolkit 0.3.16 → 0.3.22
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.
- package/.claude/agents/docs-accuracy-reviewer.md +146 -0
- package/.claude/agents/een-auth-agent.md +168 -0
- package/.claude/agents/een-devices-agent.md +294 -0
- package/.claude/agents/een-events-agent.md +375 -0
- package/.claude/agents/een-media-agent.md +256 -0
- package/.claude/agents/een-setup-agent.md +126 -0
- package/.claude/agents/een-users-agent.md +239 -0
- package/.claude/agents/test-runner.md +144 -0
- package/CHANGELOG.md +151 -10
- package/README.md +1 -0
- package/dist/index.cjs +3 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +561 -0
- package/dist/index.js +483 -260
- package/dist/index.js.map +1 -1
- package/docs/AI-CONTEXT.md +128 -1648
- package/docs/ai-reference/AI-AUTH.md +288 -0
- package/docs/ai-reference/AI-DEVICES.md +569 -0
- package/docs/ai-reference/AI-EVENTS.md +1745 -0
- package/docs/ai-reference/AI-MEDIA.md +974 -0
- package/docs/ai-reference/AI-SETUP.md +267 -0
- package/docs/ai-reference/AI-USERS.md +255 -0
- package/examples/vue-event-subscriptions/.env.example +15 -0
- package/examples/vue-event-subscriptions/README.md +103 -0
- package/examples/vue-event-subscriptions/e2e/app.spec.ts +71 -0
- package/examples/vue-event-subscriptions/e2e/auth.spec.ts +290 -0
- package/examples/vue-event-subscriptions/index.html +13 -0
- package/examples/vue-event-subscriptions/package-lock.json +1726 -0
- package/examples/vue-event-subscriptions/package.json +29 -0
- package/examples/vue-event-subscriptions/playwright.config.ts +47 -0
- package/examples/vue-event-subscriptions/src/App.vue +193 -0
- package/examples/vue-event-subscriptions/src/composables/useHlsPlayer.ts +272 -0
- package/examples/vue-event-subscriptions/src/main.ts +25 -0
- package/examples/vue-event-subscriptions/src/router/index.ts +68 -0
- package/examples/vue-event-subscriptions/src/stores/connection.ts +101 -0
- package/examples/vue-event-subscriptions/src/stores/mediaSession.ts +79 -0
- package/examples/vue-event-subscriptions/src/views/Callback.vue +76 -0
- package/examples/vue-event-subscriptions/src/views/Home.vue +192 -0
- package/examples/vue-event-subscriptions/src/views/LiveEvents.vue +901 -0
- package/examples/vue-event-subscriptions/src/views/Login.vue +33 -0
- package/examples/vue-event-subscriptions/src/views/Logout.vue +65 -0
- package/examples/vue-event-subscriptions/src/views/Subscriptions.vue +389 -0
- package/examples/vue-event-subscriptions/src/vite-env.d.ts +12 -0
- package/examples/vue-event-subscriptions/tsconfig.json +21 -0
- package/examples/vue-event-subscriptions/tsconfig.node.json +10 -0
- package/examples/vue-event-subscriptions/vite.config.ts +12 -0
- package/examples/vue-events/package-lock.json +8 -1
- package/examples/vue-events/package.json +1 -0
- package/examples/vue-events/src/components/EventsModal.vue +269 -47
- package/examples/vue-events/src/composables/useHlsPlayer.ts +272 -0
- package/examples/vue-events/src/stores/mediaSession.ts +79 -0
- package/package.json +10 -2
- package/scripts/setup-agents.ts +116 -0
package/docs/AI-CONTEXT.md
CHANGED
|
@@ -1,303 +1,161 @@
|
|
|
1
1
|
# EEN API Toolkit - AI Reference
|
|
2
2
|
|
|
3
|
-
> **Version:** 0.3.
|
|
3
|
+
> **Version:** 0.3.22
|
|
4
4
|
>
|
|
5
|
-
> This
|
|
6
|
-
>
|
|
5
|
+
> This documentation is optimized for AI assistants. It provides focused, domain-specific
|
|
6
|
+
> references to help you understand and use the een-api-toolkit efficiently.
|
|
7
7
|
>
|
|
8
|
-
>
|
|
9
|
-
> [Eagle Eye Networks Developer Portal](https://developer.eagleeyenetworks.com).
|
|
10
|
-
|
|
11
|
-
> **Working Examples:** The installed package includes complete Vue 3 example applications
|
|
12
|
-
> at `./node_modules/een-api-toolkit/examples/`. These demonstrate OAuth authentication,
|
|
13
|
-
> user management, camera listing, live/recorded media, video feeds, events, and metrics.
|
|
14
|
-
> For Live Main Video streaming, see `vue-feeds/src/views/Feeds.vue`.
|
|
15
|
-
> For Events API with thumbnails, see `vue-events/src/components/EventsModal.vue`.
|
|
16
|
-
> For Event Metrics with Chart.js visualization, see `vue-alerts-metrics/src/components/MetricsChart.vue`.
|
|
8
|
+
> **Split Documentation:** Load only the documents you need to preserve context.
|
|
17
9
|
|
|
18
10
|
---
|
|
19
11
|
|
|
20
|
-
##
|
|
21
|
-
|
|
22
|
-
> **⚠️ CRITICAL:** This section contains essential setup requirements.
|
|
23
|
-
> Skipping these steps will cause runtime errors.
|
|
24
|
-
|
|
25
|
-
### Prerequisites
|
|
26
|
-
|
|
27
|
-
Before using the een-api-toolkit, ensure you have:
|
|
28
|
-
|
|
29
|
-
| Requirement | Details |
|
|
30
|
-
|-------------|---------|
|
|
31
|
-
| **Vue 3.x** | The toolkit is built for Vue 3 Composition API |
|
|
32
|
-
| **Pinia** | Required peer dependency for state management |
|
|
33
|
-
| **Vite** | Recommended build tool (dev server must run on `127.0.0.1:3333`) |
|
|
34
|
-
| **OAuth Proxy** | Required for secure token management (see [een-oauth-proxy](https://github.com/klaushofrichter/een-oauth-proxy)) |
|
|
35
|
-
|
|
36
|
-
### Installation
|
|
37
|
-
|
|
38
|
-
```bash
|
|
39
|
-
npm install een-api-toolkit pinia
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
### Complete Setup (main.ts)
|
|
43
|
-
|
|
44
|
-
> **⚠️ CRITICAL:** Pinia MUST be installed on the Vue app BEFORE calling
|
|
45
|
-
> `initEenToolkit()` or using `useAuthStore()`. Failure to do so will cause
|
|
46
|
-
> a runtime error.
|
|
47
|
-
|
|
48
|
-
```typescript
|
|
49
|
-
// main.ts - Complete Setup Example
|
|
50
|
-
import { createApp } from 'vue'
|
|
51
|
-
import { createPinia } from 'pinia'
|
|
52
|
-
import { initEenToolkit } from 'een-api-toolkit'
|
|
53
|
-
import App from './App.vue'
|
|
54
|
-
import router from './router'
|
|
55
|
-
|
|
56
|
-
const app = createApp(App)
|
|
57
|
-
const pinia = createPinia()
|
|
58
|
-
|
|
59
|
-
// Step 1: Install Pinia FIRST (required)
|
|
60
|
-
app.use(pinia)
|
|
61
|
-
|
|
62
|
-
// Step 2: Install router if using Vue Router
|
|
63
|
-
app.use(router)
|
|
64
|
-
|
|
65
|
-
// Step 3: Initialize the toolkit (Pinia must already be installed)
|
|
66
|
-
initEenToolkit({
|
|
67
|
-
proxyUrl: import.meta.env.VITE_PROXY_URL, // e.g., 'http://localhost:8787'
|
|
68
|
-
clientId: import.meta.env.VITE_EEN_CLIENT_ID, // Your EEN OAuth client ID
|
|
69
|
-
redirectUri: 'http://127.0.0.1:3333', // Must be exactly this
|
|
70
|
-
debug: import.meta.env.VITE_DEBUG === 'true'
|
|
71
|
-
})
|
|
72
|
-
|
|
73
|
-
// Step 4: Mount the app
|
|
74
|
-
app.mount('#app')
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
### Environment Variables (.env)
|
|
78
|
-
|
|
79
|
-
```env
|
|
80
|
-
VITE_PROXY_URL=http://localhost:8787
|
|
81
|
-
VITE_EEN_CLIENT_ID=your-een-client-id
|
|
82
|
-
VITE_DEBUG=true
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
### Common Errors
|
|
86
|
-
|
|
87
|
-
#### "getActivePinia() was called but there was no active Pinia"
|
|
88
|
-
|
|
89
|
-
**Cause:** Pinia was not installed before `initEenToolkit()` was called or before using `useAuthStore()`.
|
|
90
|
-
|
|
91
|
-
**Solution:** Ensure your `main.ts` calls `app.use(pinia)` BEFORE `initEenToolkit()`:
|
|
92
|
-
|
|
93
|
-
```typescript
|
|
94
|
-
const app = createApp(App)
|
|
95
|
-
const pinia = createPinia()
|
|
96
|
-
|
|
97
|
-
app.use(pinia) // ✅ First - install Pinia
|
|
98
|
-
initEenToolkit({...}) // ✅ Second - initialize toolkit
|
|
99
|
-
app.mount('#app') // ✅ Last - mount app
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
#### "Redirect URI mismatch"
|
|
103
|
-
|
|
104
|
-
**Cause:** OAuth redirect URI doesn't exactly match `http://127.0.0.1:3333`.
|
|
105
|
-
|
|
106
|
-
**Solution:**
|
|
107
|
-
- Use `127.0.0.1` not `localhost`
|
|
108
|
-
- Use port `3333` exactly
|
|
109
|
-
- No trailing slash (not `http://127.0.0.1:3333/`)
|
|
110
|
-
- No path (not `http://127.0.0.1:3333/callback`)
|
|
111
|
-
- Register this exact URI at [EEN Developer Portal](https://developer.eagleeyenetworks.com/page/my-application)
|
|
112
|
-
|
|
113
|
-
**Vite config:**
|
|
114
|
-
```typescript
|
|
115
|
-
// vite.config.ts
|
|
116
|
-
server: { host: '127.0.0.1', port: 3333, strictPort: true }
|
|
117
|
-
```
|
|
118
|
-
|
|
119
|
-
**Router pattern:** Handle OAuth callback on root path, then redirect internally:
|
|
120
|
-
```typescript
|
|
121
|
-
// router/index.ts - root path must catch OAuth params
|
|
122
|
-
{
|
|
123
|
-
path: '/',
|
|
124
|
-
beforeEnter: (to, _from, next) => {
|
|
125
|
-
if (to.query.code && to.query.state) {
|
|
126
|
-
next({ name: 'callback', query: to.query })
|
|
127
|
-
} else {
|
|
128
|
-
next()
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
```
|
|
12
|
+
## Document Navigation
|
|
133
13
|
|
|
134
|
-
|
|
14
|
+
| Task | Document | Tokens |
|
|
15
|
+
|------|----------|--------|
|
|
16
|
+
| Setting up a new Vue 3 app | [AI-SETUP.md](./ai-reference/AI-SETUP.md) | ~2.5K |
|
|
17
|
+
| Implementing OAuth login | [AI-AUTH.md](./ai-reference/AI-AUTH.md) | ~2K |
|
|
18
|
+
| Working with users | [AI-USERS.md](./ai-reference/AI-USERS.md) | ~1.5K |
|
|
19
|
+
| Working with cameras or bridges | [AI-DEVICES.md](./ai-reference/AI-DEVICES.md) | ~3K |
|
|
20
|
+
| Live video, images, HLS playback | [AI-MEDIA.md](./ai-reference/AI-MEDIA.md) | ~4K |
|
|
21
|
+
| Events, alerts, metrics, SSE | [AI-EVENTS.md](./ai-reference/AI-EVENTS.md) | ~3.5K |
|
|
135
22
|
|
|
136
|
-
|
|
23
|
+
## Specialized Agents
|
|
137
24
|
|
|
138
|
-
|
|
25
|
+
Specialized agents are available in `.claude/agents/` for domain-specific tasks:
|
|
139
26
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
27
|
+
| Agent | Use When |
|
|
28
|
+
|-------|----------|
|
|
29
|
+
| `een-setup-agent` | Creating a new Vue 3 app, fixing Pinia errors, OAuth redirect issues |
|
|
30
|
+
| `een-auth-agent` | Implementing login/logout, auth callbacks, route guards, token refresh |
|
|
31
|
+
| `een-users-agent` | Listing users, user profiles, user management features |
|
|
32
|
+
| `een-devices-agent` | Working with cameras or bridges, filtering by status/tags |
|
|
33
|
+
| `een-media-agent` | Live video, camera previews, HLS playback, recorded images |
|
|
34
|
+
| `een-events-agent` | Events, alerts, metrics, real-time SSE subscriptions |
|
|
145
35
|
|
|
146
|
-
**
|
|
36
|
+
**How to Use Agents:**
|
|
147
37
|
|
|
148
|
-
|
|
38
|
+
1. **In the toolkit repository:** Agents are automatically discovered in `.claude/agents/`
|
|
149
39
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
40
|
+
2. **In your own project using een-api-toolkit:**
|
|
41
|
+
```bash
|
|
42
|
+
# Install agents to your project (user must run this command)
|
|
43
|
+
npx een-setup-agents
|
|
44
|
+
```
|
|
45
|
+
This copies agents to `.claude/agents/` where Claude Code discovers them automatically.
|
|
156
46
|
|
|
157
|
-
**
|
|
47
|
+
3. **Manual access:** Agents can also be read directly from `node_modules/een-api-toolkit/.claude/agents/`
|
|
158
48
|
|
|
159
|
-
|
|
49
|
+
**For AI assistants:**
|
|
160
50
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
51
|
+
> **IMPORTANT:** If specialized agents are not installed in this project's `.claude/agents/` directory,
|
|
52
|
+
> recommend that the user run `npx een-setup-agents` to install them. This enables automatic
|
|
53
|
+
> agent discovery and provides focused, context-efficient assistance for EEN API development.
|
|
164
54
|
|
|
165
|
-
|
|
166
|
-
if (data?.imageData) {
|
|
167
|
-
imgElement.src = data.imageData // "data:image/jpeg;base64,..."
|
|
168
|
-
}
|
|
55
|
+
To use an agent's knowledge without installation, read its definition directly:
|
|
169
56
|
```
|
|
170
|
-
|
|
171
|
-
**Why it works:** The toolkit handles authentication internally and returns a base64-encoded data URL that can be used directly without browser restrictions.
|
|
172
|
-
|
|
173
|
-
### Choosing the Right Preview Method
|
|
174
|
-
|
|
175
|
-
| Use Case | Method | Notes |
|
|
176
|
-
|----------|--------|-------|
|
|
177
|
-
| Grid of camera thumbnails (20+) | `getLiveImage()` | Best for large grids, handles auth internally |
|
|
178
|
-
| Periodic refresh (e.g., every 3s) | `getLiveImage()` | Call repeatedly in a timer |
|
|
179
|
-
| Camera grid (<20 cameras) | `multipartUrl` | Automatic updates via continuous MJPEG stream. Requires `initMediaSession()` first |
|
|
180
|
-
| One-time snapshot | `getLiveImage()` | Simple and self-contained |
|
|
181
|
-
| Full-quality live video | Live Video SDK | For modal/fullscreen video playback |
|
|
182
|
-
|
|
183
|
-
**Quick Reference:**
|
|
184
|
-
|
|
185
|
-
```typescript
|
|
186
|
-
// For thumbnails and grids - USE THIS
|
|
187
|
-
const { data } = await getLiveImage({ deviceId })
|
|
188
|
-
img.src = data.imageData
|
|
189
|
-
|
|
190
|
-
// For continuous MJPEG stream (grids <20 cameras, auto-updates)
|
|
191
|
-
await initMediaSession()
|
|
192
|
-
const { data: feeds } = await listFeeds({ deviceId, include: ['multipartUrl'] })
|
|
193
|
-
img.src = feeds.results.find(f => f.type === 'preview')?.multipartUrl
|
|
194
|
-
|
|
195
|
-
// For HD live video - use @een/live-video-web-sdk
|
|
196
|
-
const player = new LivePlayer()
|
|
197
|
-
player.start({ videoElement, cameraId, baseUrl, jwt })
|
|
57
|
+
Read: node_modules/een-api-toolkit/.claude/agents/een-auth-agent.md
|
|
198
58
|
```
|
|
59
|
+
Then follow the context files and instructions specified within.
|
|
199
60
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
## Quick Reference
|
|
203
|
-
|
|
204
|
-
### Example Applications
|
|
205
|
-
|
|
206
|
-
Complete Vue 3 applications demonstrating toolkit features:
|
|
61
|
+
## Example Applications
|
|
207
62
|
|
|
208
63
|
| Example | Description | Key Files |
|
|
209
64
|
|---------|-------------|-----------|
|
|
210
65
|
| [vue-users](../examples/vue-users/) | User management with pagination | `src/views/Users.vue` |
|
|
211
66
|
| [vue-cameras](../examples/vue-cameras/) | Camera listing with status filters | `src/views/Cameras.vue` |
|
|
212
67
|
| [vue-bridges](../examples/vue-bridges/) | Bridge listing with device info | `src/views/Bridges.vue` |
|
|
213
|
-
| [vue-media](../examples/vue-media/) | Live and recorded image viewing | `src/views/LiveCamera.vue
|
|
214
|
-
| [vue-feeds](../examples/vue-feeds/) | Live video streaming
|
|
215
|
-
| [vue-events](../examples/vue-events/) | Events with bounding
|
|
216
|
-
| [vue-alerts-metrics](../examples/vue-alerts-metrics/) | Event metrics
|
|
68
|
+
| [vue-media](../examples/vue-media/) | Live and recorded image viewing | `src/views/LiveCamera.vue` |
|
|
69
|
+
| [vue-feeds](../examples/vue-feeds/) | Live video streaming | `src/views/Feeds.vue` |
|
|
70
|
+
| [vue-events](../examples/vue-events/) | Events with bounding boxes | `src/components/EventsModal.vue` |
|
|
71
|
+
| [vue-alerts-metrics](../examples/vue-alerts-metrics/) | Event metrics and alerts | `src/components/MetricsChart.vue` |
|
|
72
|
+
| [vue-event-subscriptions](../examples/vue-event-subscriptions/) | Real-time SSE streaming | `src/views/LiveEvents.vue` |
|
|
217
73
|
|
|
218
|
-
|
|
74
|
+
---
|
|
219
75
|
|
|
76
|
+
## Quick Reference - All Functions
|
|
77
|
+
|
|
78
|
+
### Configuration
|
|
220
79
|
| Function | Purpose |
|
|
221
80
|
|----------|---------|
|
|
222
81
|
| `initEenToolkit(config)` | Initialize the toolkit with proxy URL and client ID |
|
|
223
82
|
|
|
224
|
-
### Authentication
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
| `
|
|
229
|
-
| `
|
|
230
|
-
| `
|
|
231
|
-
| `revokeToken()` | Revoke token and logout | `Result<void>` |
|
|
232
|
-
|
|
233
|
-
### User Functions
|
|
234
|
-
|
|
235
|
-
| Function | Purpose | Returns |
|
|
236
|
-
|----------|---------|---------|
|
|
237
|
-
| `getCurrentUser()` | Get current user profile | `Result<UserProfile>` |
|
|
238
|
-
| `getUsers(params?)` | List all users (paginated) | `Result<PaginatedResult<User>>` |
|
|
239
|
-
| `getUser(userId, params?)` | Get a specific user | `Result<User>` |
|
|
240
|
-
|
|
241
|
-
### Camera Functions
|
|
242
|
-
|
|
243
|
-
| Function | Purpose | Returns |
|
|
244
|
-
|----------|---------|---------|
|
|
245
|
-
| `getCameras(params?)` | List all cameras (paginated) | `Result<PaginatedResult<Camera>>` |
|
|
246
|
-
| `getCamera(cameraId, params?)` | Get a specific camera | `Result<Camera>` |
|
|
247
|
-
|
|
248
|
-
### Bridge Functions
|
|
249
|
-
|
|
250
|
-
| Function | Purpose | Returns |
|
|
251
|
-
|----------|---------|---------|
|
|
252
|
-
| `getBridges(params?)` | List all bridges (paginated) | `Result<PaginatedResult<Bridge>>` |
|
|
253
|
-
| `getBridge(bridgeId, params?)` | Get a specific bridge | `Result<Bridge>` |
|
|
254
|
-
|
|
255
|
-
### Media Functions
|
|
256
|
-
|
|
257
|
-
| Function | Purpose | Returns |
|
|
258
|
-
|----------|---------|---------|
|
|
259
|
-
| `listMedia(params)` | List media intervals for a device | `Result<PaginatedResult<MediaInterval>>` |
|
|
260
|
-
| `listFeeds(params)` | List available feeds for a device | `Result<ListFeedsResult>` |
|
|
261
|
-
| `getLiveImage(params)` | Get live preview image from camera | `Result<LiveImageResult>` |
|
|
262
|
-
| `getRecordedImage(params)` | Get recorded image from history | `Result<RecordedImageResult>` |
|
|
263
|
-
| `getMediaSession()` | Get media session URL for cookies | `Result<MediaSessionResponse>` |
|
|
264
|
-
| `initMediaSession()` | Initialize media session (sets cookie) | `Result<MediaSessionResult>` |
|
|
265
|
-
|
|
266
|
-
### Events Functions
|
|
83
|
+
### Authentication
|
|
84
|
+
| Function | Purpose |
|
|
85
|
+
|----------|---------|
|
|
86
|
+
| `getAuthUrl()` | Generate OAuth authorization URL |
|
|
87
|
+
| `handleAuthCallback(code, state)` | Exchange auth code for token |
|
|
88
|
+
| `refreshToken()` | Refresh the access token |
|
|
89
|
+
| `revokeToken()` | Revoke token and logout |
|
|
267
90
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
| `
|
|
272
|
-
| `
|
|
273
|
-
| `
|
|
91
|
+
### Users
|
|
92
|
+
| Function | Purpose |
|
|
93
|
+
|----------|---------|
|
|
94
|
+
| `getCurrentUser()` | Get current user profile |
|
|
95
|
+
| `getUsers(params?)` | List all users (paginated) |
|
|
96
|
+
| `getUser(userId, params?)` | Get a specific user |
|
|
274
97
|
|
|
275
|
-
###
|
|
98
|
+
### Cameras
|
|
99
|
+
| Function | Purpose |
|
|
100
|
+
|----------|---------|
|
|
101
|
+
| `getCameras(params?)` | List all cameras (paginated) |
|
|
102
|
+
| `getCamera(cameraId, params?)` | Get a specific camera |
|
|
276
103
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
104
|
+
### Bridges
|
|
105
|
+
| Function | Purpose |
|
|
106
|
+
|----------|---------|
|
|
107
|
+
| `getBridges(params?)` | List all bridges (paginated) |
|
|
108
|
+
| `getBridge(bridgeId, params?)` | Get a specific bridge |
|
|
280
109
|
|
|
281
|
-
###
|
|
110
|
+
### Media
|
|
111
|
+
| Function | Purpose |
|
|
112
|
+
|----------|---------|
|
|
113
|
+
| `listMedia(params)` | List media intervals for a device |
|
|
114
|
+
| `listFeeds(params)` | List available feeds for a device |
|
|
115
|
+
| `getLiveImage(params)` | Get live preview image from camera |
|
|
116
|
+
| `getRecordedImage(params)` | Get recorded image from history |
|
|
117
|
+
| `getMediaSession()` | Get media session URL for cookies |
|
|
118
|
+
| `initMediaSession()` | Initialize media session (sets cookie) |
|
|
119
|
+
|
|
120
|
+
### Events
|
|
121
|
+
| Function | Purpose |
|
|
122
|
+
|----------|---------|
|
|
123
|
+
| `listEvents(params)` | List events for a device |
|
|
124
|
+
| `getEvent(eventId, params?)` | Get a specific event |
|
|
125
|
+
| `listEventTypes(params?)` | List all available event types |
|
|
126
|
+
| `listEventFieldValues(params)` | Get event types for a device |
|
|
282
127
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
| `
|
|
287
|
-
| `listAlertTypes(params?)` | List all available alert types | `Result<PaginatedResult<AlertType>>` |
|
|
128
|
+
### Event Metrics
|
|
129
|
+
| Function | Purpose |
|
|
130
|
+
|----------|---------|
|
|
131
|
+
| `getEventMetrics(params)` | Get event count metrics over time |
|
|
288
132
|
|
|
289
|
-
###
|
|
133
|
+
### Alerts
|
|
134
|
+
| Function | Purpose |
|
|
135
|
+
|----------|---------|
|
|
136
|
+
| `listAlerts(params?)` | List alerts with filters |
|
|
137
|
+
| `getAlert(id, params?)` | Get a specific alert |
|
|
138
|
+
| `listAlertTypes(params?)` | List all available alert types |
|
|
290
139
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
| `
|
|
140
|
+
### Notifications
|
|
141
|
+
| Function | Purpose |
|
|
142
|
+
|----------|---------|
|
|
143
|
+
| `listNotifications(params?)` | List notifications |
|
|
144
|
+
| `getNotification(id)` | Get a specific notification |
|
|
295
145
|
|
|
296
|
-
###
|
|
146
|
+
### Event Subscriptions
|
|
147
|
+
| Function | Purpose |
|
|
148
|
+
|----------|---------|
|
|
149
|
+
| `listEventSubscriptions(params?)` | List all subscriptions |
|
|
150
|
+
| `getEventSubscription(id)` | Get a specific subscription |
|
|
151
|
+
| `createEventSubscription(params)` | Create a new subscription |
|
|
152
|
+
| `deleteEventSubscription(id)` | Delete a subscription |
|
|
153
|
+
| `connectToEventSubscription(sseUrl, options)` | Connect to SSE stream |
|
|
297
154
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
155
|
+
### Utilities
|
|
156
|
+
| Function | Purpose |
|
|
157
|
+
|----------|---------|
|
|
158
|
+
| `formatTimestamp(timestamp)` | Convert Z to +00:00 format |
|
|
301
159
|
|
|
302
160
|
---
|
|
303
161
|
|
|
@@ -349,1320 +207,15 @@ interface PaginatedResult<T> {
|
|
|
349
207
|
}
|
|
350
208
|
```
|
|
351
209
|
|
|
352
|
-
### Configuration Type
|
|
353
|
-
|
|
354
|
-
```typescript
|
|
355
|
-
type StorageStrategy = 'localStorage' | 'sessionStorage' | 'memory'
|
|
356
|
-
|
|
357
|
-
interface EenToolkitConfig {
|
|
358
|
-
proxyUrl?: string // OAuth proxy URL (required for API calls)
|
|
359
|
-
clientId?: string // EEN OAuth client ID
|
|
360
|
-
redirectUri?: string // OAuth redirect URI (default: http://127.0.0.1:3333)
|
|
361
|
-
storageStrategy?: StorageStrategy // Token storage: 'localStorage' (default), 'sessionStorage', or 'memory'
|
|
362
|
-
debug?: boolean // Enable debug logging
|
|
363
|
-
}
|
|
364
|
-
```
|
|
365
|
-
|
|
366
|
-
### Storage Strategy Descriptions
|
|
367
|
-
|
|
368
|
-
Human-readable descriptions for each storage strategy, useful for displaying in UI components:
|
|
369
|
-
|
|
370
|
-
```typescript
|
|
371
|
-
import { getStorageStrategy, STORAGE_STRATEGY_DESCRIPTIONS } from 'een-api-toolkit'
|
|
372
|
-
|
|
373
|
-
// STORAGE_STRATEGY_DESCRIPTIONS is a Record<StorageStrategy, string>:
|
|
374
|
-
// {
|
|
375
|
-
// localStorage: 'persists across sessions',
|
|
376
|
-
// sessionStorage: 'per-tab, cleared on tab close',
|
|
377
|
-
// memory: 'tokens lost on page refresh'
|
|
378
|
-
// }
|
|
379
|
-
|
|
380
|
-
const strategy = getStorageStrategy()
|
|
381
|
-
const description = STORAGE_STRATEGY_DESCRIPTIONS[strategy]
|
|
382
|
-
console.log(`Using ${strategy}: ${description}`)
|
|
383
|
-
```
|
|
384
|
-
|
|
385
|
-
---
|
|
386
|
-
|
|
387
|
-
## Entity Types
|
|
388
|
-
|
|
389
|
-
### User
|
|
390
|
-
|
|
391
|
-
```typescript
|
|
392
|
-
interface User {
|
|
393
|
-
id: string
|
|
394
|
-
email: string
|
|
395
|
-
firstName: string
|
|
396
|
-
lastName: string
|
|
397
|
-
accountId?: string
|
|
398
|
-
timeZone?: string // IANA timezone (e.g., "America/Los_Angeles")
|
|
399
|
-
language?: string // ISO 639-1 code (e.g., "en")
|
|
400
|
-
phone?: string
|
|
401
|
-
mobilePhone?: string
|
|
402
|
-
permissions?: string[] // Requires include: ['permissions']
|
|
403
|
-
lastLogin?: string // ISO 8601 timestamp
|
|
404
|
-
isActive?: boolean
|
|
405
|
-
createdAt?: string
|
|
406
|
-
updatedAt?: string
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
interface UserProfile {
|
|
410
|
-
id: string
|
|
411
|
-
email: string
|
|
412
|
-
firstName: string
|
|
413
|
-
lastName: string
|
|
414
|
-
accountId?: string
|
|
415
|
-
timeZone?: string
|
|
416
|
-
language?: string
|
|
417
|
-
}
|
|
418
|
-
```
|
|
419
|
-
|
|
420
|
-
### Camera
|
|
421
|
-
|
|
422
|
-
```typescript
|
|
423
|
-
type CameraStatus =
|
|
424
|
-
| 'online' | 'offline' | 'deviceOffline' | 'bridgeOffline'
|
|
425
|
-
| 'invalidCredentials' | 'error' | 'streaming' | 'registered'
|
|
426
|
-
| 'attaching' | 'initializing'
|
|
427
|
-
|
|
428
|
-
interface Camera {
|
|
429
|
-
id: string
|
|
430
|
-
name: string
|
|
431
|
-
accountId: string
|
|
432
|
-
bridgeId?: string | null
|
|
433
|
-
locationId?: string | null
|
|
434
|
-
status?: CameraStatus
|
|
435
|
-
timezone?: string
|
|
436
|
-
guid?: string
|
|
437
|
-
ipAddress?: string
|
|
438
|
-
macAddress?: string
|
|
439
|
-
tags?: string[]
|
|
440
|
-
notes?: string
|
|
441
|
-
multiCameraId?: string
|
|
442
|
-
recordingModes?: CameraRecordingModes
|
|
443
|
-
deviceInfo?: CameraDeviceInfo
|
|
444
|
-
shareDetails?: CameraShareDetails
|
|
445
|
-
devicePosition?: CameraDevicePosition
|
|
446
|
-
enabledAnalytics?: string[]
|
|
447
|
-
packages?: string[]
|
|
448
|
-
createdAt?: string
|
|
449
|
-
updatedAt?: string
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
interface CameraDeviceInfo {
|
|
453
|
-
make?: string // Manufacturer (e.g., "Axis", "Hikvision")
|
|
454
|
-
model?: string // Model name
|
|
455
|
-
firmwareVersion?: string
|
|
456
|
-
directToCloud?: boolean // Direct-to-cloud camera (no bridge)
|
|
457
|
-
serialNumber?: string
|
|
458
|
-
resolution?: string
|
|
459
|
-
type?: string // Camera type (e.g., "IP", "Analog")
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
interface CameraShareDetails {
|
|
463
|
-
shared?: boolean
|
|
464
|
-
accountId?: string // Sharing account ID
|
|
465
|
-
firstResponder?: boolean
|
|
466
|
-
permissions?: string[]
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
interface CameraDevicePosition {
|
|
470
|
-
latitude?: number
|
|
471
|
-
longitude?: number
|
|
472
|
-
altitude?: number
|
|
473
|
-
floor?: number
|
|
474
|
-
azimuth?: number
|
|
475
|
-
}
|
|
476
|
-
```
|
|
477
|
-
|
|
478
|
-
### User Parameter Types
|
|
479
|
-
|
|
480
|
-
```typescript
|
|
481
|
-
interface ListUsersParams {
|
|
482
|
-
pageSize?: number // Results per page (default: 100, max: 1000)
|
|
483
|
-
pageToken?: string // Pagination token
|
|
484
|
-
include?: string[] // Additional fields (e.g., ['permissions'])
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
interface GetUserParams {
|
|
488
|
-
include?: string[] // Additional fields to include
|
|
489
|
-
}
|
|
490
|
-
```
|
|
491
|
-
|
|
492
|
-
### Camera Parameter Types
|
|
493
|
-
|
|
494
|
-
```typescript
|
|
495
|
-
interface ListCamerasParams {
|
|
496
|
-
pageSize?: number // Results per page
|
|
497
|
-
pageToken?: string // Pagination token
|
|
498
|
-
include?: string[] // Additional fields to include
|
|
499
|
-
sort?: string[] // Sort order
|
|
500
|
-
status__in?: CameraStatus[] // Filter by status
|
|
501
|
-
status__ne?: CameraStatus // Exclude by status
|
|
502
|
-
tags__contains?: string[] // Filter by tags (all must match)
|
|
503
|
-
tags__any?: string[] // Filter by tags (any match)
|
|
504
|
-
name?: string // Exact name match
|
|
505
|
-
name__contains?: string // Partial name match
|
|
506
|
-
name__in?: string[] // Name in list
|
|
507
|
-
id__in?: string[] // ID in list
|
|
508
|
-
id__notIn?: string[] // ID not in list
|
|
509
|
-
bridgeId__in?: string[] // Bridge ID filter
|
|
510
|
-
locationId__in?: string[] // Location ID filter
|
|
511
|
-
shared?: boolean // Shared camera filter
|
|
512
|
-
directToCloud?: boolean // Direct-to-cloud filter
|
|
513
|
-
q?: string // Full-text search
|
|
514
|
-
// ... and more filter parameters
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
interface GetCameraParams {
|
|
518
|
-
include?: string[] // Valid values: bridge, account, status, locationSummary,
|
|
519
|
-
// deviceAddress, timeZone, notes, tags, devicePosition,
|
|
520
|
-
// networkInfo, deviceInfo, effectivePermissions, firmware,
|
|
521
|
-
// shareDetails, visibleByBridges, capabilities, analog,
|
|
522
|
-
// packages, dewarpConfig, adminCredentials,
|
|
523
|
-
// publicSafetySharing, enabledAnalytics
|
|
524
|
-
}
|
|
525
|
-
```
|
|
526
|
-
|
|
527
|
-
### Bridge
|
|
528
|
-
|
|
529
|
-
```typescript
|
|
530
|
-
type BridgeStatus =
|
|
531
|
-
| 'online' | 'offline' | 'error' | 'idle'
|
|
532
|
-
| 'registered' | 'attaching' | 'initializing'
|
|
533
|
-
|
|
534
|
-
interface Bridge {
|
|
535
|
-
id: string
|
|
536
|
-
name: string
|
|
537
|
-
accountId: string
|
|
538
|
-
locationId?: string | null
|
|
539
|
-
guid?: string
|
|
540
|
-
timezone?: string
|
|
541
|
-
status?: BridgeStatus | { connectionStatus?: BridgeStatus }
|
|
542
|
-
tags?: string[]
|
|
543
|
-
deviceInfo?: BridgeDeviceInfo
|
|
544
|
-
networkInfo?: BridgeNetworkInfo
|
|
545
|
-
devicePosition?: BridgeDevicePosition
|
|
546
|
-
cameraCount?: number
|
|
547
|
-
createdAt?: string
|
|
548
|
-
updatedAt?: string
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
interface BridgeDeviceInfo {
|
|
552
|
-
make?: string // Manufacturer
|
|
553
|
-
model?: string // Model name
|
|
554
|
-
firmwareVersion?: string
|
|
555
|
-
serialNumber?: string
|
|
556
|
-
hardwareVersion?: string
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
interface BridgeNetworkInfo {
|
|
560
|
-
localIpAddress?: string
|
|
561
|
-
publicIpAddress?: string
|
|
562
|
-
macAddress?: string
|
|
563
|
-
subnetMask?: string
|
|
564
|
-
gateway?: string
|
|
565
|
-
dnsServers?: string[]
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
interface BridgeDevicePosition {
|
|
569
|
-
latitude?: number
|
|
570
|
-
longitude?: number
|
|
571
|
-
altitude?: number
|
|
572
|
-
floor?: number
|
|
573
|
-
azimuth?: number
|
|
574
|
-
}
|
|
575
|
-
```
|
|
576
|
-
|
|
577
|
-
### Bridge Parameter Types
|
|
578
|
-
|
|
579
|
-
```typescript
|
|
580
|
-
interface ListBridgesParams {
|
|
581
|
-
pageSize?: number // Results per page
|
|
582
|
-
pageToken?: string // Pagination token
|
|
583
|
-
include?: string[] // Additional fields to include
|
|
584
|
-
sort?: string[] // Sort order
|
|
585
|
-
status__in?: BridgeStatus[] // Filter by status
|
|
586
|
-
status__ne?: BridgeStatus // Exclude by status
|
|
587
|
-
tags__contains?: string[] // Filter by tags (all must match)
|
|
588
|
-
tags__any?: string[] // Filter by tags (any match)
|
|
589
|
-
name?: string // Exact name match
|
|
590
|
-
name__contains?: string // Partial name match
|
|
591
|
-
name__in?: string[] // Name in list
|
|
592
|
-
id__in?: string[] // ID in list
|
|
593
|
-
id__notIn?: string[] // ID not in list
|
|
594
|
-
locationId__in?: string[] // Location ID filter
|
|
595
|
-
q?: string // Full-text search
|
|
596
|
-
qRelevance__gte?: number // Minimum relevance score
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
interface GetBridgeParams {
|
|
600
|
-
include?: string[] // Valid values: status, deviceInfo, networkInfo,
|
|
601
|
-
// devicePosition, tags, effectivePermissions, etc.
|
|
602
|
-
}
|
|
603
|
-
```
|
|
604
|
-
|
|
605
|
-
### Media Types
|
|
606
|
-
|
|
607
|
-
```typescript
|
|
608
|
-
type MediaType = 'video' | 'image'
|
|
609
|
-
type MediaStreamType = 'preview' | 'main'
|
|
610
|
-
|
|
611
|
-
interface MediaInterval {
|
|
612
|
-
type: MediaStreamType
|
|
613
|
-
deviceId: string
|
|
614
|
-
mediaType: MediaType
|
|
615
|
-
startTimestamp: string // ISO 8601
|
|
616
|
-
endTimestamp: string // ISO 8601
|
|
617
|
-
flvUrl?: string | null
|
|
618
|
-
rtspUrl?: string
|
|
619
|
-
rtspsUrl?: string
|
|
620
|
-
hlsUrl?: string
|
|
621
|
-
mp4Url?: string
|
|
622
|
-
multipartUrl?: string
|
|
623
|
-
wsLiveUrl?: string
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
interface ListMediaParams {
|
|
627
|
-
deviceId: string // Required - camera ID
|
|
628
|
-
type: MediaStreamType // 'preview' or 'main'
|
|
629
|
-
mediaType: MediaType // 'video' or 'image'
|
|
630
|
-
startTimestamp: string // ISO 8601 start time
|
|
631
|
-
endTimestamp?: string // ISO 8601 end time
|
|
632
|
-
coalesce?: boolean // Merge adjacent intervals
|
|
633
|
-
include?: string[] // e.g., ['flvUrl', 'hlsUrl', 'wsLiveUrl']
|
|
634
|
-
pageToken?: string
|
|
635
|
-
pageSize?: number
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
interface GetLiveImageParams {
|
|
639
|
-
deviceId: string // Required - camera ID
|
|
640
|
-
type?: 'preview' // Only 'preview' supported for live images
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
interface LiveImageResult {
|
|
644
|
-
imageData: string // Base64 data URL (data:image/jpeg;base64,...)
|
|
645
|
-
timestamp: string | null // X-Een-Timestamp header value
|
|
646
|
-
prevToken: string | null // X-Een-PrevToken for navigation
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
interface GetRecordedImageParams {
|
|
650
|
-
deviceId?: string // Camera ID (optional if using pageToken)
|
|
651
|
-
pageToken?: string // Token for specific image
|
|
652
|
-
type?: MediaStreamType // 'preview' or 'main'
|
|
653
|
-
timestamp__lt?: string // Before timestamp
|
|
654
|
-
timestamp__lte?: string // At or before timestamp
|
|
655
|
-
timestamp?: string // Exact timestamp
|
|
656
|
-
timestamp__gte?: string // At or after timestamp
|
|
657
|
-
timestamp__gt?: string // After timestamp
|
|
658
|
-
overlayId__in?: string[] // Overlay filter
|
|
659
|
-
include?: string[] // e.g., ['overlayEmbedded', 'overlaySvgHeader']
|
|
660
|
-
targetWidth?: number // Resize width
|
|
661
|
-
targetHeight?: number // Resize height
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
interface RecordedImageResult {
|
|
665
|
-
imageData: string // Base64 data URL
|
|
666
|
-
timestamp: string | null // X-Een-Timestamp header value
|
|
667
|
-
nextToken: string | null // X-Een-NextToken for next image
|
|
668
|
-
prevToken: string | null // X-Een-PrevToken for previous image
|
|
669
|
-
overlaySvg: string | null // X-Een-OverlaySvg for overlays
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
// Media Session types - for setting up cookie-based media access
|
|
673
|
-
interface MediaSessionResponse {
|
|
674
|
-
url: string // URL to call to set the session cookie
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
interface MediaSessionResult {
|
|
678
|
-
success: boolean // Whether cookie was set successfully
|
|
679
|
-
sessionUrl: string // The URL that was called
|
|
680
|
-
}
|
|
681
|
-
```
|
|
682
|
-
|
|
683
|
-
### Feed Types
|
|
684
|
-
|
|
685
|
-
```typescript
|
|
686
|
-
type FeedStreamType = 'main' | 'preview' | 'talkdown'
|
|
687
|
-
type FeedMediaType = 'video' | 'audio' | 'image' | 'halfDuplex' | 'fullDuplex'
|
|
688
|
-
type FeedIncludeOption = 'flvUrl' | 'rtspUrl' | 'rtspsUrl' | 'localRtspUrl' | 'hlsUrl' | 'multipartUrl' | 'webRtcUrl' | 'audioPushHttpsUrl'
|
|
689
|
-
|
|
690
|
-
interface Feed {
|
|
691
|
-
id: string // Feed identifier (typically deviceId-type)
|
|
692
|
-
type: FeedStreamType // Stream type
|
|
693
|
-
deviceId: string // Device generating this feed
|
|
694
|
-
mediaType: FeedMediaType // Media type
|
|
695
|
-
flvUrl?: string | null // Flash Video URL (if requested)
|
|
696
|
-
rtspUrl?: string | null // RTSP URL (if requested)
|
|
697
|
-
rtspsUrl?: string | null // RTSP over TLS (if requested)
|
|
698
|
-
localRtspUrl?: string | null // Local RTSP to bridge (if requested)
|
|
699
|
-
hlsUrl?: string | null // HLS URL (if requested)
|
|
700
|
-
multipartUrl?: string | null // Multipart URL for raw frames (if requested)
|
|
701
|
-
webRtcUrl?: string | null // WebRTC URL (if requested)
|
|
702
|
-
audioPushHttpsUrl?: string | null // Audio push for speakers (if requested)
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
interface ListFeedsParams {
|
|
706
|
-
deviceId?: string // Filter by single device ID
|
|
707
|
-
deviceId__in?: string[] // Filter by multiple device IDs
|
|
708
|
-
type?: FeedStreamType // Filter by stream type
|
|
709
|
-
include?: FeedIncludeOption[] // URL fields to include in response
|
|
710
|
-
pageSize?: number
|
|
711
|
-
pageToken?: string
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
interface ListFeedsResult {
|
|
715
|
-
results: Feed[]
|
|
716
|
-
nextPageToken?: string
|
|
717
|
-
totalSize?: number
|
|
718
|
-
}
|
|
719
|
-
```
|
|
720
|
-
|
|
721
|
-
### Event Types
|
|
722
|
-
|
|
723
|
-
```typescript
|
|
724
|
-
type ActorType = 'bridge' | 'camera' | 'speaker' | 'account' | 'user' | 'layout' | 'job' | 'measurement' | 'sensor' | 'gateway'
|
|
725
|
-
|
|
726
|
-
interface Event {
|
|
727
|
-
id: string
|
|
728
|
-
startTimestamp: string // ISO 8601
|
|
729
|
-
endTimestamp?: string | null
|
|
730
|
-
span: boolean
|
|
731
|
-
accountId: string
|
|
732
|
-
actorId: string
|
|
733
|
-
actorAccountId: string
|
|
734
|
-
actorType: ActorType
|
|
735
|
-
creatorId: string
|
|
736
|
-
type: string // e.g., 'een.motionDetectionEvent.v1'
|
|
737
|
-
dataSchemas: string[]
|
|
738
|
-
data: EventData[]
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
interface EventData {
|
|
742
|
-
type: string
|
|
743
|
-
creatorId: string
|
|
744
|
-
[key: string]: unknown // Event data is polymorphic
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
interface EventType {
|
|
748
|
-
type: string // e.g., 'een.motionDetectionEvent.v1'
|
|
749
|
-
name: string // Human-readable name
|
|
750
|
-
description: string
|
|
751
|
-
}
|
|
752
|
-
|
|
753
|
-
interface EventFieldValues {
|
|
754
|
-
type: string[] // Available event types for the actor
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
interface ListEventsParams {
|
|
758
|
-
actor: string // Required: 'camera:{id}' format
|
|
759
|
-
type__in: string[] // Required: event types to fetch
|
|
760
|
-
startTimestamp__gte: string // Required: ISO 8601 timestamp
|
|
761
|
-
endTimestamp__lte?: string // Optional: filter by end time
|
|
762
|
-
pageSize?: number
|
|
763
|
-
pageToken?: string
|
|
764
|
-
sort?: '+startTimestamp' | '-startTimestamp'
|
|
765
|
-
include?: string[] // e.g., ['data.een.fullFrameImageUrl.v1']
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
interface ListEventFieldValuesParams {
|
|
769
|
-
actor: string // Required: 'camera:{id}' format
|
|
770
|
-
}
|
|
771
|
-
```
|
|
772
|
-
|
|
773
|
-
### Event Metrics Types
|
|
774
|
-
|
|
775
|
-
```typescript
|
|
776
|
-
type MetricActorType = 'bridge' | 'camera' | 'speaker' | 'account' | 'user' | 'layout' | 'job'
|
|
777
|
-
|
|
778
|
-
// Data point: [timestamp_ms, value]
|
|
779
|
-
type MetricDataPoint = [number, number]
|
|
780
|
-
|
|
781
|
-
interface EventMetric {
|
|
782
|
-
eventType: string
|
|
783
|
-
actorId: string
|
|
784
|
-
actorType: MetricActorType
|
|
785
|
-
target: string // e.g., 'count'
|
|
786
|
-
dataPoints: MetricDataPoint[]
|
|
787
|
-
[key: string]: unknown // Additional properties
|
|
788
|
-
}
|
|
789
|
-
|
|
790
|
-
interface GetEventMetricsParams {
|
|
791
|
-
actor: string // Required: 'camera:{id}' format
|
|
792
|
-
eventType: string // Required: e.g., 'een.motionDetectionEvent.v1'
|
|
793
|
-
timestamp__gte?: string // Optional: defaults to 7 days ago
|
|
794
|
-
timestamp__lte?: string // Optional: defaults to now
|
|
795
|
-
aggregateByMinutes?: number // Optional: default 60, minimum 60
|
|
796
|
-
}
|
|
797
|
-
```
|
|
798
|
-
|
|
799
|
-
### Alert Types
|
|
800
|
-
|
|
801
|
-
```typescript
|
|
802
|
-
interface AlertAction {
|
|
803
|
-
name: string
|
|
804
|
-
type: string
|
|
805
|
-
success: boolean
|
|
806
|
-
timestamp: string
|
|
807
|
-
status?: 'fired' | 'success' | 'partialSuccess' | 'silenced' | 'failed' | 'internalError'
|
|
808
|
-
}
|
|
809
|
-
|
|
810
|
-
interface Alert {
|
|
811
|
-
id: string
|
|
812
|
-
timestamp: string
|
|
813
|
-
createTimestamp: string
|
|
814
|
-
creatorId: string
|
|
815
|
-
alertType: string
|
|
816
|
-
alertName?: string
|
|
817
|
-
category?: string
|
|
818
|
-
serviceRuleId?: string
|
|
819
|
-
eventType?: string
|
|
820
|
-
actorId: string
|
|
821
|
-
actorType: string
|
|
822
|
-
actorAccountId: string
|
|
823
|
-
actorName?: string
|
|
824
|
-
ruleId?: string
|
|
825
|
-
eventId?: string
|
|
826
|
-
locationId?: string
|
|
827
|
-
locationName?: string
|
|
828
|
-
priority?: number // 0-20
|
|
829
|
-
dataSchemas?: string[]
|
|
830
|
-
data?: Record<string, unknown>
|
|
831
|
-
actions?: Record<string, AlertAction>
|
|
832
|
-
description?: string
|
|
833
|
-
}
|
|
834
|
-
|
|
835
|
-
interface AlertType {
|
|
836
|
-
type: string
|
|
837
|
-
description: string
|
|
838
|
-
}
|
|
839
|
-
|
|
840
|
-
type AlertInclude = 'data' | 'actions' | 'dataSchemas' | 'description'
|
|
841
|
-
type AlertSort = '+timestamp' | '-timestamp'
|
|
842
|
-
type AlertActionStatus = 'fired' | 'success' | 'partialSuccess' | 'silenced' | 'failed' | 'internalError'
|
|
843
|
-
|
|
844
|
-
interface ListAlertsParams {
|
|
845
|
-
pageSize?: number
|
|
846
|
-
pageToken?: string
|
|
847
|
-
timestamp__lte?: string
|
|
848
|
-
timestamp__gte?: string
|
|
849
|
-
creatorId?: string
|
|
850
|
-
alertType__in?: string[]
|
|
851
|
-
actorId__in?: string[]
|
|
852
|
-
actorType__in?: string[]
|
|
853
|
-
actorAccountId?: string
|
|
854
|
-
ruleId?: string
|
|
855
|
-
ruleId__in?: string[]
|
|
856
|
-
eventId?: string
|
|
857
|
-
locationId__in?: string[]
|
|
858
|
-
priority__gte?: number
|
|
859
|
-
priority__lte?: number
|
|
860
|
-
showInvalidAlerts?: boolean
|
|
861
|
-
alertActionId__in?: string[]
|
|
862
|
-
alertActionStatus__in?: AlertActionStatus[]
|
|
863
|
-
include?: AlertInclude[]
|
|
864
|
-
sort?: AlertSort[]
|
|
865
|
-
language?: string
|
|
866
|
-
}
|
|
867
|
-
|
|
868
|
-
interface GetAlertParams {
|
|
869
|
-
include?: AlertInclude[]
|
|
870
|
-
}
|
|
871
|
-
|
|
872
|
-
interface ListAlertTypesParams {
|
|
873
|
-
pageSize?: number
|
|
874
|
-
pageToken?: string
|
|
875
|
-
}
|
|
876
|
-
```
|
|
877
|
-
|
|
878
|
-
### Notification Types
|
|
879
|
-
|
|
880
|
-
```typescript
|
|
881
|
-
type NotificationCategory = 'health' | 'video' | 'operational' | 'audit' | 'job' | 'security' | 'sharing'
|
|
882
|
-
|
|
883
|
-
type NotificationStatus =
|
|
884
|
-
| 'pending' | 'bounced' | 'dropped' | 'deferred' | 'delivered' | 'sent'
|
|
885
|
-
| 'outsideUsersSchedule' | 'notificationsDisabled' | 'noNotificationActions'
|
|
886
|
-
| 'sendingFailed' | 'throttled' | 'unableToGetSettings'
|
|
887
|
-
|
|
888
|
-
interface Notification {
|
|
889
|
-
id: string
|
|
890
|
-
timestamp: string
|
|
891
|
-
createTimestamp: string
|
|
892
|
-
sentTimestamp?: string
|
|
893
|
-
alertId?: string | null
|
|
894
|
-
alertType?: string
|
|
895
|
-
actorId: string
|
|
896
|
-
actorName?: string
|
|
897
|
-
actorType: string
|
|
898
|
-
actorAccountId: string
|
|
899
|
-
userId: string
|
|
900
|
-
accountId: string
|
|
901
|
-
read: boolean
|
|
902
|
-
status: NotificationStatus
|
|
903
|
-
category: NotificationCategory
|
|
904
|
-
description?: string
|
|
905
|
-
notificationActions: string[]
|
|
906
|
-
dataSchemas: string[]
|
|
907
|
-
data: Record<string, unknown>
|
|
908
|
-
}
|
|
909
|
-
|
|
910
|
-
interface ListNotificationsParams {
|
|
911
|
-
pageSize?: number
|
|
912
|
-
pageToken?: string
|
|
913
|
-
timestamp__lte?: string
|
|
914
|
-
timestamp__gte?: string
|
|
915
|
-
alertId?: string
|
|
916
|
-
alertType?: string
|
|
917
|
-
actorId?: string
|
|
918
|
-
actorType?: string
|
|
919
|
-
actorAccountId?: string
|
|
920
|
-
category?: NotificationCategory
|
|
921
|
-
userId?: string
|
|
922
|
-
read?: boolean
|
|
923
|
-
status?: NotificationStatus
|
|
924
|
-
includeV1Notifications?: boolean
|
|
925
|
-
sort?: ('+timestamp' | '-timestamp')[]
|
|
926
|
-
language?: string
|
|
927
|
-
}
|
|
928
|
-
```
|
|
929
|
-
|
|
930
210
|
---
|
|
931
211
|
|
|
932
|
-
## API Reference
|
|
933
|
-
|
|
934
|
-
### initEenToolkit
|
|
935
212
|
|
|
936
|
-
Initialize the toolkit. Call this before using any API functions.
|
|
937
|
-
|
|
938
|
-
```typescript
|
|
939
|
-
import { initEenToolkit } from 'een-api-toolkit'
|
|
940
|
-
|
|
941
|
-
// In main.ts
|
|
942
|
-
initEenToolkit({
|
|
943
|
-
proxyUrl: import.meta.env.VITE_PROXY_URL,
|
|
944
|
-
clientId: import.meta.env.VITE_EEN_CLIENT_ID,
|
|
945
|
-
debug: true // optional
|
|
946
|
-
})
|
|
947
|
-
```
|
|
948
|
-
|
|
949
|
-
### getAuthUrl
|
|
950
|
-
|
|
951
|
-
Generate the OAuth authorization URL. Redirect the user here to start login.
|
|
952
|
-
|
|
953
|
-
```typescript
|
|
954
|
-
import { getAuthUrl } from 'een-api-toolkit'
|
|
955
|
-
|
|
956
|
-
function login() {
|
|
957
|
-
window.location.href = getAuthUrl()
|
|
958
|
-
}
|
|
959
|
-
```
|
|
960
|
-
|
|
961
|
-
### handleAuthCallback
|
|
962
|
-
|
|
963
|
-
Handle the OAuth callback after user authorizes. Call this when user returns to your redirect URI.
|
|
964
|
-
|
|
965
|
-
```typescript
|
|
966
|
-
import { handleAuthCallback } from 'een-api-toolkit'
|
|
967
|
-
|
|
968
|
-
// In your callback route handler
|
|
969
|
-
const url = new URL(window.location.href)
|
|
970
|
-
const code = url.searchParams.get('code')
|
|
971
|
-
const state = url.searchParams.get('state')
|
|
972
|
-
|
|
973
|
-
if (code && state) {
|
|
974
|
-
const { data, error } = await handleAuthCallback(code, state)
|
|
975
|
-
|
|
976
|
-
if (error) {
|
|
977
|
-
console.error('Auth failed:', error.message)
|
|
978
|
-
return
|
|
979
|
-
}
|
|
980
|
-
|
|
981
|
-
// User is now authenticated
|
|
982
|
-
router.push('/dashboard')
|
|
983
|
-
}
|
|
984
|
-
```
|
|
985
|
-
|
|
986
|
-
### getCurrentUser
|
|
987
|
-
|
|
988
|
-
Get the current authenticated user's profile.
|
|
989
|
-
|
|
990
|
-
```typescript
|
|
991
|
-
import { getCurrentUser } from 'een-api-toolkit'
|
|
992
|
-
|
|
993
|
-
const { data, error } = await getCurrentUser()
|
|
994
|
-
|
|
995
|
-
if (error) {
|
|
996
|
-
if (error.code === 'AUTH_REQUIRED') {
|
|
997
|
-
router.push('/login')
|
|
998
|
-
}
|
|
999
|
-
return
|
|
1000
|
-
}
|
|
1001
|
-
|
|
1002
|
-
console.log(`Welcome, ${data.firstName} ${data.lastName}`)
|
|
1003
|
-
```
|
|
1004
|
-
|
|
1005
|
-
### getUsers
|
|
1006
|
-
|
|
1007
|
-
List users with optional pagination.
|
|
1008
|
-
|
|
1009
|
-
```typescript
|
|
1010
|
-
import { getUsers } from 'een-api-toolkit'
|
|
1011
|
-
|
|
1012
|
-
// Basic usage
|
|
1013
|
-
const { data, error } = await getUsers()
|
|
1014
|
-
|
|
1015
|
-
// With pagination
|
|
1016
|
-
const { data } = await getUsers({ pageSize: 50 })
|
|
1017
|
-
|
|
1018
|
-
// Fetch all users
|
|
1019
|
-
let allUsers: User[] = []
|
|
1020
|
-
let pageToken: string | undefined
|
|
1021
|
-
|
|
1022
|
-
do {
|
|
1023
|
-
const { data, error } = await getUsers({ pageSize: 100, pageToken })
|
|
1024
|
-
if (error) break
|
|
1025
|
-
allUsers.push(...data.results)
|
|
1026
|
-
pageToken = data.nextPageToken
|
|
1027
|
-
} while (pageToken)
|
|
1028
|
-
```
|
|
1029
|
-
|
|
1030
|
-
### getUser
|
|
1031
|
-
|
|
1032
|
-
Get a specific user by ID.
|
|
1033
|
-
|
|
1034
|
-
```typescript
|
|
1035
|
-
import { getUser } from 'een-api-toolkit'
|
|
1036
|
-
|
|
1037
|
-
const { data, error } = await getUser('user-id-123')
|
|
1038
|
-
|
|
1039
|
-
if (error) {
|
|
1040
|
-
if (error.code === 'NOT_FOUND') {
|
|
1041
|
-
console.log('User not found')
|
|
1042
|
-
}
|
|
1043
|
-
return
|
|
1044
|
-
}
|
|
1045
|
-
|
|
1046
|
-
// With permissions
|
|
1047
|
-
const { data: userWithPerms } = await getUser('user-id-123', {
|
|
1048
|
-
include: ['permissions']
|
|
1049
|
-
})
|
|
1050
|
-
```
|
|
1051
|
-
|
|
1052
|
-
### getCameras
|
|
1053
|
-
|
|
1054
|
-
List cameras with optional pagination and filtering.
|
|
1055
|
-
|
|
1056
|
-
```typescript
|
|
1057
|
-
import { getCameras } from 'een-api-toolkit'
|
|
1058
|
-
|
|
1059
|
-
// Basic usage
|
|
1060
|
-
const { data, error } = await getCameras()
|
|
1061
|
-
|
|
1062
|
-
// With pagination
|
|
1063
|
-
const { data } = await getCameras({ pageSize: 50 })
|
|
1064
|
-
|
|
1065
|
-
// With status filter
|
|
1066
|
-
const { data } = await getCameras({
|
|
1067
|
-
pageSize: 20,
|
|
1068
|
-
status__in: ['online', 'streaming']
|
|
1069
|
-
})
|
|
1070
|
-
|
|
1071
|
-
// With search
|
|
1072
|
-
const { data } = await getCameras({
|
|
1073
|
-
q: 'front door',
|
|
1074
|
-
include: ['deviceInfo', 'status']
|
|
1075
|
-
})
|
|
1076
|
-
```
|
|
1077
|
-
|
|
1078
|
-
### getCamera
|
|
1079
|
-
|
|
1080
|
-
Get a specific camera by ID.
|
|
1081
|
-
|
|
1082
|
-
```typescript
|
|
1083
|
-
import { getCamera } from 'een-api-toolkit'
|
|
1084
|
-
|
|
1085
|
-
const { data, error } = await getCamera('camera-id-123')
|
|
1086
|
-
|
|
1087
|
-
if (error) {
|
|
1088
|
-
if (error.code === 'NOT_FOUND') {
|
|
1089
|
-
console.log('Camera not found')
|
|
1090
|
-
}
|
|
1091
|
-
return
|
|
1092
|
-
}
|
|
1093
|
-
|
|
1094
|
-
// With additional fields
|
|
1095
|
-
const { data: cameraWithDetails } = await getCamera('camera-id-123', {
|
|
1096
|
-
include: ['deviceInfo', 'status', 'shareDetails']
|
|
1097
|
-
})
|
|
1098
|
-
```
|
|
1099
|
-
|
|
1100
|
-
### getBridges
|
|
1101
|
-
|
|
1102
|
-
List bridges with optional pagination and filtering.
|
|
1103
|
-
|
|
1104
|
-
```typescript
|
|
1105
|
-
import { getBridges } from 'een-api-toolkit'
|
|
1106
|
-
|
|
1107
|
-
// Basic usage
|
|
1108
|
-
const { data, error } = await getBridges()
|
|
1109
|
-
|
|
1110
|
-
// With pagination
|
|
1111
|
-
const { data } = await getBridges({ pageSize: 50 })
|
|
1112
|
-
|
|
1113
|
-
// With status filter
|
|
1114
|
-
const { data } = await getBridges({
|
|
1115
|
-
pageSize: 20,
|
|
1116
|
-
status__in: ['online']
|
|
1117
|
-
})
|
|
1118
|
-
|
|
1119
|
-
// With search
|
|
1120
|
-
const { data } = await getBridges({
|
|
1121
|
-
q: 'office',
|
|
1122
|
-
include: ['deviceInfo', 'status', 'networkInfo']
|
|
1123
|
-
})
|
|
1124
|
-
```
|
|
1125
|
-
|
|
1126
|
-
### getBridge
|
|
1127
|
-
|
|
1128
|
-
Get a specific bridge by ID.
|
|
1129
|
-
|
|
1130
|
-
```typescript
|
|
1131
|
-
import { getBridge } from 'een-api-toolkit'
|
|
1132
|
-
|
|
1133
|
-
const { data, error } = await getBridge('bridge-id-123')
|
|
1134
|
-
|
|
1135
|
-
if (error) {
|
|
1136
|
-
if (error.code === 'NOT_FOUND') {
|
|
1137
|
-
console.log('Bridge not found')
|
|
1138
|
-
}
|
|
1139
|
-
return
|
|
1140
|
-
}
|
|
1141
|
-
|
|
1142
|
-
// With additional fields
|
|
1143
|
-
const { data: bridgeWithDetails } = await getBridge('bridge-id-123', {
|
|
1144
|
-
include: ['deviceInfo', 'networkInfo', 'status']
|
|
1145
|
-
})
|
|
1146
|
-
```
|
|
1147
|
-
|
|
1148
|
-
### listMedia
|
|
1149
|
-
|
|
1150
|
-
List media intervals for a camera. Useful for finding when recordings exist.
|
|
1151
|
-
|
|
1152
|
-
```typescript
|
|
1153
|
-
import { listMedia } from 'een-api-toolkit'
|
|
1154
|
-
|
|
1155
|
-
const { data, error } = await listMedia({
|
|
1156
|
-
deviceId: 'camera-id-123',
|
|
1157
|
-
type: 'preview',
|
|
1158
|
-
mediaType: 'video',
|
|
1159
|
-
startTimestamp: '2024-01-01T00:00:00.000Z',
|
|
1160
|
-
endTimestamp: '2024-01-02T00:00:00.000Z'
|
|
1161
|
-
})
|
|
1162
|
-
|
|
1163
|
-
if (error) {
|
|
1164
|
-
console.error('Failed to list media:', error.message)
|
|
1165
|
-
return
|
|
1166
|
-
}
|
|
1167
|
-
|
|
1168
|
-
// Get available recording intervals
|
|
1169
|
-
for (const interval of data.results) {
|
|
1170
|
-
console.log(`Recording: ${interval.startTimestamp} - ${interval.endTimestamp}`)
|
|
1171
|
-
}
|
|
1172
|
-
```
|
|
1173
|
-
|
|
1174
|
-
### getLiveImage
|
|
1175
|
-
|
|
1176
|
-
Get a live preview image from a camera. Returns base64-encoded image data.
|
|
1177
|
-
|
|
1178
|
-
```typescript
|
|
1179
|
-
import { getLiveImage } from 'een-api-toolkit'
|
|
1180
|
-
|
|
1181
|
-
const { data, error } = await getLiveImage({
|
|
1182
|
-
deviceId: 'camera-id-123'
|
|
1183
|
-
})
|
|
1184
|
-
|
|
1185
|
-
if (error) {
|
|
1186
|
-
console.error('Failed to get live image:', error.message)
|
|
1187
|
-
return
|
|
1188
|
-
}
|
|
1189
|
-
|
|
1190
|
-
// Use in an <img> element
|
|
1191
|
-
const imgElement = document.querySelector('img')
|
|
1192
|
-
imgElement.src = data.imageData // data:image/jpeg;base64,...
|
|
1193
|
-
|
|
1194
|
-
// Timestamp of the image
|
|
1195
|
-
console.log('Image timestamp:', data.timestamp)
|
|
1196
|
-
```
|
|
1197
|
-
|
|
1198
|
-
### getRecordedImage
|
|
1199
|
-
|
|
1200
|
-
Get a recorded image from camera history. Supports timestamp-based navigation.
|
|
1201
|
-
|
|
1202
|
-
```typescript
|
|
1203
|
-
import { getRecordedImage } from 'een-api-toolkit'
|
|
1204
|
-
|
|
1205
|
-
// Get image at specific timestamp
|
|
1206
|
-
const { data, error } = await getRecordedImage({
|
|
1207
|
-
deviceId: 'camera-id-123',
|
|
1208
|
-
timestamp: '2024-01-15T14:30:00.000Z'
|
|
1209
|
-
})
|
|
1210
|
-
|
|
1211
|
-
if (error) {
|
|
1212
|
-
console.error('Failed to get recorded image:', error.message)
|
|
1213
|
-
return
|
|
1214
|
-
}
|
|
1215
|
-
|
|
1216
|
-
// Display the image
|
|
1217
|
-
const imgElement = document.querySelector('img')
|
|
1218
|
-
if (imgElement) {
|
|
1219
|
-
imgElement.src = data.imageData
|
|
1220
|
-
}
|
|
1221
|
-
|
|
1222
|
-
// Navigate to next/previous image
|
|
1223
|
-
if (data.nextToken) {
|
|
1224
|
-
const { data: nextImage } = await getRecordedImage({
|
|
1225
|
-
pageToken: data.nextToken
|
|
1226
|
-
})
|
|
1227
|
-
// Use nextImage...
|
|
1228
|
-
}
|
|
1229
|
-
```
|
|
1230
|
-
|
|
1231
|
-
### initMediaSession
|
|
1232
|
-
|
|
1233
|
-
Initialize media session for cookie-based authentication. Required before using
|
|
1234
|
-
multipart URLs directly in HTML elements.
|
|
1235
|
-
|
|
1236
|
-
```typescript
|
|
1237
|
-
import { initMediaSession, listFeeds } from 'een-api-toolkit'
|
|
1238
|
-
|
|
1239
|
-
// Initialize the media session (do this once after login)
|
|
1240
|
-
const { data, error } = await initMediaSession()
|
|
1241
|
-
|
|
1242
|
-
if (error) {
|
|
1243
|
-
console.error('Failed to init media session:', error.message)
|
|
1244
|
-
return
|
|
1245
|
-
}
|
|
1246
|
-
|
|
1247
|
-
console.log('Media session initialized:', data.sessionUrl)
|
|
1248
|
-
|
|
1249
|
-
// Now multipart URLs can be used directly in <img> elements
|
|
1250
|
-
const { data: feeds } = await listFeeds({
|
|
1251
|
-
deviceId: 'camera-123',
|
|
1252
|
-
include: ['multipartUrl']
|
|
1253
|
-
})
|
|
1254
|
-
|
|
1255
|
-
if (feeds?.results[0]?.multipartUrl) {
|
|
1256
|
-
// This works because the session cookie is set
|
|
1257
|
-
const imgElement = document.querySelector('img')
|
|
1258
|
-
imgElement.src = feeds.results[0].multipartUrl
|
|
1259
|
-
}
|
|
1260
|
-
```
|
|
1261
|
-
|
|
1262
|
-
### getMediaSession
|
|
1263
|
-
|
|
1264
|
-
Get the media session URL without setting the cookie. Use `initMediaSession()`
|
|
1265
|
-
for most cases.
|
|
1266
|
-
|
|
1267
|
-
```typescript
|
|
1268
|
-
import { getMediaSession } from 'een-api-toolkit'
|
|
1269
|
-
|
|
1270
|
-
// Get the session URL (step 1 of 2)
|
|
1271
|
-
const { data, error } = await getMediaSession()
|
|
1272
|
-
|
|
1273
|
-
if (error) {
|
|
1274
|
-
console.error('Failed to get media session:', error.message)
|
|
1275
|
-
return
|
|
1276
|
-
}
|
|
1277
|
-
|
|
1278
|
-
console.log('Session URL:', data.url)
|
|
1279
|
-
// Manually call data.url with credentials: 'include' to set the cookie
|
|
1280
|
-
```
|
|
1281
|
-
|
|
1282
|
-
---
|
|
1283
|
-
|
|
1284
|
-
## Utilities
|
|
1285
|
-
|
|
1286
|
-
### formatTimestamp
|
|
1287
|
-
|
|
1288
|
-
Converts ISO 8601 timestamps from `Z` (Zulu/UTC) format to `+00:00` format, as required by the EEN API.
|
|
1289
|
-
|
|
1290
|
-
**Why this is needed:** JavaScript's `Date.toISOString()` returns timestamps with a `Z` suffix (e.g., `2025-01-15T22:30:00.000Z`), but the EEN API requires the `+00:00` format (e.g., `2025-01-15T22:30:00.000+00:00`).
|
|
1291
|
-
|
|
1292
|
-
```typescript
|
|
1293
|
-
import { formatTimestamp } from 'een-api-toolkit'
|
|
1294
|
-
|
|
1295
|
-
// Convert Z format to +00:00 format
|
|
1296
|
-
formatTimestamp('2025-01-15T22:30:00.000Z')
|
|
1297
|
-
// Returns: '2025-01-15T22:30:00.000+00:00'
|
|
1298
|
-
|
|
1299
|
-
// Already in +00:00 format - returns unchanged
|
|
1300
|
-
formatTimestamp('2025-01-15T22:30:00.000+00:00')
|
|
1301
|
-
// Returns: '2025-01-15T22:30:00.000+00:00'
|
|
1302
|
-
|
|
1303
|
-
// Common usage with Date objects
|
|
1304
|
-
const now = new Date()
|
|
1305
|
-
const apiTimestamp = formatTimestamp(now.toISOString())
|
|
1306
|
-
// Returns timestamp in EEN API format
|
|
1307
|
-
```
|
|
1308
|
-
|
|
1309
|
-
**When to use:**
|
|
1310
|
-
- When displaying timestamps that match the exact format used in API calls (e.g., for debugging)
|
|
1311
|
-
- When making direct API calls outside of the toolkit
|
|
1312
|
-
- When building custom timestamp display components
|
|
1313
|
-
|
|
1314
|
-
**Note:** The toolkit's API functions (`listAlerts`, `getEventMetrics`, `listNotifications`, `listEvents`, etc.) automatically apply `formatTimestamp` internally, so you don't need to pre-format timestamps when using these functions.
|
|
1315
|
-
|
|
1316
|
-
---
|
|
1317
|
-
|
|
1318
|
-
## Live Video Streaming
|
|
1319
|
-
|
|
1320
|
-
The EEN API Toolkit supports two methods for displaying live video from cameras:
|
|
1321
|
-
|
|
1322
|
-
### Stream Types Comparison
|
|
1323
|
-
|
|
1324
|
-
| Feature | Preview Stream | Main Stream |
|
|
1325
|
-
|---------|---------------|-------------|
|
|
1326
|
-
| Quality | Lower resolution | Full resolution |
|
|
1327
|
-
| Authentication | Session cookie (multipart URL) | JWT token (Live SDK) |
|
|
1328
|
-
| Element Type | `<img>` element | `<video>` element |
|
|
1329
|
-
| Technology | MJPEG multipart | WebCodecs via SDK |
|
|
1330
|
-
| Browser Support | All modern browsers | Chrome 94+, Edge 94+, Opera 80+ (WebCodecs) |
|
|
1331
|
-
| Use Case | Thumbnails, quick previews | Full video playback |
|
|
1332
|
-
| Setup | `initMediaSession()` | `@een/live-video-web-sdk` |
|
|
1333
|
-
|
|
1334
|
-
### Preview Streams (Multipart URL)
|
|
1335
|
-
|
|
1336
|
-
Preview streams use cookie-based authentication with multipart URLs. These are ideal
|
|
1337
|
-
for thumbnails and quick previews using simple `<img>` elements.
|
|
1338
|
-
|
|
1339
|
-
**Step 1: Initialize the media session (once after login)**
|
|
1340
|
-
|
|
1341
|
-
```typescript
|
|
1342
|
-
import { initMediaSession } from 'een-api-toolkit'
|
|
1343
|
-
|
|
1344
|
-
// Call once after authentication
|
|
1345
|
-
const { data, error } = await initMediaSession()
|
|
1346
|
-
if (error) {
|
|
1347
|
-
console.error('Failed to init media session:', error.message)
|
|
1348
|
-
return
|
|
1349
|
-
}
|
|
1350
|
-
// Session cookie is now set
|
|
1351
|
-
```
|
|
1352
|
-
|
|
1353
|
-
**Step 2: Get the feed URL and display it**
|
|
1354
|
-
|
|
1355
|
-
```typescript
|
|
1356
|
-
import { listFeeds } from 'een-api-toolkit'
|
|
1357
|
-
|
|
1358
|
-
const { data: feeds } = await listFeeds({
|
|
1359
|
-
deviceId: cameraId,
|
|
1360
|
-
type: 'preview',
|
|
1361
|
-
include: ['multipartUrl']
|
|
1362
|
-
})
|
|
1363
|
-
|
|
1364
|
-
// Find a feed with multipartUrl
|
|
1365
|
-
const previewFeed = feeds?.results.find(f => f.multipartUrl)
|
|
1366
|
-
if (previewFeed?.multipartUrl) {
|
|
1367
|
-
// Can be used directly in an <img> element
|
|
1368
|
-
imgElement.src = previewFeed.multipartUrl
|
|
1369
|
-
}
|
|
1370
|
-
```
|
|
1371
|
-
|
|
1372
|
-
**Complete Vue Example (Preview Stream):**
|
|
1373
|
-
|
|
1374
|
-
```vue
|
|
1375
|
-
<script setup lang="ts">
|
|
1376
|
-
import { ref, onMounted } from 'vue'
|
|
1377
|
-
import { initMediaSession, listFeeds, type Feed } from 'een-api-toolkit'
|
|
1378
|
-
|
|
1379
|
-
const props = defineProps<{ cameraId: string }>()
|
|
1380
|
-
const previewUrl = ref<string | null>(null)
|
|
1381
|
-
const loading = ref(true)
|
|
1382
|
-
|
|
1383
|
-
onMounted(async () => {
|
|
1384
|
-
// Initialize media session for cookie-based auth
|
|
1385
|
-
const { error: sessionError } = await initMediaSession()
|
|
1386
|
-
if (sessionError) {
|
|
1387
|
-
console.error('Failed to init session:', sessionError.message)
|
|
1388
|
-
loading.value = false
|
|
1389
|
-
return
|
|
1390
|
-
}
|
|
1391
|
-
|
|
1392
|
-
// Get preview feed
|
|
1393
|
-
const { data, error } = await listFeeds({
|
|
1394
|
-
deviceId: props.cameraId,
|
|
1395
|
-
type: 'preview',
|
|
1396
|
-
include: ['multipartUrl']
|
|
1397
|
-
})
|
|
1398
|
-
|
|
1399
|
-
if (error) {
|
|
1400
|
-
console.error('Failed to get feeds:', error.message)
|
|
1401
|
-
loading.value = false
|
|
1402
|
-
return
|
|
1403
|
-
}
|
|
1404
|
-
|
|
1405
|
-
const feed = data.results.find(f => f.multipartUrl)
|
|
1406
|
-
previewUrl.value = feed?.multipartUrl ?? null
|
|
1407
|
-
loading.value = false
|
|
1408
|
-
})
|
|
1409
|
-
</script>
|
|
1410
|
-
|
|
1411
|
-
<template>
|
|
1412
|
-
<div class="preview-container">
|
|
1413
|
-
<div v-if="loading">Loading...</div>
|
|
1414
|
-
<img v-else-if="previewUrl" :src="previewUrl" alt="Camera preview" />
|
|
1415
|
-
<div v-else>No preview available</div>
|
|
1416
|
-
</div>
|
|
1417
|
-
</template>
|
|
1418
|
-
```
|
|
1419
|
-
|
|
1420
|
-
### Main Streams (Live Video SDK)
|
|
1421
|
-
|
|
1422
|
-
Main streams provide full-resolution video using the `@een/live-video-web-sdk`.
|
|
1423
|
-
This requires JWT authentication and uses WebCodecs for efficient video playback.
|
|
1424
|
-
|
|
1425
|
-
**Installation:**
|
|
1426
|
-
|
|
1427
|
-
```bash
|
|
1428
|
-
npm install @een/live-video-web-sdk
|
|
1429
|
-
```
|
|
1430
|
-
|
|
1431
|
-
**Complete Vue Example (Main Stream):**
|
|
1432
|
-
|
|
1433
|
-
```vue
|
|
1434
|
-
<script setup lang="ts">
|
|
1435
|
-
import { ref, onMounted, onUnmounted, nextTick } from 'vue'
|
|
1436
|
-
import { LivePlayer } from '@een/live-video-web-sdk'
|
|
1437
|
-
import { useAuthStore, listFeeds, type Feed } from 'een-api-toolkit'
|
|
1438
|
-
|
|
1439
|
-
const props = defineProps<{ cameraId: string }>()
|
|
1440
|
-
const authStore = useAuthStore()
|
|
1441
|
-
const videoElement = ref<HTMLVideoElement | null>(null)
|
|
1442
|
-
const loading = ref(true)
|
|
1443
|
-
const statusMessage = ref('')
|
|
1444
|
-
|
|
1445
|
-
let livePlayer: LivePlayer | null = null
|
|
1446
|
-
|
|
1447
|
-
async function initPlayer() {
|
|
1448
|
-
// Get the main feed to verify it exists
|
|
1449
|
-
const { data, error } = await listFeeds({
|
|
1450
|
-
deviceId: props.cameraId,
|
|
1451
|
-
type: 'main',
|
|
1452
|
-
include: ['multipartUrl']
|
|
1453
|
-
})
|
|
1454
|
-
|
|
1455
|
-
if (error || !data.results.length) {
|
|
1456
|
-
statusMessage.value = 'No main feed available'
|
|
1457
|
-
loading.value = false
|
|
1458
|
-
return
|
|
1459
|
-
}
|
|
1460
|
-
|
|
1461
|
-
// Wait for video element to be mounted
|
|
1462
|
-
await nextTick()
|
|
1463
|
-
if (!videoElement.value) return
|
|
1464
|
-
|
|
1465
|
-
// Ensure auth is valid
|
|
1466
|
-
if (!authStore.baseUrl || !authStore.token) {
|
|
1467
|
-
statusMessage.value = 'Not authenticated'
|
|
1468
|
-
loading.value = false
|
|
1469
|
-
return
|
|
1470
|
-
}
|
|
1471
|
-
|
|
1472
|
-
// Initialize the Live SDK player
|
|
1473
|
-
livePlayer = new LivePlayer()
|
|
1474
|
-
|
|
1475
|
-
// Subscribe to status updates
|
|
1476
|
-
livePlayer.onStatusChange((status) => {
|
|
1477
|
-
statusMessage.value = status
|
|
1478
|
-
if (status === 'playing') {
|
|
1479
|
-
loading.value = false
|
|
1480
|
-
}
|
|
1481
|
-
})
|
|
1482
|
-
|
|
1483
|
-
// Start playback
|
|
1484
|
-
await livePlayer.start({
|
|
1485
|
-
videoElement: videoElement.value,
|
|
1486
|
-
cameraId: props.cameraId,
|
|
1487
|
-
baseUrl: authStore.baseUrl,
|
|
1488
|
-
jwt: authStore.token
|
|
1489
|
-
})
|
|
1490
|
-
}
|
|
1491
|
-
|
|
1492
|
-
function handleVideoError(event: Event) {
|
|
1493
|
-
const video = event.target as HTMLVideoElement
|
|
1494
|
-
console.error('Video error:', video.error?.message)
|
|
1495
|
-
statusMessage.value = 'Playback error'
|
|
1496
|
-
loading.value = false
|
|
1497
|
-
}
|
|
1498
|
-
|
|
1499
|
-
function cleanup() {
|
|
1500
|
-
if (livePlayer) {
|
|
1501
|
-
livePlayer.stop()
|
|
1502
|
-
livePlayer = null
|
|
1503
|
-
}
|
|
1504
|
-
}
|
|
1505
|
-
|
|
1506
|
-
onMounted(() => {
|
|
1507
|
-
initPlayer()
|
|
1508
|
-
})
|
|
1509
|
-
|
|
1510
|
-
onUnmounted(() => {
|
|
1511
|
-
cleanup()
|
|
1512
|
-
})
|
|
1513
|
-
</script>
|
|
1514
|
-
|
|
1515
|
-
<template>
|
|
1516
|
-
<div class="video-container">
|
|
1517
|
-
<div v-if="loading" class="loading-overlay">
|
|
1518
|
-
<span>{{ statusMessage || 'Connecting...' }}</span>
|
|
1519
|
-
</div>
|
|
1520
|
-
<video
|
|
1521
|
-
ref="videoElement"
|
|
1522
|
-
autoplay
|
|
1523
|
-
muted
|
|
1524
|
-
playsinline
|
|
1525
|
-
@error="handleVideoError"
|
|
1526
|
-
/>
|
|
1527
|
-
<div v-if="statusMessage && !loading" class="status">{{ statusMessage }}</div>
|
|
1528
|
-
</div>
|
|
1529
|
-
</template>
|
|
1530
|
-
|
|
1531
|
-
<style scoped>
|
|
1532
|
-
.video-container {
|
|
1533
|
-
position: relative;
|
|
1534
|
-
background: #000;
|
|
1535
|
-
}
|
|
1536
|
-
video {
|
|
1537
|
-
width: 100%;
|
|
1538
|
-
height: auto;
|
|
1539
|
-
}
|
|
1540
|
-
.loading-overlay {
|
|
1541
|
-
position: absolute;
|
|
1542
|
-
inset: 0;
|
|
1543
|
-
display: flex;
|
|
1544
|
-
align-items: center;
|
|
1545
|
-
justify-content: center;
|
|
1546
|
-
background: rgba(0, 0, 0, 0.7);
|
|
1547
|
-
color: white;
|
|
1548
|
-
}
|
|
1549
|
-
</style>
|
|
1550
|
-
```
|
|
1551
|
-
|
|
1552
|
-
### Choosing Between Preview and Main Streams
|
|
1553
|
-
|
|
1554
|
-
- **Preview streams** are simpler to implement and work well for:
|
|
1555
|
-
- Camera selection grids
|
|
1556
|
-
- Thumbnail previews
|
|
1557
|
-
- Lower bandwidth scenarios
|
|
1558
|
-
- Simple `<img>` element integration
|
|
1559
|
-
|
|
1560
|
-
- **Main streams** are better for:
|
|
1561
|
-
- Full-screen video viewing
|
|
1562
|
-
- High-quality playback
|
|
1563
|
-
- Professional monitoring applications
|
|
1564
|
-
- Integration with video controls
|
|
1565
|
-
|
|
1566
|
-
---
|
|
1567
|
-
|
|
1568
|
-
## HLS Video Playback Troubleshooting
|
|
1569
|
-
|
|
1570
|
-
For detailed troubleshooting, see the [HLS Video Troubleshooting Guide](./guides/HLS-VIDEO-TROUBLESHOOTING.md).
|
|
1571
|
-
|
|
1572
|
-
### Overview
|
|
1573
|
-
|
|
1574
|
-
HLS video playback from the EEN API requires:
|
|
1575
|
-
|
|
1576
|
-
1. **Initialize media session** - `initMediaSession()`
|
|
1577
|
-
2. **Find recording intervals** - `listMedia()` with `include: ['hlsUrl']`
|
|
1578
|
-
3. **Extract HLS URL** - From interval containing target timestamp
|
|
1579
|
-
4. **Configure HLS.js with auth** - Bearer token in Authorization header
|
|
1580
|
-
|
|
1581
|
-
### Key Requirements
|
|
1582
|
-
|
|
1583
|
-
| Requirement | Details |
|
|
1584
|
-
|-------------|---------|
|
|
1585
|
-
| Feed Type | HLS only available for `main` feeds, not `preview` |
|
|
1586
|
-
| Timestamp Format | Use `formatTimestamp()` to convert `Z` to `+00:00` |
|
|
1587
|
-
| Authentication | HLS.js requires `xhr.setRequestHeader('Authorization', `Bearer ${token}`)` |
|
|
1588
|
-
| Recording Coverage | Target timestamp must fall within a recording interval |
|
|
1589
|
-
|
|
1590
|
-
### Common Issues
|
|
1591
|
-
|
|
1592
|
-
#### 401 Unauthorized
|
|
1593
|
-
|
|
1594
|
-
**Cause:** Using `withCredentials: true` instead of Authorization header.
|
|
1595
|
-
|
|
1596
|
-
```typescript
|
|
1597
|
-
// WRONG
|
|
1598
|
-
const hls = new Hls({
|
|
1599
|
-
xhrSetup: (xhr) => { xhr.withCredentials = true }
|
|
1600
|
-
})
|
|
1601
|
-
|
|
1602
|
-
// CORRECT
|
|
1603
|
-
import { useAuthStore } from 'een-api-toolkit'
|
|
1604
|
-
const authStore = useAuthStore()
|
|
1605
|
-
|
|
1606
|
-
const hls = new Hls({
|
|
1607
|
-
xhrSetup: (xhr) => {
|
|
1608
|
-
xhr.setRequestHeader('Authorization', `Bearer ${authStore.token}`)
|
|
1609
|
-
}
|
|
1610
|
-
})
|
|
1611
|
-
```
|
|
1612
|
-
|
|
1613
|
-
#### No Video Available for Timestamp
|
|
1614
|
-
|
|
1615
|
-
**Cause:** Search range too narrow or using wrong feed type.
|
|
1616
|
-
|
|
1617
|
-
```typescript
|
|
1618
|
-
import { listMedia, formatTimestamp } from 'een-api-toolkit'
|
|
1619
|
-
|
|
1620
|
-
const targetTime = new Date(alertTimestamp)
|
|
1621
|
-
const searchStart = new Date(targetTime.getTime() - 60 * 60 * 1000) // 1 hour before
|
|
1622
|
-
const searchEnd = new Date(targetTime.getTime() + 60 * 60 * 1000) // 1 hour after
|
|
1623
|
-
|
|
1624
|
-
const result = await listMedia({
|
|
1625
|
-
deviceId: cameraId,
|
|
1626
|
-
type: 'main', // MUST be 'main' for HLS
|
|
1627
|
-
mediaType: 'video',
|
|
1628
|
-
startTimestamp: formatTimestamp(searchStart.toISOString()),
|
|
1629
|
-
endTimestamp: formatTimestamp(searchEnd.toISOString()),
|
|
1630
|
-
include: ['hlsUrl'] // MUST include 'hlsUrl'
|
|
1631
|
-
})
|
|
1632
|
-
|
|
1633
|
-
// Find interval containing target timestamp
|
|
1634
|
-
const intervals = result.data?.results ?? []
|
|
1635
|
-
const targetTimeMs = targetTime.getTime()
|
|
1636
|
-
|
|
1637
|
-
const matchingInterval = intervals.find(i => {
|
|
1638
|
-
if (!i.hlsUrl) return false
|
|
1639
|
-
const start = new Date(i.startTimestamp).getTime()
|
|
1640
|
-
const end = new Date(i.endTimestamp).getTime()
|
|
1641
|
-
return targetTimeMs >= start && targetTimeMs <= end
|
|
1642
|
-
})
|
|
1643
|
-
```
|
|
1644
|
-
|
|
1645
|
-
#### Timestamp Format Error
|
|
1646
|
-
|
|
1647
|
-
**Cause:** Using `Z` suffix instead of `+00:00`.
|
|
1648
|
-
|
|
1649
|
-
```typescript
|
|
1650
|
-
// WRONG
|
|
1651
|
-
const timestamp = new Date().toISOString() // "2025-01-15T22:30:00.000Z"
|
|
1652
|
-
|
|
1653
|
-
// CORRECT
|
|
1654
|
-
import { formatTimestamp } from 'een-api-toolkit'
|
|
1655
|
-
const timestamp = formatTimestamp(new Date().toISOString()) // "2025-01-15T22:30:00.000+00:00"
|
|
1656
|
-
```
|
|
1657
|
-
|
|
1658
|
-
---
|
|
1659
213
|
|
|
1660
214
|
## Common Patterns
|
|
1661
215
|
|
|
1662
216
|
### Error Handling
|
|
1663
217
|
|
|
1664
218
|
```typescript
|
|
1665
|
-
// All functions return Result<T>, never throw
|
|
1666
219
|
const { data, error } = await getUsers()
|
|
1667
220
|
|
|
1668
221
|
if (error) {
|
|
@@ -1670,12 +223,9 @@ if (error) {
|
|
|
1670
223
|
case 'AUTH_REQUIRED':
|
|
1671
224
|
router.push('/login')
|
|
1672
225
|
break
|
|
1673
|
-
case '
|
|
1674
|
-
|
|
226
|
+
case 'NOT_FOUND':
|
|
227
|
+
showNotFound()
|
|
1675
228
|
break
|
|
1676
|
-
case 'RATE_LIMITED':
|
|
1677
|
-
await sleep(1000)
|
|
1678
|
-
return retry()
|
|
1679
229
|
default:
|
|
1680
230
|
showError(error.message)
|
|
1681
231
|
}
|
|
@@ -1689,75 +239,23 @@ processUsers(data.results)
|
|
|
1689
239
|
### Pagination
|
|
1690
240
|
|
|
1691
241
|
```typescript
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
242
|
+
async function fetchAll<T>(
|
|
243
|
+
fetcher: (params: { pageToken?: string }) => Promise<Result<PaginatedResult<T>>>
|
|
244
|
+
): Promise<T[]> {
|
|
245
|
+
const all: T[] = []
|
|
1695
246
|
let pageToken: string | undefined
|
|
1696
247
|
|
|
1697
248
|
do {
|
|
1698
|
-
const { data, error } = await
|
|
1699
|
-
if (error) break
|
|
1700
|
-
|
|
249
|
+
const { data, error } = await fetcher({ pageToken })
|
|
250
|
+
if (error) break
|
|
251
|
+
all.push(...data.results)
|
|
1701
252
|
pageToken = data.nextPageToken
|
|
1702
253
|
} while (pageToken)
|
|
1703
254
|
|
|
1704
|
-
return
|
|
255
|
+
return all
|
|
1705
256
|
}
|
|
1706
257
|
```
|
|
1707
258
|
|
|
1708
|
-
### Auth Guard (Vue Router)
|
|
1709
|
-
|
|
1710
|
-
```typescript
|
|
1711
|
-
import { useAuthStore } from 'een-api-toolkit'
|
|
1712
|
-
|
|
1713
|
-
router.beforeEach((to, from, next) => {
|
|
1714
|
-
const authStore = useAuthStore()
|
|
1715
|
-
|
|
1716
|
-
if (to.meta.requiresAuth && !authStore.isAuthenticated) {
|
|
1717
|
-
next('/login')
|
|
1718
|
-
} else {
|
|
1719
|
-
next()
|
|
1720
|
-
}
|
|
1721
|
-
})
|
|
1722
|
-
```
|
|
1723
|
-
|
|
1724
|
-
### Vue Component Example
|
|
1725
|
-
|
|
1726
|
-
```vue
|
|
1727
|
-
<script setup lang="ts">
|
|
1728
|
-
import { ref, onMounted } from 'vue'
|
|
1729
|
-
import { getCurrentUser, type UserProfile, type EenError } from 'een-api-toolkit'
|
|
1730
|
-
|
|
1731
|
-
const user = ref<UserProfile | null>(null)
|
|
1732
|
-
const loading = ref(false)
|
|
1733
|
-
const error = ref<EenError | null>(null)
|
|
1734
|
-
|
|
1735
|
-
async function fetchUser() {
|
|
1736
|
-
loading.value = true
|
|
1737
|
-
const result = await getCurrentUser()
|
|
1738
|
-
loading.value = false
|
|
1739
|
-
|
|
1740
|
-
if (result.error) {
|
|
1741
|
-
error.value = result.error
|
|
1742
|
-
return
|
|
1743
|
-
}
|
|
1744
|
-
|
|
1745
|
-
user.value = result.data
|
|
1746
|
-
}
|
|
1747
|
-
|
|
1748
|
-
onMounted(() => {
|
|
1749
|
-
fetchUser()
|
|
1750
|
-
})
|
|
1751
|
-
</script>
|
|
1752
|
-
|
|
1753
|
-
<template>
|
|
1754
|
-
<div v-if="loading">Loading...</div>
|
|
1755
|
-
<div v-else-if="error">{{ error.message }}</div>
|
|
1756
|
-
<div v-else-if="user">Welcome, {{ user.firstName }}!</div>
|
|
1757
|
-
<div v-else>Not authenticated or user data not available.</div>
|
|
1758
|
-
</template>
|
|
1759
|
-
```
|
|
1760
|
-
|
|
1761
259
|
---
|
|
1762
260
|
|
|
1763
261
|
## Anti-Patterns (What NOT to Do)
|
|
@@ -1787,7 +285,7 @@ data.results.forEach(...) // TypeError if error occurred!
|
|
|
1787
285
|
// CORRECT
|
|
1788
286
|
const { data, error } = await getUsers()
|
|
1789
287
|
if (error) return
|
|
1790
|
-
data.results.forEach(...) // Safe
|
|
288
|
+
data.results.forEach(...) // Safe
|
|
1791
289
|
```
|
|
1792
290
|
|
|
1793
291
|
### DON'T: Call initEenToolkit multiple times
|
|
@@ -1801,28 +299,10 @@ export default {
|
|
|
1801
299
|
}
|
|
1802
300
|
|
|
1803
301
|
// CORRECT - call once in main.ts
|
|
1804
|
-
// main.ts
|
|
1805
302
|
initEenToolkit({ ... })
|
|
1806
303
|
app.mount('#app')
|
|
1807
304
|
```
|
|
1808
305
|
|
|
1809
|
-
### DON'T: Access data before checking error
|
|
1810
|
-
|
|
1811
|
-
```typescript
|
|
1812
|
-
// WRONG - unsafe access
|
|
1813
|
-
const { data, error } = await getUser(id)
|
|
1814
|
-
console.log(data.email) // TypeError if error!
|
|
1815
|
-
if (error) { ... }
|
|
1816
|
-
|
|
1817
|
-
// CORRECT - check error first
|
|
1818
|
-
const { data, error } = await getUser(id)
|
|
1819
|
-
if (error) {
|
|
1820
|
-
console.error(error.message)
|
|
1821
|
-
return
|
|
1822
|
-
}
|
|
1823
|
-
console.log(data.email) // Safe
|
|
1824
|
-
```
|
|
1825
|
-
|
|
1826
306
|
---
|
|
1827
307
|
|
|
1828
308
|
## External Resources
|
|
@@ -1830,4 +310,4 @@ console.log(data.email) // Safe
|
|
|
1830
310
|
- [Eagle Eye Networks Developer Portal](https://developer.eagleeyenetworks.com)
|
|
1831
311
|
- [EEN API v3.0 Reference](https://developer.eagleeyenetworks.com/reference/using-the-api)
|
|
1832
312
|
- [GitHub Repository](https://github.com/klaushofrichter/een-api-toolkit)
|
|
1833
|
-
- [OAuth Proxy](https://github.com/klaushofrichter/een-oauth-proxy)
|
|
313
|
+
- [OAuth Proxy](https://github.com/klaushofrichter/een-oauth-proxy)
|