@camera.ui/cli 0.0.49 → 0.0.51
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 +116 -0
- package/dist/commands/artifacts.js.map +1 -0
- package/dist/commands/bundle.d.ts +34 -1
- package/dist/commands/bundle.js +402 -57
- 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 +95 -39
- package/dist/commands/publish.js.map +1 -1
- package/dist/index.js +13 -4
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +24 -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 -185
- 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 +85 -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,85 @@
|
|
|
1
|
+
// postinstall.js — generated by @camera.ui/cli
|
|
2
|
+
const { platform, arch } = require('os');
|
|
3
|
+
const { existsSync, copyFileSync, mkdirSync, chmodSync, readFileSync } = require('fs');
|
|
4
|
+
const { join, dirname } = require('path');
|
|
5
|
+
|
|
6
|
+
const os = platform();
|
|
7
|
+
const a = arch();
|
|
8
|
+
// Map Node's arch/platform names back to Go's so the right platform package
|
|
9
|
+
// resolves. Most match; the one exception is x64->amd64.
|
|
10
|
+
const goArch = a === 'x64' ? 'amd64' : a;
|
|
11
|
+
const goos = os === 'win32' ? 'windows' : os; // 'linux'/'darwin' match as-is
|
|
12
|
+
const ext = os === 'win32' ? '.exe' : '';
|
|
13
|
+
|
|
14
|
+
// On Linux, glibc and musl builds ship as separate platform packages (the musl
|
|
15
|
+
// one carries a `-musl` suffix). Detect musl so the matching binary is chosen —
|
|
16
|
+
// a glibc binary can't exec on Alpine/musl and vice versa.
|
|
17
|
+
function isMusl() {
|
|
18
|
+
if (os !== 'linux') return false;
|
|
19
|
+
// 1) The process report lists the runtime's shared objects + glibc version.
|
|
20
|
+
try {
|
|
21
|
+
if (process.report && typeof process.report.getReport === 'function') {
|
|
22
|
+
process.report.excludeNetwork = true;
|
|
23
|
+
const rep = process.report.getReport();
|
|
24
|
+
if (rep && rep.header && rep.header.glibcVersionRuntime) return false;
|
|
25
|
+
if (rep && Array.isArray(rep.sharedObjects) && rep.sharedObjects.some((f) => f.includes('libc.musl-') || f.includes('ld-musl-'))) {
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
} catch {
|
|
30
|
+
// fall through to the ldd-based checks
|
|
31
|
+
}
|
|
32
|
+
// 2) The dynamic loader on musl systems references musl.
|
|
33
|
+
try {
|
|
34
|
+
if (readFileSync('/usr/bin/ldd', 'utf-8').includes('musl')) return true;
|
|
35
|
+
} catch {
|
|
36
|
+
// /usr/bin/ldd may not exist — try invoking ldd directly
|
|
37
|
+
}
|
|
38
|
+
// 3) Last resort: ask ldd for its version banner.
|
|
39
|
+
try {
|
|
40
|
+
if (require('child_process').execSync('ldd --version 2>&1', { encoding: 'utf-8' }).includes('musl')) return true;
|
|
41
|
+
} catch {
|
|
42
|
+
// give up — assume glibc
|
|
43
|
+
}
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const key = goos + '-' + goArch + (isMusl() ? '-musl' : '');
|
|
48
|
+
|
|
49
|
+
const pkg = require('./package.json');
|
|
50
|
+
const pluginName = pkg.name.replace(/^@[^/]+\//, '');
|
|
51
|
+
const scope = pkg.name.includes('/') ? pkg.name.split('/')[0] : undefined;
|
|
52
|
+
const platformPkgName = scope ? scope + '/' + pluginName + '-' + key : pluginName + '-' + key;
|
|
53
|
+
|
|
54
|
+
let binarySource;
|
|
55
|
+
try {
|
|
56
|
+
const pkgDir = dirname(require.resolve(platformPkgName + '/package.json'));
|
|
57
|
+
binarySource = join(pkgDir, pluginName + ext);
|
|
58
|
+
} catch {
|
|
59
|
+
// The matching platform package isn't installed. Either this platform isn't
|
|
60
|
+
// supported, or npm skipped the optional dependency (a known npm bug:
|
|
61
|
+
// https://github.com/npm/cli/issues/4828). Warn but don't fail the install.
|
|
62
|
+
console.warn(
|
|
63
|
+
'[' +
|
|
64
|
+
pluginName +
|
|
65
|
+
'] no native binary for ' +
|
|
66
|
+
key +
|
|
67
|
+
' (expected package ' +
|
|
68
|
+
platformPkgName +
|
|
69
|
+
').\n' +
|
|
70
|
+
' If your platform should be supported, remove node_modules + package-lock.json and reinstall.',
|
|
71
|
+
);
|
|
72
|
+
process.exit(0);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (!existsSync(binarySource)) process.exit(0);
|
|
76
|
+
|
|
77
|
+
const binDir = join(__dirname, 'dist', 'bin');
|
|
78
|
+
mkdirSync(binDir, { recursive: true });
|
|
79
|
+
const target = join(binDir, 'plugin' + ext);
|
|
80
|
+
copyFileSync(binarySource, target);
|
|
81
|
+
try {
|
|
82
|
+
chmodSync(target, 0o755);
|
|
83
|
+
} catch {
|
|
84
|
+
// Ignore chmod errors on platforms that don't support it (e.g. Windows)
|
|
85
|
+
}
|
|
@@ -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
|
];
|