een-api-toolkit 0.3.97 → 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/CHANGELOG.md +42 -51
- 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 +1 -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 +1 -1
- package/docs/ai-reference/AI-SETUP.md +1 -1
- package/docs/ai-reference/AI-USERS.md +1 -1
- package/examples/vue-ptz/src/components/PositionInput.vue +236 -58
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,50 +2,10 @@
|
|
|
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 #128: Release v0.3.89: PTZ camera control API and vue-ptz example
|
|
10
|
-
## Summary
|
|
11
|
-
|
|
12
|
-
This release adds PTZ (Pan/Tilt/Zoom) camera control support to the toolkit:
|
|
13
|
-
|
|
14
|
-
- **PTZ API functions**: `getPtzPosition()`, `movePtz()`, `getPtzSettings()`, `updatePtzSettings()` with full TypeScript types
|
|
15
|
-
- **PTZ types**: `PtzPosition`, `PtzMove` (discriminated union: position/direction/centerOn), `PtzSettings`, `PtzPreset`, `PtzSettingsUpdate`
|
|
16
|
-
- **vue-ptz example app**: Complete PTZ control application with live video, direction pad, click-to-center, position display, preset management, and API call logging
|
|
17
|
-
- **Camera capabilities**: Added `capabilities` field to `Camera` type for PTZ detection via `include: ['capabilities']`
|
|
18
|
-
- **Documentation**: AI-PTZ.md reference doc, een-ptz-agent, updated AI-CONTEXT.md and CLAUDE.md
|
|
19
|
-
- **E2E tests**: Conditional PTZ API tests that exercise position read, direction move, and preset loading when a PTZ camera is available
|
|
20
|
-
|
|
21
|
-
### Additional changes
|
|
22
|
-
- Dependabot: CodeQL action bump to 4.32.3
|
|
23
|
-
- CI: Use floating v1 tag for claude-code-action instead of SHA pin
|
|
24
|
-
|
|
25
|
-
## Commits
|
|
26
|
-
|
|
27
|
-
- `c552fd0` feat: add PTZ camera control API and vue-ptz example app
|
|
28
|
-
- `6a12649` fix: use single getCameras call with include and pagination for PTZ discovery
|
|
29
|
-
- `2b04b26` fix: address review findings in vue-ptz example app
|
|
30
|
-
- `92a18e4` fix: address remaining review findings
|
|
31
|
-
- `4d01c2c` fix: use floating v1 tag for claude-code-action instead of SHA pin
|
|
32
|
-
- `3f197ba` chore(deps): bump github/codeql-action from 4.32.2 to 4.32.3
|
|
33
|
-
|
|
34
|
-
## Test Results
|
|
35
|
-
|
|
36
|
-
| Check | Result |
|
|
37
|
-
|-------|--------|
|
|
38
|
-
| Lint | Pass (0 errors) |
|
|
39
|
-
| Unit tests | 681 passed (24 suites) |
|
|
40
|
-
| Build | Pass |
|
|
41
|
-
| E2E (12 apps) | All passed |
|
|
42
|
-
| Security review | No vulnerabilities found |
|
|
43
|
-
| Confidential data scan | Clean |
|
|
44
|
-
|
|
45
|
-
**Version**: 0.3.89
|
|
46
|
-
|
|
47
|
-
🤖 Generated with [Claude Code](https://claude.com/claude-code)
|
|
48
|
-
|
|
49
9
|
#### PR #130: Release v0.3.96: PTZ fisheye exclusion and API coverage docs
|
|
50
10
|
## Summary
|
|
51
11
|
- Add fisheye camera exclusion to PTZ type definitions, agent docs, AI reference docs, and vue-ptz example
|
|
@@ -72,22 +32,53 @@ v0.3.96
|
|
|
72
32
|
|
|
73
33
|
🤖 Generated with [Claude Code](https://claude.com/claude-code)
|
|
74
34
|
|
|
35
|
+
#### PR #134: release: v0.3.101 - multi-move-type PTZ support
|
|
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
|
+
|
|
43
|
+
## Version
|
|
44
|
+
`0.3.101`
|
|
45
|
+
|
|
46
|
+
## Commits
|
|
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
|
|
65
|
+
|
|
66
|
+
🤖 Generated with [Claude Code](https://claude.com/claude-code)
|
|
67
|
+
|
|
75
68
|
|
|
76
69
|
### Detailed Changes
|
|
77
70
|
|
|
78
|
-
####
|
|
79
|
-
-
|
|
80
|
-
- fix: exclude fisheye cameras from PTZ selector in vue-ptz example
|
|
81
|
-
- fix: address review findings for PTZ fisheye guidance
|
|
82
|
-
- fix: add PTZ sub-capability fields to Camera type and update docs
|
|
71
|
+
#### Features
|
|
72
|
+
- feat: add multi-move-type support to vue-ptz PositionInput
|
|
83
73
|
|
|
84
|
-
####
|
|
85
|
-
-
|
|
86
|
-
-
|
|
74
|
+
#### Bug Fixes
|
|
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
|
|
87
78
|
|
|
88
79
|
### Links
|
|
89
80
|
- [npm package](https://www.npmjs.com/package/een-api-toolkit)
|
|
90
|
-
- [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)
|
|
91
82
|
|
|
92
83
|
---
|
|
93
|
-
*Released: 2026-02-
|
|
84
|
+
*Released: 2026-02-23 18:17:46 CST*
|
package/docs/AI-CONTEXT.md
CHANGED
|
@@ -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;
|