@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.mjs CHANGED
@@ -2,7 +2,7 @@ import { brotliCompressSync, deflateSync, gzipSync } from "node:zlib";
2
2
  import { readFileSync } from "node:fs";
3
3
  import path from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
- //#region ../types/dist/event-category-DmZDPDf_.mjs
5
+ //#region ../types/dist/event-category-CaURIwQA.mjs
6
6
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
7
7
  EventCategory["SystemBoot"] = "system.boot";
8
8
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -718,6 +718,10 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
718
718
  EventCategory["NotificationRuleBroken"] = "notification.rule-broken";
719
719
  /** The clear of {@link NotificationRuleBroken}: the rule loads and names nothing deleted. */
720
720
  EventCategory["NotificationRuleRepaired"] = "notification.rule-repaired";
721
+ /** A migration failed or was blocked; the Alert Center re-reads the ledger. */
722
+ EventCategory["MigrationFailed"] = "migration.failed";
723
+ /** A migration applied or a failure cleared; the Alert Center re-reads the ledger. */
724
+ EventCategory["MigrationApplied"] = "migration.applied";
721
725
  EventCategory["DeviceUpdated"] = "device.updated";
722
726
  /**
723
727
  * Transport-level connectivity. Emitted by the device driver when
@@ -780,6 +784,378 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
780
784
  return EventCategory;
781
785
  }({});
782
786
  //#endregion
787
+ //#region ../types/dist/device-type-CEqjmUnR.mjs
788
+ /**
789
+ import { errMsg } from '@camstack/types'
790
+ * Extract a human-readable message from an unknown error value.
791
+ * Replaces the ubiquitous `errMsg(err)` pattern.
792
+ */
793
+ function errMsg$1(err) {
794
+ if (err instanceof Error) return err.message;
795
+ if (typeof err === "string") return err;
796
+ return String(err);
797
+ }
798
+ var DeviceType = /* @__PURE__ */ function(DeviceType) {
799
+ DeviceType["Camera"] = "camera";
800
+ DeviceType["Hub"] = "hub";
801
+ DeviceType["Light"] = "light";
802
+ DeviceType["Siren"] = "siren";
803
+ DeviceType["Switch"] = "switch";
804
+ DeviceType["Sensor"] = "sensor";
805
+ DeviceType["Thermostat"] = "thermostat";
806
+ /** Air-conditioner / heat-pump climate device (HVAC) — shares the
807
+ * `climate-control` cap surface with `Thermostat` but renders a
808
+ * dedicated AC-appropriate control UI (mode chips, fan speed,
809
+ * independent vertical/horizontal swing). Sources: native Gree, and
810
+ * reusable by other AC integrations. */
811
+ DeviceType["Climate"] = "climate";
812
+ DeviceType["Button"] = "button";
813
+ /** Generic stateless event emitter — carries a device's EXACT declared
814
+ * event vocabulary verbatim (no normalization). Installed with the
815
+ * `event-emitter` cap. Sources: HA `event.*` entities (structured) and
816
+ * HA bus events (e.g. `zha_event`, generic). */
817
+ DeviceType["EventEmitter"] = "event-emitter";
818
+ /** Firmware/software update entity — current vs available version,
819
+ * updatable flag, update state, and an install action. Installed with
820
+ * the `update` cap. Sources: Homematic firmware-update channels (and
821
+ * reusable by other providers, e.g. HA `update.*` entities). */
822
+ DeviceType["Update"] = "update";
823
+ DeviceType["Generic"] = "generic";
824
+ /** Generic notification delivery target (HA `notify.<service>`, future
825
+ * Telegram / Discord / ntfy / SMTP, …). One device per delivery
826
+ * endpoint; the `notifier` cap defines the send surface. */
827
+ DeviceType["Notifier"] = "notifier";
828
+ /** Pre-recorded action sequence with optional parameters
829
+ * (HA `script.*`). Runnable via `script-runner` cap. */
830
+ DeviceType["Script"] = "script";
831
+ /** Automation rule (HA `automation.*`) — enable/disable + manual
832
+ * trigger surface exposed via `automation-control` cap. */
833
+ DeviceType["Automation"] = "automation";
834
+ /** Door / smart lock device (HA `lock.*`). `lock-control` cap. */
835
+ DeviceType["Lock"] = "lock";
836
+ /** Window covering, blinds, garage door, valve, etc. (HA `cover.*`,
837
+ * `valve.*`). `cover` cap with sub-roles for variant. */
838
+ DeviceType["Cover"] = "cover";
839
+ /** Pipe / water / gas valve with open/close/stop and optional
840
+ * position (HA `valve.*`). `valve` cap — a cover-sibling actuator
841
+ * modelled on the same open/closed lifecycle. */
842
+ DeviceType["Valve"] = "valve";
843
+ /** Humidifier / dehumidifier with on/off + target humidity + mode
844
+ * (HA `humidifier.*`). `humidifier` cap — a climate-family actuator
845
+ * modelled on the same target / mode lifecycle. */
846
+ DeviceType["Humidifier"] = "humidifier";
847
+ /** Water heater / boiler with target temperature + operation mode +
848
+ * away mode (HA `water_heater.*`). `water-heater` cap — a
849
+ * climate-family actuator. */
850
+ DeviceType["WaterHeater"] = "water-heater";
851
+ /** Ceiling / standing / exhaust fan (HA `fan.*`). `fan-control` cap. */
852
+ DeviceType["Fan"] = "fan";
853
+ /** Audio / video playback endpoint (HA `media_player.*`). Disjoint from
854
+ * the camera surface — those use `Camera`. `media-player` cap. */
855
+ DeviceType["MediaPlayer"] = "media-player";
856
+ /** Security panel / alarm system (HA `alarm_control_panel.*`).
857
+ * `alarm-panel` cap. */
858
+ DeviceType["AlarmPanel"] = "alarm-panel";
859
+ /** Generic user-settable input (HA `number` / `input_number` / `select`
860
+ * / `input_select` / `text` / `input_text` / `input_datetime`).
861
+ * Sub-type via `DeviceRole`: NumericControl / SelectControl /
862
+ * TextControl / DateTimeControl. */
863
+ DeviceType["Control"] = "control";
864
+ /** Person / device-tracker presence (HA `person.*`, `device_tracker.*`).
865
+ * `presence` cap. */
866
+ DeviceType["Presence"] = "presence";
867
+ /** Weather provider (HA `weather.*`). Tier-3, low MVP priority.
868
+ * `weather` cap. */
869
+ DeviceType["Weather"] = "weather";
870
+ /** Robot vacuum (HA `vacuum.*`). Tier-3. `vacuum-control` cap. */
871
+ DeviceType["Vacuum"] = "vacuum";
872
+ /** Robotic lawn mower (HA `lawn_mower.*`). Tier-3.
873
+ * `lawn-mower-control` cap. */
874
+ DeviceType["LawnMower"] = "lawn-mower";
875
+ /** Physical HA device group — parent container for entity-children
876
+ * adopted from a single HA device entry. Not renderable as a
877
+ * standalone device; exists only to anchor child entities. */
878
+ DeviceType["Container"] = "container";
879
+ /** Single still-image entity (HA `image.*`). Read-only display of an
880
+ * `entity_picture` signed URL the browser loads directly. `image` cap. */
881
+ DeviceType["Image"] = "image";
882
+ /** Smart pet feeder — cloud-connected food dispenser with a bowl food
883
+ * level, battery, desiccant life, feeding state and manual-feed /
884
+ * call-pet / maintenance actions. Installed with the `pet-feeder` cap;
885
+ * dual-hopper models (D4S/D4SH) expose per-hopper portions. Sources:
886
+ * native PetKit (`nodepetkit` `FeederDevice`), reusable by other feeder
887
+ * integrations sharing the same food/desiccant/hopper surface. */
888
+ DeviceType["PetFeeder"] = "pet-feeder";
889
+ /** Pet water fountain — tank level, water drunk, pump and its duty cycle.
890
+ * Installed with the `pet-fountain` cap. Reaches CamStack as loose entities
891
+ * of a container and is BUILT as a composed device (D967). */
892
+ DeviceType["PetFountain"] = "pet-fountain";
893
+ /** Built-in dishwasher — program lifecycle, door / remote-start gates,
894
+ * salt and rinse-aid levels. Installed with the `dishwasher` cap.
895
+ * Sources: native Bosch Home Connect, reusable by similar integrations.
896
+ * A plug-built dishwasher carries running-state and power instead. */
897
+ DeviceType["Dishwasher"] = "dishwasher";
898
+ /** Washing machine. A plug-built one is running while its power stays
899
+ * above a threshold. No program cap — that surface is the dishwasher's. */
900
+ DeviceType["Washer"] = "washer";
901
+ /** Tumble dryer. Same plug-built running surface as {@link Washer}. */
902
+ DeviceType["Dryer"] = "dryer";
903
+ /** The physical bridge box of a vendor ecosystem (Velux KLF-200, and later
904
+ * other radio-to-IP bridges). It carries the box's own facts (firmware,
905
+ * address, connection state) and the actions the box really supports
906
+ * (`reboot`, `device-admin-link`). Products are NOT its children — they are
907
+ * adopted beside it — and its connection settings stay on the integration. */
908
+ DeviceType["Gateway"] = "gateway";
909
+ return DeviceType;
910
+ }({});
911
+ var DeviceFeature = /* @__PURE__ */ function(DeviceFeature) {
912
+ DeviceFeature["BatteryOperated"] = "battery-operated";
913
+ DeviceFeature["Rebootable"] = "rebootable";
914
+ /**
915
+ * Device supports an on-demand re-sync of its derived spec with its
916
+ * upstream source — drives the generic Re-sync button. The owning
917
+ * provider implements the action via the `device-adoption.resync` cap.
918
+ */
919
+ DeviceFeature["Resyncable"] = "resyncable";
920
+ DeviceFeature["NativeSnapshot"] = "native-snapshot";
921
+ DeviceFeature["DoorbellButton"] = "doorbell-button";
922
+ DeviceFeature["TwoWayAudio"] = "two-way-audio";
923
+ DeviceFeature["PanTiltZoom"] = "pan-tilt-zoom";
924
+ /**
925
+ * Camera supports the on-firmware autotrack subsystem (subject-
926
+ * following). Distinct from `PanTiltZoom` because not every PTZ
927
+ * camera ships autotrack — the admin UI uses this flag to gate
928
+ * the autotrack toggle / settings card without re-deriving from
929
+ * the cap registry. Mirrors `ptz-autotrack` cap registration:
930
+ * driver sets this feature when probe confirms the firmware
931
+ * surface, and registers the cap in the same code path.
932
+ */
933
+ DeviceFeature["PtzAutotrack"] = "ptz-autotrack";
934
+ /**
935
+ * Accessory exposes a "trigger on motion" toggle — the parent camera's
936
+ * motion detection automatically activates this device. Mirrors
937
+ * `motion-trigger` cap registration: drivers set this feature in the
938
+ * same code path that calls `ctx.registerNativeCap(motionTriggerCapability, ...)`.
939
+ *
940
+ * Used by admin UI (gate the in-hero `MotionTriggerToggle` against a
941
+ * fast scalar without binding fetch), notifier rules, and `listAll`
942
+ * filters that want "all devices with on-motion behaviour".
943
+ */
944
+ DeviceFeature["MotionTrigger"] = "motion-trigger";
945
+ /** Light supports rgb-triplet color via `color` cap. */
946
+ DeviceFeature["LightColorRgb"] = "light-color-rgb";
947
+ /** Light supports HSV color via `color` cap. */
948
+ DeviceFeature["LightColorHsv"] = "light-color-hsv";
949
+ /** Light supports color-temperature (mired) via `color` cap. */
950
+ DeviceFeature["LightColorMired"] = "light-color-mired";
951
+ /** Thermostat supports a `heat_cool` dual setpoint (targetLow +
952
+ * targetHigh). Gates the range slider UI. */
953
+ DeviceFeature["ClimateDualSetpoint"] = "climate-dual-setpoint";
954
+ /** Thermostat exposes target humidity and/or current humidity
955
+ * readings. Gates the humidity controls. */
956
+ DeviceFeature["ClimateHumidity"] = "climate-humidity";
957
+ /** Thermostat exposes a fan-mode selector. */
958
+ DeviceFeature["ClimateFanMode"] = "climate-fan-mode";
959
+ /** Thermostat exposes preset modes (eco / away / sleep / vendor). */
960
+ DeviceFeature["ClimatePreset"] = "climate-preset";
961
+ /** Thermostat exposes a vertical louver swing toggle. Gates the
962
+ * vertical-swing switch in the climate UI. Independent of horizontal. */
963
+ DeviceFeature["ClimateSwingVertical"] = "climate-swing-vertical";
964
+ /** Thermostat exposes a horizontal louver swing toggle. Gates the
965
+ * horizontal-swing switch in the climate UI. Independent of vertical. */
966
+ DeviceFeature["ClimateSwingHorizontal"] = "climate-swing-horizontal";
967
+ /** Cover exposes intermediate position control (0..100). Gates the
968
+ * position slider UI. */
969
+ DeviceFeature["CoverPositionable"] = "cover-positionable";
970
+ /** Cover exposes slat-tilt control. Gates the tilt slider UI. */
971
+ DeviceFeature["CoverTilt"] = "cover-tilt";
972
+ /** Valve exposes intermediate position control (0..100). Gates the
973
+ * position slider / drag surface UI. */
974
+ DeviceFeature["ValvePositionable"] = "valve-positionable";
975
+ /** Fan exposes a speed-percentage setter. Gates the speed slider UI. */
976
+ DeviceFeature["FanSpeed"] = "fan-speed";
977
+ /** Fan exposes a preset mode selector. */
978
+ DeviceFeature["FanPreset"] = "fan-preset";
979
+ /** Fan exposes blade direction (forward/reverse) — typical of
980
+ * ceiling fans. */
981
+ DeviceFeature["FanDirection"] = "fan-direction";
982
+ /** Fan exposes an oscillation toggle. */
983
+ DeviceFeature["FanOscillating"] = "fan-oscillating";
984
+ /** Lock requires a PIN code on lock/unlock. Gates the code-entry
985
+ * field on the UI lock-controls panel. */
986
+ DeviceFeature["LockPinRequired"] = "lock-pin-required";
987
+ /** Lock supports a latch-release ("open door") action distinct from
988
+ * unlock. Mirrors HA `LockEntityFeature.OPEN` (bit 1) in
989
+ * `supported_features`. Gates the Open Door button in the UI. */
990
+ DeviceFeature["LockOpen"] = "lock-open";
991
+ /** Media player exposes a seek-to-position surface. */
992
+ DeviceFeature["MediaPlayerSeek"] = "media-player-seek";
993
+ /** Media player exposes a volume-level setter. */
994
+ DeviceFeature["MediaPlayerVolume"] = "media-player-volume";
995
+ /** Media player exposes a mute toggle distinct from volume=0. */
996
+ DeviceFeature["MediaPlayerMute"] = "media-player-mute";
997
+ /** Media player exposes a shuffle toggle. */
998
+ DeviceFeature["MediaPlayerShuffle"] = "media-player-shuffle";
999
+ /** Media player exposes a repeat mode (off / all / one). */
1000
+ DeviceFeature["MediaPlayerRepeat"] = "media-player-repeat";
1001
+ /** Media player exposes a source / input selector. */
1002
+ DeviceFeature["MediaPlayerSelectSource"] = "media-player-select-source";
1003
+ /** Media player exposes a play-arbitrary-media surface (URL / id). */
1004
+ DeviceFeature["MediaPlayerPlayMedia"] = "media-player-play-media";
1005
+ /** Media player exposes next-track. */
1006
+ DeviceFeature["MediaPlayerNext"] = "media-player-next";
1007
+ /** Media player exposes previous-track. */
1008
+ DeviceFeature["MediaPlayerPrevious"] = "media-player-previous";
1009
+ /** Media player exposes stop distinct from pause. */
1010
+ DeviceFeature["MediaPlayerStop"] = "media-player-stop";
1011
+ /** Alarm panel requires a PIN code on arm/disarm. */
1012
+ DeviceFeature["AlarmPinRequired"] = "alarm-pin-required";
1013
+ /** Presence device carries GPS coordinates (lat/lng/accuracy) in
1014
+ * addition to a textual location. */
1015
+ DeviceFeature["PresenceGps"] = "presence-gps";
1016
+ /** Notifier accepts an inline / URL image attachment. */
1017
+ DeviceFeature["NotifierImage"] = "notifier-image";
1018
+ /** Notifier accepts a priority hint (high/normal/low). */
1019
+ DeviceFeature["NotifierPriority"] = "notifier-priority";
1020
+ /** Notifier accepts a free-form `data` payload for platform-specific
1021
+ * fields. */
1022
+ DeviceFeature["NotifierData"] = "notifier-data";
1023
+ /** Notifier supports interactive action buttons / callbacks. */
1024
+ DeviceFeature["NotifierActions"] = "notifier-actions";
1025
+ /** Notifier supports per-call recipient targeting (multi-user). */
1026
+ DeviceFeature["NotifierRecipients"] = "notifier-recipients";
1027
+ /** Script runner accepts a variables map on each run invocation. */
1028
+ DeviceFeature["ScriptVariables"] = "script-variables";
1029
+ /** Automation `trigger` accepts a skipCondition flag — fires the
1030
+ * automation's actions while bypassing its condition block. */
1031
+ DeviceFeature["AutomationSkipCondition"] = "automation-skip-condition";
1032
+ /** Robot vacuum exposes a live cleaning map (image child). Gates the
1033
+ * map tile in the vacuum UI. */
1034
+ DeviceFeature["VacuumHasMap"] = "vacuum-has-map";
1035
+ /** Robot vacuum exposes AI obstacle-detection toggles. Gates the AI
1036
+ * switches group. */
1037
+ DeviceFeature["VacuumHasAi"] = "vacuum-has-ai";
1038
+ /** Robot mower exposes a live mowing map (SVG image child). Gates the
1039
+ * mower map tile. */
1040
+ DeviceFeature["MowerHasMap"] = "mower-has-map";
1041
+ /** Robot mower exposes targeted mowing (all-area / zones / edges /
1042
+ * spots) — gates the Mow action + map selectors. */
1043
+ DeviceFeature["MowerHasTargetedMowing"] = "mower-has-targeted-mowing";
1044
+ return DeviceFeature;
1045
+ }({});
1046
+ /**
1047
+ * Semantic role a device plays within its parent. Populated by driver
1048
+ * addons when creating accessory devices (Reolink siren/floodlight/
1049
+ * PIR/chime/autotrack/doorbell, ONVIF relay outputs, …). Used by the
1050
+ * admin UI to pick icons, labels, and widgets — a `Switch` with
1051
+ * `role: Floodlight` renders as a bulb with a brightness slider,
1052
+ * whereas a `Switch` with `role: Siren` renders as a klaxon.
1053
+ *
1054
+ * Undefined for top-level devices (cameras, NVRs, hubs). Persisted in
1055
+ * sqlite as a nullable TEXT column — old rows keep working unchanged.
1056
+ */
1057
+ var DeviceRole = /* @__PURE__ */ function(DeviceRole) {
1058
+ DeviceRole["Siren"] = "siren";
1059
+ DeviceRole["Floodlight"] = "floodlight";
1060
+ DeviceRole["Spotlight"] = "spotlight";
1061
+ DeviceRole["PirSensor"] = "pir-sensor";
1062
+ DeviceRole["Chime"] = "chime";
1063
+ DeviceRole["Autotrack"] = "autotrack";
1064
+ DeviceRole["Nightvision"] = "nightvision";
1065
+ DeviceRole["PrivacyMask"] = "privacy-mask";
1066
+ DeviceRole["Doorbell"] = "doorbell";
1067
+ /** Interior blind / shade / roller blind. HA `device_class: blind|shade`. */
1068
+ DeviceRole["CoverBlind"] = "cover-blind";
1069
+ /** Exterior roller shutter. HA `device_class: shutter`; Homematic BROLL. */
1070
+ DeviceRole["CoverShutter"] = "cover-shutter";
1071
+ /** Exterior awning. HA `device_class: awning`. */
1072
+ DeviceRole["CoverAwning"] = "cover-awning";
1073
+ /** Curtain / drape. HA `device_class: curtain`. */
1074
+ DeviceRole["CoverCurtain"] = "cover-curtain";
1075
+ /** Garage door. HA `device_class: garage`. A security actuator: exports gate it. */
1076
+ DeviceRole["CoverGarage"] = "cover-garage";
1077
+ /** Gate. HA `device_class: gate`. A security actuator: exports gate it. */
1078
+ DeviceRole["CoverGate"] = "cover-gate";
1079
+ /** Door. HA `device_class: door`. A security actuator: exports gate it. */
1080
+ DeviceRole["CoverDoor"] = "cover-door";
1081
+ /** Window. HA `device_class: window`. */
1082
+ DeviceRole["CoverWindow"] = "cover-window";
1083
+ /** Virtual HA toggle (input_boolean.*) — distinguishable from a
1084
+ * real Switch device for UI rendering / export adapters. */
1085
+ DeviceRole["BinaryHelper"] = "binary-helper";
1086
+ /** A smart plug: a `switch` whose source says it is an outlet (HA
1087
+ * `device_class: outlet`). The Alexa export shows it as a SMARTPLUG. */
1088
+ DeviceRole["Outlet"] = "outlet";
1089
+ /** Generic motion / occupancy / moving event source. Distinct from
1090
+ * the camera accessory PirSensor role: that one is a camera child;
1091
+ * this is a standalone HA / 3rd-party motion sensor. */
1092
+ DeviceRole["MotionSensor"] = "motion-sensor";
1093
+ DeviceRole["ContactSensor"] = "contact-sensor";
1094
+ DeviceRole["LeakSensor"] = "leak-sensor";
1095
+ DeviceRole["SmokeSensor"] = "smoke-sensor";
1096
+ DeviceRole["COSensor"] = "co-sensor";
1097
+ DeviceRole["GasSensor"] = "gas-sensor";
1098
+ DeviceRole["TamperSensor"] = "tamper-sensor";
1099
+ DeviceRole["VibrationSensor"] = "vibration-sensor";
1100
+ DeviceRole["ConnectivitySensor"] = "connectivity-sensor";
1101
+ DeviceRole["SoundSensor"] = "sound-sensor";
1102
+ /** Fallback for `binary_sensor` without a known `device_class`. */
1103
+ DeviceRole["BinarySensor"] = "binary-sensor";
1104
+ DeviceRole["TemperatureSensor"] = "temperature-sensor";
1105
+ DeviceRole["HumiditySensor"] = "humidity-sensor";
1106
+ DeviceRole["AmbientLightSensor"] = "ambient-light-sensor";
1107
+ DeviceRole["PressureSensor"] = "pressure-sensor";
1108
+ /** Wind speed or direction (weather-station `wind-sensor` cap). */
1109
+ DeviceRole["WindSensor"] = "wind-sensor";
1110
+ /** Rain accumulation or rate (weather-station `rain-sensor` cap). */
1111
+ DeviceRole["RainSensor"] = "rain-sensor";
1112
+ /** UV index (weather-station `uv-sensor` cap). */
1113
+ DeviceRole["UvSensor"] = "uv-sensor";
1114
+ /** Solar irradiance W/m² (weather-station `solar-radiation-sensor` cap).
1115
+ * Distinct from AmbientLightSensor (lux). */
1116
+ DeviceRole["SolarRadiationSensor"] = "solar-radiation-sensor";
1117
+ /** Soil moisture % (garden/weather `soil-moisture-sensor` cap). */
1118
+ DeviceRole["SoilMoistureSensor"] = "soil-moisture-sensor";
1119
+ DeviceRole["PowerSensor"] = "power-sensor";
1120
+ DeviceRole["EnergySensor"] = "energy-sensor";
1121
+ DeviceRole["VoltageSensor"] = "voltage-sensor";
1122
+ DeviceRole["CurrentSensor"] = "current-sensor";
1123
+ DeviceRole["AirQualitySensor"] = "air-quality-sensor";
1124
+ /** Battery level (numeric % via `sensor` OR low-bool via
1125
+ * `binary_sensor` — the cap distinguishes via the value type). */
1126
+ DeviceRole["BatterySensor"] = "battery-sensor";
1127
+ /** Fallback for `sensor` numeric without a known `device_class`. */
1128
+ DeviceRole["NumericSensor"] = "numeric-sensor";
1129
+ /** String / enum state (HA `sensor` with `state_class: enum` or
1130
+ * `attributes.options`). */
1131
+ DeviceRole["EnumSensor"] = "enum-sensor";
1132
+ /** Date / timestamp state (HA `sensor` with `device_class: timestamp`
1133
+ * or `date`). The slice carries the raw ISO string verbatim (hosted on
1134
+ * the `enum-sensor` cap); the UI renders it locale-formatted. */
1135
+ DeviceRole["DateTimeSensor"] = "datetime-sensor";
1136
+ /** Last-resort fallback when nothing else matches. */
1137
+ DeviceRole["GenericSensor"] = "generic-sensor";
1138
+ DeviceRole["NumericControl"] = "numeric-control";
1139
+ DeviceRole["SelectControl"] = "select-control";
1140
+ DeviceRole["TextControl"] = "text-control";
1141
+ DeviceRole["DateTimeControl"] = "datetime-control";
1142
+ /** Mobile push notifier (HA `notify.mobile_app_*`) — supports
1143
+ * rich features (image, priority, channel routing). */
1144
+ DeviceRole["MobilePushNotifier"] = "mobile-push-notifier";
1145
+ /** Chat / messaging service (HA `notify.telegram_*`,
1146
+ * `notify.discord_*`, etc.). */
1147
+ DeviceRole["MessagingNotifier"] = "messaging-notifier";
1148
+ /** Email-based delivery (HA `notify.smtp`, etc.). */
1149
+ DeviceRole["EmailNotifier"] = "email-notifier";
1150
+ /** Fallback when the notifier service name doesn't match a known
1151
+ * pattern. */
1152
+ DeviceRole["GenericNotifier"] = "generic-notifier";
1153
+ /** Emits a pulse train (infrared or RF) on request — the `ir-transmitter`
1154
+ * cap. A `DeviceType.Generic` device. */
1155
+ DeviceRole["IrTransmitter"] = "ir-transmitter";
1156
+ return DeviceRole;
1157
+ }({});
1158
+ //#endregion
783
1159
  //#region ../../node_modules/zod/v4/core/core.js
784
1160
  var _a$1;
785
1161
  function $constructor(name, initializer, params) {
@@ -5591,7 +5967,7 @@ var ZodIssueCode = {
5591
5967
  var ZodFirstPartyTypeKind;
5592
5968
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5593
5969
  //#endregion
5594
- //#region ../types/dist/sleep-0RR2wzNB.mjs
5970
+ //#region ../types/dist/sleep-CSYuxssh.mjs
5595
5971
  /**
5596
5972
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5597
5973
  * window to float samples (D455).
@@ -6107,6 +6483,102 @@ function emitReadiness(bus, params) {
6107
6483
  ts
6108
6484
  }));
6109
6485
  }
6486
+ var NOT_RUN = {
6487
+ declared: false,
6488
+ snapshot: null,
6489
+ applied: [],
6490
+ failed: [],
6491
+ blocked: [],
6492
+ skipped: [],
6493
+ blockOwner: []
6494
+ };
6495
+ /**
6496
+ * Offer `migrations` to the engine and run what it says to run, in its order. Never
6497
+ * throws: an unreachable or refusing engine is logged and reported `declared: false`,
6498
+ * because the hub (and this addon's boot) must not hang on a migration.
6499
+ */
6500
+ async function runAddonMigrations(host, migrations, logger) {
6501
+ const first = migrations[0];
6502
+ if (first === void 0) return NOT_RUN;
6503
+ const owner = first.meta.owner;
6504
+ const log = logger.withTags({ migrationOwner: owner });
6505
+ const byId = new Map(migrations.map((m) => [m.meta.id, m]));
6506
+ let declaration;
6507
+ try {
6508
+ declaration = await host.declareOwner({
6509
+ owner,
6510
+ metas: migrations.map((m) => m.meta)
6511
+ });
6512
+ } catch (err) {
6513
+ log.error("the migration engine refused or could not take this addon catalogue — none ran", { meta: { error: errMsg$1(err) } });
6514
+ return {
6515
+ ...NOT_RUN,
6516
+ blockOwner: migrations.filter((m) => m.meta.onFailure === "block-owner").map((m) => m.meta.id)
6517
+ };
6518
+ }
6519
+ const applied = [];
6520
+ const failed = [];
6521
+ const blocked = [];
6522
+ const skipped = [];
6523
+ const settle = (id, status) => {
6524
+ if (status === "applied") applied.push(id);
6525
+ else if (status === "failed") failed.push(id);
6526
+ else if (status === "blocked") blocked.push(id);
6527
+ else skipped.push(id);
6528
+ };
6529
+ for (const step of declaration.steps) {
6530
+ const bound = byId.get(step.id);
6531
+ if (bound === void 0 || step.action === "skip") {
6532
+ skipped.push(step.id);
6533
+ continue;
6534
+ }
6535
+ const tags = { migrationId: step.id };
6536
+ try {
6537
+ const gate = await host.startMigration({ id: step.id });
6538
+ if (!gate.proceed) {
6539
+ log.warn("migration not run", {
6540
+ tags,
6541
+ meta: {
6542
+ state: gate.state,
6543
+ reason: gate.reason
6544
+ }
6545
+ });
6546
+ settle(step.id, gate.state === "pending" ? "blocked" : gate.state);
6547
+ continue;
6548
+ }
6549
+ const result = await bound.execute();
6550
+ const outcome = await host.finishMigration({
6551
+ id: step.id,
6552
+ result
6553
+ });
6554
+ if (outcome.status === "applied") log.info("migration applied", {
6555
+ tags,
6556
+ meta: { rowsChanged: outcome.rowsChanged }
6557
+ });
6558
+ else log.error("migration failed", {
6559
+ tags,
6560
+ meta: { error: outcome.error }
6561
+ });
6562
+ settle(step.id, outcome.status);
6563
+ } catch (err) {
6564
+ log.error("migration could not be completed with the engine", {
6565
+ tags,
6566
+ meta: { error: errMsg$1(err) }
6567
+ });
6568
+ failed.push(step.id);
6569
+ }
6570
+ }
6571
+ const blockOwner = [...failed, ...blocked].filter((id) => byId.get(id)?.meta.onFailure === "block-owner");
6572
+ return {
6573
+ declared: true,
6574
+ snapshot: declaration.snapshot,
6575
+ applied,
6576
+ failed,
6577
+ blocked,
6578
+ skipped,
6579
+ blockOwner
6580
+ };
6581
+ }
6110
6582
  /**
6111
6583
  * Build a {@link DurableState} over a single store key. Transport-agnostic:
6112
6584
  * `read`/`write` are the addon-store or device-store accessors. The whole
@@ -7162,6 +7634,28 @@ var BaseAddon = class {
7162
7634
  return normalized;
7163
7635
  }
7164
7636
  /**
7637
+ * Run this addon's migration catalogue (D987 M3) through the hub's `migrations`
7638
+ * engine. Call it at the point of the boot where the data must already have the new
7639
+ * shape — `onInitialize`, or `onRestoreDevices` for a migration that needs the
7640
+ * restored devices — with the catalogue bound to the addon's ports
7641
+ * (`defineAddonMigrations(ADDON_MIGRATIONS, ports)`).
7642
+ *
7643
+ * An engine that cannot be reached or refuses the catalogue is logged and runs
7644
+ * nothing; the boot continues. A migration that FAILED (or was blocked by one)
7645
+ * and declares `onFailure: 'block-owner'` makes this throw, so the addon does not
7646
+ * serve data whose shape it cannot trust; `tolerate` migrations never do.
7647
+ */
7648
+ async runMigrations(migrations) {
7649
+ const api = this.ctx.api.migrations;
7650
+ const report = await runAddonMigrations({
7651
+ declareOwner: (input) => api.declareOwner.mutate(input),
7652
+ startMigration: (input) => api.startMigration.mutate(input),
7653
+ finishMigration: (input) => api.finishMigration.mutate(input)
7654
+ }, migrations, this.ctx.logger);
7655
+ 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`);
7656
+ return report;
7657
+ }
7658
+ /**
7165
7659
  * Called by the isolated-process runner AFTER `broker.start()`.
7166
7660
  * In-process addons never need this because the hub broker is already
7167
7661
  * running when `initialize()` fires. For forked children the broker
@@ -7294,6 +7788,26 @@ var BaseAddon = class {
7294
7788
  * alternative — skip it — turns a rename into a surface that quietly
7295
7789
  * empties, which looks exactly like an addon with nothing to configure.
7296
7790
  */
7791
+ /**
7792
+ * The form schema of ONE integration instance, or `null` to use {@link globalSettingsSchema}.
7793
+ *
7794
+ * Override it only when an `instanceMode: multiple` addon's instances differ: an NVR integration
7795
+ * needs its connection fields, the default integration the addon's own sections. It is the
7796
+ * SHAPE of the form only — the values live in `integrations.getSettings`, and the write path is
7797
+ * unchanged (`integrations.setSettings` for an account integration).
7798
+ */
7799
+ async integrationSettingsSchema(_instance) {
7800
+ return null;
7801
+ }
7802
+ /**
7803
+ * What the create wizard and the integration edit page render for one instance: the addon's
7804
+ * per-instance schema when it declares one, else its global schema (what they always showed).
7805
+ */
7806
+ async getIntegrationFormSchema(instance, nodeId) {
7807
+ const scoped = await this.integrationSettingsSchema(instance);
7808
+ if (scoped === null) return this.getGlobalSettings(void 0, void 0, nodeId);
7809
+ return hydrateSchema(scoped, {}, { inlineSecretsAsViews: true });
7810
+ }
7297
7811
  async getIntegrationSettings(nodeId) {
7298
7812
  const declared = this.integrationSettingSections();
7299
7813
  if (declared.length === 0) return null;
@@ -8397,407 +8911,6 @@ function defineRecordKinds(specs) {
8397
8911
  }
8398
8912
  return out;
8399
8913
  }
8400
- var READING_QUANTITIES = [
8401
- "temperature",
8402
- "pressure",
8403
- "speed",
8404
- "precipitation",
8405
- "precipitation-rate",
8406
- "length",
8407
- "area",
8408
- "illuminance",
8409
- "irradiance",
8410
- "power",
8411
- "energy",
8412
- "voltage",
8413
- "current",
8414
- "apparent-power",
8415
- "humidity",
8416
- "percentage",
8417
- "concentration-volume",
8418
- "concentration-mass",
8419
- "concentration-ppb",
8420
- "volume",
8421
- "duration",
8422
- "duration-ms",
8423
- "information",
8424
- "sound-level",
8425
- "signal-level",
8426
- "colour-temperature",
8427
- "angle",
8428
- "bitrate",
8429
- "rate",
8430
- "mass",
8431
- "index",
8432
- "ratio",
8433
- "count",
8434
- "none",
8435
- "device"
8436
- ];
8437
- var ReadingQuantitySchema = _enum(READING_QUANTITIES);
8438
- new Set(READING_QUANTITIES);
8439
- /**
8440
- * Stored unit symbol for a quantity. `none` means the number is dimensionless
8441
- * (or an index / ratio / count). `device` means the cap has no single unit.
8442
- */
8443
- var QUANTITY_BASE = {
8444
- temperature: "°C",
8445
- pressure: "hPa",
8446
- speed: "m/s",
8447
- precipitation: "mm",
8448
- "precipitation-rate": "mm/h",
8449
- length: "m",
8450
- area: "m²",
8451
- illuminance: "lx",
8452
- irradiance: "W/m²",
8453
- power: "W",
8454
- energy: "kWh",
8455
- voltage: "V",
8456
- current: "A",
8457
- "apparent-power": "VA",
8458
- humidity: "%",
8459
- percentage: "%",
8460
- "concentration-volume": "ppm",
8461
- "concentration-mass": "µg/m³",
8462
- "concentration-ppb": "ppb",
8463
- volume: "L",
8464
- duration: "s",
8465
- "duration-ms": "ms",
8466
- information: "B",
8467
- "sound-level": "dBFS",
8468
- "signal-level": "dBm",
8469
- "colour-temperature": "mired",
8470
- angle: "°",
8471
- bitrate: "kbps",
8472
- rate: "fps",
8473
- mass: "kg",
8474
- index: "none",
8475
- ratio: "none",
8476
- count: "none",
8477
- none: "none",
8478
- device: "device"
8479
- };
8480
- /** The stored unit, or `undefined` when the quantity has no unit symbol. */
8481
- function baseUnitOf(quantity) {
8482
- const symbol = QUANTITY_BASE[quantity];
8483
- if (symbol === "none" || symbol === "device") return void 0;
8484
- return symbol;
8485
- }
8486
- function readingMeta(quantity, extras) {
8487
- return {
8488
- quantity,
8489
- ...extras?.precision !== void 0 ? { precision: extras.precision } : {},
8490
- ...extras?.label !== void 0 ? { label: extras.label } : {}
8491
- };
8492
- }
8493
- /** Shared Zod schemas used across streaming capabilities. */
8494
- var CamProfileSchema = _enum([
8495
- "high",
8496
- "mid",
8497
- "low"
8498
- ]);
8499
- /**
8500
- * Which rung a per-camera consumer listens on: an explicit profile, or `auto`.
8501
- *
8502
- * `auto` is a REAL value, not an absent one — it means "resolve it by the rule
8503
- * that consumer owns". It lives beside `CamProfile` because a rung is the only
8504
- * operator-facing way to name a stream: a cam-stream id is broker-internal and
8505
- * moves when a slot is reassigned, so a stored id silently stops meaning what
8506
- * the operator picked. Today's one user is the audio analyser, whose rule is
8507
- * "the lowest AVAILABLE native rung" (D537).
8508
- */
8509
- var AudioStreamPickSchema = union([CamProfileSchema, literal("auto")]);
8510
- var CamStreamKindSchema = _enum([
8511
- "pull-rtsp",
8512
- "pull-rtmp",
8513
- "pull-http",
8514
- "pull-flv",
8515
- "pull-rfc4571",
8516
- "push-annexb",
8517
- "derived"
8518
- ]);
8519
- var CamStreamResolutionSchema = object({
8520
- width: number().int().positive().meta(readingMeta("none")),
8521
- height: number().int().positive().meta(readingMeta("none"))
8522
- });
8523
- var CameraStreamSchema = object({
8524
- /** Stable, provider-assigned id unique within the (deviceId) scope. */
8525
- camStreamId: string().min(1),
8526
- deviceId: number().int().nonnegative(),
8527
- kind: CamStreamKindSchema,
8528
- /** Required for pull-* kinds. Ignored for push-annexb. */
8529
- url: string().optional(),
8530
- codec: string().optional(),
8531
- resolution: CamStreamResolutionSchema.optional(),
8532
- fps: number().positive().optional(),
8533
- /** Human label surfaced in the Admin UI "Camera Stream" dropdown. */
8534
- label: string().optional(),
8535
- /**
8536
- * Device-level features the publisher advertised (e.g. `battery-operated`).
8537
- * The broker, snapshot orchestrator, and prebuffer manager all consult
8538
- * this list to derive policy — relaxed stall watchdog for battery
8539
- * cams, prebuffer off by default, longer snapshot rate-limit, etc.
8540
- *
8541
- * Single source of truth replacing per-stream flags like the
8542
- * historical `allowStall`: if the publisher knows the camera is
8543
- * battery-powered, every downstream service derives the right policy
8544
- * from this list.
8545
- */
8546
- deviceFeatures: array(string()).optional(),
8547
- /**
8548
- * Whether this stream participates in the broker's automatic profile
8549
- * assignment (`computeInitialAssignment`). Defaults to `true`. Publishers
8550
- * use `false` when they want a stream to be SELECTABLE in the UI but not
8551
- * picked by default — e.g. Reolink publishes its native Baichuan streams
8552
- * as `autoEligible: true` (the recommended path) and its RTSP / RTMP
8553
- * mirrors as `autoEligible: false` (still pickable per slot, just not
8554
- * the auto choice). Manual `assignProfile` calls remain valid for
8555
- * non-eligible streams.
8556
- */
8557
- autoEligible: boolean().optional(),
8558
- /**
8559
- * The profile slot the PUBLISHER says this stream is: `low` for a stream
8560
- * whose only sensible role is the low-bandwidth one, `high` for a main
8561
- * stream. Consulted by the broker's automatic ranking
8562
- * (`rankInitialAssignment`) BEFORE the pixel-count heuristic, and only there:
8563
- * an operator's manual assignment still wins, and an absent hint is the
8564
- * pixel-ranked default every camera had before this field existed.
8565
- *
8566
- * Why it exists: a camera with ONE stream ranked to `mid` — the slot no
8567
- * storage class is written for on its own (`recordings` is high|mid,
8568
- * `recordingsLow` is low) and the one profile scrub does not read. The robot
8569
- * camera's single 720p relay is a `low` in every respect except the slot the
8570
- * heuristic put it in.
8571
- */
8572
- profileHint: CamProfileSchema.optional(),
8573
- /**
8574
- * Transport-specific opaque metadata. The broker passes it through to
8575
- * the source reader without inspecting it. Currently used by
8576
- * `pull-rfc4571` streams to carry the upstream SDP (so the reader can
8577
- * route RTP packets to the right depacketizer without an in-band
8578
- * DESCRIBE phase). Other kinds typically leave it undefined.
8579
- */
8580
- metadata: record(string(), unknown()).optional()
8581
- });
8582
- var ProfileSlotStatusSchema = _enum([
8583
- "unassigned",
8584
- "idle",
8585
- "connecting",
8586
- "streaming",
8587
- "error"
8588
- ]);
8589
- var ProfileSlotSchema = object({
8590
- deviceId: number().int().nonnegative(),
8591
- profile: CamProfileSchema,
8592
- /** Broker id the rest of the system addresses: `${deviceId}/${profile}`. */
8593
- brokerId: string(),
8594
- /** `null` when the profile is unassigned. */
8595
- sourceCamStreamId: string().nullable(),
8596
- status: ProfileSlotStatusSchema,
8597
- resolution: CamStreamResolutionSchema.optional(),
8598
- codec: string().optional(),
8599
- preBufferSec: number().nonnegative().optional(),
8600
- errorMessage: string().optional()
8601
- });
8602
- /**
8603
- * Zod schema for StreamSourceEntry — the canonical stream descriptor
8604
- * exposed by ICameraDevice.getStreamSources() and consumed by the broker.
8605
- */
8606
- var StreamSourceEntrySchema$1 = object({
8607
- id: string(),
8608
- label: string(),
8609
- protocol: _enum([
8610
- "rtsp",
8611
- "rtmp",
8612
- "annexb",
8613
- "http-mjpeg",
8614
- "webrtc",
8615
- "custom"
8616
- ]),
8617
- url: string().optional(),
8618
- resolution: object({
8619
- width: number(),
8620
- height: number()
8621
- }).readonly().optional(),
8622
- fps: number().optional(),
8623
- bitrate: number().optional(),
8624
- codec: string().optional(),
8625
- profileHint: CamProfileSchema.optional()
8626
- });
8627
- object({
8628
- type: string(),
8629
- url: string(),
8630
- videoCodec: string().optional(),
8631
- audioCodec: string().optional(),
8632
- metadata: record(string(), unknown()).readonly().optional()
8633
- });
8634
- /**
8635
- * Wire schema for `FrameHandle` — a zero-pixel, serialisable reference to a
8636
- * frame in a shared-memory ring (Phase 5 / D9). Mirrors the `FrameHandle` TS
8637
- * interface in `interfaces/frame-handle.ts` field-for-field so a cap method
8638
- * (the runner's native-crop and pipeline-executor inputs) can carry handles
8639
- * over tRPC / Moleculer.
8640
- *
8641
- * The `satisfies` assertion below pins this schema to the interface: if a
8642
- * field is added to / removed from `FrameHandle` without a matching schema
8643
- * edit, `z.infer<typeof FrameHandleSchema>` stops being assignable to
8644
- * `FrameHandle` and the build fails — no silent drift.
8645
- */
8646
- var FrameHandleSchema = object({
8647
- shmId: string(),
8648
- slot: number().int().nonnegative(),
8649
- seq: number().int().nonnegative(),
8650
- width: number().int().positive(),
8651
- height: number().int().positive(),
8652
- format: _enum([
8653
- "jpeg",
8654
- "rgb",
8655
- "bgr",
8656
- "yuv420",
8657
- "gray"
8658
- ]),
8659
- pts: number(),
8660
- byteLength: number().int().nonnegative(),
8661
- nodeId: string(),
8662
- slotCount: number().int().positive()
8663
- });
8664
- /**
8665
- * Wire schema for a decoded audio chunk (Phase 5 / D9). Mirrors the
8666
- * `DecodedAudioChunk` TS interface in `interfaces/stream-broker.ts`: PCM
8667
- * sample bytes plus the track parameters a consumer needs to interpret them.
8668
- *
8669
- * Audio chunks are tiny (a ~500ms AudioCodecSession window is a few KB of
8670
- * Float32 PCM), so unlike video frames they ship their bytes INLINE over
8671
- * tRPC / Moleculer — no shared-memory plane. `data` is typed `Uint8Array`
8672
- * (the wire-serialisable supertype of `Buffer`); a `Buffer` is assignable to
8673
- * it.
8674
- */
8675
- var AudioChunkFormatSchema = _enum(AUDIO_CHUNK_FORMATS);
8676
- var DecodedAudioChunkSchema = object({
8677
- data: _instanceof(Uint8Array),
8678
- sampleRate: number().int().positive(),
8679
- channels: number().int().positive(),
8680
- timestamp: number(),
8681
- /**
8682
- * Byte format of `data`. ABSENT MEANS `f32le` — today's bytes, byte for
8683
- * byte, for any peer that never heard of this field. A coded window
8684
- * (`pcmu` / `pcma`, one byte per sample) is only ever emitted to a
8685
- * subscription that DECLARED it accepts one, so absence can never mean
8686
- * "coded bytes a consumer will read as floats" (D455).
8687
- */
8688
- format: AudioChunkFormatSchema.optional()
8689
- });
8690
- /**
8691
- * Input for `stream-broker.subscribeAudioChunks` (Phase 5 / D9). Audio ships
8692
- * its bytes inline: a consumer asks the broker for the decoded audio-chunk
8693
- * stream of one broker; the broker keeps a per-subscription bounded FIFO queue
8694
- * the consumer drains via `pullAudioChunks`.
8695
- */
8696
- var SubscribeAudioChunksInputSchema = object({
8697
- brokerId: string(),
8698
- /** Short caller-identity tag (`audio-analyzer`, …) for `listClients`. */
8699
- tag: string().optional(),
8700
- /**
8701
- * Byte formats this subscriber can READ, best first. The broker serves the
8702
- * chunk's own format when it is in this list and expands to `f32le`
8703
- * otherwise, so a subscriber is never handed bytes it cannot interpret.
8704
- *
8705
- * Absent (or without the source format) means `f32le` — the behaviour every
8706
- * subscriber had before D455, unchanged. This is the negotiation half of
8707
- * the source-bytes lever: it is what lets the broker and its consumers
8708
- * deploy one at a time across three nodes.
8709
- */
8710
- accept: array(AudioChunkFormatSchema).readonly().optional()
8711
- });
8712
- /** Result of `stream-broker.subscribeAudioChunks`. */
8713
- var SubscribeAudioChunksResultSchema = object({
8714
- /** Opaque id passed to `pullAudioChunks` / `unsubscribeAudioChunks`. */
8715
- subscriptionId: string() });
8716
- var BrokerStatsSchema = object({
8717
- status: _enum([
8718
- "idle",
8719
- "connecting",
8720
- "streaming",
8721
- "error",
8722
- "stopped"
8723
- ]),
8724
- inputFps: number(),
8725
- encodedSubscribers: number(),
8726
- uptimeMs: number(),
8727
- bitrateKbps: number(),
8728
- idrIntervalMs: number(),
8729
- codec: string().optional(),
8730
- totalBytes: number(),
8731
- packetCount: number(),
8732
- rtspClients: number(),
8733
- pipeClients: number(),
8734
- preBufferSec: number(),
8735
- preBufferMs: number(),
8736
- preBufferPackets: number(),
8737
- /**
8738
- * Detected audio track parameters from the RTSP DESCRIBE / SDP. `null`
8739
- * when the stream has no audio track or the broker is in cold start.
8740
- * `supported = false` means the codec was detected but the local
8741
- * decoder pipeline cannot produce PCM chunks (e.g. AAC without the
8742
- * AAC pipeline wired). Surfaced in the UI device-overview so operators
8743
- * can pre-pick the audio-analysis model that matches the codec.
8744
- */
8745
- audio: object({
8746
- codec: string(),
8747
- sampleRate: number(),
8748
- channels: number(),
8749
- supported: boolean()
8750
- }).nullable().optional(),
8751
- /**
8752
- * BROKER-SIDE AUDIO MUTE (D83). `true` = this broker is deliberately
8753
- * distributing none of the device's audio, on live or recording.
8754
- *
8755
- * Present so a silent camera can be told apart from a broken one on the
8756
- * stream panel itself, without cross-referencing the switch group: a
8757
- * broker holding an `audio` track descriptor while `audioMuted` is true is
8758
- * working exactly as asked. `audioMutedDropped` counts the audio units
8759
- * thrown away since the current dial — it is how you confirm from stats
8760
- * alone that the mute is on the packet path and not merely persisted.
8761
- */
8762
- audioMuted: boolean().optional(),
8763
- audioMutedDropped: number().optional()
8764
- });
8765
- /**
8766
- * Exporter-facing "profile restream" entry. Returned by
8767
- * `cameraStreams.getProfileRtspEntries` — one per ASSIGNED profile slot
8768
- * (high/mid/low). `brokerId` is the PROFILE-keyed broker id
8769
- * (`${deviceId}/${profile}`, e.g. `15/high`); its `url` is a broker
8770
- * RTSP restream that aliases the profile's assigned source broker, so
8771
- * every consumer dialling a profile converges on the same single pull.
8772
- * `codec`/`resolution` describe the assigned SOURCE camStream (for the
8773
- * `auto` resolution-closest pick). Distinct from `RtspRestreamEntry`
8774
- * (raw per-camStream, live-view only).
8775
- */
8776
- var ProfileRtspEntrySchema = object({
8777
- profile: CamProfileSchema,
8778
- /** Profile-keyed broker id, format `${deviceId}/${profile}` (e.g. `"15/high"`). */
8779
- brokerId: string(),
8780
- url: string(),
8781
- mutedUrl: string(),
8782
- enabled: boolean(),
8783
- codec: string().optional(),
8784
- resolution: CamStreamResolutionSchema.optional()
8785
- });
8786
- /**
8787
- * The model weight formats a catalog entry can ship. Schema-free on purpose:
8788
- * `types/models.ts` builds Zod schemas from it, but a runner-closure consumer
8789
- * (the model downloader in `@camstack/system`) needs only the list and must
8790
- * reach it through `@camstack/types/addon` without building the cap schema
8791
- * graph (D28).
8792
- */
8793
- var MODEL_FORMATS = [
8794
- "onnx",
8795
- "coreml",
8796
- "openvino",
8797
- "tflite",
8798
- "pt",
8799
- "gguf"
8800
- ];
8801
8914
  object({
8802
8915
  field: string().min(1),
8803
8916
  on: literal("advance")
@@ -8886,6 +8999,407 @@ function optionalMethod(input, output, options) {
8886
8999
  providerOptional: true
8887
9000
  };
8888
9001
  }
9002
+ var READING_QUANTITIES = [
9003
+ "temperature",
9004
+ "pressure",
9005
+ "speed",
9006
+ "precipitation",
9007
+ "precipitation-rate",
9008
+ "length",
9009
+ "area",
9010
+ "illuminance",
9011
+ "irradiance",
9012
+ "power",
9013
+ "energy",
9014
+ "voltage",
9015
+ "current",
9016
+ "apparent-power",
9017
+ "humidity",
9018
+ "percentage",
9019
+ "concentration-volume",
9020
+ "concentration-mass",
9021
+ "concentration-ppb",
9022
+ "volume",
9023
+ "duration",
9024
+ "duration-ms",
9025
+ "information",
9026
+ "sound-level",
9027
+ "signal-level",
9028
+ "colour-temperature",
9029
+ "angle",
9030
+ "bitrate",
9031
+ "rate",
9032
+ "mass",
9033
+ "index",
9034
+ "ratio",
9035
+ "count",
9036
+ "none",
9037
+ "device"
9038
+ ];
9039
+ var ReadingQuantitySchema = _enum(READING_QUANTITIES);
9040
+ new Set(READING_QUANTITIES);
9041
+ /**
9042
+ * Stored unit symbol for a quantity. `none` means the number is dimensionless
9043
+ * (or an index / ratio / count). `device` means the cap has no single unit.
9044
+ */
9045
+ var QUANTITY_BASE = {
9046
+ temperature: "°C",
9047
+ pressure: "hPa",
9048
+ speed: "m/s",
9049
+ precipitation: "mm",
9050
+ "precipitation-rate": "mm/h",
9051
+ length: "m",
9052
+ area: "m²",
9053
+ illuminance: "lx",
9054
+ irradiance: "W/m²",
9055
+ power: "W",
9056
+ energy: "kWh",
9057
+ voltage: "V",
9058
+ current: "A",
9059
+ "apparent-power": "VA",
9060
+ humidity: "%",
9061
+ percentage: "%",
9062
+ "concentration-volume": "ppm",
9063
+ "concentration-mass": "µg/m³",
9064
+ "concentration-ppb": "ppb",
9065
+ volume: "L",
9066
+ duration: "s",
9067
+ "duration-ms": "ms",
9068
+ information: "B",
9069
+ "sound-level": "dBFS",
9070
+ "signal-level": "dBm",
9071
+ "colour-temperature": "mired",
9072
+ angle: "°",
9073
+ bitrate: "kbps",
9074
+ rate: "fps",
9075
+ mass: "kg",
9076
+ index: "none",
9077
+ ratio: "none",
9078
+ count: "none",
9079
+ none: "none",
9080
+ device: "device"
9081
+ };
9082
+ /** The stored unit, or `undefined` when the quantity has no unit symbol. */
9083
+ function baseUnitOf(quantity) {
9084
+ const symbol = QUANTITY_BASE[quantity];
9085
+ if (symbol === "none" || symbol === "device") return void 0;
9086
+ return symbol;
9087
+ }
9088
+ function readingMeta(quantity, extras) {
9089
+ return {
9090
+ quantity,
9091
+ ...extras?.precision !== void 0 ? { precision: extras.precision } : {},
9092
+ ...extras?.label !== void 0 ? { label: extras.label } : {}
9093
+ };
9094
+ }
9095
+ /** Shared Zod schemas used across streaming capabilities. */
9096
+ var CamProfileSchema = _enum([
9097
+ "high",
9098
+ "mid",
9099
+ "low"
9100
+ ]);
9101
+ /**
9102
+ * Which rung a per-camera consumer listens on: an explicit profile, or `auto`.
9103
+ *
9104
+ * `auto` is a REAL value, not an absent one — it means "resolve it by the rule
9105
+ * that consumer owns". It lives beside `CamProfile` because a rung is the only
9106
+ * operator-facing way to name a stream: a cam-stream id is broker-internal and
9107
+ * moves when a slot is reassigned, so a stored id silently stops meaning what
9108
+ * the operator picked. Today's one user is the audio analyser, whose rule is
9109
+ * "the lowest AVAILABLE native rung" (D537).
9110
+ */
9111
+ var AudioStreamPickSchema = union([CamProfileSchema, literal("auto")]);
9112
+ var CamStreamKindSchema = _enum([
9113
+ "pull-rtsp",
9114
+ "pull-rtmp",
9115
+ "pull-http",
9116
+ "pull-flv",
9117
+ "pull-rfc4571",
9118
+ "push-annexb",
9119
+ "derived"
9120
+ ]);
9121
+ var CamStreamResolutionSchema = object({
9122
+ width: number().int().positive().meta(readingMeta("none")),
9123
+ height: number().int().positive().meta(readingMeta("none"))
9124
+ });
9125
+ var CameraStreamSchema = object({
9126
+ /** Stable, provider-assigned id unique within the (deviceId) scope. */
9127
+ camStreamId: string().min(1),
9128
+ deviceId: number().int().nonnegative(),
9129
+ kind: CamStreamKindSchema,
9130
+ /** Required for pull-* kinds. Ignored for push-annexb. */
9131
+ url: string().optional(),
9132
+ codec: string().optional(),
9133
+ resolution: CamStreamResolutionSchema.optional(),
9134
+ fps: number().positive().optional(),
9135
+ /** Human label surfaced in the Admin UI "Camera Stream" dropdown. */
9136
+ label: string().optional(),
9137
+ /**
9138
+ * Device-level features the publisher advertised (e.g. `battery-operated`).
9139
+ * The broker, snapshot orchestrator, and prebuffer manager all consult
9140
+ * this list to derive policy — relaxed stall watchdog for battery
9141
+ * cams, prebuffer off by default, longer snapshot rate-limit, etc.
9142
+ *
9143
+ * Single source of truth replacing per-stream flags like the
9144
+ * historical `allowStall`: if the publisher knows the camera is
9145
+ * battery-powered, every downstream service derives the right policy
9146
+ * from this list.
9147
+ */
9148
+ deviceFeatures: array(string()).optional(),
9149
+ /**
9150
+ * Whether this stream participates in the broker's automatic profile
9151
+ * assignment (`computeInitialAssignment`). Defaults to `true`. Publishers
9152
+ * use `false` when they want a stream to be SELECTABLE in the UI but not
9153
+ * picked by default — e.g. Reolink publishes its native Baichuan streams
9154
+ * as `autoEligible: true` (the recommended path) and its RTSP / RTMP
9155
+ * mirrors as `autoEligible: false` (still pickable per slot, just not
9156
+ * the auto choice). Manual `assignProfile` calls remain valid for
9157
+ * non-eligible streams.
9158
+ */
9159
+ autoEligible: boolean().optional(),
9160
+ /**
9161
+ * The profile slot the PUBLISHER says this stream is: `low` for a stream
9162
+ * whose only sensible role is the low-bandwidth one, `high` for a main
9163
+ * stream. Consulted by the broker's automatic ranking
9164
+ * (`rankInitialAssignment`) BEFORE the pixel-count heuristic, and only there:
9165
+ * an operator's manual assignment still wins, and an absent hint is the
9166
+ * pixel-ranked default every camera had before this field existed.
9167
+ *
9168
+ * Why it exists: a camera with ONE stream ranked to `mid` — the slot no
9169
+ * storage class is written for on its own (`recordings` is high|mid,
9170
+ * `recordingsLow` is low) and the one profile scrub does not read. The robot
9171
+ * camera's single 720p relay is a `low` in every respect except the slot the
9172
+ * heuristic put it in.
9173
+ */
9174
+ profileHint: CamProfileSchema.optional(),
9175
+ /**
9176
+ * Transport-specific opaque metadata. The broker passes it through to
9177
+ * the source reader without inspecting it. Currently used by
9178
+ * `pull-rfc4571` streams to carry the upstream SDP (so the reader can
9179
+ * route RTP packets to the right depacketizer without an in-band
9180
+ * DESCRIBE phase). Other kinds typically leave it undefined.
9181
+ */
9182
+ metadata: record(string(), unknown()).optional()
9183
+ });
9184
+ var ProfileSlotStatusSchema = _enum([
9185
+ "unassigned",
9186
+ "idle",
9187
+ "connecting",
9188
+ "streaming",
9189
+ "error"
9190
+ ]);
9191
+ var ProfileSlotSchema = object({
9192
+ deviceId: number().int().nonnegative(),
9193
+ profile: CamProfileSchema,
9194
+ /** Broker id the rest of the system addresses: `${deviceId}/${profile}`. */
9195
+ brokerId: string(),
9196
+ /** `null` when the profile is unassigned. */
9197
+ sourceCamStreamId: string().nullable(),
9198
+ status: ProfileSlotStatusSchema,
9199
+ resolution: CamStreamResolutionSchema.optional(),
9200
+ codec: string().optional(),
9201
+ preBufferSec: number().nonnegative().optional(),
9202
+ errorMessage: string().optional()
9203
+ });
9204
+ /**
9205
+ * Zod schema for StreamSourceEntry — the canonical stream descriptor
9206
+ * exposed by ICameraDevice.getStreamSources() and consumed by the broker.
9207
+ */
9208
+ var StreamSourceEntrySchema$1 = object({
9209
+ id: string(),
9210
+ label: string(),
9211
+ protocol: _enum([
9212
+ "rtsp",
9213
+ "rtmp",
9214
+ "annexb",
9215
+ "http-mjpeg",
9216
+ "webrtc",
9217
+ "custom"
9218
+ ]),
9219
+ url: string().optional(),
9220
+ resolution: object({
9221
+ width: number(),
9222
+ height: number()
9223
+ }).readonly().optional(),
9224
+ fps: number().optional(),
9225
+ bitrate: number().optional(),
9226
+ codec: string().optional(),
9227
+ profileHint: CamProfileSchema.optional()
9228
+ });
9229
+ object({
9230
+ type: string(),
9231
+ url: string(),
9232
+ videoCodec: string().optional(),
9233
+ audioCodec: string().optional(),
9234
+ metadata: record(string(), unknown()).readonly().optional()
9235
+ });
9236
+ /**
9237
+ * Wire schema for `FrameHandle` — a zero-pixel, serialisable reference to a
9238
+ * frame in a shared-memory ring (Phase 5 / D9). Mirrors the `FrameHandle` TS
9239
+ * interface in `interfaces/frame-handle.ts` field-for-field so a cap method
9240
+ * (the runner's native-crop and pipeline-executor inputs) can carry handles
9241
+ * over tRPC / Moleculer.
9242
+ *
9243
+ * The `satisfies` assertion below pins this schema to the interface: if a
9244
+ * field is added to / removed from `FrameHandle` without a matching schema
9245
+ * edit, `z.infer<typeof FrameHandleSchema>` stops being assignable to
9246
+ * `FrameHandle` and the build fails — no silent drift.
9247
+ */
9248
+ var FrameHandleSchema = object({
9249
+ shmId: string(),
9250
+ slot: number().int().nonnegative(),
9251
+ seq: number().int().nonnegative(),
9252
+ width: number().int().positive(),
9253
+ height: number().int().positive(),
9254
+ format: _enum([
9255
+ "jpeg",
9256
+ "rgb",
9257
+ "bgr",
9258
+ "yuv420",
9259
+ "gray"
9260
+ ]),
9261
+ pts: number(),
9262
+ byteLength: number().int().nonnegative(),
9263
+ nodeId: string(),
9264
+ slotCount: number().int().positive()
9265
+ });
9266
+ /**
9267
+ * Wire schema for a decoded audio chunk (Phase 5 / D9). Mirrors the
9268
+ * `DecodedAudioChunk` TS interface in `interfaces/stream-broker.ts`: PCM
9269
+ * sample bytes plus the track parameters a consumer needs to interpret them.
9270
+ *
9271
+ * Audio chunks are tiny (a ~500ms AudioCodecSession window is a few KB of
9272
+ * Float32 PCM), so unlike video frames they ship their bytes INLINE over
9273
+ * tRPC / Moleculer — no shared-memory plane. `data` is typed `Uint8Array`
9274
+ * (the wire-serialisable supertype of `Buffer`); a `Buffer` is assignable to
9275
+ * it.
9276
+ */
9277
+ var AudioChunkFormatSchema = _enum(AUDIO_CHUNK_FORMATS);
9278
+ var DecodedAudioChunkSchema = object({
9279
+ data: _instanceof(Uint8Array),
9280
+ sampleRate: number().int().positive(),
9281
+ channels: number().int().positive(),
9282
+ timestamp: number(),
9283
+ /**
9284
+ * Byte format of `data`. ABSENT MEANS `f32le` — today's bytes, byte for
9285
+ * byte, for any peer that never heard of this field. A coded window
9286
+ * (`pcmu` / `pcma`, one byte per sample) is only ever emitted to a
9287
+ * subscription that DECLARED it accepts one, so absence can never mean
9288
+ * "coded bytes a consumer will read as floats" (D455).
9289
+ */
9290
+ format: AudioChunkFormatSchema.optional()
9291
+ });
9292
+ /**
9293
+ * Input for `stream-broker.subscribeAudioChunks` (Phase 5 / D9). Audio ships
9294
+ * its bytes inline: a consumer asks the broker for the decoded audio-chunk
9295
+ * stream of one broker; the broker keeps a per-subscription bounded FIFO queue
9296
+ * the consumer drains via `pullAudioChunks`.
9297
+ */
9298
+ var SubscribeAudioChunksInputSchema = object({
9299
+ brokerId: string(),
9300
+ /** Short caller-identity tag (`audio-analyzer`, …) for `listClients`. */
9301
+ tag: string().optional(),
9302
+ /**
9303
+ * Byte formats this subscriber can READ, best first. The broker serves the
9304
+ * chunk's own format when it is in this list and expands to `f32le`
9305
+ * otherwise, so a subscriber is never handed bytes it cannot interpret.
9306
+ *
9307
+ * Absent (or without the source format) means `f32le` — the behaviour every
9308
+ * subscriber had before D455, unchanged. This is the negotiation half of
9309
+ * the source-bytes lever: it is what lets the broker and its consumers
9310
+ * deploy one at a time across three nodes.
9311
+ */
9312
+ accept: array(AudioChunkFormatSchema).readonly().optional()
9313
+ });
9314
+ /** Result of `stream-broker.subscribeAudioChunks`. */
9315
+ var SubscribeAudioChunksResultSchema = object({
9316
+ /** Opaque id passed to `pullAudioChunks` / `unsubscribeAudioChunks`. */
9317
+ subscriptionId: string() });
9318
+ var BrokerStatsSchema = object({
9319
+ status: _enum([
9320
+ "idle",
9321
+ "connecting",
9322
+ "streaming",
9323
+ "error",
9324
+ "stopped"
9325
+ ]),
9326
+ inputFps: number(),
9327
+ encodedSubscribers: number(),
9328
+ uptimeMs: number(),
9329
+ bitrateKbps: number(),
9330
+ idrIntervalMs: number(),
9331
+ codec: string().optional(),
9332
+ totalBytes: number(),
9333
+ packetCount: number(),
9334
+ rtspClients: number(),
9335
+ pipeClients: number(),
9336
+ preBufferSec: number(),
9337
+ preBufferMs: number(),
9338
+ preBufferPackets: number(),
9339
+ /**
9340
+ * Detected audio track parameters from the RTSP DESCRIBE / SDP. `null`
9341
+ * when the stream has no audio track or the broker is in cold start.
9342
+ * `supported = false` means the codec was detected but the local
9343
+ * decoder pipeline cannot produce PCM chunks (e.g. AAC without the
9344
+ * AAC pipeline wired). Surfaced in the UI device-overview so operators
9345
+ * can pre-pick the audio-analysis model that matches the codec.
9346
+ */
9347
+ audio: object({
9348
+ codec: string(),
9349
+ sampleRate: number(),
9350
+ channels: number(),
9351
+ supported: boolean()
9352
+ }).nullable().optional(),
9353
+ /**
9354
+ * BROKER-SIDE AUDIO MUTE (D83). `true` = this broker is deliberately
9355
+ * distributing none of the device's audio, on live or recording.
9356
+ *
9357
+ * Present so a silent camera can be told apart from a broken one on the
9358
+ * stream panel itself, without cross-referencing the switch group: a
9359
+ * broker holding an `audio` track descriptor while `audioMuted` is true is
9360
+ * working exactly as asked. `audioMutedDropped` counts the audio units
9361
+ * thrown away since the current dial — it is how you confirm from stats
9362
+ * alone that the mute is on the packet path and not merely persisted.
9363
+ */
9364
+ audioMuted: boolean().optional(),
9365
+ audioMutedDropped: number().optional()
9366
+ });
9367
+ /**
9368
+ * Exporter-facing "profile restream" entry. Returned by
9369
+ * `cameraStreams.getProfileRtspEntries` — one per ASSIGNED profile slot
9370
+ * (high/mid/low). `brokerId` is the PROFILE-keyed broker id
9371
+ * (`${deviceId}/${profile}`, e.g. `15/high`); its `url` is a broker
9372
+ * RTSP restream that aliases the profile's assigned source broker, so
9373
+ * every consumer dialling a profile converges on the same single pull.
9374
+ * `codec`/`resolution` describe the assigned SOURCE camStream (for the
9375
+ * `auto` resolution-closest pick). Distinct from `RtspRestreamEntry`
9376
+ * (raw per-camStream, live-view only).
9377
+ */
9378
+ var ProfileRtspEntrySchema = object({
9379
+ profile: CamProfileSchema,
9380
+ /** Profile-keyed broker id, format `${deviceId}/${profile}` (e.g. `"15/high"`). */
9381
+ brokerId: string(),
9382
+ url: string(),
9383
+ mutedUrl: string(),
9384
+ enabled: boolean(),
9385
+ codec: string().optional(),
9386
+ resolution: CamStreamResolutionSchema.optional()
9387
+ });
9388
+ /**
9389
+ * The model weight formats a catalog entry can ship. Schema-free on purpose:
9390
+ * `types/models.ts` builds Zod schemas from it, but a runner-closure consumer
9391
+ * (the model downloader in `@camstack/server/system`) needs only the list and must
9392
+ * reach it through `@camstack/types/addon` without building the cap schema
9393
+ * graph (D28).
9394
+ */
9395
+ var MODEL_FORMATS = [
9396
+ "onnx",
9397
+ "coreml",
9398
+ "openvino",
9399
+ "tflite",
9400
+ "pt",
9401
+ "gguf"
9402
+ ];
8889
9403
  /**
8890
9404
  * `addon-pages-source` — collection cap exposing per-provider raw page
8891
9405
  * declarations. Every addon that contributes a UI page registers a
@@ -9149,6 +9663,7 @@ var CAP_NAMES = Object.freeze([
9149
9663
  "mesh-network",
9150
9664
  "meter",
9151
9665
  "metrics-provider",
9666
+ "migrations",
9152
9667
  "model-convert",
9153
9668
  "model-distributor",
9154
9669
  "motion",
@@ -9181,6 +9696,7 @@ var CAP_NAMES = Object.freeze([
9181
9696
  "plate-gallery",
9182
9697
  "platform-probe",
9183
9698
  "power-meter",
9699
+ "power-source",
9184
9700
  "presence",
9185
9701
  "pressure-sensor",
9186
9702
  "privacy-mask",
@@ -10144,7 +10660,8 @@ Object.freeze({
10144
10660
  "unlocked",
10145
10661
  "locking",
10146
10662
  "unlocking",
10147
- "jammed"
10663
+ "jammed",
10664
+ "door-open"
10148
10665
  ]
10149
10666
  }],
10150
10667
  "media-player": [
@@ -10531,6 +11048,13 @@ Object.freeze({
10531
11048
  "valueType": "number"
10532
11049
  }
10533
11050
  ],
11051
+ "power-source": [{
11052
+ "path": "switchOn",
11053
+ "valueType": "boolean"
11054
+ }, {
11055
+ "path": "offSeconds",
11056
+ "valueType": "number"
11057
+ }],
10534
11058
  "presence": [
10535
11059
  {
10536
11060
  "path": "state",
@@ -11056,369 +11580,7 @@ function userError(code, params, englishFallback) {
11056
11580
  return new CamstackError(code, params, englishFallback);
11057
11581
  }
11058
11582
  //#endregion
11059
- //#region ../types/dist/err-msg-BD5buxvT.mjs
11060
- var DeviceType = /* @__PURE__ */ function(DeviceType) {
11061
- DeviceType["Camera"] = "camera";
11062
- DeviceType["Hub"] = "hub";
11063
- DeviceType["Light"] = "light";
11064
- DeviceType["Siren"] = "siren";
11065
- DeviceType["Switch"] = "switch";
11066
- DeviceType["Sensor"] = "sensor";
11067
- DeviceType["Thermostat"] = "thermostat";
11068
- /** Air-conditioner / heat-pump climate device (HVAC) — shares the
11069
- * `climate-control` cap surface with `Thermostat` but renders a
11070
- * dedicated AC-appropriate control UI (mode chips, fan speed,
11071
- * independent vertical/horizontal swing). Sources: native Gree, and
11072
- * reusable by other AC integrations. */
11073
- DeviceType["Climate"] = "climate";
11074
- DeviceType["Button"] = "button";
11075
- /** Generic stateless event emitter — carries a device's EXACT declared
11076
- * event vocabulary verbatim (no normalization). Installed with the
11077
- * `event-emitter` cap. Sources: HA `event.*` entities (structured) and
11078
- * HA bus events (e.g. `zha_event`, generic). */
11079
- DeviceType["EventEmitter"] = "event-emitter";
11080
- /** Firmware/software update entity — current vs available version,
11081
- * updatable flag, update state, and an install action. Installed with
11082
- * the `update` cap. Sources: Homematic firmware-update channels (and
11083
- * reusable by other providers, e.g. HA `update.*` entities). */
11084
- DeviceType["Update"] = "update";
11085
- DeviceType["Generic"] = "generic";
11086
- /** Generic notification delivery target (HA `notify.<service>`, future
11087
- * Telegram / Discord / ntfy / SMTP, …). One device per delivery
11088
- * endpoint; the `notifier` cap defines the send surface. */
11089
- DeviceType["Notifier"] = "notifier";
11090
- /** Pre-recorded action sequence with optional parameters
11091
- * (HA `script.*`). Runnable via `script-runner` cap. */
11092
- DeviceType["Script"] = "script";
11093
- /** Automation rule (HA `automation.*`) — enable/disable + manual
11094
- * trigger surface exposed via `automation-control` cap. */
11095
- DeviceType["Automation"] = "automation";
11096
- /** Door / smart lock device (HA `lock.*`). `lock-control` cap. */
11097
- DeviceType["Lock"] = "lock";
11098
- /** Window covering, blinds, garage door, valve, etc. (HA `cover.*`,
11099
- * `valve.*`). `cover` cap with sub-roles for variant. */
11100
- DeviceType["Cover"] = "cover";
11101
- /** Pipe / water / gas valve with open/close/stop and optional
11102
- * position (HA `valve.*`). `valve` cap — a cover-sibling actuator
11103
- * modelled on the same open/closed lifecycle. */
11104
- DeviceType["Valve"] = "valve";
11105
- /** Humidifier / dehumidifier with on/off + target humidity + mode
11106
- * (HA `humidifier.*`). `humidifier` cap — a climate-family actuator
11107
- * modelled on the same target / mode lifecycle. */
11108
- DeviceType["Humidifier"] = "humidifier";
11109
- /** Water heater / boiler with target temperature + operation mode +
11110
- * away mode (HA `water_heater.*`). `water-heater` cap — a
11111
- * climate-family actuator. */
11112
- DeviceType["WaterHeater"] = "water-heater";
11113
- /** Ceiling / standing / exhaust fan (HA `fan.*`). `fan-control` cap. */
11114
- DeviceType["Fan"] = "fan";
11115
- /** Audio / video playback endpoint (HA `media_player.*`). Disjoint from
11116
- * the camera surface — those use `Camera`. `media-player` cap. */
11117
- DeviceType["MediaPlayer"] = "media-player";
11118
- /** Security panel / alarm system (HA `alarm_control_panel.*`).
11119
- * `alarm-panel` cap. */
11120
- DeviceType["AlarmPanel"] = "alarm-panel";
11121
- /** Generic user-settable input (HA `number` / `input_number` / `select`
11122
- * / `input_select` / `text` / `input_text` / `input_datetime`).
11123
- * Sub-type via `DeviceRole`: NumericControl / SelectControl /
11124
- * TextControl / DateTimeControl. */
11125
- DeviceType["Control"] = "control";
11126
- /** Person / device-tracker presence (HA `person.*`, `device_tracker.*`).
11127
- * `presence` cap. */
11128
- DeviceType["Presence"] = "presence";
11129
- /** Weather provider (HA `weather.*`). Tier-3, low MVP priority.
11130
- * `weather` cap. */
11131
- DeviceType["Weather"] = "weather";
11132
- /** Robot vacuum (HA `vacuum.*`). Tier-3. `vacuum-control` cap. */
11133
- DeviceType["Vacuum"] = "vacuum";
11134
- /** Robotic lawn mower (HA `lawn_mower.*`). Tier-3.
11135
- * `lawn-mower-control` cap. */
11136
- DeviceType["LawnMower"] = "lawn-mower";
11137
- /** Physical HA device group — parent container for entity-children
11138
- * adopted from a single HA device entry. Not renderable as a
11139
- * standalone device; exists only to anchor child entities. */
11140
- DeviceType["Container"] = "container";
11141
- /** Single still-image entity (HA `image.*`). Read-only display of an
11142
- * `entity_picture` signed URL the browser loads directly. `image` cap. */
11143
- DeviceType["Image"] = "image";
11144
- /** Smart pet feeder — cloud-connected food dispenser with a bowl food
11145
- * level, battery, desiccant life, feeding state and manual-feed /
11146
- * call-pet / maintenance actions. Installed with the `pet-feeder` cap;
11147
- * dual-hopper models (D4S/D4SH) expose per-hopper portions. Sources:
11148
- * native PetKit (`nodepetkit` `FeederDevice`), reusable by other feeder
11149
- * integrations sharing the same food/desiccant/hopper surface. */
11150
- DeviceType["PetFeeder"] = "pet-feeder";
11151
- /** Pet water fountain — tank level, water drunk, pump and its duty cycle.
11152
- * Installed with the `pet-fountain` cap. Reaches CamStack as loose entities
11153
- * of a container and is BUILT as a composed device (D967). */
11154
- DeviceType["PetFountain"] = "pet-fountain";
11155
- /** Built-in dishwasher — program lifecycle, door / remote-start gates,
11156
- * salt and rinse-aid levels. Installed with the `dishwasher` cap.
11157
- * Sources: native Bosch Home Connect, reusable by similar integrations.
11158
- * A plug-built dishwasher carries running-state and power instead. */
11159
- DeviceType["Dishwasher"] = "dishwasher";
11160
- /** Washing machine. A plug-built one is running while its power stays
11161
- * above a threshold. No program cap — that surface is the dishwasher's. */
11162
- DeviceType["Washer"] = "washer";
11163
- /** Tumble dryer. Same plug-built running surface as {@link Washer}. */
11164
- DeviceType["Dryer"] = "dryer";
11165
- /** The physical bridge box of a vendor ecosystem (Velux KLF-200, and later
11166
- * other radio-to-IP bridges). It carries the box's own facts (firmware,
11167
- * address, connection state) and the actions the box really supports
11168
- * (`reboot`, `device-admin-link`). Products are NOT its children — they are
11169
- * adopted beside it — and its connection settings stay on the integration. */
11170
- DeviceType["Gateway"] = "gateway";
11171
- return DeviceType;
11172
- }({});
11173
- var DeviceFeature = /* @__PURE__ */ function(DeviceFeature) {
11174
- DeviceFeature["BatteryOperated"] = "battery-operated";
11175
- DeviceFeature["Rebootable"] = "rebootable";
11176
- /**
11177
- * Device supports an on-demand re-sync of its derived spec with its
11178
- * upstream source — drives the generic Re-sync button. The owning
11179
- * provider implements the action via the `device-adoption.resync` cap.
11180
- */
11181
- DeviceFeature["Resyncable"] = "resyncable";
11182
- DeviceFeature["NativeSnapshot"] = "native-snapshot";
11183
- DeviceFeature["DoorbellButton"] = "doorbell-button";
11184
- DeviceFeature["TwoWayAudio"] = "two-way-audio";
11185
- DeviceFeature["PanTiltZoom"] = "pan-tilt-zoom";
11186
- /**
11187
- * Camera supports the on-firmware autotrack subsystem (subject-
11188
- * following). Distinct from `PanTiltZoom` because not every PTZ
11189
- * camera ships autotrack — the admin UI uses this flag to gate
11190
- * the autotrack toggle / settings card without re-deriving from
11191
- * the cap registry. Mirrors `ptz-autotrack` cap registration:
11192
- * driver sets this feature when probe confirms the firmware
11193
- * surface, and registers the cap in the same code path.
11194
- */
11195
- DeviceFeature["PtzAutotrack"] = "ptz-autotrack";
11196
- /**
11197
- * Accessory exposes a "trigger on motion" toggle — the parent camera's
11198
- * motion detection automatically activates this device. Mirrors
11199
- * `motion-trigger` cap registration: drivers set this feature in the
11200
- * same code path that calls `ctx.registerNativeCap(motionTriggerCapability, ...)`.
11201
- *
11202
- * Used by admin UI (gate the in-hero `MotionTriggerToggle` against a
11203
- * fast scalar without binding fetch), notifier rules, and `listAll`
11204
- * filters that want "all devices with on-motion behaviour".
11205
- */
11206
- DeviceFeature["MotionTrigger"] = "motion-trigger";
11207
- /** Light supports rgb-triplet color via `color` cap. */
11208
- DeviceFeature["LightColorRgb"] = "light-color-rgb";
11209
- /** Light supports HSV color via `color` cap. */
11210
- DeviceFeature["LightColorHsv"] = "light-color-hsv";
11211
- /** Light supports color-temperature (mired) via `color` cap. */
11212
- DeviceFeature["LightColorMired"] = "light-color-mired";
11213
- /** Thermostat supports a `heat_cool` dual setpoint (targetLow +
11214
- * targetHigh). Gates the range slider UI. */
11215
- DeviceFeature["ClimateDualSetpoint"] = "climate-dual-setpoint";
11216
- /** Thermostat exposes target humidity and/or current humidity
11217
- * readings. Gates the humidity controls. */
11218
- DeviceFeature["ClimateHumidity"] = "climate-humidity";
11219
- /** Thermostat exposes a fan-mode selector. */
11220
- DeviceFeature["ClimateFanMode"] = "climate-fan-mode";
11221
- /** Thermostat exposes preset modes (eco / away / sleep / vendor). */
11222
- DeviceFeature["ClimatePreset"] = "climate-preset";
11223
- /** Thermostat exposes a vertical louver swing toggle. Gates the
11224
- * vertical-swing switch in the climate UI. Independent of horizontal. */
11225
- DeviceFeature["ClimateSwingVertical"] = "climate-swing-vertical";
11226
- /** Thermostat exposes a horizontal louver swing toggle. Gates the
11227
- * horizontal-swing switch in the climate UI. Independent of vertical. */
11228
- DeviceFeature["ClimateSwingHorizontal"] = "climate-swing-horizontal";
11229
- /** Cover exposes intermediate position control (0..100). Gates the
11230
- * position slider UI. */
11231
- DeviceFeature["CoverPositionable"] = "cover-positionable";
11232
- /** Cover exposes slat-tilt control. Gates the tilt slider UI. */
11233
- DeviceFeature["CoverTilt"] = "cover-tilt";
11234
- /** Valve exposes intermediate position control (0..100). Gates the
11235
- * position slider / drag surface UI. */
11236
- DeviceFeature["ValvePositionable"] = "valve-positionable";
11237
- /** Fan exposes a speed-percentage setter. Gates the speed slider UI. */
11238
- DeviceFeature["FanSpeed"] = "fan-speed";
11239
- /** Fan exposes a preset mode selector. */
11240
- DeviceFeature["FanPreset"] = "fan-preset";
11241
- /** Fan exposes blade direction (forward/reverse) — typical of
11242
- * ceiling fans. */
11243
- DeviceFeature["FanDirection"] = "fan-direction";
11244
- /** Fan exposes an oscillation toggle. */
11245
- DeviceFeature["FanOscillating"] = "fan-oscillating";
11246
- /** Lock requires a PIN code on lock/unlock. Gates the code-entry
11247
- * field on the UI lock-controls panel. */
11248
- DeviceFeature["LockPinRequired"] = "lock-pin-required";
11249
- /** Lock supports a latch-release ("open door") action distinct from
11250
- * unlock. Mirrors HA `LockEntityFeature.OPEN` (bit 1) in
11251
- * `supported_features`. Gates the Open Door button in the UI. */
11252
- DeviceFeature["LockOpen"] = "lock-open";
11253
- /** Media player exposes a seek-to-position surface. */
11254
- DeviceFeature["MediaPlayerSeek"] = "media-player-seek";
11255
- /** Media player exposes a volume-level setter. */
11256
- DeviceFeature["MediaPlayerVolume"] = "media-player-volume";
11257
- /** Media player exposes a mute toggle distinct from volume=0. */
11258
- DeviceFeature["MediaPlayerMute"] = "media-player-mute";
11259
- /** Media player exposes a shuffle toggle. */
11260
- DeviceFeature["MediaPlayerShuffle"] = "media-player-shuffle";
11261
- /** Media player exposes a repeat mode (off / all / one). */
11262
- DeviceFeature["MediaPlayerRepeat"] = "media-player-repeat";
11263
- /** Media player exposes a source / input selector. */
11264
- DeviceFeature["MediaPlayerSelectSource"] = "media-player-select-source";
11265
- /** Media player exposes a play-arbitrary-media surface (URL / id). */
11266
- DeviceFeature["MediaPlayerPlayMedia"] = "media-player-play-media";
11267
- /** Media player exposes next-track. */
11268
- DeviceFeature["MediaPlayerNext"] = "media-player-next";
11269
- /** Media player exposes previous-track. */
11270
- DeviceFeature["MediaPlayerPrevious"] = "media-player-previous";
11271
- /** Media player exposes stop distinct from pause. */
11272
- DeviceFeature["MediaPlayerStop"] = "media-player-stop";
11273
- /** Alarm panel requires a PIN code on arm/disarm. */
11274
- DeviceFeature["AlarmPinRequired"] = "alarm-pin-required";
11275
- /** Presence device carries GPS coordinates (lat/lng/accuracy) in
11276
- * addition to a textual location. */
11277
- DeviceFeature["PresenceGps"] = "presence-gps";
11278
- /** Notifier accepts an inline / URL image attachment. */
11279
- DeviceFeature["NotifierImage"] = "notifier-image";
11280
- /** Notifier accepts a priority hint (high/normal/low). */
11281
- DeviceFeature["NotifierPriority"] = "notifier-priority";
11282
- /** Notifier accepts a free-form `data` payload for platform-specific
11283
- * fields. */
11284
- DeviceFeature["NotifierData"] = "notifier-data";
11285
- /** Notifier supports interactive action buttons / callbacks. */
11286
- DeviceFeature["NotifierActions"] = "notifier-actions";
11287
- /** Notifier supports per-call recipient targeting (multi-user). */
11288
- DeviceFeature["NotifierRecipients"] = "notifier-recipients";
11289
- /** Script runner accepts a variables map on each run invocation. */
11290
- DeviceFeature["ScriptVariables"] = "script-variables";
11291
- /** Automation `trigger` accepts a skipCondition flag — fires the
11292
- * automation's actions while bypassing its condition block. */
11293
- DeviceFeature["AutomationSkipCondition"] = "automation-skip-condition";
11294
- /** Robot vacuum exposes a live cleaning map (image child). Gates the
11295
- * map tile in the vacuum UI. */
11296
- DeviceFeature["VacuumHasMap"] = "vacuum-has-map";
11297
- /** Robot vacuum exposes AI obstacle-detection toggles. Gates the AI
11298
- * switches group. */
11299
- DeviceFeature["VacuumHasAi"] = "vacuum-has-ai";
11300
- /** Robot mower exposes a live mowing map (SVG image child). Gates the
11301
- * mower map tile. */
11302
- DeviceFeature["MowerHasMap"] = "mower-has-map";
11303
- /** Robot mower exposes targeted mowing (all-area / zones / edges /
11304
- * spots) — gates the Mow action + map selectors. */
11305
- DeviceFeature["MowerHasTargetedMowing"] = "mower-has-targeted-mowing";
11306
- return DeviceFeature;
11307
- }({});
11308
- /**
11309
- * Semantic role a device plays within its parent. Populated by driver
11310
- * addons when creating accessory devices (Reolink siren/floodlight/
11311
- * PIR/chime/autotrack/doorbell, ONVIF relay outputs, …). Used by the
11312
- * admin UI to pick icons, labels, and widgets — a `Switch` with
11313
- * `role: Floodlight` renders as a bulb with a brightness slider,
11314
- * whereas a `Switch` with `role: Siren` renders as a klaxon.
11315
- *
11316
- * Undefined for top-level devices (cameras, NVRs, hubs). Persisted in
11317
- * sqlite as a nullable TEXT column — old rows keep working unchanged.
11318
- */
11319
- var DeviceRole = /* @__PURE__ */ function(DeviceRole) {
11320
- DeviceRole["Siren"] = "siren";
11321
- DeviceRole["Floodlight"] = "floodlight";
11322
- DeviceRole["Spotlight"] = "spotlight";
11323
- DeviceRole["PirSensor"] = "pir-sensor";
11324
- DeviceRole["Chime"] = "chime";
11325
- DeviceRole["Autotrack"] = "autotrack";
11326
- DeviceRole["Nightvision"] = "nightvision";
11327
- DeviceRole["PrivacyMask"] = "privacy-mask";
11328
- DeviceRole["Doorbell"] = "doorbell";
11329
- /** Interior blind / shade / roller blind. HA `device_class: blind|shade`. */
11330
- DeviceRole["CoverBlind"] = "cover-blind";
11331
- /** Exterior roller shutter. HA `device_class: shutter`; Homematic BROLL. */
11332
- DeviceRole["CoverShutter"] = "cover-shutter";
11333
- /** Exterior awning. HA `device_class: awning`. */
11334
- DeviceRole["CoverAwning"] = "cover-awning";
11335
- /** Curtain / drape. HA `device_class: curtain`. */
11336
- DeviceRole["CoverCurtain"] = "cover-curtain";
11337
- /** Garage door. HA `device_class: garage`. A security actuator: exports gate it. */
11338
- DeviceRole["CoverGarage"] = "cover-garage";
11339
- /** Gate. HA `device_class: gate`. A security actuator: exports gate it. */
11340
- DeviceRole["CoverGate"] = "cover-gate";
11341
- /** Door. HA `device_class: door`. A security actuator: exports gate it. */
11342
- DeviceRole["CoverDoor"] = "cover-door";
11343
- /** Window. HA `device_class: window`. */
11344
- DeviceRole["CoverWindow"] = "cover-window";
11345
- /** Virtual HA toggle (input_boolean.*) — distinguishable from a
11346
- * real Switch device for UI rendering / export adapters. */
11347
- DeviceRole["BinaryHelper"] = "binary-helper";
11348
- /** A smart plug: a `switch` whose source says it is an outlet (HA
11349
- * `device_class: outlet`). The Alexa export shows it as a SMARTPLUG. */
11350
- DeviceRole["Outlet"] = "outlet";
11351
- /** Generic motion / occupancy / moving event source. Distinct from
11352
- * the camera accessory PirSensor role: that one is a camera child;
11353
- * this is a standalone HA / 3rd-party motion sensor. */
11354
- DeviceRole["MotionSensor"] = "motion-sensor";
11355
- DeviceRole["ContactSensor"] = "contact-sensor";
11356
- DeviceRole["LeakSensor"] = "leak-sensor";
11357
- DeviceRole["SmokeSensor"] = "smoke-sensor";
11358
- DeviceRole["COSensor"] = "co-sensor";
11359
- DeviceRole["GasSensor"] = "gas-sensor";
11360
- DeviceRole["TamperSensor"] = "tamper-sensor";
11361
- DeviceRole["VibrationSensor"] = "vibration-sensor";
11362
- DeviceRole["ConnectivitySensor"] = "connectivity-sensor";
11363
- DeviceRole["SoundSensor"] = "sound-sensor";
11364
- /** Fallback for `binary_sensor` without a known `device_class`. */
11365
- DeviceRole["BinarySensor"] = "binary-sensor";
11366
- DeviceRole["TemperatureSensor"] = "temperature-sensor";
11367
- DeviceRole["HumiditySensor"] = "humidity-sensor";
11368
- DeviceRole["AmbientLightSensor"] = "ambient-light-sensor";
11369
- DeviceRole["PressureSensor"] = "pressure-sensor";
11370
- /** Wind speed or direction (weather-station `wind-sensor` cap). */
11371
- DeviceRole["WindSensor"] = "wind-sensor";
11372
- /** Rain accumulation or rate (weather-station `rain-sensor` cap). */
11373
- DeviceRole["RainSensor"] = "rain-sensor";
11374
- /** UV index (weather-station `uv-sensor` cap). */
11375
- DeviceRole["UvSensor"] = "uv-sensor";
11376
- /** Solar irradiance W/m² (weather-station `solar-radiation-sensor` cap).
11377
- * Distinct from AmbientLightSensor (lux). */
11378
- DeviceRole["SolarRadiationSensor"] = "solar-radiation-sensor";
11379
- /** Soil moisture % (garden/weather `soil-moisture-sensor` cap). */
11380
- DeviceRole["SoilMoistureSensor"] = "soil-moisture-sensor";
11381
- DeviceRole["PowerSensor"] = "power-sensor";
11382
- DeviceRole["EnergySensor"] = "energy-sensor";
11383
- DeviceRole["VoltageSensor"] = "voltage-sensor";
11384
- DeviceRole["CurrentSensor"] = "current-sensor";
11385
- DeviceRole["AirQualitySensor"] = "air-quality-sensor";
11386
- /** Battery level (numeric % via `sensor` OR low-bool via
11387
- * `binary_sensor` — the cap distinguishes via the value type). */
11388
- DeviceRole["BatterySensor"] = "battery-sensor";
11389
- /** Fallback for `sensor` numeric without a known `device_class`. */
11390
- DeviceRole["NumericSensor"] = "numeric-sensor";
11391
- /** String / enum state (HA `sensor` with `state_class: enum` or
11392
- * `attributes.options`). */
11393
- DeviceRole["EnumSensor"] = "enum-sensor";
11394
- /** Date / timestamp state (HA `sensor` with `device_class: timestamp`
11395
- * or `date`). The slice carries the raw ISO string verbatim (hosted on
11396
- * the `enum-sensor` cap); the UI renders it locale-formatted. */
11397
- DeviceRole["DateTimeSensor"] = "datetime-sensor";
11398
- /** Last-resort fallback when nothing else matches. */
11399
- DeviceRole["GenericSensor"] = "generic-sensor";
11400
- DeviceRole["NumericControl"] = "numeric-control";
11401
- DeviceRole["SelectControl"] = "select-control";
11402
- DeviceRole["TextControl"] = "text-control";
11403
- DeviceRole["DateTimeControl"] = "datetime-control";
11404
- /** Mobile push notifier (HA `notify.mobile_app_*`) — supports
11405
- * rich features (image, priority, channel routing). */
11406
- DeviceRole["MobilePushNotifier"] = "mobile-push-notifier";
11407
- /** Chat / messaging service (HA `notify.telegram_*`,
11408
- * `notify.discord_*`, etc.). */
11409
- DeviceRole["MessagingNotifier"] = "messaging-notifier";
11410
- /** Email-based delivery (HA `notify.smtp`, etc.). */
11411
- DeviceRole["EmailNotifier"] = "email-notifier";
11412
- /** Fallback when the notifier service name doesn't match a known
11413
- * pattern. */
11414
- DeviceRole["GenericNotifier"] = "generic-notifier";
11415
- /** Emits a pulse train (infrared or RF) on request — the `ir-transmitter`
11416
- * cap. A `DeviceType.Generic` device. */
11417
- DeviceRole["IrTransmitter"] = "ir-transmitter";
11418
- return DeviceRole;
11419
- }({});
11420
- //#endregion
11421
- //#region ../types/dist/composition-C7WdThmP.mjs
11583
+ //#region ../types/dist/composition-BBzVf2xd.mjs
11422
11584
  /**
11423
11585
  * The ffmpeg build every CamStack node downloads, per platform and arch.
11424
11586
  *
@@ -12637,6 +12799,90 @@ var EventOwnerTypeSchema = _enum(EVENT_OWNER_TYPES);
12637
12799
  var MediaPresenceOwnerKindSchema = _enum([...EVENT_OWNER_TYPES, "track"]);
12638
12800
  new Set(EVENT_OWNER_TYPES);
12639
12801
  /**
12802
+ * What a published package needs from the rest of the install (D989).
12803
+ *
12804
+ * One published framework package (`@camstack/server`) carries the kernel, the
12805
+ * builtins and the capability contracts, so "which addon runs on which
12806
+ * framework" is a question of version, and the answer is DECLARED by the
12807
+ * package that knows it:
12808
+ *
12809
+ * - an addon says the oldest server it runs on (`requires.server: ">=X"`);
12810
+ * - the server says the oldest addon it works with
12811
+ * (`requires.addons: { "@camstack/addon-x": ">=Y" }`) and the oldest shell
12812
+ * around it (`requires.wrapper: { desktop, image }`);
12813
+ * - a wrapper (the desktop app, the container starter) says which server
12814
+ * range it can host, and reports its own kind and version.
12815
+ *
12816
+ * The publish workflow STAMPS these after the version bump
12817
+ * (`scripts/ci/stamp-requires.mjs`); nobody types a number by hand.
12818
+ *
12819
+ * A requirement that cannot be checked is NOT met: a shell that reports no
12820
+ * version counts as below the minimum, and an unreadable running version is
12821
+ * named in the result (a failed read is never a pass).
12822
+ */
12823
+ /** `>=MAJOR.MINOR.PATCH[-prerelease]` — the only range shape a requirement uses. */
12824
+ var MinVersionSchema = string().regex(/^>=\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/, "a minimum is written \">=X.Y.Z\"");
12825
+ object({
12826
+ /** The oldest `@camstack/server` this package runs on. */
12827
+ server: MinVersionSchema.optional(),
12828
+ /** The oldest installed version of each named addon package. */
12829
+ addons: record(string(), MinVersionSchema).optional(),
12830
+ /** The oldest shell around the server, per shell kind. */
12831
+ wrapper: object({
12832
+ desktop: MinVersionSchema.optional(),
12833
+ image: MinVersionSchema.optional()
12834
+ }).optional()
12835
+ });
12836
+ /** One unmet requirement; each names the versions on both sides. */
12837
+ var UnmetRequirementSchema = discriminatedUnion("kind", [
12838
+ object({
12839
+ kind: literal("server-too-old"),
12840
+ required: string(),
12841
+ /** `null` — the running server version could not be read. */
12842
+ running: string().nullable()
12843
+ }),
12844
+ object({
12845
+ kind: literal("addon-too-old"),
12846
+ packageName: string(),
12847
+ required: string(),
12848
+ /** `null` — the addon is not installed (or its version is unreadable). */
12849
+ running: string().nullable()
12850
+ }),
12851
+ object({
12852
+ kind: literal("wrapper-too-old"),
12853
+ wrapper: _enum(["desktop", "image"]),
12854
+ required: string(),
12855
+ /** `null` — the wrapper reported no version. */
12856
+ running: string().nullable()
12857
+ })
12858
+ ]);
12859
+ var RequiredAddonUpdateSchema = object({
12860
+ packageName: string(),
12861
+ /** The minimum the target server declares. */
12862
+ required: string(),
12863
+ /** What this node has; `null` when the addon is not installed. */
12864
+ installed: string().nullable()
12865
+ });
12866
+ /**
12867
+ * Which integration instance a settings form is for. An addon that serves several instances
12868
+ * (`instanceMode: multiple`) can show a different form per instance — an NVR integration
12869
+ * needs its connection, the default integration needs the addon's own sections.
12870
+ *
12871
+ * - `integrationId` is `null` in the create wizard: nothing exists yet, the operator is
12872
+ * setting up a NEW instance.
12873
+ * - `mode` is the wizard mode of the integration type (`getAvailableTypes`).
12874
+ * - `candidateStableId` is the discovery candidate a wizard was seeded from, if any.
12875
+ */
12876
+ var IntegrationInstanceRefSchema = object({
12877
+ integrationId: string().nullable(),
12878
+ mode: _enum([
12879
+ "standalone",
12880
+ "account",
12881
+ "broker"
12882
+ ]),
12883
+ candidateStableId: string().optional()
12884
+ });
12885
+ /**
12640
12886
  * Build an `IAddonRouteProvider` from a list of routes. Implements
12641
12887
  * both the operator-facing `getRoutes` (returning route descriptors
12642
12888
  * minus the handlers, which can't cross JSON) and the framework-
@@ -13209,6 +13455,613 @@ var CameraSwitchGroupSchema = object({
13209
13455
  fetchedAt: number()
13210
13456
  });
13211
13457
  /**
13458
+ * Query filter for settings-store collections.
13459
+ */
13460
+ var QueryFilterSchema = object({
13461
+ where: record(string(), unknown()).optional(),
13462
+ whereIn: record(string(), array(unknown())).optional(),
13463
+ whereBetween: record(string(), tuple([unknown(), unknown()])).optional(),
13464
+ /** NULL-safe exclusion: matches rows whose field is NULL OR != the value. */
13465
+ whereNot: record(string(), unknown()).optional(),
13466
+ /** NULL-safe exclusion of a SET — `whereNot` for more than one value. An
13467
+ * empty list excludes nothing. See `QueryFilter.whereNotIn`. */
13468
+ whereNotIn: record(string(), array(unknown())).optional(),
13469
+ orderBy: object({
13470
+ field: string(),
13471
+ direction: _enum(["asc", "desc"])
13472
+ }).optional(),
13473
+ limit: number().optional(),
13474
+ offset: number().optional()
13475
+ });
13476
+ /**
13477
+ * The predicate half of a filter, for BULK MUTATIONS.
13478
+ *
13479
+ * Deliberately not `QueryFilterSchema`: `orderBy` / `limit` / `offset` have no
13480
+ * meaning for a statement that rewrites a set, and accepting them would invite
13481
+ * a caller to believe `limit` bounds the damage. Every field is optional here
13482
+ * only so the shape stays composable — the implementation REJECTS a filter
13483
+ * that compiles to no predicate, because that is the whole collection.
13484
+ */
13485
+ var MutationFilterSchema = object({
13486
+ where: record(string(), unknown()).optional(),
13487
+ whereIn: record(string(), array(unknown())).optional(),
13488
+ whereBetween: record(string(), tuple([unknown(), unknown()])).optional(),
13489
+ whereNot: record(string(), unknown()).optional(),
13490
+ whereNotIn: record(string(), array(unknown())).optional()
13491
+ });
13492
+ /**
13493
+ * One scalar an {@link settingsStoreCapability.methods.aggregate} call asks for.
13494
+ *
13495
+ * `as` names the slot in the result, so the SAME column may be asked twice with
13496
+ * two operations (`MIN(startMs)` and `MAX(startMs)` in one round trip) — which
13497
+ * a `Record<column, op>` shape could not express.
13498
+ */
13499
+ var AggregateFieldSchema = object({
13500
+ /** Result key. */
13501
+ as: string().min(1),
13502
+ /** Column to aggregate. Must be a real column of a declared collection. */
13503
+ field: string().min(1),
13504
+ op: _enum([
13505
+ "sum",
13506
+ "min",
13507
+ "max"
13508
+ ])
13509
+ });
13510
+ /**
13511
+ * `COUNT(*)` plus one number per requested field.
13512
+ *
13513
+ * `null` means NO ROW MATCHED, never zero: a `SUM` over an empty set and a sum
13514
+ * that really is 0 are different facts, and an accounting caller that renders
13515
+ * "0 bytes, oldest = 0" for "nothing here" reports a lie about a disk.
13516
+ */
13517
+ var AggregateResultSchema = object({
13518
+ count: number().int(),
13519
+ values: record(string(), number().nullable())
13520
+ });
13521
+ /** A single stored record: `{ id, data }`. */
13522
+ var SettingsRecordSchema = object({
13523
+ id: string(),
13524
+ data: record(string(), unknown())
13525
+ });
13526
+ /**
13527
+ * One record of a BULK insert — {@link SettingsRecordSchema} with the id made
13528
+ * optional.
13529
+ *
13530
+ * A separate schema rather than loosening the shared one: every other method
13531
+ * on this cap addresses a row BY its id, and making that field optional
13532
+ * everywhere would turn a forgotten key into a silently generated one on
13533
+ * `update` and `delete` as well.
13534
+ */
13535
+ var BulkRecordSchema = object({
13536
+ id: string().optional(),
13537
+ data: record(string(), unknown())
13538
+ });
13539
+ /**
13540
+ * The database FILES one `sqlite-settings` engine process owns (D851).
13541
+ * Closed on purpose: a file is a thing the engine opens, backs up and gives a
13542
+ * serving thread, so a new one is a code change, never a string a caller
13543
+ * invents. `settings` is `camstack.db` and the default for every collection
13544
+ * that names no file.
13545
+ */
13546
+ var DataStoreDatabaseSchema = _enum([
13547
+ "settings",
13548
+ "events",
13549
+ "history"
13550
+ ]);
13551
+ DataStoreDatabaseSchema.options;
13552
+ /**
13553
+ * One write inside a {@link settingsStoreCapability.methods.transact} call.
13554
+ * The three that a "replace this set, bump that revision" write needs, plus
13555
+ * the batched upsert a rollup writer needs.
13556
+ */
13557
+ var TransactOpSchema = discriminatedUnion("op", [
13558
+ object({
13559
+ op: literal("deleteWhere"),
13560
+ collection: string(),
13561
+ filter: MutationFilterSchema
13562
+ }),
13563
+ object({
13564
+ op: literal("insertMany"),
13565
+ collection: string(),
13566
+ records: array(BulkRecordSchema).readonly()
13567
+ }),
13568
+ object({
13569
+ op: literal("update"),
13570
+ collection: string(),
13571
+ id: string(),
13572
+ data: record(string(), unknown())
13573
+ }),
13574
+ object({
13575
+ op: literal("upsertMany"),
13576
+ collection: string(),
13577
+ conflict: array(string()).readonly(),
13578
+ records: array(BulkRecordSchema).readonly()
13579
+ })
13580
+ ]);
13581
+ /**
13582
+ * The precondition of a transaction: the row `key` of `collection` must hold
13583
+ * `equals` in `column`, read INSIDE the transaction.
13584
+ */
13585
+ var TransactGuardSchema = object({
13586
+ collection: string(),
13587
+ key: string(),
13588
+ column: string(),
13589
+ equals: unknown()
13590
+ });
13591
+ /**
13592
+ * Column declaration for a structured (SQL-backed) collection.
13593
+ *
13594
+ * Logical types — the backend translates each to the matching SQLite
13595
+ * storage class and handles per-type marshaling:
13596
+ * - `TEXT` / `INTEGER` / `REAL` — native SQLite types, pass-through
13597
+ * - `JSON` — TEXT under the hood; serialised on write, parsed on read
13598
+ * - `BOOLEAN` — INTEGER 0/1 under the hood; coerced both directions
13599
+ */
13600
+ var CollectionColumnSchema = object({
13601
+ name: string(),
13602
+ type: _enum([
13603
+ "TEXT",
13604
+ "INTEGER",
13605
+ "REAL",
13606
+ "JSON",
13607
+ "BOOLEAN"
13608
+ ]),
13609
+ primaryKey: boolean().optional(),
13610
+ notNull: boolean().optional(),
13611
+ unique: boolean().optional(),
13612
+ /**
13613
+ * Column DEFAULT. Required for B2: the four `ensureTable` consumers declare
13614
+ * columns like `enabled INTEGER NOT NULL DEFAULT 1`, and without this the
13615
+ * collection surface simply cannot express their existing tables — which is
13616
+ * why they were still on the uncapped `table*` API.
13617
+ */
13618
+ defaultValue: union([
13619
+ string(),
13620
+ number(),
13621
+ boolean()
13622
+ ]).optional()
13623
+ });
13624
+ var CollectionIndexSchema = object({
13625
+ name: string(),
13626
+ columns: array(string()).readonly(),
13627
+ unique: boolean().optional()
13628
+ });
13629
+ /**
13630
+ * The input of `declareCollection` — ONE schema for the door and for the
13631
+ * engine behind it, so the two surfaces cannot drift (the dispatcher forwards
13632
+ * the payload untouched).
13633
+ *
13634
+ * - `database` — the file the collection lives in (D851). Absent means
13635
+ * `settings`. A declaration naming a file other than the one the table is
13636
+ * already in is REFUSED by the engine, never moved.
13637
+ * - `primaryKey` — a COMPOSITE key, as column names. A single name is the same
13638
+ * as `primaryKey: true` on that column. A composite-keyed collection has no
13639
+ * `id`: it is written with `insertMany` / `upsertMany` / `transact` and read
13640
+ * with `query` / `count` / `aggregate` / `deleteWhere`; the calls that
13641
+ * address one row by id are refused.
13642
+ * - `withoutRowid` — emit `WITHOUT ROWID`. Requires a primary key.
13643
+ */
13644
+ var DeclareCollectionInputSchema = object({
13645
+ namespace: string().optional(),
13646
+ collection: string(),
13647
+ columns: array(CollectionColumnSchema).readonly(),
13648
+ indexes: array(CollectionIndexSchema).readonly().optional(),
13649
+ database: DataStoreDatabaseSchema.optional(),
13650
+ primaryKey: array(string()).readonly().optional(),
13651
+ withoutRowid: boolean().optional()
13652
+ });
13653
+ method(object({
13654
+ namespace: string().optional(),
13655
+ collection: string(),
13656
+ key: string()
13657
+ }), unknown(), { auth: "admin" }), method(object({
13658
+ namespace: string().optional(),
13659
+ collection: string(),
13660
+ key: string(),
13661
+ value: unknown()
13662
+ }), _void(), {
13663
+ kind: "mutation",
13664
+ auth: "admin"
13665
+ }), method(object({
13666
+ namespace: string().optional(),
13667
+ collection: string(),
13668
+ filter: QueryFilterSchema.optional(),
13669
+ columns: array(string()).readonly().optional()
13670
+ }), array(SettingsRecordSchema).readonly(), { auth: "admin" }), method(object({
13671
+ namespace: string().optional(),
13672
+ collection: string(),
13673
+ /** Declared columns identifying the slot; must be covered by a UNIQUE
13674
+ * index on the collection. Never empty. */
13675
+ conflict: array(string()).readonly(),
13676
+ record: BulkRecordSchema
13677
+ }), object({
13678
+ /** The id the row HAS: assigned by SQLite on an insert, or the existing
13679
+ * row's own id on an update. */
13680
+ id: union([string(), number()]) }), {
13681
+ kind: "mutation",
13682
+ auth: "admin"
13683
+ }), method(object({
13684
+ namespace: string().optional(),
13685
+ collection: string(),
13686
+ conflict: array(string()).readonly(),
13687
+ records: array(BulkRecordSchema).readonly()
13688
+ }), object({ upserted: number().int() }), {
13689
+ kind: "mutation",
13690
+ auth: "admin"
13691
+ }), method(object({
13692
+ namespace: string().optional(),
13693
+ collection: string(),
13694
+ record: object({
13695
+ id: string().optional(),
13696
+ data: record(string(), unknown())
13697
+ })
13698
+ }), object({
13699
+ /**
13700
+ * The id the row ACTUALLY got (D473): the one supplied, the UUID minted
13701
+ * for an absent one, or the ROWID SQLite assigned on an `INTEGER`
13702
+ * primary key — which is the only place an auto key is knowable.
13703
+ */
13704
+ id: union([string(), number()]) }), {
13705
+ kind: "mutation",
13706
+ auth: "admin"
13707
+ }), method(object({
13708
+ namespace: string().optional(),
13709
+ collection: string(),
13710
+ records: array(BulkRecordSchema).readonly()
13711
+ }), object({ inserted: number().int() }), {
13712
+ kind: "mutation",
13713
+ auth: "admin"
13714
+ }), method(object({
13715
+ namespace: string().optional(),
13716
+ guard: TransactGuardSchema.optional(),
13717
+ ops: array(TransactOpSchema).readonly()
13718
+ }), object({
13719
+ applied: boolean(),
13720
+ current: unknown().optional()
13721
+ }), {
13722
+ kind: "mutation",
13723
+ auth: "admin"
13724
+ }), method(object({
13725
+ namespace: string().optional(),
13726
+ collection: string(),
13727
+ id: string(),
13728
+ data: record(string(), unknown())
13729
+ }), _void(), {
13730
+ kind: "mutation",
13731
+ auth: "admin"
13732
+ }), method(object({
13733
+ namespace: string().optional(),
13734
+ collection: string(),
13735
+ key: string()
13736
+ }), _void(), {
13737
+ kind: "mutation",
13738
+ auth: "admin"
13739
+ }), method(object({
13740
+ namespace: string().optional(),
13741
+ collection: string(),
13742
+ filter: MutationFilterSchema
13743
+ }), object({ deleted: number().int() }), {
13744
+ kind: "mutation",
13745
+ auth: "admin"
13746
+ }), method(object({
13747
+ namespace: string().optional(),
13748
+ collection: string(),
13749
+ filter: MutationFilterSchema,
13750
+ data: record(string(), unknown())
13751
+ }), object({ updated: number().int() }), {
13752
+ kind: "mutation",
13753
+ auth: "admin"
13754
+ }), method(object({
13755
+ namespace: string().optional(),
13756
+ collection: string(),
13757
+ filter: QueryFilterSchema.optional()
13758
+ }), number(), { auth: "admin" }), method(object({
13759
+ namespace: string().optional(),
13760
+ collection: string(),
13761
+ fields: array(AggregateFieldSchema).readonly(),
13762
+ filter: QueryFilterSchema.optional()
13763
+ }), AggregateResultSchema, { auth: "admin" }), method(object({
13764
+ namespace: string().optional(),
13765
+ collection: string(),
13766
+ field: string(),
13767
+ bucketSize: number().int().positive(),
13768
+ origin: number().int(),
13769
+ filter: QueryFilterSchema.optional()
13770
+ }), array(object({
13771
+ bucket: number().int(),
13772
+ count: number().int()
13773
+ })).readonly(), { auth: "admin" }), method(object({
13774
+ namespace: string().optional(),
13775
+ collection: string()
13776
+ }), boolean(), { auth: "admin" }), method(DeclareCollectionInputSchema, _void(), {
13777
+ kind: "mutation",
13778
+ auth: "admin"
13779
+ });
13780
+ 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>\"");
13781
+ var MigrationLevelSchema = _enum([
13782
+ "hub-store",
13783
+ "addon-state",
13784
+ "node-config",
13785
+ "viewer",
13786
+ "addon-internal"
13787
+ ]);
13788
+ var MigrationStoreSchema = _enum([
13789
+ "settings",
13790
+ "events",
13791
+ "history",
13792
+ "node-local",
13793
+ "addon-files"
13794
+ ]);
13795
+ var MigrationScopeSchema = _enum(["cluster", "per-node"]);
13796
+ /** What depends on a migration while it is pending: `block-owner` is the default. */
13797
+ var MigrationOnFailureSchema = _enum(["block-owner", "tolerate"]);
13798
+ /**
13799
+ * `sql` is a hub-only body over the owner's connection. `plan` returns edits the
13800
+ * engine applies inside the migration's own transaction (hub-owned: computed in
13801
+ * that transaction; addon-owned: computed by the owner and shipped to the engine).
13802
+ * `procedure` is addon-owned work that is not SQL (keychain, integration registry,
13803
+ * device tombstones); the ledger records its outcome and it must be idempotent.
13804
+ */
13805
+ var MigrationBodyKindSchema = _enum([
13806
+ "sql",
13807
+ "plan",
13808
+ "procedure"
13809
+ ]);
13810
+ var MigrationMetaSchema = object({
13811
+ id: MigrationIdSchema,
13812
+ /** `hub`, an addon id, `node` or `viewer`. */
13813
+ owner: string().regex(/^[a-z0-9-]+$/),
13814
+ level: MigrationLevelSchema,
13815
+ /** Strictly increasing per owner: the order within an owner. */
13816
+ seq: number().int().positive(),
13817
+ /** Ids of any owner: the order across owners. */
13818
+ dependsOn: array(MigrationIdSchema),
13819
+ store: MigrationStoreSchema,
13820
+ scope: MigrationScopeSchema,
13821
+ onFailure: MigrationOnFailureSchema,
13822
+ bodyKind: MigrationBodyKindSchema,
13823
+ description: string().min(1),
13824
+ /** The marked field the migration's spec must assert survived. */
13825
+ fixtureMark: string().min(1)
13826
+ }).refine((m) => m.id.startsWith(`${m.owner}/`), {
13827
+ message: "the id must start with \"<owner>/\"",
13828
+ path: ["id"]
13829
+ });
13830
+ /** A cycle in the dependency graph, named by the ids on it. */
13831
+ var MigrationCycleError = class extends Error {
13832
+ ids;
13833
+ constructor(ids) {
13834
+ super(`migration dependsOn cycle: ${ids.join(" -> ")}`);
13835
+ this.ids = ids;
13836
+ this.name = "MigrationCycleError";
13837
+ }
13838
+ };
13839
+ /**
13840
+ * Topological order over `dependsOn` plus the implicit edge "a lower `seq` of an
13841
+ * owner runs first". Ties are broken by `(owner, seq)`. Throws
13842
+ * {@link MigrationCycleError} on a cycle. A `dependsOn` id that is not in the list
13843
+ * is ignored here — the catalogue schema is what refuses it.
13844
+ */
13845
+ function orderMigrations(items) {
13846
+ const byId = new Map(items.map((item) => [item.id, item]));
13847
+ const deps = /* @__PURE__ */ new Map();
13848
+ const ownerSorted = /* @__PURE__ */ new Map();
13849
+ for (const item of items) {
13850
+ const list = ownerSorted.get(item.owner) ?? [];
13851
+ list.push(item);
13852
+ ownerSorted.set(item.owner, list);
13853
+ }
13854
+ for (const item of items) deps.set(item.id, new Set(item.dependsOn.filter((id) => byId.has(id))));
13855
+ for (const list of ownerSorted.values()) {
13856
+ list.sort((a, b) => a.seq - b.seq);
13857
+ for (let i = 1; i < list.length; i++) {
13858
+ const previous = list[i - 1];
13859
+ const current = list[i];
13860
+ if (previous !== void 0 && current !== void 0) deps.get(current.id)?.add(previous.id);
13861
+ }
13862
+ }
13863
+ const tie = (a, b) => a.owner < b.owner ? -1 : a.owner > b.owner ? 1 : a.seq - b.seq;
13864
+ const done = /* @__PURE__ */ new Set();
13865
+ const ordered = [];
13866
+ let remaining = [...items];
13867
+ while (remaining.length > 0) {
13868
+ const ready = remaining.filter((item) => [...deps.get(item.id) ?? []].every((dep) => done.has(dep)));
13869
+ if (ready.length === 0) throw new MigrationCycleError(remaining.map((item) => item.id).sort());
13870
+ ready.sort(tie);
13871
+ const next = ready[0];
13872
+ if (next === void 0) break;
13873
+ ordered.push(next);
13874
+ done.add(next.id);
13875
+ remaining = remaining.filter((item) => item.id !== next.id);
13876
+ }
13877
+ return ordered;
13878
+ }
13879
+ /** Every way a list of migrations (in declaration order) is not a catalogue. */
13880
+ function catalogueIssues(items) {
13881
+ const issues = [];
13882
+ const seen = /* @__PURE__ */ new Set();
13883
+ const known = new Set(items.map((item) => item.id));
13884
+ const lastSeq = /* @__PURE__ */ new Map();
13885
+ const seqUsed = /* @__PURE__ */ new Map();
13886
+ for (const item of items) {
13887
+ if (seen.has(item.id)) issues.push({
13888
+ code: "duplicate-id",
13889
+ ids: [item.id],
13890
+ message: `duplicate id ${item.id}`
13891
+ });
13892
+ seen.add(item.id);
13893
+ const used = seqUsed.get(item.owner) ?? /* @__PURE__ */ new Set();
13894
+ if (used.has(item.seq)) issues.push({
13895
+ code: "duplicate-seq",
13896
+ ids: [item.id],
13897
+ message: `owner ${item.owner} has two migrations with seq ${item.seq}`
13898
+ });
13899
+ else if (item.seq < (lastSeq.get(item.owner) ?? 0)) issues.push({
13900
+ code: "seq-order",
13901
+ ids: [item.id],
13902
+ message: `seq ${item.seq} of ${item.id} does not increase in declaration order (owner ${item.owner})`
13903
+ });
13904
+ used.add(item.seq);
13905
+ seqUsed.set(item.owner, used);
13906
+ lastSeq.set(item.owner, Math.max(item.seq, lastSeq.get(item.owner) ?? 0));
13907
+ for (const dep of item.dependsOn) if (!known.has(dep)) issues.push({
13908
+ code: "unknown-dependency",
13909
+ ids: [item.id, dep],
13910
+ message: `${item.id} dependsOn unknown id ${dep}`
13911
+ });
13912
+ }
13913
+ try {
13914
+ orderMigrations(items);
13915
+ } catch (err) {
13916
+ if (err instanceof MigrationCycleError) issues.push({
13917
+ code: "cycle",
13918
+ ids: err.ids,
13919
+ message: err.message
13920
+ });
13921
+ else throw err;
13922
+ }
13923
+ return issues;
13924
+ }
13925
+ array(MigrationMetaSchema).superRefine((items, ctx) => {
13926
+ for (const issue of catalogueIssues(items)) ctx.addIssue({
13927
+ code: "custom",
13928
+ message: `${issue.code}: ${issue.message}`
13929
+ });
13930
+ });
13931
+ /**
13932
+ * The canonical JSON (`preconditionOf`) of the value a plan read; the apply step
13933
+ * re-reads, and a plan whose read no longer matches is refused (the migration is
13934
+ * `failed`, nothing it edited is kept).
13935
+ */
13936
+ var PreconditionSchema = string().min(1).optional();
13937
+ var MigrationPlanSchema = object({ edits: array(discriminatedUnion("op", [
13938
+ object({
13939
+ op: literal("setKeys"),
13940
+ scope: string().min(1),
13941
+ values: record(string(), unknown()),
13942
+ precondition: PreconditionSchema
13943
+ }),
13944
+ object({
13945
+ op: literal("deleteKeys"),
13946
+ scope: string().min(1),
13947
+ keys: array(string().min(1)).min(1),
13948
+ precondition: PreconditionSchema
13949
+ }),
13950
+ object({
13951
+ op: literal("putRow"),
13952
+ table: string().min(1),
13953
+ key: record(string(), unknown()),
13954
+ row: record(string(), unknown()),
13955
+ precondition: PreconditionSchema
13956
+ }),
13957
+ object({
13958
+ op: literal("deleteRow"),
13959
+ table: string().min(1),
13960
+ key: record(string(), unknown()),
13961
+ precondition: PreconditionSchema
13962
+ }),
13963
+ object({
13964
+ op: literal("upsertRecord"),
13965
+ kind: string().min(1),
13966
+ recordKey: string().min(1),
13967
+ value: unknown(),
13968
+ precondition: PreconditionSchema
13969
+ }),
13970
+ object({
13971
+ op: literal("deleteRecord"),
13972
+ kind: string().min(1),
13973
+ recordKey: string().min(1),
13974
+ precondition: PreconditionSchema
13975
+ })
13976
+ ])) });
13977
+ /**
13978
+ * What a `procedure` body reports. `already-done` means the rerun observed the
13979
+ * work finished (a crash between the work and the ledger row, or a manual fix)
13980
+ * and did none of it again; the ledger records `applied` either way.
13981
+ */
13982
+ var MigrationProcedureResultSchema = object({
13983
+ outcome: _enum(["done", "already-done"]),
13984
+ summary: record(string(), unknown())
13985
+ });
13986
+ /** What an owner hands the engine when one of its migrations ran. */
13987
+ var MigrationOwnerResultSchema = discriminatedUnion("kind", [
13988
+ object({
13989
+ kind: literal("plan"),
13990
+ plan: MigrationPlanSchema
13991
+ }),
13992
+ object({
13993
+ kind: literal("procedure"),
13994
+ result: MigrationProcedureResultSchema
13995
+ }),
13996
+ object({
13997
+ kind: literal("failed"),
13998
+ error: string().min(1)
13999
+ })
14000
+ ]);
14001
+ /**
14002
+ * `held`: a restore of a pre-migration snapshot undid this id; it does not rerun
14003
+ * until the operator retries or a new code version ships.
14004
+ * `skipped-newer`: the database knows an id this code does not.
14005
+ */
14006
+ var MigrationStatusSchema = _enum([
14007
+ "applied",
14008
+ "failed",
14009
+ "blocked",
14010
+ "running",
14011
+ "held",
14012
+ "skipped-newer"
14013
+ ]);
14014
+ var MigrationLedgerEntrySchema = object({
14015
+ id: string(),
14016
+ database: DataStoreDatabaseSchema,
14017
+ owner: string(),
14018
+ level: MigrationLevelSchema,
14019
+ seq: number().int().nonnegative(),
14020
+ status: MigrationStatusSchema,
14021
+ attempts: number().int().nonnegative(),
14022
+ appliedAt: number().nullable(),
14023
+ codeVersion: string().nullable(),
14024
+ /** What the apply changed: rows per table, as the engine counted them. */
14025
+ planSummary: record(string(), unknown()).nullable(),
14026
+ progress: record(string(), unknown()).nullable(),
14027
+ error: string().nullable(),
14028
+ /** Why it is `blocked` or `held` (`dependency-failed`, `snapshot-refused`, …). */
14029
+ reason: string().nullable(),
14030
+ nodeId: string().nullable(),
14031
+ bootId: string().nullable()
14032
+ });
14033
+ /** A catalogued migration that is not applied, with where it stands. */
14034
+ var MigrationPendingEntrySchema = object({
14035
+ meta: MigrationMetaSchema,
14036
+ database: DataStoreDatabaseSchema,
14037
+ state: _enum([
14038
+ "pending",
14039
+ "failed",
14040
+ "blocked",
14041
+ "held"
14042
+ ]),
14043
+ reason: string().nullable()
14044
+ });
14045
+ /** Rows per table before and after a dry run, and what it would have changed. */
14046
+ var MigrationDryRunResultSchema = object({
14047
+ id: string(),
14048
+ ok: boolean(),
14049
+ error: string().nullable(),
14050
+ rowsChanged: number().int().nonnegative(),
14051
+ tables: array(object({
14052
+ table: string(),
14053
+ before: number().int().nonnegative(),
14054
+ after: number().int().nonnegative()
14055
+ }))
14056
+ });
14057
+ /** One of an owner's migrations, as the engine tells the owner to treat it this boot. */
14058
+ var MigrationOwnerStepSchema = object({
14059
+ id: MigrationIdSchema,
14060
+ action: _enum(["run", "skip"]),
14061
+ /** The ledger state the engine found: `pending` has no row yet. */
14062
+ state: union([literal("pending"), MigrationStatusSchema])
14063
+ });
14064
+ /**
13212
14065
  * The trainer contract — what the hub hands the cloud trainer and what it
13213
14066
  * gets back (cloud training, D805+).
13214
14067
  *
@@ -16611,6 +17464,41 @@ var ACTUATOR_CAP_VOCABULARY = {
16611
17464
  icon: "droplets"
16612
17465
  }]
16613
17466
  },
17467
+ "power-source": {
17468
+ scope: "device",
17469
+ noun: t("Power source", "Alimentazione"),
17470
+ actions: [
17471
+ {
17472
+ id: "powerSource.on",
17473
+ method: "powerOn",
17474
+ label: t("Power on", "Dai corrente"),
17475
+ phrase: t("Power on {device}", "Dai corrente a {device}"),
17476
+ icon: "power"
17477
+ },
17478
+ {
17479
+ id: "powerSource.off",
17480
+ method: "powerOff",
17481
+ label: t("Power off", "Togli corrente"),
17482
+ phrase: t("Cut the power of {device}", "Togli la corrente a {device}"),
17483
+ icon: "power-off"
17484
+ },
17485
+ {
17486
+ id: "powerSource.forceRestart",
17487
+ method: "forceRestart",
17488
+ label: t("Force restart", "Riavvio forzato"),
17489
+ phrase: t("Force restart {device} by cutting its power", "Riavvia {device} togliendo la corrente"),
17490
+ icon: "refresh-cw"
17491
+ }
17492
+ ],
17493
+ state: [{
17494
+ kind: "boolean",
17495
+ field: "switchOn",
17496
+ label: t("Power switch on", "Presa accesa"),
17497
+ whenTrue: t("When the power of {device} comes on", "Quando arriva la corrente a {device}"),
17498
+ whenFalse: t("When the power of {device} is cut", "Quando manca la corrente a {device}"),
17499
+ icon: "power"
17500
+ }]
17501
+ },
16614
17502
  "lock-control": {
16615
17503
  scope: "device",
16616
17504
  noun: t("Lock", "Serratura"),
@@ -16692,6 +17580,11 @@ var ACTUATOR_CAP_VOCABULARY = {
16692
17580
  value: "jammed",
16693
17581
  label: t("Jammed", "Inceppata"),
16694
17582
  phrase: t("When {device} jams", "Quando {device} si inceppa")
17583
+ },
17584
+ {
17585
+ value: "door-open",
17586
+ label: t("Door open", "Porta aperta"),
17587
+ phrase: t("When the door of {device} opens", "Quando la porta di {device} si apre")
16695
17588
  }
16696
17589
  ],
16697
17590
  icon: "lock"
@@ -17081,7 +17974,7 @@ var ACTUATOR_CAP_VOCABULARY = {
17081
17974
  method: "setDirection",
17082
17975
  label: t("Spin forward", "Rotazione avanti"),
17083
17976
  phrase: t("Spin {device} forward", "Fai girare {device} in avanti"),
17084
- icon: "rotate-cw",
17977
+ icon: "refresh-cw",
17085
17978
  effect: {
17086
17979
  field: "direction",
17087
17980
  value: "forward"
@@ -20159,6 +21052,7 @@ var SYSTEM_CAP_VOCABULARY = {
20159
21052
  "login-method": system(IDENTITY),
20160
21053
  "mesh-network": system(NETWORK),
20161
21054
  "metrics-provider": system(CLUSTER),
21055
+ migrations: system(INTERNAL),
20162
21056
  "model-convert": system(ML),
20163
21057
  "model-distributor": system(ML),
20164
21058
  "mqtt-broker": system(NETWORK),
@@ -42464,7 +43358,7 @@ var notificationOutputCapability = {
42464
43358
  * rebuilds without manual reload.
42465
43359
  *
42466
43360
  * The hub-local builtin `addon-pages-aggregator` (see
42467
- * `@camstack/system/builtins/addon-pages-aggregator`) registers the
43361
+ * `@camstack/server/system/builtins/addon-pages-aggregator`) registers the
42468
43362
  * provider. Splitting the public aggregator from the raw collection
42469
43363
  * keeps both ends in codegen — there's no hand-written
42470
43364
  * `addon-pages.router.ts` wrapper anymore.
@@ -42554,7 +43448,7 @@ var addonRoutesCapability = {
42554
43448
  */
42555
43449
  invoke: method(InvokeRequestSchema, InvokeReplyEnvelopeSchema, { kind: "mutation" })
42556
43450
  },
42557
- /** BIG PLAN 2: declarative mount hint — read by `@camstack/system` `buildCapRouters`. */
43451
+ /** BIG PLAN 2: declarative mount hint — read by `@camstack/server/system` `buildCapRouters`. */
42558
43452
  mount: { kind: "skip" }
42559
43453
  };
42560
43454
  var ConfigTabDeclarationSchema = object({
@@ -42609,6 +43503,10 @@ method(object({
42609
43503
  }), method(object({
42610
43504
  addonId: string(),
42611
43505
  nodeId: string().optional()
43506
+ }), SettingsSchemaWithValuesSchema.nullable()), method(object({
43507
+ addonId: string(),
43508
+ nodeId: string().optional(),
43509
+ instance: IntegrationInstanceRefSchema
42612
43510
  }), SettingsSchemaWithValuesSchema.nullable()), method(object({
42613
43511
  addonId: string(),
42614
43512
  nodeId: string().optional()
@@ -42779,7 +43677,7 @@ method(_void(), array(WidgetMetadataSchema).readonly(), { auth: "admin" });
42779
43677
  * manual reload — same scheme used by `addon-pages`.
42780
43678
  *
42781
43679
  * The hub-local builtin `addon-widgets-aggregator` (see
42782
- * `@camstack/system/builtins/addon-widgets-aggregator`) registers the
43680
+ * `@camstack/server/system/builtins/addon-widgets-aggregator`) registers the
42783
43681
  * provider. Splitting the public aggregator from the raw collection
42784
43682
  * keeps both ends in codegen — there's no hand-written wrapper.
42785
43683
  */
@@ -43140,329 +44038,6 @@ method(object({
43140
44038
  password: string()
43141
44039
  }), AuthResultSchema.nullable(), { kind: "mutation" }), method(object({ state: string() }), string()), method(record(string(), string()), AuthResultSchema, { kind: "mutation" }), method(object({ token: string() }), AuthResultSchema.nullable());
43142
44040
  /**
43143
- * Query filter for settings-store collections.
43144
- */
43145
- var QueryFilterSchema = object({
43146
- where: record(string(), unknown()).optional(),
43147
- whereIn: record(string(), array(unknown())).optional(),
43148
- whereBetween: record(string(), tuple([unknown(), unknown()])).optional(),
43149
- /** NULL-safe exclusion: matches rows whose field is NULL OR != the value. */
43150
- whereNot: record(string(), unknown()).optional(),
43151
- /** NULL-safe exclusion of a SET — `whereNot` for more than one value. An
43152
- * empty list excludes nothing. See `QueryFilter.whereNotIn`. */
43153
- whereNotIn: record(string(), array(unknown())).optional(),
43154
- orderBy: object({
43155
- field: string(),
43156
- direction: _enum(["asc", "desc"])
43157
- }).optional(),
43158
- limit: number().optional(),
43159
- offset: number().optional()
43160
- });
43161
- /**
43162
- * The predicate half of a filter, for BULK MUTATIONS.
43163
- *
43164
- * Deliberately not `QueryFilterSchema`: `orderBy` / `limit` / `offset` have no
43165
- * meaning for a statement that rewrites a set, and accepting them would invite
43166
- * a caller to believe `limit` bounds the damage. Every field is optional here
43167
- * only so the shape stays composable — the implementation REJECTS a filter
43168
- * that compiles to no predicate, because that is the whole collection.
43169
- */
43170
- var MutationFilterSchema = object({
43171
- where: record(string(), unknown()).optional(),
43172
- whereIn: record(string(), array(unknown())).optional(),
43173
- whereBetween: record(string(), tuple([unknown(), unknown()])).optional(),
43174
- whereNot: record(string(), unknown()).optional(),
43175
- whereNotIn: record(string(), array(unknown())).optional()
43176
- });
43177
- /**
43178
- * One scalar an {@link settingsStoreCapability.methods.aggregate} call asks for.
43179
- *
43180
- * `as` names the slot in the result, so the SAME column may be asked twice with
43181
- * two operations (`MIN(startMs)` and `MAX(startMs)` in one round trip) — which
43182
- * a `Record<column, op>` shape could not express.
43183
- */
43184
- var AggregateFieldSchema = object({
43185
- /** Result key. */
43186
- as: string().min(1),
43187
- /** Column to aggregate. Must be a real column of a declared collection. */
43188
- field: string().min(1),
43189
- op: _enum([
43190
- "sum",
43191
- "min",
43192
- "max"
43193
- ])
43194
- });
43195
- /**
43196
- * `COUNT(*)` plus one number per requested field.
43197
- *
43198
- * `null` means NO ROW MATCHED, never zero: a `SUM` over an empty set and a sum
43199
- * that really is 0 are different facts, and an accounting caller that renders
43200
- * "0 bytes, oldest = 0" for "nothing here" reports a lie about a disk.
43201
- */
43202
- var AggregateResultSchema = object({
43203
- count: number().int(),
43204
- values: record(string(), number().nullable())
43205
- });
43206
- /** A single stored record: `{ id, data }`. */
43207
- var SettingsRecordSchema = object({
43208
- id: string(),
43209
- data: record(string(), unknown())
43210
- });
43211
- /**
43212
- * One record of a BULK insert — {@link SettingsRecordSchema} with the id made
43213
- * optional.
43214
- *
43215
- * A separate schema rather than loosening the shared one: every other method
43216
- * on this cap addresses a row BY its id, and making that field optional
43217
- * everywhere would turn a forgotten key into a silently generated one on
43218
- * `update` and `delete` as well.
43219
- */
43220
- var BulkRecordSchema = object({
43221
- id: string().optional(),
43222
- data: record(string(), unknown())
43223
- });
43224
- /**
43225
- * The database FILES one `sqlite-settings` engine process owns (D851).
43226
- * Closed on purpose: a file is a thing the engine opens, backs up and gives a
43227
- * serving thread, so a new one is a code change, never a string a caller
43228
- * invents. `settings` is `camstack.db` and the default for every collection
43229
- * that names no file.
43230
- */
43231
- var DataStoreDatabaseSchema = _enum([
43232
- "settings",
43233
- "events",
43234
- "history"
43235
- ]);
43236
- DataStoreDatabaseSchema.options;
43237
- /**
43238
- * One write inside a {@link settingsStoreCapability.methods.transact} call.
43239
- * The three that a "replace this set, bump that revision" write needs, plus
43240
- * the batched upsert a rollup writer needs.
43241
- */
43242
- var TransactOpSchema = discriminatedUnion("op", [
43243
- object({
43244
- op: literal("deleteWhere"),
43245
- collection: string(),
43246
- filter: MutationFilterSchema
43247
- }),
43248
- object({
43249
- op: literal("insertMany"),
43250
- collection: string(),
43251
- records: array(BulkRecordSchema).readonly()
43252
- }),
43253
- object({
43254
- op: literal("update"),
43255
- collection: string(),
43256
- id: string(),
43257
- data: record(string(), unknown())
43258
- }),
43259
- object({
43260
- op: literal("upsertMany"),
43261
- collection: string(),
43262
- conflict: array(string()).readonly(),
43263
- records: array(BulkRecordSchema).readonly()
43264
- })
43265
- ]);
43266
- /**
43267
- * The precondition of a transaction: the row `key` of `collection` must hold
43268
- * `equals` in `column`, read INSIDE the transaction.
43269
- */
43270
- var TransactGuardSchema = object({
43271
- collection: string(),
43272
- key: string(),
43273
- column: string(),
43274
- equals: unknown()
43275
- });
43276
- /**
43277
- * Column declaration for a structured (SQL-backed) collection.
43278
- *
43279
- * Logical types — the backend translates each to the matching SQLite
43280
- * storage class and handles per-type marshaling:
43281
- * - `TEXT` / `INTEGER` / `REAL` — native SQLite types, pass-through
43282
- * - `JSON` — TEXT under the hood; serialised on write, parsed on read
43283
- * - `BOOLEAN` — INTEGER 0/1 under the hood; coerced both directions
43284
- */
43285
- var CollectionColumnSchema = object({
43286
- name: string(),
43287
- type: _enum([
43288
- "TEXT",
43289
- "INTEGER",
43290
- "REAL",
43291
- "JSON",
43292
- "BOOLEAN"
43293
- ]),
43294
- primaryKey: boolean().optional(),
43295
- notNull: boolean().optional(),
43296
- unique: boolean().optional(),
43297
- /**
43298
- * Column DEFAULT. Required for B2: the four `ensureTable` consumers declare
43299
- * columns like `enabled INTEGER NOT NULL DEFAULT 1`, and without this the
43300
- * collection surface simply cannot express their existing tables — which is
43301
- * why they were still on the uncapped `table*` API.
43302
- */
43303
- defaultValue: union([
43304
- string(),
43305
- number(),
43306
- boolean()
43307
- ]).optional()
43308
- });
43309
- var CollectionIndexSchema = object({
43310
- name: string(),
43311
- columns: array(string()).readonly(),
43312
- unique: boolean().optional()
43313
- });
43314
- /**
43315
- * The input of `declareCollection` — ONE schema for the door and for the
43316
- * engine behind it, so the two surfaces cannot drift (the dispatcher forwards
43317
- * the payload untouched).
43318
- *
43319
- * - `database` — the file the collection lives in (D851). Absent means
43320
- * `settings`. A declaration naming a file other than the one the table is
43321
- * already in is REFUSED by the engine, never moved.
43322
- * - `primaryKey` — a COMPOSITE key, as column names. A single name is the same
43323
- * as `primaryKey: true` on that column. A composite-keyed collection has no
43324
- * `id`: it is written with `insertMany` / `upsertMany` / `transact` and read
43325
- * with `query` / `count` / `aggregate` / `deleteWhere`; the calls that
43326
- * address one row by id are refused.
43327
- * - `withoutRowid` — emit `WITHOUT ROWID`. Requires a primary key.
43328
- */
43329
- var DeclareCollectionInputSchema = object({
43330
- namespace: string().optional(),
43331
- collection: string(),
43332
- columns: array(CollectionColumnSchema).readonly(),
43333
- indexes: array(CollectionIndexSchema).readonly().optional(),
43334
- database: DataStoreDatabaseSchema.optional(),
43335
- primaryKey: array(string()).readonly().optional(),
43336
- withoutRowid: boolean().optional()
43337
- });
43338
- method(object({
43339
- namespace: string().optional(),
43340
- collection: string(),
43341
- key: string()
43342
- }), unknown(), { auth: "admin" }), method(object({
43343
- namespace: string().optional(),
43344
- collection: string(),
43345
- key: string(),
43346
- value: unknown()
43347
- }), _void(), {
43348
- kind: "mutation",
43349
- auth: "admin"
43350
- }), method(object({
43351
- namespace: string().optional(),
43352
- collection: string(),
43353
- filter: QueryFilterSchema.optional(),
43354
- columns: array(string()).readonly().optional()
43355
- }), array(SettingsRecordSchema).readonly(), { auth: "admin" }), method(object({
43356
- namespace: string().optional(),
43357
- collection: string(),
43358
- /** Declared columns identifying the slot; must be covered by a UNIQUE
43359
- * index on the collection. Never empty. */
43360
- conflict: array(string()).readonly(),
43361
- record: BulkRecordSchema
43362
- }), object({
43363
- /** The id the row HAS: assigned by SQLite on an insert, or the existing
43364
- * row's own id on an update. */
43365
- id: union([string(), number()]) }), {
43366
- kind: "mutation",
43367
- auth: "admin"
43368
- }), method(object({
43369
- namespace: string().optional(),
43370
- collection: string(),
43371
- conflict: array(string()).readonly(),
43372
- records: array(BulkRecordSchema).readonly()
43373
- }), object({ upserted: number().int() }), {
43374
- kind: "mutation",
43375
- auth: "admin"
43376
- }), method(object({
43377
- namespace: string().optional(),
43378
- collection: string(),
43379
- record: object({
43380
- id: string().optional(),
43381
- data: record(string(), unknown())
43382
- })
43383
- }), object({
43384
- /**
43385
- * The id the row ACTUALLY got (D473): the one supplied, the UUID minted
43386
- * for an absent one, or the ROWID SQLite assigned on an `INTEGER`
43387
- * primary key — which is the only place an auto key is knowable.
43388
- */
43389
- id: union([string(), number()]) }), {
43390
- kind: "mutation",
43391
- auth: "admin"
43392
- }), method(object({
43393
- namespace: string().optional(),
43394
- collection: string(),
43395
- records: array(BulkRecordSchema).readonly()
43396
- }), object({ inserted: number().int() }), {
43397
- kind: "mutation",
43398
- auth: "admin"
43399
- }), method(object({
43400
- namespace: string().optional(),
43401
- guard: TransactGuardSchema.optional(),
43402
- ops: array(TransactOpSchema).readonly()
43403
- }), object({
43404
- applied: boolean(),
43405
- current: unknown().optional()
43406
- }), {
43407
- kind: "mutation",
43408
- auth: "admin"
43409
- }), method(object({
43410
- namespace: string().optional(),
43411
- collection: string(),
43412
- id: string(),
43413
- data: record(string(), unknown())
43414
- }), _void(), {
43415
- kind: "mutation",
43416
- auth: "admin"
43417
- }), method(object({
43418
- namespace: string().optional(),
43419
- collection: string(),
43420
- key: string()
43421
- }), _void(), {
43422
- kind: "mutation",
43423
- auth: "admin"
43424
- }), method(object({
43425
- namespace: string().optional(),
43426
- collection: string(),
43427
- filter: MutationFilterSchema
43428
- }), object({ deleted: number().int() }), {
43429
- kind: "mutation",
43430
- auth: "admin"
43431
- }), method(object({
43432
- namespace: string().optional(),
43433
- collection: string(),
43434
- filter: MutationFilterSchema,
43435
- data: record(string(), unknown())
43436
- }), object({ updated: number().int() }), {
43437
- kind: "mutation",
43438
- auth: "admin"
43439
- }), method(object({
43440
- namespace: string().optional(),
43441
- collection: string(),
43442
- filter: QueryFilterSchema.optional()
43443
- }), number(), { auth: "admin" }), method(object({
43444
- namespace: string().optional(),
43445
- collection: string(),
43446
- fields: array(AggregateFieldSchema).readonly(),
43447
- filter: QueryFilterSchema.optional()
43448
- }), AggregateResultSchema, { auth: "admin" }), method(object({
43449
- namespace: string().optional(),
43450
- collection: string(),
43451
- field: string(),
43452
- bucketSize: number().int().positive(),
43453
- origin: number().int(),
43454
- filter: QueryFilterSchema.optional()
43455
- }), array(object({
43456
- bucket: number().int(),
43457
- count: number().int()
43458
- })).readonly(), { auth: "admin" }), method(object({
43459
- namespace: string().optional(),
43460
- collection: string()
43461
- }), boolean(), { auth: "admin" }), method(DeclareCollectionInputSchema, _void(), {
43462
- kind: "mutation",
43463
- auth: "admin"
43464
- });
43465
- /**
43466
44041
  * How long one snapshot may take end to end: the pinned copy of the whole
43467
44042
  * database plus `PRAGMA integrity_check` over the copy, on the hub's slowest
43468
44043
  * disk. ~800 MB at a pessimistic 10 MB/s is under 3 minutes; the bound is
@@ -45270,6 +45845,7 @@ method(object({
45270
45845
  kind: "mutation",
45271
45846
  auth: "admin"
45272
45847
  }), method(object({ blockIds: array(string()) }), object({ states: array(CompositionBlockStateSchema) }), { auth: "admin" });
45848
+ 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;
45273
45849
  var NUMBER = {
45274
45850
  kind: "fixed",
45275
45851
  result: ["number"]
@@ -46280,6 +46856,57 @@ id: union([string(), number()]) }), {
46280
46856
  kind: "mutation",
46281
46857
  auth: "admin"
46282
46858
  });
46859
+ 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({
46860
+ id: string().min(1),
46861
+ error: string().min(1)
46862
+ }), MigrationLedgerEntrySchema, {
46863
+ kind: "mutation",
46864
+ auth: "admin"
46865
+ }), method(object({ id: string().min(1) }), object({ entries: array(MigrationLedgerEntrySchema) }), {
46866
+ kind: "mutation",
46867
+ auth: "admin"
46868
+ }), method(object({
46869
+ database: DataStoreDatabaseSchema,
46870
+ snapshotId: string().min(1)
46871
+ }), object({
46872
+ scheduled: boolean(),
46873
+ held: array(string()),
46874
+ restartRequired: boolean()
46875
+ }), {
46876
+ kind: "mutation",
46877
+ auth: "admin"
46878
+ }), method(object({
46879
+ owner: string().min(1),
46880
+ metas: array(MigrationMetaSchema)
46881
+ }), object({
46882
+ bootId: string(),
46883
+ snapshot: _enum([
46884
+ "none",
46885
+ "written",
46886
+ "refused"
46887
+ ]),
46888
+ steps: array(MigrationOwnerStepSchema)
46889
+ }), {
46890
+ kind: "mutation",
46891
+ auth: "admin"
46892
+ }), method(object({ id: string().min(1) }), object({
46893
+ proceed: boolean(),
46894
+ state: union([literal("pending"), MigrationStatusSchema]),
46895
+ reason: string().nullable()
46896
+ }), {
46897
+ kind: "mutation",
46898
+ auth: "admin"
46899
+ }), method(object({
46900
+ id: string().min(1),
46901
+ result: MigrationOwnerResultSchema
46902
+ }), object({
46903
+ status: MigrationStatusSchema,
46904
+ error: string().nullable(),
46905
+ rowsChanged: number().int().nonnegative()
46906
+ }), {
46907
+ kind: "mutation",
46908
+ auth: "admin"
46909
+ });
46283
46910
  /**
46284
46911
  * records - an addon's own facts and documents (D881): ledgers, audit rows,
46285
46912
  * config documents, mirrors. The third storage API next to `ctx.settings`
@@ -60782,7 +61409,18 @@ var ServerUpdateActionResultSchema = object({
60782
61409
  * store (D877), present when the pre-flight refused the update
60783
61410
  * (`reason: 'addon-would-refuse'`). Optional for version skew.
60784
61411
  */
60785
- blockers: array(UpdateBlockerSchema).optional()
61412
+ blockers: array(UpdateBlockerSchema).optional(),
61413
+ /**
61414
+ * The minimums the target server declares and this node does not meet
61415
+ * (D989), present when the update was refused with
61416
+ * `reason: 'requirements-unmet'`. Each names the versions on both sides.
61417
+ */
61418
+ unmet: array(UnmetRequirementSchema).optional(),
61419
+ /**
61420
+ * The addons this node has below the minimum the staged server declares —
61421
+ * the ones to update WITH the server. Present on an accepted update.
61422
+ */
61423
+ requiredAddonUpdates: array(RequiredAddonUpdateSchema).optional()
60786
61424
  });
60787
61425
  var LaunchOverridesSchema = object({
60788
61426
  /** The persisted heap ceiling in MB, or `null` when none is set. */
@@ -63293,7 +63931,9 @@ var PackageUpdateSchema = object({
63293
63931
  latestVersion: string(),
63294
63932
  category: _enum(["addon", "core"]),
63295
63933
  requiresRestart: boolean(),
63296
- isSystem: boolean()
63934
+ isSystem: boolean(),
63935
+ /** Held: what the newer version needs and this node does not meet (D989). */
63936
+ blockedBy: array(UnmetRequirementSchema).readonly().optional()
63297
63937
  });
63298
63938
  var PackageVersionInfoSchema = object({
63299
63939
  version: string(),
@@ -64337,19 +64977,19 @@ var RemoteSetConfigResultSchema = discriminatedUnion("status", [object({ status:
64337
64977
  reason: _enum(["unknown-template", "not-a-media-player"]),
64338
64978
  detail: string()
64339
64979
  })]);
64340
- var deviceInput = object({ deviceId: number().int().nonnegative() });
64980
+ var deviceInput$1 = object({ deviceId: number().int().nonnegative() });
64341
64981
  DeviceType.MediaPlayer;
64342
- method(deviceInput, RemoteConfigSchema, { auth: "admin" }), method(deviceInput.extend({ config: RemoteConfigSchema }), RemoteSetConfigResultSchema, {
64982
+ method(deviceInput$1, RemoteConfigSchema, { auth: "admin" }), method(deviceInput$1.extend({ config: RemoteConfigSchema }), RemoteSetConfigResultSchema, {
64343
64983
  kind: "mutation",
64344
64984
  auth: "admin"
64345
- }), method(deviceInput, RemoteCommandListSchema, { auth: "protected" }), method(deviceInput.extend({
64985
+ }), method(deviceInput$1, RemoteCommandListSchema, { auth: "protected" }), method(deviceInput$1.extend({
64346
64986
  role: RemoteRoleSchema,
64347
64987
  /** Sends in a row (volume steps). */
64348
64988
  repeat: number().int().min(1).max(50).default(1)
64349
64989
  }), RemoteSendResultSchema, {
64350
64990
  kind: "mutation",
64351
64991
  auth: "protected"
64352
- }), method(deviceInput.extend({
64992
+ }), method(deviceInput$1.extend({
64353
64993
  command: RemoteMediaCommandSchema,
64354
64994
  muted: boolean().optional(),
64355
64995
  source: string().optional(),
@@ -66499,6 +67139,8 @@ var IntegrationWithStateSchema = object({
66499
67139
  name: string(),
66500
67140
  enabled: boolean(),
66501
67141
  info: record(string(), unknown()).readonly(),
67142
+ /** The discovery candidate this integration was set up from (D984). */
67143
+ originStableId: string().optional(),
66502
67144
  createdAt: number(),
66503
67145
  updatedAt: number(),
66504
67146
  processState: string()
@@ -66509,6 +67151,8 @@ var IntegrationLiteSchema = object({
66509
67151
  name: string(),
66510
67152
  enabled: boolean(),
66511
67153
  info: record(string(), unknown()).readonly(),
67154
+ /** The discovery candidate this integration was set up from (D984). */
67155
+ originStableId: string().optional(),
66512
67156
  createdAt: number(),
66513
67157
  updatedAt: number()
66514
67158
  });
@@ -66606,6 +67250,8 @@ var CreateIntegrationInputSchema = object({
66606
67250
  name: string(),
66607
67251
  enabled: boolean().optional(),
66608
67252
  info: record(string(), unknown()).optional(),
67253
+ /** `stableId` of the discovery candidate the form was seeded from (D984). */
67254
+ originStableId: string().optional(),
66609
67255
  settings: record(string(), unknown()).optional(),
66610
67256
  /** Keys of `settings` the form's schema declares secret (by type) — kept in
66611
67257
  * the keychain, not the table (D787). */
@@ -66826,7 +67472,7 @@ DeviceType.LawnMower, method(object({ deviceId: number().int().nonnegative() }),
66826
67472
  * clients — they reverse-connect to the hub. Exposing their interfaces
66827
67473
  * via the same surface would leak internal topology with no upside.
66828
67474
  *
66829
- * Implementation in `@camstack/system/builtins/local-network/`.
67475
+ * Implementation in `@camstack/server/system/builtins/local-network/`.
66830
67476
  */
66831
67477
  /** Coarse classification derived from the interface name + IP range. */
66832
67478
  var InterfaceKindEnum = _enum([
@@ -67223,11 +67869,13 @@ object({
67223
67869
  "unlocked",
67224
67870
  "locking",
67225
67871
  "unlocking",
67226
- "jammed"
67872
+ "jammed",
67873
+ "door-open"
67227
67874
  ]),
67228
67875
  /** Ms epoch when the slice was last updated. */
67229
67876
  lastChangedAt: number()
67230
67877
  });
67878
+ DeviceType.Lock;
67231
67879
  DeviceType.Lock, method(object({
67232
67880
  deviceId: number().int().nonnegative(),
67233
67881
  /** Optional PIN code required by some keypad locks. NOT
@@ -67253,6 +67901,49 @@ DeviceType.Lock, method(object({
67253
67901
  kind: "mutation",
67254
67902
  auth: "protected"
67255
67903
  });
67904
+ var PowerSourceRefusalReasonSchema = _enum([
67905
+ "no-power-source",
67906
+ "switch-missing",
67907
+ "switch-offline",
67908
+ "switch-unknown",
67909
+ "restart-in-progress",
67910
+ "switch-call-failed"
67911
+ ]);
67912
+ var PowerSourceActionResultSchema = discriminatedUnion("accepted", [object({ accepted: literal(true) }), object({
67913
+ accepted: literal(false),
67914
+ reason: PowerSourceRefusalReasonSchema,
67915
+ /** One line for the operator, naming the switch. */
67916
+ message: string()
67917
+ })]);
67918
+ var PowerSourceInfoSchema = object({
67919
+ /** False when the device has no power source linked; the rest is then empty. */
67920
+ linked: boolean(),
67921
+ switchDeviceId: number().nullable(),
67922
+ switchName: string().nullable(),
67923
+ /** The switch's on/off state, or null when it is unknown. Never false for unknown (D393). */
67924
+ switchOn: boolean().nullable(),
67925
+ switchOnline: boolean().nullable(),
67926
+ restartInProgress: boolean(),
67927
+ offSeconds: number().int()
67928
+ });
67929
+ object({
67930
+ /** Null while the switch has not reported: unknown is never `false` (D393). */
67931
+ switchOn: boolean().nullable(),
67932
+ offSeconds: number().int(),
67933
+ lastFetchedAt: number()
67934
+ });
67935
+ 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;
67936
+ var deviceInput = object({ deviceId: number().int().nonnegative() });
67937
+ systemMethod(deviceInput, PowerSourceInfoSchema, { auth: "admin" }), systemMethod(deviceInput, PowerSourceActionResultSchema, {
67938
+ kind: "mutation",
67939
+ auth: "admin"
67940
+ }), systemMethod(deviceInput, PowerSourceActionResultSchema, {
67941
+ kind: "mutation",
67942
+ auth: "admin"
67943
+ }), systemMethod(deviceInput, PowerSourceActionResultSchema, {
67944
+ kind: "mutation",
67945
+ auth: "admin"
67946
+ });
67256
67947
  object({
67257
67948
  value: number().nullable().meta(readingMeta("device", { label: "Total" })),
67258
67949
  lastFetchedAt: number()
@@ -68061,7 +68752,7 @@ DeviceType.Camera, DeviceType.Sensor, DeviceType.Button, DeviceType.Switch, Devi
68061
68752
  * jitter, and observed/peak bandwidth per device + per client.
68062
68753
  *
68063
68754
  * Implementation lives in the server's `NetworkQualityService` (thin
68064
- * wrapper over the shared `NetworkQualityTracker` from `@camstack/system`).
68755
+ * wrapper over the shared `NetworkQualityTracker` from `@camstack/server/system`).
68065
68756
  * The provider is registered from `trpc.router.ts` against the existing
68066
68757
  * service instance — no addon owns this state.
68067
68758
  *
@@ -73617,7 +74308,6 @@ discriminatedUnion("kind", [object({
73617
74308
  })]);
73618
74309
  DeviceFeature.FanSpeed, DeviceFeature.FanPreset, DeviceFeature.FanDirection, DeviceFeature.FanOscillating, DeviceFeature.CoverPositionable, DeviceFeature.CoverTilt, DeviceFeature.ValvePositionable, DeviceFeature.MediaPlayerVolume, DeviceFeature.ClimateFanMode;
73619
74310
  SOURCE_CAPS.filter((cap) => cap !== "switch" && cap !== "motion");
73620
- 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;
73621
74311
  String.raw`rose\(active(\d+), changed(\d+), (\d+(?:\.\d+)?)\)`;
73622
74312
  DeviceRole.BinarySensor, DeviceRole.MotionSensor;
73623
74313
  new Set(CAP_NAMES);
@@ -74079,6 +74769,13 @@ Object.freeze({
74079
74769
  access: "view",
74080
74770
  role: "admin"
74081
74771
  },
74772
+ "addonSettings.getIntegrationFormSchema": {
74773
+ capName: "addon-settings",
74774
+ capScope: "system",
74775
+ addonId: null,
74776
+ access: "view",
74777
+ role: "admin"
74778
+ },
74082
74779
  "addonSettings.getIntegrationSettings": {
74083
74780
  capName: "addon-settings",
74084
74781
  capScope: "system",
@@ -78167,6 +78864,69 @@ Object.freeze({
78167
78864
  access: "view",
78168
78865
  role: "admin"
78169
78866
  },
78867
+ "migrations.declareOwner": {
78868
+ capName: "migrations",
78869
+ capScope: "system",
78870
+ addonId: null,
78871
+ access: "create",
78872
+ role: "admin"
78873
+ },
78874
+ "migrations.dryRun": {
78875
+ capName: "migrations",
78876
+ capScope: "system",
78877
+ addonId: null,
78878
+ access: "view",
78879
+ role: "admin"
78880
+ },
78881
+ "migrations.finishMigration": {
78882
+ capName: "migrations",
78883
+ capScope: "system",
78884
+ addonId: null,
78885
+ access: "create",
78886
+ role: "admin"
78887
+ },
78888
+ "migrations.listLedger": {
78889
+ capName: "migrations",
78890
+ capScope: "system",
78891
+ addonId: null,
78892
+ access: "view",
78893
+ role: "admin"
78894
+ },
78895
+ "migrations.listPending": {
78896
+ capName: "migrations",
78897
+ capScope: "system",
78898
+ addonId: null,
78899
+ access: "view",
78900
+ role: "admin"
78901
+ },
78902
+ "migrations.markFailed": {
78903
+ capName: "migrations",
78904
+ capScope: "system",
78905
+ addonId: null,
78906
+ access: "create",
78907
+ role: "admin"
78908
+ },
78909
+ "migrations.restorePreMigration": {
78910
+ capName: "migrations",
78911
+ capScope: "system",
78912
+ addonId: null,
78913
+ access: "create",
78914
+ role: "admin"
78915
+ },
78916
+ "migrations.retry": {
78917
+ capName: "migrations",
78918
+ capScope: "system",
78919
+ addonId: null,
78920
+ access: "create",
78921
+ role: "admin"
78922
+ },
78923
+ "migrations.startMigration": {
78924
+ capName: "migrations",
78925
+ capScope: "system",
78926
+ addonId: null,
78927
+ access: "create",
78928
+ role: "admin"
78929
+ },
78170
78930
  "modelConvert.convert": {
78171
78931
  capName: "model-convert",
78172
78932
  capScope: "system",
@@ -80799,6 +81559,34 @@ Object.freeze({
80799
81559
  access: "view",
80800
81560
  role: "viewer"
80801
81561
  },
81562
+ "powerSource.forceRestart": {
81563
+ capName: "power-source",
81564
+ capScope: "device",
81565
+ addonId: null,
81566
+ access: "create",
81567
+ role: "admin"
81568
+ },
81569
+ "powerSource.getStatus": {
81570
+ capName: "power-source",
81571
+ capScope: "device",
81572
+ addonId: null,
81573
+ access: "view",
81574
+ role: "admin"
81575
+ },
81576
+ "powerSource.powerOff": {
81577
+ capName: "power-source",
81578
+ capScope: "device",
81579
+ addonId: null,
81580
+ access: "create",
81581
+ role: "admin"
81582
+ },
81583
+ "powerSource.powerOn": {
81584
+ capName: "power-source",
81585
+ capScope: "device",
81586
+ addonId: null,
81587
+ access: "create",
81588
+ role: "admin"
81589
+ },
80802
81590
  "presence.getStatus": {
80803
81591
  capName: "presence",
80804
81592
  capScope: "device",
@@ -85947,6 +86735,26 @@ Object.freeze({
85947
86735
  form: "single",
85948
86736
  optional: false
85949
86737
  }],
86738
+ "powerSource.forceRestart": [{
86739
+ name: "deviceId",
86740
+ form: "single",
86741
+ optional: false
86742
+ }],
86743
+ "powerSource.getStatus": [{
86744
+ name: "deviceId",
86745
+ form: "single",
86746
+ optional: false
86747
+ }],
86748
+ "powerSource.powerOff": [{
86749
+ name: "deviceId",
86750
+ form: "single",
86751
+ optional: false
86752
+ }],
86753
+ "powerSource.powerOn": [{
86754
+ name: "deviceId",
86755
+ form: "single",
86756
+ optional: false
86757
+ }],
85950
86758
  "presence.getStatus": [{
85951
86759
  name: "deviceId",
85952
86760
  form: "single",
@@ -87877,6 +88685,7 @@ new Set([
87877
88685
  "locking",
87878
88686
  "unlocking",
87879
88687
  "jammed",
88688
+ "door-open",
87880
88689
  "on",
87881
88690
  "off",
87882
88691
  "home",