een-api-toolkit 0.3.91 → 0.3.101
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/api-coverage-agent.md +71 -29
- package/.claude/agents/een-ptz-agent.md +12 -2
- package/CHANGELOG.md +51 -69
- package/dist/index.d.ts +12 -0
- package/docs/AI-CONTEXT.md +1 -1
- package/docs/ai-reference/AI-AUTH.md +1 -1
- package/docs/ai-reference/AI-AUTOMATIONS.md +1 -1
- package/docs/ai-reference/AI-DEVICES.md +12 -1
- package/docs/ai-reference/AI-EVENT-DATA-SCHEMAS.md +1 -1
- package/docs/ai-reference/AI-EVENTS.md +1 -1
- package/docs/ai-reference/AI-GROUPING.md +1 -1
- package/docs/ai-reference/AI-JOBS.md +1 -1
- package/docs/ai-reference/AI-MEDIA.md +1 -1
- package/docs/ai-reference/AI-PTZ.md +17 -1
- package/docs/ai-reference/AI-SETUP.md +1 -1
- package/docs/ai-reference/AI-USERS.md +1 -1
- package/examples/vue-ptz/README.md +1 -1
- package/examples/vue-ptz/src/components/CameraSelector.vue +1 -1
- package/examples/vue-ptz/src/components/PositionInput.vue +236 -58
- package/package.json +1 -1
|
@@ -205,40 +205,82 @@ Implemented: [brief list of what IS implemented]
|
|
|
205
205
|
|
|
206
206
|
#### Document 4: `docs/een-api-coverage.html`
|
|
207
207
|
|
|
208
|
-
Generate a self-contained HTML file
|
|
209
|
-
|
|
210
|
-
**
|
|
211
|
-
-
|
|
212
|
-
-
|
|
213
|
-
-
|
|
214
|
-
|
|
215
|
-
**
|
|
216
|
-
-
|
|
217
|
-
-
|
|
218
|
-
-
|
|
219
|
-
-
|
|
220
|
-
|
|
221
|
-
**
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
-
|
|
225
|
-
-
|
|
226
|
-
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
-
|
|
233
|
-
-
|
|
208
|
+
Generate a self-contained HTML file matching this exact visual style:
|
|
209
|
+
|
|
210
|
+
**Global reset and body**:
|
|
211
|
+
- `*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }`
|
|
212
|
+
- Font: `-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, sans-serif`
|
|
213
|
+
- Background: `#f5f7fa`, color: `#1a202c`
|
|
214
|
+
|
|
215
|
+
**Header** (`<header>`):
|
|
216
|
+
- Dark navy background: `#1a365d`, white text
|
|
217
|
+
- Padding: `24px 32px`
|
|
218
|
+
- `<h1>` at `1.5rem` bold: "Eagle Eye Networks API v3.0 — Toolkit Coverage"
|
|
219
|
+
- `<p>` at `0.875rem`, color `#bee3f8`: generation date and source info
|
|
220
|
+
|
|
221
|
+
**Container**: `max-width: 100%` (use full page width), centered, padding `24px 32px`
|
|
222
|
+
|
|
223
|
+
**Stat cards** (`.stats` grid):
|
|
224
|
+
- `grid-template-columns: repeat(auto-fit, minmax(180px, 1fr))`, gap `16px`
|
|
225
|
+
- Each card: white background, `border-radius: 8px`, padding `20px`, `box-shadow: 0 1px 3px rgba(0,0,0,.08)`
|
|
226
|
+
- Colored `border-top: 4px solid` — blue (`#4299e1`) for Total, green (`#48bb78`) for Implemented, red (`#fc8181`) for Missing, indigo (`#667eea`) for Coverage
|
|
227
|
+
- Label: `0.75rem` uppercase, `letter-spacing: .05em`, color `#718096`
|
|
228
|
+
- Value: `2rem` bold, color `#2d3748`
|
|
229
|
+
- Sub text: `0.8rem`, color `#718096`
|
|
230
|
+
|
|
231
|
+
**Progress bar** (`.progress-wrap`):
|
|
232
|
+
- White card with same shadow/radius as stat cards
|
|
233
|
+
- Label row: flex with `justify-content: space-between`, `0.875rem`, color `#4a5568`
|
|
234
|
+
- Bar background: `#e2e8f0`, `border-radius: 9999px`, height `12px`
|
|
235
|
+
- Fill: `linear-gradient(90deg, #48bb78, #38a169)`, same radius, `transition: width .4s ease`
|
|
236
|
+
|
|
237
|
+
**Filter bar** (`.filter-bar`):
|
|
238
|
+
- White card, flex with `flex-wrap: wrap`, gap `12px`
|
|
239
|
+
- Labels: `0.8rem`, color `#718096`
|
|
240
|
+
- Inputs/selects: border `1px solid #e2e8f0`, `border-radius: 6px`, padding `6px 10px`, `0.875rem`
|
|
241
|
+
- Focus: `border-color: #4299e1`, `box-shadow: 0 0 0 3px rgba(66,153,225,.15)`
|
|
242
|
+
- Text input width: `220px`
|
|
243
|
+
- Filter count: `margin-left: auto`, `0.8rem`, color `#718096`
|
|
244
|
+
- Dropdowns: Status (All/Implemented/Missing), Category (All + each category), Method (All/GET/POST/PATCH/DELETE/PUT)
|
|
245
|
+
- Text search placeholder: "path, function, description..."
|
|
246
|
+
|
|
247
|
+
**Table** (`.table-wrap`):
|
|
248
|
+
- White card with `overflow-x: auto` for horizontal scrollbar on narrow viewports
|
|
249
|
+
- The `<table>` element must have `min-width: 1200px` so content is never clipped — the scrollbar activates instead of truncating columns
|
|
250
|
+
- `<thead>`: dark background `#2d3748`, white text, uppercase `0.75rem`, `letter-spacing: .05em`
|
|
251
|
+
- Headers clickable (cursor pointer) with sort icon `⇅`, changing to `▲`/`▼` when sorted
|
|
252
|
+
- `<tbody>` rows: `border-bottom: 1px solid #edf2f7`, hover `#f7fafc`
|
|
253
|
+
- Cell padding: `10px 14px`
|
|
254
|
+
- Column classes:
|
|
255
|
+
- `.td-num`: color `#a0aec0`, `0.75rem`, width `44px`
|
|
256
|
+
- `.td-cat`: color `#4a5568`, `font-weight: 500`
|
|
257
|
+
- `.td-sub`: color `#718096`
|
|
258
|
+
- `.td-path`: monospace (`'SFMono-Regular', Consolas, monospace`), `0.8rem`, color `#2d3748`
|
|
259
|
+
- `.td-desc`: color `#4a5568`, `max-width: 280px`
|
|
260
|
+
- `.td-func`: monospace, `0.78rem`, color `#553c9a`
|
|
261
|
+
|
|
262
|
+
**Badges** (`.badge`):
|
|
263
|
+
- `display: inline-flex`, padding `2px 8px`, `border-radius: 4px`, `0.72rem` bold uppercase
|
|
264
|
+
- Method colors: GET (`#ebf8ff`/`#2b6cb0`), POST (`#f0fff4`/`#276749`), PATCH (`#fffff0`/`#975a16`), DELETE (`#fff5f5`/`#c53030`), PUT (`#f0e6ff`/`#553c9a`)
|
|
265
|
+
- Status colors: Implemented (`#f0fff4`/`#276749`), Missing (`#fff5f5`/`#c53030`)
|
|
266
|
+
|
|
267
|
+
**Empty state**: centered, padding `48px`, color `#a0aec0`, with a search SVG icon
|
|
268
|
+
|
|
269
|
+
**Footer**: centered, padding `24px`, color `#a0aec0`, `0.8rem`
|
|
270
|
+
|
|
271
|
+
**Columns**: #, Category, Subcategory, Method, Path, Description, Status, Toolkit Function (8 columns)
|
|
234
272
|
|
|
235
273
|
**JavaScript**:
|
|
236
274
|
- All endpoint data in a `const endpoints = [...]` array with objects: `{cat, sub, method, path, desc, status, func}`
|
|
275
|
+
- Endpoints grouped by category/subcategory with `// ─── Category - Subcategory ───` comment dividers
|
|
237
276
|
- `status` values: `"impl"` for implemented, `"miss"` for missing
|
|
238
277
|
- `func` is empty string for missing endpoints
|
|
239
|
-
- `
|
|
240
|
-
- `
|
|
241
|
-
- `
|
|
278
|
+
- `methodBadge(m)` and `statusBadge(s)` helper functions
|
|
279
|
+
- `renderTable(data)` function to populate tbody and update filter count
|
|
280
|
+
- `filterTable()` function combining all four filter inputs (status, category, method, text search across path+func+desc+sub)
|
|
281
|
+
- `sortTable(colIndex)` function with toggle direction, updating header sort icons
|
|
282
|
+
- `sortCol`/`sortAsc` state variables
|
|
283
|
+
- Initial call to `renderTable(endpoints)` at the end
|
|
242
284
|
- All code inline (no external dependencies)
|
|
243
285
|
|
|
244
286
|
## Important Guidelines
|
|
@@ -221,14 +221,24 @@ nested `capabilities.ptz.capable` field. The structure is:
|
|
|
221
221
|
```
|
|
222
222
|
|
|
223
223
|
**IMPORTANT:** The PTZ capability is at `capabilities.ptz.capable` (nested under a `ptz` object),
|
|
224
|
-
NOT at `capabilities.ptzCapable` (flat).
|
|
224
|
+
NOT at `capabilities.ptzCapable` (flat). Fisheye cameras report `capabilities.ptz.capable: true`
|
|
225
|
+
but are NOT true PTZ cameras — always exclude them. Use this pattern:
|
|
226
|
+
|
|
227
|
+
```typescript
|
|
228
|
+
import { computed } from 'vue'
|
|
229
|
+
|
|
230
|
+
const isPtzCapable = computed(() => {
|
|
231
|
+
const ptz = camera.value?.capabilities?.ptz
|
|
232
|
+
return ptz?.capable === true && ptz?.fisheye !== true
|
|
233
|
+
})
|
|
234
|
+
```
|
|
225
235
|
|
|
226
236
|
Also check `effectivePermissions.controlPTZ` to verify the user has permission to move the camera,
|
|
227
237
|
and `effectivePermissions.editPTZStations` for managing presets.
|
|
228
238
|
|
|
229
239
|
## Constraints
|
|
230
240
|
- Always check authentication before API calls
|
|
231
|
-
- Verify camera has PTZ capability (`capabilities.ptz.capable`) before showing controls
|
|
241
|
+
- Verify camera has PTZ capability (`capabilities.ptz.capable`) and is not fisheye (`capabilities.ptz.fisheye !== true`) before showing controls
|
|
232
242
|
- Check user permissions (`effectivePermissions.controlPTZ`) before enabling movement
|
|
233
243
|
- Poll position periodically (every 5s) for position display
|
|
234
244
|
- Handle 204 responses for PUT/PATCH (no response body)
|
package/CHANGELOG.md
CHANGED
|
@@ -2,78 +2,66 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
-
## [0.3.
|
|
5
|
+
## [0.3.101] - 2026-02-23
|
|
6
6
|
|
|
7
7
|
### Release Summary
|
|
8
8
|
|
|
9
|
-
#### PR #
|
|
9
|
+
#### PR #130: Release v0.3.96: PTZ fisheye exclusion and API coverage docs
|
|
10
10
|
## Summary
|
|
11
|
+
- Add fisheye camera exclusion to PTZ type definitions, agent docs, AI reference docs, and vue-ptz example
|
|
12
|
+
- Add PTZ sub-capability fields (`fisheye`, `panTilt`, `zoom`, `positionMove`, `directionMove`, `centerOnMove`) to `Camera.capabilities.ptz` type
|
|
13
|
+
- Regenerate API coverage docs with full-width layout and scrollbar fix
|
|
11
14
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
- Update README with agent list and coverage section
|
|
15
|
-
- Fix workflow_dispatch restriction to production branch (security)
|
|
15
|
+
## Version
|
|
16
|
+
v0.3.96
|
|
16
17
|
|
|
17
18
|
## Commits
|
|
19
|
+
- `1d9da62` docs: regenerate API coverage docs with full-width layout and scrollbar fix
|
|
20
|
+
- `044c9ae` docs: add fisheye camera exclusion guidance to PTZ agent
|
|
21
|
+
- `f65ff2e` fix: add PTZ sub-capability fields to Camera type and update docs
|
|
22
|
+
- `6ecf253` fix: address review findings for PTZ fisheye guidance
|
|
23
|
+
- `97cf632` fix: exclude fisheye cameras from PTZ selector in vue-ptz example
|
|
18
24
|
|
|
19
|
-
|
|
20
|
-
-
|
|
21
|
-
-
|
|
22
|
-
-
|
|
23
|
-
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
- **Lint**: Passed
|
|
28
|
-
- **Unit tests**: 644 passed
|
|
29
|
-
- **Build**: Succeeded
|
|
30
|
-
- **E2E tests**: 236 passed across all 11 example apps
|
|
31
|
-
- **Security review**: No vulnerabilities
|
|
32
|
-
- **Code review**: Approved by Claude Sonnet 4.5
|
|
33
|
-
|
|
34
|
-
## Version
|
|
35
|
-
|
|
36
|
-
v0.3.85
|
|
25
|
+
## Test Results
|
|
26
|
+
- Lint: passed (1 pre-existing warning)
|
|
27
|
+
- Unit tests: 683/683 passed
|
|
28
|
+
- Build: passed
|
|
29
|
+
- E2E: 12/12 example apps passed
|
|
30
|
+
- Security: no vulnerabilities (type/docs changes only)
|
|
31
|
+
- Confidential data scan: clean across 280 changed .md files
|
|
37
32
|
|
|
38
33
|
🤖 Generated with [Claude Code](https://claude.com/claude-code)
|
|
39
34
|
|
|
40
|
-
#### PR #
|
|
35
|
+
#### PR #134: release: v0.3.101 - multi-move-type PTZ support
|
|
41
36
|
## Summary
|
|
37
|
+
- Add move type dropdown (Position / Direction / Center On) to the PositionInput panel in `examples/vue-ptz`
|
|
38
|
+
- Dynamic input fields per move type with proper validation
|
|
39
|
+
- Replace auto-updating x/y/z with explicit "Import Current Position" button
|
|
40
|
+
- Refactored `apply()` with `buildMove()` extraction for cleaner code
|
|
41
|
+
- `try/finally` for defensive state reset, null-safe optional chaining
|
|
42
42
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
- **PTZ API functions**: `getPtzPosition()`, `movePtz()`, `getPtzSettings()`, `updatePtzSettings()` with full TypeScript types
|
|
46
|
-
- **PTZ types**: `PtzPosition`, `PtzMove` (discriminated union: position/direction/centerOn), `PtzSettings`, `PtzPreset`, `PtzSettingsUpdate`
|
|
47
|
-
- **vue-ptz example app**: Complete PTZ control application with live video, direction pad, click-to-center, position display, preset management, and API call logging
|
|
48
|
-
- **Camera capabilities**: Added `capabilities` field to `Camera` type for PTZ detection via `include: ['capabilities']`
|
|
49
|
-
- **Documentation**: AI-PTZ.md reference doc, een-ptz-agent, updated AI-CONTEXT.md and CLAUDE.md
|
|
50
|
-
- **E2E tests**: Conditional PTZ API tests that exercise position read, direction move, and preset loading when a PTZ camera is available
|
|
51
|
-
|
|
52
|
-
### Additional changes
|
|
53
|
-
- Dependabot: CodeQL action bump to 4.32.3
|
|
54
|
-
- CI: Use floating v1 tag for claude-code-action instead of SHA pin
|
|
43
|
+
## Version
|
|
44
|
+
`0.3.101`
|
|
55
45
|
|
|
56
46
|
## Commits
|
|
57
|
-
|
|
58
|
-
- `
|
|
59
|
-
- `
|
|
60
|
-
- `
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
**Version**: 0.3.89
|
|
47
|
+
- `2a4aac8f` feat: add multi-move-type support to vue-ptz PositionInput
|
|
48
|
+
- `f125139c` fix: address code review findings for PositionInput
|
|
49
|
+
- `addcc0ec` fix: wrap movePtz calls in try/finally to always reset applying state
|
|
50
|
+
- `a1365f24` fix: address remaining review findings for PositionInput
|
|
51
|
+
|
|
52
|
+
## Local test results
|
|
53
|
+
- Lint: passed (0 errors, 1 pre-existing warning)
|
|
54
|
+
- Unit tests: 684/684 passed
|
|
55
|
+
- Build: success
|
|
56
|
+
- E2E tests: 12/12 example apps passed
|
|
57
|
+
- Security review: no issues found
|
|
58
|
+
- Docs confidential data scan: clean (274 .md files scanned)
|
|
59
|
+
|
|
60
|
+
## Test plan
|
|
61
|
+
- [ ] Select "Position" move type — verify x/y/z inputs, Import Current Position button
|
|
62
|
+
- [ ] Select "Direction" move type — verify checkboxes + step size, Apply sends direction move
|
|
63
|
+
- [ ] Select "Center On" move type — verify rX/rY inputs, Apply sends centerOn move
|
|
64
|
+
- [ ] Verify x/y/z fields do NOT auto-update when position refreshes
|
|
77
65
|
|
|
78
66
|
🤖 Generated with [Claude Code](https://claude.com/claude-code)
|
|
79
67
|
|
|
@@ -81,22 +69,16 @@ This release adds PTZ (Pan/Tilt/Zoom) camera control support to the toolkit:
|
|
|
81
69
|
### Detailed Changes
|
|
82
70
|
|
|
83
71
|
#### Features
|
|
84
|
-
- feat: add
|
|
72
|
+
- feat: add multi-move-type support to vue-ptz PositionInput
|
|
85
73
|
|
|
86
74
|
#### Bug Fixes
|
|
87
|
-
- fix: address remaining
|
|
88
|
-
- fix:
|
|
89
|
-
- fix: address
|
|
90
|
-
- fix: address review findings in vue-ptz example app
|
|
91
|
-
- fix: use single getCameras call with include and pagination for PTZ discovery
|
|
92
|
-
- fix: use floating v1 tag for claude-code-action instead of SHA pin
|
|
93
|
-
|
|
94
|
-
#### Other Changes
|
|
95
|
-
- chore(deps): bump github/codeql-action from 4.32.2 to 4.32.3
|
|
75
|
+
- fix: address remaining review findings for PositionInput
|
|
76
|
+
- fix: wrap movePtz calls in try/finally to always reset applying state
|
|
77
|
+
- fix: address code review findings for PositionInput
|
|
96
78
|
|
|
97
79
|
### Links
|
|
98
80
|
- [npm package](https://www.npmjs.com/package/een-api-toolkit)
|
|
99
|
-
- [Full Changelog](https://github.com/klaushofrichter/een-api-toolkit/compare/v0.3.
|
|
81
|
+
- [Full Changelog](https://github.com/klaushofrichter/een-api-toolkit/compare/v0.3.97...v0.3.101)
|
|
100
82
|
|
|
101
83
|
---
|
|
102
|
-
*Released: 2026-02-
|
|
84
|
+
*Released: 2026-02-23 18:17:46 CST*
|
package/dist/index.d.ts
CHANGED
|
@@ -610,6 +610,18 @@ export declare interface Camera {
|
|
|
610
610
|
ptz?: {
|
|
611
611
|
/** Whether this camera supports PTZ controls */
|
|
612
612
|
capable?: boolean;
|
|
613
|
+
/** Whether this is a fisheye camera (not a true PTZ camera) */
|
|
614
|
+
fisheye?: boolean;
|
|
615
|
+
/** Whether the camera supports pan/tilt movements */
|
|
616
|
+
panTilt?: boolean;
|
|
617
|
+
/** Whether the camera supports zoom */
|
|
618
|
+
zoom?: boolean;
|
|
619
|
+
/** Whether the camera supports absolute position moves */
|
|
620
|
+
positionMove?: boolean;
|
|
621
|
+
/** Whether the camera supports directional moves */
|
|
622
|
+
directionMove?: boolean;
|
|
623
|
+
/** Whether the camera supports center-on moves */
|
|
624
|
+
centerOnMove?: boolean;
|
|
613
625
|
};
|
|
614
626
|
};
|
|
615
627
|
/** List of enabled analytics on this camera */
|
package/docs/AI-CONTEXT.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Cameras & Bridges API - EEN API Toolkit
|
|
2
2
|
|
|
3
|
-
> **Version:** 0.3.
|
|
3
|
+
> **Version:** 0.3.101
|
|
4
4
|
>
|
|
5
5
|
> Complete reference for camera and bridge management.
|
|
6
6
|
> Load this document when working with devices.
|
|
@@ -33,6 +33,17 @@ interface Camera {
|
|
|
33
33
|
deviceInfo?: CameraDeviceInfo
|
|
34
34
|
shareDetails?: CameraShareDetails
|
|
35
35
|
devicePosition?: CameraDevicePosition
|
|
36
|
+
capabilities?: {
|
|
37
|
+
ptz?: {
|
|
38
|
+
capable?: boolean
|
|
39
|
+
fisheye?: boolean
|
|
40
|
+
panTilt?: boolean
|
|
41
|
+
zoom?: boolean
|
|
42
|
+
positionMove?: boolean
|
|
43
|
+
directionMove?: boolean
|
|
44
|
+
centerOnMove?: boolean
|
|
45
|
+
}
|
|
46
|
+
}
|
|
36
47
|
createdAt?: string
|
|
37
48
|
updatedAt?: string
|
|
38
49
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# PTZ Camera Controls
|
|
2
2
|
|
|
3
|
-
> **Version:** 0.3.
|
|
3
|
+
> **Version:** 0.3.101
|
|
4
4
|
>
|
|
5
5
|
> Pan/Tilt/Zoom camera control: position, movement, presets, and automation.
|
|
6
6
|
|
|
@@ -142,6 +142,22 @@ function handleVideoClick(event: MouseEvent) {
|
|
|
142
142
|
| FORBIDDEN | No permission | Show access denied |
|
|
143
143
|
| VALIDATION_ERROR | Empty camera ID | Fix input |
|
|
144
144
|
|
|
145
|
+
## Fisheye Camera Exclusion
|
|
146
|
+
|
|
147
|
+
**IMPORTANT:** Fisheye cameras report `capabilities.ptz.capable: true` but are NOT true PTZ cameras.
|
|
148
|
+
Always exclude fisheye cameras when checking PTZ capability:
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
import { computed } from 'vue'
|
|
152
|
+
|
|
153
|
+
const isPtzCapable = computed(() => {
|
|
154
|
+
const ptz = camera.value?.capabilities?.ptz
|
|
155
|
+
return ptz?.capable === true && ptz?.fisheye !== true
|
|
156
|
+
})
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Also check `effectivePermissions.controlPTZ` to verify the user has permission to move the camera.
|
|
160
|
+
|
|
145
161
|
---
|
|
146
162
|
|
|
147
163
|
## Reference Examples
|
|
@@ -214,7 +214,7 @@ let pageToken: string | undefined
|
|
|
214
214
|
do {
|
|
215
215
|
const result = await getCameras({ pageSize: 100, include: ['capabilities'], pageToken })
|
|
216
216
|
for (const cam of result.data?.results || []) {
|
|
217
|
-
if (cam.capabilities?.ptz?.capable) ptzCameras.push(cam)
|
|
217
|
+
if (cam.capabilities?.ptz?.capable && !cam.capabilities?.ptz?.fisheye) ptzCameras.push(cam)
|
|
218
218
|
}
|
|
219
219
|
pageToken = result.data?.nextPageToken ?? undefined
|
|
220
220
|
} while (pageToken)
|
|
@@ -35,7 +35,7 @@ async function loadPtzCameras() {
|
|
|
35
35
|
|
|
36
36
|
const allCameras = result.data?.results || []
|
|
37
37
|
for (const cam of allCameras) {
|
|
38
|
-
if (cam.capabilities?.ptz?.capable) {
|
|
38
|
+
if (cam.capabilities?.ptz?.capable && !cam.capabilities?.ptz?.fisheye) {
|
|
39
39
|
ptzCameras.push(cam)
|
|
40
40
|
}
|
|
41
41
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { ref, watch } from 'vue'
|
|
3
3
|
import { movePtz } from 'een-api-toolkit'
|
|
4
|
-
import type { PtzPositionResponse } from 'een-api-toolkit'
|
|
4
|
+
import type { PtzPositionResponse, PtzDirection, PtzStepSize, PtzMoveType, PtzMove } from 'een-api-toolkit'
|
|
5
5
|
import { useApiLog } from '../composables/useApiLog'
|
|
6
6
|
|
|
7
7
|
const props = defineProps<{
|
|
@@ -14,45 +14,97 @@ const emit = defineEmits<{
|
|
|
14
14
|
}>()
|
|
15
15
|
|
|
16
16
|
const { log: apiLog } = useApiLog()
|
|
17
|
+
|
|
18
|
+
// Move type selection
|
|
19
|
+
const moveType = ref<PtzMoveType>('position')
|
|
20
|
+
|
|
21
|
+
// Position fields
|
|
17
22
|
const x = ref<string>('0')
|
|
18
23
|
const y = ref<string>('0')
|
|
19
24
|
const z = ref<string>('0')
|
|
25
|
+
|
|
26
|
+
// Direction fields
|
|
27
|
+
const directions = ref<PtzDirection[]>([])
|
|
28
|
+
const stepSize = ref<PtzStepSize>('medium')
|
|
29
|
+
const allDirections: PtzDirection[] = ['up', 'down', 'left', 'right', 'in', 'out']
|
|
30
|
+
|
|
31
|
+
// CenterOn fields
|
|
32
|
+
const relativeX = ref<string>('0.5')
|
|
33
|
+
const relativeY = ref<string>('0.5')
|
|
34
|
+
|
|
20
35
|
const applying = ref(false)
|
|
21
36
|
const error = ref<string | null>(null)
|
|
22
37
|
|
|
23
|
-
watch(
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
38
|
+
watch(moveType, () => {
|
|
39
|
+
error.value = null
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
function importPosition() {
|
|
43
|
+
if (props.currentPosition) {
|
|
44
|
+
x.value = props.currentPosition.x?.toFixed(3) ?? '0'
|
|
45
|
+
y.value = props.currentPosition.y?.toFixed(3) ?? '0'
|
|
46
|
+
z.value = props.currentPosition.z?.toFixed(3) ?? '0'
|
|
28
47
|
}
|
|
29
|
-
}
|
|
48
|
+
}
|
|
30
49
|
|
|
31
|
-
|
|
32
|
-
if (
|
|
50
|
+
function buildMove(): PtzMove | null {
|
|
51
|
+
if (moveType.value === 'position') {
|
|
52
|
+
const xVal = parseFloat(x.value)
|
|
53
|
+
const yVal = parseFloat(y.value)
|
|
54
|
+
const zVal = parseFloat(z.value)
|
|
55
|
+
|
|
56
|
+
if (isNaN(xVal) || isNaN(yVal) || isNaN(zVal)) {
|
|
57
|
+
error.value = 'Invalid numeric values'
|
|
58
|
+
return null
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Position coordinates are camera-specific absolute values (not normalised to 0-1)
|
|
62
|
+
return { moveType: 'position', x: xVal, y: yVal, z: zVal }
|
|
63
|
+
} else if (moveType.value === 'direction') {
|
|
64
|
+
if (directions.value.length === 0) {
|
|
65
|
+
error.value = 'Select at least one direction'
|
|
66
|
+
return null
|
|
67
|
+
}
|
|
33
68
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
69
|
+
return { moveType: 'direction', direction: [...directions.value], stepSize: stepSize.value }
|
|
70
|
+
} else {
|
|
71
|
+
const rxVal = parseFloat(relativeX.value)
|
|
72
|
+
const ryVal = parseFloat(relativeY.value)
|
|
73
|
+
|
|
74
|
+
if (isNaN(rxVal) || isNaN(ryVal)) {
|
|
75
|
+
error.value = 'Invalid numeric values'
|
|
76
|
+
return null
|
|
77
|
+
}
|
|
37
78
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
79
|
+
if (rxVal < 0 || rxVal > 1 || ryVal < 0 || ryVal > 1) {
|
|
80
|
+
error.value = 'Values must be between 0 and 1'
|
|
81
|
+
return null
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return { moveType: 'centerOn', relativeX: rxVal, relativeY: ryVal }
|
|
41
85
|
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function apply() {
|
|
89
|
+
if (!props.cameraId || applying.value) return
|
|
42
90
|
|
|
43
|
-
applying.value = true
|
|
44
91
|
error.value = null
|
|
45
92
|
|
|
46
|
-
const move =
|
|
47
|
-
|
|
48
|
-
apiLog('movePtz', { cameraId: props.cameraId, move }, result.error ?? result.data, !!result.error)
|
|
93
|
+
const move = buildMove()
|
|
94
|
+
if (!move) return
|
|
49
95
|
|
|
50
|
-
applying.value =
|
|
96
|
+
applying.value = true
|
|
97
|
+
try {
|
|
98
|
+
const result = await movePtz(props.cameraId, move)
|
|
99
|
+
apiLog('movePtz', { cameraId: props.cameraId, move }, result.error ?? result.data, !!result.error)
|
|
51
100
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
101
|
+
if (result.error) {
|
|
102
|
+
error.value = result.error.message
|
|
103
|
+
} else {
|
|
104
|
+
emit('move-complete')
|
|
105
|
+
}
|
|
106
|
+
} finally {
|
|
107
|
+
applying.value = false
|
|
56
108
|
}
|
|
57
109
|
}
|
|
58
110
|
</script>
|
|
@@ -60,39 +112,114 @@ async function apply() {
|
|
|
60
112
|
<template>
|
|
61
113
|
<div class="position-input" data-testid="position-input">
|
|
62
114
|
<div class="input-row">
|
|
63
|
-
<label>
|
|
64
|
-
<
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
data-testid="input-x"
|
|
70
|
-
@keyup.enter="apply"
|
|
71
|
-
/>
|
|
72
|
-
</div>
|
|
73
|
-
<div class="input-row">
|
|
74
|
-
<label>Y</label>
|
|
75
|
-
<input
|
|
76
|
-
v-model="y"
|
|
77
|
-
type="number"
|
|
78
|
-
step="0.01"
|
|
79
|
-
:disabled="!cameraId || applying"
|
|
80
|
-
data-testid="input-y"
|
|
81
|
-
@keyup.enter="apply"
|
|
82
|
-
/>
|
|
83
|
-
</div>
|
|
84
|
-
<div class="input-row">
|
|
85
|
-
<label>Z</label>
|
|
86
|
-
<input
|
|
87
|
-
v-model="z"
|
|
88
|
-
type="number"
|
|
89
|
-
step="0.1"
|
|
90
|
-
min="0"
|
|
91
|
-
:disabled="!cameraId || applying"
|
|
92
|
-
data-testid="input-z"
|
|
93
|
-
@keyup.enter="apply"
|
|
94
|
-
/>
|
|
115
|
+
<label>Move</label>
|
|
116
|
+
<select v-model="moveType" :disabled="!cameraId || applying" data-testid="move-type-select">
|
|
117
|
+
<option value="position">Position</option>
|
|
118
|
+
<option value="direction">Direction</option>
|
|
119
|
+
<option value="centerOn">Center On</option>
|
|
120
|
+
</select>
|
|
95
121
|
</div>
|
|
122
|
+
|
|
123
|
+
<!-- Position fields -->
|
|
124
|
+
<template v-if="moveType === 'position'">
|
|
125
|
+
<div class="input-row">
|
|
126
|
+
<label>X</label>
|
|
127
|
+
<input
|
|
128
|
+
v-model="x"
|
|
129
|
+
type="number"
|
|
130
|
+
step="0.01"
|
|
131
|
+
:disabled="!cameraId || applying"
|
|
132
|
+
data-testid="input-x"
|
|
133
|
+
@keyup.enter="apply"
|
|
134
|
+
/>
|
|
135
|
+
</div>
|
|
136
|
+
<div class="input-row">
|
|
137
|
+
<label>Y</label>
|
|
138
|
+
<input
|
|
139
|
+
v-model="y"
|
|
140
|
+
type="number"
|
|
141
|
+
step="0.01"
|
|
142
|
+
:disabled="!cameraId || applying"
|
|
143
|
+
data-testid="input-y"
|
|
144
|
+
@keyup.enter="apply"
|
|
145
|
+
/>
|
|
146
|
+
</div>
|
|
147
|
+
<div class="input-row">
|
|
148
|
+
<label>Z</label>
|
|
149
|
+
<input
|
|
150
|
+
v-model="z"
|
|
151
|
+
type="number"
|
|
152
|
+
step="0.1"
|
|
153
|
+
min="0"
|
|
154
|
+
:disabled="!cameraId || applying"
|
|
155
|
+
data-testid="input-z"
|
|
156
|
+
@keyup.enter="apply"
|
|
157
|
+
/>
|
|
158
|
+
</div>
|
|
159
|
+
<button
|
|
160
|
+
class="import-btn"
|
|
161
|
+
:disabled="!cameraId || !currentPosition || applying"
|
|
162
|
+
@click="importPosition"
|
|
163
|
+
data-testid="import-position"
|
|
164
|
+
>
|
|
165
|
+
Import Current Position
|
|
166
|
+
</button>
|
|
167
|
+
</template>
|
|
168
|
+
|
|
169
|
+
<!-- Direction fields -->
|
|
170
|
+
<template v-else-if="moveType === 'direction'">
|
|
171
|
+
<div class="direction-checkboxes">
|
|
172
|
+
<label v-for="dir in allDirections" :key="dir" class="checkbox-label">
|
|
173
|
+
<input
|
|
174
|
+
type="checkbox"
|
|
175
|
+
:value="dir"
|
|
176
|
+
v-model="directions"
|
|
177
|
+
:disabled="!cameraId || applying"
|
|
178
|
+
:data-testid="'dir-' + dir"
|
|
179
|
+
/>
|
|
180
|
+
{{ dir }}
|
|
181
|
+
</label>
|
|
182
|
+
</div>
|
|
183
|
+
<div class="input-row">
|
|
184
|
+
<label>Step</label>
|
|
185
|
+
<select v-model="stepSize" :disabled="!cameraId || applying" data-testid="step-size-select">
|
|
186
|
+
<option value="small">Small</option>
|
|
187
|
+
<option value="medium">Medium</option>
|
|
188
|
+
<option value="large">Large</option>
|
|
189
|
+
</select>
|
|
190
|
+
</div>
|
|
191
|
+
</template>
|
|
192
|
+
|
|
193
|
+
<!-- CenterOn fields -->
|
|
194
|
+
<template v-else>
|
|
195
|
+
<div class="input-row">
|
|
196
|
+
<label>rX</label>
|
|
197
|
+
<input
|
|
198
|
+
v-model="relativeX"
|
|
199
|
+
type="number"
|
|
200
|
+
step="0.01"
|
|
201
|
+
min="0"
|
|
202
|
+
max="1"
|
|
203
|
+
:disabled="!cameraId || applying"
|
|
204
|
+
data-testid="input-relative-x"
|
|
205
|
+
@keyup.enter="apply"
|
|
206
|
+
/>
|
|
207
|
+
</div>
|
|
208
|
+
<div class="input-row">
|
|
209
|
+
<label>rY</label>
|
|
210
|
+
<input
|
|
211
|
+
v-model="relativeY"
|
|
212
|
+
type="number"
|
|
213
|
+
step="0.01"
|
|
214
|
+
min="0"
|
|
215
|
+
max="1"
|
|
216
|
+
:disabled="!cameraId || applying"
|
|
217
|
+
data-testid="input-relative-y"
|
|
218
|
+
@keyup.enter="apply"
|
|
219
|
+
/>
|
|
220
|
+
</div>
|
|
221
|
+
</template>
|
|
222
|
+
|
|
96
223
|
<button
|
|
97
224
|
class="apply-btn"
|
|
98
225
|
:disabled="!cameraId || applying"
|
|
@@ -127,7 +254,7 @@ async function apply() {
|
|
|
127
254
|
font-size: 13px;
|
|
128
255
|
font-weight: 600;
|
|
129
256
|
color: #555;
|
|
130
|
-
min-width:
|
|
257
|
+
min-width: 32px;
|
|
131
258
|
text-align: right;
|
|
132
259
|
}
|
|
133
260
|
|
|
@@ -140,10 +267,61 @@ async function apply() {
|
|
|
140
267
|
font-family: monospace;
|
|
141
268
|
}
|
|
142
269
|
|
|
143
|
-
.input-row
|
|
270
|
+
.input-row select {
|
|
271
|
+
flex: 1;
|
|
272
|
+
padding: 5px 8px;
|
|
273
|
+
border: 1px solid #ddd;
|
|
274
|
+
border-radius: 4px;
|
|
275
|
+
font-size: 13px;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
.input-row input:disabled,
|
|
279
|
+
.input-row select:disabled {
|
|
144
280
|
opacity: 0.5;
|
|
145
281
|
}
|
|
146
282
|
|
|
283
|
+
.direction-checkboxes {
|
|
284
|
+
display: grid;
|
|
285
|
+
grid-template-columns: 1fr 1fr;
|
|
286
|
+
gap: 4px 12px;
|
|
287
|
+
margin-bottom: 8px;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
.checkbox-label {
|
|
291
|
+
display: flex;
|
|
292
|
+
align-items: center;
|
|
293
|
+
gap: 6px;
|
|
294
|
+
font-size: 13px;
|
|
295
|
+
color: #555;
|
|
296
|
+
cursor: pointer;
|
|
297
|
+
text-transform: capitalize;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
.checkbox-label input[type="checkbox"] {
|
|
301
|
+
margin: 0;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
.import-btn {
|
|
305
|
+
width: 100%;
|
|
306
|
+
padding: 6px;
|
|
307
|
+
font-size: 11px;
|
|
308
|
+
background: #6c757d;
|
|
309
|
+
color: white;
|
|
310
|
+
border: none;
|
|
311
|
+
border-radius: 4px;
|
|
312
|
+
cursor: pointer;
|
|
313
|
+
margin-bottom: 4px;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
.import-btn:hover:not(:disabled) {
|
|
317
|
+
background: #5a6268;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
.import-btn:disabled {
|
|
321
|
+
opacity: 0.4;
|
|
322
|
+
cursor: not-allowed;
|
|
323
|
+
}
|
|
324
|
+
|
|
147
325
|
.apply-btn {
|
|
148
326
|
width: 100%;
|
|
149
327
|
padding: 8px;
|