@camstack/addon-provider-reolink 1.2.215 → 1.2.217

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.
Files changed (4) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/addon.js +2087 -1117
  3. package/dist/addon.mjs +2087 -1117
  4. package/package.json +5 -2
package/dist/addon.mjs CHANGED
@@ -101020,7 +101020,7 @@ function buildInitialStatus2(config, enabled) {
101020
101020
  };
101021
101021
  }
101022
101022
  //#endregion
101023
- //#region ../types/dist/event-category-DmZDPDf_.mjs
101023
+ //#region ../types/dist/event-category-CaURIwQA.mjs
101024
101024
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
101025
101025
  EventCategory["SystemBoot"] = "system.boot";
101026
101026
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -101736,6 +101736,10 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
101736
101736
  EventCategory["NotificationRuleBroken"] = "notification.rule-broken";
101737
101737
  /** The clear of {@link NotificationRuleBroken}: the rule loads and names nothing deleted. */
101738
101738
  EventCategory["NotificationRuleRepaired"] = "notification.rule-repaired";
101739
+ /** A migration failed or was blocked; the Alert Center re-reads the ledger. */
101740
+ EventCategory["MigrationFailed"] = "migration.failed";
101741
+ /** A migration applied or a failure cleared; the Alert Center re-reads the ledger. */
101742
+ EventCategory["MigrationApplied"] = "migration.applied";
101739
101743
  EventCategory["DeviceUpdated"] = "device.updated";
101740
101744
  /**
101741
101745
  * Transport-level connectivity. Emitted by the device driver when
@@ -101798,6 +101802,378 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
101798
101802
  return EventCategory;
101799
101803
  }({});
101800
101804
  //#endregion
101805
+ //#region ../types/dist/device-type-CEqjmUnR.mjs
101806
+ /**
101807
+ import { errMsg } from '@camstack/types'
101808
+ * Extract a human-readable message from an unknown error value.
101809
+ * Replaces the ubiquitous `errMsg(err)` pattern.
101810
+ */
101811
+ function errMsg$2(err) {
101812
+ if (err instanceof Error) return err.message;
101813
+ if (typeof err === "string") return err;
101814
+ return String(err);
101815
+ }
101816
+ var DeviceType = /* @__PURE__ */ function(DeviceType) {
101817
+ DeviceType["Camera"] = "camera";
101818
+ DeviceType["Hub"] = "hub";
101819
+ DeviceType["Light"] = "light";
101820
+ DeviceType["Siren"] = "siren";
101821
+ DeviceType["Switch"] = "switch";
101822
+ DeviceType["Sensor"] = "sensor";
101823
+ DeviceType["Thermostat"] = "thermostat";
101824
+ /** Air-conditioner / heat-pump climate device (HVAC) — shares the
101825
+ * `climate-control` cap surface with `Thermostat` but renders a
101826
+ * dedicated AC-appropriate control UI (mode chips, fan speed,
101827
+ * independent vertical/horizontal swing). Sources: native Gree, and
101828
+ * reusable by other AC integrations. */
101829
+ DeviceType["Climate"] = "climate";
101830
+ DeviceType["Button"] = "button";
101831
+ /** Generic stateless event emitter — carries a device's EXACT declared
101832
+ * event vocabulary verbatim (no normalization). Installed with the
101833
+ * `event-emitter` cap. Sources: HA `event.*` entities (structured) and
101834
+ * HA bus events (e.g. `zha_event`, generic). */
101835
+ DeviceType["EventEmitter"] = "event-emitter";
101836
+ /** Firmware/software update entity — current vs available version,
101837
+ * updatable flag, update state, and an install action. Installed with
101838
+ * the `update` cap. Sources: Homematic firmware-update channels (and
101839
+ * reusable by other providers, e.g. HA `update.*` entities). */
101840
+ DeviceType["Update"] = "update";
101841
+ DeviceType["Generic"] = "generic";
101842
+ /** Generic notification delivery target (HA `notify.<service>`, future
101843
+ * Telegram / Discord / ntfy / SMTP, …). One device per delivery
101844
+ * endpoint; the `notifier` cap defines the send surface. */
101845
+ DeviceType["Notifier"] = "notifier";
101846
+ /** Pre-recorded action sequence with optional parameters
101847
+ * (HA `script.*`). Runnable via `script-runner` cap. */
101848
+ DeviceType["Script"] = "script";
101849
+ /** Automation rule (HA `automation.*`) — enable/disable + manual
101850
+ * trigger surface exposed via `automation-control` cap. */
101851
+ DeviceType["Automation"] = "automation";
101852
+ /** Door / smart lock device (HA `lock.*`). `lock-control` cap. */
101853
+ DeviceType["Lock"] = "lock";
101854
+ /** Window covering, blinds, garage door, valve, etc. (HA `cover.*`,
101855
+ * `valve.*`). `cover` cap with sub-roles for variant. */
101856
+ DeviceType["Cover"] = "cover";
101857
+ /** Pipe / water / gas valve with open/close/stop and optional
101858
+ * position (HA `valve.*`). `valve` cap — a cover-sibling actuator
101859
+ * modelled on the same open/closed lifecycle. */
101860
+ DeviceType["Valve"] = "valve";
101861
+ /** Humidifier / dehumidifier with on/off + target humidity + mode
101862
+ * (HA `humidifier.*`). `humidifier` cap — a climate-family actuator
101863
+ * modelled on the same target / mode lifecycle. */
101864
+ DeviceType["Humidifier"] = "humidifier";
101865
+ /** Water heater / boiler with target temperature + operation mode +
101866
+ * away mode (HA `water_heater.*`). `water-heater` cap — a
101867
+ * climate-family actuator. */
101868
+ DeviceType["WaterHeater"] = "water-heater";
101869
+ /** Ceiling / standing / exhaust fan (HA `fan.*`). `fan-control` cap. */
101870
+ DeviceType["Fan"] = "fan";
101871
+ /** Audio / video playback endpoint (HA `media_player.*`). Disjoint from
101872
+ * the camera surface — those use `Camera`. `media-player` cap. */
101873
+ DeviceType["MediaPlayer"] = "media-player";
101874
+ /** Security panel / alarm system (HA `alarm_control_panel.*`).
101875
+ * `alarm-panel` cap. */
101876
+ DeviceType["AlarmPanel"] = "alarm-panel";
101877
+ /** Generic user-settable input (HA `number` / `input_number` / `select`
101878
+ * / `input_select` / `text` / `input_text` / `input_datetime`).
101879
+ * Sub-type via `DeviceRole`: NumericControl / SelectControl /
101880
+ * TextControl / DateTimeControl. */
101881
+ DeviceType["Control"] = "control";
101882
+ /** Person / device-tracker presence (HA `person.*`, `device_tracker.*`).
101883
+ * `presence` cap. */
101884
+ DeviceType["Presence"] = "presence";
101885
+ /** Weather provider (HA `weather.*`). Tier-3, low MVP priority.
101886
+ * `weather` cap. */
101887
+ DeviceType["Weather"] = "weather";
101888
+ /** Robot vacuum (HA `vacuum.*`). Tier-3. `vacuum-control` cap. */
101889
+ DeviceType["Vacuum"] = "vacuum";
101890
+ /** Robotic lawn mower (HA `lawn_mower.*`). Tier-3.
101891
+ * `lawn-mower-control` cap. */
101892
+ DeviceType["LawnMower"] = "lawn-mower";
101893
+ /** Physical HA device group — parent container for entity-children
101894
+ * adopted from a single HA device entry. Not renderable as a
101895
+ * standalone device; exists only to anchor child entities. */
101896
+ DeviceType["Container"] = "container";
101897
+ /** Single still-image entity (HA `image.*`). Read-only display of an
101898
+ * `entity_picture` signed URL the browser loads directly. `image` cap. */
101899
+ DeviceType["Image"] = "image";
101900
+ /** Smart pet feeder — cloud-connected food dispenser with a bowl food
101901
+ * level, battery, desiccant life, feeding state and manual-feed /
101902
+ * call-pet / maintenance actions. Installed with the `pet-feeder` cap;
101903
+ * dual-hopper models (D4S/D4SH) expose per-hopper portions. Sources:
101904
+ * native PetKit (`nodepetkit` `FeederDevice`), reusable by other feeder
101905
+ * integrations sharing the same food/desiccant/hopper surface. */
101906
+ DeviceType["PetFeeder"] = "pet-feeder";
101907
+ /** Pet water fountain — tank level, water drunk, pump and its duty cycle.
101908
+ * Installed with the `pet-fountain` cap. Reaches CamStack as loose entities
101909
+ * of a container and is BUILT as a composed device (D967). */
101910
+ DeviceType["PetFountain"] = "pet-fountain";
101911
+ /** Built-in dishwasher — program lifecycle, door / remote-start gates,
101912
+ * salt and rinse-aid levels. Installed with the `dishwasher` cap.
101913
+ * Sources: native Bosch Home Connect, reusable by similar integrations.
101914
+ * A plug-built dishwasher carries running-state and power instead. */
101915
+ DeviceType["Dishwasher"] = "dishwasher";
101916
+ /** Washing machine. A plug-built one is running while its power stays
101917
+ * above a threshold. No program cap — that surface is the dishwasher's. */
101918
+ DeviceType["Washer"] = "washer";
101919
+ /** Tumble dryer. Same plug-built running surface as {@link Washer}. */
101920
+ DeviceType["Dryer"] = "dryer";
101921
+ /** The physical bridge box of a vendor ecosystem (Velux KLF-200, and later
101922
+ * other radio-to-IP bridges). It carries the box's own facts (firmware,
101923
+ * address, connection state) and the actions the box really supports
101924
+ * (`reboot`, `device-admin-link`). Products are NOT its children — they are
101925
+ * adopted beside it — and its connection settings stay on the integration. */
101926
+ DeviceType["Gateway"] = "gateway";
101927
+ return DeviceType;
101928
+ }({});
101929
+ var DeviceFeature = /* @__PURE__ */ function(DeviceFeature) {
101930
+ DeviceFeature["BatteryOperated"] = "battery-operated";
101931
+ DeviceFeature["Rebootable"] = "rebootable";
101932
+ /**
101933
+ * Device supports an on-demand re-sync of its derived spec with its
101934
+ * upstream source — drives the generic Re-sync button. The owning
101935
+ * provider implements the action via the `device-adoption.resync` cap.
101936
+ */
101937
+ DeviceFeature["Resyncable"] = "resyncable";
101938
+ DeviceFeature["NativeSnapshot"] = "native-snapshot";
101939
+ DeviceFeature["DoorbellButton"] = "doorbell-button";
101940
+ DeviceFeature["TwoWayAudio"] = "two-way-audio";
101941
+ DeviceFeature["PanTiltZoom"] = "pan-tilt-zoom";
101942
+ /**
101943
+ * Camera supports the on-firmware autotrack subsystem (subject-
101944
+ * following). Distinct from `PanTiltZoom` because not every PTZ
101945
+ * camera ships autotrack — the admin UI uses this flag to gate
101946
+ * the autotrack toggle / settings card without re-deriving from
101947
+ * the cap registry. Mirrors `ptz-autotrack` cap registration:
101948
+ * driver sets this feature when probe confirms the firmware
101949
+ * surface, and registers the cap in the same code path.
101950
+ */
101951
+ DeviceFeature["PtzAutotrack"] = "ptz-autotrack";
101952
+ /**
101953
+ * Accessory exposes a "trigger on motion" toggle — the parent camera's
101954
+ * motion detection automatically activates this device. Mirrors
101955
+ * `motion-trigger` cap registration: drivers set this feature in the
101956
+ * same code path that calls `ctx.registerNativeCap(motionTriggerCapability, ...)`.
101957
+ *
101958
+ * Used by admin UI (gate the in-hero `MotionTriggerToggle` against a
101959
+ * fast scalar without binding fetch), notifier rules, and `listAll`
101960
+ * filters that want "all devices with on-motion behaviour".
101961
+ */
101962
+ DeviceFeature["MotionTrigger"] = "motion-trigger";
101963
+ /** Light supports rgb-triplet color via `color` cap. */
101964
+ DeviceFeature["LightColorRgb"] = "light-color-rgb";
101965
+ /** Light supports HSV color via `color` cap. */
101966
+ DeviceFeature["LightColorHsv"] = "light-color-hsv";
101967
+ /** Light supports color-temperature (mired) via `color` cap. */
101968
+ DeviceFeature["LightColorMired"] = "light-color-mired";
101969
+ /** Thermostat supports a `heat_cool` dual setpoint (targetLow +
101970
+ * targetHigh). Gates the range slider UI. */
101971
+ DeviceFeature["ClimateDualSetpoint"] = "climate-dual-setpoint";
101972
+ /** Thermostat exposes target humidity and/or current humidity
101973
+ * readings. Gates the humidity controls. */
101974
+ DeviceFeature["ClimateHumidity"] = "climate-humidity";
101975
+ /** Thermostat exposes a fan-mode selector. */
101976
+ DeviceFeature["ClimateFanMode"] = "climate-fan-mode";
101977
+ /** Thermostat exposes preset modes (eco / away / sleep / vendor). */
101978
+ DeviceFeature["ClimatePreset"] = "climate-preset";
101979
+ /** Thermostat exposes a vertical louver swing toggle. Gates the
101980
+ * vertical-swing switch in the climate UI. Independent of horizontal. */
101981
+ DeviceFeature["ClimateSwingVertical"] = "climate-swing-vertical";
101982
+ /** Thermostat exposes a horizontal louver swing toggle. Gates the
101983
+ * horizontal-swing switch in the climate UI. Independent of vertical. */
101984
+ DeviceFeature["ClimateSwingHorizontal"] = "climate-swing-horizontal";
101985
+ /** Cover exposes intermediate position control (0..100). Gates the
101986
+ * position slider UI. */
101987
+ DeviceFeature["CoverPositionable"] = "cover-positionable";
101988
+ /** Cover exposes slat-tilt control. Gates the tilt slider UI. */
101989
+ DeviceFeature["CoverTilt"] = "cover-tilt";
101990
+ /** Valve exposes intermediate position control (0..100). Gates the
101991
+ * position slider / drag surface UI. */
101992
+ DeviceFeature["ValvePositionable"] = "valve-positionable";
101993
+ /** Fan exposes a speed-percentage setter. Gates the speed slider UI. */
101994
+ DeviceFeature["FanSpeed"] = "fan-speed";
101995
+ /** Fan exposes a preset mode selector. */
101996
+ DeviceFeature["FanPreset"] = "fan-preset";
101997
+ /** Fan exposes blade direction (forward/reverse) — typical of
101998
+ * ceiling fans. */
101999
+ DeviceFeature["FanDirection"] = "fan-direction";
102000
+ /** Fan exposes an oscillation toggle. */
102001
+ DeviceFeature["FanOscillating"] = "fan-oscillating";
102002
+ /** Lock requires a PIN code on lock/unlock. Gates the code-entry
102003
+ * field on the UI lock-controls panel. */
102004
+ DeviceFeature["LockPinRequired"] = "lock-pin-required";
102005
+ /** Lock supports a latch-release ("open door") action distinct from
102006
+ * unlock. Mirrors HA `LockEntityFeature.OPEN` (bit 1) in
102007
+ * `supported_features`. Gates the Open Door button in the UI. */
102008
+ DeviceFeature["LockOpen"] = "lock-open";
102009
+ /** Media player exposes a seek-to-position surface. */
102010
+ DeviceFeature["MediaPlayerSeek"] = "media-player-seek";
102011
+ /** Media player exposes a volume-level setter. */
102012
+ DeviceFeature["MediaPlayerVolume"] = "media-player-volume";
102013
+ /** Media player exposes a mute toggle distinct from volume=0. */
102014
+ DeviceFeature["MediaPlayerMute"] = "media-player-mute";
102015
+ /** Media player exposes a shuffle toggle. */
102016
+ DeviceFeature["MediaPlayerShuffle"] = "media-player-shuffle";
102017
+ /** Media player exposes a repeat mode (off / all / one). */
102018
+ DeviceFeature["MediaPlayerRepeat"] = "media-player-repeat";
102019
+ /** Media player exposes a source / input selector. */
102020
+ DeviceFeature["MediaPlayerSelectSource"] = "media-player-select-source";
102021
+ /** Media player exposes a play-arbitrary-media surface (URL / id). */
102022
+ DeviceFeature["MediaPlayerPlayMedia"] = "media-player-play-media";
102023
+ /** Media player exposes next-track. */
102024
+ DeviceFeature["MediaPlayerNext"] = "media-player-next";
102025
+ /** Media player exposes previous-track. */
102026
+ DeviceFeature["MediaPlayerPrevious"] = "media-player-previous";
102027
+ /** Media player exposes stop distinct from pause. */
102028
+ DeviceFeature["MediaPlayerStop"] = "media-player-stop";
102029
+ /** Alarm panel requires a PIN code on arm/disarm. */
102030
+ DeviceFeature["AlarmPinRequired"] = "alarm-pin-required";
102031
+ /** Presence device carries GPS coordinates (lat/lng/accuracy) in
102032
+ * addition to a textual location. */
102033
+ DeviceFeature["PresenceGps"] = "presence-gps";
102034
+ /** Notifier accepts an inline / URL image attachment. */
102035
+ DeviceFeature["NotifierImage"] = "notifier-image";
102036
+ /** Notifier accepts a priority hint (high/normal/low). */
102037
+ DeviceFeature["NotifierPriority"] = "notifier-priority";
102038
+ /** Notifier accepts a free-form `data` payload for platform-specific
102039
+ * fields. */
102040
+ DeviceFeature["NotifierData"] = "notifier-data";
102041
+ /** Notifier supports interactive action buttons / callbacks. */
102042
+ DeviceFeature["NotifierActions"] = "notifier-actions";
102043
+ /** Notifier supports per-call recipient targeting (multi-user). */
102044
+ DeviceFeature["NotifierRecipients"] = "notifier-recipients";
102045
+ /** Script runner accepts a variables map on each run invocation. */
102046
+ DeviceFeature["ScriptVariables"] = "script-variables";
102047
+ /** Automation `trigger` accepts a skipCondition flag — fires the
102048
+ * automation's actions while bypassing its condition block. */
102049
+ DeviceFeature["AutomationSkipCondition"] = "automation-skip-condition";
102050
+ /** Robot vacuum exposes a live cleaning map (image child). Gates the
102051
+ * map tile in the vacuum UI. */
102052
+ DeviceFeature["VacuumHasMap"] = "vacuum-has-map";
102053
+ /** Robot vacuum exposes AI obstacle-detection toggles. Gates the AI
102054
+ * switches group. */
102055
+ DeviceFeature["VacuumHasAi"] = "vacuum-has-ai";
102056
+ /** Robot mower exposes a live mowing map (SVG image child). Gates the
102057
+ * mower map tile. */
102058
+ DeviceFeature["MowerHasMap"] = "mower-has-map";
102059
+ /** Robot mower exposes targeted mowing (all-area / zones / edges /
102060
+ * spots) — gates the Mow action + map selectors. */
102061
+ DeviceFeature["MowerHasTargetedMowing"] = "mower-has-targeted-mowing";
102062
+ return DeviceFeature;
102063
+ }({});
102064
+ /**
102065
+ * Semantic role a device plays within its parent. Populated by driver
102066
+ * addons when creating accessory devices (Reolink siren/floodlight/
102067
+ * PIR/chime/autotrack/doorbell, ONVIF relay outputs, …). Used by the
102068
+ * admin UI to pick icons, labels, and widgets — a `Switch` with
102069
+ * `role: Floodlight` renders as a bulb with a brightness slider,
102070
+ * whereas a `Switch` with `role: Siren` renders as a klaxon.
102071
+ *
102072
+ * Undefined for top-level devices (cameras, NVRs, hubs). Persisted in
102073
+ * sqlite as a nullable TEXT column — old rows keep working unchanged.
102074
+ */
102075
+ var DeviceRole = /* @__PURE__ */ function(DeviceRole) {
102076
+ DeviceRole["Siren"] = "siren";
102077
+ DeviceRole["Floodlight"] = "floodlight";
102078
+ DeviceRole["Spotlight"] = "spotlight";
102079
+ DeviceRole["PirSensor"] = "pir-sensor";
102080
+ DeviceRole["Chime"] = "chime";
102081
+ DeviceRole["Autotrack"] = "autotrack";
102082
+ DeviceRole["Nightvision"] = "nightvision";
102083
+ DeviceRole["PrivacyMask"] = "privacy-mask";
102084
+ DeviceRole["Doorbell"] = "doorbell";
102085
+ /** Interior blind / shade / roller blind. HA `device_class: blind|shade`. */
102086
+ DeviceRole["CoverBlind"] = "cover-blind";
102087
+ /** Exterior roller shutter. HA `device_class: shutter`; Homematic BROLL. */
102088
+ DeviceRole["CoverShutter"] = "cover-shutter";
102089
+ /** Exterior awning. HA `device_class: awning`. */
102090
+ DeviceRole["CoverAwning"] = "cover-awning";
102091
+ /** Curtain / drape. HA `device_class: curtain`. */
102092
+ DeviceRole["CoverCurtain"] = "cover-curtain";
102093
+ /** Garage door. HA `device_class: garage`. A security actuator: exports gate it. */
102094
+ DeviceRole["CoverGarage"] = "cover-garage";
102095
+ /** Gate. HA `device_class: gate`. A security actuator: exports gate it. */
102096
+ DeviceRole["CoverGate"] = "cover-gate";
102097
+ /** Door. HA `device_class: door`. A security actuator: exports gate it. */
102098
+ DeviceRole["CoverDoor"] = "cover-door";
102099
+ /** Window. HA `device_class: window`. */
102100
+ DeviceRole["CoverWindow"] = "cover-window";
102101
+ /** Virtual HA toggle (input_boolean.*) — distinguishable from a
102102
+ * real Switch device for UI rendering / export adapters. */
102103
+ DeviceRole["BinaryHelper"] = "binary-helper";
102104
+ /** A smart plug: a `switch` whose source says it is an outlet (HA
102105
+ * `device_class: outlet`). The Alexa export shows it as a SMARTPLUG. */
102106
+ DeviceRole["Outlet"] = "outlet";
102107
+ /** Generic motion / occupancy / moving event source. Distinct from
102108
+ * the camera accessory PirSensor role: that one is a camera child;
102109
+ * this is a standalone HA / 3rd-party motion sensor. */
102110
+ DeviceRole["MotionSensor"] = "motion-sensor";
102111
+ DeviceRole["ContactSensor"] = "contact-sensor";
102112
+ DeviceRole["LeakSensor"] = "leak-sensor";
102113
+ DeviceRole["SmokeSensor"] = "smoke-sensor";
102114
+ DeviceRole["COSensor"] = "co-sensor";
102115
+ DeviceRole["GasSensor"] = "gas-sensor";
102116
+ DeviceRole["TamperSensor"] = "tamper-sensor";
102117
+ DeviceRole["VibrationSensor"] = "vibration-sensor";
102118
+ DeviceRole["ConnectivitySensor"] = "connectivity-sensor";
102119
+ DeviceRole["SoundSensor"] = "sound-sensor";
102120
+ /** Fallback for `binary_sensor` without a known `device_class`. */
102121
+ DeviceRole["BinarySensor"] = "binary-sensor";
102122
+ DeviceRole["TemperatureSensor"] = "temperature-sensor";
102123
+ DeviceRole["HumiditySensor"] = "humidity-sensor";
102124
+ DeviceRole["AmbientLightSensor"] = "ambient-light-sensor";
102125
+ DeviceRole["PressureSensor"] = "pressure-sensor";
102126
+ /** Wind speed or direction (weather-station `wind-sensor` cap). */
102127
+ DeviceRole["WindSensor"] = "wind-sensor";
102128
+ /** Rain accumulation or rate (weather-station `rain-sensor` cap). */
102129
+ DeviceRole["RainSensor"] = "rain-sensor";
102130
+ /** UV index (weather-station `uv-sensor` cap). */
102131
+ DeviceRole["UvSensor"] = "uv-sensor";
102132
+ /** Solar irradiance W/m² (weather-station `solar-radiation-sensor` cap).
102133
+ * Distinct from AmbientLightSensor (lux). */
102134
+ DeviceRole["SolarRadiationSensor"] = "solar-radiation-sensor";
102135
+ /** Soil moisture % (garden/weather `soil-moisture-sensor` cap). */
102136
+ DeviceRole["SoilMoistureSensor"] = "soil-moisture-sensor";
102137
+ DeviceRole["PowerSensor"] = "power-sensor";
102138
+ DeviceRole["EnergySensor"] = "energy-sensor";
102139
+ DeviceRole["VoltageSensor"] = "voltage-sensor";
102140
+ DeviceRole["CurrentSensor"] = "current-sensor";
102141
+ DeviceRole["AirQualitySensor"] = "air-quality-sensor";
102142
+ /** Battery level (numeric % via `sensor` OR low-bool via
102143
+ * `binary_sensor` — the cap distinguishes via the value type). */
102144
+ DeviceRole["BatterySensor"] = "battery-sensor";
102145
+ /** Fallback for `sensor` numeric without a known `device_class`. */
102146
+ DeviceRole["NumericSensor"] = "numeric-sensor";
102147
+ /** String / enum state (HA `sensor` with `state_class: enum` or
102148
+ * `attributes.options`). */
102149
+ DeviceRole["EnumSensor"] = "enum-sensor";
102150
+ /** Date / timestamp state (HA `sensor` with `device_class: timestamp`
102151
+ * or `date`). The slice carries the raw ISO string verbatim (hosted on
102152
+ * the `enum-sensor` cap); the UI renders it locale-formatted. */
102153
+ DeviceRole["DateTimeSensor"] = "datetime-sensor";
102154
+ /** Last-resort fallback when nothing else matches. */
102155
+ DeviceRole["GenericSensor"] = "generic-sensor";
102156
+ DeviceRole["NumericControl"] = "numeric-control";
102157
+ DeviceRole["SelectControl"] = "select-control";
102158
+ DeviceRole["TextControl"] = "text-control";
102159
+ DeviceRole["DateTimeControl"] = "datetime-control";
102160
+ /** Mobile push notifier (HA `notify.mobile_app_*`) — supports
102161
+ * rich features (image, priority, channel routing). */
102162
+ DeviceRole["MobilePushNotifier"] = "mobile-push-notifier";
102163
+ /** Chat / messaging service (HA `notify.telegram_*`,
102164
+ * `notify.discord_*`, etc.). */
102165
+ DeviceRole["MessagingNotifier"] = "messaging-notifier";
102166
+ /** Email-based delivery (HA `notify.smtp`, etc.). */
102167
+ DeviceRole["EmailNotifier"] = "email-notifier";
102168
+ /** Fallback when the notifier service name doesn't match a known
102169
+ * pattern. */
102170
+ DeviceRole["GenericNotifier"] = "generic-notifier";
102171
+ /** Emits a pulse train (infrared or RF) on request — the `ir-transmitter`
102172
+ * cap. A `DeviceType.Generic` device. */
102173
+ DeviceRole["IrTransmitter"] = "ir-transmitter";
102174
+ return DeviceRole;
102175
+ }({});
102176
+ //#endregion
101801
102177
  //#region ../../node_modules/zod/v4/core/core.js
101802
102178
  var _a$1;
101803
102179
  function $constructor(name, initializer, params) {
@@ -106476,7 +106852,7 @@ var ZodIssueCode = {
106476
106852
  var ZodFirstPartyTypeKind;
106477
106853
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
106478
106854
  //#endregion
106479
- //#region ../types/dist/sleep-DSVWZfJC.mjs
106855
+ //#region ../types/dist/sleep-CSYuxssh.mjs
106480
106856
  /**
106481
106857
  * The audio chunk plane's byte format, and the ONE expansion from a coded
106482
106858
  * window to float samples (D455).
@@ -106996,6 +107372,102 @@ function emitReadiness(bus, params) {
106996
107372
  ts
106997
107373
  }));
106998
107374
  }
107375
+ var NOT_RUN = {
107376
+ declared: false,
107377
+ snapshot: null,
107378
+ applied: [],
107379
+ failed: [],
107380
+ blocked: [],
107381
+ skipped: [],
107382
+ blockOwner: []
107383
+ };
107384
+ /**
107385
+ * Offer `migrations` to the engine and run what it says to run, in its order. Never
107386
+ * throws: an unreachable or refusing engine is logged and reported `declared: false`,
107387
+ * because the hub (and this addon's boot) must not hang on a migration.
107388
+ */
107389
+ async function runAddonMigrations(host, migrations, logger) {
107390
+ const first = migrations[0];
107391
+ if (first === void 0) return NOT_RUN;
107392
+ const owner = first.meta.owner;
107393
+ const log = logger.withTags({ migrationOwner: owner });
107394
+ const byId = new Map(migrations.map((m) => [m.meta.id, m]));
107395
+ let declaration;
107396
+ try {
107397
+ declaration = await host.declareOwner({
107398
+ owner,
107399
+ metas: migrations.map((m) => m.meta)
107400
+ });
107401
+ } catch (err) {
107402
+ log.error("the migration engine refused or could not take this addon catalogue — none ran", { meta: { error: errMsg$2(err) } });
107403
+ return {
107404
+ ...NOT_RUN,
107405
+ blockOwner: migrations.filter((m) => m.meta.onFailure === "block-owner").map((m) => m.meta.id)
107406
+ };
107407
+ }
107408
+ const applied = [];
107409
+ const failed = [];
107410
+ const blocked = [];
107411
+ const skipped = [];
107412
+ const settle = (id, status) => {
107413
+ if (status === "applied") applied.push(id);
107414
+ else if (status === "failed") failed.push(id);
107415
+ else if (status === "blocked") blocked.push(id);
107416
+ else skipped.push(id);
107417
+ };
107418
+ for (const step of declaration.steps) {
107419
+ const bound = byId.get(step.id);
107420
+ if (bound === void 0 || step.action === "skip") {
107421
+ skipped.push(step.id);
107422
+ continue;
107423
+ }
107424
+ const tags = { migrationId: step.id };
107425
+ try {
107426
+ const gate = await host.startMigration({ id: step.id });
107427
+ if (!gate.proceed) {
107428
+ log.warn("migration not run", {
107429
+ tags,
107430
+ meta: {
107431
+ state: gate.state,
107432
+ reason: gate.reason
107433
+ }
107434
+ });
107435
+ settle(step.id, gate.state === "pending" ? "blocked" : gate.state);
107436
+ continue;
107437
+ }
107438
+ const result = await bound.execute();
107439
+ const outcome = await host.finishMigration({
107440
+ id: step.id,
107441
+ result
107442
+ });
107443
+ if (outcome.status === "applied") log.info("migration applied", {
107444
+ tags,
107445
+ meta: { rowsChanged: outcome.rowsChanged }
107446
+ });
107447
+ else log.error("migration failed", {
107448
+ tags,
107449
+ meta: { error: outcome.error }
107450
+ });
107451
+ settle(step.id, outcome.status);
107452
+ } catch (err) {
107453
+ log.error("migration could not be completed with the engine", {
107454
+ tags,
107455
+ meta: { error: errMsg$2(err) }
107456
+ });
107457
+ failed.push(step.id);
107458
+ }
107459
+ }
107460
+ const blockOwner = [...failed, ...blocked].filter((id) => byId.get(id)?.meta.onFailure === "block-owner");
107461
+ return {
107462
+ declared: true,
107463
+ snapshot: declaration.snapshot,
107464
+ applied,
107465
+ failed,
107466
+ blocked,
107467
+ skipped,
107468
+ blockOwner
107469
+ };
107470
+ }
106999
107471
  /**
107000
107472
  * Build a {@link DurableState} over a single store key. Transport-agnostic:
107001
107473
  * `read`/`write` are the addon-store or device-store accessors. The whole
@@ -108051,6 +108523,28 @@ var BaseAddon = class {
108051
108523
  return normalized;
108052
108524
  }
108053
108525
  /**
108526
+ * Run this addon's migration catalogue (D987 M3) through the hub's `migrations`
108527
+ * engine. Call it at the point of the boot where the data must already have the new
108528
+ * shape — `onInitialize`, or `onRestoreDevices` for a migration that needs the
108529
+ * restored devices — with the catalogue bound to the addon's ports
108530
+ * (`defineAddonMigrations(ADDON_MIGRATIONS, ports)`).
108531
+ *
108532
+ * An engine that cannot be reached or refuses the catalogue is logged and runs
108533
+ * nothing; the boot continues. A migration that FAILED (or was blocked by one)
108534
+ * and declares `onFailure: 'block-owner'` makes this throw, so the addon does not
108535
+ * serve data whose shape it cannot trust; `tolerate` migrations never do.
108536
+ */
108537
+ async runMigrations(migrations) {
108538
+ const api = this.ctx.api.migrations;
108539
+ const report = await runAddonMigrations({
108540
+ declareOwner: (input) => api.declareOwner.mutate(input),
108541
+ startMigration: (input) => api.startMigration.mutate(input),
108542
+ finishMigration: (input) => api.finishMigration.mutate(input)
108543
+ }, migrations, this.ctx.logger);
108544
+ if (report.blockOwner.length > 0) throw new Error(`migration(s) ${report.blockOwner.join(", ")} did not apply and block this addon — fix or retry them (migrations.retry), then restart it`);
108545
+ return report;
108546
+ }
108547
+ /**
108054
108548
  * Called by the isolated-process runner AFTER `broker.start()`.
108055
108549
  * In-process addons never need this because the hub broker is already
108056
108550
  * running when `initialize()` fires. For forked children the broker
@@ -109160,407 +109654,6 @@ var RecordKindDeclarationSchema = object({
109160
109654
  }).strict();
109161
109655
  /** The kind names: lower-case, dash separated, short enough for an index name. */
109162
109656
  var RecordKindNameSchema = string().regex(/^[a-z][a-z0-9-]{0,47}$/);
109163
- var READING_QUANTITIES = [
109164
- "temperature",
109165
- "pressure",
109166
- "speed",
109167
- "precipitation",
109168
- "precipitation-rate",
109169
- "length",
109170
- "area",
109171
- "illuminance",
109172
- "irradiance",
109173
- "power",
109174
- "energy",
109175
- "voltage",
109176
- "current",
109177
- "apparent-power",
109178
- "humidity",
109179
- "percentage",
109180
- "concentration-volume",
109181
- "concentration-mass",
109182
- "concentration-ppb",
109183
- "volume",
109184
- "duration",
109185
- "duration-ms",
109186
- "information",
109187
- "sound-level",
109188
- "signal-level",
109189
- "colour-temperature",
109190
- "angle",
109191
- "bitrate",
109192
- "rate",
109193
- "mass",
109194
- "index",
109195
- "ratio",
109196
- "count",
109197
- "none",
109198
- "device"
109199
- ];
109200
- var ReadingQuantitySchema = _enum(READING_QUANTITIES);
109201
- new Set(READING_QUANTITIES);
109202
- /**
109203
- * Stored unit symbol for a quantity. `none` means the number is dimensionless
109204
- * (or an index / ratio / count). `device` means the cap has no single unit.
109205
- */
109206
- var QUANTITY_BASE = {
109207
- temperature: "°C",
109208
- pressure: "hPa",
109209
- speed: "m/s",
109210
- precipitation: "mm",
109211
- "precipitation-rate": "mm/h",
109212
- length: "m",
109213
- area: "m²",
109214
- illuminance: "lx",
109215
- irradiance: "W/m²",
109216
- power: "W",
109217
- energy: "kWh",
109218
- voltage: "V",
109219
- current: "A",
109220
- "apparent-power": "VA",
109221
- humidity: "%",
109222
- percentage: "%",
109223
- "concentration-volume": "ppm",
109224
- "concentration-mass": "µg/m³",
109225
- "concentration-ppb": "ppb",
109226
- volume: "L",
109227
- duration: "s",
109228
- "duration-ms": "ms",
109229
- information: "B",
109230
- "sound-level": "dBFS",
109231
- "signal-level": "dBm",
109232
- "colour-temperature": "mired",
109233
- angle: "°",
109234
- bitrate: "kbps",
109235
- rate: "fps",
109236
- mass: "kg",
109237
- index: "none",
109238
- ratio: "none",
109239
- count: "none",
109240
- none: "none",
109241
- device: "device"
109242
- };
109243
- /** The stored unit, or `undefined` when the quantity has no unit symbol. */
109244
- function baseUnitOf(quantity) {
109245
- const symbol = QUANTITY_BASE[quantity];
109246
- if (symbol === "none" || symbol === "device") return void 0;
109247
- return symbol;
109248
- }
109249
- function readingMeta(quantity, extras) {
109250
- return {
109251
- quantity,
109252
- ...extras?.precision !== void 0 ? { precision: extras.precision } : {},
109253
- ...extras?.label !== void 0 ? { label: extras.label } : {}
109254
- };
109255
- }
109256
- /** Shared Zod schemas used across streaming capabilities. */
109257
- var CamProfileSchema = _enum([
109258
- "high",
109259
- "mid",
109260
- "low"
109261
- ]);
109262
- /**
109263
- * Which rung a per-camera consumer listens on: an explicit profile, or `auto`.
109264
- *
109265
- * `auto` is a REAL value, not an absent one — it means "resolve it by the rule
109266
- * that consumer owns". It lives beside `CamProfile` because a rung is the only
109267
- * operator-facing way to name a stream: a cam-stream id is broker-internal and
109268
- * moves when a slot is reassigned, so a stored id silently stops meaning what
109269
- * the operator picked. Today's one user is the audio analyser, whose rule is
109270
- * "the lowest AVAILABLE native rung" (D537).
109271
- */
109272
- var AudioStreamPickSchema = union([CamProfileSchema, literal("auto")]);
109273
- var CamStreamKindSchema = _enum([
109274
- "pull-rtsp",
109275
- "pull-rtmp",
109276
- "pull-http",
109277
- "pull-flv",
109278
- "pull-rfc4571",
109279
- "push-annexb",
109280
- "derived"
109281
- ]);
109282
- var CamStreamResolutionSchema = object({
109283
- width: number().int().positive().meta(readingMeta("none")),
109284
- height: number().int().positive().meta(readingMeta("none"))
109285
- });
109286
- var CameraStreamSchema = object({
109287
- /** Stable, provider-assigned id unique within the (deviceId) scope. */
109288
- camStreamId: string().min(1),
109289
- deviceId: number().int().nonnegative(),
109290
- kind: CamStreamKindSchema,
109291
- /** Required for pull-* kinds. Ignored for push-annexb. */
109292
- url: string().optional(),
109293
- codec: string().optional(),
109294
- resolution: CamStreamResolutionSchema.optional(),
109295
- fps: number().positive().optional(),
109296
- /** Human label surfaced in the Admin UI "Camera Stream" dropdown. */
109297
- label: string().optional(),
109298
- /**
109299
- * Device-level features the publisher advertised (e.g. `battery-operated`).
109300
- * The broker, snapshot orchestrator, and prebuffer manager all consult
109301
- * this list to derive policy — relaxed stall watchdog for battery
109302
- * cams, prebuffer off by default, longer snapshot rate-limit, etc.
109303
- *
109304
- * Single source of truth replacing per-stream flags like the
109305
- * historical `allowStall`: if the publisher knows the camera is
109306
- * battery-powered, every downstream service derives the right policy
109307
- * from this list.
109308
- */
109309
- deviceFeatures: array(string()).optional(),
109310
- /**
109311
- * Whether this stream participates in the broker's automatic profile
109312
- * assignment (`computeInitialAssignment`). Defaults to `true`. Publishers
109313
- * use `false` when they want a stream to be SELECTABLE in the UI but not
109314
- * picked by default — e.g. Reolink publishes its native Baichuan streams
109315
- * as `autoEligible: true` (the recommended path) and its RTSP / RTMP
109316
- * mirrors as `autoEligible: false` (still pickable per slot, just not
109317
- * the auto choice). Manual `assignProfile` calls remain valid for
109318
- * non-eligible streams.
109319
- */
109320
- autoEligible: boolean().optional(),
109321
- /**
109322
- * The profile slot the PUBLISHER says this stream is: `low` for a stream
109323
- * whose only sensible role is the low-bandwidth one, `high` for a main
109324
- * stream. Consulted by the broker's automatic ranking
109325
- * (`rankInitialAssignment`) BEFORE the pixel-count heuristic, and only there:
109326
- * an operator's manual assignment still wins, and an absent hint is the
109327
- * pixel-ranked default every camera had before this field existed.
109328
- *
109329
- * Why it exists: a camera with ONE stream ranked to `mid` — the slot no
109330
- * storage class is written for on its own (`recordings` is high|mid,
109331
- * `recordingsLow` is low) and the one profile scrub does not read. The robot
109332
- * camera's single 720p relay is a `low` in every respect except the slot the
109333
- * heuristic put it in.
109334
- */
109335
- profileHint: CamProfileSchema.optional(),
109336
- /**
109337
- * Transport-specific opaque metadata. The broker passes it through to
109338
- * the source reader without inspecting it. Currently used by
109339
- * `pull-rfc4571` streams to carry the upstream SDP (so the reader can
109340
- * route RTP packets to the right depacketizer without an in-band
109341
- * DESCRIBE phase). Other kinds typically leave it undefined.
109342
- */
109343
- metadata: record(string(), unknown()).optional()
109344
- });
109345
- var ProfileSlotStatusSchema = _enum([
109346
- "unassigned",
109347
- "idle",
109348
- "connecting",
109349
- "streaming",
109350
- "error"
109351
- ]);
109352
- var ProfileSlotSchema = object({
109353
- deviceId: number().int().nonnegative(),
109354
- profile: CamProfileSchema,
109355
- /** Broker id the rest of the system addresses: `${deviceId}/${profile}`. */
109356
- brokerId: string(),
109357
- /** `null` when the profile is unassigned. */
109358
- sourceCamStreamId: string().nullable(),
109359
- status: ProfileSlotStatusSchema,
109360
- resolution: CamStreamResolutionSchema.optional(),
109361
- codec: string().optional(),
109362
- preBufferSec: number().nonnegative().optional(),
109363
- errorMessage: string().optional()
109364
- });
109365
- /**
109366
- * Zod schema for StreamSourceEntry — the canonical stream descriptor
109367
- * exposed by ICameraDevice.getStreamSources() and consumed by the broker.
109368
- */
109369
- var StreamSourceEntrySchema$1 = object({
109370
- id: string(),
109371
- label: string(),
109372
- protocol: _enum([
109373
- "rtsp",
109374
- "rtmp",
109375
- "annexb",
109376
- "http-mjpeg",
109377
- "webrtc",
109378
- "custom"
109379
- ]),
109380
- url: string().optional(),
109381
- resolution: object({
109382
- width: number(),
109383
- height: number()
109384
- }).readonly().optional(),
109385
- fps: number().optional(),
109386
- bitrate: number().optional(),
109387
- codec: string().optional(),
109388
- profileHint: CamProfileSchema.optional()
109389
- });
109390
- object({
109391
- type: string(),
109392
- url: string(),
109393
- videoCodec: string().optional(),
109394
- audioCodec: string().optional(),
109395
- metadata: record(string(), unknown()).readonly().optional()
109396
- });
109397
- /**
109398
- * Wire schema for `FrameHandle` — a zero-pixel, serialisable reference to a
109399
- * frame in a shared-memory ring (Phase 5 / D9). Mirrors the `FrameHandle` TS
109400
- * interface in `interfaces/frame-handle.ts` field-for-field so a cap method
109401
- * (the runner's native-crop and pipeline-executor inputs) can carry handles
109402
- * over tRPC / Moleculer.
109403
- *
109404
- * The `satisfies` assertion below pins this schema to the interface: if a
109405
- * field is added to / removed from `FrameHandle` without a matching schema
109406
- * edit, `z.infer<typeof FrameHandleSchema>` stops being assignable to
109407
- * `FrameHandle` and the build fails — no silent drift.
109408
- */
109409
- var FrameHandleSchema = object({
109410
- shmId: string(),
109411
- slot: number().int().nonnegative(),
109412
- seq: number().int().nonnegative(),
109413
- width: number().int().positive(),
109414
- height: number().int().positive(),
109415
- format: _enum([
109416
- "jpeg",
109417
- "rgb",
109418
- "bgr",
109419
- "yuv420",
109420
- "gray"
109421
- ]),
109422
- pts: number(),
109423
- byteLength: number().int().nonnegative(),
109424
- nodeId: string(),
109425
- slotCount: number().int().positive()
109426
- });
109427
- /**
109428
- * Wire schema for a decoded audio chunk (Phase 5 / D9). Mirrors the
109429
- * `DecodedAudioChunk` TS interface in `interfaces/stream-broker.ts`: PCM
109430
- * sample bytes plus the track parameters a consumer needs to interpret them.
109431
- *
109432
- * Audio chunks are tiny (a ~500ms AudioCodecSession window is a few KB of
109433
- * Float32 PCM), so unlike video frames they ship their bytes INLINE over
109434
- * tRPC / Moleculer — no shared-memory plane. `data` is typed `Uint8Array`
109435
- * (the wire-serialisable supertype of `Buffer`); a `Buffer` is assignable to
109436
- * it.
109437
- */
109438
- var AudioChunkFormatSchema = _enum(AUDIO_CHUNK_FORMATS);
109439
- var DecodedAudioChunkSchema = object({
109440
- data: _instanceof(Uint8Array),
109441
- sampleRate: number().int().positive(),
109442
- channels: number().int().positive(),
109443
- timestamp: number(),
109444
- /**
109445
- * Byte format of `data`. ABSENT MEANS `f32le` — today's bytes, byte for
109446
- * byte, for any peer that never heard of this field. A coded window
109447
- * (`pcmu` / `pcma`, one byte per sample) is only ever emitted to a
109448
- * subscription that DECLARED it accepts one, so absence can never mean
109449
- * "coded bytes a consumer will read as floats" (D455).
109450
- */
109451
- format: AudioChunkFormatSchema.optional()
109452
- });
109453
- /**
109454
- * Input for `stream-broker.subscribeAudioChunks` (Phase 5 / D9). Audio ships
109455
- * its bytes inline: a consumer asks the broker for the decoded audio-chunk
109456
- * stream of one broker; the broker keeps a per-subscription bounded FIFO queue
109457
- * the consumer drains via `pullAudioChunks`.
109458
- */
109459
- var SubscribeAudioChunksInputSchema = object({
109460
- brokerId: string(),
109461
- /** Short caller-identity tag (`audio-analyzer`, …) for `listClients`. */
109462
- tag: string().optional(),
109463
- /**
109464
- * Byte formats this subscriber can READ, best first. The broker serves the
109465
- * chunk's own format when it is in this list and expands to `f32le`
109466
- * otherwise, so a subscriber is never handed bytes it cannot interpret.
109467
- *
109468
- * Absent (or without the source format) means `f32le` — the behaviour every
109469
- * subscriber had before D455, unchanged. This is the negotiation half of
109470
- * the source-bytes lever: it is what lets the broker and its consumers
109471
- * deploy one at a time across three nodes.
109472
- */
109473
- accept: array(AudioChunkFormatSchema).readonly().optional()
109474
- });
109475
- /** Result of `stream-broker.subscribeAudioChunks`. */
109476
- var SubscribeAudioChunksResultSchema = object({
109477
- /** Opaque id passed to `pullAudioChunks` / `unsubscribeAudioChunks`. */
109478
- subscriptionId: string() });
109479
- var BrokerStatsSchema = object({
109480
- status: _enum([
109481
- "idle",
109482
- "connecting",
109483
- "streaming",
109484
- "error",
109485
- "stopped"
109486
- ]),
109487
- inputFps: number(),
109488
- encodedSubscribers: number(),
109489
- uptimeMs: number(),
109490
- bitrateKbps: number(),
109491
- idrIntervalMs: number(),
109492
- codec: string().optional(),
109493
- totalBytes: number(),
109494
- packetCount: number(),
109495
- rtspClients: number(),
109496
- pipeClients: number(),
109497
- preBufferSec: number(),
109498
- preBufferMs: number(),
109499
- preBufferPackets: number(),
109500
- /**
109501
- * Detected audio track parameters from the RTSP DESCRIBE / SDP. `null`
109502
- * when the stream has no audio track or the broker is in cold start.
109503
- * `supported = false` means the codec was detected but the local
109504
- * decoder pipeline cannot produce PCM chunks (e.g. AAC without the
109505
- * AAC pipeline wired). Surfaced in the UI device-overview so operators
109506
- * can pre-pick the audio-analysis model that matches the codec.
109507
- */
109508
- audio: object({
109509
- codec: string(),
109510
- sampleRate: number(),
109511
- channels: number(),
109512
- supported: boolean()
109513
- }).nullable().optional(),
109514
- /**
109515
- * BROKER-SIDE AUDIO MUTE (D83). `true` = this broker is deliberately
109516
- * distributing none of the device's audio, on live or recording.
109517
- *
109518
- * Present so a silent camera can be told apart from a broken one on the
109519
- * stream panel itself, without cross-referencing the switch group: a
109520
- * broker holding an `audio` track descriptor while `audioMuted` is true is
109521
- * working exactly as asked. `audioMutedDropped` counts the audio units
109522
- * thrown away since the current dial — it is how you confirm from stats
109523
- * alone that the mute is on the packet path and not merely persisted.
109524
- */
109525
- audioMuted: boolean().optional(),
109526
- audioMutedDropped: number().optional()
109527
- });
109528
- /**
109529
- * Exporter-facing "profile restream" entry. Returned by
109530
- * `cameraStreams.getProfileRtspEntries` — one per ASSIGNED profile slot
109531
- * (high/mid/low). `brokerId` is the PROFILE-keyed broker id
109532
- * (`${deviceId}/${profile}`, e.g. `15/high`); its `url` is a broker
109533
- * RTSP restream that aliases the profile's assigned source broker, so
109534
- * every consumer dialling a profile converges on the same single pull.
109535
- * `codec`/`resolution` describe the assigned SOURCE camStream (for the
109536
- * `auto` resolution-closest pick). Distinct from `RtspRestreamEntry`
109537
- * (raw per-camStream, live-view only).
109538
- */
109539
- var ProfileRtspEntrySchema = object({
109540
- profile: CamProfileSchema,
109541
- /** Profile-keyed broker id, format `${deviceId}/${profile}` (e.g. `"15/high"`). */
109542
- brokerId: string(),
109543
- url: string(),
109544
- mutedUrl: string(),
109545
- enabled: boolean(),
109546
- codec: string().optional(),
109547
- resolution: CamStreamResolutionSchema.optional()
109548
- });
109549
- /**
109550
- * The model weight formats a catalog entry can ship. Schema-free on purpose:
109551
- * `types/models.ts` builds Zod schemas from it, but a runner-closure consumer
109552
- * (the model downloader in `@camstack/system`) needs only the list and must
109553
- * reach it through `@camstack/types/addon` without building the cap schema
109554
- * graph (D28).
109555
- */
109556
- var MODEL_FORMATS = [
109557
- "onnx",
109558
- "coreml",
109559
- "openvino",
109560
- "tflite",
109561
- "pt",
109562
- "gguf"
109563
- ];
109564
109657
  object({
109565
109658
  field: string().min(1),
109566
109659
  on: literal("advance")
@@ -109653,6 +109746,407 @@ function optionalMethod(input, output, options) {
109653
109746
  function event(data) {
109654
109747
  return { data };
109655
109748
  }
109749
+ var READING_QUANTITIES = [
109750
+ "temperature",
109751
+ "pressure",
109752
+ "speed",
109753
+ "precipitation",
109754
+ "precipitation-rate",
109755
+ "length",
109756
+ "area",
109757
+ "illuminance",
109758
+ "irradiance",
109759
+ "power",
109760
+ "energy",
109761
+ "voltage",
109762
+ "current",
109763
+ "apparent-power",
109764
+ "humidity",
109765
+ "percentage",
109766
+ "concentration-volume",
109767
+ "concentration-mass",
109768
+ "concentration-ppb",
109769
+ "volume",
109770
+ "duration",
109771
+ "duration-ms",
109772
+ "information",
109773
+ "sound-level",
109774
+ "signal-level",
109775
+ "colour-temperature",
109776
+ "angle",
109777
+ "bitrate",
109778
+ "rate",
109779
+ "mass",
109780
+ "index",
109781
+ "ratio",
109782
+ "count",
109783
+ "none",
109784
+ "device"
109785
+ ];
109786
+ var ReadingQuantitySchema = _enum(READING_QUANTITIES);
109787
+ new Set(READING_QUANTITIES);
109788
+ /**
109789
+ * Stored unit symbol for a quantity. `none` means the number is dimensionless
109790
+ * (or an index / ratio / count). `device` means the cap has no single unit.
109791
+ */
109792
+ var QUANTITY_BASE = {
109793
+ temperature: "°C",
109794
+ pressure: "hPa",
109795
+ speed: "m/s",
109796
+ precipitation: "mm",
109797
+ "precipitation-rate": "mm/h",
109798
+ length: "m",
109799
+ area: "m²",
109800
+ illuminance: "lx",
109801
+ irradiance: "W/m²",
109802
+ power: "W",
109803
+ energy: "kWh",
109804
+ voltage: "V",
109805
+ current: "A",
109806
+ "apparent-power": "VA",
109807
+ humidity: "%",
109808
+ percentage: "%",
109809
+ "concentration-volume": "ppm",
109810
+ "concentration-mass": "µg/m³",
109811
+ "concentration-ppb": "ppb",
109812
+ volume: "L",
109813
+ duration: "s",
109814
+ "duration-ms": "ms",
109815
+ information: "B",
109816
+ "sound-level": "dBFS",
109817
+ "signal-level": "dBm",
109818
+ "colour-temperature": "mired",
109819
+ angle: "°",
109820
+ bitrate: "kbps",
109821
+ rate: "fps",
109822
+ mass: "kg",
109823
+ index: "none",
109824
+ ratio: "none",
109825
+ count: "none",
109826
+ none: "none",
109827
+ device: "device"
109828
+ };
109829
+ /** The stored unit, or `undefined` when the quantity has no unit symbol. */
109830
+ function baseUnitOf(quantity) {
109831
+ const symbol = QUANTITY_BASE[quantity];
109832
+ if (symbol === "none" || symbol === "device") return void 0;
109833
+ return symbol;
109834
+ }
109835
+ function readingMeta(quantity, extras) {
109836
+ return {
109837
+ quantity,
109838
+ ...extras?.precision !== void 0 ? { precision: extras.precision } : {},
109839
+ ...extras?.label !== void 0 ? { label: extras.label } : {}
109840
+ };
109841
+ }
109842
+ /** Shared Zod schemas used across streaming capabilities. */
109843
+ var CamProfileSchema = _enum([
109844
+ "high",
109845
+ "mid",
109846
+ "low"
109847
+ ]);
109848
+ /**
109849
+ * Which rung a per-camera consumer listens on: an explicit profile, or `auto`.
109850
+ *
109851
+ * `auto` is a REAL value, not an absent one — it means "resolve it by the rule
109852
+ * that consumer owns". It lives beside `CamProfile` because a rung is the only
109853
+ * operator-facing way to name a stream: a cam-stream id is broker-internal and
109854
+ * moves when a slot is reassigned, so a stored id silently stops meaning what
109855
+ * the operator picked. Today's one user is the audio analyser, whose rule is
109856
+ * "the lowest AVAILABLE native rung" (D537).
109857
+ */
109858
+ var AudioStreamPickSchema = union([CamProfileSchema, literal("auto")]);
109859
+ var CamStreamKindSchema = _enum([
109860
+ "pull-rtsp",
109861
+ "pull-rtmp",
109862
+ "pull-http",
109863
+ "pull-flv",
109864
+ "pull-rfc4571",
109865
+ "push-annexb",
109866
+ "derived"
109867
+ ]);
109868
+ var CamStreamResolutionSchema = object({
109869
+ width: number().int().positive().meta(readingMeta("none")),
109870
+ height: number().int().positive().meta(readingMeta("none"))
109871
+ });
109872
+ var CameraStreamSchema = object({
109873
+ /** Stable, provider-assigned id unique within the (deviceId) scope. */
109874
+ camStreamId: string().min(1),
109875
+ deviceId: number().int().nonnegative(),
109876
+ kind: CamStreamKindSchema,
109877
+ /** Required for pull-* kinds. Ignored for push-annexb. */
109878
+ url: string().optional(),
109879
+ codec: string().optional(),
109880
+ resolution: CamStreamResolutionSchema.optional(),
109881
+ fps: number().positive().optional(),
109882
+ /** Human label surfaced in the Admin UI "Camera Stream" dropdown. */
109883
+ label: string().optional(),
109884
+ /**
109885
+ * Device-level features the publisher advertised (e.g. `battery-operated`).
109886
+ * The broker, snapshot orchestrator, and prebuffer manager all consult
109887
+ * this list to derive policy — relaxed stall watchdog for battery
109888
+ * cams, prebuffer off by default, longer snapshot rate-limit, etc.
109889
+ *
109890
+ * Single source of truth replacing per-stream flags like the
109891
+ * historical `allowStall`: if the publisher knows the camera is
109892
+ * battery-powered, every downstream service derives the right policy
109893
+ * from this list.
109894
+ */
109895
+ deviceFeatures: array(string()).optional(),
109896
+ /**
109897
+ * Whether this stream participates in the broker's automatic profile
109898
+ * assignment (`computeInitialAssignment`). Defaults to `true`. Publishers
109899
+ * use `false` when they want a stream to be SELECTABLE in the UI but not
109900
+ * picked by default — e.g. Reolink publishes its native Baichuan streams
109901
+ * as `autoEligible: true` (the recommended path) and its RTSP / RTMP
109902
+ * mirrors as `autoEligible: false` (still pickable per slot, just not
109903
+ * the auto choice). Manual `assignProfile` calls remain valid for
109904
+ * non-eligible streams.
109905
+ */
109906
+ autoEligible: boolean().optional(),
109907
+ /**
109908
+ * The profile slot the PUBLISHER says this stream is: `low` for a stream
109909
+ * whose only sensible role is the low-bandwidth one, `high` for a main
109910
+ * stream. Consulted by the broker's automatic ranking
109911
+ * (`rankInitialAssignment`) BEFORE the pixel-count heuristic, and only there:
109912
+ * an operator's manual assignment still wins, and an absent hint is the
109913
+ * pixel-ranked default every camera had before this field existed.
109914
+ *
109915
+ * Why it exists: a camera with ONE stream ranked to `mid` — the slot no
109916
+ * storage class is written for on its own (`recordings` is high|mid,
109917
+ * `recordingsLow` is low) and the one profile scrub does not read. The robot
109918
+ * camera's single 720p relay is a `low` in every respect except the slot the
109919
+ * heuristic put it in.
109920
+ */
109921
+ profileHint: CamProfileSchema.optional(),
109922
+ /**
109923
+ * Transport-specific opaque metadata. The broker passes it through to
109924
+ * the source reader without inspecting it. Currently used by
109925
+ * `pull-rfc4571` streams to carry the upstream SDP (so the reader can
109926
+ * route RTP packets to the right depacketizer without an in-band
109927
+ * DESCRIBE phase). Other kinds typically leave it undefined.
109928
+ */
109929
+ metadata: record(string(), unknown()).optional()
109930
+ });
109931
+ var ProfileSlotStatusSchema = _enum([
109932
+ "unassigned",
109933
+ "idle",
109934
+ "connecting",
109935
+ "streaming",
109936
+ "error"
109937
+ ]);
109938
+ var ProfileSlotSchema = object({
109939
+ deviceId: number().int().nonnegative(),
109940
+ profile: CamProfileSchema,
109941
+ /** Broker id the rest of the system addresses: `${deviceId}/${profile}`. */
109942
+ brokerId: string(),
109943
+ /** `null` when the profile is unassigned. */
109944
+ sourceCamStreamId: string().nullable(),
109945
+ status: ProfileSlotStatusSchema,
109946
+ resolution: CamStreamResolutionSchema.optional(),
109947
+ codec: string().optional(),
109948
+ preBufferSec: number().nonnegative().optional(),
109949
+ errorMessage: string().optional()
109950
+ });
109951
+ /**
109952
+ * Zod schema for StreamSourceEntry — the canonical stream descriptor
109953
+ * exposed by ICameraDevice.getStreamSources() and consumed by the broker.
109954
+ */
109955
+ var StreamSourceEntrySchema$1 = object({
109956
+ id: string(),
109957
+ label: string(),
109958
+ protocol: _enum([
109959
+ "rtsp",
109960
+ "rtmp",
109961
+ "annexb",
109962
+ "http-mjpeg",
109963
+ "webrtc",
109964
+ "custom"
109965
+ ]),
109966
+ url: string().optional(),
109967
+ resolution: object({
109968
+ width: number(),
109969
+ height: number()
109970
+ }).readonly().optional(),
109971
+ fps: number().optional(),
109972
+ bitrate: number().optional(),
109973
+ codec: string().optional(),
109974
+ profileHint: CamProfileSchema.optional()
109975
+ });
109976
+ object({
109977
+ type: string(),
109978
+ url: string(),
109979
+ videoCodec: string().optional(),
109980
+ audioCodec: string().optional(),
109981
+ metadata: record(string(), unknown()).readonly().optional()
109982
+ });
109983
+ /**
109984
+ * Wire schema for `FrameHandle` — a zero-pixel, serialisable reference to a
109985
+ * frame in a shared-memory ring (Phase 5 / D9). Mirrors the `FrameHandle` TS
109986
+ * interface in `interfaces/frame-handle.ts` field-for-field so a cap method
109987
+ * (the runner's native-crop and pipeline-executor inputs) can carry handles
109988
+ * over tRPC / Moleculer.
109989
+ *
109990
+ * The `satisfies` assertion below pins this schema to the interface: if a
109991
+ * field is added to / removed from `FrameHandle` without a matching schema
109992
+ * edit, `z.infer<typeof FrameHandleSchema>` stops being assignable to
109993
+ * `FrameHandle` and the build fails — no silent drift.
109994
+ */
109995
+ var FrameHandleSchema = object({
109996
+ shmId: string(),
109997
+ slot: number().int().nonnegative(),
109998
+ seq: number().int().nonnegative(),
109999
+ width: number().int().positive(),
110000
+ height: number().int().positive(),
110001
+ format: _enum([
110002
+ "jpeg",
110003
+ "rgb",
110004
+ "bgr",
110005
+ "yuv420",
110006
+ "gray"
110007
+ ]),
110008
+ pts: number(),
110009
+ byteLength: number().int().nonnegative(),
110010
+ nodeId: string(),
110011
+ slotCount: number().int().positive()
110012
+ });
110013
+ /**
110014
+ * Wire schema for a decoded audio chunk (Phase 5 / D9). Mirrors the
110015
+ * `DecodedAudioChunk` TS interface in `interfaces/stream-broker.ts`: PCM
110016
+ * sample bytes plus the track parameters a consumer needs to interpret them.
110017
+ *
110018
+ * Audio chunks are tiny (a ~500ms AudioCodecSession window is a few KB of
110019
+ * Float32 PCM), so unlike video frames they ship their bytes INLINE over
110020
+ * tRPC / Moleculer — no shared-memory plane. `data` is typed `Uint8Array`
110021
+ * (the wire-serialisable supertype of `Buffer`); a `Buffer` is assignable to
110022
+ * it.
110023
+ */
110024
+ var AudioChunkFormatSchema = _enum(AUDIO_CHUNK_FORMATS);
110025
+ var DecodedAudioChunkSchema = object({
110026
+ data: _instanceof(Uint8Array),
110027
+ sampleRate: number().int().positive(),
110028
+ channels: number().int().positive(),
110029
+ timestamp: number(),
110030
+ /**
110031
+ * Byte format of `data`. ABSENT MEANS `f32le` — today's bytes, byte for
110032
+ * byte, for any peer that never heard of this field. A coded window
110033
+ * (`pcmu` / `pcma`, one byte per sample) is only ever emitted to a
110034
+ * subscription that DECLARED it accepts one, so absence can never mean
110035
+ * "coded bytes a consumer will read as floats" (D455).
110036
+ */
110037
+ format: AudioChunkFormatSchema.optional()
110038
+ });
110039
+ /**
110040
+ * Input for `stream-broker.subscribeAudioChunks` (Phase 5 / D9). Audio ships
110041
+ * its bytes inline: a consumer asks the broker for the decoded audio-chunk
110042
+ * stream of one broker; the broker keeps a per-subscription bounded FIFO queue
110043
+ * the consumer drains via `pullAudioChunks`.
110044
+ */
110045
+ var SubscribeAudioChunksInputSchema = object({
110046
+ brokerId: string(),
110047
+ /** Short caller-identity tag (`audio-analyzer`, …) for `listClients`. */
110048
+ tag: string().optional(),
110049
+ /**
110050
+ * Byte formats this subscriber can READ, best first. The broker serves the
110051
+ * chunk's own format when it is in this list and expands to `f32le`
110052
+ * otherwise, so a subscriber is never handed bytes it cannot interpret.
110053
+ *
110054
+ * Absent (or without the source format) means `f32le` — the behaviour every
110055
+ * subscriber had before D455, unchanged. This is the negotiation half of
110056
+ * the source-bytes lever: it is what lets the broker and its consumers
110057
+ * deploy one at a time across three nodes.
110058
+ */
110059
+ accept: array(AudioChunkFormatSchema).readonly().optional()
110060
+ });
110061
+ /** Result of `stream-broker.subscribeAudioChunks`. */
110062
+ var SubscribeAudioChunksResultSchema = object({
110063
+ /** Opaque id passed to `pullAudioChunks` / `unsubscribeAudioChunks`. */
110064
+ subscriptionId: string() });
110065
+ var BrokerStatsSchema = object({
110066
+ status: _enum([
110067
+ "idle",
110068
+ "connecting",
110069
+ "streaming",
110070
+ "error",
110071
+ "stopped"
110072
+ ]),
110073
+ inputFps: number(),
110074
+ encodedSubscribers: number(),
110075
+ uptimeMs: number(),
110076
+ bitrateKbps: number(),
110077
+ idrIntervalMs: number(),
110078
+ codec: string().optional(),
110079
+ totalBytes: number(),
110080
+ packetCount: number(),
110081
+ rtspClients: number(),
110082
+ pipeClients: number(),
110083
+ preBufferSec: number(),
110084
+ preBufferMs: number(),
110085
+ preBufferPackets: number(),
110086
+ /**
110087
+ * Detected audio track parameters from the RTSP DESCRIBE / SDP. `null`
110088
+ * when the stream has no audio track or the broker is in cold start.
110089
+ * `supported = false` means the codec was detected but the local
110090
+ * decoder pipeline cannot produce PCM chunks (e.g. AAC without the
110091
+ * AAC pipeline wired). Surfaced in the UI device-overview so operators
110092
+ * can pre-pick the audio-analysis model that matches the codec.
110093
+ */
110094
+ audio: object({
110095
+ codec: string(),
110096
+ sampleRate: number(),
110097
+ channels: number(),
110098
+ supported: boolean()
110099
+ }).nullable().optional(),
110100
+ /**
110101
+ * BROKER-SIDE AUDIO MUTE (D83). `true` = this broker is deliberately
110102
+ * distributing none of the device's audio, on live or recording.
110103
+ *
110104
+ * Present so a silent camera can be told apart from a broken one on the
110105
+ * stream panel itself, without cross-referencing the switch group: a
110106
+ * broker holding an `audio` track descriptor while `audioMuted` is true is
110107
+ * working exactly as asked. `audioMutedDropped` counts the audio units
110108
+ * thrown away since the current dial — it is how you confirm from stats
110109
+ * alone that the mute is on the packet path and not merely persisted.
110110
+ */
110111
+ audioMuted: boolean().optional(),
110112
+ audioMutedDropped: number().optional()
110113
+ });
110114
+ /**
110115
+ * Exporter-facing "profile restream" entry. Returned by
110116
+ * `cameraStreams.getProfileRtspEntries` — one per ASSIGNED profile slot
110117
+ * (high/mid/low). `brokerId` is the PROFILE-keyed broker id
110118
+ * (`${deviceId}/${profile}`, e.g. `15/high`); its `url` is a broker
110119
+ * RTSP restream that aliases the profile's assigned source broker, so
110120
+ * every consumer dialling a profile converges on the same single pull.
110121
+ * `codec`/`resolution` describe the assigned SOURCE camStream (for the
110122
+ * `auto` resolution-closest pick). Distinct from `RtspRestreamEntry`
110123
+ * (raw per-camStream, live-view only).
110124
+ */
110125
+ var ProfileRtspEntrySchema = object({
110126
+ profile: CamProfileSchema,
110127
+ /** Profile-keyed broker id, format `${deviceId}/${profile}` (e.g. `"15/high"`). */
110128
+ brokerId: string(),
110129
+ url: string(),
110130
+ mutedUrl: string(),
110131
+ enabled: boolean(),
110132
+ codec: string().optional(),
110133
+ resolution: CamStreamResolutionSchema.optional()
110134
+ });
110135
+ /**
110136
+ * The model weight formats a catalog entry can ship. Schema-free on purpose:
110137
+ * `types/models.ts` builds Zod schemas from it, but a runner-closure consumer
110138
+ * (the model downloader in `@camstack/server/system`) needs only the list and must
110139
+ * reach it through `@camstack/types/addon` without building the cap schema
110140
+ * graph (D28).
110141
+ */
110142
+ var MODEL_FORMATS = [
110143
+ "onnx",
110144
+ "coreml",
110145
+ "openvino",
110146
+ "tflite",
110147
+ "pt",
110148
+ "gguf"
110149
+ ];
109656
110150
  /**
109657
110151
  * `addon-pages-source` — collection cap exposing per-provider raw page
109658
110152
  * declarations. Every addon that contributes a UI page registers a
@@ -109928,6 +110422,7 @@ var CAP_NAMES = Object.freeze([
109928
110422
  "mesh-network",
109929
110423
  "meter",
109930
110424
  "metrics-provider",
110425
+ "migrations",
109931
110426
  "model-convert",
109932
110427
  "model-distributor",
109933
110428
  "motion",
@@ -109960,6 +110455,7 @@ var CAP_NAMES = Object.freeze([
109960
110455
  "plate-gallery",
109961
110456
  "platform-probe",
109962
110457
  "power-meter",
110458
+ "power-source",
109963
110459
  "presence",
109964
110460
  "pressure-sensor",
109965
110461
  "privacy-mask",
@@ -111311,6 +111807,13 @@ Object.freeze({
111311
111807
  "valueType": "number"
111312
111808
  }
111313
111809
  ],
111810
+ "power-source": [{
111811
+ "path": "switchOn",
111812
+ "valueType": "boolean"
111813
+ }, {
111814
+ "path": "offSeconds",
111815
+ "valueType": "number"
111816
+ }],
111314
111817
  "presence": [
111315
111818
  {
111316
111819
  "path": "state",
@@ -111860,379 +112363,7 @@ function sleep(ms) {
111860
112363
  return new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));
111861
112364
  }
111862
112365
  //#endregion
111863
- //#region ../types/dist/err-msg-BD5buxvT.mjs
111864
- var DeviceType = /* @__PURE__ */ function(DeviceType) {
111865
- DeviceType["Camera"] = "camera";
111866
- DeviceType["Hub"] = "hub";
111867
- DeviceType["Light"] = "light";
111868
- DeviceType["Siren"] = "siren";
111869
- DeviceType["Switch"] = "switch";
111870
- DeviceType["Sensor"] = "sensor";
111871
- DeviceType["Thermostat"] = "thermostat";
111872
- /** Air-conditioner / heat-pump climate device (HVAC) — shares the
111873
- * `climate-control` cap surface with `Thermostat` but renders a
111874
- * dedicated AC-appropriate control UI (mode chips, fan speed,
111875
- * independent vertical/horizontal swing). Sources: native Gree, and
111876
- * reusable by other AC integrations. */
111877
- DeviceType["Climate"] = "climate";
111878
- DeviceType["Button"] = "button";
111879
- /** Generic stateless event emitter — carries a device's EXACT declared
111880
- * event vocabulary verbatim (no normalization). Installed with the
111881
- * `event-emitter` cap. Sources: HA `event.*` entities (structured) and
111882
- * HA bus events (e.g. `zha_event`, generic). */
111883
- DeviceType["EventEmitter"] = "event-emitter";
111884
- /** Firmware/software update entity — current vs available version,
111885
- * updatable flag, update state, and an install action. Installed with
111886
- * the `update` cap. Sources: Homematic firmware-update channels (and
111887
- * reusable by other providers, e.g. HA `update.*` entities). */
111888
- DeviceType["Update"] = "update";
111889
- DeviceType["Generic"] = "generic";
111890
- /** Generic notification delivery target (HA `notify.<service>`, future
111891
- * Telegram / Discord / ntfy / SMTP, …). One device per delivery
111892
- * endpoint; the `notifier` cap defines the send surface. */
111893
- DeviceType["Notifier"] = "notifier";
111894
- /** Pre-recorded action sequence with optional parameters
111895
- * (HA `script.*`). Runnable via `script-runner` cap. */
111896
- DeviceType["Script"] = "script";
111897
- /** Automation rule (HA `automation.*`) — enable/disable + manual
111898
- * trigger surface exposed via `automation-control` cap. */
111899
- DeviceType["Automation"] = "automation";
111900
- /** Door / smart lock device (HA `lock.*`). `lock-control` cap. */
111901
- DeviceType["Lock"] = "lock";
111902
- /** Window covering, blinds, garage door, valve, etc. (HA `cover.*`,
111903
- * `valve.*`). `cover` cap with sub-roles for variant. */
111904
- DeviceType["Cover"] = "cover";
111905
- /** Pipe / water / gas valve with open/close/stop and optional
111906
- * position (HA `valve.*`). `valve` cap — a cover-sibling actuator
111907
- * modelled on the same open/closed lifecycle. */
111908
- DeviceType["Valve"] = "valve";
111909
- /** Humidifier / dehumidifier with on/off + target humidity + mode
111910
- * (HA `humidifier.*`). `humidifier` cap — a climate-family actuator
111911
- * modelled on the same target / mode lifecycle. */
111912
- DeviceType["Humidifier"] = "humidifier";
111913
- /** Water heater / boiler with target temperature + operation mode +
111914
- * away mode (HA `water_heater.*`). `water-heater` cap — a
111915
- * climate-family actuator. */
111916
- DeviceType["WaterHeater"] = "water-heater";
111917
- /** Ceiling / standing / exhaust fan (HA `fan.*`). `fan-control` cap. */
111918
- DeviceType["Fan"] = "fan";
111919
- /** Audio / video playback endpoint (HA `media_player.*`). Disjoint from
111920
- * the camera surface — those use `Camera`. `media-player` cap. */
111921
- DeviceType["MediaPlayer"] = "media-player";
111922
- /** Security panel / alarm system (HA `alarm_control_panel.*`).
111923
- * `alarm-panel` cap. */
111924
- DeviceType["AlarmPanel"] = "alarm-panel";
111925
- /** Generic user-settable input (HA `number` / `input_number` / `select`
111926
- * / `input_select` / `text` / `input_text` / `input_datetime`).
111927
- * Sub-type via `DeviceRole`: NumericControl / SelectControl /
111928
- * TextControl / DateTimeControl. */
111929
- DeviceType["Control"] = "control";
111930
- /** Person / device-tracker presence (HA `person.*`, `device_tracker.*`).
111931
- * `presence` cap. */
111932
- DeviceType["Presence"] = "presence";
111933
- /** Weather provider (HA `weather.*`). Tier-3, low MVP priority.
111934
- * `weather` cap. */
111935
- DeviceType["Weather"] = "weather";
111936
- /** Robot vacuum (HA `vacuum.*`). Tier-3. `vacuum-control` cap. */
111937
- DeviceType["Vacuum"] = "vacuum";
111938
- /** Robotic lawn mower (HA `lawn_mower.*`). Tier-3.
111939
- * `lawn-mower-control` cap. */
111940
- DeviceType["LawnMower"] = "lawn-mower";
111941
- /** Physical HA device group — parent container for entity-children
111942
- * adopted from a single HA device entry. Not renderable as a
111943
- * standalone device; exists only to anchor child entities. */
111944
- DeviceType["Container"] = "container";
111945
- /** Single still-image entity (HA `image.*`). Read-only display of an
111946
- * `entity_picture` signed URL the browser loads directly. `image` cap. */
111947
- DeviceType["Image"] = "image";
111948
- /** Smart pet feeder — cloud-connected food dispenser with a bowl food
111949
- * level, battery, desiccant life, feeding state and manual-feed /
111950
- * call-pet / maintenance actions. Installed with the `pet-feeder` cap;
111951
- * dual-hopper models (D4S/D4SH) expose per-hopper portions. Sources:
111952
- * native PetKit (`nodepetkit` `FeederDevice`), reusable by other feeder
111953
- * integrations sharing the same food/desiccant/hopper surface. */
111954
- DeviceType["PetFeeder"] = "pet-feeder";
111955
- /** Pet water fountain — tank level, water drunk, pump and its duty cycle.
111956
- * Installed with the `pet-fountain` cap. Reaches CamStack as loose entities
111957
- * of a container and is BUILT as a composed device (D967). */
111958
- DeviceType["PetFountain"] = "pet-fountain";
111959
- /** Built-in dishwasher — program lifecycle, door / remote-start gates,
111960
- * salt and rinse-aid levels. Installed with the `dishwasher` cap.
111961
- * Sources: native Bosch Home Connect, reusable by similar integrations.
111962
- * A plug-built dishwasher carries running-state and power instead. */
111963
- DeviceType["Dishwasher"] = "dishwasher";
111964
- /** Washing machine. A plug-built one is running while its power stays
111965
- * above a threshold. No program cap — that surface is the dishwasher's. */
111966
- DeviceType["Washer"] = "washer";
111967
- /** Tumble dryer. Same plug-built running surface as {@link Washer}. */
111968
- DeviceType["Dryer"] = "dryer";
111969
- /** The physical bridge box of a vendor ecosystem (Velux KLF-200, and later
111970
- * other radio-to-IP bridges). It carries the box's own facts (firmware,
111971
- * address, connection state) and the actions the box really supports
111972
- * (`reboot`, `device-admin-link`). Products are NOT its children — they are
111973
- * adopted beside it — and its connection settings stay on the integration. */
111974
- DeviceType["Gateway"] = "gateway";
111975
- return DeviceType;
111976
- }({});
111977
- var DeviceFeature = /* @__PURE__ */ function(DeviceFeature) {
111978
- DeviceFeature["BatteryOperated"] = "battery-operated";
111979
- DeviceFeature["Rebootable"] = "rebootable";
111980
- /**
111981
- * Device supports an on-demand re-sync of its derived spec with its
111982
- * upstream source — drives the generic Re-sync button. The owning
111983
- * provider implements the action via the `device-adoption.resync` cap.
111984
- */
111985
- DeviceFeature["Resyncable"] = "resyncable";
111986
- DeviceFeature["NativeSnapshot"] = "native-snapshot";
111987
- DeviceFeature["DoorbellButton"] = "doorbell-button";
111988
- DeviceFeature["TwoWayAudio"] = "two-way-audio";
111989
- DeviceFeature["PanTiltZoom"] = "pan-tilt-zoom";
111990
- /**
111991
- * Camera supports the on-firmware autotrack subsystem (subject-
111992
- * following). Distinct from `PanTiltZoom` because not every PTZ
111993
- * camera ships autotrack — the admin UI uses this flag to gate
111994
- * the autotrack toggle / settings card without re-deriving from
111995
- * the cap registry. Mirrors `ptz-autotrack` cap registration:
111996
- * driver sets this feature when probe confirms the firmware
111997
- * surface, and registers the cap in the same code path.
111998
- */
111999
- DeviceFeature["PtzAutotrack"] = "ptz-autotrack";
112000
- /**
112001
- * Accessory exposes a "trigger on motion" toggle — the parent camera's
112002
- * motion detection automatically activates this device. Mirrors
112003
- * `motion-trigger` cap registration: drivers set this feature in the
112004
- * same code path that calls `ctx.registerNativeCap(motionTriggerCapability, ...)`.
112005
- *
112006
- * Used by admin UI (gate the in-hero `MotionTriggerToggle` against a
112007
- * fast scalar without binding fetch), notifier rules, and `listAll`
112008
- * filters that want "all devices with on-motion behaviour".
112009
- */
112010
- DeviceFeature["MotionTrigger"] = "motion-trigger";
112011
- /** Light supports rgb-triplet color via `color` cap. */
112012
- DeviceFeature["LightColorRgb"] = "light-color-rgb";
112013
- /** Light supports HSV color via `color` cap. */
112014
- DeviceFeature["LightColorHsv"] = "light-color-hsv";
112015
- /** Light supports color-temperature (mired) via `color` cap. */
112016
- DeviceFeature["LightColorMired"] = "light-color-mired";
112017
- /** Thermostat supports a `heat_cool` dual setpoint (targetLow +
112018
- * targetHigh). Gates the range slider UI. */
112019
- DeviceFeature["ClimateDualSetpoint"] = "climate-dual-setpoint";
112020
- /** Thermostat exposes target humidity and/or current humidity
112021
- * readings. Gates the humidity controls. */
112022
- DeviceFeature["ClimateHumidity"] = "climate-humidity";
112023
- /** Thermostat exposes a fan-mode selector. */
112024
- DeviceFeature["ClimateFanMode"] = "climate-fan-mode";
112025
- /** Thermostat exposes preset modes (eco / away / sleep / vendor). */
112026
- DeviceFeature["ClimatePreset"] = "climate-preset";
112027
- /** Thermostat exposes a vertical louver swing toggle. Gates the
112028
- * vertical-swing switch in the climate UI. Independent of horizontal. */
112029
- DeviceFeature["ClimateSwingVertical"] = "climate-swing-vertical";
112030
- /** Thermostat exposes a horizontal louver swing toggle. Gates the
112031
- * horizontal-swing switch in the climate UI. Independent of vertical. */
112032
- DeviceFeature["ClimateSwingHorizontal"] = "climate-swing-horizontal";
112033
- /** Cover exposes intermediate position control (0..100). Gates the
112034
- * position slider UI. */
112035
- DeviceFeature["CoverPositionable"] = "cover-positionable";
112036
- /** Cover exposes slat-tilt control. Gates the tilt slider UI. */
112037
- DeviceFeature["CoverTilt"] = "cover-tilt";
112038
- /** Valve exposes intermediate position control (0..100). Gates the
112039
- * position slider / drag surface UI. */
112040
- DeviceFeature["ValvePositionable"] = "valve-positionable";
112041
- /** Fan exposes a speed-percentage setter. Gates the speed slider UI. */
112042
- DeviceFeature["FanSpeed"] = "fan-speed";
112043
- /** Fan exposes a preset mode selector. */
112044
- DeviceFeature["FanPreset"] = "fan-preset";
112045
- /** Fan exposes blade direction (forward/reverse) — typical of
112046
- * ceiling fans. */
112047
- DeviceFeature["FanDirection"] = "fan-direction";
112048
- /** Fan exposes an oscillation toggle. */
112049
- DeviceFeature["FanOscillating"] = "fan-oscillating";
112050
- /** Lock requires a PIN code on lock/unlock. Gates the code-entry
112051
- * field on the UI lock-controls panel. */
112052
- DeviceFeature["LockPinRequired"] = "lock-pin-required";
112053
- /** Lock supports a latch-release ("open door") action distinct from
112054
- * unlock. Mirrors HA `LockEntityFeature.OPEN` (bit 1) in
112055
- * `supported_features`. Gates the Open Door button in the UI. */
112056
- DeviceFeature["LockOpen"] = "lock-open";
112057
- /** Media player exposes a seek-to-position surface. */
112058
- DeviceFeature["MediaPlayerSeek"] = "media-player-seek";
112059
- /** Media player exposes a volume-level setter. */
112060
- DeviceFeature["MediaPlayerVolume"] = "media-player-volume";
112061
- /** Media player exposes a mute toggle distinct from volume=0. */
112062
- DeviceFeature["MediaPlayerMute"] = "media-player-mute";
112063
- /** Media player exposes a shuffle toggle. */
112064
- DeviceFeature["MediaPlayerShuffle"] = "media-player-shuffle";
112065
- /** Media player exposes a repeat mode (off / all / one). */
112066
- DeviceFeature["MediaPlayerRepeat"] = "media-player-repeat";
112067
- /** Media player exposes a source / input selector. */
112068
- DeviceFeature["MediaPlayerSelectSource"] = "media-player-select-source";
112069
- /** Media player exposes a play-arbitrary-media surface (URL / id). */
112070
- DeviceFeature["MediaPlayerPlayMedia"] = "media-player-play-media";
112071
- /** Media player exposes next-track. */
112072
- DeviceFeature["MediaPlayerNext"] = "media-player-next";
112073
- /** Media player exposes previous-track. */
112074
- DeviceFeature["MediaPlayerPrevious"] = "media-player-previous";
112075
- /** Media player exposes stop distinct from pause. */
112076
- DeviceFeature["MediaPlayerStop"] = "media-player-stop";
112077
- /** Alarm panel requires a PIN code on arm/disarm. */
112078
- DeviceFeature["AlarmPinRequired"] = "alarm-pin-required";
112079
- /** Presence device carries GPS coordinates (lat/lng/accuracy) in
112080
- * addition to a textual location. */
112081
- DeviceFeature["PresenceGps"] = "presence-gps";
112082
- /** Notifier accepts an inline / URL image attachment. */
112083
- DeviceFeature["NotifierImage"] = "notifier-image";
112084
- /** Notifier accepts a priority hint (high/normal/low). */
112085
- DeviceFeature["NotifierPriority"] = "notifier-priority";
112086
- /** Notifier accepts a free-form `data` payload for platform-specific
112087
- * fields. */
112088
- DeviceFeature["NotifierData"] = "notifier-data";
112089
- /** Notifier supports interactive action buttons / callbacks. */
112090
- DeviceFeature["NotifierActions"] = "notifier-actions";
112091
- /** Notifier supports per-call recipient targeting (multi-user). */
112092
- DeviceFeature["NotifierRecipients"] = "notifier-recipients";
112093
- /** Script runner accepts a variables map on each run invocation. */
112094
- DeviceFeature["ScriptVariables"] = "script-variables";
112095
- /** Automation `trigger` accepts a skipCondition flag — fires the
112096
- * automation's actions while bypassing its condition block. */
112097
- DeviceFeature["AutomationSkipCondition"] = "automation-skip-condition";
112098
- /** Robot vacuum exposes a live cleaning map (image child). Gates the
112099
- * map tile in the vacuum UI. */
112100
- DeviceFeature["VacuumHasMap"] = "vacuum-has-map";
112101
- /** Robot vacuum exposes AI obstacle-detection toggles. Gates the AI
112102
- * switches group. */
112103
- DeviceFeature["VacuumHasAi"] = "vacuum-has-ai";
112104
- /** Robot mower exposes a live mowing map (SVG image child). Gates the
112105
- * mower map tile. */
112106
- DeviceFeature["MowerHasMap"] = "mower-has-map";
112107
- /** Robot mower exposes targeted mowing (all-area / zones / edges /
112108
- * spots) — gates the Mow action + map selectors. */
112109
- DeviceFeature["MowerHasTargetedMowing"] = "mower-has-targeted-mowing";
112110
- return DeviceFeature;
112111
- }({});
112112
- /**
112113
- * Semantic role a device plays within its parent. Populated by driver
112114
- * addons when creating accessory devices (Reolink siren/floodlight/
112115
- * PIR/chime/autotrack/doorbell, ONVIF relay outputs, …). Used by the
112116
- * admin UI to pick icons, labels, and widgets — a `Switch` with
112117
- * `role: Floodlight` renders as a bulb with a brightness slider,
112118
- * whereas a `Switch` with `role: Siren` renders as a klaxon.
112119
- *
112120
- * Undefined for top-level devices (cameras, NVRs, hubs). Persisted in
112121
- * sqlite as a nullable TEXT column — old rows keep working unchanged.
112122
- */
112123
- var DeviceRole = /* @__PURE__ */ function(DeviceRole) {
112124
- DeviceRole["Siren"] = "siren";
112125
- DeviceRole["Floodlight"] = "floodlight";
112126
- DeviceRole["Spotlight"] = "spotlight";
112127
- DeviceRole["PirSensor"] = "pir-sensor";
112128
- DeviceRole["Chime"] = "chime";
112129
- DeviceRole["Autotrack"] = "autotrack";
112130
- DeviceRole["Nightvision"] = "nightvision";
112131
- DeviceRole["PrivacyMask"] = "privacy-mask";
112132
- DeviceRole["Doorbell"] = "doorbell";
112133
- /** Interior blind / shade / roller blind. HA `device_class: blind|shade`. */
112134
- DeviceRole["CoverBlind"] = "cover-blind";
112135
- /** Exterior roller shutter. HA `device_class: shutter`; Homematic BROLL. */
112136
- DeviceRole["CoverShutter"] = "cover-shutter";
112137
- /** Exterior awning. HA `device_class: awning`. */
112138
- DeviceRole["CoverAwning"] = "cover-awning";
112139
- /** Curtain / drape. HA `device_class: curtain`. */
112140
- DeviceRole["CoverCurtain"] = "cover-curtain";
112141
- /** Garage door. HA `device_class: garage`. A security actuator: exports gate it. */
112142
- DeviceRole["CoverGarage"] = "cover-garage";
112143
- /** Gate. HA `device_class: gate`. A security actuator: exports gate it. */
112144
- DeviceRole["CoverGate"] = "cover-gate";
112145
- /** Door. HA `device_class: door`. A security actuator: exports gate it. */
112146
- DeviceRole["CoverDoor"] = "cover-door";
112147
- /** Window. HA `device_class: window`. */
112148
- DeviceRole["CoverWindow"] = "cover-window";
112149
- /** Virtual HA toggle (input_boolean.*) — distinguishable from a
112150
- * real Switch device for UI rendering / export adapters. */
112151
- DeviceRole["BinaryHelper"] = "binary-helper";
112152
- /** A smart plug: a `switch` whose source says it is an outlet (HA
112153
- * `device_class: outlet`). The Alexa export shows it as a SMARTPLUG. */
112154
- DeviceRole["Outlet"] = "outlet";
112155
- /** Generic motion / occupancy / moving event source. Distinct from
112156
- * the camera accessory PirSensor role: that one is a camera child;
112157
- * this is a standalone HA / 3rd-party motion sensor. */
112158
- DeviceRole["MotionSensor"] = "motion-sensor";
112159
- DeviceRole["ContactSensor"] = "contact-sensor";
112160
- DeviceRole["LeakSensor"] = "leak-sensor";
112161
- DeviceRole["SmokeSensor"] = "smoke-sensor";
112162
- DeviceRole["COSensor"] = "co-sensor";
112163
- DeviceRole["GasSensor"] = "gas-sensor";
112164
- DeviceRole["TamperSensor"] = "tamper-sensor";
112165
- DeviceRole["VibrationSensor"] = "vibration-sensor";
112166
- DeviceRole["ConnectivitySensor"] = "connectivity-sensor";
112167
- DeviceRole["SoundSensor"] = "sound-sensor";
112168
- /** Fallback for `binary_sensor` without a known `device_class`. */
112169
- DeviceRole["BinarySensor"] = "binary-sensor";
112170
- DeviceRole["TemperatureSensor"] = "temperature-sensor";
112171
- DeviceRole["HumiditySensor"] = "humidity-sensor";
112172
- DeviceRole["AmbientLightSensor"] = "ambient-light-sensor";
112173
- DeviceRole["PressureSensor"] = "pressure-sensor";
112174
- /** Wind speed or direction (weather-station `wind-sensor` cap). */
112175
- DeviceRole["WindSensor"] = "wind-sensor";
112176
- /** Rain accumulation or rate (weather-station `rain-sensor` cap). */
112177
- DeviceRole["RainSensor"] = "rain-sensor";
112178
- /** UV index (weather-station `uv-sensor` cap). */
112179
- DeviceRole["UvSensor"] = "uv-sensor";
112180
- /** Solar irradiance W/m² (weather-station `solar-radiation-sensor` cap).
112181
- * Distinct from AmbientLightSensor (lux). */
112182
- DeviceRole["SolarRadiationSensor"] = "solar-radiation-sensor";
112183
- /** Soil moisture % (garden/weather `soil-moisture-sensor` cap). */
112184
- DeviceRole["SoilMoistureSensor"] = "soil-moisture-sensor";
112185
- DeviceRole["PowerSensor"] = "power-sensor";
112186
- DeviceRole["EnergySensor"] = "energy-sensor";
112187
- DeviceRole["VoltageSensor"] = "voltage-sensor";
112188
- DeviceRole["CurrentSensor"] = "current-sensor";
112189
- DeviceRole["AirQualitySensor"] = "air-quality-sensor";
112190
- /** Battery level (numeric % via `sensor` OR low-bool via
112191
- * `binary_sensor` — the cap distinguishes via the value type). */
112192
- DeviceRole["BatterySensor"] = "battery-sensor";
112193
- /** Fallback for `sensor` numeric without a known `device_class`. */
112194
- DeviceRole["NumericSensor"] = "numeric-sensor";
112195
- /** String / enum state (HA `sensor` with `state_class: enum` or
112196
- * `attributes.options`). */
112197
- DeviceRole["EnumSensor"] = "enum-sensor";
112198
- /** Date / timestamp state (HA `sensor` with `device_class: timestamp`
112199
- * or `date`). The slice carries the raw ISO string verbatim (hosted on
112200
- * the `enum-sensor` cap); the UI renders it locale-formatted. */
112201
- DeviceRole["DateTimeSensor"] = "datetime-sensor";
112202
- /** Last-resort fallback when nothing else matches. */
112203
- DeviceRole["GenericSensor"] = "generic-sensor";
112204
- DeviceRole["NumericControl"] = "numeric-control";
112205
- DeviceRole["SelectControl"] = "select-control";
112206
- DeviceRole["TextControl"] = "text-control";
112207
- DeviceRole["DateTimeControl"] = "datetime-control";
112208
- /** Mobile push notifier (HA `notify.mobile_app_*`) — supports
112209
- * rich features (image, priority, channel routing). */
112210
- DeviceRole["MobilePushNotifier"] = "mobile-push-notifier";
112211
- /** Chat / messaging service (HA `notify.telegram_*`,
112212
- * `notify.discord_*`, etc.). */
112213
- DeviceRole["MessagingNotifier"] = "messaging-notifier";
112214
- /** Email-based delivery (HA `notify.smtp`, etc.). */
112215
- DeviceRole["EmailNotifier"] = "email-notifier";
112216
- /** Fallback when the notifier service name doesn't match a known
112217
- * pattern. */
112218
- DeviceRole["GenericNotifier"] = "generic-notifier";
112219
- /** Emits a pulse train (infrared or RF) on request — the `ir-transmitter`
112220
- * cap. A `DeviceType.Generic` device. */
112221
- DeviceRole["IrTransmitter"] = "ir-transmitter";
112222
- return DeviceRole;
112223
- }({});
112224
- /**
112225
- import { errMsg } from '@camstack/types'
112226
- * Extract a human-readable message from an unknown error value.
112227
- * Replaces the ubiquitous `errMsg(err)` pattern.
112228
- */
112229
- function errMsg$2(err) {
112230
- if (err instanceof Error) return err.message;
112231
- if (typeof err === "string") return err;
112232
- return String(err);
112233
- }
112234
- //#endregion
112235
- //#region ../types/dist/composition-C7WdThmP.mjs
112366
+ //#region ../types/dist/composition-BBzVf2xd.mjs
112236
112367
  /**
112237
112368
  * The ffmpeg build every CamStack node downloads, per platform and arch.
112238
112369
  *
@@ -113451,6 +113582,71 @@ var EventOwnerTypeSchema = _enum(EVENT_OWNER_TYPES);
113451
113582
  var MediaPresenceOwnerKindSchema = _enum([...EVENT_OWNER_TYPES, "track"]);
113452
113583
  new Set(EVENT_OWNER_TYPES);
113453
113584
  /**
113585
+ * What a published package needs from the rest of the install (D989).
113586
+ *
113587
+ * One published framework package (`@camstack/server`) carries the kernel, the
113588
+ * builtins and the capability contracts, so "which addon runs on which
113589
+ * framework" is a question of version, and the answer is DECLARED by the
113590
+ * package that knows it:
113591
+ *
113592
+ * - an addon says the oldest server it runs on (`requires.server: ">=X"`);
113593
+ * - the server says the oldest addon it works with
113594
+ * (`requires.addons: { "@camstack/addon-x": ">=Y" }`) and the oldest shell
113595
+ * around it (`requires.wrapper: { desktop, image }`);
113596
+ * - a wrapper (the desktop app, the container starter) says which server
113597
+ * range it can host, and reports its own kind and version.
113598
+ *
113599
+ * The publish workflow STAMPS these after the version bump
113600
+ * (`scripts/ci/stamp-requires.mjs`); nobody types a number by hand.
113601
+ *
113602
+ * A requirement that cannot be checked is NOT met: a shell that reports no
113603
+ * version counts as below the minimum, and an unreadable running version is
113604
+ * named in the result (a failed read is never a pass).
113605
+ */
113606
+ /** `>=MAJOR.MINOR.PATCH[-prerelease]` — the only range shape a requirement uses. */
113607
+ var MinVersionSchema = string().regex(/^>=\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/, "a minimum is written \">=X.Y.Z\"");
113608
+ object({
113609
+ /** The oldest `@camstack/server` this package runs on. */
113610
+ server: MinVersionSchema.optional(),
113611
+ /** The oldest installed version of each named addon package. */
113612
+ addons: record(string(), MinVersionSchema).optional(),
113613
+ /** The oldest shell around the server, per shell kind. */
113614
+ wrapper: object({
113615
+ desktop: MinVersionSchema.optional(),
113616
+ image: MinVersionSchema.optional()
113617
+ }).optional()
113618
+ });
113619
+ /** One unmet requirement; each names the versions on both sides. */
113620
+ var UnmetRequirementSchema = discriminatedUnion("kind", [
113621
+ object({
113622
+ kind: literal("server-too-old"),
113623
+ required: string(),
113624
+ /** `null` — the running server version could not be read. */
113625
+ running: string().nullable()
113626
+ }),
113627
+ object({
113628
+ kind: literal("addon-too-old"),
113629
+ packageName: string(),
113630
+ required: string(),
113631
+ /** `null` — the addon is not installed (or its version is unreadable). */
113632
+ running: string().nullable()
113633
+ }),
113634
+ object({
113635
+ kind: literal("wrapper-too-old"),
113636
+ wrapper: _enum(["desktop", "image"]),
113637
+ required: string(),
113638
+ /** `null` — the wrapper reported no version. */
113639
+ running: string().nullable()
113640
+ })
113641
+ ]);
113642
+ var RequiredAddonUpdateSchema = object({
113643
+ packageName: string(),
113644
+ /** The minimum the target server declares. */
113645
+ required: string(),
113646
+ /** What this node has; `null` when the addon is not installed. */
113647
+ installed: string().nullable()
113648
+ });
113649
+ /**
113454
113650
  * Which integration instance a settings form is for. An addon that serves several instances
113455
113651
  * (`instanceMode: multiple`) can show a different form per instance — an NVR integration
113456
113652
  * needs its connection, the default integration needs the addon's own sections.
@@ -113945,6 +114141,613 @@ var CameraSwitchGroupSchema = object({
113945
114141
  fetchedAt: number()
113946
114142
  });
113947
114143
  /**
114144
+ * Query filter for settings-store collections.
114145
+ */
114146
+ var QueryFilterSchema = object({
114147
+ where: record(string(), unknown()).optional(),
114148
+ whereIn: record(string(), array(unknown())).optional(),
114149
+ whereBetween: record(string(), tuple([unknown(), unknown()])).optional(),
114150
+ /** NULL-safe exclusion: matches rows whose field is NULL OR != the value. */
114151
+ whereNot: record(string(), unknown()).optional(),
114152
+ /** NULL-safe exclusion of a SET — `whereNot` for more than one value. An
114153
+ * empty list excludes nothing. See `QueryFilter.whereNotIn`. */
114154
+ whereNotIn: record(string(), array(unknown())).optional(),
114155
+ orderBy: object({
114156
+ field: string(),
114157
+ direction: _enum(["asc", "desc"])
114158
+ }).optional(),
114159
+ limit: number().optional(),
114160
+ offset: number().optional()
114161
+ });
114162
+ /**
114163
+ * The predicate half of a filter, for BULK MUTATIONS.
114164
+ *
114165
+ * Deliberately not `QueryFilterSchema`: `orderBy` / `limit` / `offset` have no
114166
+ * meaning for a statement that rewrites a set, and accepting them would invite
114167
+ * a caller to believe `limit` bounds the damage. Every field is optional here
114168
+ * only so the shape stays composable — the implementation REJECTS a filter
114169
+ * that compiles to no predicate, because that is the whole collection.
114170
+ */
114171
+ var MutationFilterSchema = object({
114172
+ where: record(string(), unknown()).optional(),
114173
+ whereIn: record(string(), array(unknown())).optional(),
114174
+ whereBetween: record(string(), tuple([unknown(), unknown()])).optional(),
114175
+ whereNot: record(string(), unknown()).optional(),
114176
+ whereNotIn: record(string(), array(unknown())).optional()
114177
+ });
114178
+ /**
114179
+ * One scalar an {@link settingsStoreCapability.methods.aggregate} call asks for.
114180
+ *
114181
+ * `as` names the slot in the result, so the SAME column may be asked twice with
114182
+ * two operations (`MIN(startMs)` and `MAX(startMs)` in one round trip) — which
114183
+ * a `Record<column, op>` shape could not express.
114184
+ */
114185
+ var AggregateFieldSchema = object({
114186
+ /** Result key. */
114187
+ as: string().min(1),
114188
+ /** Column to aggregate. Must be a real column of a declared collection. */
114189
+ field: string().min(1),
114190
+ op: _enum([
114191
+ "sum",
114192
+ "min",
114193
+ "max"
114194
+ ])
114195
+ });
114196
+ /**
114197
+ * `COUNT(*)` plus one number per requested field.
114198
+ *
114199
+ * `null` means NO ROW MATCHED, never zero: a `SUM` over an empty set and a sum
114200
+ * that really is 0 are different facts, and an accounting caller that renders
114201
+ * "0 bytes, oldest = 0" for "nothing here" reports a lie about a disk.
114202
+ */
114203
+ var AggregateResultSchema = object({
114204
+ count: number().int(),
114205
+ values: record(string(), number().nullable())
114206
+ });
114207
+ /** A single stored record: `{ id, data }`. */
114208
+ var SettingsRecordSchema = object({
114209
+ id: string(),
114210
+ data: record(string(), unknown())
114211
+ });
114212
+ /**
114213
+ * One record of a BULK insert — {@link SettingsRecordSchema} with the id made
114214
+ * optional.
114215
+ *
114216
+ * A separate schema rather than loosening the shared one: every other method
114217
+ * on this cap addresses a row BY its id, and making that field optional
114218
+ * everywhere would turn a forgotten key into a silently generated one on
114219
+ * `update` and `delete` as well.
114220
+ */
114221
+ var BulkRecordSchema = object({
114222
+ id: string().optional(),
114223
+ data: record(string(), unknown())
114224
+ });
114225
+ /**
114226
+ * The database FILES one `sqlite-settings` engine process owns (D851).
114227
+ * Closed on purpose: a file is a thing the engine opens, backs up and gives a
114228
+ * serving thread, so a new one is a code change, never a string a caller
114229
+ * invents. `settings` is `camstack.db` and the default for every collection
114230
+ * that names no file.
114231
+ */
114232
+ var DataStoreDatabaseSchema = _enum([
114233
+ "settings",
114234
+ "events",
114235
+ "history"
114236
+ ]);
114237
+ DataStoreDatabaseSchema.options;
114238
+ /**
114239
+ * One write inside a {@link settingsStoreCapability.methods.transact} call.
114240
+ * The three that a "replace this set, bump that revision" write needs, plus
114241
+ * the batched upsert a rollup writer needs.
114242
+ */
114243
+ var TransactOpSchema = discriminatedUnion("op", [
114244
+ object({
114245
+ op: literal("deleteWhere"),
114246
+ collection: string(),
114247
+ filter: MutationFilterSchema
114248
+ }),
114249
+ object({
114250
+ op: literal("insertMany"),
114251
+ collection: string(),
114252
+ records: array(BulkRecordSchema).readonly()
114253
+ }),
114254
+ object({
114255
+ op: literal("update"),
114256
+ collection: string(),
114257
+ id: string(),
114258
+ data: record(string(), unknown())
114259
+ }),
114260
+ object({
114261
+ op: literal("upsertMany"),
114262
+ collection: string(),
114263
+ conflict: array(string()).readonly(),
114264
+ records: array(BulkRecordSchema).readonly()
114265
+ })
114266
+ ]);
114267
+ /**
114268
+ * The precondition of a transaction: the row `key` of `collection` must hold
114269
+ * `equals` in `column`, read INSIDE the transaction.
114270
+ */
114271
+ var TransactGuardSchema = object({
114272
+ collection: string(),
114273
+ key: string(),
114274
+ column: string(),
114275
+ equals: unknown()
114276
+ });
114277
+ /**
114278
+ * Column declaration for a structured (SQL-backed) collection.
114279
+ *
114280
+ * Logical types — the backend translates each to the matching SQLite
114281
+ * storage class and handles per-type marshaling:
114282
+ * - `TEXT` / `INTEGER` / `REAL` — native SQLite types, pass-through
114283
+ * - `JSON` — TEXT under the hood; serialised on write, parsed on read
114284
+ * - `BOOLEAN` — INTEGER 0/1 under the hood; coerced both directions
114285
+ */
114286
+ var CollectionColumnSchema = object({
114287
+ name: string(),
114288
+ type: _enum([
114289
+ "TEXT",
114290
+ "INTEGER",
114291
+ "REAL",
114292
+ "JSON",
114293
+ "BOOLEAN"
114294
+ ]),
114295
+ primaryKey: boolean().optional(),
114296
+ notNull: boolean().optional(),
114297
+ unique: boolean().optional(),
114298
+ /**
114299
+ * Column DEFAULT. Required for B2: the four `ensureTable` consumers declare
114300
+ * columns like `enabled INTEGER NOT NULL DEFAULT 1`, and without this the
114301
+ * collection surface simply cannot express their existing tables — which is
114302
+ * why they were still on the uncapped `table*` API.
114303
+ */
114304
+ defaultValue: union([
114305
+ string(),
114306
+ number(),
114307
+ boolean()
114308
+ ]).optional()
114309
+ });
114310
+ var CollectionIndexSchema = object({
114311
+ name: string(),
114312
+ columns: array(string()).readonly(),
114313
+ unique: boolean().optional()
114314
+ });
114315
+ /**
114316
+ * The input of `declareCollection` — ONE schema for the door and for the
114317
+ * engine behind it, so the two surfaces cannot drift (the dispatcher forwards
114318
+ * the payload untouched).
114319
+ *
114320
+ * - `database` — the file the collection lives in (D851). Absent means
114321
+ * `settings`. A declaration naming a file other than the one the table is
114322
+ * already in is REFUSED by the engine, never moved.
114323
+ * - `primaryKey` — a COMPOSITE key, as column names. A single name is the same
114324
+ * as `primaryKey: true` on that column. A composite-keyed collection has no
114325
+ * `id`: it is written with `insertMany` / `upsertMany` / `transact` and read
114326
+ * with `query` / `count` / `aggregate` / `deleteWhere`; the calls that
114327
+ * address one row by id are refused.
114328
+ * - `withoutRowid` — emit `WITHOUT ROWID`. Requires a primary key.
114329
+ */
114330
+ var DeclareCollectionInputSchema = object({
114331
+ namespace: string().optional(),
114332
+ collection: string(),
114333
+ columns: array(CollectionColumnSchema).readonly(),
114334
+ indexes: array(CollectionIndexSchema).readonly().optional(),
114335
+ database: DataStoreDatabaseSchema.optional(),
114336
+ primaryKey: array(string()).readonly().optional(),
114337
+ withoutRowid: boolean().optional()
114338
+ });
114339
+ method(object({
114340
+ namespace: string().optional(),
114341
+ collection: string(),
114342
+ key: string()
114343
+ }), unknown(), { auth: "admin" }), method(object({
114344
+ namespace: string().optional(),
114345
+ collection: string(),
114346
+ key: string(),
114347
+ value: unknown()
114348
+ }), _void(), {
114349
+ kind: "mutation",
114350
+ auth: "admin"
114351
+ }), method(object({
114352
+ namespace: string().optional(),
114353
+ collection: string(),
114354
+ filter: QueryFilterSchema.optional(),
114355
+ columns: array(string()).readonly().optional()
114356
+ }), array(SettingsRecordSchema).readonly(), { auth: "admin" }), method(object({
114357
+ namespace: string().optional(),
114358
+ collection: string(),
114359
+ /** Declared columns identifying the slot; must be covered by a UNIQUE
114360
+ * index on the collection. Never empty. */
114361
+ conflict: array(string()).readonly(),
114362
+ record: BulkRecordSchema
114363
+ }), object({
114364
+ /** The id the row HAS: assigned by SQLite on an insert, or the existing
114365
+ * row's own id on an update. */
114366
+ id: union([string(), number()]) }), {
114367
+ kind: "mutation",
114368
+ auth: "admin"
114369
+ }), method(object({
114370
+ namespace: string().optional(),
114371
+ collection: string(),
114372
+ conflict: array(string()).readonly(),
114373
+ records: array(BulkRecordSchema).readonly()
114374
+ }), object({ upserted: number().int() }), {
114375
+ kind: "mutation",
114376
+ auth: "admin"
114377
+ }), method(object({
114378
+ namespace: string().optional(),
114379
+ collection: string(),
114380
+ record: object({
114381
+ id: string().optional(),
114382
+ data: record(string(), unknown())
114383
+ })
114384
+ }), object({
114385
+ /**
114386
+ * The id the row ACTUALLY got (D473): the one supplied, the UUID minted
114387
+ * for an absent one, or the ROWID SQLite assigned on an `INTEGER`
114388
+ * primary key — which is the only place an auto key is knowable.
114389
+ */
114390
+ id: union([string(), number()]) }), {
114391
+ kind: "mutation",
114392
+ auth: "admin"
114393
+ }), method(object({
114394
+ namespace: string().optional(),
114395
+ collection: string(),
114396
+ records: array(BulkRecordSchema).readonly()
114397
+ }), object({ inserted: number().int() }), {
114398
+ kind: "mutation",
114399
+ auth: "admin"
114400
+ }), method(object({
114401
+ namespace: string().optional(),
114402
+ guard: TransactGuardSchema.optional(),
114403
+ ops: array(TransactOpSchema).readonly()
114404
+ }), object({
114405
+ applied: boolean(),
114406
+ current: unknown().optional()
114407
+ }), {
114408
+ kind: "mutation",
114409
+ auth: "admin"
114410
+ }), method(object({
114411
+ namespace: string().optional(),
114412
+ collection: string(),
114413
+ id: string(),
114414
+ data: record(string(), unknown())
114415
+ }), _void(), {
114416
+ kind: "mutation",
114417
+ auth: "admin"
114418
+ }), method(object({
114419
+ namespace: string().optional(),
114420
+ collection: string(),
114421
+ key: string()
114422
+ }), _void(), {
114423
+ kind: "mutation",
114424
+ auth: "admin"
114425
+ }), method(object({
114426
+ namespace: string().optional(),
114427
+ collection: string(),
114428
+ filter: MutationFilterSchema
114429
+ }), object({ deleted: number().int() }), {
114430
+ kind: "mutation",
114431
+ auth: "admin"
114432
+ }), method(object({
114433
+ namespace: string().optional(),
114434
+ collection: string(),
114435
+ filter: MutationFilterSchema,
114436
+ data: record(string(), unknown())
114437
+ }), object({ updated: number().int() }), {
114438
+ kind: "mutation",
114439
+ auth: "admin"
114440
+ }), method(object({
114441
+ namespace: string().optional(),
114442
+ collection: string(),
114443
+ filter: QueryFilterSchema.optional()
114444
+ }), number(), { auth: "admin" }), method(object({
114445
+ namespace: string().optional(),
114446
+ collection: string(),
114447
+ fields: array(AggregateFieldSchema).readonly(),
114448
+ filter: QueryFilterSchema.optional()
114449
+ }), AggregateResultSchema, { auth: "admin" }), method(object({
114450
+ namespace: string().optional(),
114451
+ collection: string(),
114452
+ field: string(),
114453
+ bucketSize: number().int().positive(),
114454
+ origin: number().int(),
114455
+ filter: QueryFilterSchema.optional()
114456
+ }), array(object({
114457
+ bucket: number().int(),
114458
+ count: number().int()
114459
+ })).readonly(), { auth: "admin" }), method(object({
114460
+ namespace: string().optional(),
114461
+ collection: string()
114462
+ }), boolean(), { auth: "admin" }), method(DeclareCollectionInputSchema, _void(), {
114463
+ kind: "mutation",
114464
+ auth: "admin"
114465
+ });
114466
+ var MigrationIdSchema = string().regex(/^[a-z0-9-]+\/\d{4}-\d{2}-\d{2}-[a-z0-9-]+$/, "a migration id is \"<owner>/<yyyy-mm-dd>-<slug>\"");
114467
+ var MigrationLevelSchema = _enum([
114468
+ "hub-store",
114469
+ "addon-state",
114470
+ "node-config",
114471
+ "viewer",
114472
+ "addon-internal"
114473
+ ]);
114474
+ var MigrationStoreSchema = _enum([
114475
+ "settings",
114476
+ "events",
114477
+ "history",
114478
+ "node-local",
114479
+ "addon-files"
114480
+ ]);
114481
+ var MigrationScopeSchema = _enum(["cluster", "per-node"]);
114482
+ /** What depends on a migration while it is pending: `block-owner` is the default. */
114483
+ var MigrationOnFailureSchema = _enum(["block-owner", "tolerate"]);
114484
+ /**
114485
+ * `sql` is a hub-only body over the owner's connection. `plan` returns edits the
114486
+ * engine applies inside the migration's own transaction (hub-owned: computed in
114487
+ * that transaction; addon-owned: computed by the owner and shipped to the engine).
114488
+ * `procedure` is addon-owned work that is not SQL (keychain, integration registry,
114489
+ * device tombstones); the ledger records its outcome and it must be idempotent.
114490
+ */
114491
+ var MigrationBodyKindSchema = _enum([
114492
+ "sql",
114493
+ "plan",
114494
+ "procedure"
114495
+ ]);
114496
+ var MigrationMetaSchema = object({
114497
+ id: MigrationIdSchema,
114498
+ /** `hub`, an addon id, `node` or `viewer`. */
114499
+ owner: string().regex(/^[a-z0-9-]+$/),
114500
+ level: MigrationLevelSchema,
114501
+ /** Strictly increasing per owner: the order within an owner. */
114502
+ seq: number().int().positive(),
114503
+ /** Ids of any owner: the order across owners. */
114504
+ dependsOn: array(MigrationIdSchema),
114505
+ store: MigrationStoreSchema,
114506
+ scope: MigrationScopeSchema,
114507
+ onFailure: MigrationOnFailureSchema,
114508
+ bodyKind: MigrationBodyKindSchema,
114509
+ description: string().min(1),
114510
+ /** The marked field the migration's spec must assert survived. */
114511
+ fixtureMark: string().min(1)
114512
+ }).refine((m) => m.id.startsWith(`${m.owner}/`), {
114513
+ message: "the id must start with \"<owner>/\"",
114514
+ path: ["id"]
114515
+ });
114516
+ /** A cycle in the dependency graph, named by the ids on it. */
114517
+ var MigrationCycleError = class extends Error {
114518
+ ids;
114519
+ constructor(ids) {
114520
+ super(`migration dependsOn cycle: ${ids.join(" -> ")}`);
114521
+ this.ids = ids;
114522
+ this.name = "MigrationCycleError";
114523
+ }
114524
+ };
114525
+ /**
114526
+ * Topological order over `dependsOn` plus the implicit edge "a lower `seq` of an
114527
+ * owner runs first". Ties are broken by `(owner, seq)`. Throws
114528
+ * {@link MigrationCycleError} on a cycle. A `dependsOn` id that is not in the list
114529
+ * is ignored here — the catalogue schema is what refuses it.
114530
+ */
114531
+ function orderMigrations(items) {
114532
+ const byId = new Map(items.map((item) => [item.id, item]));
114533
+ const deps = /* @__PURE__ */ new Map();
114534
+ const ownerSorted = /* @__PURE__ */ new Map();
114535
+ for (const item of items) {
114536
+ const list = ownerSorted.get(item.owner) ?? [];
114537
+ list.push(item);
114538
+ ownerSorted.set(item.owner, list);
114539
+ }
114540
+ for (const item of items) deps.set(item.id, new Set(item.dependsOn.filter((id) => byId.has(id))));
114541
+ for (const list of ownerSorted.values()) {
114542
+ list.sort((a, b) => a.seq - b.seq);
114543
+ for (let i = 1; i < list.length; i++) {
114544
+ const previous = list[i - 1];
114545
+ const current = list[i];
114546
+ if (previous !== void 0 && current !== void 0) deps.get(current.id)?.add(previous.id);
114547
+ }
114548
+ }
114549
+ const tie = (a, b) => a.owner < b.owner ? -1 : a.owner > b.owner ? 1 : a.seq - b.seq;
114550
+ const done = /* @__PURE__ */ new Set();
114551
+ const ordered = [];
114552
+ let remaining = [...items];
114553
+ while (remaining.length > 0) {
114554
+ const ready = remaining.filter((item) => [...deps.get(item.id) ?? []].every((dep) => done.has(dep)));
114555
+ if (ready.length === 0) throw new MigrationCycleError(remaining.map((item) => item.id).sort());
114556
+ ready.sort(tie);
114557
+ const next = ready[0];
114558
+ if (next === void 0) break;
114559
+ ordered.push(next);
114560
+ done.add(next.id);
114561
+ remaining = remaining.filter((item) => item.id !== next.id);
114562
+ }
114563
+ return ordered;
114564
+ }
114565
+ /** Every way a list of migrations (in declaration order) is not a catalogue. */
114566
+ function catalogueIssues(items) {
114567
+ const issues = [];
114568
+ const seen = /* @__PURE__ */ new Set();
114569
+ const known = new Set(items.map((item) => item.id));
114570
+ const lastSeq = /* @__PURE__ */ new Map();
114571
+ const seqUsed = /* @__PURE__ */ new Map();
114572
+ for (const item of items) {
114573
+ if (seen.has(item.id)) issues.push({
114574
+ code: "duplicate-id",
114575
+ ids: [item.id],
114576
+ message: `duplicate id ${item.id}`
114577
+ });
114578
+ seen.add(item.id);
114579
+ const used = seqUsed.get(item.owner) ?? /* @__PURE__ */ new Set();
114580
+ if (used.has(item.seq)) issues.push({
114581
+ code: "duplicate-seq",
114582
+ ids: [item.id],
114583
+ message: `owner ${item.owner} has two migrations with seq ${item.seq}`
114584
+ });
114585
+ else if (item.seq < (lastSeq.get(item.owner) ?? 0)) issues.push({
114586
+ code: "seq-order",
114587
+ ids: [item.id],
114588
+ message: `seq ${item.seq} of ${item.id} does not increase in declaration order (owner ${item.owner})`
114589
+ });
114590
+ used.add(item.seq);
114591
+ seqUsed.set(item.owner, used);
114592
+ lastSeq.set(item.owner, Math.max(item.seq, lastSeq.get(item.owner) ?? 0));
114593
+ for (const dep of item.dependsOn) if (!known.has(dep)) issues.push({
114594
+ code: "unknown-dependency",
114595
+ ids: [item.id, dep],
114596
+ message: `${item.id} dependsOn unknown id ${dep}`
114597
+ });
114598
+ }
114599
+ try {
114600
+ orderMigrations(items);
114601
+ } catch (err) {
114602
+ if (err instanceof MigrationCycleError) issues.push({
114603
+ code: "cycle",
114604
+ ids: err.ids,
114605
+ message: err.message
114606
+ });
114607
+ else throw err;
114608
+ }
114609
+ return issues;
114610
+ }
114611
+ array(MigrationMetaSchema).superRefine((items, ctx) => {
114612
+ for (const issue of catalogueIssues(items)) ctx.addIssue({
114613
+ code: "custom",
114614
+ message: `${issue.code}: ${issue.message}`
114615
+ });
114616
+ });
114617
+ /**
114618
+ * The canonical JSON (`preconditionOf`) of the value a plan read; the apply step
114619
+ * re-reads, and a plan whose read no longer matches is refused (the migration is
114620
+ * `failed`, nothing it edited is kept).
114621
+ */
114622
+ var PreconditionSchema = string().min(1).optional();
114623
+ var MigrationPlanSchema = object({ edits: array(discriminatedUnion("op", [
114624
+ object({
114625
+ op: literal("setKeys"),
114626
+ scope: string().min(1),
114627
+ values: record(string(), unknown()),
114628
+ precondition: PreconditionSchema
114629
+ }),
114630
+ object({
114631
+ op: literal("deleteKeys"),
114632
+ scope: string().min(1),
114633
+ keys: array(string().min(1)).min(1),
114634
+ precondition: PreconditionSchema
114635
+ }),
114636
+ object({
114637
+ op: literal("putRow"),
114638
+ table: string().min(1),
114639
+ key: record(string(), unknown()),
114640
+ row: record(string(), unknown()),
114641
+ precondition: PreconditionSchema
114642
+ }),
114643
+ object({
114644
+ op: literal("deleteRow"),
114645
+ table: string().min(1),
114646
+ key: record(string(), unknown()),
114647
+ precondition: PreconditionSchema
114648
+ }),
114649
+ object({
114650
+ op: literal("upsertRecord"),
114651
+ kind: string().min(1),
114652
+ recordKey: string().min(1),
114653
+ value: unknown(),
114654
+ precondition: PreconditionSchema
114655
+ }),
114656
+ object({
114657
+ op: literal("deleteRecord"),
114658
+ kind: string().min(1),
114659
+ recordKey: string().min(1),
114660
+ precondition: PreconditionSchema
114661
+ })
114662
+ ])) });
114663
+ /**
114664
+ * What a `procedure` body reports. `already-done` means the rerun observed the
114665
+ * work finished (a crash between the work and the ledger row, or a manual fix)
114666
+ * and did none of it again; the ledger records `applied` either way.
114667
+ */
114668
+ var MigrationProcedureResultSchema = object({
114669
+ outcome: _enum(["done", "already-done"]),
114670
+ summary: record(string(), unknown())
114671
+ });
114672
+ /** What an owner hands the engine when one of its migrations ran. */
114673
+ var MigrationOwnerResultSchema = discriminatedUnion("kind", [
114674
+ object({
114675
+ kind: literal("plan"),
114676
+ plan: MigrationPlanSchema
114677
+ }),
114678
+ object({
114679
+ kind: literal("procedure"),
114680
+ result: MigrationProcedureResultSchema
114681
+ }),
114682
+ object({
114683
+ kind: literal("failed"),
114684
+ error: string().min(1)
114685
+ })
114686
+ ]);
114687
+ /**
114688
+ * `held`: a restore of a pre-migration snapshot undid this id; it does not rerun
114689
+ * until the operator retries or a new code version ships.
114690
+ * `skipped-newer`: the database knows an id this code does not.
114691
+ */
114692
+ var MigrationStatusSchema = _enum([
114693
+ "applied",
114694
+ "failed",
114695
+ "blocked",
114696
+ "running",
114697
+ "held",
114698
+ "skipped-newer"
114699
+ ]);
114700
+ var MigrationLedgerEntrySchema = object({
114701
+ id: string(),
114702
+ database: DataStoreDatabaseSchema,
114703
+ owner: string(),
114704
+ level: MigrationLevelSchema,
114705
+ seq: number().int().nonnegative(),
114706
+ status: MigrationStatusSchema,
114707
+ attempts: number().int().nonnegative(),
114708
+ appliedAt: number().nullable(),
114709
+ codeVersion: string().nullable(),
114710
+ /** What the apply changed: rows per table, as the engine counted them. */
114711
+ planSummary: record(string(), unknown()).nullable(),
114712
+ progress: record(string(), unknown()).nullable(),
114713
+ error: string().nullable(),
114714
+ /** Why it is `blocked` or `held` (`dependency-failed`, `snapshot-refused`, …). */
114715
+ reason: string().nullable(),
114716
+ nodeId: string().nullable(),
114717
+ bootId: string().nullable()
114718
+ });
114719
+ /** A catalogued migration that is not applied, with where it stands. */
114720
+ var MigrationPendingEntrySchema = object({
114721
+ meta: MigrationMetaSchema,
114722
+ database: DataStoreDatabaseSchema,
114723
+ state: _enum([
114724
+ "pending",
114725
+ "failed",
114726
+ "blocked",
114727
+ "held"
114728
+ ]),
114729
+ reason: string().nullable()
114730
+ });
114731
+ /** Rows per table before and after a dry run, and what it would have changed. */
114732
+ var MigrationDryRunResultSchema = object({
114733
+ id: string(),
114734
+ ok: boolean(),
114735
+ error: string().nullable(),
114736
+ rowsChanged: number().int().nonnegative(),
114737
+ tables: array(object({
114738
+ table: string(),
114739
+ before: number().int().nonnegative(),
114740
+ after: number().int().nonnegative()
114741
+ }))
114742
+ });
114743
+ /** One of an owner's migrations, as the engine tells the owner to treat it this boot. */
114744
+ var MigrationOwnerStepSchema = object({
114745
+ id: MigrationIdSchema,
114746
+ action: _enum(["run", "skip"]),
114747
+ /** The ledger state the engine found: `pending` has no row yet. */
114748
+ state: union([literal("pending"), MigrationStatusSchema])
114749
+ });
114750
+ /**
113948
114751
  * The trainer contract — what the hub hands the cloud trainer and what it
113949
114752
  * gets back (cloud training, D805+).
113950
114753
  *
@@ -117897,6 +118700,41 @@ var ACTUATOR_CAP_VOCABULARY = {
117897
118700
  icon: "droplets"
117898
118701
  }]
117899
118702
  },
118703
+ "power-source": {
118704
+ scope: "device",
118705
+ noun: t("Power source", "Alimentazione"),
118706
+ actions: [
118707
+ {
118708
+ id: "powerSource.on",
118709
+ method: "powerOn",
118710
+ label: t("Power on", "Dai corrente"),
118711
+ phrase: t("Power on {device}", "Dai corrente a {device}"),
118712
+ icon: "power"
118713
+ },
118714
+ {
118715
+ id: "powerSource.off",
118716
+ method: "powerOff",
118717
+ label: t("Power off", "Togli corrente"),
118718
+ phrase: t("Cut the power of {device}", "Togli la corrente a {device}"),
118719
+ icon: "power-off"
118720
+ },
118721
+ {
118722
+ id: "powerSource.forceRestart",
118723
+ method: "forceRestart",
118724
+ label: t("Force restart", "Riavvio forzato"),
118725
+ phrase: t("Force restart {device} by cutting its power", "Riavvia {device} togliendo la corrente"),
118726
+ icon: "refresh-cw"
118727
+ }
118728
+ ],
118729
+ state: [{
118730
+ kind: "boolean",
118731
+ field: "switchOn",
118732
+ label: t("Power switch on", "Presa accesa"),
118733
+ whenTrue: t("When the power of {device} comes on", "Quando arriva la corrente a {device}"),
118734
+ whenFalse: t("When the power of {device} is cut", "Quando manca la corrente a {device}"),
118735
+ icon: "power"
118736
+ }]
118737
+ },
117900
118738
  "lock-control": {
117901
118739
  scope: "device",
117902
118740
  noun: t("Lock", "Serratura"),
@@ -118372,7 +119210,7 @@ var ACTUATOR_CAP_VOCABULARY = {
118372
119210
  method: "setDirection",
118373
119211
  label: t("Spin forward", "Rotazione avanti"),
118374
119212
  phrase: t("Spin {device} forward", "Fai girare {device} in avanti"),
118375
- icon: "rotate-cw",
119213
+ icon: "refresh-cw",
118376
119214
  effect: {
118377
119215
  field: "direction",
118378
119216
  value: "forward"
@@ -121450,6 +122288,7 @@ var SYSTEM_CAP_VOCABULARY = {
121450
122288
  "login-method": system(IDENTITY),
121451
122289
  "mesh-network": system(NETWORK),
121452
122290
  "metrics-provider": system(CLUSTER),
122291
+ migrations: system(INTERNAL),
121453
122292
  "model-convert": system(ML),
121454
122293
  "model-distributor": system(ML),
121455
122294
  "mqtt-broker": system(NETWORK),
@@ -143793,7 +144632,7 @@ method(object({}), array(TargetKindSchema)), method(object({}), array(TargetSche
143793
144632
  * rebuilds without manual reload.
143794
144633
  *
143795
144634
  * The hub-local builtin `addon-pages-aggregator` (see
143796
- * `@camstack/system/builtins/addon-pages-aggregator`) registers the
144635
+ * `@camstack/server/system/builtins/addon-pages-aggregator`) registers the
143797
144636
  * provider. Splitting the public aggregator from the raw collection
143798
144637
  * keeps both ends in codegen — there's no hand-written
143799
144638
  * `addon-pages.router.ts` wrapper anymore.
@@ -144093,7 +144932,7 @@ method(_void(), array(WidgetMetadataSchema).readonly(), { auth: "admin" });
144093
144932
  * manual reload — same scheme used by `addon-pages`.
144094
144933
  *
144095
144934
  * The hub-local builtin `addon-widgets-aggregator` (see
144096
- * `@camstack/system/builtins/addon-widgets-aggregator`) registers the
144935
+ * `@camstack/server/system/builtins/addon-widgets-aggregator`) registers the
144097
144936
  * provider. Splitting the public aggregator from the raw collection
144098
144937
  * keeps both ends in codegen — there's no hand-written wrapper.
144099
144938
  */
@@ -144454,329 +145293,6 @@ method(object({
144454
145293
  password: string()
144455
145294
  }), AuthResultSchema.nullable(), { kind: "mutation" }), method(object({ state: string() }), string()), method(record(string(), string()), AuthResultSchema, { kind: "mutation" }), method(object({ token: string() }), AuthResultSchema.nullable());
144456
145295
  /**
144457
- * Query filter for settings-store collections.
144458
- */
144459
- var QueryFilterSchema = object({
144460
- where: record(string(), unknown()).optional(),
144461
- whereIn: record(string(), array(unknown())).optional(),
144462
- whereBetween: record(string(), tuple([unknown(), unknown()])).optional(),
144463
- /** NULL-safe exclusion: matches rows whose field is NULL OR != the value. */
144464
- whereNot: record(string(), unknown()).optional(),
144465
- /** NULL-safe exclusion of a SET — `whereNot` for more than one value. An
144466
- * empty list excludes nothing. See `QueryFilter.whereNotIn`. */
144467
- whereNotIn: record(string(), array(unknown())).optional(),
144468
- orderBy: object({
144469
- field: string(),
144470
- direction: _enum(["asc", "desc"])
144471
- }).optional(),
144472
- limit: number().optional(),
144473
- offset: number().optional()
144474
- });
144475
- /**
144476
- * The predicate half of a filter, for BULK MUTATIONS.
144477
- *
144478
- * Deliberately not `QueryFilterSchema`: `orderBy` / `limit` / `offset` have no
144479
- * meaning for a statement that rewrites a set, and accepting them would invite
144480
- * a caller to believe `limit` bounds the damage. Every field is optional here
144481
- * only so the shape stays composable — the implementation REJECTS a filter
144482
- * that compiles to no predicate, because that is the whole collection.
144483
- */
144484
- var MutationFilterSchema = object({
144485
- where: record(string(), unknown()).optional(),
144486
- whereIn: record(string(), array(unknown())).optional(),
144487
- whereBetween: record(string(), tuple([unknown(), unknown()])).optional(),
144488
- whereNot: record(string(), unknown()).optional(),
144489
- whereNotIn: record(string(), array(unknown())).optional()
144490
- });
144491
- /**
144492
- * One scalar an {@link settingsStoreCapability.methods.aggregate} call asks for.
144493
- *
144494
- * `as` names the slot in the result, so the SAME column may be asked twice with
144495
- * two operations (`MIN(startMs)` and `MAX(startMs)` in one round trip) — which
144496
- * a `Record<column, op>` shape could not express.
144497
- */
144498
- var AggregateFieldSchema = object({
144499
- /** Result key. */
144500
- as: string().min(1),
144501
- /** Column to aggregate. Must be a real column of a declared collection. */
144502
- field: string().min(1),
144503
- op: _enum([
144504
- "sum",
144505
- "min",
144506
- "max"
144507
- ])
144508
- });
144509
- /**
144510
- * `COUNT(*)` plus one number per requested field.
144511
- *
144512
- * `null` means NO ROW MATCHED, never zero: a `SUM` over an empty set and a sum
144513
- * that really is 0 are different facts, and an accounting caller that renders
144514
- * "0 bytes, oldest = 0" for "nothing here" reports a lie about a disk.
144515
- */
144516
- var AggregateResultSchema = object({
144517
- count: number().int(),
144518
- values: record(string(), number().nullable())
144519
- });
144520
- /** A single stored record: `{ id, data }`. */
144521
- var SettingsRecordSchema = object({
144522
- id: string(),
144523
- data: record(string(), unknown())
144524
- });
144525
- /**
144526
- * One record of a BULK insert — {@link SettingsRecordSchema} with the id made
144527
- * optional.
144528
- *
144529
- * A separate schema rather than loosening the shared one: every other method
144530
- * on this cap addresses a row BY its id, and making that field optional
144531
- * everywhere would turn a forgotten key into a silently generated one on
144532
- * `update` and `delete` as well.
144533
- */
144534
- var BulkRecordSchema = object({
144535
- id: string().optional(),
144536
- data: record(string(), unknown())
144537
- });
144538
- /**
144539
- * The database FILES one `sqlite-settings` engine process owns (D851).
144540
- * Closed on purpose: a file is a thing the engine opens, backs up and gives a
144541
- * serving thread, so a new one is a code change, never a string a caller
144542
- * invents. `settings` is `camstack.db` and the default for every collection
144543
- * that names no file.
144544
- */
144545
- var DataStoreDatabaseSchema = _enum([
144546
- "settings",
144547
- "events",
144548
- "history"
144549
- ]);
144550
- DataStoreDatabaseSchema.options;
144551
- /**
144552
- * One write inside a {@link settingsStoreCapability.methods.transact} call.
144553
- * The three that a "replace this set, bump that revision" write needs, plus
144554
- * the batched upsert a rollup writer needs.
144555
- */
144556
- var TransactOpSchema = discriminatedUnion("op", [
144557
- object({
144558
- op: literal("deleteWhere"),
144559
- collection: string(),
144560
- filter: MutationFilterSchema
144561
- }),
144562
- object({
144563
- op: literal("insertMany"),
144564
- collection: string(),
144565
- records: array(BulkRecordSchema).readonly()
144566
- }),
144567
- object({
144568
- op: literal("update"),
144569
- collection: string(),
144570
- id: string(),
144571
- data: record(string(), unknown())
144572
- }),
144573
- object({
144574
- op: literal("upsertMany"),
144575
- collection: string(),
144576
- conflict: array(string()).readonly(),
144577
- records: array(BulkRecordSchema).readonly()
144578
- })
144579
- ]);
144580
- /**
144581
- * The precondition of a transaction: the row `key` of `collection` must hold
144582
- * `equals` in `column`, read INSIDE the transaction.
144583
- */
144584
- var TransactGuardSchema = object({
144585
- collection: string(),
144586
- key: string(),
144587
- column: string(),
144588
- equals: unknown()
144589
- });
144590
- /**
144591
- * Column declaration for a structured (SQL-backed) collection.
144592
- *
144593
- * Logical types — the backend translates each to the matching SQLite
144594
- * storage class and handles per-type marshaling:
144595
- * - `TEXT` / `INTEGER` / `REAL` — native SQLite types, pass-through
144596
- * - `JSON` — TEXT under the hood; serialised on write, parsed on read
144597
- * - `BOOLEAN` — INTEGER 0/1 under the hood; coerced both directions
144598
- */
144599
- var CollectionColumnSchema = object({
144600
- name: string(),
144601
- type: _enum([
144602
- "TEXT",
144603
- "INTEGER",
144604
- "REAL",
144605
- "JSON",
144606
- "BOOLEAN"
144607
- ]),
144608
- primaryKey: boolean().optional(),
144609
- notNull: boolean().optional(),
144610
- unique: boolean().optional(),
144611
- /**
144612
- * Column DEFAULT. Required for B2: the four `ensureTable` consumers declare
144613
- * columns like `enabled INTEGER NOT NULL DEFAULT 1`, and without this the
144614
- * collection surface simply cannot express their existing tables — which is
144615
- * why they were still on the uncapped `table*` API.
144616
- */
144617
- defaultValue: union([
144618
- string(),
144619
- number(),
144620
- boolean()
144621
- ]).optional()
144622
- });
144623
- var CollectionIndexSchema = object({
144624
- name: string(),
144625
- columns: array(string()).readonly(),
144626
- unique: boolean().optional()
144627
- });
144628
- /**
144629
- * The input of `declareCollection` — ONE schema for the door and for the
144630
- * engine behind it, so the two surfaces cannot drift (the dispatcher forwards
144631
- * the payload untouched).
144632
- *
144633
- * - `database` — the file the collection lives in (D851). Absent means
144634
- * `settings`. A declaration naming a file other than the one the table is
144635
- * already in is REFUSED by the engine, never moved.
144636
- * - `primaryKey` — a COMPOSITE key, as column names. A single name is the same
144637
- * as `primaryKey: true` on that column. A composite-keyed collection has no
144638
- * `id`: it is written with `insertMany` / `upsertMany` / `transact` and read
144639
- * with `query` / `count` / `aggregate` / `deleteWhere`; the calls that
144640
- * address one row by id are refused.
144641
- * - `withoutRowid` — emit `WITHOUT ROWID`. Requires a primary key.
144642
- */
144643
- var DeclareCollectionInputSchema = object({
144644
- namespace: string().optional(),
144645
- collection: string(),
144646
- columns: array(CollectionColumnSchema).readonly(),
144647
- indexes: array(CollectionIndexSchema).readonly().optional(),
144648
- database: DataStoreDatabaseSchema.optional(),
144649
- primaryKey: array(string()).readonly().optional(),
144650
- withoutRowid: boolean().optional()
144651
- });
144652
- method(object({
144653
- namespace: string().optional(),
144654
- collection: string(),
144655
- key: string()
144656
- }), unknown(), { auth: "admin" }), method(object({
144657
- namespace: string().optional(),
144658
- collection: string(),
144659
- key: string(),
144660
- value: unknown()
144661
- }), _void(), {
144662
- kind: "mutation",
144663
- auth: "admin"
144664
- }), method(object({
144665
- namespace: string().optional(),
144666
- collection: string(),
144667
- filter: QueryFilterSchema.optional(),
144668
- columns: array(string()).readonly().optional()
144669
- }), array(SettingsRecordSchema).readonly(), { auth: "admin" }), method(object({
144670
- namespace: string().optional(),
144671
- collection: string(),
144672
- /** Declared columns identifying the slot; must be covered by a UNIQUE
144673
- * index on the collection. Never empty. */
144674
- conflict: array(string()).readonly(),
144675
- record: BulkRecordSchema
144676
- }), object({
144677
- /** The id the row HAS: assigned by SQLite on an insert, or the existing
144678
- * row's own id on an update. */
144679
- id: union([string(), number()]) }), {
144680
- kind: "mutation",
144681
- auth: "admin"
144682
- }), method(object({
144683
- namespace: string().optional(),
144684
- collection: string(),
144685
- conflict: array(string()).readonly(),
144686
- records: array(BulkRecordSchema).readonly()
144687
- }), object({ upserted: number().int() }), {
144688
- kind: "mutation",
144689
- auth: "admin"
144690
- }), method(object({
144691
- namespace: string().optional(),
144692
- collection: string(),
144693
- record: object({
144694
- id: string().optional(),
144695
- data: record(string(), unknown())
144696
- })
144697
- }), object({
144698
- /**
144699
- * The id the row ACTUALLY got (D473): the one supplied, the UUID minted
144700
- * for an absent one, or the ROWID SQLite assigned on an `INTEGER`
144701
- * primary key — which is the only place an auto key is knowable.
144702
- */
144703
- id: union([string(), number()]) }), {
144704
- kind: "mutation",
144705
- auth: "admin"
144706
- }), method(object({
144707
- namespace: string().optional(),
144708
- collection: string(),
144709
- records: array(BulkRecordSchema).readonly()
144710
- }), object({ inserted: number().int() }), {
144711
- kind: "mutation",
144712
- auth: "admin"
144713
- }), method(object({
144714
- namespace: string().optional(),
144715
- guard: TransactGuardSchema.optional(),
144716
- ops: array(TransactOpSchema).readonly()
144717
- }), object({
144718
- applied: boolean(),
144719
- current: unknown().optional()
144720
- }), {
144721
- kind: "mutation",
144722
- auth: "admin"
144723
- }), method(object({
144724
- namespace: string().optional(),
144725
- collection: string(),
144726
- id: string(),
144727
- data: record(string(), unknown())
144728
- }), _void(), {
144729
- kind: "mutation",
144730
- auth: "admin"
144731
- }), method(object({
144732
- namespace: string().optional(),
144733
- collection: string(),
144734
- key: string()
144735
- }), _void(), {
144736
- kind: "mutation",
144737
- auth: "admin"
144738
- }), method(object({
144739
- namespace: string().optional(),
144740
- collection: string(),
144741
- filter: MutationFilterSchema
144742
- }), object({ deleted: number().int() }), {
144743
- kind: "mutation",
144744
- auth: "admin"
144745
- }), method(object({
144746
- namespace: string().optional(),
144747
- collection: string(),
144748
- filter: MutationFilterSchema,
144749
- data: record(string(), unknown())
144750
- }), object({ updated: number().int() }), {
144751
- kind: "mutation",
144752
- auth: "admin"
144753
- }), method(object({
144754
- namespace: string().optional(),
144755
- collection: string(),
144756
- filter: QueryFilterSchema.optional()
144757
- }), number(), { auth: "admin" }), method(object({
144758
- namespace: string().optional(),
144759
- collection: string(),
144760
- fields: array(AggregateFieldSchema).readonly(),
144761
- filter: QueryFilterSchema.optional()
144762
- }), AggregateResultSchema, { auth: "admin" }), method(object({
144763
- namespace: string().optional(),
144764
- collection: string(),
144765
- field: string(),
144766
- bucketSize: number().int().positive(),
144767
- origin: number().int(),
144768
- filter: QueryFilterSchema.optional()
144769
- }), array(object({
144770
- bucket: number().int(),
144771
- count: number().int()
144772
- })).readonly(), { auth: "admin" }), method(object({
144773
- namespace: string().optional(),
144774
- collection: string()
144775
- }), boolean(), { auth: "admin" }), method(DeclareCollectionInputSchema, _void(), {
144776
- kind: "mutation",
144777
- auth: "admin"
144778
- });
144779
- /**
144780
145296
  * How long one snapshot may take end to end: the pinned copy of the whole
144781
145297
  * database plus `PRAGMA integrity_check` over the copy, on the hub's slowest
144782
145298
  * disk. ~800 MB at a pessimistic 10 MB/s is under 3 minutes; the bound is
@@ -146690,7 +147206,7 @@ method(object({
146690
147206
  kind: "mutation",
146691
147207
  auth: "admin"
146692
147208
  }), method(object({ blockIds: array(string()) }), object({ states: array(CompositionBlockStateSchema) }), { auth: "admin" });
146693
- DeviceType.Camera, DeviceType.Sensor, DeviceType.Button, DeviceType.Switch, DeviceType.PetFeeder, DeviceType.Container, DeviceType.Thermostat, DeviceType.EventEmitter, DeviceType.Camera, DeviceType.Hub, DeviceType.Light, DeviceType.Siren, DeviceType.Switch, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Fan, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder, DeviceType.PetFountain, DeviceType.Dishwasher, DeviceType.Washer, DeviceType.Dryer, DeviceType.Gateway, DeviceType.Camera, DeviceType.Lock, DeviceType.Camera, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.MediaPlayer, DeviceType.WaterHeater, DeviceType.Humidifier, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.PetFeeder;
147209
+ DeviceType.Camera, DeviceType.Sensor, DeviceType.Button, DeviceType.Switch, DeviceType.PetFeeder, DeviceType.Container, DeviceType.Thermostat, DeviceType.EventEmitter, DeviceType.Camera, DeviceType.Hub, DeviceType.Light, DeviceType.Siren, DeviceType.Switch, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Fan, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder, DeviceType.PetFountain, DeviceType.Dishwasher, DeviceType.Washer, DeviceType.Dryer, DeviceType.Gateway, DeviceType.Camera, DeviceType.Lock, DeviceType.Camera, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Gateway, DeviceType.Hub, DeviceType.Camera, DeviceType.Sensor, DeviceType.MediaPlayer, DeviceType.Vacuum, DeviceType.Generic, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.PetFountain, DeviceType.Fan, DeviceType.Humidifier, DeviceType.Climate, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.MediaPlayer, DeviceType.WaterHeater, DeviceType.Humidifier, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.PetFeeder;
146694
147210
  var NUMBER = {
146695
147211
  kind: "fixed",
146696
147212
  result: ["number"]
@@ -147701,6 +148217,57 @@ id: union([string(), number()]) }), {
147701
148217
  kind: "mutation",
147702
148218
  auth: "admin"
147703
148219
  });
148220
+ method(object({}), object({ entries: array(MigrationLedgerEntrySchema) }), { auth: "admin" }), method(object({ owner: string().optional() }), object({ pending: array(MigrationPendingEntrySchema) }), { auth: "admin" }), method(object({ id: string().min(1) }), MigrationDryRunResultSchema, { auth: "admin" }), method(object({
148221
+ id: string().min(1),
148222
+ error: string().min(1)
148223
+ }), MigrationLedgerEntrySchema, {
148224
+ kind: "mutation",
148225
+ auth: "admin"
148226
+ }), method(object({ id: string().min(1) }), object({ entries: array(MigrationLedgerEntrySchema) }), {
148227
+ kind: "mutation",
148228
+ auth: "admin"
148229
+ }), method(object({
148230
+ database: DataStoreDatabaseSchema,
148231
+ snapshotId: string().min(1)
148232
+ }), object({
148233
+ scheduled: boolean(),
148234
+ held: array(string()),
148235
+ restartRequired: boolean()
148236
+ }), {
148237
+ kind: "mutation",
148238
+ auth: "admin"
148239
+ }), method(object({
148240
+ owner: string().min(1),
148241
+ metas: array(MigrationMetaSchema)
148242
+ }), object({
148243
+ bootId: string(),
148244
+ snapshot: _enum([
148245
+ "none",
148246
+ "written",
148247
+ "refused"
148248
+ ]),
148249
+ steps: array(MigrationOwnerStepSchema)
148250
+ }), {
148251
+ kind: "mutation",
148252
+ auth: "admin"
148253
+ }), method(object({ id: string().min(1) }), object({
148254
+ proceed: boolean(),
148255
+ state: union([literal("pending"), MigrationStatusSchema]),
148256
+ reason: string().nullable()
148257
+ }), {
148258
+ kind: "mutation",
148259
+ auth: "admin"
148260
+ }), method(object({
148261
+ id: string().min(1),
148262
+ result: MigrationOwnerResultSchema
148263
+ }), object({
148264
+ status: MigrationStatusSchema,
148265
+ error: string().nullable(),
148266
+ rowsChanged: number().int().nonnegative()
148267
+ }), {
148268
+ kind: "mutation",
148269
+ auth: "admin"
148270
+ });
147704
148271
  /**
147705
148272
  * records - an addon's own facts and documents (D881): ledgers, audit rows,
147706
148273
  * config documents, mirrors. The third storage API next to `ctx.settings`
@@ -162605,7 +163172,18 @@ var ServerUpdateActionResultSchema = object({
162605
163172
  * store (D877), present when the pre-flight refused the update
162606
163173
  * (`reason: 'addon-would-refuse'`). Optional for version skew.
162607
163174
  */
162608
- blockers: array(UpdateBlockerSchema).optional()
163175
+ blockers: array(UpdateBlockerSchema).optional(),
163176
+ /**
163177
+ * The minimums the target server declares and this node does not meet
163178
+ * (D989), present when the update was refused with
163179
+ * `reason: 'requirements-unmet'`. Each names the versions on both sides.
163180
+ */
163181
+ unmet: array(UnmetRequirementSchema).optional(),
163182
+ /**
163183
+ * The addons this node has below the minimum the staged server declares —
163184
+ * the ones to update WITH the server. Present on an accepted update.
163185
+ */
163186
+ requiredAddonUpdates: array(RequiredAddonUpdateSchema).optional()
162609
163187
  });
162610
163188
  var LaunchOverridesSchema = object({
162611
163189
  /** The persisted heap ceiling in MB, or `null` when none is set. */
@@ -162718,7 +163296,7 @@ var SnapshotImageSchema = object({
162718
163296
  * straight from the camera (HTTP snapshot URL, ONVIF action, etc.).
162719
163297
  *
162720
163298
  * - **Wrapper** providers (kind:'wrapper'): register as a system
162721
- * provider (SnapshotAddon in `@camstack/system/builtins/snapshot`). The
163299
+ * provider (SnapshotAddon in `@camstack/server/system/builtins/snapshot`). The
162722
163300
  * wrapper owns the cache and invokes the native via
162723
163301
  * `ctx.getNativeProvider(snapshotCapability, deviceId)` on miss.
162724
163302
  *
@@ -165671,7 +166249,9 @@ var PackageUpdateSchema = object({
165671
166249
  latestVersion: string(),
165672
166250
  category: _enum(["addon", "core"]),
165673
166251
  requiresRestart: boolean(),
165674
- isSystem: boolean()
166252
+ isSystem: boolean(),
166253
+ /** Held: what the newer version needs and this node does not meet (D989). */
166254
+ blockedBy: array(UnmetRequirementSchema).readonly().optional()
165675
166255
  });
165676
166256
  var PackageVersionInfoSchema = object({
165677
166257
  version: string(),
@@ -167127,19 +167707,19 @@ var RemoteSetConfigResultSchema = discriminatedUnion("status", [object({ status:
167127
167707
  reason: _enum(["unknown-template", "not-a-media-player"]),
167128
167708
  detail: string()
167129
167709
  })]);
167130
- var deviceInput = object({ deviceId: number().int().nonnegative() });
167710
+ var deviceInput$1 = object({ deviceId: number().int().nonnegative() });
167131
167711
  DeviceType.MediaPlayer;
167132
- method(deviceInput, RemoteConfigSchema, { auth: "admin" }), method(deviceInput.extend({ config: RemoteConfigSchema }), RemoteSetConfigResultSchema, {
167712
+ method(deviceInput$1, RemoteConfigSchema, { auth: "admin" }), method(deviceInput$1.extend({ config: RemoteConfigSchema }), RemoteSetConfigResultSchema, {
167133
167713
  kind: "mutation",
167134
167714
  auth: "admin"
167135
- }), method(deviceInput, RemoteCommandListSchema, { auth: "protected" }), method(deviceInput.extend({
167715
+ }), method(deviceInput$1, RemoteCommandListSchema, { auth: "protected" }), method(deviceInput$1.extend({
167136
167716
  role: RemoteRoleSchema,
167137
167717
  /** Sends in a row (volume steps). */
167138
167718
  repeat: number().int().min(1).max(50).default(1)
167139
167719
  }), RemoteSendResultSchema, {
167140
167720
  kind: "mutation",
167141
167721
  auth: "protected"
167142
- }), method(deviceInput.extend({
167722
+ }), method(deviceInput$1.extend({
167143
167723
  command: RemoteMediaCommandSchema,
167144
167724
  muted: boolean().optional(),
167145
167725
  source: string().optional(),
@@ -170954,7 +171534,7 @@ var lawnMowerControlCapability = {
170954
171534
  * clients — they reverse-connect to the hub. Exposing their interfaces
170955
171535
  * via the same surface would leak internal topology with no upside.
170956
171536
  *
170957
- * Implementation in `@camstack/system/builtins/local-network/`.
171537
+ * Implementation in `@camstack/server/system/builtins/local-network/`.
170958
171538
  */
170959
171539
  /** Coarse classification derived from the interface name + IP range. */
170960
171540
  var InterfaceKindEnum = _enum([
@@ -171451,6 +172031,117 @@ var lockControlCapability = {
171451
172031
  * whether persisting is worth a SQLite commit. */
171452
172032
  volatileStateFields: ["lastChangedAt"]
171453
172033
  };
172034
+ var PowerSourceRefusalReasonSchema = _enum([
172035
+ "no-power-source",
172036
+ "switch-missing",
172037
+ "switch-offline",
172038
+ "switch-unknown",
172039
+ "restart-in-progress",
172040
+ "switch-call-failed"
172041
+ ]);
172042
+ var PowerSourceActionResultSchema = discriminatedUnion("accepted", [object({ accepted: literal(true) }), object({
172043
+ accepted: literal(false),
172044
+ reason: PowerSourceRefusalReasonSchema,
172045
+ /** One line for the operator, naming the switch. */
172046
+ message: string()
172047
+ })]);
172048
+ var PowerSourceInfoSchema = object({
172049
+ /** False when the device has no power source linked; the rest is then empty. */
172050
+ linked: boolean(),
172051
+ switchDeviceId: number().nullable(),
172052
+ switchName: string().nullable(),
172053
+ /** The switch's on/off state, or null when it is unknown. Never false for unknown (D393). */
172054
+ switchOn: boolean().nullable(),
172055
+ switchOnline: boolean().nullable(),
172056
+ restartInProgress: boolean(),
172057
+ offSeconds: number().int()
172058
+ });
172059
+ /** The composed slice: the switch state the link mirrors, plus the stored off time. */
172060
+ var PowerSourceStateSchema = object({
172061
+ /** Null while the switch has not reported: unknown is never `false` (D393). */
172062
+ switchOn: boolean().nullable(),
172063
+ offSeconds: number().int(),
172064
+ lastFetchedAt: number()
172065
+ });
172066
+ var POWER_SOURCE_DEVICE_TYPES = [
172067
+ DeviceType.Gateway,
172068
+ DeviceType.Hub,
172069
+ DeviceType.Camera,
172070
+ DeviceType.Sensor,
172071
+ DeviceType.MediaPlayer,
172072
+ DeviceType.Vacuum,
172073
+ DeviceType.Generic,
172074
+ DeviceType.Washer,
172075
+ DeviceType.Dryer,
172076
+ DeviceType.Dishwasher,
172077
+ DeviceType.PetFountain,
172078
+ DeviceType.Fan,
172079
+ DeviceType.Humidifier,
172080
+ DeviceType.Climate,
172081
+ DeviceType.WaterHeater
172082
+ ];
172083
+ var powerSourceCustomize = {
172084
+ version: 1,
172085
+ targets: [{
172086
+ id: "switch",
172087
+ cardinality: "scalar",
172088
+ vocabulary: {
172089
+ cap: "power-source",
172090
+ stateField: "switchOn"
172091
+ },
172092
+ deviceTypes: POWER_SOURCE_DEVICE_TYPES,
172093
+ apply: "add",
172094
+ path: "switchOn",
172095
+ sources: [{
172096
+ kind: "switch",
172097
+ transforms: [{ kind: "identity" }]
172098
+ }],
172099
+ companions: [{
172100
+ kind: "constant",
172101
+ path: "offSeconds",
172102
+ value: 10,
172103
+ when: "add"
172104
+ }, {
172105
+ kind: "clock",
172106
+ path: "lastFetchedAt",
172107
+ when: "always"
172108
+ }],
172109
+ sourceReport: { status: "assignment-status" }
172110
+ }]
172111
+ };
172112
+ var deviceInput = object({ deviceId: number().int().nonnegative() });
172113
+ var powerSourceCapability = {
172114
+ name: "power-source",
172115
+ scope: "device",
172116
+ mode: "singleton",
172117
+ deviceTypes: POWER_SOURCE_DEVICE_TYPES,
172118
+ methods: {
172119
+ getStatus: systemMethod(deviceInput, PowerSourceInfoSchema, { auth: "admin" }),
172120
+ powerOn: systemMethod(deviceInput, PowerSourceActionResultSchema, {
172121
+ kind: "mutation",
172122
+ auth: "admin"
172123
+ }),
172124
+ powerOff: systemMethod(deviceInput, PowerSourceActionResultSchema, {
172125
+ kind: "mutation",
172126
+ auth: "admin"
172127
+ }),
172128
+ forceRestart: systemMethod(deviceInput, PowerSourceActionResultSchema, {
172129
+ kind: "mutation",
172130
+ auth: "admin"
172131
+ })
172132
+ },
172133
+ runtimeState: PowerSourceStateSchema,
172134
+ customize: powerSourceCustomize,
172135
+ /**
172136
+ * Runtime-state durability: **session** — mirrored from the linked switch on
172137
+ * its next report; a restored value would outlive the reading it names.
172138
+ *
172139
+ * See `RuntimeStateDurability`. Enforced by
172140
+ * `scripts/check-runtime-state-durability.ts`.
172141
+ */
172142
+ durability: "session",
172143
+ history: "none"
172144
+ };
171454
172145
  /**
171455
172146
  * A monotonic total between resets (D944). Energy uses `power-meter.energy`
171456
172147
  * so dashboards keep one series; every other instantaneous reading lands here.
@@ -172751,7 +173442,7 @@ onStatusChanged: { data: object({
172751
173442
  * jitter, and observed/peak bandwidth per device + per client.
172752
173443
  *
172753
173444
  * Implementation lives in the server's `NetworkQualityService` (thin
172754
- * wrapper over the shared `NetworkQualityTracker` from `@camstack/system`).
173445
+ * wrapper over the shared `NetworkQualityTracker` from `@camstack/server/system`).
172755
173446
  * The provider is registered from `trpc.router.ts` against the existing
172756
173447
  * service instance — no addon owns this state.
172757
173448
  *
@@ -175060,7 +175751,7 @@ var ptzAutotrackCapability = {
175060
175751
  * - **Native**: the camera's own firmware autotrack, registered per device by
175061
175752
  * the driver when the probe says it exists (Reolink: yes; Hikvision:
175062
175753
  * model-dependent).
175063
- * - **Wrapper**: the emulated implementation (`@camstack/system`'s `autotrack` builtin),
175754
+ * - **Wrapper**: the emulated implementation (`@camstack/server/system`'s `autotrack` builtin),
175064
175755
  * which follows a subject with `ptz.move` pulses driven by the detection
175065
175756
  * pipeline. `defaultActive: false` — the operator binds it per camera in
175066
175757
  * the bindings UI.
@@ -180910,6 +181601,7 @@ var DEVICE_LOCAL_STATE_CAPS = {
180910
181601
  petFeeder: petFeederCapability,
180911
181602
  petFountain: petFountainCapability,
180912
181603
  powerMeter: powerMeterCapability,
181604
+ powerSource: powerSourceCapability,
180913
181605
  presence: presenceCapability,
180914
181606
  pressureSensor: pressureSensorCapability,
180915
181607
  privacyMask: privacyMaskCapability,
@@ -186674,6 +187366,69 @@ Object.freeze({
186674
187366
  access: "view",
186675
187367
  role: "admin"
186676
187368
  },
187369
+ "migrations.declareOwner": {
187370
+ capName: "migrations",
187371
+ capScope: "system",
187372
+ addonId: null,
187373
+ access: "create",
187374
+ role: "admin"
187375
+ },
187376
+ "migrations.dryRun": {
187377
+ capName: "migrations",
187378
+ capScope: "system",
187379
+ addonId: null,
187380
+ access: "view",
187381
+ role: "admin"
187382
+ },
187383
+ "migrations.finishMigration": {
187384
+ capName: "migrations",
187385
+ capScope: "system",
187386
+ addonId: null,
187387
+ access: "create",
187388
+ role: "admin"
187389
+ },
187390
+ "migrations.listLedger": {
187391
+ capName: "migrations",
187392
+ capScope: "system",
187393
+ addonId: null,
187394
+ access: "view",
187395
+ role: "admin"
187396
+ },
187397
+ "migrations.listPending": {
187398
+ capName: "migrations",
187399
+ capScope: "system",
187400
+ addonId: null,
187401
+ access: "view",
187402
+ role: "admin"
187403
+ },
187404
+ "migrations.markFailed": {
187405
+ capName: "migrations",
187406
+ capScope: "system",
187407
+ addonId: null,
187408
+ access: "create",
187409
+ role: "admin"
187410
+ },
187411
+ "migrations.restorePreMigration": {
187412
+ capName: "migrations",
187413
+ capScope: "system",
187414
+ addonId: null,
187415
+ access: "create",
187416
+ role: "admin"
187417
+ },
187418
+ "migrations.retry": {
187419
+ capName: "migrations",
187420
+ capScope: "system",
187421
+ addonId: null,
187422
+ access: "create",
187423
+ role: "admin"
187424
+ },
187425
+ "migrations.startMigration": {
187426
+ capName: "migrations",
187427
+ capScope: "system",
187428
+ addonId: null,
187429
+ access: "create",
187430
+ role: "admin"
187431
+ },
186677
187432
  "modelConvert.convert": {
186678
187433
  capName: "model-convert",
186679
187434
  capScope: "system",
@@ -189306,6 +190061,34 @@ Object.freeze({
189306
190061
  access: "view",
189307
190062
  role: "viewer"
189308
190063
  },
190064
+ "powerSource.forceRestart": {
190065
+ capName: "power-source",
190066
+ capScope: "device",
190067
+ addonId: null,
190068
+ access: "create",
190069
+ role: "admin"
190070
+ },
190071
+ "powerSource.getStatus": {
190072
+ capName: "power-source",
190073
+ capScope: "device",
190074
+ addonId: null,
190075
+ access: "view",
190076
+ role: "admin"
190077
+ },
190078
+ "powerSource.powerOff": {
190079
+ capName: "power-source",
190080
+ capScope: "device",
190081
+ addonId: null,
190082
+ access: "create",
190083
+ role: "admin"
190084
+ },
190085
+ "powerSource.powerOn": {
190086
+ capName: "power-source",
190087
+ capScope: "device",
190088
+ addonId: null,
190089
+ access: "create",
190090
+ role: "admin"
190091
+ },
189309
190092
  "presence.getStatus": {
189310
190093
  capName: "presence",
189311
190094
  capScope: "device",
@@ -194606,6 +195389,26 @@ Object.freeze({
194606
195389
  form: "single",
194607
195390
  optional: false
194608
195391
  }],
195392
+ "powerSource.forceRestart": [{
195393
+ name: "deviceId",
195394
+ form: "single",
195395
+ optional: false
195396
+ }],
195397
+ "powerSource.getStatus": [{
195398
+ name: "deviceId",
195399
+ form: "single",
195400
+ optional: false
195401
+ }],
195402
+ "powerSource.powerOff": [{
195403
+ name: "deviceId",
195404
+ form: "single",
195405
+ optional: false
195406
+ }],
195407
+ "powerSource.powerOn": [{
195408
+ name: "deviceId",
195409
+ form: "single",
195410
+ optional: false
195411
+ }],
194609
195412
  "presence.getStatus": [{
194610
195413
  name: "deviceId",
194611
195414
  form: "single",
@@ -203947,6 +204750,14 @@ var ReolinkHubSession = class {
203947
204750
  get isOnline() {
203948
204751
  return this.#online;
203949
204752
  }
204753
+ /** The integration's display name. */
204754
+ get name() {
204755
+ return this.#deps.name;
204756
+ }
204757
+ /** Restart the hub itself (not a camera behind it). */
204758
+ async reboot() {
204759
+ await (await this.getApi()).reboot();
204760
+ }
203950
204761
  /** The last channel list, for the adoption surface. */
203951
204762
  channelSnapshot() {
203952
204763
  return this.#snapshot;
@@ -204405,6 +205216,28 @@ function toDiscoveredChild(channel, adoptedDeviceId) {
204405
205216
  adoptedDeviceId
204406
205217
  };
204407
205218
  }
205219
+ var GATEWAY_SUFFIX = ":gateway";
205220
+ /** The `childNativeId` of the hub box itself, one per integration. */
205221
+ function hubGatewayNativeId(integrationId) {
205222
+ return `${integrationId}${GATEWAY_SUFFIX}`;
205223
+ }
205224
+ /**
205225
+ * The hub (NVR / Home Hub) as an adoption candidate: a {@link DeviceType.Gateway} that is a
205226
+ * sibling of its channel cameras, never their parent. Only what the library really exposes:
205227
+ * `reboot` (Baichuan `Reboot`) and the unit's web page.
205228
+ */
205229
+ function toDiscoveredGateway(input) {
205230
+ return {
205231
+ childNativeId: hubGatewayNativeId(input.integrationId),
205232
+ name: input.name,
205233
+ type: DeviceType.Gateway,
205234
+ status: input.online ? "online" : "offline",
205235
+ metadata: { manufacturer: "Reolink" },
205236
+ alreadyAdopted: input.adoptedDeviceId !== null,
205237
+ adoptedDeviceId: input.adoptedDeviceId,
205238
+ capabilities: [rebootCapability.name, deviceAdminLinkCapability.name]
205239
+ };
205240
+ }
204408
205241
  /**
204409
205242
  * The per-integration registry the cameras reach their session through (the shape of
204410
205243
  * `veluxGateways` / `enpalConnections`). It also holds the children, so a child that activates
@@ -217679,7 +218512,13 @@ function buildReolinkAdoptionProvider(deps) {
217679
218512
  const session = getSession(integrationId);
217680
218513
  if (session === null) return [];
217681
218514
  const adopted = await listAdopted();
217682
- return session.channelSnapshot().channels.map((channel) => toDiscoveredChild(channel, findAdoptedForChannel(adopted, integrationId, channel)));
218515
+ const gateways = await deps.listAdoptedGateways();
218516
+ return [toDiscoveredGateway({
218517
+ integrationId,
218518
+ name: session.name,
218519
+ online: session.isOnline,
218520
+ adoptedDeviceId: gateways.find((g) => g.integrationId === integrationId)?.id ?? null
218521
+ }), ...session.channelSnapshot().channels.map((channel) => toDiscoveredChild(channel, findAdoptedForChannel(adopted, integrationId, channel)))];
217683
218522
  }
217684
218523
  async function status(integrationId) {
217685
218524
  const all = await candidates(integrationId);
@@ -217740,6 +218579,19 @@ function buildReolinkAdoptionProvider(deps) {
217740
218579
  const adopted = [];
217741
218580
  let failures = 0;
217742
218581
  for (const childNativeId of childNativeIds) try {
218582
+ if (childNativeId === hubGatewayNativeId(integrationId)) {
218583
+ const known = (await deps.listAdoptedGateways()).find((g) => g.integrationId === integrationId);
218584
+ const { deviceId } = known !== void 0 ? { deviceId: known.id } : await deps.adoptGateway({
218585
+ integrationId,
218586
+ name: perCandidate?.[childNativeId]?.name ?? session.name
218587
+ });
218588
+ adopted.push({
218589
+ childNativeId,
218590
+ parentDeviceId: deviceId,
218591
+ accessoryDeviceIds: []
218592
+ });
218593
+ continue;
218594
+ }
217743
218595
  const channel = session.channelSnapshot().channels.find((c) => c.childNativeId === childNativeId);
217744
218596
  if (channel === void 0) {
217745
218597
  logger.warn("reolink adopt: the hub lists no such channel — skipping", { meta: {
@@ -217786,7 +218638,7 @@ function buildReolinkAdoptionProvider(deps) {
217786
218638
  },
217787
218639
  resync: async ({ camDeviceId }) => {
217788
218640
  const cfg = await deps.findDeviceConfig(camDeviceId);
217789
- if (cfg === null || typeof cfg["integrationId"] !== "string") throw new Error(`reolink resync: device ${camDeviceId} is not an NVR-adopted camera`);
218641
+ if (cfg === null || typeof cfg["integrationId"] !== "string") throw new Error(`reolink resync: device ${camDeviceId} is not an NVR-adopted device`);
217790
218642
  return {
217791
218643
  changed: false,
217792
218644
  rebuiltChildren: 0
@@ -218024,6 +218876,86 @@ async function retireMigratedHubs(retirements, ports) {
218024
218876
  return retired;
218025
218877
  }
218026
218878
  //#endregion
218879
+ //#region src/reolink-gateway-device.ts
218880
+ var reolinkGatewayDeviceSchema = object({
218881
+ integrationId: string().min(1).describe("Integration (hub session) the device belongs to"),
218882
+ /** The adoption candidate id (`<integration>:gateway`). */
218883
+ nativeId: string().min(1)
218884
+ });
218885
+ /**
218886
+ * The physical NVR / Home Hub, as a {@link DeviceType.Gateway}. Its connection and password
218887
+ * live on the INTEGRATION; this device is the box's presence in the device list, and carries
218888
+ * the actions the library really supports through existing caps:
218889
+ *
218890
+ * - `reboot` (Baichuan `Reboot`, no channel), advertised as {@link DeviceFeature.Rebootable};
218891
+ * - `device-admin-link`, the unit's own web page.
218892
+ *
218893
+ * Cameras are NOT children of it: they are adopted as their own top-level devices of the
218894
+ * same integration. The addon flips `online` when the hub socket changes.
218895
+ */
218896
+ var ReolinkGatewayDevice = class extends BaseDevice {
218897
+ features = [DeviceFeature.Rebootable];
218898
+ integrationId;
218899
+ constructor(ctx) {
218900
+ const cfg = reolinkGatewayDeviceSchema.parse(ctx.persistedConfig ?? {});
218901
+ super(ctx, reolinkGatewayDeviceSchema, { type: DeviceType.Gateway });
218902
+ this.integrationId = cfg.integrationId;
218903
+ this.online = reolinkHubSessions.get(this.integrationId)?.isOnline === true;
218904
+ this.registerRebootCap();
218905
+ this.registerAdminLinkCap();
218906
+ this.updateSourceInfo({
218907
+ id: cfg.nativeId,
218908
+ system: "reolink"
218909
+ });
218910
+ }
218911
+ registerRebootCap() {
218912
+ this.ctx.registerNativeCap(rebootCapability, { reboot: async ({ deviceId }) => {
218913
+ if (deviceId !== this.id) throw new Error(`ReolinkGatewayDevice: deviceId mismatch, expected ${this.id}, got ${deviceId}`);
218914
+ const session = reolinkHubSessions.get(this.integrationId);
218915
+ if (session === null) {
218916
+ this.ctx.logger.warn("Reolink hub reboot refused: no live session", { tags: { deviceId: this.id } });
218917
+ throw new Error("The hub session is down, so it cannot be told to reboot.");
218918
+ }
218919
+ await session.reboot();
218920
+ this.ctx.logger.info("Reolink hub reboot requested", { tags: { deviceId: this.id } });
218921
+ return { success: true };
218922
+ } });
218923
+ }
218924
+ /**
218925
+ * The hub's web interface, on the hub host. The password is NOT placed in the URL; the page
218926
+ * asks for it.
218927
+ */
218928
+ registerAdminLinkCap() {
218929
+ this.ctx.registerNativeCap(deviceAdminLinkCapability, { getAdminLink: async ({ deviceId }) => {
218930
+ if (deviceId !== this.id) throw new Error(`ReolinkGatewayDevice: deviceId mismatch, expected ${this.id}, got ${deviceId}`);
218931
+ const host = reolinkHubSessions.get(this.integrationId)?.relayEndpoint().host;
218932
+ if (host === void 0) {
218933
+ this.ctx.logger.debug("reolink admin link unavailable: integration not running", { tags: { deviceId: this.id } });
218934
+ return null;
218935
+ }
218936
+ const built = buildDeviceAdminUrl({
218937
+ host,
218938
+ port: null,
218939
+ scheme: "https",
218940
+ path: "/"
218941
+ });
218942
+ if (!built.ok) {
218943
+ this.ctx.logger.debug("reolink admin link unavailable", {
218944
+ tags: { deviceId: this.id },
218945
+ meta: { reason: built.reason }
218946
+ });
218947
+ return null;
218948
+ }
218949
+ return {
218950
+ url: built.url,
218951
+ label: "Reolink web interface",
218952
+ target: "integration",
218953
+ host
218954
+ };
218955
+ } });
218956
+ }
218957
+ };
218958
+ //#endregion
218027
218959
  //#region src/addon.ts
218028
218960
  /**
218029
218961
  * Adapt a camstack scoped logger to the lib's `Logger` shape (just
@@ -218187,7 +219119,10 @@ var ReolinkProviderAddon = class extends BaseDeviceProvider {
218187
219119
  secretStatePaths = ["emailPushAuthPassword"];
218188
219120
  addonId = REOLINK_ADDON_ID;
218189
219121
  providerName = "Reolink";
218190
- deviceClasses = { [DeviceType.Camera]: ReolinkCamera };
219122
+ deviceClasses = {
219123
+ [DeviceType.Camera]: ReolinkCamera,
219124
+ [DeviceType.Gateway]: ReolinkGatewayDevice
219125
+ };
218191
219126
  /**
218192
219127
  * Short-lived cache that bridges `testCreationField` and
218193
219128
  * `onCreateDevice` — Slice 15 socket reuse. Both code paths key off
@@ -218402,6 +219337,15 @@ var ReolinkProviderAddon = class extends BaseDeviceProvider {
218402
219337
  this.ctx.eventBus.subscribe({ category: EventCategory.IntegrationDisabled }, handler);
218403
219338
  this.ctx.eventBus.subscribe({ category: EventCategory.IntegrationDeleted }, handler);
218404
219339
  }
219340
+ /** The hub box follows its integration's socket. */
219341
+ markGatewaysOnline(integrationId, online) {
219342
+ const reg = this.ctx.kernel.deviceRegistry;
219343
+ if (!reg) return;
219344
+ for (const d of reg.getAllForAddon(this.addonId)) {
219345
+ if (!(d instanceof ReolinkGatewayDevice) || d.integrationId !== integrationId) continue;
219346
+ if (d.online !== online) d.markOnline(online);
219347
+ }
219348
+ }
218405
219349
  buildAdoptionProvider() {
218406
219350
  return buildReolinkAdoptionProvider({
218407
219351
  getSession: (integrationId) => this.hubManager?.get(integrationId) ?? null,
@@ -218447,6 +219391,29 @@ var ReolinkProviderAddon = class extends BaseDeviceProvider {
218447
219391
  accessoryDeviceIds: children.map((c) => c.id)
218448
219392
  };
218449
219393
  },
219394
+ listAdoptedGateways: async () => {
219395
+ const reg = this.ctx.kernel.deviceRegistry;
219396
+ if (!reg) return [];
219397
+ const out = [];
219398
+ for (const d of reg.getAllForAddon(this.addonId)) if (d instanceof ReolinkGatewayDevice) out.push({
219399
+ id: d.id,
219400
+ integrationId: d.integrationId
219401
+ });
219402
+ return out;
219403
+ },
219404
+ adoptGateway: async ({ integrationId, name }) => {
219405
+ const devices = this.ctx.kernel.devices;
219406
+ if (!devices) throw new Error("reolink adopt: kernel.devices unavailable");
219407
+ const nativeId = hubGatewayNativeId(integrationId);
219408
+ return { deviceId: (await devices.create(`${integrationId}-${nativeId}`, ReolinkGatewayDevice, {
219409
+ integrationId,
219410
+ nativeId
219411
+ }, null, {
219412
+ type: DeviceType.Gateway,
219413
+ name,
219414
+ integrationId
219415
+ })).id };
219416
+ },
218450
219417
  removeDevice: async (id) => {
218451
219418
  await this.ctx.kernel.devices?.remove(id);
218452
219419
  },
@@ -218652,7 +219619,10 @@ var ReolinkProviderAddon = class extends BaseDeviceProvider {
218652
219619
  this.listReolinkCameras();
218653
219620
  this.hubManager = createReolinkHubManager({
218654
219621
  logger: this.ctx.logger,
218655
- onConnectionChange: (integrationId, online) => this.ctx.logger.info(`Reolink NVR socket ${online ? "up" : "down"}`, { meta: { integrationId } })
219622
+ onConnectionChange: (integrationId, online) => {
219623
+ this.ctx.logger.info(`Reolink NVR socket ${online ? "up" : "down"}`, { meta: { integrationId } });
219624
+ this.markGatewaysOnline(integrationId, online);
219625
+ }
218656
219626
  }, reolinkHubSessions);
218657
219627
  await this.reconcileIntegrations();
218658
219628
  this.subscribeIntegrationLifecycle();