@camstack/addon-notifiers 1.2.625 → 1.2.626

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 +6 -0
  2. package/dist/addon.js +1913 -1104
  3. package/dist/addon.mjs +1913 -1104
  4. package/package.json +5 -2
package/dist/addon.js CHANGED
@@ -29,7 +29,7 @@ let node_fs = require("node:fs");
29
29
  let node_path = require("node:path");
30
30
  node_path = __toESM(node_path);
31
31
  let node_url = require("node:url");
32
- //#region ../types/dist/event-category-DmZDPDf_.mjs
32
+ //#region ../types/dist/event-category-CaURIwQA.mjs
33
33
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
34
34
  EventCategory["SystemBoot"] = "system.boot";
35
35
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -745,6 +745,10 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
745
745
  EventCategory["NotificationRuleBroken"] = "notification.rule-broken";
746
746
  /** The clear of {@link NotificationRuleBroken}: the rule loads and names nothing deleted. */
747
747
  EventCategory["NotificationRuleRepaired"] = "notification.rule-repaired";
748
+ /** A migration failed or was blocked; the Alert Center re-reads the ledger. */
749
+ EventCategory["MigrationFailed"] = "migration.failed";
750
+ /** A migration applied or a failure cleared; the Alert Center re-reads the ledger. */
751
+ EventCategory["MigrationApplied"] = "migration.applied";
748
752
  EventCategory["DeviceUpdated"] = "device.updated";
749
753
  /**
750
754
  * Transport-level connectivity. Emitted by the device driver when
@@ -807,6 +811,378 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
807
811
  return EventCategory;
808
812
  }({});
809
813
  //#endregion
814
+ //#region ../types/dist/device-type-CEqjmUnR.mjs
815
+ /**
816
+ import { errMsg } from '@camstack/types'
817
+ * Extract a human-readable message from an unknown error value.
818
+ * Replaces the ubiquitous `errMsg(err)` pattern.
819
+ */
820
+ function errMsg$1(err) {
821
+ if (err instanceof Error) return err.message;
822
+ if (typeof err === "string") return err;
823
+ return String(err);
824
+ }
825
+ var DeviceType = /* @__PURE__ */ function(DeviceType) {
826
+ DeviceType["Camera"] = "camera";
827
+ DeviceType["Hub"] = "hub";
828
+ DeviceType["Light"] = "light";
829
+ DeviceType["Siren"] = "siren";
830
+ DeviceType["Switch"] = "switch";
831
+ DeviceType["Sensor"] = "sensor";
832
+ DeviceType["Thermostat"] = "thermostat";
833
+ /** Air-conditioner / heat-pump climate device (HVAC) — shares the
834
+ * `climate-control` cap surface with `Thermostat` but renders a
835
+ * dedicated AC-appropriate control UI (mode chips, fan speed,
836
+ * independent vertical/horizontal swing). Sources: native Gree, and
837
+ * reusable by other AC integrations. */
838
+ DeviceType["Climate"] = "climate";
839
+ DeviceType["Button"] = "button";
840
+ /** Generic stateless event emitter — carries a device's EXACT declared
841
+ * event vocabulary verbatim (no normalization). Installed with the
842
+ * `event-emitter` cap. Sources: HA `event.*` entities (structured) and
843
+ * HA bus events (e.g. `zha_event`, generic). */
844
+ DeviceType["EventEmitter"] = "event-emitter";
845
+ /** Firmware/software update entity — current vs available version,
846
+ * updatable flag, update state, and an install action. Installed with
847
+ * the `update` cap. Sources: Homematic firmware-update channels (and
848
+ * reusable by other providers, e.g. HA `update.*` entities). */
849
+ DeviceType["Update"] = "update";
850
+ DeviceType["Generic"] = "generic";
851
+ /** Generic notification delivery target (HA `notify.<service>`, future
852
+ * Telegram / Discord / ntfy / SMTP, …). One device per delivery
853
+ * endpoint; the `notifier` cap defines the send surface. */
854
+ DeviceType["Notifier"] = "notifier";
855
+ /** Pre-recorded action sequence with optional parameters
856
+ * (HA `script.*`). Runnable via `script-runner` cap. */
857
+ DeviceType["Script"] = "script";
858
+ /** Automation rule (HA `automation.*`) — enable/disable + manual
859
+ * trigger surface exposed via `automation-control` cap. */
860
+ DeviceType["Automation"] = "automation";
861
+ /** Door / smart lock device (HA `lock.*`). `lock-control` cap. */
862
+ DeviceType["Lock"] = "lock";
863
+ /** Window covering, blinds, garage door, valve, etc. (HA `cover.*`,
864
+ * `valve.*`). `cover` cap with sub-roles for variant. */
865
+ DeviceType["Cover"] = "cover";
866
+ /** Pipe / water / gas valve with open/close/stop and optional
867
+ * position (HA `valve.*`). `valve` cap — a cover-sibling actuator
868
+ * modelled on the same open/closed lifecycle. */
869
+ DeviceType["Valve"] = "valve";
870
+ /** Humidifier / dehumidifier with on/off + target humidity + mode
871
+ * (HA `humidifier.*`). `humidifier` cap — a climate-family actuator
872
+ * modelled on the same target / mode lifecycle. */
873
+ DeviceType["Humidifier"] = "humidifier";
874
+ /** Water heater / boiler with target temperature + operation mode +
875
+ * away mode (HA `water_heater.*`). `water-heater` cap — a
876
+ * climate-family actuator. */
877
+ DeviceType["WaterHeater"] = "water-heater";
878
+ /** Ceiling / standing / exhaust fan (HA `fan.*`). `fan-control` cap. */
879
+ DeviceType["Fan"] = "fan";
880
+ /** Audio / video playback endpoint (HA `media_player.*`). Disjoint from
881
+ * the camera surface — those use `Camera`. `media-player` cap. */
882
+ DeviceType["MediaPlayer"] = "media-player";
883
+ /** Security panel / alarm system (HA `alarm_control_panel.*`).
884
+ * `alarm-panel` cap. */
885
+ DeviceType["AlarmPanel"] = "alarm-panel";
886
+ /** Generic user-settable input (HA `number` / `input_number` / `select`
887
+ * / `input_select` / `text` / `input_text` / `input_datetime`).
888
+ * Sub-type via `DeviceRole`: NumericControl / SelectControl /
889
+ * TextControl / DateTimeControl. */
890
+ DeviceType["Control"] = "control";
891
+ /** Person / device-tracker presence (HA `person.*`, `device_tracker.*`).
892
+ * `presence` cap. */
893
+ DeviceType["Presence"] = "presence";
894
+ /** Weather provider (HA `weather.*`). Tier-3, low MVP priority.
895
+ * `weather` cap. */
896
+ DeviceType["Weather"] = "weather";
897
+ /** Robot vacuum (HA `vacuum.*`). Tier-3. `vacuum-control` cap. */
898
+ DeviceType["Vacuum"] = "vacuum";
899
+ /** Robotic lawn mower (HA `lawn_mower.*`). Tier-3.
900
+ * `lawn-mower-control` cap. */
901
+ DeviceType["LawnMower"] = "lawn-mower";
902
+ /** Physical HA device group — parent container for entity-children
903
+ * adopted from a single HA device entry. Not renderable as a
904
+ * standalone device; exists only to anchor child entities. */
905
+ DeviceType["Container"] = "container";
906
+ /** Single still-image entity (HA `image.*`). Read-only display of an
907
+ * `entity_picture` signed URL the browser loads directly. `image` cap. */
908
+ DeviceType["Image"] = "image";
909
+ /** Smart pet feeder — cloud-connected food dispenser with a bowl food
910
+ * level, battery, desiccant life, feeding state and manual-feed /
911
+ * call-pet / maintenance actions. Installed with the `pet-feeder` cap;
912
+ * dual-hopper models (D4S/D4SH) expose per-hopper portions. Sources:
913
+ * native PetKit (`nodepetkit` `FeederDevice`), reusable by other feeder
914
+ * integrations sharing the same food/desiccant/hopper surface. */
915
+ DeviceType["PetFeeder"] = "pet-feeder";
916
+ /** Pet water fountain — tank level, water drunk, pump and its duty cycle.
917
+ * Installed with the `pet-fountain` cap. Reaches CamStack as loose entities
918
+ * of a container and is BUILT as a composed device (D967). */
919
+ DeviceType["PetFountain"] = "pet-fountain";
920
+ /** Built-in dishwasher — program lifecycle, door / remote-start gates,
921
+ * salt and rinse-aid levels. Installed with the `dishwasher` cap.
922
+ * Sources: native Bosch Home Connect, reusable by similar integrations.
923
+ * A plug-built dishwasher carries running-state and power instead. */
924
+ DeviceType["Dishwasher"] = "dishwasher";
925
+ /** Washing machine. A plug-built one is running while its power stays
926
+ * above a threshold. No program cap — that surface is the dishwasher's. */
927
+ DeviceType["Washer"] = "washer";
928
+ /** Tumble dryer. Same plug-built running surface as {@link Washer}. */
929
+ DeviceType["Dryer"] = "dryer";
930
+ /** The physical bridge box of a vendor ecosystem (Velux KLF-200, and later
931
+ * other radio-to-IP bridges). It carries the box's own facts (firmware,
932
+ * address, connection state) and the actions the box really supports
933
+ * (`reboot`, `device-admin-link`). Products are NOT its children — they are
934
+ * adopted beside it — and its connection settings stay on the integration. */
935
+ DeviceType["Gateway"] = "gateway";
936
+ return DeviceType;
937
+ }({});
938
+ var DeviceFeature = /* @__PURE__ */ function(DeviceFeature) {
939
+ DeviceFeature["BatteryOperated"] = "battery-operated";
940
+ DeviceFeature["Rebootable"] = "rebootable";
941
+ /**
942
+ * Device supports an on-demand re-sync of its derived spec with its
943
+ * upstream source — drives the generic Re-sync button. The owning
944
+ * provider implements the action via the `device-adoption.resync` cap.
945
+ */
946
+ DeviceFeature["Resyncable"] = "resyncable";
947
+ DeviceFeature["NativeSnapshot"] = "native-snapshot";
948
+ DeviceFeature["DoorbellButton"] = "doorbell-button";
949
+ DeviceFeature["TwoWayAudio"] = "two-way-audio";
950
+ DeviceFeature["PanTiltZoom"] = "pan-tilt-zoom";
951
+ /**
952
+ * Camera supports the on-firmware autotrack subsystem (subject-
953
+ * following). Distinct from `PanTiltZoom` because not every PTZ
954
+ * camera ships autotrack — the admin UI uses this flag to gate
955
+ * the autotrack toggle / settings card without re-deriving from
956
+ * the cap registry. Mirrors `ptz-autotrack` cap registration:
957
+ * driver sets this feature when probe confirms the firmware
958
+ * surface, and registers the cap in the same code path.
959
+ */
960
+ DeviceFeature["PtzAutotrack"] = "ptz-autotrack";
961
+ /**
962
+ * Accessory exposes a "trigger on motion" toggle — the parent camera's
963
+ * motion detection automatically activates this device. Mirrors
964
+ * `motion-trigger` cap registration: drivers set this feature in the
965
+ * same code path that calls `ctx.registerNativeCap(motionTriggerCapability, ...)`.
966
+ *
967
+ * Used by admin UI (gate the in-hero `MotionTriggerToggle` against a
968
+ * fast scalar without binding fetch), notifier rules, and `listAll`
969
+ * filters that want "all devices with on-motion behaviour".
970
+ */
971
+ DeviceFeature["MotionTrigger"] = "motion-trigger";
972
+ /** Light supports rgb-triplet color via `color` cap. */
973
+ DeviceFeature["LightColorRgb"] = "light-color-rgb";
974
+ /** Light supports HSV color via `color` cap. */
975
+ DeviceFeature["LightColorHsv"] = "light-color-hsv";
976
+ /** Light supports color-temperature (mired) via `color` cap. */
977
+ DeviceFeature["LightColorMired"] = "light-color-mired";
978
+ /** Thermostat supports a `heat_cool` dual setpoint (targetLow +
979
+ * targetHigh). Gates the range slider UI. */
980
+ DeviceFeature["ClimateDualSetpoint"] = "climate-dual-setpoint";
981
+ /** Thermostat exposes target humidity and/or current humidity
982
+ * readings. Gates the humidity controls. */
983
+ DeviceFeature["ClimateHumidity"] = "climate-humidity";
984
+ /** Thermostat exposes a fan-mode selector. */
985
+ DeviceFeature["ClimateFanMode"] = "climate-fan-mode";
986
+ /** Thermostat exposes preset modes (eco / away / sleep / vendor). */
987
+ DeviceFeature["ClimatePreset"] = "climate-preset";
988
+ /** Thermostat exposes a vertical louver swing toggle. Gates the
989
+ * vertical-swing switch in the climate UI. Independent of horizontal. */
990
+ DeviceFeature["ClimateSwingVertical"] = "climate-swing-vertical";
991
+ /** Thermostat exposes a horizontal louver swing toggle. Gates the
992
+ * horizontal-swing switch in the climate UI. Independent of vertical. */
993
+ DeviceFeature["ClimateSwingHorizontal"] = "climate-swing-horizontal";
994
+ /** Cover exposes intermediate position control (0..100). Gates the
995
+ * position slider UI. */
996
+ DeviceFeature["CoverPositionable"] = "cover-positionable";
997
+ /** Cover exposes slat-tilt control. Gates the tilt slider UI. */
998
+ DeviceFeature["CoverTilt"] = "cover-tilt";
999
+ /** Valve exposes intermediate position control (0..100). Gates the
1000
+ * position slider / drag surface UI. */
1001
+ DeviceFeature["ValvePositionable"] = "valve-positionable";
1002
+ /** Fan exposes a speed-percentage setter. Gates the speed slider UI. */
1003
+ DeviceFeature["FanSpeed"] = "fan-speed";
1004
+ /** Fan exposes a preset mode selector. */
1005
+ DeviceFeature["FanPreset"] = "fan-preset";
1006
+ /** Fan exposes blade direction (forward/reverse) — typical of
1007
+ * ceiling fans. */
1008
+ DeviceFeature["FanDirection"] = "fan-direction";
1009
+ /** Fan exposes an oscillation toggle. */
1010
+ DeviceFeature["FanOscillating"] = "fan-oscillating";
1011
+ /** Lock requires a PIN code on lock/unlock. Gates the code-entry
1012
+ * field on the UI lock-controls panel. */
1013
+ DeviceFeature["LockPinRequired"] = "lock-pin-required";
1014
+ /** Lock supports a latch-release ("open door") action distinct from
1015
+ * unlock. Mirrors HA `LockEntityFeature.OPEN` (bit 1) in
1016
+ * `supported_features`. Gates the Open Door button in the UI. */
1017
+ DeviceFeature["LockOpen"] = "lock-open";
1018
+ /** Media player exposes a seek-to-position surface. */
1019
+ DeviceFeature["MediaPlayerSeek"] = "media-player-seek";
1020
+ /** Media player exposes a volume-level setter. */
1021
+ DeviceFeature["MediaPlayerVolume"] = "media-player-volume";
1022
+ /** Media player exposes a mute toggle distinct from volume=0. */
1023
+ DeviceFeature["MediaPlayerMute"] = "media-player-mute";
1024
+ /** Media player exposes a shuffle toggle. */
1025
+ DeviceFeature["MediaPlayerShuffle"] = "media-player-shuffle";
1026
+ /** Media player exposes a repeat mode (off / all / one). */
1027
+ DeviceFeature["MediaPlayerRepeat"] = "media-player-repeat";
1028
+ /** Media player exposes a source / input selector. */
1029
+ DeviceFeature["MediaPlayerSelectSource"] = "media-player-select-source";
1030
+ /** Media player exposes a play-arbitrary-media surface (URL / id). */
1031
+ DeviceFeature["MediaPlayerPlayMedia"] = "media-player-play-media";
1032
+ /** Media player exposes next-track. */
1033
+ DeviceFeature["MediaPlayerNext"] = "media-player-next";
1034
+ /** Media player exposes previous-track. */
1035
+ DeviceFeature["MediaPlayerPrevious"] = "media-player-previous";
1036
+ /** Media player exposes stop distinct from pause. */
1037
+ DeviceFeature["MediaPlayerStop"] = "media-player-stop";
1038
+ /** Alarm panel requires a PIN code on arm/disarm. */
1039
+ DeviceFeature["AlarmPinRequired"] = "alarm-pin-required";
1040
+ /** Presence device carries GPS coordinates (lat/lng/accuracy) in
1041
+ * addition to a textual location. */
1042
+ DeviceFeature["PresenceGps"] = "presence-gps";
1043
+ /** Notifier accepts an inline / URL image attachment. */
1044
+ DeviceFeature["NotifierImage"] = "notifier-image";
1045
+ /** Notifier accepts a priority hint (high/normal/low). */
1046
+ DeviceFeature["NotifierPriority"] = "notifier-priority";
1047
+ /** Notifier accepts a free-form `data` payload for platform-specific
1048
+ * fields. */
1049
+ DeviceFeature["NotifierData"] = "notifier-data";
1050
+ /** Notifier supports interactive action buttons / callbacks. */
1051
+ DeviceFeature["NotifierActions"] = "notifier-actions";
1052
+ /** Notifier supports per-call recipient targeting (multi-user). */
1053
+ DeviceFeature["NotifierRecipients"] = "notifier-recipients";
1054
+ /** Script runner accepts a variables map on each run invocation. */
1055
+ DeviceFeature["ScriptVariables"] = "script-variables";
1056
+ /** Automation `trigger` accepts a skipCondition flag — fires the
1057
+ * automation's actions while bypassing its condition block. */
1058
+ DeviceFeature["AutomationSkipCondition"] = "automation-skip-condition";
1059
+ /** Robot vacuum exposes a live cleaning map (image child). Gates the
1060
+ * map tile in the vacuum UI. */
1061
+ DeviceFeature["VacuumHasMap"] = "vacuum-has-map";
1062
+ /** Robot vacuum exposes AI obstacle-detection toggles. Gates the AI
1063
+ * switches group. */
1064
+ DeviceFeature["VacuumHasAi"] = "vacuum-has-ai";
1065
+ /** Robot mower exposes a live mowing map (SVG image child). Gates the
1066
+ * mower map tile. */
1067
+ DeviceFeature["MowerHasMap"] = "mower-has-map";
1068
+ /** Robot mower exposes targeted mowing (all-area / zones / edges /
1069
+ * spots) — gates the Mow action + map selectors. */
1070
+ DeviceFeature["MowerHasTargetedMowing"] = "mower-has-targeted-mowing";
1071
+ return DeviceFeature;
1072
+ }({});
1073
+ /**
1074
+ * Semantic role a device plays within its parent. Populated by driver
1075
+ * addons when creating accessory devices (Reolink siren/floodlight/
1076
+ * PIR/chime/autotrack/doorbell, ONVIF relay outputs, …). Used by the
1077
+ * admin UI to pick icons, labels, and widgets — a `Switch` with
1078
+ * `role: Floodlight` renders as a bulb with a brightness slider,
1079
+ * whereas a `Switch` with `role: Siren` renders as a klaxon.
1080
+ *
1081
+ * Undefined for top-level devices (cameras, NVRs, hubs). Persisted in
1082
+ * sqlite as a nullable TEXT column — old rows keep working unchanged.
1083
+ */
1084
+ var DeviceRole = /* @__PURE__ */ function(DeviceRole) {
1085
+ DeviceRole["Siren"] = "siren";
1086
+ DeviceRole["Floodlight"] = "floodlight";
1087
+ DeviceRole["Spotlight"] = "spotlight";
1088
+ DeviceRole["PirSensor"] = "pir-sensor";
1089
+ DeviceRole["Chime"] = "chime";
1090
+ DeviceRole["Autotrack"] = "autotrack";
1091
+ DeviceRole["Nightvision"] = "nightvision";
1092
+ DeviceRole["PrivacyMask"] = "privacy-mask";
1093
+ DeviceRole["Doorbell"] = "doorbell";
1094
+ /** Interior blind / shade / roller blind. HA `device_class: blind|shade`. */
1095
+ DeviceRole["CoverBlind"] = "cover-blind";
1096
+ /** Exterior roller shutter. HA `device_class: shutter`; Homematic BROLL. */
1097
+ DeviceRole["CoverShutter"] = "cover-shutter";
1098
+ /** Exterior awning. HA `device_class: awning`. */
1099
+ DeviceRole["CoverAwning"] = "cover-awning";
1100
+ /** Curtain / drape. HA `device_class: curtain`. */
1101
+ DeviceRole["CoverCurtain"] = "cover-curtain";
1102
+ /** Garage door. HA `device_class: garage`. A security actuator: exports gate it. */
1103
+ DeviceRole["CoverGarage"] = "cover-garage";
1104
+ /** Gate. HA `device_class: gate`. A security actuator: exports gate it. */
1105
+ DeviceRole["CoverGate"] = "cover-gate";
1106
+ /** Door. HA `device_class: door`. A security actuator: exports gate it. */
1107
+ DeviceRole["CoverDoor"] = "cover-door";
1108
+ /** Window. HA `device_class: window`. */
1109
+ DeviceRole["CoverWindow"] = "cover-window";
1110
+ /** Virtual HA toggle (input_boolean.*) — distinguishable from a
1111
+ * real Switch device for UI rendering / export adapters. */
1112
+ DeviceRole["BinaryHelper"] = "binary-helper";
1113
+ /** A smart plug: a `switch` whose source says it is an outlet (HA
1114
+ * `device_class: outlet`). The Alexa export shows it as a SMARTPLUG. */
1115
+ DeviceRole["Outlet"] = "outlet";
1116
+ /** Generic motion / occupancy / moving event source. Distinct from
1117
+ * the camera accessory PirSensor role: that one is a camera child;
1118
+ * this is a standalone HA / 3rd-party motion sensor. */
1119
+ DeviceRole["MotionSensor"] = "motion-sensor";
1120
+ DeviceRole["ContactSensor"] = "contact-sensor";
1121
+ DeviceRole["LeakSensor"] = "leak-sensor";
1122
+ DeviceRole["SmokeSensor"] = "smoke-sensor";
1123
+ DeviceRole["COSensor"] = "co-sensor";
1124
+ DeviceRole["GasSensor"] = "gas-sensor";
1125
+ DeviceRole["TamperSensor"] = "tamper-sensor";
1126
+ DeviceRole["VibrationSensor"] = "vibration-sensor";
1127
+ DeviceRole["ConnectivitySensor"] = "connectivity-sensor";
1128
+ DeviceRole["SoundSensor"] = "sound-sensor";
1129
+ /** Fallback for `binary_sensor` without a known `device_class`. */
1130
+ DeviceRole["BinarySensor"] = "binary-sensor";
1131
+ DeviceRole["TemperatureSensor"] = "temperature-sensor";
1132
+ DeviceRole["HumiditySensor"] = "humidity-sensor";
1133
+ DeviceRole["AmbientLightSensor"] = "ambient-light-sensor";
1134
+ DeviceRole["PressureSensor"] = "pressure-sensor";
1135
+ /** Wind speed or direction (weather-station `wind-sensor` cap). */
1136
+ DeviceRole["WindSensor"] = "wind-sensor";
1137
+ /** Rain accumulation or rate (weather-station `rain-sensor` cap). */
1138
+ DeviceRole["RainSensor"] = "rain-sensor";
1139
+ /** UV index (weather-station `uv-sensor` cap). */
1140
+ DeviceRole["UvSensor"] = "uv-sensor";
1141
+ /** Solar irradiance W/m² (weather-station `solar-radiation-sensor` cap).
1142
+ * Distinct from AmbientLightSensor (lux). */
1143
+ DeviceRole["SolarRadiationSensor"] = "solar-radiation-sensor";
1144
+ /** Soil moisture % (garden/weather `soil-moisture-sensor` cap). */
1145
+ DeviceRole["SoilMoistureSensor"] = "soil-moisture-sensor";
1146
+ DeviceRole["PowerSensor"] = "power-sensor";
1147
+ DeviceRole["EnergySensor"] = "energy-sensor";
1148
+ DeviceRole["VoltageSensor"] = "voltage-sensor";
1149
+ DeviceRole["CurrentSensor"] = "current-sensor";
1150
+ DeviceRole["AirQualitySensor"] = "air-quality-sensor";
1151
+ /** Battery level (numeric % via `sensor` OR low-bool via
1152
+ * `binary_sensor` — the cap distinguishes via the value type). */
1153
+ DeviceRole["BatterySensor"] = "battery-sensor";
1154
+ /** Fallback for `sensor` numeric without a known `device_class`. */
1155
+ DeviceRole["NumericSensor"] = "numeric-sensor";
1156
+ /** String / enum state (HA `sensor` with `state_class: enum` or
1157
+ * `attributes.options`). */
1158
+ DeviceRole["EnumSensor"] = "enum-sensor";
1159
+ /** Date / timestamp state (HA `sensor` with `device_class: timestamp`
1160
+ * or `date`). The slice carries the raw ISO string verbatim (hosted on
1161
+ * the `enum-sensor` cap); the UI renders it locale-formatted. */
1162
+ DeviceRole["DateTimeSensor"] = "datetime-sensor";
1163
+ /** Last-resort fallback when nothing else matches. */
1164
+ DeviceRole["GenericSensor"] = "generic-sensor";
1165
+ DeviceRole["NumericControl"] = "numeric-control";
1166
+ DeviceRole["SelectControl"] = "select-control";
1167
+ DeviceRole["TextControl"] = "text-control";
1168
+ DeviceRole["DateTimeControl"] = "datetime-control";
1169
+ /** Mobile push notifier (HA `notify.mobile_app_*`) — supports
1170
+ * rich features (image, priority, channel routing). */
1171
+ DeviceRole["MobilePushNotifier"] = "mobile-push-notifier";
1172
+ /** Chat / messaging service (HA `notify.telegram_*`,
1173
+ * `notify.discord_*`, etc.). */
1174
+ DeviceRole["MessagingNotifier"] = "messaging-notifier";
1175
+ /** Email-based delivery (HA `notify.smtp`, etc.). */
1176
+ DeviceRole["EmailNotifier"] = "email-notifier";
1177
+ /** Fallback when the notifier service name doesn't match a known
1178
+ * pattern. */
1179
+ DeviceRole["GenericNotifier"] = "generic-notifier";
1180
+ /** Emits a pulse train (infrared or RF) on request — the `ir-transmitter`
1181
+ * cap. A `DeviceType.Generic` device. */
1182
+ DeviceRole["IrTransmitter"] = "ir-transmitter";
1183
+ return DeviceRole;
1184
+ }({});
1185
+ //#endregion
810
1186
  //#region ../../node_modules/zod/v4/core/core.js
811
1187
  var _a$1;
812
1188
  function $constructor(name, initializer, params) {
@@ -5618,7 +5994,7 @@ var ZodIssueCode = {
5618
5994
  var ZodFirstPartyTypeKind;
5619
5995
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5620
5996
  //#endregion
5621
- //#region ../types/dist/sleep-0RR2wzNB.mjs
5997
+ //#region ../types/dist/sleep-CSYuxssh.mjs
5622
5998
  /**
5623
5999
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5624
6000
  * window to float samples (D455).
@@ -6134,6 +6510,102 @@ function emitReadiness(bus, params) {
6134
6510
  ts
6135
6511
  }));
6136
6512
  }
6513
+ var NOT_RUN = {
6514
+ declared: false,
6515
+ snapshot: null,
6516
+ applied: [],
6517
+ failed: [],
6518
+ blocked: [],
6519
+ skipped: [],
6520
+ blockOwner: []
6521
+ };
6522
+ /**
6523
+ * Offer `migrations` to the engine and run what it says to run, in its order. Never
6524
+ * throws: an unreachable or refusing engine is logged and reported `declared: false`,
6525
+ * because the hub (and this addon's boot) must not hang on a migration.
6526
+ */
6527
+ async function runAddonMigrations(host, migrations, logger) {
6528
+ const first = migrations[0];
6529
+ if (first === void 0) return NOT_RUN;
6530
+ const owner = first.meta.owner;
6531
+ const log = logger.withTags({ migrationOwner: owner });
6532
+ const byId = new Map(migrations.map((m) => [m.meta.id, m]));
6533
+ let declaration;
6534
+ try {
6535
+ declaration = await host.declareOwner({
6536
+ owner,
6537
+ metas: migrations.map((m) => m.meta)
6538
+ });
6539
+ } catch (err) {
6540
+ log.error("the migration engine refused or could not take this addon catalogue — none ran", { meta: { error: errMsg$1(err) } });
6541
+ return {
6542
+ ...NOT_RUN,
6543
+ blockOwner: migrations.filter((m) => m.meta.onFailure === "block-owner").map((m) => m.meta.id)
6544
+ };
6545
+ }
6546
+ const applied = [];
6547
+ const failed = [];
6548
+ const blocked = [];
6549
+ const skipped = [];
6550
+ const settle = (id, status) => {
6551
+ if (status === "applied") applied.push(id);
6552
+ else if (status === "failed") failed.push(id);
6553
+ else if (status === "blocked") blocked.push(id);
6554
+ else skipped.push(id);
6555
+ };
6556
+ for (const step of declaration.steps) {
6557
+ const bound = byId.get(step.id);
6558
+ if (bound === void 0 || step.action === "skip") {
6559
+ skipped.push(step.id);
6560
+ continue;
6561
+ }
6562
+ const tags = { migrationId: step.id };
6563
+ try {
6564
+ const gate = await host.startMigration({ id: step.id });
6565
+ if (!gate.proceed) {
6566
+ log.warn("migration not run", {
6567
+ tags,
6568
+ meta: {
6569
+ state: gate.state,
6570
+ reason: gate.reason
6571
+ }
6572
+ });
6573
+ settle(step.id, gate.state === "pending" ? "blocked" : gate.state);
6574
+ continue;
6575
+ }
6576
+ const result = await bound.execute();
6577
+ const outcome = await host.finishMigration({
6578
+ id: step.id,
6579
+ result
6580
+ });
6581
+ if (outcome.status === "applied") log.info("migration applied", {
6582
+ tags,
6583
+ meta: { rowsChanged: outcome.rowsChanged }
6584
+ });
6585
+ else log.error("migration failed", {
6586
+ tags,
6587
+ meta: { error: outcome.error }
6588
+ });
6589
+ settle(step.id, outcome.status);
6590
+ } catch (err) {
6591
+ log.error("migration could not be completed with the engine", {
6592
+ tags,
6593
+ meta: { error: errMsg$1(err) }
6594
+ });
6595
+ failed.push(step.id);
6596
+ }
6597
+ }
6598
+ const blockOwner = [...failed, ...blocked].filter((id) => byId.get(id)?.meta.onFailure === "block-owner");
6599
+ return {
6600
+ declared: true,
6601
+ snapshot: declaration.snapshot,
6602
+ applied,
6603
+ failed,
6604
+ blocked,
6605
+ skipped,
6606
+ blockOwner
6607
+ };
6608
+ }
6137
6609
  /**
6138
6610
  * Build a {@link DurableState} over a single store key. Transport-agnostic:
6139
6611
  * `read`/`write` are the addon-store or device-store accessors. The whole
@@ -7189,6 +7661,28 @@ var BaseAddon = class {
7189
7661
  return normalized;
7190
7662
  }
7191
7663
  /**
7664
+ * Run this addon's migration catalogue (D987 M3) through the hub's `migrations`
7665
+ * engine. Call it at the point of the boot where the data must already have the new
7666
+ * shape — `onInitialize`, or `onRestoreDevices` for a migration that needs the
7667
+ * restored devices — with the catalogue bound to the addon's ports
7668
+ * (`defineAddonMigrations(ADDON_MIGRATIONS, ports)`).
7669
+ *
7670
+ * An engine that cannot be reached or refuses the catalogue is logged and runs
7671
+ * nothing; the boot continues. A migration that FAILED (or was blocked by one)
7672
+ * and declares `onFailure: 'block-owner'` makes this throw, so the addon does not
7673
+ * serve data whose shape it cannot trust; `tolerate` migrations never do.
7674
+ */
7675
+ async runMigrations(migrations) {
7676
+ const api = this.ctx.api.migrations;
7677
+ const report = await runAddonMigrations({
7678
+ declareOwner: (input) => api.declareOwner.mutate(input),
7679
+ startMigration: (input) => api.startMigration.mutate(input),
7680
+ finishMigration: (input) => api.finishMigration.mutate(input)
7681
+ }, migrations, this.ctx.logger);
7682
+ 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`);
7683
+ return report;
7684
+ }
7685
+ /**
7192
7686
  * Called by the isolated-process runner AFTER `broker.start()`.
7193
7687
  * In-process addons never need this because the hub broker is already
7194
7688
  * running when `initialize()` fires. For forked children the broker
@@ -7321,6 +7815,26 @@ var BaseAddon = class {
7321
7815
  * alternative — skip it — turns a rename into a surface that quietly
7322
7816
  * empties, which looks exactly like an addon with nothing to configure.
7323
7817
  */
7818
+ /**
7819
+ * The form schema of ONE integration instance, or `null` to use {@link globalSettingsSchema}.
7820
+ *
7821
+ * Override it only when an `instanceMode: multiple` addon's instances differ: an NVR integration
7822
+ * needs its connection fields, the default integration the addon's own sections. It is the
7823
+ * SHAPE of the form only — the values live in `integrations.getSettings`, and the write path is
7824
+ * unchanged (`integrations.setSettings` for an account integration).
7825
+ */
7826
+ async integrationSettingsSchema(_instance) {
7827
+ return null;
7828
+ }
7829
+ /**
7830
+ * What the create wizard and the integration edit page render for one instance: the addon's
7831
+ * per-instance schema when it declares one, else its global schema (what they always showed).
7832
+ */
7833
+ async getIntegrationFormSchema(instance, nodeId) {
7834
+ const scoped = await this.integrationSettingsSchema(instance);
7835
+ if (scoped === null) return this.getGlobalSettings(void 0, void 0, nodeId);
7836
+ return hydrateSchema(scoped, {}, { inlineSecretsAsViews: true });
7837
+ }
7324
7838
  async getIntegrationSettings(nodeId) {
7325
7839
  const declared = this.integrationSettingSections();
7326
7840
  if (declared.length === 0) return null;
@@ -8424,407 +8938,6 @@ function defineRecordKinds(specs) {
8424
8938
  }
8425
8939
  return out;
8426
8940
  }
8427
- var READING_QUANTITIES = [
8428
- "temperature",
8429
- "pressure",
8430
- "speed",
8431
- "precipitation",
8432
- "precipitation-rate",
8433
- "length",
8434
- "area",
8435
- "illuminance",
8436
- "irradiance",
8437
- "power",
8438
- "energy",
8439
- "voltage",
8440
- "current",
8441
- "apparent-power",
8442
- "humidity",
8443
- "percentage",
8444
- "concentration-volume",
8445
- "concentration-mass",
8446
- "concentration-ppb",
8447
- "volume",
8448
- "duration",
8449
- "duration-ms",
8450
- "information",
8451
- "sound-level",
8452
- "signal-level",
8453
- "colour-temperature",
8454
- "angle",
8455
- "bitrate",
8456
- "rate",
8457
- "mass",
8458
- "index",
8459
- "ratio",
8460
- "count",
8461
- "none",
8462
- "device"
8463
- ];
8464
- var ReadingQuantitySchema = _enum(READING_QUANTITIES);
8465
- new Set(READING_QUANTITIES);
8466
- /**
8467
- * Stored unit symbol for a quantity. `none` means the number is dimensionless
8468
- * (or an index / ratio / count). `device` means the cap has no single unit.
8469
- */
8470
- var QUANTITY_BASE = {
8471
- temperature: "°C",
8472
- pressure: "hPa",
8473
- speed: "m/s",
8474
- precipitation: "mm",
8475
- "precipitation-rate": "mm/h",
8476
- length: "m",
8477
- area: "m²",
8478
- illuminance: "lx",
8479
- irradiance: "W/m²",
8480
- power: "W",
8481
- energy: "kWh",
8482
- voltage: "V",
8483
- current: "A",
8484
- "apparent-power": "VA",
8485
- humidity: "%",
8486
- percentage: "%",
8487
- "concentration-volume": "ppm",
8488
- "concentration-mass": "µg/m³",
8489
- "concentration-ppb": "ppb",
8490
- volume: "L",
8491
- duration: "s",
8492
- "duration-ms": "ms",
8493
- information: "B",
8494
- "sound-level": "dBFS",
8495
- "signal-level": "dBm",
8496
- "colour-temperature": "mired",
8497
- angle: "°",
8498
- bitrate: "kbps",
8499
- rate: "fps",
8500
- mass: "kg",
8501
- index: "none",
8502
- ratio: "none",
8503
- count: "none",
8504
- none: "none",
8505
- device: "device"
8506
- };
8507
- /** The stored unit, or `undefined` when the quantity has no unit symbol. */
8508
- function baseUnitOf(quantity) {
8509
- const symbol = QUANTITY_BASE[quantity];
8510
- if (symbol === "none" || symbol === "device") return void 0;
8511
- return symbol;
8512
- }
8513
- function readingMeta(quantity, extras) {
8514
- return {
8515
- quantity,
8516
- ...extras?.precision !== void 0 ? { precision: extras.precision } : {},
8517
- ...extras?.label !== void 0 ? { label: extras.label } : {}
8518
- };
8519
- }
8520
- /** Shared Zod schemas used across streaming capabilities. */
8521
- var CamProfileSchema = _enum([
8522
- "high",
8523
- "mid",
8524
- "low"
8525
- ]);
8526
- /**
8527
- * Which rung a per-camera consumer listens on: an explicit profile, or `auto`.
8528
- *
8529
- * `auto` is a REAL value, not an absent one — it means "resolve it by the rule
8530
- * that consumer owns". It lives beside `CamProfile` because a rung is the only
8531
- * operator-facing way to name a stream: a cam-stream id is broker-internal and
8532
- * moves when a slot is reassigned, so a stored id silently stops meaning what
8533
- * the operator picked. Today's one user is the audio analyser, whose rule is
8534
- * "the lowest AVAILABLE native rung" (D537).
8535
- */
8536
- var AudioStreamPickSchema = union([CamProfileSchema, literal("auto")]);
8537
- var CamStreamKindSchema = _enum([
8538
- "pull-rtsp",
8539
- "pull-rtmp",
8540
- "pull-http",
8541
- "pull-flv",
8542
- "pull-rfc4571",
8543
- "push-annexb",
8544
- "derived"
8545
- ]);
8546
- var CamStreamResolutionSchema = object({
8547
- width: number().int().positive().meta(readingMeta("none")),
8548
- height: number().int().positive().meta(readingMeta("none"))
8549
- });
8550
- var CameraStreamSchema = object({
8551
- /** Stable, provider-assigned id unique within the (deviceId) scope. */
8552
- camStreamId: string().min(1),
8553
- deviceId: number().int().nonnegative(),
8554
- kind: CamStreamKindSchema,
8555
- /** Required for pull-* kinds. Ignored for push-annexb. */
8556
- url: string().optional(),
8557
- codec: string().optional(),
8558
- resolution: CamStreamResolutionSchema.optional(),
8559
- fps: number().positive().optional(),
8560
- /** Human label surfaced in the Admin UI "Camera Stream" dropdown. */
8561
- label: string().optional(),
8562
- /**
8563
- * Device-level features the publisher advertised (e.g. `battery-operated`).
8564
- * The broker, snapshot orchestrator, and prebuffer manager all consult
8565
- * this list to derive policy — relaxed stall watchdog for battery
8566
- * cams, prebuffer off by default, longer snapshot rate-limit, etc.
8567
- *
8568
- * Single source of truth replacing per-stream flags like the
8569
- * historical `allowStall`: if the publisher knows the camera is
8570
- * battery-powered, every downstream service derives the right policy
8571
- * from this list.
8572
- */
8573
- deviceFeatures: array(string()).optional(),
8574
- /**
8575
- * Whether this stream participates in the broker's automatic profile
8576
- * assignment (`computeInitialAssignment`). Defaults to `true`. Publishers
8577
- * use `false` when they want a stream to be SELECTABLE in the UI but not
8578
- * picked by default — e.g. Reolink publishes its native Baichuan streams
8579
- * as `autoEligible: true` (the recommended path) and its RTSP / RTMP
8580
- * mirrors as `autoEligible: false` (still pickable per slot, just not
8581
- * the auto choice). Manual `assignProfile` calls remain valid for
8582
- * non-eligible streams.
8583
- */
8584
- autoEligible: boolean().optional(),
8585
- /**
8586
- * The profile slot the PUBLISHER says this stream is: `low` for a stream
8587
- * whose only sensible role is the low-bandwidth one, `high` for a main
8588
- * stream. Consulted by the broker's automatic ranking
8589
- * (`rankInitialAssignment`) BEFORE the pixel-count heuristic, and only there:
8590
- * an operator's manual assignment still wins, and an absent hint is the
8591
- * pixel-ranked default every camera had before this field existed.
8592
- *
8593
- * Why it exists: a camera with ONE stream ranked to `mid` — the slot no
8594
- * storage class is written for on its own (`recordings` is high|mid,
8595
- * `recordingsLow` is low) and the one profile scrub does not read. The robot
8596
- * camera's single 720p relay is a `low` in every respect except the slot the
8597
- * heuristic put it in.
8598
- */
8599
- profileHint: CamProfileSchema.optional(),
8600
- /**
8601
- * Transport-specific opaque metadata. The broker passes it through to
8602
- * the source reader without inspecting it. Currently used by
8603
- * `pull-rfc4571` streams to carry the upstream SDP (so the reader can
8604
- * route RTP packets to the right depacketizer without an in-band
8605
- * DESCRIBE phase). Other kinds typically leave it undefined.
8606
- */
8607
- metadata: record(string(), unknown()).optional()
8608
- });
8609
- var ProfileSlotStatusSchema = _enum([
8610
- "unassigned",
8611
- "idle",
8612
- "connecting",
8613
- "streaming",
8614
- "error"
8615
- ]);
8616
- var ProfileSlotSchema = object({
8617
- deviceId: number().int().nonnegative(),
8618
- profile: CamProfileSchema,
8619
- /** Broker id the rest of the system addresses: `${deviceId}/${profile}`. */
8620
- brokerId: string(),
8621
- /** `null` when the profile is unassigned. */
8622
- sourceCamStreamId: string().nullable(),
8623
- status: ProfileSlotStatusSchema,
8624
- resolution: CamStreamResolutionSchema.optional(),
8625
- codec: string().optional(),
8626
- preBufferSec: number().nonnegative().optional(),
8627
- errorMessage: string().optional()
8628
- });
8629
- /**
8630
- * Zod schema for StreamSourceEntry — the canonical stream descriptor
8631
- * exposed by ICameraDevice.getStreamSources() and consumed by the broker.
8632
- */
8633
- var StreamSourceEntrySchema$1 = object({
8634
- id: string(),
8635
- label: string(),
8636
- protocol: _enum([
8637
- "rtsp",
8638
- "rtmp",
8639
- "annexb",
8640
- "http-mjpeg",
8641
- "webrtc",
8642
- "custom"
8643
- ]),
8644
- url: string().optional(),
8645
- resolution: object({
8646
- width: number(),
8647
- height: number()
8648
- }).readonly().optional(),
8649
- fps: number().optional(),
8650
- bitrate: number().optional(),
8651
- codec: string().optional(),
8652
- profileHint: CamProfileSchema.optional()
8653
- });
8654
- object({
8655
- type: string(),
8656
- url: string(),
8657
- videoCodec: string().optional(),
8658
- audioCodec: string().optional(),
8659
- metadata: record(string(), unknown()).readonly().optional()
8660
- });
8661
- /**
8662
- * Wire schema for `FrameHandle` — a zero-pixel, serialisable reference to a
8663
- * frame in a shared-memory ring (Phase 5 / D9). Mirrors the `FrameHandle` TS
8664
- * interface in `interfaces/frame-handle.ts` field-for-field so a cap method
8665
- * (the runner's native-crop and pipeline-executor inputs) can carry handles
8666
- * over tRPC / Moleculer.
8667
- *
8668
- * The `satisfies` assertion below pins this schema to the interface: if a
8669
- * field is added to / removed from `FrameHandle` without a matching schema
8670
- * edit, `z.infer<typeof FrameHandleSchema>` stops being assignable to
8671
- * `FrameHandle` and the build fails — no silent drift.
8672
- */
8673
- var FrameHandleSchema = object({
8674
- shmId: string(),
8675
- slot: number().int().nonnegative(),
8676
- seq: number().int().nonnegative(),
8677
- width: number().int().positive(),
8678
- height: number().int().positive(),
8679
- format: _enum([
8680
- "jpeg",
8681
- "rgb",
8682
- "bgr",
8683
- "yuv420",
8684
- "gray"
8685
- ]),
8686
- pts: number(),
8687
- byteLength: number().int().nonnegative(),
8688
- nodeId: string(),
8689
- slotCount: number().int().positive()
8690
- });
8691
- /**
8692
- * Wire schema for a decoded audio chunk (Phase 5 / D9). Mirrors the
8693
- * `DecodedAudioChunk` TS interface in `interfaces/stream-broker.ts`: PCM
8694
- * sample bytes plus the track parameters a consumer needs to interpret them.
8695
- *
8696
- * Audio chunks are tiny (a ~500ms AudioCodecSession window is a few KB of
8697
- * Float32 PCM), so unlike video frames they ship their bytes INLINE over
8698
- * tRPC / Moleculer — no shared-memory plane. `data` is typed `Uint8Array`
8699
- * (the wire-serialisable supertype of `Buffer`); a `Buffer` is assignable to
8700
- * it.
8701
- */
8702
- var AudioChunkFormatSchema = _enum(AUDIO_CHUNK_FORMATS);
8703
- var DecodedAudioChunkSchema = object({
8704
- data: _instanceof(Uint8Array),
8705
- sampleRate: number().int().positive(),
8706
- channels: number().int().positive(),
8707
- timestamp: number(),
8708
- /**
8709
- * Byte format of `data`. ABSENT MEANS `f32le` — today's bytes, byte for
8710
- * byte, for any peer that never heard of this field. A coded window
8711
- * (`pcmu` / `pcma`, one byte per sample) is only ever emitted to a
8712
- * subscription that DECLARED it accepts one, so absence can never mean
8713
- * "coded bytes a consumer will read as floats" (D455).
8714
- */
8715
- format: AudioChunkFormatSchema.optional()
8716
- });
8717
- /**
8718
- * Input for `stream-broker.subscribeAudioChunks` (Phase 5 / D9). Audio ships
8719
- * its bytes inline: a consumer asks the broker for the decoded audio-chunk
8720
- * stream of one broker; the broker keeps a per-subscription bounded FIFO queue
8721
- * the consumer drains via `pullAudioChunks`.
8722
- */
8723
- var SubscribeAudioChunksInputSchema = object({
8724
- brokerId: string(),
8725
- /** Short caller-identity tag (`audio-analyzer`, …) for `listClients`. */
8726
- tag: string().optional(),
8727
- /**
8728
- * Byte formats this subscriber can READ, best first. The broker serves the
8729
- * chunk's own format when it is in this list and expands to `f32le`
8730
- * otherwise, so a subscriber is never handed bytes it cannot interpret.
8731
- *
8732
- * Absent (or without the source format) means `f32le` — the behaviour every
8733
- * subscriber had before D455, unchanged. This is the negotiation half of
8734
- * the source-bytes lever: it is what lets the broker and its consumers
8735
- * deploy one at a time across three nodes.
8736
- */
8737
- accept: array(AudioChunkFormatSchema).readonly().optional()
8738
- });
8739
- /** Result of `stream-broker.subscribeAudioChunks`. */
8740
- var SubscribeAudioChunksResultSchema = object({
8741
- /** Opaque id passed to `pullAudioChunks` / `unsubscribeAudioChunks`. */
8742
- subscriptionId: string() });
8743
- var BrokerStatsSchema = object({
8744
- status: _enum([
8745
- "idle",
8746
- "connecting",
8747
- "streaming",
8748
- "error",
8749
- "stopped"
8750
- ]),
8751
- inputFps: number(),
8752
- encodedSubscribers: number(),
8753
- uptimeMs: number(),
8754
- bitrateKbps: number(),
8755
- idrIntervalMs: number(),
8756
- codec: string().optional(),
8757
- totalBytes: number(),
8758
- packetCount: number(),
8759
- rtspClients: number(),
8760
- pipeClients: number(),
8761
- preBufferSec: number(),
8762
- preBufferMs: number(),
8763
- preBufferPackets: number(),
8764
- /**
8765
- * Detected audio track parameters from the RTSP DESCRIBE / SDP. `null`
8766
- * when the stream has no audio track or the broker is in cold start.
8767
- * `supported = false` means the codec was detected but the local
8768
- * decoder pipeline cannot produce PCM chunks (e.g. AAC without the
8769
- * AAC pipeline wired). Surfaced in the UI device-overview so operators
8770
- * can pre-pick the audio-analysis model that matches the codec.
8771
- */
8772
- audio: object({
8773
- codec: string(),
8774
- sampleRate: number(),
8775
- channels: number(),
8776
- supported: boolean()
8777
- }).nullable().optional(),
8778
- /**
8779
- * BROKER-SIDE AUDIO MUTE (D83). `true` = this broker is deliberately
8780
- * distributing none of the device's audio, on live or recording.
8781
- *
8782
- * Present so a silent camera can be told apart from a broken one on the
8783
- * stream panel itself, without cross-referencing the switch group: a
8784
- * broker holding an `audio` track descriptor while `audioMuted` is true is
8785
- * working exactly as asked. `audioMutedDropped` counts the audio units
8786
- * thrown away since the current dial — it is how you confirm from stats
8787
- * alone that the mute is on the packet path and not merely persisted.
8788
- */
8789
- audioMuted: boolean().optional(),
8790
- audioMutedDropped: number().optional()
8791
- });
8792
- /**
8793
- * Exporter-facing "profile restream" entry. Returned by
8794
- * `cameraStreams.getProfileRtspEntries` — one per ASSIGNED profile slot
8795
- * (high/mid/low). `brokerId` is the PROFILE-keyed broker id
8796
- * (`${deviceId}/${profile}`, e.g. `15/high`); its `url` is a broker
8797
- * RTSP restream that aliases the profile's assigned source broker, so
8798
- * every consumer dialling a profile converges on the same single pull.
8799
- * `codec`/`resolution` describe the assigned SOURCE camStream (for the
8800
- * `auto` resolution-closest pick). Distinct from `RtspRestreamEntry`
8801
- * (raw per-camStream, live-view only).
8802
- */
8803
- var ProfileRtspEntrySchema = object({
8804
- profile: CamProfileSchema,
8805
- /** Profile-keyed broker id, format `${deviceId}/${profile}` (e.g. `"15/high"`). */
8806
- brokerId: string(),
8807
- url: string(),
8808
- mutedUrl: string(),
8809
- enabled: boolean(),
8810
- codec: string().optional(),
8811
- resolution: CamStreamResolutionSchema.optional()
8812
- });
8813
- /**
8814
- * The model weight formats a catalog entry can ship. Schema-free on purpose:
8815
- * `types/models.ts` builds Zod schemas from it, but a runner-closure consumer
8816
- * (the model downloader in `@camstack/system`) needs only the list and must
8817
- * reach it through `@camstack/types/addon` without building the cap schema
8818
- * graph (D28).
8819
- */
8820
- var MODEL_FORMATS = [
8821
- "onnx",
8822
- "coreml",
8823
- "openvino",
8824
- "tflite",
8825
- "pt",
8826
- "gguf"
8827
- ];
8828
8941
  object({
8829
8942
  field: string().min(1),
8830
8943
  on: literal("advance")
@@ -8913,6 +9026,407 @@ function optionalMethod(input, output, options) {
8913
9026
  providerOptional: true
8914
9027
  };
8915
9028
  }
9029
+ var READING_QUANTITIES = [
9030
+ "temperature",
9031
+ "pressure",
9032
+ "speed",
9033
+ "precipitation",
9034
+ "precipitation-rate",
9035
+ "length",
9036
+ "area",
9037
+ "illuminance",
9038
+ "irradiance",
9039
+ "power",
9040
+ "energy",
9041
+ "voltage",
9042
+ "current",
9043
+ "apparent-power",
9044
+ "humidity",
9045
+ "percentage",
9046
+ "concentration-volume",
9047
+ "concentration-mass",
9048
+ "concentration-ppb",
9049
+ "volume",
9050
+ "duration",
9051
+ "duration-ms",
9052
+ "information",
9053
+ "sound-level",
9054
+ "signal-level",
9055
+ "colour-temperature",
9056
+ "angle",
9057
+ "bitrate",
9058
+ "rate",
9059
+ "mass",
9060
+ "index",
9061
+ "ratio",
9062
+ "count",
9063
+ "none",
9064
+ "device"
9065
+ ];
9066
+ var ReadingQuantitySchema = _enum(READING_QUANTITIES);
9067
+ new Set(READING_QUANTITIES);
9068
+ /**
9069
+ * Stored unit symbol for a quantity. `none` means the number is dimensionless
9070
+ * (or an index / ratio / count). `device` means the cap has no single unit.
9071
+ */
9072
+ var QUANTITY_BASE = {
9073
+ temperature: "°C",
9074
+ pressure: "hPa",
9075
+ speed: "m/s",
9076
+ precipitation: "mm",
9077
+ "precipitation-rate": "mm/h",
9078
+ length: "m",
9079
+ area: "m²",
9080
+ illuminance: "lx",
9081
+ irradiance: "W/m²",
9082
+ power: "W",
9083
+ energy: "kWh",
9084
+ voltage: "V",
9085
+ current: "A",
9086
+ "apparent-power": "VA",
9087
+ humidity: "%",
9088
+ percentage: "%",
9089
+ "concentration-volume": "ppm",
9090
+ "concentration-mass": "µg/m³",
9091
+ "concentration-ppb": "ppb",
9092
+ volume: "L",
9093
+ duration: "s",
9094
+ "duration-ms": "ms",
9095
+ information: "B",
9096
+ "sound-level": "dBFS",
9097
+ "signal-level": "dBm",
9098
+ "colour-temperature": "mired",
9099
+ angle: "°",
9100
+ bitrate: "kbps",
9101
+ rate: "fps",
9102
+ mass: "kg",
9103
+ index: "none",
9104
+ ratio: "none",
9105
+ count: "none",
9106
+ none: "none",
9107
+ device: "device"
9108
+ };
9109
+ /** The stored unit, or `undefined` when the quantity has no unit symbol. */
9110
+ function baseUnitOf(quantity) {
9111
+ const symbol = QUANTITY_BASE[quantity];
9112
+ if (symbol === "none" || symbol === "device") return void 0;
9113
+ return symbol;
9114
+ }
9115
+ function readingMeta(quantity, extras) {
9116
+ return {
9117
+ quantity,
9118
+ ...extras?.precision !== void 0 ? { precision: extras.precision } : {},
9119
+ ...extras?.label !== void 0 ? { label: extras.label } : {}
9120
+ };
9121
+ }
9122
+ /** Shared Zod schemas used across streaming capabilities. */
9123
+ var CamProfileSchema = _enum([
9124
+ "high",
9125
+ "mid",
9126
+ "low"
9127
+ ]);
9128
+ /**
9129
+ * Which rung a per-camera consumer listens on: an explicit profile, or `auto`.
9130
+ *
9131
+ * `auto` is a REAL value, not an absent one — it means "resolve it by the rule
9132
+ * that consumer owns". It lives beside `CamProfile` because a rung is the only
9133
+ * operator-facing way to name a stream: a cam-stream id is broker-internal and
9134
+ * moves when a slot is reassigned, so a stored id silently stops meaning what
9135
+ * the operator picked. Today's one user is the audio analyser, whose rule is
9136
+ * "the lowest AVAILABLE native rung" (D537).
9137
+ */
9138
+ var AudioStreamPickSchema = union([CamProfileSchema, literal("auto")]);
9139
+ var CamStreamKindSchema = _enum([
9140
+ "pull-rtsp",
9141
+ "pull-rtmp",
9142
+ "pull-http",
9143
+ "pull-flv",
9144
+ "pull-rfc4571",
9145
+ "push-annexb",
9146
+ "derived"
9147
+ ]);
9148
+ var CamStreamResolutionSchema = object({
9149
+ width: number().int().positive().meta(readingMeta("none")),
9150
+ height: number().int().positive().meta(readingMeta("none"))
9151
+ });
9152
+ var CameraStreamSchema = object({
9153
+ /** Stable, provider-assigned id unique within the (deviceId) scope. */
9154
+ camStreamId: string().min(1),
9155
+ deviceId: number().int().nonnegative(),
9156
+ kind: CamStreamKindSchema,
9157
+ /** Required for pull-* kinds. Ignored for push-annexb. */
9158
+ url: string().optional(),
9159
+ codec: string().optional(),
9160
+ resolution: CamStreamResolutionSchema.optional(),
9161
+ fps: number().positive().optional(),
9162
+ /** Human label surfaced in the Admin UI "Camera Stream" dropdown. */
9163
+ label: string().optional(),
9164
+ /**
9165
+ * Device-level features the publisher advertised (e.g. `battery-operated`).
9166
+ * The broker, snapshot orchestrator, and prebuffer manager all consult
9167
+ * this list to derive policy — relaxed stall watchdog for battery
9168
+ * cams, prebuffer off by default, longer snapshot rate-limit, etc.
9169
+ *
9170
+ * Single source of truth replacing per-stream flags like the
9171
+ * historical `allowStall`: if the publisher knows the camera is
9172
+ * battery-powered, every downstream service derives the right policy
9173
+ * from this list.
9174
+ */
9175
+ deviceFeatures: array(string()).optional(),
9176
+ /**
9177
+ * Whether this stream participates in the broker's automatic profile
9178
+ * assignment (`computeInitialAssignment`). Defaults to `true`. Publishers
9179
+ * use `false` when they want a stream to be SELECTABLE in the UI but not
9180
+ * picked by default — e.g. Reolink publishes its native Baichuan streams
9181
+ * as `autoEligible: true` (the recommended path) and its RTSP / RTMP
9182
+ * mirrors as `autoEligible: false` (still pickable per slot, just not
9183
+ * the auto choice). Manual `assignProfile` calls remain valid for
9184
+ * non-eligible streams.
9185
+ */
9186
+ autoEligible: boolean().optional(),
9187
+ /**
9188
+ * The profile slot the PUBLISHER says this stream is: `low` for a stream
9189
+ * whose only sensible role is the low-bandwidth one, `high` for a main
9190
+ * stream. Consulted by the broker's automatic ranking
9191
+ * (`rankInitialAssignment`) BEFORE the pixel-count heuristic, and only there:
9192
+ * an operator's manual assignment still wins, and an absent hint is the
9193
+ * pixel-ranked default every camera had before this field existed.
9194
+ *
9195
+ * Why it exists: a camera with ONE stream ranked to `mid` — the slot no
9196
+ * storage class is written for on its own (`recordings` is high|mid,
9197
+ * `recordingsLow` is low) and the one profile scrub does not read. The robot
9198
+ * camera's single 720p relay is a `low` in every respect except the slot the
9199
+ * heuristic put it in.
9200
+ */
9201
+ profileHint: CamProfileSchema.optional(),
9202
+ /**
9203
+ * Transport-specific opaque metadata. The broker passes it through to
9204
+ * the source reader without inspecting it. Currently used by
9205
+ * `pull-rfc4571` streams to carry the upstream SDP (so the reader can
9206
+ * route RTP packets to the right depacketizer without an in-band
9207
+ * DESCRIBE phase). Other kinds typically leave it undefined.
9208
+ */
9209
+ metadata: record(string(), unknown()).optional()
9210
+ });
9211
+ var ProfileSlotStatusSchema = _enum([
9212
+ "unassigned",
9213
+ "idle",
9214
+ "connecting",
9215
+ "streaming",
9216
+ "error"
9217
+ ]);
9218
+ var ProfileSlotSchema = object({
9219
+ deviceId: number().int().nonnegative(),
9220
+ profile: CamProfileSchema,
9221
+ /** Broker id the rest of the system addresses: `${deviceId}/${profile}`. */
9222
+ brokerId: string(),
9223
+ /** `null` when the profile is unassigned. */
9224
+ sourceCamStreamId: string().nullable(),
9225
+ status: ProfileSlotStatusSchema,
9226
+ resolution: CamStreamResolutionSchema.optional(),
9227
+ codec: string().optional(),
9228
+ preBufferSec: number().nonnegative().optional(),
9229
+ errorMessage: string().optional()
9230
+ });
9231
+ /**
9232
+ * Zod schema for StreamSourceEntry — the canonical stream descriptor
9233
+ * exposed by ICameraDevice.getStreamSources() and consumed by the broker.
9234
+ */
9235
+ var StreamSourceEntrySchema$1 = object({
9236
+ id: string(),
9237
+ label: string(),
9238
+ protocol: _enum([
9239
+ "rtsp",
9240
+ "rtmp",
9241
+ "annexb",
9242
+ "http-mjpeg",
9243
+ "webrtc",
9244
+ "custom"
9245
+ ]),
9246
+ url: string().optional(),
9247
+ resolution: object({
9248
+ width: number(),
9249
+ height: number()
9250
+ }).readonly().optional(),
9251
+ fps: number().optional(),
9252
+ bitrate: number().optional(),
9253
+ codec: string().optional(),
9254
+ profileHint: CamProfileSchema.optional()
9255
+ });
9256
+ object({
9257
+ type: string(),
9258
+ url: string(),
9259
+ videoCodec: string().optional(),
9260
+ audioCodec: string().optional(),
9261
+ metadata: record(string(), unknown()).readonly().optional()
9262
+ });
9263
+ /**
9264
+ * Wire schema for `FrameHandle` — a zero-pixel, serialisable reference to a
9265
+ * frame in a shared-memory ring (Phase 5 / D9). Mirrors the `FrameHandle` TS
9266
+ * interface in `interfaces/frame-handle.ts` field-for-field so a cap method
9267
+ * (the runner's native-crop and pipeline-executor inputs) can carry handles
9268
+ * over tRPC / Moleculer.
9269
+ *
9270
+ * The `satisfies` assertion below pins this schema to the interface: if a
9271
+ * field is added to / removed from `FrameHandle` without a matching schema
9272
+ * edit, `z.infer<typeof FrameHandleSchema>` stops being assignable to
9273
+ * `FrameHandle` and the build fails — no silent drift.
9274
+ */
9275
+ var FrameHandleSchema = object({
9276
+ shmId: string(),
9277
+ slot: number().int().nonnegative(),
9278
+ seq: number().int().nonnegative(),
9279
+ width: number().int().positive(),
9280
+ height: number().int().positive(),
9281
+ format: _enum([
9282
+ "jpeg",
9283
+ "rgb",
9284
+ "bgr",
9285
+ "yuv420",
9286
+ "gray"
9287
+ ]),
9288
+ pts: number(),
9289
+ byteLength: number().int().nonnegative(),
9290
+ nodeId: string(),
9291
+ slotCount: number().int().positive()
9292
+ });
9293
+ /**
9294
+ * Wire schema for a decoded audio chunk (Phase 5 / D9). Mirrors the
9295
+ * `DecodedAudioChunk` TS interface in `interfaces/stream-broker.ts`: PCM
9296
+ * sample bytes plus the track parameters a consumer needs to interpret them.
9297
+ *
9298
+ * Audio chunks are tiny (a ~500ms AudioCodecSession window is a few KB of
9299
+ * Float32 PCM), so unlike video frames they ship their bytes INLINE over
9300
+ * tRPC / Moleculer — no shared-memory plane. `data` is typed `Uint8Array`
9301
+ * (the wire-serialisable supertype of `Buffer`); a `Buffer` is assignable to
9302
+ * it.
9303
+ */
9304
+ var AudioChunkFormatSchema = _enum(AUDIO_CHUNK_FORMATS);
9305
+ var DecodedAudioChunkSchema = object({
9306
+ data: _instanceof(Uint8Array),
9307
+ sampleRate: number().int().positive(),
9308
+ channels: number().int().positive(),
9309
+ timestamp: number(),
9310
+ /**
9311
+ * Byte format of `data`. ABSENT MEANS `f32le` — today's bytes, byte for
9312
+ * byte, for any peer that never heard of this field. A coded window
9313
+ * (`pcmu` / `pcma`, one byte per sample) is only ever emitted to a
9314
+ * subscription that DECLARED it accepts one, so absence can never mean
9315
+ * "coded bytes a consumer will read as floats" (D455).
9316
+ */
9317
+ format: AudioChunkFormatSchema.optional()
9318
+ });
9319
+ /**
9320
+ * Input for `stream-broker.subscribeAudioChunks` (Phase 5 / D9). Audio ships
9321
+ * its bytes inline: a consumer asks the broker for the decoded audio-chunk
9322
+ * stream of one broker; the broker keeps a per-subscription bounded FIFO queue
9323
+ * the consumer drains via `pullAudioChunks`.
9324
+ */
9325
+ var SubscribeAudioChunksInputSchema = object({
9326
+ brokerId: string(),
9327
+ /** Short caller-identity tag (`audio-analyzer`, …) for `listClients`. */
9328
+ tag: string().optional(),
9329
+ /**
9330
+ * Byte formats this subscriber can READ, best first. The broker serves the
9331
+ * chunk's own format when it is in this list and expands to `f32le`
9332
+ * otherwise, so a subscriber is never handed bytes it cannot interpret.
9333
+ *
9334
+ * Absent (or without the source format) means `f32le` — the behaviour every
9335
+ * subscriber had before D455, unchanged. This is the negotiation half of
9336
+ * the source-bytes lever: it is what lets the broker and its consumers
9337
+ * deploy one at a time across three nodes.
9338
+ */
9339
+ accept: array(AudioChunkFormatSchema).readonly().optional()
9340
+ });
9341
+ /** Result of `stream-broker.subscribeAudioChunks`. */
9342
+ var SubscribeAudioChunksResultSchema = object({
9343
+ /** Opaque id passed to `pullAudioChunks` / `unsubscribeAudioChunks`. */
9344
+ subscriptionId: string() });
9345
+ var BrokerStatsSchema = object({
9346
+ status: _enum([
9347
+ "idle",
9348
+ "connecting",
9349
+ "streaming",
9350
+ "error",
9351
+ "stopped"
9352
+ ]),
9353
+ inputFps: number(),
9354
+ encodedSubscribers: number(),
9355
+ uptimeMs: number(),
9356
+ bitrateKbps: number(),
9357
+ idrIntervalMs: number(),
9358
+ codec: string().optional(),
9359
+ totalBytes: number(),
9360
+ packetCount: number(),
9361
+ rtspClients: number(),
9362
+ pipeClients: number(),
9363
+ preBufferSec: number(),
9364
+ preBufferMs: number(),
9365
+ preBufferPackets: number(),
9366
+ /**
9367
+ * Detected audio track parameters from the RTSP DESCRIBE / SDP. `null`
9368
+ * when the stream has no audio track or the broker is in cold start.
9369
+ * `supported = false` means the codec was detected but the local
9370
+ * decoder pipeline cannot produce PCM chunks (e.g. AAC without the
9371
+ * AAC pipeline wired). Surfaced in the UI device-overview so operators
9372
+ * can pre-pick the audio-analysis model that matches the codec.
9373
+ */
9374
+ audio: object({
9375
+ codec: string(),
9376
+ sampleRate: number(),
9377
+ channels: number(),
9378
+ supported: boolean()
9379
+ }).nullable().optional(),
9380
+ /**
9381
+ * BROKER-SIDE AUDIO MUTE (D83). `true` = this broker is deliberately
9382
+ * distributing none of the device's audio, on live or recording.
9383
+ *
9384
+ * Present so a silent camera can be told apart from a broken one on the
9385
+ * stream panel itself, without cross-referencing the switch group: a
9386
+ * broker holding an `audio` track descriptor while `audioMuted` is true is
9387
+ * working exactly as asked. `audioMutedDropped` counts the audio units
9388
+ * thrown away since the current dial — it is how you confirm from stats
9389
+ * alone that the mute is on the packet path and not merely persisted.
9390
+ */
9391
+ audioMuted: boolean().optional(),
9392
+ audioMutedDropped: number().optional()
9393
+ });
9394
+ /**
9395
+ * Exporter-facing "profile restream" entry. Returned by
9396
+ * `cameraStreams.getProfileRtspEntries` — one per ASSIGNED profile slot
9397
+ * (high/mid/low). `brokerId` is the PROFILE-keyed broker id
9398
+ * (`${deviceId}/${profile}`, e.g. `15/high`); its `url` is a broker
9399
+ * RTSP restream that aliases the profile's assigned source broker, so
9400
+ * every consumer dialling a profile converges on the same single pull.
9401
+ * `codec`/`resolution` describe the assigned SOURCE camStream (for the
9402
+ * `auto` resolution-closest pick). Distinct from `RtspRestreamEntry`
9403
+ * (raw per-camStream, live-view only).
9404
+ */
9405
+ var ProfileRtspEntrySchema = object({
9406
+ profile: CamProfileSchema,
9407
+ /** Profile-keyed broker id, format `${deviceId}/${profile}` (e.g. `"15/high"`). */
9408
+ brokerId: string(),
9409
+ url: string(),
9410
+ mutedUrl: string(),
9411
+ enabled: boolean(),
9412
+ codec: string().optional(),
9413
+ resolution: CamStreamResolutionSchema.optional()
9414
+ });
9415
+ /**
9416
+ * The model weight formats a catalog entry can ship. Schema-free on purpose:
9417
+ * `types/models.ts` builds Zod schemas from it, but a runner-closure consumer
9418
+ * (the model downloader in `@camstack/server/system`) needs only the list and must
9419
+ * reach it through `@camstack/types/addon` without building the cap schema
9420
+ * graph (D28).
9421
+ */
9422
+ var MODEL_FORMATS = [
9423
+ "onnx",
9424
+ "coreml",
9425
+ "openvino",
9426
+ "tflite",
9427
+ "pt",
9428
+ "gguf"
9429
+ ];
8916
9430
  /**
8917
9431
  * `addon-pages-source` — collection cap exposing per-provider raw page
8918
9432
  * declarations. Every addon that contributes a UI page registers a
@@ -9176,6 +9690,7 @@ var CAP_NAMES = Object.freeze([
9176
9690
  "mesh-network",
9177
9691
  "meter",
9178
9692
  "metrics-provider",
9693
+ "migrations",
9179
9694
  "model-convert",
9180
9695
  "model-distributor",
9181
9696
  "motion",
@@ -9208,6 +9723,7 @@ var CAP_NAMES = Object.freeze([
9208
9723
  "plate-gallery",
9209
9724
  "platform-probe",
9210
9725
  "power-meter",
9726
+ "power-source",
9211
9727
  "presence",
9212
9728
  "pressure-sensor",
9213
9729
  "privacy-mask",
@@ -10171,7 +10687,8 @@ Object.freeze({
10171
10687
  "unlocked",
10172
10688
  "locking",
10173
10689
  "unlocking",
10174
- "jammed"
10690
+ "jammed",
10691
+ "door-open"
10175
10692
  ]
10176
10693
  }],
10177
10694
  "media-player": [
@@ -10558,6 +11075,13 @@ Object.freeze({
10558
11075
  "valueType": "number"
10559
11076
  }
10560
11077
  ],
11078
+ "power-source": [{
11079
+ "path": "switchOn",
11080
+ "valueType": "boolean"
11081
+ }, {
11082
+ "path": "offSeconds",
11083
+ "valueType": "number"
11084
+ }],
10561
11085
  "presence": [
10562
11086
  {
10563
11087
  "path": "state",
@@ -11083,369 +11607,7 @@ function userError(code, params, englishFallback) {
11083
11607
  return new CamstackError(code, params, englishFallback);
11084
11608
  }
11085
11609
  //#endregion
11086
- //#region ../types/dist/err-msg-BD5buxvT.mjs
11087
- var DeviceType = /* @__PURE__ */ function(DeviceType) {
11088
- DeviceType["Camera"] = "camera";
11089
- DeviceType["Hub"] = "hub";
11090
- DeviceType["Light"] = "light";
11091
- DeviceType["Siren"] = "siren";
11092
- DeviceType["Switch"] = "switch";
11093
- DeviceType["Sensor"] = "sensor";
11094
- DeviceType["Thermostat"] = "thermostat";
11095
- /** Air-conditioner / heat-pump climate device (HVAC) — shares the
11096
- * `climate-control` cap surface with `Thermostat` but renders a
11097
- * dedicated AC-appropriate control UI (mode chips, fan speed,
11098
- * independent vertical/horizontal swing). Sources: native Gree, and
11099
- * reusable by other AC integrations. */
11100
- DeviceType["Climate"] = "climate";
11101
- DeviceType["Button"] = "button";
11102
- /** Generic stateless event emitter — carries a device's EXACT declared
11103
- * event vocabulary verbatim (no normalization). Installed with the
11104
- * `event-emitter` cap. Sources: HA `event.*` entities (structured) and
11105
- * HA bus events (e.g. `zha_event`, generic). */
11106
- DeviceType["EventEmitter"] = "event-emitter";
11107
- /** Firmware/software update entity — current vs available version,
11108
- * updatable flag, update state, and an install action. Installed with
11109
- * the `update` cap. Sources: Homematic firmware-update channels (and
11110
- * reusable by other providers, e.g. HA `update.*` entities). */
11111
- DeviceType["Update"] = "update";
11112
- DeviceType["Generic"] = "generic";
11113
- /** Generic notification delivery target (HA `notify.<service>`, future
11114
- * Telegram / Discord / ntfy / SMTP, …). One device per delivery
11115
- * endpoint; the `notifier` cap defines the send surface. */
11116
- DeviceType["Notifier"] = "notifier";
11117
- /** Pre-recorded action sequence with optional parameters
11118
- * (HA `script.*`). Runnable via `script-runner` cap. */
11119
- DeviceType["Script"] = "script";
11120
- /** Automation rule (HA `automation.*`) — enable/disable + manual
11121
- * trigger surface exposed via `automation-control` cap. */
11122
- DeviceType["Automation"] = "automation";
11123
- /** Door / smart lock device (HA `lock.*`). `lock-control` cap. */
11124
- DeviceType["Lock"] = "lock";
11125
- /** Window covering, blinds, garage door, valve, etc. (HA `cover.*`,
11126
- * `valve.*`). `cover` cap with sub-roles for variant. */
11127
- DeviceType["Cover"] = "cover";
11128
- /** Pipe / water / gas valve with open/close/stop and optional
11129
- * position (HA `valve.*`). `valve` cap — a cover-sibling actuator
11130
- * modelled on the same open/closed lifecycle. */
11131
- DeviceType["Valve"] = "valve";
11132
- /** Humidifier / dehumidifier with on/off + target humidity + mode
11133
- * (HA `humidifier.*`). `humidifier` cap — a climate-family actuator
11134
- * modelled on the same target / mode lifecycle. */
11135
- DeviceType["Humidifier"] = "humidifier";
11136
- /** Water heater / boiler with target temperature + operation mode +
11137
- * away mode (HA `water_heater.*`). `water-heater` cap — a
11138
- * climate-family actuator. */
11139
- DeviceType["WaterHeater"] = "water-heater";
11140
- /** Ceiling / standing / exhaust fan (HA `fan.*`). `fan-control` cap. */
11141
- DeviceType["Fan"] = "fan";
11142
- /** Audio / video playback endpoint (HA `media_player.*`). Disjoint from
11143
- * the camera surface — those use `Camera`. `media-player` cap. */
11144
- DeviceType["MediaPlayer"] = "media-player";
11145
- /** Security panel / alarm system (HA `alarm_control_panel.*`).
11146
- * `alarm-panel` cap. */
11147
- DeviceType["AlarmPanel"] = "alarm-panel";
11148
- /** Generic user-settable input (HA `number` / `input_number` / `select`
11149
- * / `input_select` / `text` / `input_text` / `input_datetime`).
11150
- * Sub-type via `DeviceRole`: NumericControl / SelectControl /
11151
- * TextControl / DateTimeControl. */
11152
- DeviceType["Control"] = "control";
11153
- /** Person / device-tracker presence (HA `person.*`, `device_tracker.*`).
11154
- * `presence` cap. */
11155
- DeviceType["Presence"] = "presence";
11156
- /** Weather provider (HA `weather.*`). Tier-3, low MVP priority.
11157
- * `weather` cap. */
11158
- DeviceType["Weather"] = "weather";
11159
- /** Robot vacuum (HA `vacuum.*`). Tier-3. `vacuum-control` cap. */
11160
- DeviceType["Vacuum"] = "vacuum";
11161
- /** Robotic lawn mower (HA `lawn_mower.*`). Tier-3.
11162
- * `lawn-mower-control` cap. */
11163
- DeviceType["LawnMower"] = "lawn-mower";
11164
- /** Physical HA device group — parent container for entity-children
11165
- * adopted from a single HA device entry. Not renderable as a
11166
- * standalone device; exists only to anchor child entities. */
11167
- DeviceType["Container"] = "container";
11168
- /** Single still-image entity (HA `image.*`). Read-only display of an
11169
- * `entity_picture` signed URL the browser loads directly. `image` cap. */
11170
- DeviceType["Image"] = "image";
11171
- /** Smart pet feeder — cloud-connected food dispenser with a bowl food
11172
- * level, battery, desiccant life, feeding state and manual-feed /
11173
- * call-pet / maintenance actions. Installed with the `pet-feeder` cap;
11174
- * dual-hopper models (D4S/D4SH) expose per-hopper portions. Sources:
11175
- * native PetKit (`nodepetkit` `FeederDevice`), reusable by other feeder
11176
- * integrations sharing the same food/desiccant/hopper surface. */
11177
- DeviceType["PetFeeder"] = "pet-feeder";
11178
- /** Pet water fountain — tank level, water drunk, pump and its duty cycle.
11179
- * Installed with the `pet-fountain` cap. Reaches CamStack as loose entities
11180
- * of a container and is BUILT as a composed device (D967). */
11181
- DeviceType["PetFountain"] = "pet-fountain";
11182
- /** Built-in dishwasher — program lifecycle, door / remote-start gates,
11183
- * salt and rinse-aid levels. Installed with the `dishwasher` cap.
11184
- * Sources: native Bosch Home Connect, reusable by similar integrations.
11185
- * A plug-built dishwasher carries running-state and power instead. */
11186
- DeviceType["Dishwasher"] = "dishwasher";
11187
- /** Washing machine. A plug-built one is running while its power stays
11188
- * above a threshold. No program cap — that surface is the dishwasher's. */
11189
- DeviceType["Washer"] = "washer";
11190
- /** Tumble dryer. Same plug-built running surface as {@link Washer}. */
11191
- DeviceType["Dryer"] = "dryer";
11192
- /** The physical bridge box of a vendor ecosystem (Velux KLF-200, and later
11193
- * other radio-to-IP bridges). It carries the box's own facts (firmware,
11194
- * address, connection state) and the actions the box really supports
11195
- * (`reboot`, `device-admin-link`). Products are NOT its children — they are
11196
- * adopted beside it — and its connection settings stay on the integration. */
11197
- DeviceType["Gateway"] = "gateway";
11198
- return DeviceType;
11199
- }({});
11200
- var DeviceFeature = /* @__PURE__ */ function(DeviceFeature) {
11201
- DeviceFeature["BatteryOperated"] = "battery-operated";
11202
- DeviceFeature["Rebootable"] = "rebootable";
11203
- /**
11204
- * Device supports an on-demand re-sync of its derived spec with its
11205
- * upstream source — drives the generic Re-sync button. The owning
11206
- * provider implements the action via the `device-adoption.resync` cap.
11207
- */
11208
- DeviceFeature["Resyncable"] = "resyncable";
11209
- DeviceFeature["NativeSnapshot"] = "native-snapshot";
11210
- DeviceFeature["DoorbellButton"] = "doorbell-button";
11211
- DeviceFeature["TwoWayAudio"] = "two-way-audio";
11212
- DeviceFeature["PanTiltZoom"] = "pan-tilt-zoom";
11213
- /**
11214
- * Camera supports the on-firmware autotrack subsystem (subject-
11215
- * following). Distinct from `PanTiltZoom` because not every PTZ
11216
- * camera ships autotrack — the admin UI uses this flag to gate
11217
- * the autotrack toggle / settings card without re-deriving from
11218
- * the cap registry. Mirrors `ptz-autotrack` cap registration:
11219
- * driver sets this feature when probe confirms the firmware
11220
- * surface, and registers the cap in the same code path.
11221
- */
11222
- DeviceFeature["PtzAutotrack"] = "ptz-autotrack";
11223
- /**
11224
- * Accessory exposes a "trigger on motion" toggle — the parent camera's
11225
- * motion detection automatically activates this device. Mirrors
11226
- * `motion-trigger` cap registration: drivers set this feature in the
11227
- * same code path that calls `ctx.registerNativeCap(motionTriggerCapability, ...)`.
11228
- *
11229
- * Used by admin UI (gate the in-hero `MotionTriggerToggle` against a
11230
- * fast scalar without binding fetch), notifier rules, and `listAll`
11231
- * filters that want "all devices with on-motion behaviour".
11232
- */
11233
- DeviceFeature["MotionTrigger"] = "motion-trigger";
11234
- /** Light supports rgb-triplet color via `color` cap. */
11235
- DeviceFeature["LightColorRgb"] = "light-color-rgb";
11236
- /** Light supports HSV color via `color` cap. */
11237
- DeviceFeature["LightColorHsv"] = "light-color-hsv";
11238
- /** Light supports color-temperature (mired) via `color` cap. */
11239
- DeviceFeature["LightColorMired"] = "light-color-mired";
11240
- /** Thermostat supports a `heat_cool` dual setpoint (targetLow +
11241
- * targetHigh). Gates the range slider UI. */
11242
- DeviceFeature["ClimateDualSetpoint"] = "climate-dual-setpoint";
11243
- /** Thermostat exposes target humidity and/or current humidity
11244
- * readings. Gates the humidity controls. */
11245
- DeviceFeature["ClimateHumidity"] = "climate-humidity";
11246
- /** Thermostat exposes a fan-mode selector. */
11247
- DeviceFeature["ClimateFanMode"] = "climate-fan-mode";
11248
- /** Thermostat exposes preset modes (eco / away / sleep / vendor). */
11249
- DeviceFeature["ClimatePreset"] = "climate-preset";
11250
- /** Thermostat exposes a vertical louver swing toggle. Gates the
11251
- * vertical-swing switch in the climate UI. Independent of horizontal. */
11252
- DeviceFeature["ClimateSwingVertical"] = "climate-swing-vertical";
11253
- /** Thermostat exposes a horizontal louver swing toggle. Gates the
11254
- * horizontal-swing switch in the climate UI. Independent of vertical. */
11255
- DeviceFeature["ClimateSwingHorizontal"] = "climate-swing-horizontal";
11256
- /** Cover exposes intermediate position control (0..100). Gates the
11257
- * position slider UI. */
11258
- DeviceFeature["CoverPositionable"] = "cover-positionable";
11259
- /** Cover exposes slat-tilt control. Gates the tilt slider UI. */
11260
- DeviceFeature["CoverTilt"] = "cover-tilt";
11261
- /** Valve exposes intermediate position control (0..100). Gates the
11262
- * position slider / drag surface UI. */
11263
- DeviceFeature["ValvePositionable"] = "valve-positionable";
11264
- /** Fan exposes a speed-percentage setter. Gates the speed slider UI. */
11265
- DeviceFeature["FanSpeed"] = "fan-speed";
11266
- /** Fan exposes a preset mode selector. */
11267
- DeviceFeature["FanPreset"] = "fan-preset";
11268
- /** Fan exposes blade direction (forward/reverse) — typical of
11269
- * ceiling fans. */
11270
- DeviceFeature["FanDirection"] = "fan-direction";
11271
- /** Fan exposes an oscillation toggle. */
11272
- DeviceFeature["FanOscillating"] = "fan-oscillating";
11273
- /** Lock requires a PIN code on lock/unlock. Gates the code-entry
11274
- * field on the UI lock-controls panel. */
11275
- DeviceFeature["LockPinRequired"] = "lock-pin-required";
11276
- /** Lock supports a latch-release ("open door") action distinct from
11277
- * unlock. Mirrors HA `LockEntityFeature.OPEN` (bit 1) in
11278
- * `supported_features`. Gates the Open Door button in the UI. */
11279
- DeviceFeature["LockOpen"] = "lock-open";
11280
- /** Media player exposes a seek-to-position surface. */
11281
- DeviceFeature["MediaPlayerSeek"] = "media-player-seek";
11282
- /** Media player exposes a volume-level setter. */
11283
- DeviceFeature["MediaPlayerVolume"] = "media-player-volume";
11284
- /** Media player exposes a mute toggle distinct from volume=0. */
11285
- DeviceFeature["MediaPlayerMute"] = "media-player-mute";
11286
- /** Media player exposes a shuffle toggle. */
11287
- DeviceFeature["MediaPlayerShuffle"] = "media-player-shuffle";
11288
- /** Media player exposes a repeat mode (off / all / one). */
11289
- DeviceFeature["MediaPlayerRepeat"] = "media-player-repeat";
11290
- /** Media player exposes a source / input selector. */
11291
- DeviceFeature["MediaPlayerSelectSource"] = "media-player-select-source";
11292
- /** Media player exposes a play-arbitrary-media surface (URL / id). */
11293
- DeviceFeature["MediaPlayerPlayMedia"] = "media-player-play-media";
11294
- /** Media player exposes next-track. */
11295
- DeviceFeature["MediaPlayerNext"] = "media-player-next";
11296
- /** Media player exposes previous-track. */
11297
- DeviceFeature["MediaPlayerPrevious"] = "media-player-previous";
11298
- /** Media player exposes stop distinct from pause. */
11299
- DeviceFeature["MediaPlayerStop"] = "media-player-stop";
11300
- /** Alarm panel requires a PIN code on arm/disarm. */
11301
- DeviceFeature["AlarmPinRequired"] = "alarm-pin-required";
11302
- /** Presence device carries GPS coordinates (lat/lng/accuracy) in
11303
- * addition to a textual location. */
11304
- DeviceFeature["PresenceGps"] = "presence-gps";
11305
- /** Notifier accepts an inline / URL image attachment. */
11306
- DeviceFeature["NotifierImage"] = "notifier-image";
11307
- /** Notifier accepts a priority hint (high/normal/low). */
11308
- DeviceFeature["NotifierPriority"] = "notifier-priority";
11309
- /** Notifier accepts a free-form `data` payload for platform-specific
11310
- * fields. */
11311
- DeviceFeature["NotifierData"] = "notifier-data";
11312
- /** Notifier supports interactive action buttons / callbacks. */
11313
- DeviceFeature["NotifierActions"] = "notifier-actions";
11314
- /** Notifier supports per-call recipient targeting (multi-user). */
11315
- DeviceFeature["NotifierRecipients"] = "notifier-recipients";
11316
- /** Script runner accepts a variables map on each run invocation. */
11317
- DeviceFeature["ScriptVariables"] = "script-variables";
11318
- /** Automation `trigger` accepts a skipCondition flag — fires the
11319
- * automation's actions while bypassing its condition block. */
11320
- DeviceFeature["AutomationSkipCondition"] = "automation-skip-condition";
11321
- /** Robot vacuum exposes a live cleaning map (image child). Gates the
11322
- * map tile in the vacuum UI. */
11323
- DeviceFeature["VacuumHasMap"] = "vacuum-has-map";
11324
- /** Robot vacuum exposes AI obstacle-detection toggles. Gates the AI
11325
- * switches group. */
11326
- DeviceFeature["VacuumHasAi"] = "vacuum-has-ai";
11327
- /** Robot mower exposes a live mowing map (SVG image child). Gates the
11328
- * mower map tile. */
11329
- DeviceFeature["MowerHasMap"] = "mower-has-map";
11330
- /** Robot mower exposes targeted mowing (all-area / zones / edges /
11331
- * spots) — gates the Mow action + map selectors. */
11332
- DeviceFeature["MowerHasTargetedMowing"] = "mower-has-targeted-mowing";
11333
- return DeviceFeature;
11334
- }({});
11335
- /**
11336
- * Semantic role a device plays within its parent. Populated by driver
11337
- * addons when creating accessory devices (Reolink siren/floodlight/
11338
- * PIR/chime/autotrack/doorbell, ONVIF relay outputs, …). Used by the
11339
- * admin UI to pick icons, labels, and widgets — a `Switch` with
11340
- * `role: Floodlight` renders as a bulb with a brightness slider,
11341
- * whereas a `Switch` with `role: Siren` renders as a klaxon.
11342
- *
11343
- * Undefined for top-level devices (cameras, NVRs, hubs). Persisted in
11344
- * sqlite as a nullable TEXT column — old rows keep working unchanged.
11345
- */
11346
- var DeviceRole = /* @__PURE__ */ function(DeviceRole) {
11347
- DeviceRole["Siren"] = "siren";
11348
- DeviceRole["Floodlight"] = "floodlight";
11349
- DeviceRole["Spotlight"] = "spotlight";
11350
- DeviceRole["PirSensor"] = "pir-sensor";
11351
- DeviceRole["Chime"] = "chime";
11352
- DeviceRole["Autotrack"] = "autotrack";
11353
- DeviceRole["Nightvision"] = "nightvision";
11354
- DeviceRole["PrivacyMask"] = "privacy-mask";
11355
- DeviceRole["Doorbell"] = "doorbell";
11356
- /** Interior blind / shade / roller blind. HA `device_class: blind|shade`. */
11357
- DeviceRole["CoverBlind"] = "cover-blind";
11358
- /** Exterior roller shutter. HA `device_class: shutter`; Homematic BROLL. */
11359
- DeviceRole["CoverShutter"] = "cover-shutter";
11360
- /** Exterior awning. HA `device_class: awning`. */
11361
- DeviceRole["CoverAwning"] = "cover-awning";
11362
- /** Curtain / drape. HA `device_class: curtain`. */
11363
- DeviceRole["CoverCurtain"] = "cover-curtain";
11364
- /** Garage door. HA `device_class: garage`. A security actuator: exports gate it. */
11365
- DeviceRole["CoverGarage"] = "cover-garage";
11366
- /** Gate. HA `device_class: gate`. A security actuator: exports gate it. */
11367
- DeviceRole["CoverGate"] = "cover-gate";
11368
- /** Door. HA `device_class: door`. A security actuator: exports gate it. */
11369
- DeviceRole["CoverDoor"] = "cover-door";
11370
- /** Window. HA `device_class: window`. */
11371
- DeviceRole["CoverWindow"] = "cover-window";
11372
- /** Virtual HA toggle (input_boolean.*) — distinguishable from a
11373
- * real Switch device for UI rendering / export adapters. */
11374
- DeviceRole["BinaryHelper"] = "binary-helper";
11375
- /** A smart plug: a `switch` whose source says it is an outlet (HA
11376
- * `device_class: outlet`). The Alexa export shows it as a SMARTPLUG. */
11377
- DeviceRole["Outlet"] = "outlet";
11378
- /** Generic motion / occupancy / moving event source. Distinct from
11379
- * the camera accessory PirSensor role: that one is a camera child;
11380
- * this is a standalone HA / 3rd-party motion sensor. */
11381
- DeviceRole["MotionSensor"] = "motion-sensor";
11382
- DeviceRole["ContactSensor"] = "contact-sensor";
11383
- DeviceRole["LeakSensor"] = "leak-sensor";
11384
- DeviceRole["SmokeSensor"] = "smoke-sensor";
11385
- DeviceRole["COSensor"] = "co-sensor";
11386
- DeviceRole["GasSensor"] = "gas-sensor";
11387
- DeviceRole["TamperSensor"] = "tamper-sensor";
11388
- DeviceRole["VibrationSensor"] = "vibration-sensor";
11389
- DeviceRole["ConnectivitySensor"] = "connectivity-sensor";
11390
- DeviceRole["SoundSensor"] = "sound-sensor";
11391
- /** Fallback for `binary_sensor` without a known `device_class`. */
11392
- DeviceRole["BinarySensor"] = "binary-sensor";
11393
- DeviceRole["TemperatureSensor"] = "temperature-sensor";
11394
- DeviceRole["HumiditySensor"] = "humidity-sensor";
11395
- DeviceRole["AmbientLightSensor"] = "ambient-light-sensor";
11396
- DeviceRole["PressureSensor"] = "pressure-sensor";
11397
- /** Wind speed or direction (weather-station `wind-sensor` cap). */
11398
- DeviceRole["WindSensor"] = "wind-sensor";
11399
- /** Rain accumulation or rate (weather-station `rain-sensor` cap). */
11400
- DeviceRole["RainSensor"] = "rain-sensor";
11401
- /** UV index (weather-station `uv-sensor` cap). */
11402
- DeviceRole["UvSensor"] = "uv-sensor";
11403
- /** Solar irradiance W/m² (weather-station `solar-radiation-sensor` cap).
11404
- * Distinct from AmbientLightSensor (lux). */
11405
- DeviceRole["SolarRadiationSensor"] = "solar-radiation-sensor";
11406
- /** Soil moisture % (garden/weather `soil-moisture-sensor` cap). */
11407
- DeviceRole["SoilMoistureSensor"] = "soil-moisture-sensor";
11408
- DeviceRole["PowerSensor"] = "power-sensor";
11409
- DeviceRole["EnergySensor"] = "energy-sensor";
11410
- DeviceRole["VoltageSensor"] = "voltage-sensor";
11411
- DeviceRole["CurrentSensor"] = "current-sensor";
11412
- DeviceRole["AirQualitySensor"] = "air-quality-sensor";
11413
- /** Battery level (numeric % via `sensor` OR low-bool via
11414
- * `binary_sensor` — the cap distinguishes via the value type). */
11415
- DeviceRole["BatterySensor"] = "battery-sensor";
11416
- /** Fallback for `sensor` numeric without a known `device_class`. */
11417
- DeviceRole["NumericSensor"] = "numeric-sensor";
11418
- /** String / enum state (HA `sensor` with `state_class: enum` or
11419
- * `attributes.options`). */
11420
- DeviceRole["EnumSensor"] = "enum-sensor";
11421
- /** Date / timestamp state (HA `sensor` with `device_class: timestamp`
11422
- * or `date`). The slice carries the raw ISO string verbatim (hosted on
11423
- * the `enum-sensor` cap); the UI renders it locale-formatted. */
11424
- DeviceRole["DateTimeSensor"] = "datetime-sensor";
11425
- /** Last-resort fallback when nothing else matches. */
11426
- DeviceRole["GenericSensor"] = "generic-sensor";
11427
- DeviceRole["NumericControl"] = "numeric-control";
11428
- DeviceRole["SelectControl"] = "select-control";
11429
- DeviceRole["TextControl"] = "text-control";
11430
- DeviceRole["DateTimeControl"] = "datetime-control";
11431
- /** Mobile push notifier (HA `notify.mobile_app_*`) — supports
11432
- * rich features (image, priority, channel routing). */
11433
- DeviceRole["MobilePushNotifier"] = "mobile-push-notifier";
11434
- /** Chat / messaging service (HA `notify.telegram_*`,
11435
- * `notify.discord_*`, etc.). */
11436
- DeviceRole["MessagingNotifier"] = "messaging-notifier";
11437
- /** Email-based delivery (HA `notify.smtp`, etc.). */
11438
- DeviceRole["EmailNotifier"] = "email-notifier";
11439
- /** Fallback when the notifier service name doesn't match a known
11440
- * pattern. */
11441
- DeviceRole["GenericNotifier"] = "generic-notifier";
11442
- /** Emits a pulse train (infrared or RF) on request — the `ir-transmitter`
11443
- * cap. A `DeviceType.Generic` device. */
11444
- DeviceRole["IrTransmitter"] = "ir-transmitter";
11445
- return DeviceRole;
11446
- }({});
11447
- //#endregion
11448
- //#region ../types/dist/composition-C7WdThmP.mjs
11610
+ //#region ../types/dist/composition-BBzVf2xd.mjs
11449
11611
  /**
11450
11612
  * The ffmpeg build every CamStack node downloads, per platform and arch.
11451
11613
  *
@@ -12664,6 +12826,90 @@ var EventOwnerTypeSchema = _enum(EVENT_OWNER_TYPES);
12664
12826
  var MediaPresenceOwnerKindSchema = _enum([...EVENT_OWNER_TYPES, "track"]);
12665
12827
  new Set(EVENT_OWNER_TYPES);
12666
12828
  /**
12829
+ * What a published package needs from the rest of the install (D989).
12830
+ *
12831
+ * One published framework package (`@camstack/server`) carries the kernel, the
12832
+ * builtins and the capability contracts, so "which addon runs on which
12833
+ * framework" is a question of version, and the answer is DECLARED by the
12834
+ * package that knows it:
12835
+ *
12836
+ * - an addon says the oldest server it runs on (`requires.server: ">=X"`);
12837
+ * - the server says the oldest addon it works with
12838
+ * (`requires.addons: { "@camstack/addon-x": ">=Y" }`) and the oldest shell
12839
+ * around it (`requires.wrapper: { desktop, image }`);
12840
+ * - a wrapper (the desktop app, the container starter) says which server
12841
+ * range it can host, and reports its own kind and version.
12842
+ *
12843
+ * The publish workflow STAMPS these after the version bump
12844
+ * (`scripts/ci/stamp-requires.mjs`); nobody types a number by hand.
12845
+ *
12846
+ * A requirement that cannot be checked is NOT met: a shell that reports no
12847
+ * version counts as below the minimum, and an unreadable running version is
12848
+ * named in the result (a failed read is never a pass).
12849
+ */
12850
+ /** `>=MAJOR.MINOR.PATCH[-prerelease]` — the only range shape a requirement uses. */
12851
+ var MinVersionSchema = string().regex(/^>=\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/, "a minimum is written \">=X.Y.Z\"");
12852
+ object({
12853
+ /** The oldest `@camstack/server` this package runs on. */
12854
+ server: MinVersionSchema.optional(),
12855
+ /** The oldest installed version of each named addon package. */
12856
+ addons: record(string(), MinVersionSchema).optional(),
12857
+ /** The oldest shell around the server, per shell kind. */
12858
+ wrapper: object({
12859
+ desktop: MinVersionSchema.optional(),
12860
+ image: MinVersionSchema.optional()
12861
+ }).optional()
12862
+ });
12863
+ /** One unmet requirement; each names the versions on both sides. */
12864
+ var UnmetRequirementSchema = discriminatedUnion("kind", [
12865
+ object({
12866
+ kind: literal("server-too-old"),
12867
+ required: string(),
12868
+ /** `null` — the running server version could not be read. */
12869
+ running: string().nullable()
12870
+ }),
12871
+ object({
12872
+ kind: literal("addon-too-old"),
12873
+ packageName: string(),
12874
+ required: string(),
12875
+ /** `null` — the addon is not installed (or its version is unreadable). */
12876
+ running: string().nullable()
12877
+ }),
12878
+ object({
12879
+ kind: literal("wrapper-too-old"),
12880
+ wrapper: _enum(["desktop", "image"]),
12881
+ required: string(),
12882
+ /** `null` — the wrapper reported no version. */
12883
+ running: string().nullable()
12884
+ })
12885
+ ]);
12886
+ var RequiredAddonUpdateSchema = object({
12887
+ packageName: string(),
12888
+ /** The minimum the target server declares. */
12889
+ required: string(),
12890
+ /** What this node has; `null` when the addon is not installed. */
12891
+ installed: string().nullable()
12892
+ });
12893
+ /**
12894
+ * Which integration instance a settings form is for. An addon that serves several instances
12895
+ * (`instanceMode: multiple`) can show a different form per instance — an NVR integration
12896
+ * needs its connection, the default integration needs the addon's own sections.
12897
+ *
12898
+ * - `integrationId` is `null` in the create wizard: nothing exists yet, the operator is
12899
+ * setting up a NEW instance.
12900
+ * - `mode` is the wizard mode of the integration type (`getAvailableTypes`).
12901
+ * - `candidateStableId` is the discovery candidate a wizard was seeded from, if any.
12902
+ */
12903
+ var IntegrationInstanceRefSchema = object({
12904
+ integrationId: string().nullable(),
12905
+ mode: _enum([
12906
+ "standalone",
12907
+ "account",
12908
+ "broker"
12909
+ ]),
12910
+ candidateStableId: string().optional()
12911
+ });
12912
+ /**
12667
12913
  * Build an `IAddonRouteProvider` from a list of routes. Implements
12668
12914
  * both the operator-facing `getRoutes` (returning route descriptors
12669
12915
  * minus the handlers, which can't cross JSON) and the framework-
@@ -13236,6 +13482,613 @@ var CameraSwitchGroupSchema = object({
13236
13482
  fetchedAt: number()
13237
13483
  });
13238
13484
  /**
13485
+ * Query filter for settings-store collections.
13486
+ */
13487
+ var QueryFilterSchema = object({
13488
+ where: record(string(), unknown()).optional(),
13489
+ whereIn: record(string(), array(unknown())).optional(),
13490
+ whereBetween: record(string(), tuple([unknown(), unknown()])).optional(),
13491
+ /** NULL-safe exclusion: matches rows whose field is NULL OR != the value. */
13492
+ whereNot: record(string(), unknown()).optional(),
13493
+ /** NULL-safe exclusion of a SET — `whereNot` for more than one value. An
13494
+ * empty list excludes nothing. See `QueryFilter.whereNotIn`. */
13495
+ whereNotIn: record(string(), array(unknown())).optional(),
13496
+ orderBy: object({
13497
+ field: string(),
13498
+ direction: _enum(["asc", "desc"])
13499
+ }).optional(),
13500
+ limit: number().optional(),
13501
+ offset: number().optional()
13502
+ });
13503
+ /**
13504
+ * The predicate half of a filter, for BULK MUTATIONS.
13505
+ *
13506
+ * Deliberately not `QueryFilterSchema`: `orderBy` / `limit` / `offset` have no
13507
+ * meaning for a statement that rewrites a set, and accepting them would invite
13508
+ * a caller to believe `limit` bounds the damage. Every field is optional here
13509
+ * only so the shape stays composable — the implementation REJECTS a filter
13510
+ * that compiles to no predicate, because that is the whole collection.
13511
+ */
13512
+ var MutationFilterSchema = object({
13513
+ where: record(string(), unknown()).optional(),
13514
+ whereIn: record(string(), array(unknown())).optional(),
13515
+ whereBetween: record(string(), tuple([unknown(), unknown()])).optional(),
13516
+ whereNot: record(string(), unknown()).optional(),
13517
+ whereNotIn: record(string(), array(unknown())).optional()
13518
+ });
13519
+ /**
13520
+ * One scalar an {@link settingsStoreCapability.methods.aggregate} call asks for.
13521
+ *
13522
+ * `as` names the slot in the result, so the SAME column may be asked twice with
13523
+ * two operations (`MIN(startMs)` and `MAX(startMs)` in one round trip) — which
13524
+ * a `Record<column, op>` shape could not express.
13525
+ */
13526
+ var AggregateFieldSchema = object({
13527
+ /** Result key. */
13528
+ as: string().min(1),
13529
+ /** Column to aggregate. Must be a real column of a declared collection. */
13530
+ field: string().min(1),
13531
+ op: _enum([
13532
+ "sum",
13533
+ "min",
13534
+ "max"
13535
+ ])
13536
+ });
13537
+ /**
13538
+ * `COUNT(*)` plus one number per requested field.
13539
+ *
13540
+ * `null` means NO ROW MATCHED, never zero: a `SUM` over an empty set and a sum
13541
+ * that really is 0 are different facts, and an accounting caller that renders
13542
+ * "0 bytes, oldest = 0" for "nothing here" reports a lie about a disk.
13543
+ */
13544
+ var AggregateResultSchema = object({
13545
+ count: number().int(),
13546
+ values: record(string(), number().nullable())
13547
+ });
13548
+ /** A single stored record: `{ id, data }`. */
13549
+ var SettingsRecordSchema = object({
13550
+ id: string(),
13551
+ data: record(string(), unknown())
13552
+ });
13553
+ /**
13554
+ * One record of a BULK insert — {@link SettingsRecordSchema} with the id made
13555
+ * optional.
13556
+ *
13557
+ * A separate schema rather than loosening the shared one: every other method
13558
+ * on this cap addresses a row BY its id, and making that field optional
13559
+ * everywhere would turn a forgotten key into a silently generated one on
13560
+ * `update` and `delete` as well.
13561
+ */
13562
+ var BulkRecordSchema = object({
13563
+ id: string().optional(),
13564
+ data: record(string(), unknown())
13565
+ });
13566
+ /**
13567
+ * The database FILES one `sqlite-settings` engine process owns (D851).
13568
+ * Closed on purpose: a file is a thing the engine opens, backs up and gives a
13569
+ * serving thread, so a new one is a code change, never a string a caller
13570
+ * invents. `settings` is `camstack.db` and the default for every collection
13571
+ * that names no file.
13572
+ */
13573
+ var DataStoreDatabaseSchema = _enum([
13574
+ "settings",
13575
+ "events",
13576
+ "history"
13577
+ ]);
13578
+ DataStoreDatabaseSchema.options;
13579
+ /**
13580
+ * One write inside a {@link settingsStoreCapability.methods.transact} call.
13581
+ * The three that a "replace this set, bump that revision" write needs, plus
13582
+ * the batched upsert a rollup writer needs.
13583
+ */
13584
+ var TransactOpSchema = discriminatedUnion("op", [
13585
+ object({
13586
+ op: literal("deleteWhere"),
13587
+ collection: string(),
13588
+ filter: MutationFilterSchema
13589
+ }),
13590
+ object({
13591
+ op: literal("insertMany"),
13592
+ collection: string(),
13593
+ records: array(BulkRecordSchema).readonly()
13594
+ }),
13595
+ object({
13596
+ op: literal("update"),
13597
+ collection: string(),
13598
+ id: string(),
13599
+ data: record(string(), unknown())
13600
+ }),
13601
+ object({
13602
+ op: literal("upsertMany"),
13603
+ collection: string(),
13604
+ conflict: array(string()).readonly(),
13605
+ records: array(BulkRecordSchema).readonly()
13606
+ })
13607
+ ]);
13608
+ /**
13609
+ * The precondition of a transaction: the row `key` of `collection` must hold
13610
+ * `equals` in `column`, read INSIDE the transaction.
13611
+ */
13612
+ var TransactGuardSchema = object({
13613
+ collection: string(),
13614
+ key: string(),
13615
+ column: string(),
13616
+ equals: unknown()
13617
+ });
13618
+ /**
13619
+ * Column declaration for a structured (SQL-backed) collection.
13620
+ *
13621
+ * Logical types — the backend translates each to the matching SQLite
13622
+ * storage class and handles per-type marshaling:
13623
+ * - `TEXT` / `INTEGER` / `REAL` — native SQLite types, pass-through
13624
+ * - `JSON` — TEXT under the hood; serialised on write, parsed on read
13625
+ * - `BOOLEAN` — INTEGER 0/1 under the hood; coerced both directions
13626
+ */
13627
+ var CollectionColumnSchema = object({
13628
+ name: string(),
13629
+ type: _enum([
13630
+ "TEXT",
13631
+ "INTEGER",
13632
+ "REAL",
13633
+ "JSON",
13634
+ "BOOLEAN"
13635
+ ]),
13636
+ primaryKey: boolean().optional(),
13637
+ notNull: boolean().optional(),
13638
+ unique: boolean().optional(),
13639
+ /**
13640
+ * Column DEFAULT. Required for B2: the four `ensureTable` consumers declare
13641
+ * columns like `enabled INTEGER NOT NULL DEFAULT 1`, and without this the
13642
+ * collection surface simply cannot express their existing tables — which is
13643
+ * why they were still on the uncapped `table*` API.
13644
+ */
13645
+ defaultValue: union([
13646
+ string(),
13647
+ number(),
13648
+ boolean()
13649
+ ]).optional()
13650
+ });
13651
+ var CollectionIndexSchema = object({
13652
+ name: string(),
13653
+ columns: array(string()).readonly(),
13654
+ unique: boolean().optional()
13655
+ });
13656
+ /**
13657
+ * The input of `declareCollection` — ONE schema for the door and for the
13658
+ * engine behind it, so the two surfaces cannot drift (the dispatcher forwards
13659
+ * the payload untouched).
13660
+ *
13661
+ * - `database` — the file the collection lives in (D851). Absent means
13662
+ * `settings`. A declaration naming a file other than the one the table is
13663
+ * already in is REFUSED by the engine, never moved.
13664
+ * - `primaryKey` — a COMPOSITE key, as column names. A single name is the same
13665
+ * as `primaryKey: true` on that column. A composite-keyed collection has no
13666
+ * `id`: it is written with `insertMany` / `upsertMany` / `transact` and read
13667
+ * with `query` / `count` / `aggregate` / `deleteWhere`; the calls that
13668
+ * address one row by id are refused.
13669
+ * - `withoutRowid` — emit `WITHOUT ROWID`. Requires a primary key.
13670
+ */
13671
+ var DeclareCollectionInputSchema = object({
13672
+ namespace: string().optional(),
13673
+ collection: string(),
13674
+ columns: array(CollectionColumnSchema).readonly(),
13675
+ indexes: array(CollectionIndexSchema).readonly().optional(),
13676
+ database: DataStoreDatabaseSchema.optional(),
13677
+ primaryKey: array(string()).readonly().optional(),
13678
+ withoutRowid: boolean().optional()
13679
+ });
13680
+ method(object({
13681
+ namespace: string().optional(),
13682
+ collection: string(),
13683
+ key: string()
13684
+ }), unknown(), { auth: "admin" }), method(object({
13685
+ namespace: string().optional(),
13686
+ collection: string(),
13687
+ key: string(),
13688
+ value: unknown()
13689
+ }), _void(), {
13690
+ kind: "mutation",
13691
+ auth: "admin"
13692
+ }), method(object({
13693
+ namespace: string().optional(),
13694
+ collection: string(),
13695
+ filter: QueryFilterSchema.optional(),
13696
+ columns: array(string()).readonly().optional()
13697
+ }), array(SettingsRecordSchema).readonly(), { auth: "admin" }), method(object({
13698
+ namespace: string().optional(),
13699
+ collection: string(),
13700
+ /** Declared columns identifying the slot; must be covered by a UNIQUE
13701
+ * index on the collection. Never empty. */
13702
+ conflict: array(string()).readonly(),
13703
+ record: BulkRecordSchema
13704
+ }), object({
13705
+ /** The id the row HAS: assigned by SQLite on an insert, or the existing
13706
+ * row's own id on an update. */
13707
+ id: union([string(), number()]) }), {
13708
+ kind: "mutation",
13709
+ auth: "admin"
13710
+ }), method(object({
13711
+ namespace: string().optional(),
13712
+ collection: string(),
13713
+ conflict: array(string()).readonly(),
13714
+ records: array(BulkRecordSchema).readonly()
13715
+ }), object({ upserted: number().int() }), {
13716
+ kind: "mutation",
13717
+ auth: "admin"
13718
+ }), method(object({
13719
+ namespace: string().optional(),
13720
+ collection: string(),
13721
+ record: object({
13722
+ id: string().optional(),
13723
+ data: record(string(), unknown())
13724
+ })
13725
+ }), object({
13726
+ /**
13727
+ * The id the row ACTUALLY got (D473): the one supplied, the UUID minted
13728
+ * for an absent one, or the ROWID SQLite assigned on an `INTEGER`
13729
+ * primary key — which is the only place an auto key is knowable.
13730
+ */
13731
+ id: union([string(), number()]) }), {
13732
+ kind: "mutation",
13733
+ auth: "admin"
13734
+ }), method(object({
13735
+ namespace: string().optional(),
13736
+ collection: string(),
13737
+ records: array(BulkRecordSchema).readonly()
13738
+ }), object({ inserted: number().int() }), {
13739
+ kind: "mutation",
13740
+ auth: "admin"
13741
+ }), method(object({
13742
+ namespace: string().optional(),
13743
+ guard: TransactGuardSchema.optional(),
13744
+ ops: array(TransactOpSchema).readonly()
13745
+ }), object({
13746
+ applied: boolean(),
13747
+ current: unknown().optional()
13748
+ }), {
13749
+ kind: "mutation",
13750
+ auth: "admin"
13751
+ }), method(object({
13752
+ namespace: string().optional(),
13753
+ collection: string(),
13754
+ id: string(),
13755
+ data: record(string(), unknown())
13756
+ }), _void(), {
13757
+ kind: "mutation",
13758
+ auth: "admin"
13759
+ }), method(object({
13760
+ namespace: string().optional(),
13761
+ collection: string(),
13762
+ key: string()
13763
+ }), _void(), {
13764
+ kind: "mutation",
13765
+ auth: "admin"
13766
+ }), method(object({
13767
+ namespace: string().optional(),
13768
+ collection: string(),
13769
+ filter: MutationFilterSchema
13770
+ }), object({ deleted: number().int() }), {
13771
+ kind: "mutation",
13772
+ auth: "admin"
13773
+ }), method(object({
13774
+ namespace: string().optional(),
13775
+ collection: string(),
13776
+ filter: MutationFilterSchema,
13777
+ data: record(string(), unknown())
13778
+ }), object({ updated: number().int() }), {
13779
+ kind: "mutation",
13780
+ auth: "admin"
13781
+ }), method(object({
13782
+ namespace: string().optional(),
13783
+ collection: string(),
13784
+ filter: QueryFilterSchema.optional()
13785
+ }), number(), { auth: "admin" }), method(object({
13786
+ namespace: string().optional(),
13787
+ collection: string(),
13788
+ fields: array(AggregateFieldSchema).readonly(),
13789
+ filter: QueryFilterSchema.optional()
13790
+ }), AggregateResultSchema, { auth: "admin" }), method(object({
13791
+ namespace: string().optional(),
13792
+ collection: string(),
13793
+ field: string(),
13794
+ bucketSize: number().int().positive(),
13795
+ origin: number().int(),
13796
+ filter: QueryFilterSchema.optional()
13797
+ }), array(object({
13798
+ bucket: number().int(),
13799
+ count: number().int()
13800
+ })).readonly(), { auth: "admin" }), method(object({
13801
+ namespace: string().optional(),
13802
+ collection: string()
13803
+ }), boolean(), { auth: "admin" }), method(DeclareCollectionInputSchema, _void(), {
13804
+ kind: "mutation",
13805
+ auth: "admin"
13806
+ });
13807
+ 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>\"");
13808
+ var MigrationLevelSchema = _enum([
13809
+ "hub-store",
13810
+ "addon-state",
13811
+ "node-config",
13812
+ "viewer",
13813
+ "addon-internal"
13814
+ ]);
13815
+ var MigrationStoreSchema = _enum([
13816
+ "settings",
13817
+ "events",
13818
+ "history",
13819
+ "node-local",
13820
+ "addon-files"
13821
+ ]);
13822
+ var MigrationScopeSchema = _enum(["cluster", "per-node"]);
13823
+ /** What depends on a migration while it is pending: `block-owner` is the default. */
13824
+ var MigrationOnFailureSchema = _enum(["block-owner", "tolerate"]);
13825
+ /**
13826
+ * `sql` is a hub-only body over the owner's connection. `plan` returns edits the
13827
+ * engine applies inside the migration's own transaction (hub-owned: computed in
13828
+ * that transaction; addon-owned: computed by the owner and shipped to the engine).
13829
+ * `procedure` is addon-owned work that is not SQL (keychain, integration registry,
13830
+ * device tombstones); the ledger records its outcome and it must be idempotent.
13831
+ */
13832
+ var MigrationBodyKindSchema = _enum([
13833
+ "sql",
13834
+ "plan",
13835
+ "procedure"
13836
+ ]);
13837
+ var MigrationMetaSchema = object({
13838
+ id: MigrationIdSchema,
13839
+ /** `hub`, an addon id, `node` or `viewer`. */
13840
+ owner: string().regex(/^[a-z0-9-]+$/),
13841
+ level: MigrationLevelSchema,
13842
+ /** Strictly increasing per owner: the order within an owner. */
13843
+ seq: number().int().positive(),
13844
+ /** Ids of any owner: the order across owners. */
13845
+ dependsOn: array(MigrationIdSchema),
13846
+ store: MigrationStoreSchema,
13847
+ scope: MigrationScopeSchema,
13848
+ onFailure: MigrationOnFailureSchema,
13849
+ bodyKind: MigrationBodyKindSchema,
13850
+ description: string().min(1),
13851
+ /** The marked field the migration's spec must assert survived. */
13852
+ fixtureMark: string().min(1)
13853
+ }).refine((m) => m.id.startsWith(`${m.owner}/`), {
13854
+ message: "the id must start with \"<owner>/\"",
13855
+ path: ["id"]
13856
+ });
13857
+ /** A cycle in the dependency graph, named by the ids on it. */
13858
+ var MigrationCycleError = class extends Error {
13859
+ ids;
13860
+ constructor(ids) {
13861
+ super(`migration dependsOn cycle: ${ids.join(" -> ")}`);
13862
+ this.ids = ids;
13863
+ this.name = "MigrationCycleError";
13864
+ }
13865
+ };
13866
+ /**
13867
+ * Topological order over `dependsOn` plus the implicit edge "a lower `seq` of an
13868
+ * owner runs first". Ties are broken by `(owner, seq)`. Throws
13869
+ * {@link MigrationCycleError} on a cycle. A `dependsOn` id that is not in the list
13870
+ * is ignored here — the catalogue schema is what refuses it.
13871
+ */
13872
+ function orderMigrations(items) {
13873
+ const byId = new Map(items.map((item) => [item.id, item]));
13874
+ const deps = /* @__PURE__ */ new Map();
13875
+ const ownerSorted = /* @__PURE__ */ new Map();
13876
+ for (const item of items) {
13877
+ const list = ownerSorted.get(item.owner) ?? [];
13878
+ list.push(item);
13879
+ ownerSorted.set(item.owner, list);
13880
+ }
13881
+ for (const item of items) deps.set(item.id, new Set(item.dependsOn.filter((id) => byId.has(id))));
13882
+ for (const list of ownerSorted.values()) {
13883
+ list.sort((a, b) => a.seq - b.seq);
13884
+ for (let i = 1; i < list.length; i++) {
13885
+ const previous = list[i - 1];
13886
+ const current = list[i];
13887
+ if (previous !== void 0 && current !== void 0) deps.get(current.id)?.add(previous.id);
13888
+ }
13889
+ }
13890
+ const tie = (a, b) => a.owner < b.owner ? -1 : a.owner > b.owner ? 1 : a.seq - b.seq;
13891
+ const done = /* @__PURE__ */ new Set();
13892
+ const ordered = [];
13893
+ let remaining = [...items];
13894
+ while (remaining.length > 0) {
13895
+ const ready = remaining.filter((item) => [...deps.get(item.id) ?? []].every((dep) => done.has(dep)));
13896
+ if (ready.length === 0) throw new MigrationCycleError(remaining.map((item) => item.id).sort());
13897
+ ready.sort(tie);
13898
+ const next = ready[0];
13899
+ if (next === void 0) break;
13900
+ ordered.push(next);
13901
+ done.add(next.id);
13902
+ remaining = remaining.filter((item) => item.id !== next.id);
13903
+ }
13904
+ return ordered;
13905
+ }
13906
+ /** Every way a list of migrations (in declaration order) is not a catalogue. */
13907
+ function catalogueIssues(items) {
13908
+ const issues = [];
13909
+ const seen = /* @__PURE__ */ new Set();
13910
+ const known = new Set(items.map((item) => item.id));
13911
+ const lastSeq = /* @__PURE__ */ new Map();
13912
+ const seqUsed = /* @__PURE__ */ new Map();
13913
+ for (const item of items) {
13914
+ if (seen.has(item.id)) issues.push({
13915
+ code: "duplicate-id",
13916
+ ids: [item.id],
13917
+ message: `duplicate id ${item.id}`
13918
+ });
13919
+ seen.add(item.id);
13920
+ const used = seqUsed.get(item.owner) ?? /* @__PURE__ */ new Set();
13921
+ if (used.has(item.seq)) issues.push({
13922
+ code: "duplicate-seq",
13923
+ ids: [item.id],
13924
+ message: `owner ${item.owner} has two migrations with seq ${item.seq}`
13925
+ });
13926
+ else if (item.seq < (lastSeq.get(item.owner) ?? 0)) issues.push({
13927
+ code: "seq-order",
13928
+ ids: [item.id],
13929
+ message: `seq ${item.seq} of ${item.id} does not increase in declaration order (owner ${item.owner})`
13930
+ });
13931
+ used.add(item.seq);
13932
+ seqUsed.set(item.owner, used);
13933
+ lastSeq.set(item.owner, Math.max(item.seq, lastSeq.get(item.owner) ?? 0));
13934
+ for (const dep of item.dependsOn) if (!known.has(dep)) issues.push({
13935
+ code: "unknown-dependency",
13936
+ ids: [item.id, dep],
13937
+ message: `${item.id} dependsOn unknown id ${dep}`
13938
+ });
13939
+ }
13940
+ try {
13941
+ orderMigrations(items);
13942
+ } catch (err) {
13943
+ if (err instanceof MigrationCycleError) issues.push({
13944
+ code: "cycle",
13945
+ ids: err.ids,
13946
+ message: err.message
13947
+ });
13948
+ else throw err;
13949
+ }
13950
+ return issues;
13951
+ }
13952
+ array(MigrationMetaSchema).superRefine((items, ctx) => {
13953
+ for (const issue of catalogueIssues(items)) ctx.addIssue({
13954
+ code: "custom",
13955
+ message: `${issue.code}: ${issue.message}`
13956
+ });
13957
+ });
13958
+ /**
13959
+ * The canonical JSON (`preconditionOf`) of the value a plan read; the apply step
13960
+ * re-reads, and a plan whose read no longer matches is refused (the migration is
13961
+ * `failed`, nothing it edited is kept).
13962
+ */
13963
+ var PreconditionSchema = string().min(1).optional();
13964
+ var MigrationPlanSchema = object({ edits: array(discriminatedUnion("op", [
13965
+ object({
13966
+ op: literal("setKeys"),
13967
+ scope: string().min(1),
13968
+ values: record(string(), unknown()),
13969
+ precondition: PreconditionSchema
13970
+ }),
13971
+ object({
13972
+ op: literal("deleteKeys"),
13973
+ scope: string().min(1),
13974
+ keys: array(string().min(1)).min(1),
13975
+ precondition: PreconditionSchema
13976
+ }),
13977
+ object({
13978
+ op: literal("putRow"),
13979
+ table: string().min(1),
13980
+ key: record(string(), unknown()),
13981
+ row: record(string(), unknown()),
13982
+ precondition: PreconditionSchema
13983
+ }),
13984
+ object({
13985
+ op: literal("deleteRow"),
13986
+ table: string().min(1),
13987
+ key: record(string(), unknown()),
13988
+ precondition: PreconditionSchema
13989
+ }),
13990
+ object({
13991
+ op: literal("upsertRecord"),
13992
+ kind: string().min(1),
13993
+ recordKey: string().min(1),
13994
+ value: unknown(),
13995
+ precondition: PreconditionSchema
13996
+ }),
13997
+ object({
13998
+ op: literal("deleteRecord"),
13999
+ kind: string().min(1),
14000
+ recordKey: string().min(1),
14001
+ precondition: PreconditionSchema
14002
+ })
14003
+ ])) });
14004
+ /**
14005
+ * What a `procedure` body reports. `already-done` means the rerun observed the
14006
+ * work finished (a crash between the work and the ledger row, or a manual fix)
14007
+ * and did none of it again; the ledger records `applied` either way.
14008
+ */
14009
+ var MigrationProcedureResultSchema = object({
14010
+ outcome: _enum(["done", "already-done"]),
14011
+ summary: record(string(), unknown())
14012
+ });
14013
+ /** What an owner hands the engine when one of its migrations ran. */
14014
+ var MigrationOwnerResultSchema = discriminatedUnion("kind", [
14015
+ object({
14016
+ kind: literal("plan"),
14017
+ plan: MigrationPlanSchema
14018
+ }),
14019
+ object({
14020
+ kind: literal("procedure"),
14021
+ result: MigrationProcedureResultSchema
14022
+ }),
14023
+ object({
14024
+ kind: literal("failed"),
14025
+ error: string().min(1)
14026
+ })
14027
+ ]);
14028
+ /**
14029
+ * `held`: a restore of a pre-migration snapshot undid this id; it does not rerun
14030
+ * until the operator retries or a new code version ships.
14031
+ * `skipped-newer`: the database knows an id this code does not.
14032
+ */
14033
+ var MigrationStatusSchema = _enum([
14034
+ "applied",
14035
+ "failed",
14036
+ "blocked",
14037
+ "running",
14038
+ "held",
14039
+ "skipped-newer"
14040
+ ]);
14041
+ var MigrationLedgerEntrySchema = object({
14042
+ id: string(),
14043
+ database: DataStoreDatabaseSchema,
14044
+ owner: string(),
14045
+ level: MigrationLevelSchema,
14046
+ seq: number().int().nonnegative(),
14047
+ status: MigrationStatusSchema,
14048
+ attempts: number().int().nonnegative(),
14049
+ appliedAt: number().nullable(),
14050
+ codeVersion: string().nullable(),
14051
+ /** What the apply changed: rows per table, as the engine counted them. */
14052
+ planSummary: record(string(), unknown()).nullable(),
14053
+ progress: record(string(), unknown()).nullable(),
14054
+ error: string().nullable(),
14055
+ /** Why it is `blocked` or `held` (`dependency-failed`, `snapshot-refused`, …). */
14056
+ reason: string().nullable(),
14057
+ nodeId: string().nullable(),
14058
+ bootId: string().nullable()
14059
+ });
14060
+ /** A catalogued migration that is not applied, with where it stands. */
14061
+ var MigrationPendingEntrySchema = object({
14062
+ meta: MigrationMetaSchema,
14063
+ database: DataStoreDatabaseSchema,
14064
+ state: _enum([
14065
+ "pending",
14066
+ "failed",
14067
+ "blocked",
14068
+ "held"
14069
+ ]),
14070
+ reason: string().nullable()
14071
+ });
14072
+ /** Rows per table before and after a dry run, and what it would have changed. */
14073
+ var MigrationDryRunResultSchema = object({
14074
+ id: string(),
14075
+ ok: boolean(),
14076
+ error: string().nullable(),
14077
+ rowsChanged: number().int().nonnegative(),
14078
+ tables: array(object({
14079
+ table: string(),
14080
+ before: number().int().nonnegative(),
14081
+ after: number().int().nonnegative()
14082
+ }))
14083
+ });
14084
+ /** One of an owner's migrations, as the engine tells the owner to treat it this boot. */
14085
+ var MigrationOwnerStepSchema = object({
14086
+ id: MigrationIdSchema,
14087
+ action: _enum(["run", "skip"]),
14088
+ /** The ledger state the engine found: `pending` has no row yet. */
14089
+ state: union([literal("pending"), MigrationStatusSchema])
14090
+ });
14091
+ /**
13239
14092
  * The trainer contract — what the hub hands the cloud trainer and what it
13240
14093
  * gets back (cloud training, D805+).
13241
14094
  *
@@ -16638,6 +17491,41 @@ var ACTUATOR_CAP_VOCABULARY = {
16638
17491
  icon: "droplets"
16639
17492
  }]
16640
17493
  },
17494
+ "power-source": {
17495
+ scope: "device",
17496
+ noun: t("Power source", "Alimentazione"),
17497
+ actions: [
17498
+ {
17499
+ id: "powerSource.on",
17500
+ method: "powerOn",
17501
+ label: t("Power on", "Dai corrente"),
17502
+ phrase: t("Power on {device}", "Dai corrente a {device}"),
17503
+ icon: "power"
17504
+ },
17505
+ {
17506
+ id: "powerSource.off",
17507
+ method: "powerOff",
17508
+ label: t("Power off", "Togli corrente"),
17509
+ phrase: t("Cut the power of {device}", "Togli la corrente a {device}"),
17510
+ icon: "power-off"
17511
+ },
17512
+ {
17513
+ id: "powerSource.forceRestart",
17514
+ method: "forceRestart",
17515
+ label: t("Force restart", "Riavvio forzato"),
17516
+ phrase: t("Force restart {device} by cutting its power", "Riavvia {device} togliendo la corrente"),
17517
+ icon: "refresh-cw"
17518
+ }
17519
+ ],
17520
+ state: [{
17521
+ kind: "boolean",
17522
+ field: "switchOn",
17523
+ label: t("Power switch on", "Presa accesa"),
17524
+ whenTrue: t("When the power of {device} comes on", "Quando arriva la corrente a {device}"),
17525
+ whenFalse: t("When the power of {device} is cut", "Quando manca la corrente a {device}"),
17526
+ icon: "power"
17527
+ }]
17528
+ },
16641
17529
  "lock-control": {
16642
17530
  scope: "device",
16643
17531
  noun: t("Lock", "Serratura"),
@@ -16719,6 +17607,11 @@ var ACTUATOR_CAP_VOCABULARY = {
16719
17607
  value: "jammed",
16720
17608
  label: t("Jammed", "Inceppata"),
16721
17609
  phrase: t("When {device} jams", "Quando {device} si inceppa")
17610
+ },
17611
+ {
17612
+ value: "door-open",
17613
+ label: t("Door open", "Porta aperta"),
17614
+ phrase: t("When the door of {device} opens", "Quando la porta di {device} si apre")
16722
17615
  }
16723
17616
  ],
16724
17617
  icon: "lock"
@@ -17108,7 +18001,7 @@ var ACTUATOR_CAP_VOCABULARY = {
17108
18001
  method: "setDirection",
17109
18002
  label: t("Spin forward", "Rotazione avanti"),
17110
18003
  phrase: t("Spin {device} forward", "Fai girare {device} in avanti"),
17111
- icon: "rotate-cw",
18004
+ icon: "refresh-cw",
17112
18005
  effect: {
17113
18006
  field: "direction",
17114
18007
  value: "forward"
@@ -20186,6 +21079,7 @@ var SYSTEM_CAP_VOCABULARY = {
20186
21079
  "login-method": system(IDENTITY),
20187
21080
  "mesh-network": system(NETWORK),
20188
21081
  "metrics-provider": system(CLUSTER),
21082
+ migrations: system(INTERNAL),
20189
21083
  "model-convert": system(ML),
20190
21084
  "model-distributor": system(ML),
20191
21085
  "mqtt-broker": system(NETWORK),
@@ -42491,7 +43385,7 @@ var notificationOutputCapability = {
42491
43385
  * rebuilds without manual reload.
42492
43386
  *
42493
43387
  * The hub-local builtin `addon-pages-aggregator` (see
42494
- * `@camstack/system/builtins/addon-pages-aggregator`) registers the
43388
+ * `@camstack/server/system/builtins/addon-pages-aggregator`) registers the
42495
43389
  * provider. Splitting the public aggregator from the raw collection
42496
43390
  * keeps both ends in codegen — there's no hand-written
42497
43391
  * `addon-pages.router.ts` wrapper anymore.
@@ -42581,7 +43475,7 @@ var addonRoutesCapability = {
42581
43475
  */
42582
43476
  invoke: method(InvokeRequestSchema, InvokeReplyEnvelopeSchema, { kind: "mutation" })
42583
43477
  },
42584
- /** BIG PLAN 2: declarative mount hint — read by `@camstack/system` `buildCapRouters`. */
43478
+ /** BIG PLAN 2: declarative mount hint — read by `@camstack/server/system` `buildCapRouters`. */
42585
43479
  mount: { kind: "skip" }
42586
43480
  };
42587
43481
  var ConfigTabDeclarationSchema = object({
@@ -42636,6 +43530,10 @@ method(object({
42636
43530
  }), method(object({
42637
43531
  addonId: string(),
42638
43532
  nodeId: string().optional()
43533
+ }), SettingsSchemaWithValuesSchema.nullable()), method(object({
43534
+ addonId: string(),
43535
+ nodeId: string().optional(),
43536
+ instance: IntegrationInstanceRefSchema
42639
43537
  }), SettingsSchemaWithValuesSchema.nullable()), method(object({
42640
43538
  addonId: string(),
42641
43539
  nodeId: string().optional()
@@ -42806,7 +43704,7 @@ method(_void(), array(WidgetMetadataSchema).readonly(), { auth: "admin" });
42806
43704
  * manual reload — same scheme used by `addon-pages`.
42807
43705
  *
42808
43706
  * The hub-local builtin `addon-widgets-aggregator` (see
42809
- * `@camstack/system/builtins/addon-widgets-aggregator`) registers the
43707
+ * `@camstack/server/system/builtins/addon-widgets-aggregator`) registers the
42810
43708
  * provider. Splitting the public aggregator from the raw collection
42811
43709
  * keeps both ends in codegen — there's no hand-written wrapper.
42812
43710
  */
@@ -43167,329 +44065,6 @@ method(object({
43167
44065
  password: string()
43168
44066
  }), AuthResultSchema.nullable(), { kind: "mutation" }), method(object({ state: string() }), string()), method(record(string(), string()), AuthResultSchema, { kind: "mutation" }), method(object({ token: string() }), AuthResultSchema.nullable());
43169
44067
  /**
43170
- * Query filter for settings-store collections.
43171
- */
43172
- var QueryFilterSchema = object({
43173
- where: record(string(), unknown()).optional(),
43174
- whereIn: record(string(), array(unknown())).optional(),
43175
- whereBetween: record(string(), tuple([unknown(), unknown()])).optional(),
43176
- /** NULL-safe exclusion: matches rows whose field is NULL OR != the value. */
43177
- whereNot: record(string(), unknown()).optional(),
43178
- /** NULL-safe exclusion of a SET — `whereNot` for more than one value. An
43179
- * empty list excludes nothing. See `QueryFilter.whereNotIn`. */
43180
- whereNotIn: record(string(), array(unknown())).optional(),
43181
- orderBy: object({
43182
- field: string(),
43183
- direction: _enum(["asc", "desc"])
43184
- }).optional(),
43185
- limit: number().optional(),
43186
- offset: number().optional()
43187
- });
43188
- /**
43189
- * The predicate half of a filter, for BULK MUTATIONS.
43190
- *
43191
- * Deliberately not `QueryFilterSchema`: `orderBy` / `limit` / `offset` have no
43192
- * meaning for a statement that rewrites a set, and accepting them would invite
43193
- * a caller to believe `limit` bounds the damage. Every field is optional here
43194
- * only so the shape stays composable — the implementation REJECTS a filter
43195
- * that compiles to no predicate, because that is the whole collection.
43196
- */
43197
- var MutationFilterSchema = object({
43198
- where: record(string(), unknown()).optional(),
43199
- whereIn: record(string(), array(unknown())).optional(),
43200
- whereBetween: record(string(), tuple([unknown(), unknown()])).optional(),
43201
- whereNot: record(string(), unknown()).optional(),
43202
- whereNotIn: record(string(), array(unknown())).optional()
43203
- });
43204
- /**
43205
- * One scalar an {@link settingsStoreCapability.methods.aggregate} call asks for.
43206
- *
43207
- * `as` names the slot in the result, so the SAME column may be asked twice with
43208
- * two operations (`MIN(startMs)` and `MAX(startMs)` in one round trip) — which
43209
- * a `Record<column, op>` shape could not express.
43210
- */
43211
- var AggregateFieldSchema = object({
43212
- /** Result key. */
43213
- as: string().min(1),
43214
- /** Column to aggregate. Must be a real column of a declared collection. */
43215
- field: string().min(1),
43216
- op: _enum([
43217
- "sum",
43218
- "min",
43219
- "max"
43220
- ])
43221
- });
43222
- /**
43223
- * `COUNT(*)` plus one number per requested field.
43224
- *
43225
- * `null` means NO ROW MATCHED, never zero: a `SUM` over an empty set and a sum
43226
- * that really is 0 are different facts, and an accounting caller that renders
43227
- * "0 bytes, oldest = 0" for "nothing here" reports a lie about a disk.
43228
- */
43229
- var AggregateResultSchema = object({
43230
- count: number().int(),
43231
- values: record(string(), number().nullable())
43232
- });
43233
- /** A single stored record: `{ id, data }`. */
43234
- var SettingsRecordSchema = object({
43235
- id: string(),
43236
- data: record(string(), unknown())
43237
- });
43238
- /**
43239
- * One record of a BULK insert — {@link SettingsRecordSchema} with the id made
43240
- * optional.
43241
- *
43242
- * A separate schema rather than loosening the shared one: every other method
43243
- * on this cap addresses a row BY its id, and making that field optional
43244
- * everywhere would turn a forgotten key into a silently generated one on
43245
- * `update` and `delete` as well.
43246
- */
43247
- var BulkRecordSchema = object({
43248
- id: string().optional(),
43249
- data: record(string(), unknown())
43250
- });
43251
- /**
43252
- * The database FILES one `sqlite-settings` engine process owns (D851).
43253
- * Closed on purpose: a file is a thing the engine opens, backs up and gives a
43254
- * serving thread, so a new one is a code change, never a string a caller
43255
- * invents. `settings` is `camstack.db` and the default for every collection
43256
- * that names no file.
43257
- */
43258
- var DataStoreDatabaseSchema = _enum([
43259
- "settings",
43260
- "events",
43261
- "history"
43262
- ]);
43263
- DataStoreDatabaseSchema.options;
43264
- /**
43265
- * One write inside a {@link settingsStoreCapability.methods.transact} call.
43266
- * The three that a "replace this set, bump that revision" write needs, plus
43267
- * the batched upsert a rollup writer needs.
43268
- */
43269
- var TransactOpSchema = discriminatedUnion("op", [
43270
- object({
43271
- op: literal("deleteWhere"),
43272
- collection: string(),
43273
- filter: MutationFilterSchema
43274
- }),
43275
- object({
43276
- op: literal("insertMany"),
43277
- collection: string(),
43278
- records: array(BulkRecordSchema).readonly()
43279
- }),
43280
- object({
43281
- op: literal("update"),
43282
- collection: string(),
43283
- id: string(),
43284
- data: record(string(), unknown())
43285
- }),
43286
- object({
43287
- op: literal("upsertMany"),
43288
- collection: string(),
43289
- conflict: array(string()).readonly(),
43290
- records: array(BulkRecordSchema).readonly()
43291
- })
43292
- ]);
43293
- /**
43294
- * The precondition of a transaction: the row `key` of `collection` must hold
43295
- * `equals` in `column`, read INSIDE the transaction.
43296
- */
43297
- var TransactGuardSchema = object({
43298
- collection: string(),
43299
- key: string(),
43300
- column: string(),
43301
- equals: unknown()
43302
- });
43303
- /**
43304
- * Column declaration for a structured (SQL-backed) collection.
43305
- *
43306
- * Logical types — the backend translates each to the matching SQLite
43307
- * storage class and handles per-type marshaling:
43308
- * - `TEXT` / `INTEGER` / `REAL` — native SQLite types, pass-through
43309
- * - `JSON` — TEXT under the hood; serialised on write, parsed on read
43310
- * - `BOOLEAN` — INTEGER 0/1 under the hood; coerced both directions
43311
- */
43312
- var CollectionColumnSchema = object({
43313
- name: string(),
43314
- type: _enum([
43315
- "TEXT",
43316
- "INTEGER",
43317
- "REAL",
43318
- "JSON",
43319
- "BOOLEAN"
43320
- ]),
43321
- primaryKey: boolean().optional(),
43322
- notNull: boolean().optional(),
43323
- unique: boolean().optional(),
43324
- /**
43325
- * Column DEFAULT. Required for B2: the four `ensureTable` consumers declare
43326
- * columns like `enabled INTEGER NOT NULL DEFAULT 1`, and without this the
43327
- * collection surface simply cannot express their existing tables — which is
43328
- * why they were still on the uncapped `table*` API.
43329
- */
43330
- defaultValue: union([
43331
- string(),
43332
- number(),
43333
- boolean()
43334
- ]).optional()
43335
- });
43336
- var CollectionIndexSchema = object({
43337
- name: string(),
43338
- columns: array(string()).readonly(),
43339
- unique: boolean().optional()
43340
- });
43341
- /**
43342
- * The input of `declareCollection` — ONE schema for the door and for the
43343
- * engine behind it, so the two surfaces cannot drift (the dispatcher forwards
43344
- * the payload untouched).
43345
- *
43346
- * - `database` — the file the collection lives in (D851). Absent means
43347
- * `settings`. A declaration naming a file other than the one the table is
43348
- * already in is REFUSED by the engine, never moved.
43349
- * - `primaryKey` — a COMPOSITE key, as column names. A single name is the same
43350
- * as `primaryKey: true` on that column. A composite-keyed collection has no
43351
- * `id`: it is written with `insertMany` / `upsertMany` / `transact` and read
43352
- * with `query` / `count` / `aggregate` / `deleteWhere`; the calls that
43353
- * address one row by id are refused.
43354
- * - `withoutRowid` — emit `WITHOUT ROWID`. Requires a primary key.
43355
- */
43356
- var DeclareCollectionInputSchema = object({
43357
- namespace: string().optional(),
43358
- collection: string(),
43359
- columns: array(CollectionColumnSchema).readonly(),
43360
- indexes: array(CollectionIndexSchema).readonly().optional(),
43361
- database: DataStoreDatabaseSchema.optional(),
43362
- primaryKey: array(string()).readonly().optional(),
43363
- withoutRowid: boolean().optional()
43364
- });
43365
- method(object({
43366
- namespace: string().optional(),
43367
- collection: string(),
43368
- key: string()
43369
- }), unknown(), { auth: "admin" }), method(object({
43370
- namespace: string().optional(),
43371
- collection: string(),
43372
- key: string(),
43373
- value: unknown()
43374
- }), _void(), {
43375
- kind: "mutation",
43376
- auth: "admin"
43377
- }), method(object({
43378
- namespace: string().optional(),
43379
- collection: string(),
43380
- filter: QueryFilterSchema.optional(),
43381
- columns: array(string()).readonly().optional()
43382
- }), array(SettingsRecordSchema).readonly(), { auth: "admin" }), method(object({
43383
- namespace: string().optional(),
43384
- collection: string(),
43385
- /** Declared columns identifying the slot; must be covered by a UNIQUE
43386
- * index on the collection. Never empty. */
43387
- conflict: array(string()).readonly(),
43388
- record: BulkRecordSchema
43389
- }), object({
43390
- /** The id the row HAS: assigned by SQLite on an insert, or the existing
43391
- * row's own id on an update. */
43392
- id: union([string(), number()]) }), {
43393
- kind: "mutation",
43394
- auth: "admin"
43395
- }), method(object({
43396
- namespace: string().optional(),
43397
- collection: string(),
43398
- conflict: array(string()).readonly(),
43399
- records: array(BulkRecordSchema).readonly()
43400
- }), object({ upserted: number().int() }), {
43401
- kind: "mutation",
43402
- auth: "admin"
43403
- }), method(object({
43404
- namespace: string().optional(),
43405
- collection: string(),
43406
- record: object({
43407
- id: string().optional(),
43408
- data: record(string(), unknown())
43409
- })
43410
- }), object({
43411
- /**
43412
- * The id the row ACTUALLY got (D473): the one supplied, the UUID minted
43413
- * for an absent one, or the ROWID SQLite assigned on an `INTEGER`
43414
- * primary key — which is the only place an auto key is knowable.
43415
- */
43416
- id: union([string(), number()]) }), {
43417
- kind: "mutation",
43418
- auth: "admin"
43419
- }), method(object({
43420
- namespace: string().optional(),
43421
- collection: string(),
43422
- records: array(BulkRecordSchema).readonly()
43423
- }), object({ inserted: number().int() }), {
43424
- kind: "mutation",
43425
- auth: "admin"
43426
- }), method(object({
43427
- namespace: string().optional(),
43428
- guard: TransactGuardSchema.optional(),
43429
- ops: array(TransactOpSchema).readonly()
43430
- }), object({
43431
- applied: boolean(),
43432
- current: unknown().optional()
43433
- }), {
43434
- kind: "mutation",
43435
- auth: "admin"
43436
- }), method(object({
43437
- namespace: string().optional(),
43438
- collection: string(),
43439
- id: string(),
43440
- data: record(string(), unknown())
43441
- }), _void(), {
43442
- kind: "mutation",
43443
- auth: "admin"
43444
- }), method(object({
43445
- namespace: string().optional(),
43446
- collection: string(),
43447
- key: string()
43448
- }), _void(), {
43449
- kind: "mutation",
43450
- auth: "admin"
43451
- }), method(object({
43452
- namespace: string().optional(),
43453
- collection: string(),
43454
- filter: MutationFilterSchema
43455
- }), object({ deleted: number().int() }), {
43456
- kind: "mutation",
43457
- auth: "admin"
43458
- }), method(object({
43459
- namespace: string().optional(),
43460
- collection: string(),
43461
- filter: MutationFilterSchema,
43462
- data: record(string(), unknown())
43463
- }), object({ updated: number().int() }), {
43464
- kind: "mutation",
43465
- auth: "admin"
43466
- }), method(object({
43467
- namespace: string().optional(),
43468
- collection: string(),
43469
- filter: QueryFilterSchema.optional()
43470
- }), number(), { auth: "admin" }), method(object({
43471
- namespace: string().optional(),
43472
- collection: string(),
43473
- fields: array(AggregateFieldSchema).readonly(),
43474
- filter: QueryFilterSchema.optional()
43475
- }), AggregateResultSchema, { auth: "admin" }), method(object({
43476
- namespace: string().optional(),
43477
- collection: string(),
43478
- field: string(),
43479
- bucketSize: number().int().positive(),
43480
- origin: number().int(),
43481
- filter: QueryFilterSchema.optional()
43482
- }), array(object({
43483
- bucket: number().int(),
43484
- count: number().int()
43485
- })).readonly(), { auth: "admin" }), method(object({
43486
- namespace: string().optional(),
43487
- collection: string()
43488
- }), boolean(), { auth: "admin" }), method(DeclareCollectionInputSchema, _void(), {
43489
- kind: "mutation",
43490
- auth: "admin"
43491
- });
43492
- /**
43493
44068
  * How long one snapshot may take end to end: the pinned copy of the whole
43494
44069
  * database plus `PRAGMA integrity_check` over the copy, on the hub's slowest
43495
44070
  * disk. ~800 MB at a pessimistic 10 MB/s is under 3 minutes; the bound is
@@ -45297,6 +45872,7 @@ method(object({
45297
45872
  kind: "mutation",
45298
45873
  auth: "admin"
45299
45874
  }), method(object({ blockIds: array(string()) }), object({ states: array(CompositionBlockStateSchema) }), { auth: "admin" });
45875
+ 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;
45300
45876
  var NUMBER = {
45301
45877
  kind: "fixed",
45302
45878
  result: ["number"]
@@ -46307,6 +46883,57 @@ id: union([string(), number()]) }), {
46307
46883
  kind: "mutation",
46308
46884
  auth: "admin"
46309
46885
  });
46886
+ 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({
46887
+ id: string().min(1),
46888
+ error: string().min(1)
46889
+ }), MigrationLedgerEntrySchema, {
46890
+ kind: "mutation",
46891
+ auth: "admin"
46892
+ }), method(object({ id: string().min(1) }), object({ entries: array(MigrationLedgerEntrySchema) }), {
46893
+ kind: "mutation",
46894
+ auth: "admin"
46895
+ }), method(object({
46896
+ database: DataStoreDatabaseSchema,
46897
+ snapshotId: string().min(1)
46898
+ }), object({
46899
+ scheduled: boolean(),
46900
+ held: array(string()),
46901
+ restartRequired: boolean()
46902
+ }), {
46903
+ kind: "mutation",
46904
+ auth: "admin"
46905
+ }), method(object({
46906
+ owner: string().min(1),
46907
+ metas: array(MigrationMetaSchema)
46908
+ }), object({
46909
+ bootId: string(),
46910
+ snapshot: _enum([
46911
+ "none",
46912
+ "written",
46913
+ "refused"
46914
+ ]),
46915
+ steps: array(MigrationOwnerStepSchema)
46916
+ }), {
46917
+ kind: "mutation",
46918
+ auth: "admin"
46919
+ }), method(object({ id: string().min(1) }), object({
46920
+ proceed: boolean(),
46921
+ state: union([literal("pending"), MigrationStatusSchema]),
46922
+ reason: string().nullable()
46923
+ }), {
46924
+ kind: "mutation",
46925
+ auth: "admin"
46926
+ }), method(object({
46927
+ id: string().min(1),
46928
+ result: MigrationOwnerResultSchema
46929
+ }), object({
46930
+ status: MigrationStatusSchema,
46931
+ error: string().nullable(),
46932
+ rowsChanged: number().int().nonnegative()
46933
+ }), {
46934
+ kind: "mutation",
46935
+ auth: "admin"
46936
+ });
46310
46937
  /**
46311
46938
  * records - an addon's own facts and documents (D881): ledgers, audit rows,
46312
46939
  * config documents, mirrors. The third storage API next to `ctx.settings`
@@ -60809,7 +61436,18 @@ var ServerUpdateActionResultSchema = object({
60809
61436
  * store (D877), present when the pre-flight refused the update
60810
61437
  * (`reason: 'addon-would-refuse'`). Optional for version skew.
60811
61438
  */
60812
- blockers: array(UpdateBlockerSchema).optional()
61439
+ blockers: array(UpdateBlockerSchema).optional(),
61440
+ /**
61441
+ * The minimums the target server declares and this node does not meet
61442
+ * (D989), present when the update was refused with
61443
+ * `reason: 'requirements-unmet'`. Each names the versions on both sides.
61444
+ */
61445
+ unmet: array(UnmetRequirementSchema).optional(),
61446
+ /**
61447
+ * The addons this node has below the minimum the staged server declares —
61448
+ * the ones to update WITH the server. Present on an accepted update.
61449
+ */
61450
+ requiredAddonUpdates: array(RequiredAddonUpdateSchema).optional()
60813
61451
  });
60814
61452
  var LaunchOverridesSchema = object({
60815
61453
  /** The persisted heap ceiling in MB, or `null` when none is set. */
@@ -63320,7 +63958,9 @@ var PackageUpdateSchema = object({
63320
63958
  latestVersion: string(),
63321
63959
  category: _enum(["addon", "core"]),
63322
63960
  requiresRestart: boolean(),
63323
- isSystem: boolean()
63961
+ isSystem: boolean(),
63962
+ /** Held: what the newer version needs and this node does not meet (D989). */
63963
+ blockedBy: array(UnmetRequirementSchema).readonly().optional()
63324
63964
  });
63325
63965
  var PackageVersionInfoSchema = object({
63326
63966
  version: string(),
@@ -64364,19 +65004,19 @@ var RemoteSetConfigResultSchema = discriminatedUnion("status", [object({ status:
64364
65004
  reason: _enum(["unknown-template", "not-a-media-player"]),
64365
65005
  detail: string()
64366
65006
  })]);
64367
- var deviceInput = object({ deviceId: number().int().nonnegative() });
65007
+ var deviceInput$1 = object({ deviceId: number().int().nonnegative() });
64368
65008
  DeviceType.MediaPlayer;
64369
- method(deviceInput, RemoteConfigSchema, { auth: "admin" }), method(deviceInput.extend({ config: RemoteConfigSchema }), RemoteSetConfigResultSchema, {
65009
+ method(deviceInput$1, RemoteConfigSchema, { auth: "admin" }), method(deviceInput$1.extend({ config: RemoteConfigSchema }), RemoteSetConfigResultSchema, {
64370
65010
  kind: "mutation",
64371
65011
  auth: "admin"
64372
- }), method(deviceInput, RemoteCommandListSchema, { auth: "protected" }), method(deviceInput.extend({
65012
+ }), method(deviceInput$1, RemoteCommandListSchema, { auth: "protected" }), method(deviceInput$1.extend({
64373
65013
  role: RemoteRoleSchema,
64374
65014
  /** Sends in a row (volume steps). */
64375
65015
  repeat: number().int().min(1).max(50).default(1)
64376
65016
  }), RemoteSendResultSchema, {
64377
65017
  kind: "mutation",
64378
65018
  auth: "protected"
64379
- }), method(deviceInput.extend({
65019
+ }), method(deviceInput$1.extend({
64380
65020
  command: RemoteMediaCommandSchema,
64381
65021
  muted: boolean().optional(),
64382
65022
  source: string().optional(),
@@ -66526,6 +67166,8 @@ var IntegrationWithStateSchema = object({
66526
67166
  name: string(),
66527
67167
  enabled: boolean(),
66528
67168
  info: record(string(), unknown()).readonly(),
67169
+ /** The discovery candidate this integration was set up from (D984). */
67170
+ originStableId: string().optional(),
66529
67171
  createdAt: number(),
66530
67172
  updatedAt: number(),
66531
67173
  processState: string()
@@ -66536,6 +67178,8 @@ var IntegrationLiteSchema = object({
66536
67178
  name: string(),
66537
67179
  enabled: boolean(),
66538
67180
  info: record(string(), unknown()).readonly(),
67181
+ /** The discovery candidate this integration was set up from (D984). */
67182
+ originStableId: string().optional(),
66539
67183
  createdAt: number(),
66540
67184
  updatedAt: number()
66541
67185
  });
@@ -66633,6 +67277,8 @@ var CreateIntegrationInputSchema = object({
66633
67277
  name: string(),
66634
67278
  enabled: boolean().optional(),
66635
67279
  info: record(string(), unknown()).optional(),
67280
+ /** `stableId` of the discovery candidate the form was seeded from (D984). */
67281
+ originStableId: string().optional(),
66636
67282
  settings: record(string(), unknown()).optional(),
66637
67283
  /** Keys of `settings` the form's schema declares secret (by type) — kept in
66638
67284
  * the keychain, not the table (D787). */
@@ -66853,7 +67499,7 @@ DeviceType.LawnMower, method(object({ deviceId: number().int().nonnegative() }),
66853
67499
  * clients — they reverse-connect to the hub. Exposing their interfaces
66854
67500
  * via the same surface would leak internal topology with no upside.
66855
67501
  *
66856
- * Implementation in `@camstack/system/builtins/local-network/`.
67502
+ * Implementation in `@camstack/server/system/builtins/local-network/`.
66857
67503
  */
66858
67504
  /** Coarse classification derived from the interface name + IP range. */
66859
67505
  var InterfaceKindEnum = _enum([
@@ -67250,11 +67896,13 @@ object({
67250
67896
  "unlocked",
67251
67897
  "locking",
67252
67898
  "unlocking",
67253
- "jammed"
67899
+ "jammed",
67900
+ "door-open"
67254
67901
  ]),
67255
67902
  /** Ms epoch when the slice was last updated. */
67256
67903
  lastChangedAt: number()
67257
67904
  });
67905
+ DeviceType.Lock;
67258
67906
  DeviceType.Lock, method(object({
67259
67907
  deviceId: number().int().nonnegative(),
67260
67908
  /** Optional PIN code required by some keypad locks. NOT
@@ -67280,6 +67928,49 @@ DeviceType.Lock, method(object({
67280
67928
  kind: "mutation",
67281
67929
  auth: "protected"
67282
67930
  });
67931
+ var PowerSourceRefusalReasonSchema = _enum([
67932
+ "no-power-source",
67933
+ "switch-missing",
67934
+ "switch-offline",
67935
+ "switch-unknown",
67936
+ "restart-in-progress",
67937
+ "switch-call-failed"
67938
+ ]);
67939
+ var PowerSourceActionResultSchema = discriminatedUnion("accepted", [object({ accepted: literal(true) }), object({
67940
+ accepted: literal(false),
67941
+ reason: PowerSourceRefusalReasonSchema,
67942
+ /** One line for the operator, naming the switch. */
67943
+ message: string()
67944
+ })]);
67945
+ var PowerSourceInfoSchema = object({
67946
+ /** False when the device has no power source linked; the rest is then empty. */
67947
+ linked: boolean(),
67948
+ switchDeviceId: number().nullable(),
67949
+ switchName: string().nullable(),
67950
+ /** The switch's on/off state, or null when it is unknown. Never false for unknown (D393). */
67951
+ switchOn: boolean().nullable(),
67952
+ switchOnline: boolean().nullable(),
67953
+ restartInProgress: boolean(),
67954
+ offSeconds: number().int()
67955
+ });
67956
+ object({
67957
+ /** Null while the switch has not reported: unknown is never `false` (D393). */
67958
+ switchOn: boolean().nullable(),
67959
+ offSeconds: number().int(),
67960
+ lastFetchedAt: number()
67961
+ });
67962
+ 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;
67963
+ var deviceInput = object({ deviceId: number().int().nonnegative() });
67964
+ systemMethod(deviceInput, PowerSourceInfoSchema, { auth: "admin" }), systemMethod(deviceInput, PowerSourceActionResultSchema, {
67965
+ kind: "mutation",
67966
+ auth: "admin"
67967
+ }), systemMethod(deviceInput, PowerSourceActionResultSchema, {
67968
+ kind: "mutation",
67969
+ auth: "admin"
67970
+ }), systemMethod(deviceInput, PowerSourceActionResultSchema, {
67971
+ kind: "mutation",
67972
+ auth: "admin"
67973
+ });
67283
67974
  object({
67284
67975
  value: number().nullable().meta(readingMeta("device", { label: "Total" })),
67285
67976
  lastFetchedAt: number()
@@ -68088,7 +68779,7 @@ DeviceType.Camera, DeviceType.Sensor, DeviceType.Button, DeviceType.Switch, Devi
68088
68779
  * jitter, and observed/peak bandwidth per device + per client.
68089
68780
  *
68090
68781
  * Implementation lives in the server's `NetworkQualityService` (thin
68091
- * wrapper over the shared `NetworkQualityTracker` from `@camstack/system`).
68782
+ * wrapper over the shared `NetworkQualityTracker` from `@camstack/server/system`).
68092
68783
  * The provider is registered from `trpc.router.ts` against the existing
68093
68784
  * service instance — no addon owns this state.
68094
68785
  *
@@ -73644,7 +74335,6 @@ discriminatedUnion("kind", [object({
73644
74335
  })]);
73645
74336
  DeviceFeature.FanSpeed, DeviceFeature.FanPreset, DeviceFeature.FanDirection, DeviceFeature.FanOscillating, DeviceFeature.CoverPositionable, DeviceFeature.CoverTilt, DeviceFeature.ValvePositionable, DeviceFeature.MediaPlayerVolume, DeviceFeature.ClimateFanMode;
73646
74337
  SOURCE_CAPS.filter((cap) => cap !== "switch" && cap !== "motion");
73647
- 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.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;
73648
74338
  String.raw`rose\(active(\d+), changed(\d+), (\d+(?:\.\d+)?)\)`;
73649
74339
  DeviceRole.BinarySensor, DeviceRole.MotionSensor;
73650
74340
  new Set(CAP_NAMES);
@@ -74106,6 +74796,13 @@ Object.freeze({
74106
74796
  access: "view",
74107
74797
  role: "admin"
74108
74798
  },
74799
+ "addonSettings.getIntegrationFormSchema": {
74800
+ capName: "addon-settings",
74801
+ capScope: "system",
74802
+ addonId: null,
74803
+ access: "view",
74804
+ role: "admin"
74805
+ },
74109
74806
  "addonSettings.getIntegrationSettings": {
74110
74807
  capName: "addon-settings",
74111
74808
  capScope: "system",
@@ -78194,6 +78891,69 @@ Object.freeze({
78194
78891
  access: "view",
78195
78892
  role: "admin"
78196
78893
  },
78894
+ "migrations.declareOwner": {
78895
+ capName: "migrations",
78896
+ capScope: "system",
78897
+ addonId: null,
78898
+ access: "create",
78899
+ role: "admin"
78900
+ },
78901
+ "migrations.dryRun": {
78902
+ capName: "migrations",
78903
+ capScope: "system",
78904
+ addonId: null,
78905
+ access: "view",
78906
+ role: "admin"
78907
+ },
78908
+ "migrations.finishMigration": {
78909
+ capName: "migrations",
78910
+ capScope: "system",
78911
+ addonId: null,
78912
+ access: "create",
78913
+ role: "admin"
78914
+ },
78915
+ "migrations.listLedger": {
78916
+ capName: "migrations",
78917
+ capScope: "system",
78918
+ addonId: null,
78919
+ access: "view",
78920
+ role: "admin"
78921
+ },
78922
+ "migrations.listPending": {
78923
+ capName: "migrations",
78924
+ capScope: "system",
78925
+ addonId: null,
78926
+ access: "view",
78927
+ role: "admin"
78928
+ },
78929
+ "migrations.markFailed": {
78930
+ capName: "migrations",
78931
+ capScope: "system",
78932
+ addonId: null,
78933
+ access: "create",
78934
+ role: "admin"
78935
+ },
78936
+ "migrations.restorePreMigration": {
78937
+ capName: "migrations",
78938
+ capScope: "system",
78939
+ addonId: null,
78940
+ access: "create",
78941
+ role: "admin"
78942
+ },
78943
+ "migrations.retry": {
78944
+ capName: "migrations",
78945
+ capScope: "system",
78946
+ addonId: null,
78947
+ access: "create",
78948
+ role: "admin"
78949
+ },
78950
+ "migrations.startMigration": {
78951
+ capName: "migrations",
78952
+ capScope: "system",
78953
+ addonId: null,
78954
+ access: "create",
78955
+ role: "admin"
78956
+ },
78197
78957
  "modelConvert.convert": {
78198
78958
  capName: "model-convert",
78199
78959
  capScope: "system",
@@ -80826,6 +81586,34 @@ Object.freeze({
80826
81586
  access: "view",
80827
81587
  role: "viewer"
80828
81588
  },
81589
+ "powerSource.forceRestart": {
81590
+ capName: "power-source",
81591
+ capScope: "device",
81592
+ addonId: null,
81593
+ access: "create",
81594
+ role: "admin"
81595
+ },
81596
+ "powerSource.getStatus": {
81597
+ capName: "power-source",
81598
+ capScope: "device",
81599
+ addonId: null,
81600
+ access: "view",
81601
+ role: "admin"
81602
+ },
81603
+ "powerSource.powerOff": {
81604
+ capName: "power-source",
81605
+ capScope: "device",
81606
+ addonId: null,
81607
+ access: "create",
81608
+ role: "admin"
81609
+ },
81610
+ "powerSource.powerOn": {
81611
+ capName: "power-source",
81612
+ capScope: "device",
81613
+ addonId: null,
81614
+ access: "create",
81615
+ role: "admin"
81616
+ },
80829
81617
  "presence.getStatus": {
80830
81618
  capName: "presence",
80831
81619
  capScope: "device",
@@ -85974,6 +86762,26 @@ Object.freeze({
85974
86762
  form: "single",
85975
86763
  optional: false
85976
86764
  }],
86765
+ "powerSource.forceRestart": [{
86766
+ name: "deviceId",
86767
+ form: "single",
86768
+ optional: false
86769
+ }],
86770
+ "powerSource.getStatus": [{
86771
+ name: "deviceId",
86772
+ form: "single",
86773
+ optional: false
86774
+ }],
86775
+ "powerSource.powerOff": [{
86776
+ name: "deviceId",
86777
+ form: "single",
86778
+ optional: false
86779
+ }],
86780
+ "powerSource.powerOn": [{
86781
+ name: "deviceId",
86782
+ form: "single",
86783
+ optional: false
86784
+ }],
85977
86785
  "presence.getStatus": [{
85978
86786
  name: "deviceId",
85979
86787
  form: "single",
@@ -87904,6 +88712,7 @@ new Set([
87904
88712
  "locking",
87905
88713
  "unlocking",
87906
88714
  "jammed",
88715
+ "door-open",
87907
88716
  "on",
87908
88717
  "off",
87909
88718
  "home",