een-api-toolkit 0.3.82 → 0.3.91

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/.claude/agents/api-coverage-agent.md +264 -0
  2. package/.claude/agents/een-devices-agent.md +21 -0
  3. package/.claude/agents/een-ptz-agent.md +235 -0
  4. package/CHANGELOG.md +56 -60
  5. package/README.md +24 -1
  6. package/dist/index.cjs +3 -3
  7. package/dist/index.cjs.map +1 -1
  8. package/dist/index.d.ts +400 -0
  9. package/dist/index.js +1079 -951
  10. package/dist/index.js.map +1 -1
  11. package/docs/AI-CONTEXT.md +3 -1
  12. package/docs/ai-reference/AI-AUTH.md +1 -1
  13. package/docs/ai-reference/AI-AUTOMATIONS.md +1 -1
  14. package/docs/ai-reference/AI-DEVICES.md +1 -1
  15. package/docs/ai-reference/AI-EVENT-DATA-SCHEMAS.md +1 -1
  16. package/docs/ai-reference/AI-EVENTS.md +1 -1
  17. package/docs/ai-reference/AI-GROUPING.md +1 -1
  18. package/docs/ai-reference/AI-JOBS.md +1 -1
  19. package/docs/ai-reference/AI-MEDIA.md +1 -1
  20. package/docs/ai-reference/AI-PTZ.md +158 -0
  21. package/docs/ai-reference/AI-SETUP.md +1 -1
  22. package/docs/ai-reference/AI-USERS.md +1 -1
  23. package/examples/vue-ptz/.env.example +4 -0
  24. package/examples/vue-ptz/README.md +221 -0
  25. package/examples/vue-ptz/e2e/app.spec.ts +58 -0
  26. package/examples/vue-ptz/e2e/auth.spec.ts +296 -0
  27. package/examples/vue-ptz/index.html +13 -0
  28. package/examples/vue-ptz/package-lock.json +1729 -0
  29. package/examples/vue-ptz/package.json +29 -0
  30. package/examples/vue-ptz/playwright.config.ts +49 -0
  31. package/examples/vue-ptz/screenshot-ptz.png +0 -0
  32. package/examples/vue-ptz/src/App.vue +154 -0
  33. package/examples/vue-ptz/src/components/ApiLog.vue +387 -0
  34. package/examples/vue-ptz/src/components/CameraSelector.vue +155 -0
  35. package/examples/vue-ptz/src/components/DirectionPad.vue +350 -0
  36. package/examples/vue-ptz/src/components/LiveVideoPlayer.vue +248 -0
  37. package/examples/vue-ptz/src/components/PositionDisplay.vue +206 -0
  38. package/examples/vue-ptz/src/components/PositionInput.vue +190 -0
  39. package/examples/vue-ptz/src/components/PresetManager.vue +538 -0
  40. package/examples/vue-ptz/src/composables/useApiLog.ts +89 -0
  41. package/examples/vue-ptz/src/main.ts +22 -0
  42. package/examples/vue-ptz/src/router/index.ts +61 -0
  43. package/examples/vue-ptz/src/views/Callback.vue +76 -0
  44. package/examples/vue-ptz/src/views/Home.vue +199 -0
  45. package/examples/vue-ptz/src/views/Login.vue +32 -0
  46. package/examples/vue-ptz/src/views/Logout.vue +59 -0
  47. package/examples/vue-ptz/src/views/PtzControl.vue +173 -0
  48. package/examples/vue-ptz/src/vite-env.d.ts +12 -0
  49. package/examples/vue-ptz/tsconfig.json +21 -0
  50. package/examples/vue-ptz/tsconfig.node.json +11 -0
  51. package/examples/vue-ptz/vite.config.ts +12 -0
  52. package/package.json +1 -1
@@ -0,0 +1,155 @@
1
+ <script setup lang="ts">
2
+ import { ref, onMounted } from 'vue'
3
+ import { getCameras } from 'een-api-toolkit'
4
+ import type { Camera } from 'een-api-toolkit'
5
+
6
+ const emit = defineEmits<{
7
+ (e: 'select', camera: Camera): void
8
+ }>()
9
+
10
+ const cameras = ref<Camera[]>([])
11
+ const loading = ref(false)
12
+ const error = ref<string | null>(null)
13
+
14
+ async function loadPtzCameras() {
15
+ if (loading.value) return
16
+ loading.value = true
17
+ error.value = null
18
+
19
+ // Fetch all cameras with capabilities included in a single request
20
+ const ptzCameras: Camera[] = []
21
+ let pageToken: string | undefined
22
+
23
+ do {
24
+ const result = await getCameras({
25
+ pageSize: 100,
26
+ include: ['capabilities'],
27
+ pageToken
28
+ })
29
+
30
+ if (result.error) {
31
+ error.value = result.error.message
32
+ loading.value = false
33
+ return
34
+ }
35
+
36
+ const allCameras = result.data?.results || []
37
+ for (const cam of allCameras) {
38
+ if (cam.capabilities?.ptz?.capable) {
39
+ ptzCameras.push(cam)
40
+ }
41
+ }
42
+
43
+ pageToken = result.data?.nextPageToken ?? undefined
44
+ } while (pageToken)
45
+
46
+ cameras.value = ptzCameras
47
+ loading.value = false
48
+
49
+ // Auto-select first PTZ camera
50
+ if (ptzCameras.length > 0) {
51
+ emit('select', ptzCameras[0])
52
+ }
53
+ }
54
+
55
+ function handleChange(event: Event) {
56
+ const target = event.target as HTMLSelectElement
57
+ const camera = cameras.value.find(c => c.id === target.value)
58
+ if (camera) {
59
+ emit('select', camera)
60
+ }
61
+ }
62
+
63
+ onMounted(() => {
64
+ loadPtzCameras()
65
+ })
66
+ </script>
67
+
68
+ <template>
69
+ <div class="camera-selector">
70
+ <label for="ptz-camera-select">PTZ Camera:</label>
71
+
72
+ <div v-if="loading" class="loading-inline">
73
+ Loading cameras...
74
+ </div>
75
+
76
+ <div v-else-if="error" class="error-inline">
77
+ {{ error }}
78
+ <button @click="loadPtzCameras" class="retry-btn">Retry</button>
79
+ </div>
80
+
81
+ <div v-else-if="cameras.length === 0" class="no-cameras" data-testid="no-ptz-cameras">
82
+ No PTZ-capable cameras found.
83
+ </div>
84
+
85
+ <div v-else class="selector-row">
86
+ <select
87
+ id="ptz-camera-select"
88
+ @change="handleChange"
89
+ data-testid="ptz-camera-select"
90
+ aria-label="Select a PTZ camera"
91
+ >
92
+ <option v-for="camera in cameras" :key="camera.id" :value="camera.id">
93
+ {{ camera.name || camera.id }}
94
+ </option>
95
+ </select>
96
+ <button @click="loadPtzCameras" class="refresh-btn" data-testid="refresh-cameras">
97
+ Refresh
98
+ </button>
99
+ </div>
100
+ </div>
101
+ </template>
102
+
103
+ <style scoped>
104
+ .camera-selector {
105
+ display: flex;
106
+ align-items: center;
107
+ gap: 10px;
108
+ margin-bottom: 20px;
109
+ flex-wrap: wrap;
110
+ }
111
+
112
+ .camera-selector label {
113
+ font-weight: bold;
114
+ white-space: nowrap;
115
+ }
116
+
117
+ .selector-row {
118
+ display: flex;
119
+ align-items: center;
120
+ gap: 10px;
121
+ flex: 1;
122
+ }
123
+
124
+ .selector-row select {
125
+ flex: 1;
126
+ min-width: 200px;
127
+ padding: 8px;
128
+ font-size: 14px;
129
+ border: 1px solid #ddd;
130
+ border-radius: 4px;
131
+ }
132
+
133
+ .loading-inline {
134
+ color: #666;
135
+ font-style: italic;
136
+ }
137
+
138
+ .error-inline {
139
+ color: #e74c3c;
140
+ display: flex;
141
+ align-items: center;
142
+ gap: 10px;
143
+ }
144
+
145
+ .retry-btn,
146
+ .refresh-btn {
147
+ padding: 8px 16px;
148
+ font-size: 13px;
149
+ }
150
+
151
+ .no-cameras {
152
+ color: #999;
153
+ font-style: italic;
154
+ }
155
+ </style>
@@ -0,0 +1,350 @@
1
+ <script setup lang="ts">
2
+ import { ref, computed } from 'vue'
3
+ import { movePtz } from 'een-api-toolkit'
4
+ import type { PtzDirection, PtzStepSize, PtzPreset, PtzPositionResponse } from 'een-api-toolkit'
5
+ import { useApiLog } from '../composables/useApiLog'
6
+
7
+ import { POSITION_TOLERANCE } from '../composables/useApiLog'
8
+
9
+ const props = defineProps<{
10
+ cameraId: string | null
11
+ homePreset: PtzPreset | null
12
+ currentPosition: PtzPositionResponse | null
13
+ }>()
14
+
15
+ const isAtHome = computed(() => {
16
+ if (!props.homePreset || !props.currentPosition) return false
17
+ const home = props.homePreset.position
18
+ const cur = props.currentPosition
19
+ return (
20
+ Math.abs(cur.x - home.x) < POSITION_TOLERANCE &&
21
+ Math.abs(cur.y - home.y) < POSITION_TOLERANCE &&
22
+ Math.abs(cur.z - home.z) < POSITION_TOLERANCE
23
+ )
24
+ })
25
+
26
+ const emit = defineEmits<{
27
+ (e: 'move-complete'): void
28
+ }>()
29
+
30
+ const { log: apiLog } = useApiLog()
31
+ const stepSize = ref<PtzStepSize>('medium')
32
+ const moving = ref(false)
33
+ const error = ref<string | null>(null)
34
+
35
+ async function move(directions: PtzDirection[]) {
36
+ if (!props.cameraId || moving.value) return
37
+
38
+ moving.value = true
39
+ error.value = null
40
+
41
+ const moveCmd = { moveType: 'direction' as const, direction: directions, stepSize: stepSize.value }
42
+ const result = await movePtz(props.cameraId, moveCmd)
43
+ apiLog('movePtz', { cameraId: props.cameraId, move: moveCmd }, result.error ?? result.data, !!result.error)
44
+
45
+ moving.value = false
46
+
47
+ if (result.error) {
48
+ error.value = result.error.message
49
+ } else {
50
+ emit('move-complete')
51
+ }
52
+ }
53
+
54
+ async function goHome() {
55
+ if (!props.cameraId || !props.homePreset || moving.value) return
56
+
57
+ moving.value = true
58
+ error.value = null
59
+
60
+ const pos = props.homePreset.position
61
+ const moveCmd = { moveType: 'position' as const, x: pos.x, y: pos.y, z: pos.z }
62
+ const result = await movePtz(props.cameraId, moveCmd)
63
+ apiLog('movePtz', { cameraId: props.cameraId, move: moveCmd }, result.error ?? result.data, !!result.error)
64
+
65
+ moving.value = false
66
+
67
+ if (result.error) {
68
+ error.value = result.error.message
69
+ } else {
70
+ emit('move-complete')
71
+ }
72
+ }
73
+ </script>
74
+
75
+ <template>
76
+ <div class="direction-pad" data-testid="direction-pad">
77
+ <div class="step-size-selector">
78
+ <label>Step Size:</label>
79
+ <select v-model="stepSize" data-testid="step-size-select">
80
+ <option value="small">Small</option>
81
+ <option value="medium">Medium</option>
82
+ <option value="large">Large</option>
83
+ </select>
84
+ </div>
85
+
86
+ <div class="dpad-grid">
87
+ <div class="dpad-row">
88
+ <div class="dpad-spacer"></div>
89
+ <button
90
+ class="dpad-btn"
91
+ :disabled="!cameraId || moving"
92
+ @click="move(['up'])"
93
+ data-testid="btn-up"
94
+ title="Pan Up"
95
+ >
96
+ &#9650;
97
+ </button>
98
+ <div class="dpad-spacer"></div>
99
+ </div>
100
+ <div class="dpad-row">
101
+ <button
102
+ class="dpad-btn"
103
+ :disabled="!cameraId || moving"
104
+ @click="move(['left'])"
105
+ data-testid="btn-left"
106
+ title="Pan Left"
107
+ >
108
+ &#9664;
109
+ </button>
110
+ <button
111
+ v-if="homePreset"
112
+ class="dpad-center dpad-home"
113
+ :class="isAtHome ? 'dpad-home-at' : 'dpad-home-away'"
114
+ :disabled="!cameraId || moving"
115
+ @click="goHome"
116
+ data-testid="btn-home"
117
+ :title="'Go to Home: ' + homePreset.name"
118
+ >
119
+ <svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor">
120
+ <path d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z"/>
121
+ </svg>
122
+ </button>
123
+ <div v-else class="dpad-center">PTZ</div>
124
+ <button
125
+ class="dpad-btn"
126
+ :disabled="!cameraId || moving"
127
+ @click="move(['right'])"
128
+ data-testid="btn-right"
129
+ title="Pan Right"
130
+ >
131
+ &#9654;
132
+ </button>
133
+ </div>
134
+ <div class="dpad-row">
135
+ <div class="dpad-spacer"></div>
136
+ <button
137
+ class="dpad-btn"
138
+ :disabled="!cameraId || moving"
139
+ @click="move(['down'])"
140
+ data-testid="btn-down"
141
+ title="Pan Down"
142
+ >
143
+ &#9660;
144
+ </button>
145
+ <div class="dpad-spacer"></div>
146
+ </div>
147
+ </div>
148
+
149
+ <div class="zoom-controls">
150
+ <button
151
+ class="zoom-btn zoom-in"
152
+ :disabled="!cameraId || moving"
153
+ @click="move(['in'])"
154
+ data-testid="btn-zoom-in"
155
+ title="Zoom In"
156
+ >
157
+ + Zoom In
158
+ </button>
159
+ <button
160
+ class="zoom-btn zoom-out"
161
+ :disabled="!cameraId || moving"
162
+ @click="move(['out'])"
163
+ data-testid="btn-zoom-out"
164
+ title="Zoom Out"
165
+ >
166
+ - Zoom Out
167
+ </button>
168
+ </div>
169
+
170
+ <div v-if="error" class="pad-error">
171
+ <p>{{ error }}</p>
172
+ </div>
173
+ </div>
174
+ </template>
175
+
176
+ <style scoped>
177
+ .direction-pad {
178
+ padding: 15px;
179
+ background: #f8f9fa;
180
+ border-radius: 8px;
181
+ border: 1px solid #ddd;
182
+ }
183
+
184
+ .step-size-selector {
185
+ display: flex;
186
+ align-items: center;
187
+ gap: 8px;
188
+ margin-bottom: 15px;
189
+ justify-content: center;
190
+ }
191
+
192
+ .step-size-selector label {
193
+ font-size: 13px;
194
+ font-weight: 600;
195
+ }
196
+
197
+ .step-size-selector select {
198
+ padding: 4px 8px;
199
+ border: 1px solid #ddd;
200
+ border-radius: 4px;
201
+ font-size: 13px;
202
+ }
203
+
204
+ .dpad-grid {
205
+ display: flex;
206
+ flex-direction: column;
207
+ align-items: center;
208
+ gap: 4px;
209
+ margin-bottom: 15px;
210
+ }
211
+
212
+ .dpad-row {
213
+ display: flex;
214
+ gap: 4px;
215
+ }
216
+
217
+ .dpad-btn {
218
+ width: 50px;
219
+ height: 50px;
220
+ border: 1px solid #ccc;
221
+ border-radius: 8px;
222
+ background: #fff;
223
+ color: #333;
224
+ font-size: 18px;
225
+ cursor: pointer;
226
+ display: flex;
227
+ align-items: center;
228
+ justify-content: center;
229
+ transition: all 0.15s;
230
+ padding: 0;
231
+ }
232
+
233
+ .dpad-btn:hover:not(:disabled) {
234
+ background: #3498db;
235
+ color: white;
236
+ border-color: #3498db;
237
+ }
238
+
239
+ .dpad-btn:active:not(:disabled) {
240
+ background: #2980b9;
241
+ transform: scale(0.95);
242
+ }
243
+
244
+ .dpad-btn:disabled {
245
+ opacity: 0.4;
246
+ cursor: not-allowed;
247
+ }
248
+
249
+ .dpad-center {
250
+ width: 50px;
251
+ height: 50px;
252
+ display: flex;
253
+ align-items: center;
254
+ justify-content: center;
255
+ background: #e9ecef;
256
+ border-radius: 8px;
257
+ font-size: 11px;
258
+ font-weight: 600;
259
+ color: #666;
260
+ }
261
+
262
+ .dpad-home {
263
+ border: 1px solid #ccc;
264
+ cursor: pointer;
265
+ transition: all 0.15s;
266
+ padding: 0;
267
+ }
268
+
269
+ .dpad-home-at {
270
+ color: #27ae60;
271
+ }
272
+
273
+ .dpad-home-away {
274
+ color: #e74c3c;
275
+ }
276
+
277
+ .dpad-home-at:hover:not(:disabled) {
278
+ background: #27ae60;
279
+ color: white;
280
+ border-color: #27ae60;
281
+ }
282
+
283
+ .dpad-home-away:hover:not(:disabled) {
284
+ background: #e74c3c;
285
+ color: white;
286
+ border-color: #e74c3c;
287
+ }
288
+
289
+ .dpad-home:active:not(:disabled) {
290
+ transform: scale(0.95);
291
+ }
292
+
293
+ .dpad-home:disabled {
294
+ opacity: 0.4;
295
+ cursor: not-allowed;
296
+ }
297
+
298
+ .dpad-spacer {
299
+ width: 50px;
300
+ height: 50px;
301
+ }
302
+
303
+ .zoom-controls {
304
+ display: flex;
305
+ gap: 8px;
306
+ }
307
+
308
+ .zoom-btn {
309
+ flex: 1;
310
+ padding: 10px;
311
+ font-size: 13px;
312
+ border-radius: 6px;
313
+ border: 1px solid #ccc;
314
+ background: #fff;
315
+ color: #333;
316
+ cursor: pointer;
317
+ transition: all 0.15s;
318
+ }
319
+
320
+ .zoom-btn:hover:not(:disabled) {
321
+ border-color: #3498db;
322
+ color: #3498db;
323
+ }
324
+
325
+ .zoom-btn:disabled {
326
+ opacity: 0.4;
327
+ cursor: not-allowed;
328
+ }
329
+
330
+ .zoom-in:hover:not(:disabled) {
331
+ background: #e8f5e9;
332
+ border-color: #27ae60;
333
+ color: #27ae60;
334
+ }
335
+
336
+ .zoom-out:hover:not(:disabled) {
337
+ background: #fce4ec;
338
+ border-color: #e74c3c;
339
+ color: #e74c3c;
340
+ }
341
+
342
+ .pad-error {
343
+ margin-top: 10px;
344
+ padding: 8px;
345
+ background: #f8d7da;
346
+ border-radius: 4px;
347
+ color: #721c24;
348
+ font-size: 12px;
349
+ }
350
+ </style>