@camstack/addon-matter-broker 0.2.71 → 0.2.73
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/dist/addon.js +169 -1
- package/dist/addon.mjs +169 -1
- package/package.json +1 -1
package/dist/addon.js
CHANGED
|
@@ -6459,6 +6459,21 @@ var CameraStreamSchema = object({
|
|
|
6459
6459
|
*/
|
|
6460
6460
|
autoEligible: boolean().optional(),
|
|
6461
6461
|
/**
|
|
6462
|
+
* The profile slot the PUBLISHER says this stream is: `low` for a stream
|
|
6463
|
+
* whose only sensible role is the low-bandwidth one, `high` for a main
|
|
6464
|
+
* stream. Consulted by the broker's automatic ranking
|
|
6465
|
+
* (`rankInitialAssignment`) BEFORE the pixel-count heuristic, and only there:
|
|
6466
|
+
* an operator's manual assignment still wins, and an absent hint is the
|
|
6467
|
+
* pixel-ranked default every camera had before this field existed.
|
|
6468
|
+
*
|
|
6469
|
+
* Why it exists: a camera with ONE stream ranked to `mid` — the slot no
|
|
6470
|
+
* storage class is written for on its own (`recordings` is high|mid,
|
|
6471
|
+
* `recordingsLow` is low) and the one profile scrub does not read. The robot
|
|
6472
|
+
* camera's single 720p relay is a `low` in every respect except the slot the
|
|
6473
|
+
* heuristic put it in.
|
|
6474
|
+
*/
|
|
6475
|
+
profileHint: CamProfileSchema.optional(),
|
|
6476
|
+
/**
|
|
6462
6477
|
* Transport-specific opaque metadata. The broker passes it through to
|
|
6463
6478
|
* the source reader without inspecting it. Currently used by
|
|
6464
6479
|
* `pull-rfc4571` streams to carry the upstream SDP (so the reader can
|
|
@@ -7908,7 +7923,22 @@ var RecordingTriggersSchema = object({
|
|
|
7908
7923
|
* il livello: un contatto trovato già aperto al riavvio del runner non fa
|
|
7909
7924
|
* registrare.
|
|
7910
7925
|
*/
|
|
7911
|
-
sensorDeviceIds: array(number().int().positive()).min(1).max(16).refine(noDuplicates, { message: "sensorDeviceIds must not repeat a device" }).optional()
|
|
7926
|
+
sensorDeviceIds: array(number().int().positive()).min(1).max(16).refine(noDuplicates, { message: "sensorDeviceIds must not repeat a device" }).optional(),
|
|
7927
|
+
/**
|
|
7928
|
+
* La camera registra per tutta la durata del SEGNALE che lei stessa
|
|
7929
|
+
* pubblica — la cap `recording-signal` (`active: true` mentre il device è in
|
|
7930
|
+
* funzione: un robot che pulisce). È un LIVELLO, non un fronte: la finestra
|
|
7931
|
+
* resta aperta finché il segnale è alto e si chiude `postBufferSec` dopo la
|
|
7932
|
+
* caduta, così una pulizia di quaranta minuti è UNA clip.
|
|
7933
|
+
*
|
|
7934
|
+
* Non nomina un device: il segnale è della camera stessa, tenuto fresco dal
|
|
7935
|
+
* suo provider (D224); il recorder lo ascolta su `DeviceStateChanged` e lo
|
|
7936
|
+
* riconcilia leggendo la slice via RPC, con un tetto oltre il quale un
|
|
7937
|
+
* segnale mai abbassato (un evento perso) non può tenere aperta una
|
|
7938
|
+
* registrazione (D11). Vedi `recorder/addon/band-decision.ts`
|
|
7939
|
+
* (`holdTrigger` / `releaseTrigger`).
|
|
7940
|
+
*/
|
|
7941
|
+
deviceSignal: boolean().optional()
|
|
7912
7942
|
});
|
|
7913
7943
|
/**
|
|
7914
7944
|
* Mode of a single recording band — the recorder per-band vocabulary.
|
|
@@ -11421,6 +11451,8 @@ method(object({
|
|
|
11421
11451
|
label: string$2().optional(),
|
|
11422
11452
|
deviceFeatures: array(string$2()).optional(),
|
|
11423
11453
|
autoEligible: boolean().optional(),
|
|
11454
|
+
/** The slot the publisher says this stream IS — see `CameraStreamSchema.profileHint`. */
|
|
11455
|
+
profileHint: CamProfileSchema.optional(),
|
|
11424
11456
|
metadata: record(string$2(), unknown()).optional(),
|
|
11425
11457
|
/** Required when kind === 'derived' — the source camStreamId the
|
|
11426
11458
|
* derived broker reads its encoded plane from.
|
|
@@ -29980,6 +30012,77 @@ method(object({
|
|
|
29980
30012
|
auth: "protected"
|
|
29981
30013
|
});
|
|
29982
30014
|
/**
|
|
30015
|
+
* A camera's own "record me NOW" LEVEL — a signal the device raises while
|
|
30016
|
+
* something it knows about is happening (a robot vacuum cleaning, a machine
|
|
30017
|
+
* running, a gate open) and lowers when it stops.
|
|
30018
|
+
*
|
|
30019
|
+
* It is a level, not an edge. The sensor triggers the recorder already listens
|
|
30020
|
+
* to (`SOURCE_CAP_ACTIVE_FIELD`) fire on a rising edge and record for a fixed
|
|
30021
|
+
* post-buffer; a cleaning lasts forty minutes, and forty minutes of "this is
|
|
30022
|
+
* still happening" cannot be expressed as an edge plus a post-buffer without
|
|
30023
|
+
* either re-triggering on a timer (polling — D224 forbids the consumer keeping
|
|
30024
|
+
* the fact fresh) or picking a post-buffer long enough to cover every job,
|
|
30025
|
+
* which records the dock for that long after every short one. So the recorder
|
|
30026
|
+
* reads the LEVEL: it records for as long as `active` is true, plus the band's
|
|
30027
|
+
* own post-buffer once it falls.
|
|
30028
|
+
*
|
|
30029
|
+
* Who owns the fact: the PROVIDER of the device, which already reads the state
|
|
30030
|
+
* the signal is derived from (the vacuum's lifecycle state, for Dreame). It
|
|
30031
|
+
* writes this slice on every change; consumers subscribe to the kernel's
|
|
30032
|
+
* `DeviceStateChanged` for it and read it back through
|
|
30033
|
+
* `deviceState.getCapSlice` to reconcile — never a second copy, never a poll
|
|
30034
|
+
* against the device (D224, D8/D11).
|
|
30035
|
+
*
|
|
30036
|
+
* Deliberately NOT a `binary` sensor on the camera: a binary sensor is exported
|
|
30037
|
+
* to Home Assistant, HomeKit and the sensor picker as a thing an operator can
|
|
30038
|
+
* bind, and "the camera wants to record" is not a room sensor. It is a fact
|
|
30039
|
+
* about the camera, on the camera.
|
|
30040
|
+
*/
|
|
30041
|
+
var RecordingSignalStatusSchema = object({
|
|
30042
|
+
/** True while the device asks to be recorded. */
|
|
30043
|
+
active: boolean(),
|
|
30044
|
+
/**
|
|
30045
|
+
* Why, in the provider's own vocabulary (a vacuum's `cleaning` / `docked`,
|
|
30046
|
+
* …). Free text for the log line and the UI badge; the recorder never
|
|
30047
|
+
* branches on it.
|
|
30048
|
+
*/
|
|
30049
|
+
reason: string$2(),
|
|
30050
|
+
/** Ms epoch of the last `active` transition. 0 if never observed. */
|
|
30051
|
+
lastChangedAt: number()
|
|
30052
|
+
});
|
|
30053
|
+
/** The runtime-state slice: the status plus the clock every slice carries. */
|
|
30054
|
+
var RecordingSignalRuntimeStateSchema = RecordingSignalStatusSchema.extend({ lastFetchedAt: number() });
|
|
30055
|
+
var recordingSignalCapability = {
|
|
30056
|
+
name: "recording-signal",
|
|
30057
|
+
scope: "device",
|
|
30058
|
+
deviceNative: true,
|
|
30059
|
+
mode: "singleton",
|
|
30060
|
+
deviceTypes: [DeviceType.Camera],
|
|
30061
|
+
methods: {
|
|
30062
|
+
/** The current level, straight from the slice the provider keeps fresh. */
|
|
30063
|
+
getStatus: method(object({ deviceId: number() }), RecordingSignalStatusSchema) },
|
|
30064
|
+
status: {
|
|
30065
|
+
schema: RecordingSignalStatusSchema,
|
|
30066
|
+
kind: "push",
|
|
30067
|
+
empty: {
|
|
30068
|
+
active: false,
|
|
30069
|
+
reason: "unknown",
|
|
30070
|
+
lastChangedAt: 0
|
|
30071
|
+
}
|
|
30072
|
+
},
|
|
30073
|
+
runtimeState: RecordingSignalRuntimeStateSchema,
|
|
30074
|
+
/**
|
|
30075
|
+
* Runtime-state durability: **session** — a restored `active: true` from
|
|
30076
|
+
* before a restart is exactly the stale level the recorder's reconcile bound
|
|
30077
|
+
* exists to end, and the provider re-derives the true level on activation
|
|
30078
|
+
* anyway. Nothing is lost by forgetting it; a lie is avoided.
|
|
30079
|
+
*
|
|
30080
|
+
* See `RuntimeStateDurability`. Enforced by
|
|
30081
|
+
* `scripts/check-runtime-state-durability.ts`.
|
|
30082
|
+
*/
|
|
30083
|
+
durability: "session"
|
|
30084
|
+
};
|
|
30085
|
+
/**
|
|
29983
30086
|
* scene-monitor — device-scoped reference-region state cap. An operator marks
|
|
29984
30087
|
* a rect ROI on a camera frame and names one or more states; the engine
|
|
29985
30088
|
* (pipeline-analytics, designated post-processing node) reports which state is
|
|
@@ -30477,6 +30580,8 @@ var CamStreamDescriptorSchema = object({
|
|
|
30477
30580
|
deviceFeatures: array(string$2()).optional(),
|
|
30478
30581
|
/** Eligible for automatic profile assignment. Absent = `true`. */
|
|
30479
30582
|
autoEligible: boolean().optional(),
|
|
30583
|
+
/** The slot the publisher says this stream IS — see `CameraStreamSchema.profileHint`. */
|
|
30584
|
+
profileHint: CamProfileSchema.optional(),
|
|
30480
30585
|
/** Transport-specific opaque metadata (e.g. rfc4571 SDP). */
|
|
30481
30586
|
metadata: record(string$2(), unknown()).optional()
|
|
30482
30587
|
});
|
|
@@ -32616,6 +32721,7 @@ var DEVICE_LOCAL_STATE_CAPS = {
|
|
|
32616
32721
|
pressureSensor: pressureSensorCapability,
|
|
32617
32722
|
privacyMask: privacyMaskCapability,
|
|
32618
32723
|
ptzAutotrack: ptzAutotrackCapability,
|
|
32724
|
+
recordingSignal: recordingSignalCapability,
|
|
32619
32725
|
sceneMonitor: sceneMonitorCapability,
|
|
32620
32726
|
scriptRunner: scriptRunnerCapability,
|
|
32621
32727
|
smoke: smokeCapability,
|
|
@@ -33459,6 +33565,53 @@ var BaseDeviceProvider = class extends BaseAddon {
|
|
|
33459
33565
|
const ids = [...this._permanentRestoreFailures.keys()].join(", ");
|
|
33460
33566
|
return `${this._permanentRestoreFailures.size} device(s) permanently failed restore (deviceIds: ${ids}) — restart the ${this.providerName} provider to retry`;
|
|
33461
33567
|
}
|
|
33568
|
+
/**
|
|
33569
|
+
* A top-level persisted row with NO configuration is a leftover, not a
|
|
33570
|
+
* device — quarantine it instead of dialling it.
|
|
33571
|
+
*
|
|
33572
|
+
* Device 614 was deleted, resurrected as a config-less shell by a write
|
|
33573
|
+
* already in flight (D376), and then dialled at every single boot: eight
|
|
33574
|
+
* `Failed to restore device — bounded retry scheduled` lines in one day,
|
|
33575
|
+
* each one the same Zod refusal on `host` and `password`. The bound was
|
|
33576
|
+
* real, but it is a bound PER PROCESS, and the runner restarts — so nothing
|
|
33577
|
+
* about the row ever stopped costing attempts and log lines.
|
|
33578
|
+
*
|
|
33579
|
+
* No retry can add a configuration that is not there. The row is reported
|
|
33580
|
+
* once, with its `deviceId`, and set aside.
|
|
33581
|
+
*
|
|
33582
|
+
* Top-level only: an accessory child legitimately carries an empty blob (it
|
|
33583
|
+
* lives off its parent), and a hub-adopted child with an emptied blob is
|
|
33584
|
+
* repaired by `healSavedConfig` from its parent instead. Providers that
|
|
33585
|
+
* genuinely create configuration-free top-level devices turn this off.
|
|
33586
|
+
*/
|
|
33587
|
+
quarantineConfiglessTopLevelRestores = true;
|
|
33588
|
+
isUnrestorableShell(saved) {
|
|
33589
|
+
if (!this.quarantineConfiglessTopLevelRestores) return false;
|
|
33590
|
+
if (saved.parentDeviceId !== null) return false;
|
|
33591
|
+
const config = saved.config;
|
|
33592
|
+
if (config === void 0) return false;
|
|
33593
|
+
return Object.keys(config).length === 0;
|
|
33594
|
+
}
|
|
33595
|
+
quarantineShell(saved) {
|
|
33596
|
+
this._permanentRestoreFailures.set(saved.id, {
|
|
33597
|
+
deviceId: saved.id,
|
|
33598
|
+
stableId: saved.stableId,
|
|
33599
|
+
type: saved.type,
|
|
33600
|
+
attempts: 0,
|
|
33601
|
+
lastError: "persisted row has no configuration",
|
|
33602
|
+
failedAt: Date.now()
|
|
33603
|
+
});
|
|
33604
|
+
this.ctx.logger.error("Persisted device row has NO configuration — quarantined: not restored, not retried. Remove it (deviceManager.removeDevice) or re-add the device.", {
|
|
33605
|
+
tags: {
|
|
33606
|
+
deviceId: saved.id,
|
|
33607
|
+
stableId: saved.stableId
|
|
33608
|
+
},
|
|
33609
|
+
meta: {
|
|
33610
|
+
type: saved.type,
|
|
33611
|
+
provider: this.providerName
|
|
33612
|
+
}
|
|
33613
|
+
});
|
|
33614
|
+
}
|
|
33462
33615
|
cancelRestoreRetries() {
|
|
33463
33616
|
this._restoreRetryScheduler?.cancel();
|
|
33464
33617
|
this._restoreRetryScheduler = null;
|
|
@@ -33643,6 +33796,10 @@ var BaseDeviceProvider = class extends BaseAddon {
|
|
|
33643
33796
|
});
|
|
33644
33797
|
return;
|
|
33645
33798
|
}
|
|
33799
|
+
if (this.isUnrestorableShell(saved)) {
|
|
33800
|
+
this.quarantineShell(saved);
|
|
33801
|
+
return;
|
|
33802
|
+
}
|
|
33646
33803
|
try {
|
|
33647
33804
|
await attemptRestore(saved);
|
|
33648
33805
|
} catch (err) {
|
|
@@ -38363,6 +38520,12 @@ Object.freeze({
|
|
|
38363
38520
|
addonId: null,
|
|
38364
38521
|
access: "view"
|
|
38365
38522
|
},
|
|
38523
|
+
"recordingSignal.getStatus": {
|
|
38524
|
+
capName: "recording-signal",
|
|
38525
|
+
capScope: "device",
|
|
38526
|
+
addonId: null,
|
|
38527
|
+
access: "view"
|
|
38528
|
+
},
|
|
38366
38529
|
"sceneMonitor.captureReference": {
|
|
38367
38530
|
capName: "scene-monitor",
|
|
38368
38531
|
capScope: "device",
|
|
@@ -41355,6 +41518,11 @@ Object.freeze({
|
|
|
41355
41518
|
form: "single",
|
|
41356
41519
|
optional: true
|
|
41357
41520
|
}],
|
|
41521
|
+
"recordingSignal.getStatus": [{
|
|
41522
|
+
name: "deviceId",
|
|
41523
|
+
form: "single",
|
|
41524
|
+
optional: false
|
|
41525
|
+
}],
|
|
41358
41526
|
"sceneMonitor.captureReference": [{
|
|
41359
41527
|
name: "deviceId",
|
|
41360
41528
|
form: "single",
|
package/dist/addon.mjs
CHANGED
|
@@ -6457,6 +6457,21 @@ var CameraStreamSchema = object({
|
|
|
6457
6457
|
*/
|
|
6458
6458
|
autoEligible: boolean().optional(),
|
|
6459
6459
|
/**
|
|
6460
|
+
* The profile slot the PUBLISHER says this stream is: `low` for a stream
|
|
6461
|
+
* whose only sensible role is the low-bandwidth one, `high` for a main
|
|
6462
|
+
* stream. Consulted by the broker's automatic ranking
|
|
6463
|
+
* (`rankInitialAssignment`) BEFORE the pixel-count heuristic, and only there:
|
|
6464
|
+
* an operator's manual assignment still wins, and an absent hint is the
|
|
6465
|
+
* pixel-ranked default every camera had before this field existed.
|
|
6466
|
+
*
|
|
6467
|
+
* Why it exists: a camera with ONE stream ranked to `mid` — the slot no
|
|
6468
|
+
* storage class is written for on its own (`recordings` is high|mid,
|
|
6469
|
+
* `recordingsLow` is low) and the one profile scrub does not read. The robot
|
|
6470
|
+
* camera's single 720p relay is a `low` in every respect except the slot the
|
|
6471
|
+
* heuristic put it in.
|
|
6472
|
+
*/
|
|
6473
|
+
profileHint: CamProfileSchema.optional(),
|
|
6474
|
+
/**
|
|
6460
6475
|
* Transport-specific opaque metadata. The broker passes it through to
|
|
6461
6476
|
* the source reader without inspecting it. Currently used by
|
|
6462
6477
|
* `pull-rfc4571` streams to carry the upstream SDP (so the reader can
|
|
@@ -7906,7 +7921,22 @@ var RecordingTriggersSchema = object({
|
|
|
7906
7921
|
* il livello: un contatto trovato già aperto al riavvio del runner non fa
|
|
7907
7922
|
* registrare.
|
|
7908
7923
|
*/
|
|
7909
|
-
sensorDeviceIds: array(number().int().positive()).min(1).max(16).refine(noDuplicates, { message: "sensorDeviceIds must not repeat a device" }).optional()
|
|
7924
|
+
sensorDeviceIds: array(number().int().positive()).min(1).max(16).refine(noDuplicates, { message: "sensorDeviceIds must not repeat a device" }).optional(),
|
|
7925
|
+
/**
|
|
7926
|
+
* La camera registra per tutta la durata del SEGNALE che lei stessa
|
|
7927
|
+
* pubblica — la cap `recording-signal` (`active: true` mentre il device è in
|
|
7928
|
+
* funzione: un robot che pulisce). È un LIVELLO, non un fronte: la finestra
|
|
7929
|
+
* resta aperta finché il segnale è alto e si chiude `postBufferSec` dopo la
|
|
7930
|
+
* caduta, così una pulizia di quaranta minuti è UNA clip.
|
|
7931
|
+
*
|
|
7932
|
+
* Non nomina un device: il segnale è della camera stessa, tenuto fresco dal
|
|
7933
|
+
* suo provider (D224); il recorder lo ascolta su `DeviceStateChanged` e lo
|
|
7934
|
+
* riconcilia leggendo la slice via RPC, con un tetto oltre il quale un
|
|
7935
|
+
* segnale mai abbassato (un evento perso) non può tenere aperta una
|
|
7936
|
+
* registrazione (D11). Vedi `recorder/addon/band-decision.ts`
|
|
7937
|
+
* (`holdTrigger` / `releaseTrigger`).
|
|
7938
|
+
*/
|
|
7939
|
+
deviceSignal: boolean().optional()
|
|
7910
7940
|
});
|
|
7911
7941
|
/**
|
|
7912
7942
|
* Mode of a single recording band — the recorder per-band vocabulary.
|
|
@@ -11419,6 +11449,8 @@ method(object({
|
|
|
11419
11449
|
label: string$2().optional(),
|
|
11420
11450
|
deviceFeatures: array(string$2()).optional(),
|
|
11421
11451
|
autoEligible: boolean().optional(),
|
|
11452
|
+
/** The slot the publisher says this stream IS — see `CameraStreamSchema.profileHint`. */
|
|
11453
|
+
profileHint: CamProfileSchema.optional(),
|
|
11422
11454
|
metadata: record(string$2(), unknown()).optional(),
|
|
11423
11455
|
/** Required when kind === 'derived' — the source camStreamId the
|
|
11424
11456
|
* derived broker reads its encoded plane from.
|
|
@@ -29978,6 +30010,77 @@ method(object({
|
|
|
29978
30010
|
auth: "protected"
|
|
29979
30011
|
});
|
|
29980
30012
|
/**
|
|
30013
|
+
* A camera's own "record me NOW" LEVEL — a signal the device raises while
|
|
30014
|
+
* something it knows about is happening (a robot vacuum cleaning, a machine
|
|
30015
|
+
* running, a gate open) and lowers when it stops.
|
|
30016
|
+
*
|
|
30017
|
+
* It is a level, not an edge. The sensor triggers the recorder already listens
|
|
30018
|
+
* to (`SOURCE_CAP_ACTIVE_FIELD`) fire on a rising edge and record for a fixed
|
|
30019
|
+
* post-buffer; a cleaning lasts forty minutes, and forty minutes of "this is
|
|
30020
|
+
* still happening" cannot be expressed as an edge plus a post-buffer without
|
|
30021
|
+
* either re-triggering on a timer (polling — D224 forbids the consumer keeping
|
|
30022
|
+
* the fact fresh) or picking a post-buffer long enough to cover every job,
|
|
30023
|
+
* which records the dock for that long after every short one. So the recorder
|
|
30024
|
+
* reads the LEVEL: it records for as long as `active` is true, plus the band's
|
|
30025
|
+
* own post-buffer once it falls.
|
|
30026
|
+
*
|
|
30027
|
+
* Who owns the fact: the PROVIDER of the device, which already reads the state
|
|
30028
|
+
* the signal is derived from (the vacuum's lifecycle state, for Dreame). It
|
|
30029
|
+
* writes this slice on every change; consumers subscribe to the kernel's
|
|
30030
|
+
* `DeviceStateChanged` for it and read it back through
|
|
30031
|
+
* `deviceState.getCapSlice` to reconcile — never a second copy, never a poll
|
|
30032
|
+
* against the device (D224, D8/D11).
|
|
30033
|
+
*
|
|
30034
|
+
* Deliberately NOT a `binary` sensor on the camera: a binary sensor is exported
|
|
30035
|
+
* to Home Assistant, HomeKit and the sensor picker as a thing an operator can
|
|
30036
|
+
* bind, and "the camera wants to record" is not a room sensor. It is a fact
|
|
30037
|
+
* about the camera, on the camera.
|
|
30038
|
+
*/
|
|
30039
|
+
var RecordingSignalStatusSchema = object({
|
|
30040
|
+
/** True while the device asks to be recorded. */
|
|
30041
|
+
active: boolean(),
|
|
30042
|
+
/**
|
|
30043
|
+
* Why, in the provider's own vocabulary (a vacuum's `cleaning` / `docked`,
|
|
30044
|
+
* …). Free text for the log line and the UI badge; the recorder never
|
|
30045
|
+
* branches on it.
|
|
30046
|
+
*/
|
|
30047
|
+
reason: string$2(),
|
|
30048
|
+
/** Ms epoch of the last `active` transition. 0 if never observed. */
|
|
30049
|
+
lastChangedAt: number()
|
|
30050
|
+
});
|
|
30051
|
+
/** The runtime-state slice: the status plus the clock every slice carries. */
|
|
30052
|
+
var RecordingSignalRuntimeStateSchema = RecordingSignalStatusSchema.extend({ lastFetchedAt: number() });
|
|
30053
|
+
var recordingSignalCapability = {
|
|
30054
|
+
name: "recording-signal",
|
|
30055
|
+
scope: "device",
|
|
30056
|
+
deviceNative: true,
|
|
30057
|
+
mode: "singleton",
|
|
30058
|
+
deviceTypes: [DeviceType.Camera],
|
|
30059
|
+
methods: {
|
|
30060
|
+
/** The current level, straight from the slice the provider keeps fresh. */
|
|
30061
|
+
getStatus: method(object({ deviceId: number() }), RecordingSignalStatusSchema) },
|
|
30062
|
+
status: {
|
|
30063
|
+
schema: RecordingSignalStatusSchema,
|
|
30064
|
+
kind: "push",
|
|
30065
|
+
empty: {
|
|
30066
|
+
active: false,
|
|
30067
|
+
reason: "unknown",
|
|
30068
|
+
lastChangedAt: 0
|
|
30069
|
+
}
|
|
30070
|
+
},
|
|
30071
|
+
runtimeState: RecordingSignalRuntimeStateSchema,
|
|
30072
|
+
/**
|
|
30073
|
+
* Runtime-state durability: **session** — a restored `active: true` from
|
|
30074
|
+
* before a restart is exactly the stale level the recorder's reconcile bound
|
|
30075
|
+
* exists to end, and the provider re-derives the true level on activation
|
|
30076
|
+
* anyway. Nothing is lost by forgetting it; a lie is avoided.
|
|
30077
|
+
*
|
|
30078
|
+
* See `RuntimeStateDurability`. Enforced by
|
|
30079
|
+
* `scripts/check-runtime-state-durability.ts`.
|
|
30080
|
+
*/
|
|
30081
|
+
durability: "session"
|
|
30082
|
+
};
|
|
30083
|
+
/**
|
|
29981
30084
|
* scene-monitor — device-scoped reference-region state cap. An operator marks
|
|
29982
30085
|
* a rect ROI on a camera frame and names one or more states; the engine
|
|
29983
30086
|
* (pipeline-analytics, designated post-processing node) reports which state is
|
|
@@ -30475,6 +30578,8 @@ var CamStreamDescriptorSchema = object({
|
|
|
30475
30578
|
deviceFeatures: array(string$2()).optional(),
|
|
30476
30579
|
/** Eligible for automatic profile assignment. Absent = `true`. */
|
|
30477
30580
|
autoEligible: boolean().optional(),
|
|
30581
|
+
/** The slot the publisher says this stream IS — see `CameraStreamSchema.profileHint`. */
|
|
30582
|
+
profileHint: CamProfileSchema.optional(),
|
|
30478
30583
|
/** Transport-specific opaque metadata (e.g. rfc4571 SDP). */
|
|
30479
30584
|
metadata: record(string$2(), unknown()).optional()
|
|
30480
30585
|
});
|
|
@@ -32614,6 +32719,7 @@ var DEVICE_LOCAL_STATE_CAPS = {
|
|
|
32614
32719
|
pressureSensor: pressureSensorCapability,
|
|
32615
32720
|
privacyMask: privacyMaskCapability,
|
|
32616
32721
|
ptzAutotrack: ptzAutotrackCapability,
|
|
32722
|
+
recordingSignal: recordingSignalCapability,
|
|
32617
32723
|
sceneMonitor: sceneMonitorCapability,
|
|
32618
32724
|
scriptRunner: scriptRunnerCapability,
|
|
32619
32725
|
smoke: smokeCapability,
|
|
@@ -33457,6 +33563,53 @@ var BaseDeviceProvider = class extends BaseAddon {
|
|
|
33457
33563
|
const ids = [...this._permanentRestoreFailures.keys()].join(", ");
|
|
33458
33564
|
return `${this._permanentRestoreFailures.size} device(s) permanently failed restore (deviceIds: ${ids}) — restart the ${this.providerName} provider to retry`;
|
|
33459
33565
|
}
|
|
33566
|
+
/**
|
|
33567
|
+
* A top-level persisted row with NO configuration is a leftover, not a
|
|
33568
|
+
* device — quarantine it instead of dialling it.
|
|
33569
|
+
*
|
|
33570
|
+
* Device 614 was deleted, resurrected as a config-less shell by a write
|
|
33571
|
+
* already in flight (D376), and then dialled at every single boot: eight
|
|
33572
|
+
* `Failed to restore device — bounded retry scheduled` lines in one day,
|
|
33573
|
+
* each one the same Zod refusal on `host` and `password`. The bound was
|
|
33574
|
+
* real, but it is a bound PER PROCESS, and the runner restarts — so nothing
|
|
33575
|
+
* about the row ever stopped costing attempts and log lines.
|
|
33576
|
+
*
|
|
33577
|
+
* No retry can add a configuration that is not there. The row is reported
|
|
33578
|
+
* once, with its `deviceId`, and set aside.
|
|
33579
|
+
*
|
|
33580
|
+
* Top-level only: an accessory child legitimately carries an empty blob (it
|
|
33581
|
+
* lives off its parent), and a hub-adopted child with an emptied blob is
|
|
33582
|
+
* repaired by `healSavedConfig` from its parent instead. Providers that
|
|
33583
|
+
* genuinely create configuration-free top-level devices turn this off.
|
|
33584
|
+
*/
|
|
33585
|
+
quarantineConfiglessTopLevelRestores = true;
|
|
33586
|
+
isUnrestorableShell(saved) {
|
|
33587
|
+
if (!this.quarantineConfiglessTopLevelRestores) return false;
|
|
33588
|
+
if (saved.parentDeviceId !== null) return false;
|
|
33589
|
+
const config = saved.config;
|
|
33590
|
+
if (config === void 0) return false;
|
|
33591
|
+
return Object.keys(config).length === 0;
|
|
33592
|
+
}
|
|
33593
|
+
quarantineShell(saved) {
|
|
33594
|
+
this._permanentRestoreFailures.set(saved.id, {
|
|
33595
|
+
deviceId: saved.id,
|
|
33596
|
+
stableId: saved.stableId,
|
|
33597
|
+
type: saved.type,
|
|
33598
|
+
attempts: 0,
|
|
33599
|
+
lastError: "persisted row has no configuration",
|
|
33600
|
+
failedAt: Date.now()
|
|
33601
|
+
});
|
|
33602
|
+
this.ctx.logger.error("Persisted device row has NO configuration — quarantined: not restored, not retried. Remove it (deviceManager.removeDevice) or re-add the device.", {
|
|
33603
|
+
tags: {
|
|
33604
|
+
deviceId: saved.id,
|
|
33605
|
+
stableId: saved.stableId
|
|
33606
|
+
},
|
|
33607
|
+
meta: {
|
|
33608
|
+
type: saved.type,
|
|
33609
|
+
provider: this.providerName
|
|
33610
|
+
}
|
|
33611
|
+
});
|
|
33612
|
+
}
|
|
33460
33613
|
cancelRestoreRetries() {
|
|
33461
33614
|
this._restoreRetryScheduler?.cancel();
|
|
33462
33615
|
this._restoreRetryScheduler = null;
|
|
@@ -33641,6 +33794,10 @@ var BaseDeviceProvider = class extends BaseAddon {
|
|
|
33641
33794
|
});
|
|
33642
33795
|
return;
|
|
33643
33796
|
}
|
|
33797
|
+
if (this.isUnrestorableShell(saved)) {
|
|
33798
|
+
this.quarantineShell(saved);
|
|
33799
|
+
return;
|
|
33800
|
+
}
|
|
33644
33801
|
try {
|
|
33645
33802
|
await attemptRestore(saved);
|
|
33646
33803
|
} catch (err) {
|
|
@@ -38361,6 +38518,12 @@ Object.freeze({
|
|
|
38361
38518
|
addonId: null,
|
|
38362
38519
|
access: "view"
|
|
38363
38520
|
},
|
|
38521
|
+
"recordingSignal.getStatus": {
|
|
38522
|
+
capName: "recording-signal",
|
|
38523
|
+
capScope: "device",
|
|
38524
|
+
addonId: null,
|
|
38525
|
+
access: "view"
|
|
38526
|
+
},
|
|
38364
38527
|
"sceneMonitor.captureReference": {
|
|
38365
38528
|
capName: "scene-monitor",
|
|
38366
38529
|
capScope: "device",
|
|
@@ -41353,6 +41516,11 @@ Object.freeze({
|
|
|
41353
41516
|
form: "single",
|
|
41354
41517
|
optional: true
|
|
41355
41518
|
}],
|
|
41519
|
+
"recordingSignal.getStatus": [{
|
|
41520
|
+
name: "deviceId",
|
|
41521
|
+
form: "single",
|
|
41522
|
+
optional: false
|
|
41523
|
+
}],
|
|
41356
41524
|
"sceneMonitor.captureReference": [{
|
|
41357
41525
|
name: "deviceId",
|
|
41358
41526
|
form: "single",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camstack/addon-matter-broker",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.73",
|
|
4
4
|
"description": "Matter broker addon for CamStack — owns a Matter fabric (commissioning + the long-lived controller) via the matter.js controller and brokers commissioned Matter nodes into CamStack",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"camstack",
|