@camera.ui/cli 0.0.48 → 0.0.50
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/LICENSE.md +1 -1
- package/README.md +10 -1
- package/dist/commands/artifacts.d.ts +9 -0
- package/dist/commands/artifacts.js +107 -0
- package/dist/commands/artifacts.js.map +1 -0
- package/dist/commands/bundle.d.ts +19 -1
- package/dist/commands/bundle.js +368 -56
- package/dist/commands/bundle.js.map +1 -1
- package/dist/commands/create.js +140 -93
- package/dist/commands/create.js.map +1 -1
- package/dist/commands/publish.js +77 -36
- package/dist/commands/publish.js.map +1 -1
- package/dist/index.js +11 -4
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +21 -5
- package/dist/utils/banners.d.ts +3 -1
- package/dist/utils/banners.js +15 -11
- package/dist/utils/banners.js.map +1 -1
- package/dist/utils/logger.d.ts +12 -24
- package/dist/utils/logger.js +57 -83
- package/dist/utils/logger.js.map +1 -1
- package/dist/utils/parser.d.ts +11 -4
- package/dist/utils/parser.js +27 -186
- package/dist/utils/parser.js.map +1 -1
- package/dist/utils/templates.d.ts +13 -1
- package/dist/utils/templates.js +148 -68
- package/dist/utils/templates.js.map +1 -1
- package/dist/utils/utils.js +13 -11
- package/dist/utils/utils.js.map +1 -1
- package/dist/utils/versions.js +1 -2
- package/dist/utils/versions.js.map +1 -1
- package/package.json +29 -43
- package/templates/base/README.md +2 -2
- package/templates/base/SECURITY.md +1 -1
- package/templates/base/package.json +2 -3
- package/templates/base/updates.config.js +1 -1
- package/templates/go/go.mod +5 -0
- package/templates/go/postinstall.js +35 -0
- package/templates/go/src/main.go +51 -0
- package/templates/python/requirements.txt +1 -1
- package/templates/python/ruff.toml +2 -2
- package/templates/python/src/main.py +299 -28
- package/templates/typescript/contract.ts +36 -0
- package/templates/typescript/eslint.config.js +13 -3
- package/templates/typescript/package.eslint.json +2 -2
- package/templates/typescript/package.json +4 -4
- package/templates/typescript/src/index.ts +96 -27
- package/templates/typescript/src/sensor.ts +207 -0
- package/CHANGELOG.md +0 -8
- package/CONTRIBUTING.md +0 -1
- package/scripts/chmod.js +0 -17
- package/templates/base/cameraui.config.js +0 -10
- package/templates/base/eslint.config.js +0 -64
- package/templates/base/src/index.js +0 -41
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
sdk "github.com/seydx/camera-ui-sdk"
|
|
5
|
+
)
|
|
6
|
+
|
|
7
|
+
// {{pluginName}}Plugin is the main plugin struct.
|
|
8
|
+
type {{pluginName}}Plugin struct {
|
|
9
|
+
logger *sdk.Logger
|
|
10
|
+
api *sdk.PluginAPI
|
|
11
|
+
storage *sdk.DeviceStorage
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// NewPlugin creates a new plugin instance.
|
|
15
|
+
// This is called by the SDK during plugin initialization.
|
|
16
|
+
func NewPlugin(logger *sdk.Logger, api *sdk.PluginAPI, storage *sdk.DeviceStorage) sdk.Plugin {
|
|
17
|
+
return &{{pluginName}}Plugin{
|
|
18
|
+
logger: logger,
|
|
19
|
+
api: api,
|
|
20
|
+
storage: storage,
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// ConfigureCameras is called on startup with all assigned cameras.
|
|
25
|
+
func (p *{{pluginName}}Plugin) ConfigureCameras(cameras []*sdk.CameraDevice) error {
|
|
26
|
+
p.logger.Log("Configuring cameras:", len(cameras))
|
|
27
|
+
|
|
28
|
+
for _, camera := range cameras {
|
|
29
|
+
if err := p.OnCameraAdded(camera); err != nil {
|
|
30
|
+
p.logger.Error("Failed to configure camera:", camera.Name(), err)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return nil
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// OnCameraAdded is called when a camera is added/assigned at runtime.
|
|
38
|
+
func (p *{{pluginName}}Plugin) OnCameraAdded(camera *sdk.CameraDevice) error {
|
|
39
|
+
p.logger.Log("Camera added:", camera.Name())
|
|
40
|
+
return nil
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// OnCameraReleased is called when a camera is removed/unassigned at runtime.
|
|
44
|
+
func (p *{{pluginName}}Plugin) OnCameraReleased(cameraID string) error {
|
|
45
|
+
p.logger.Log("Camera released:", cameraID)
|
|
46
|
+
return nil
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
func main() {
|
|
50
|
+
sdk.Run(NewPlugin)
|
|
51
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
camera-ui-
|
|
1
|
+
camera-ui-sdk==1.0.93
|
|
@@ -1,54 +1,325 @@
|
|
|
1
|
-
|
|
1
|
+
"""
|
|
2
|
+
Sample camera.ui Plugin (Python)
|
|
3
|
+
|
|
4
|
+
Demonstrates the camera.ui plugin architecture with sensors:
|
|
5
|
+
- MotionSensor: External motion events (webhooks, ONVIF, etc.)
|
|
6
|
+
- LightControl: Controllable light with on/brightness
|
|
7
|
+
- ClassifierDetectorSensor: Multi-provider frame-based classification
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from typing import TYPE_CHECKING, Any
|
|
13
|
+
|
|
14
|
+
from camera_ui_sdk import (
|
|
15
|
+
API_EVENT,
|
|
16
|
+
BasePlugin,
|
|
2
17
|
CameraDevice,
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
18
|
+
ClassifierDetectorSensor,
|
|
19
|
+
ClassifierResult,
|
|
20
|
+
Detection,
|
|
21
|
+
DeviceStorage,
|
|
22
|
+
JsonSchema,
|
|
23
|
+
LightControl,
|
|
6
24
|
LoggerService,
|
|
25
|
+
ModelSpec,
|
|
26
|
+
MotionSensor,
|
|
7
27
|
PluginAPI,
|
|
28
|
+
VideoFrameData,
|
|
8
29
|
)
|
|
9
30
|
|
|
31
|
+
if TYPE_CHECKING:
|
|
32
|
+
from camera_ui_sdk import SensorType
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# ============ MOTION SENSOR (External Events) ============
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class ExampleMotionSensor(MotionSensor):
|
|
39
|
+
"""
|
|
40
|
+
Example motion sensor for external triggers.
|
|
41
|
+
|
|
42
|
+
Use this for external motion sources:
|
|
43
|
+
- ONVIF camera events
|
|
44
|
+
- SMTP notifications
|
|
45
|
+
- Webhook triggers
|
|
46
|
+
- API polling
|
|
47
|
+
|
|
48
|
+
For frame-based detection, extend MotionDetectorSensor instead.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
def __init__(self, name: str) -> None:
|
|
52
|
+
super().__init__(name)
|
|
53
|
+
|
|
54
|
+
def trigger(self, detections: list[Detection] | None = None) -> None:
|
|
55
|
+
"""Trigger motion from external event."""
|
|
56
|
+
self.detected = True
|
|
57
|
+
self.detections = detections or []
|
|
58
|
+
|
|
59
|
+
def reset(self) -> None:
|
|
60
|
+
"""Clear motion state."""
|
|
61
|
+
self.detected = False
|
|
62
|
+
self.detections = []
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
# ============ LIGHT CONTROL ============
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class ExampleLightControl(LightControl):
|
|
69
|
+
"""
|
|
70
|
+
Example light control sensor.
|
|
71
|
+
|
|
72
|
+
Bidirectional control sensor - consumers can read and write state.
|
|
73
|
+
Implements setOn/setBrightness to handle state changes.
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
def __init__(self, camera: CameraDevice, name: str = "Light") -> None:
|
|
77
|
+
super().__init__(name)
|
|
78
|
+
self._camera = camera
|
|
79
|
+
|
|
80
|
+
# Initialize state
|
|
81
|
+
self.on = False
|
|
82
|
+
self.brightness = 100
|
|
83
|
+
|
|
84
|
+
# Log state changes
|
|
85
|
+
self.onPropertyChanged.subscribe(
|
|
86
|
+
lambda event: self._camera.logger.debug(f"{self.name}: {event['property']} = {event['value']}")
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
def storage_schema(self) -> list[JsonSchema]:
|
|
91
|
+
return [
|
|
92
|
+
{
|
|
93
|
+
"type": "number",
|
|
94
|
+
"key": "defaultBrightness",
|
|
95
|
+
"title": "Default Brightness",
|
|
96
|
+
"description": "Default brightness level (0-100)",
|
|
97
|
+
"defaultValue": 100,
|
|
98
|
+
"minimum": 0,
|
|
99
|
+
"maximum": 100,
|
|
100
|
+
"store": True,
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
"type": "boolean",
|
|
104
|
+
"key": "autoOff",
|
|
105
|
+
"title": "Auto-Off",
|
|
106
|
+
"description": "Automatically turn off after timeout",
|
|
107
|
+
"defaultValue": False,
|
|
108
|
+
"store": True,
|
|
109
|
+
},
|
|
110
|
+
]
|
|
111
|
+
|
|
112
|
+
async def setOn(self, value: bool) -> None:
|
|
113
|
+
"""Called when consumer sets 'on' property."""
|
|
114
|
+
self._camera.logger.log(f"Light turned {'ON' if value else 'OFF'}")
|
|
115
|
+
self.on = value
|
|
116
|
+
|
|
117
|
+
# Apply default brightness when turning on
|
|
118
|
+
if value and self.storage:
|
|
119
|
+
default_brightness = self.storage.values.get("defaultBrightness", 100)
|
|
120
|
+
self.brightness = default_brightness
|
|
121
|
+
|
|
122
|
+
async def setBrightness(self, value: int) -> None:
|
|
123
|
+
"""Called when consumer sets 'brightness' property."""
|
|
124
|
+
self._camera.logger.log(f"Light brightness: {value}%")
|
|
125
|
+
self.brightness = value
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
# ============ CLASSIFIER (Multi-Provider Example) ============
|
|
129
|
+
|
|
10
130
|
|
|
11
|
-
class
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
131
|
+
class ExampleClassifier(ClassifierDetectorSensor[dict[str, Any]]):
|
|
132
|
+
"""
|
|
133
|
+
Example classifier sensor.
|
|
134
|
+
|
|
135
|
+
Multi-provider sensor: Multiple classifiers can be registered per camera.
|
|
136
|
+
Example use cases:
|
|
137
|
+
- Bird species classifier (triggers on 'bird' from object detection)
|
|
138
|
+
- Dog breed classifier (triggers on 'dog')
|
|
139
|
+
- Plant species classifier
|
|
140
|
+
|
|
141
|
+
The DetectionCoordinator calls detectClassifications() when triggerLabels are detected.
|
|
142
|
+
"""
|
|
143
|
+
|
|
144
|
+
def __init__(self, camera: CameraDevice, name: str = "Classifier") -> None:
|
|
145
|
+
super().__init__(name)
|
|
146
|
+
self._camera = camera
|
|
147
|
+
|
|
148
|
+
@property
|
|
149
|
+
def storage_schema(self) -> list[JsonSchema]:
|
|
150
|
+
return [
|
|
151
|
+
{
|
|
152
|
+
"type": "number",
|
|
153
|
+
"key": "confidenceThreshold",
|
|
154
|
+
"title": "Confidence Threshold",
|
|
155
|
+
"description": "Minimum confidence for classifications (0-1)",
|
|
156
|
+
"defaultValue": 0.5,
|
|
157
|
+
"minimum": 0.1,
|
|
158
|
+
"maximum": 1.0,
|
|
159
|
+
"step": 0.05,
|
|
160
|
+
"store": True,
|
|
161
|
+
},
|
|
162
|
+
]
|
|
163
|
+
|
|
164
|
+
@property
|
|
165
|
+
def modelSpec(self) -> ModelSpec:
|
|
166
|
+
"""
|
|
167
|
+
Model specification.
|
|
168
|
+
|
|
169
|
+
- input: Frame size and format expected by the model
|
|
170
|
+
- outputLabels: Labels this classifier can output
|
|
171
|
+
- triggerLabels: Object labels that trigger classification
|
|
172
|
+
"""
|
|
173
|
+
return {
|
|
174
|
+
"input": {
|
|
175
|
+
"width": 224,
|
|
176
|
+
"height": 224,
|
|
177
|
+
"format": "rgb",
|
|
178
|
+
},
|
|
179
|
+
# Trigger when object detection finds these labels
|
|
180
|
+
"triggerLabels": ["animal"],
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async def detectClassifications(
|
|
184
|
+
self,
|
|
185
|
+
frame: VideoFrameData,
|
|
186
|
+
triggerRegions: list[Detection] | None = None, # noqa: ARG002
|
|
187
|
+
) -> ClassifierResult:
|
|
188
|
+
"""
|
|
189
|
+
Classify objects in a frame.
|
|
190
|
+
|
|
191
|
+
Called by DetectionCoordinator when triggerLabels are detected.
|
|
192
|
+
The frame is pre-scaled to modelSpec.input dimensions.
|
|
193
|
+
"""
|
|
194
|
+
threshold = 0.5
|
|
195
|
+
if self.storage:
|
|
196
|
+
threshold = self.storage.values.get("confidenceThreshold", 0.5)
|
|
197
|
+
|
|
198
|
+
# TODO: Implement your classification model here
|
|
199
|
+
# Example: Load TensorFlow model and run inference
|
|
200
|
+
#
|
|
201
|
+
# predictions = await self.model.classify(frame["data"])
|
|
202
|
+
# return {
|
|
203
|
+
# "detected": len(predictions) > 0,
|
|
204
|
+
# "detections": [
|
|
205
|
+
# {
|
|
206
|
+
# "label": p.label,
|
|
207
|
+
# "confidence": p.score,
|
|
208
|
+
# "box": triggerRegions[0]["box"] if triggerRegions else {"x": 0, "y": 0, "width": 1, "height": 1},
|
|
209
|
+
# }
|
|
210
|
+
# for p in predictions
|
|
211
|
+
# ],
|
|
212
|
+
# }
|
|
213
|
+
|
|
214
|
+
self._camera.logger.debug(
|
|
215
|
+
f"Classifying frame {frame['width']}x{frame['height']}, threshold: {threshold}"
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
# Return empty result (placeholder)
|
|
219
|
+
return {
|
|
220
|
+
"detected": False,
|
|
221
|
+
"detections": [],
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async def destroy(self) -> None:
|
|
225
|
+
"""Cleanup when sensor is destroyed."""
|
|
226
|
+
# Release model resources if needed
|
|
227
|
+
pass
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
# ============ PLUGIN ============
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
class SamplePlugin(BasePlugin):
|
|
234
|
+
"""
|
|
235
|
+
Sample plugin demonstrating camera.ui SDK usage.
|
|
236
|
+
|
|
237
|
+
The contract (provides/consumes) is defined in contract.ts.
|
|
238
|
+
"""
|
|
239
|
+
|
|
240
|
+
def __init__(self, logger: LoggerService, api: PluginAPI, storage: DeviceStorage[Any]) -> None:
|
|
241
|
+
super().__init__(logger, api, storage)
|
|
242
|
+
|
|
243
|
+
# Maps to track cameras and sensors
|
|
15
244
|
self.cameras: dict[str, CameraDevice] = {}
|
|
16
|
-
self.
|
|
245
|
+
self.motion_sensors: dict[str, ExampleMotionSensor] = {}
|
|
246
|
+
self.light_controls: dict[str, ExampleLightControl] = {}
|
|
17
247
|
|
|
18
|
-
|
|
19
|
-
self.api.on(
|
|
248
|
+
# Register lifecycle event handlers
|
|
249
|
+
self.api.on(API_EVENT.FINISH_LAUNCHING, self._on_finish_launching)
|
|
250
|
+
self.api.on(API_EVENT.SHUTDOWN, self._on_shutdown)
|
|
20
251
|
|
|
21
|
-
|
|
22
|
-
|
|
252
|
+
async def configureCameras(self, cameraDevices: list[CameraDevice]) -> None:
|
|
253
|
+
"""
|
|
254
|
+
Configure cameras at startup.
|
|
255
|
+
Called for cameras already assigned to this plugin.
|
|
256
|
+
"""
|
|
257
|
+
for camera in cameraDevices:
|
|
258
|
+
await self._setup_camera(camera)
|
|
23
259
|
|
|
24
|
-
async def
|
|
260
|
+
async def onCameraAdded(self, camera: CameraDevice, _sensor_type: SensorType | None = None) -> None:
|
|
261
|
+
"""Called when a camera is selected for this plugin at runtime."""
|
|
25
262
|
self.logger.log(f"Camera selected: {camera.name}")
|
|
263
|
+
await self._setup_camera(camera)
|
|
264
|
+
|
|
265
|
+
async def onCameraReleased(self, cameraId: str) -> None:
|
|
266
|
+
"""Called when a camera is deselected from this plugin."""
|
|
267
|
+
camera = self.cameras.get(cameraId)
|
|
268
|
+
if not camera:
|
|
269
|
+
return
|
|
26
270
|
|
|
271
|
+
self.logger.log(f"Camera deselected: {camera.name}")
|
|
272
|
+
|
|
273
|
+
# Remove sensors
|
|
274
|
+
motion = self.motion_sensors.get(cameraId)
|
|
275
|
+
if motion:
|
|
276
|
+
await camera.removeSensor(motion.id)
|
|
277
|
+
del self.motion_sensors[cameraId]
|
|
278
|
+
|
|
279
|
+
light = self.light_controls.get(cameraId)
|
|
280
|
+
if light:
|
|
281
|
+
await camera.removeSensor(light.id)
|
|
282
|
+
del self.light_controls[cameraId]
|
|
283
|
+
|
|
284
|
+
del self.cameras[cameraId]
|
|
285
|
+
|
|
286
|
+
async def _setup_camera(self, camera: CameraDevice) -> None:
|
|
287
|
+
"""Set up sensors for a camera."""
|
|
27
288
|
if camera.id in self.cameras:
|
|
28
289
|
return
|
|
29
290
|
|
|
30
291
|
self.cameras[camera.id] = camera
|
|
31
292
|
|
|
32
|
-
|
|
33
|
-
|
|
293
|
+
# Create motion sensor
|
|
294
|
+
motion = ExampleMotionSensor(f"Motion - {camera.name}")
|
|
295
|
+
self.motion_sensors[camera.id] = motion
|
|
296
|
+
await camera.addSensor(motion)
|
|
34
297
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
298
|
+
# Create light control
|
|
299
|
+
light = ExampleLightControl(camera, f"Light - {camera.name}")
|
|
300
|
+
self.light_controls[camera.id] = light
|
|
301
|
+
await camera.addSensor(light)
|
|
38
302
|
|
|
39
|
-
|
|
40
|
-
self.logger.log("Finished launching plugin")
|
|
303
|
+
self.logger.log(f"Sensors registered for {camera.name}")
|
|
41
304
|
|
|
42
|
-
|
|
43
|
-
|
|
305
|
+
# Example: Trigger motion after 5 seconds (for testing)
|
|
306
|
+
# import asyncio
|
|
307
|
+
# asyncio.get_event_loop().call_later(5, motion.trigger)
|
|
308
|
+
|
|
309
|
+
def _on_finish_launching(self) -> None:
|
|
310
|
+
"""Called when the plugin has finished launching."""
|
|
311
|
+
self.logger.log("Plugin started")
|
|
44
312
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
313
|
+
def _on_shutdown(self) -> None:
|
|
314
|
+
"""Called when camera.ui is shutting down."""
|
|
315
|
+
self.logger.log("Shutting down plugin")
|
|
48
316
|
|
|
49
|
-
|
|
50
|
-
|
|
317
|
+
# Cleanup all sensors
|
|
318
|
+
self.motion_sensors.clear()
|
|
319
|
+
self.light_controls.clear()
|
|
320
|
+
self.cameras.clear()
|
|
51
321
|
|
|
52
322
|
|
|
53
323
|
def __main__() -> type[SamplePlugin]:
|
|
324
|
+
"""Plugin entry point - returns the plugin class."""
|
|
54
325
|
return SamplePlugin
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { PluginRole, SensorType } from '@camera.ui/sdk';
|
|
2
|
+
|
|
3
|
+
import type { PluginContract } from '@camera.ui/sdk';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Plugin Contract
|
|
7
|
+
*
|
|
8
|
+
* Defines what sensors this plugin provides and consumes.
|
|
9
|
+
* This is used by camera.ui to determine plugin compatibility.
|
|
10
|
+
*/
|
|
11
|
+
export const contract: PluginContract = {
|
|
12
|
+
// Plugin display name
|
|
13
|
+
name: 'Sample Plugin',
|
|
14
|
+
|
|
15
|
+
// Plugin role - what this plugin does
|
|
16
|
+
role: PluginRole.SensorProvider,
|
|
17
|
+
|
|
18
|
+
// Sensors this plugin provides (add the sensor types you implement)
|
|
19
|
+
provides: [
|
|
20
|
+
SensorType.Motion,
|
|
21
|
+
SensorType.Light,
|
|
22
|
+
SensorType.Classifier, // Multi-provider: multiple classifiers per camera
|
|
23
|
+
// SensorType.Object, // Object detection
|
|
24
|
+
// SensorType.Battery, // Battery level
|
|
25
|
+
// SensorType.Doorbell, // Doorbell trigger
|
|
26
|
+
// SensorType.Contact, // Contact sensor
|
|
27
|
+
// SensorType.Siren, // Siren control
|
|
28
|
+
// SensorType.Switch, // Switch control
|
|
29
|
+
// SensorType.SecuritySystem, // Security system
|
|
30
|
+
],
|
|
31
|
+
|
|
32
|
+
// Sensors this plugin consumes from other plugins (empty if standalone)
|
|
33
|
+
consumes: [],
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export default contract;
|
|
@@ -42,7 +42,6 @@ export default [
|
|
|
42
42
|
'@typescript-eslint/consistent-type-imports': 'error',
|
|
43
43
|
'@typescript-eslint/await-thenable': 'error',
|
|
44
44
|
'@typescript-eslint/no-explicit-any': 'off',
|
|
45
|
-
'@typescript-eslint/no-unused-vars': ['error'],
|
|
46
45
|
'@typescript-eslint/no-unsafe-assignment': 'off',
|
|
47
46
|
'@typescript-eslint/no-unsafe-member-access': 'off',
|
|
48
47
|
'@typescript-eslint/no-unsafe-argument': 'off',
|
|
@@ -90,14 +89,25 @@ export default [
|
|
|
90
89
|
'eol-last': ['error', 'always'],
|
|
91
90
|
'space-before-function-paren': ['error', { named: 'never' }],
|
|
92
91
|
|
|
93
|
-
'no-unused-vars':
|
|
92
|
+
'@typescript-eslint/no-unused-vars': [
|
|
93
|
+
'error',
|
|
94
|
+
{
|
|
95
|
+
args: 'all',
|
|
96
|
+
argsIgnorePattern: '^_',
|
|
97
|
+
caughtErrors: 'all',
|
|
98
|
+
caughtErrorsIgnorePattern: '^_',
|
|
99
|
+
destructuredArrayIgnorePattern: '^_',
|
|
100
|
+
varsIgnorePattern: '^_',
|
|
101
|
+
ignoreRestSiblings: true,
|
|
102
|
+
},
|
|
103
|
+
],
|
|
94
104
|
'no-case-declarations': 'off',
|
|
95
105
|
'no-async-promise-executor': 'off',
|
|
96
106
|
'no-control-regex': 'off',
|
|
97
107
|
},
|
|
98
108
|
},
|
|
99
109
|
{
|
|
100
|
-
files: ['**/*.js', '**/*.cjs', '**/*.mjs', '*.d.ts', '*.config.ts'],
|
|
110
|
+
files: ['**/*.js', '**/*.cjs', '**/*.mjs', '*.d.ts', '*.config.ts', 'contract.ts'],
|
|
101
111
|
...tsLint.configs.disableTypeChecked,
|
|
102
112
|
},
|
|
103
113
|
];
|
|
@@ -1,46 +1,115 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { API_EVENT, BasePlugin } from '@camera.ui/sdk';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
public logger: LoggerService;
|
|
5
|
-
public api: PluginAPI;
|
|
3
|
+
import { ExampleLightControl, ExampleMotionSensor } from './sensor.js';
|
|
6
4
|
|
|
5
|
+
import type { CameraDevice, DeviceStorage, LightControl, LoggerService, MotionSensor, PluginAPI } from '@camera.ui/sdk';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Sample Plugin
|
|
9
|
+
*
|
|
10
|
+
* Demonstrates the camera.ui plugin architecture with sensors:
|
|
11
|
+
* - MotionSensor: External motion events (webhooks, ONVIF, etc.)
|
|
12
|
+
* - LightControl: Controllable light with on/brightness
|
|
13
|
+
*
|
|
14
|
+
* The contract (provides/consumes) is defined in contract.ts.
|
|
15
|
+
*/
|
|
16
|
+
export default class SamplePlugin extends BasePlugin {
|
|
17
|
+
// Maps to track cameras and sensors
|
|
7
18
|
private cameras = new Map<string, CameraDevice>();
|
|
19
|
+
private motionSensors = new Map<string, MotionSensor>();
|
|
20
|
+
private lightControls = new Map<string, LightControl>();
|
|
21
|
+
|
|
22
|
+
constructor(logger: LoggerService, api: PluginAPI, storage: DeviceStorage<any>) {
|
|
23
|
+
super(logger, api, storage);
|
|
24
|
+
|
|
25
|
+
// Register lifecycle event handlers
|
|
26
|
+
this.api.on(API_EVENT.FINISH_LAUNCHING, this.onFinishLaunching.bind(this));
|
|
27
|
+
this.api.on(API_EVENT.SHUTDOWN, this.onShutdown.bind(this));
|
|
28
|
+
}
|
|
8
29
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
30
|
+
/**
|
|
31
|
+
* Configure cameras at startup.
|
|
32
|
+
* Called for cameras already assigned to this plugin.
|
|
33
|
+
*/
|
|
34
|
+
public async configureCameras(cameras: CameraDevice[]): Promise<void> {
|
|
35
|
+
for (const camera of cameras) {
|
|
36
|
+
await this.setupCamera(camera);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Called when a camera is selected for this plugin at runtime.
|
|
42
|
+
*/
|
|
43
|
+
public async onCameraAdded(camera: CameraDevice): Promise<void> {
|
|
44
|
+
this.logger.log('Camera selected:', camera.name);
|
|
45
|
+
await this.setupCamera(camera);
|
|
46
|
+
}
|
|
12
47
|
|
|
13
|
-
|
|
14
|
-
|
|
48
|
+
/**
|
|
49
|
+
* Called when a camera is deselected from this plugin.
|
|
50
|
+
*/
|
|
51
|
+
public async onCameraReleased(cameraId: string): Promise<void> {
|
|
52
|
+
const camera = this.cameras.get(cameraId);
|
|
53
|
+
if (!camera) return;
|
|
15
54
|
|
|
16
|
-
this.
|
|
17
|
-
this.logger.log('Camera selected:', cameraDevice.name);
|
|
55
|
+
this.logger.log('Camera deselected:', camera.name);
|
|
18
56
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
57
|
+
// Remove sensors
|
|
58
|
+
const motion = this.motionSensors.get(cameraId);
|
|
59
|
+
if (motion) {
|
|
60
|
+
await camera.removeSensor(motion.id);
|
|
61
|
+
this.motionSensors.delete(cameraId);
|
|
62
|
+
}
|
|
23
63
|
|
|
24
|
-
this.
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
64
|
+
const light = this.lightControls.get(cameraId);
|
|
65
|
+
if (light) {
|
|
66
|
+
await camera.removeSensor(light.id);
|
|
67
|
+
this.lightControls.delete(cameraId);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
this.cameras.delete(cameraId);
|
|
31
71
|
}
|
|
32
72
|
|
|
33
|
-
|
|
73
|
+
/**
|
|
74
|
+
* Set up sensors for a camera.
|
|
75
|
+
*/
|
|
76
|
+
private async setupCamera(camera: CameraDevice): Promise<void> {
|
|
77
|
+
if (this.cameras.has(camera.id)) return;
|
|
78
|
+
|
|
79
|
+
this.cameras.set(camera.id, camera);
|
|
80
|
+
|
|
81
|
+
// Create motion sensor
|
|
82
|
+
const motion = new ExampleMotionSensor(`Motion - ${camera.name}`);
|
|
83
|
+
this.motionSensors.set(camera.id, motion);
|
|
84
|
+
await camera.addSensor(motion);
|
|
34
85
|
|
|
35
|
-
|
|
36
|
-
|
|
86
|
+
// Create light control
|
|
87
|
+
const light = new ExampleLightControl(camera, `Light - ${camera.name}`);
|
|
88
|
+
this.lightControls.set(camera.id, light);
|
|
89
|
+
await camera.addSensor(light);
|
|
90
|
+
|
|
91
|
+
this.logger.log(`Sensors registered for ${camera.name}`);
|
|
92
|
+
|
|
93
|
+
// Example: Trigger motion after 5 seconds (for testing)
|
|
94
|
+
// setTimeout(() => motion.trigger(), 5000);
|
|
37
95
|
}
|
|
38
96
|
|
|
39
|
-
|
|
97
|
+
/**
|
|
98
|
+
* Called when the plugin has finished launching.
|
|
99
|
+
*/
|
|
100
|
+
private onFinishLaunching(): void {
|
|
40
101
|
this.logger.log('Plugin started');
|
|
41
102
|
}
|
|
42
103
|
|
|
43
|
-
|
|
104
|
+
/**
|
|
105
|
+
* Called when camera.ui is shutting down.
|
|
106
|
+
*/
|
|
107
|
+
private async onShutdown(): Promise<void> {
|
|
44
108
|
this.logger.log('Shutting down plugin');
|
|
109
|
+
|
|
110
|
+
// Cleanup all sensors
|
|
111
|
+
this.motionSensors.clear();
|
|
112
|
+
this.lightControls.clear();
|
|
113
|
+
this.cameras.clear();
|
|
45
114
|
}
|
|
46
115
|
}
|