@camstack/addon-provider-rademacher 0.2.153 → 0.2.155

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 +13 -0
  2. package/dist/addon.js +2105 -1149
  3. package/dist/addon.mjs +2105 -1149
  4. package/package.json +5 -2
package/dist/addon.js CHANGED
@@ -2,7 +2,7 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  let crypto$1 = require("crypto");
3
3
  let events = require("events");
4
4
  let os = require("os");
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(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) {
@@ -5458,7 +5834,7 @@ var ZodIssueCode = {
5458
5834
  var ZodFirstPartyTypeKind;
5459
5835
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5460
5836
  //#endregion
5461
- //#region ../types/dist/sleep-0RR2wzNB.mjs
5837
+ //#region ../types/dist/sleep-CSYuxssh.mjs
5462
5838
  /**
5463
5839
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5464
5840
  * window to float samples (D455).
@@ -5974,6 +6350,102 @@ function emitReadiness(bus, params) {
5974
6350
  ts
5975
6351
  }));
5976
6352
  }
6353
+ var NOT_RUN = {
6354
+ declared: false,
6355
+ snapshot: null,
6356
+ applied: [],
6357
+ failed: [],
6358
+ blocked: [],
6359
+ skipped: [],
6360
+ blockOwner: []
6361
+ };
6362
+ /**
6363
+ * Offer `migrations` to the engine and run what it says to run, in its order. Never
6364
+ * throws: an unreachable or refusing engine is logged and reported `declared: false`,
6365
+ * because the hub (and this addon's boot) must not hang on a migration.
6366
+ */
6367
+ async function runAddonMigrations(host, migrations, logger) {
6368
+ const first = migrations[0];
6369
+ if (first === void 0) return NOT_RUN;
6370
+ const owner = first.meta.owner;
6371
+ const log = logger.withTags({ migrationOwner: owner });
6372
+ const byId = new Map(migrations.map((m) => [m.meta.id, m]));
6373
+ let declaration;
6374
+ try {
6375
+ declaration = await host.declareOwner({
6376
+ owner,
6377
+ metas: migrations.map((m) => m.meta)
6378
+ });
6379
+ } catch (err) {
6380
+ log.error("the migration engine refused or could not take this addon catalogue — none ran", { meta: { error: errMsg(err) } });
6381
+ return {
6382
+ ...NOT_RUN,
6383
+ blockOwner: migrations.filter((m) => m.meta.onFailure === "block-owner").map((m) => m.meta.id)
6384
+ };
6385
+ }
6386
+ const applied = [];
6387
+ const failed = [];
6388
+ const blocked = [];
6389
+ const skipped = [];
6390
+ const settle = (id, status) => {
6391
+ if (status === "applied") applied.push(id);
6392
+ else if (status === "failed") failed.push(id);
6393
+ else if (status === "blocked") blocked.push(id);
6394
+ else skipped.push(id);
6395
+ };
6396
+ for (const step of declaration.steps) {
6397
+ const bound = byId.get(step.id);
6398
+ if (bound === void 0 || step.action === "skip") {
6399
+ skipped.push(step.id);
6400
+ continue;
6401
+ }
6402
+ const tags = { migrationId: step.id };
6403
+ try {
6404
+ const gate = await host.startMigration({ id: step.id });
6405
+ if (!gate.proceed) {
6406
+ log.warn("migration not run", {
6407
+ tags,
6408
+ meta: {
6409
+ state: gate.state,
6410
+ reason: gate.reason
6411
+ }
6412
+ });
6413
+ settle(step.id, gate.state === "pending" ? "blocked" : gate.state);
6414
+ continue;
6415
+ }
6416
+ const result = await bound.execute();
6417
+ const outcome = await host.finishMigration({
6418
+ id: step.id,
6419
+ result
6420
+ });
6421
+ if (outcome.status === "applied") log.info("migration applied", {
6422
+ tags,
6423
+ meta: { rowsChanged: outcome.rowsChanged }
6424
+ });
6425
+ else log.error("migration failed", {
6426
+ tags,
6427
+ meta: { error: outcome.error }
6428
+ });
6429
+ settle(step.id, outcome.status);
6430
+ } catch (err) {
6431
+ log.error("migration could not be completed with the engine", {
6432
+ tags,
6433
+ meta: { error: errMsg(err) }
6434
+ });
6435
+ failed.push(step.id);
6436
+ }
6437
+ }
6438
+ const blockOwner = [...failed, ...blocked].filter((id) => byId.get(id)?.meta.onFailure === "block-owner");
6439
+ return {
6440
+ declared: true,
6441
+ snapshot: declaration.snapshot,
6442
+ applied,
6443
+ failed,
6444
+ blocked,
6445
+ skipped,
6446
+ blockOwner
6447
+ };
6448
+ }
5977
6449
  /**
5978
6450
  * Build a {@link DurableState} over a single store key. Transport-agnostic:
5979
6451
  * `read`/`write` are the addon-store or device-store accessors. The whole
@@ -7029,6 +7501,28 @@ var BaseAddon = class {
7029
7501
  return normalized;
7030
7502
  }
7031
7503
  /**
7504
+ * Run this addon's migration catalogue (D987 M3) through the hub's `migrations`
7505
+ * engine. Call it at the point of the boot where the data must already have the new
7506
+ * shape — `onInitialize`, or `onRestoreDevices` for a migration that needs the
7507
+ * restored devices — with the catalogue bound to the addon's ports
7508
+ * (`defineAddonMigrations(ADDON_MIGRATIONS, ports)`).
7509
+ *
7510
+ * An engine that cannot be reached or refuses the catalogue is logged and runs
7511
+ * nothing; the boot continues. A migration that FAILED (or was blocked by one)
7512
+ * and declares `onFailure: 'block-owner'` makes this throw, so the addon does not
7513
+ * serve data whose shape it cannot trust; `tolerate` migrations never do.
7514
+ */
7515
+ async runMigrations(migrations) {
7516
+ const api = this.ctx.api.migrations;
7517
+ const report = await runAddonMigrations({
7518
+ declareOwner: (input) => api.declareOwner.mutate(input),
7519
+ startMigration: (input) => api.startMigration.mutate(input),
7520
+ finishMigration: (input) => api.finishMigration.mutate(input)
7521
+ }, migrations, this.ctx.logger);
7522
+ 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`);
7523
+ return report;
7524
+ }
7525
+ /**
7032
7526
  * Called by the isolated-process runner AFTER `broker.start()`.
7033
7527
  * In-process addons never need this because the hub broker is already
7034
7528
  * running when `initialize()` fires. For forked children the broker
@@ -7161,6 +7655,26 @@ var BaseAddon = class {
7161
7655
  * alternative — skip it — turns a rename into a surface that quietly
7162
7656
  * empties, which looks exactly like an addon with nothing to configure.
7163
7657
  */
7658
+ /**
7659
+ * The form schema of ONE integration instance, or `null` to use {@link globalSettingsSchema}.
7660
+ *
7661
+ * Override it only when an `instanceMode: multiple` addon's instances differ: an NVR integration
7662
+ * needs its connection fields, the default integration the addon's own sections. It is the
7663
+ * SHAPE of the form only — the values live in `integrations.getSettings`, and the write path is
7664
+ * unchanged (`integrations.setSettings` for an account integration).
7665
+ */
7666
+ async integrationSettingsSchema(_instance) {
7667
+ return null;
7668
+ }
7669
+ /**
7670
+ * What the create wizard and the integration edit page render for one instance: the addon's
7671
+ * per-instance schema when it declares one, else its global schema (what they always showed).
7672
+ */
7673
+ async getIntegrationFormSchema(instance, nodeId) {
7674
+ const scoped = await this.integrationSettingsSchema(instance);
7675
+ if (scoped === null) return this.getGlobalSettings(void 0, void 0, nodeId);
7676
+ return hydrateSchema(scoped, {}, { inlineSecretsAsViews: true });
7677
+ }
7164
7678
  async getIntegrationSettings(nodeId) {
7165
7679
  const declared = this.integrationSettingSections();
7166
7680
  if (declared.length === 0) return null;
@@ -8118,407 +8632,6 @@ var RecordKindDeclarationSchema = object({
8118
8632
  }).strict();
8119
8633
  /** The kind names: lower-case, dash separated, short enough for an index name. */
8120
8634
  var RecordKindNameSchema = string().regex(/^[a-z][a-z0-9-]{0,47}$/);
8121
- var READING_QUANTITIES = [
8122
- "temperature",
8123
- "pressure",
8124
- "speed",
8125
- "precipitation",
8126
- "precipitation-rate",
8127
- "length",
8128
- "area",
8129
- "illuminance",
8130
- "irradiance",
8131
- "power",
8132
- "energy",
8133
- "voltage",
8134
- "current",
8135
- "apparent-power",
8136
- "humidity",
8137
- "percentage",
8138
- "concentration-volume",
8139
- "concentration-mass",
8140
- "concentration-ppb",
8141
- "volume",
8142
- "duration",
8143
- "duration-ms",
8144
- "information",
8145
- "sound-level",
8146
- "signal-level",
8147
- "colour-temperature",
8148
- "angle",
8149
- "bitrate",
8150
- "rate",
8151
- "mass",
8152
- "index",
8153
- "ratio",
8154
- "count",
8155
- "none",
8156
- "device"
8157
- ];
8158
- var ReadingQuantitySchema = _enum(READING_QUANTITIES);
8159
- new Set(READING_QUANTITIES);
8160
- /**
8161
- * Stored unit symbol for a quantity. `none` means the number is dimensionless
8162
- * (or an index / ratio / count). `device` means the cap has no single unit.
8163
- */
8164
- var QUANTITY_BASE = {
8165
- temperature: "°C",
8166
- pressure: "hPa",
8167
- speed: "m/s",
8168
- precipitation: "mm",
8169
- "precipitation-rate": "mm/h",
8170
- length: "m",
8171
- area: "m²",
8172
- illuminance: "lx",
8173
- irradiance: "W/m²",
8174
- power: "W",
8175
- energy: "kWh",
8176
- voltage: "V",
8177
- current: "A",
8178
- "apparent-power": "VA",
8179
- humidity: "%",
8180
- percentage: "%",
8181
- "concentration-volume": "ppm",
8182
- "concentration-mass": "µg/m³",
8183
- "concentration-ppb": "ppb",
8184
- volume: "L",
8185
- duration: "s",
8186
- "duration-ms": "ms",
8187
- information: "B",
8188
- "sound-level": "dBFS",
8189
- "signal-level": "dBm",
8190
- "colour-temperature": "mired",
8191
- angle: "°",
8192
- bitrate: "kbps",
8193
- rate: "fps",
8194
- mass: "kg",
8195
- index: "none",
8196
- ratio: "none",
8197
- count: "none",
8198
- none: "none",
8199
- device: "device"
8200
- };
8201
- /** The stored unit, or `undefined` when the quantity has no unit symbol. */
8202
- function baseUnitOf(quantity) {
8203
- const symbol = QUANTITY_BASE[quantity];
8204
- if (symbol === "none" || symbol === "device") return void 0;
8205
- return symbol;
8206
- }
8207
- function readingMeta(quantity, extras) {
8208
- return {
8209
- quantity,
8210
- ...extras?.precision !== void 0 ? { precision: extras.precision } : {},
8211
- ...extras?.label !== void 0 ? { label: extras.label } : {}
8212
- };
8213
- }
8214
- /** Shared Zod schemas used across streaming capabilities. */
8215
- var CamProfileSchema = _enum([
8216
- "high",
8217
- "mid",
8218
- "low"
8219
- ]);
8220
- /**
8221
- * Which rung a per-camera consumer listens on: an explicit profile, or `auto`.
8222
- *
8223
- * `auto` is a REAL value, not an absent one — it means "resolve it by the rule
8224
- * that consumer owns". It lives beside `CamProfile` because a rung is the only
8225
- * operator-facing way to name a stream: a cam-stream id is broker-internal and
8226
- * moves when a slot is reassigned, so a stored id silently stops meaning what
8227
- * the operator picked. Today's one user is the audio analyser, whose rule is
8228
- * "the lowest AVAILABLE native rung" (D537).
8229
- */
8230
- var AudioStreamPickSchema = union([CamProfileSchema, literal("auto")]);
8231
- var CamStreamKindSchema = _enum([
8232
- "pull-rtsp",
8233
- "pull-rtmp",
8234
- "pull-http",
8235
- "pull-flv",
8236
- "pull-rfc4571",
8237
- "push-annexb",
8238
- "derived"
8239
- ]);
8240
- var CamStreamResolutionSchema = object({
8241
- width: number().int().positive().meta(readingMeta("none")),
8242
- height: number().int().positive().meta(readingMeta("none"))
8243
- });
8244
- var CameraStreamSchema = object({
8245
- /** Stable, provider-assigned id unique within the (deviceId) scope. */
8246
- camStreamId: string().min(1),
8247
- deviceId: number().int().nonnegative(),
8248
- kind: CamStreamKindSchema,
8249
- /** Required for pull-* kinds. Ignored for push-annexb. */
8250
- url: string().optional(),
8251
- codec: string().optional(),
8252
- resolution: CamStreamResolutionSchema.optional(),
8253
- fps: number().positive().optional(),
8254
- /** Human label surfaced in the Admin UI "Camera Stream" dropdown. */
8255
- label: string().optional(),
8256
- /**
8257
- * Device-level features the publisher advertised (e.g. `battery-operated`).
8258
- * The broker, snapshot orchestrator, and prebuffer manager all consult
8259
- * this list to derive policy — relaxed stall watchdog for battery
8260
- * cams, prebuffer off by default, longer snapshot rate-limit, etc.
8261
- *
8262
- * Single source of truth replacing per-stream flags like the
8263
- * historical `allowStall`: if the publisher knows the camera is
8264
- * battery-powered, every downstream service derives the right policy
8265
- * from this list.
8266
- */
8267
- deviceFeatures: array(string()).optional(),
8268
- /**
8269
- * Whether this stream participates in the broker's automatic profile
8270
- * assignment (`computeInitialAssignment`). Defaults to `true`. Publishers
8271
- * use `false` when they want a stream to be SELECTABLE in the UI but not
8272
- * picked by default — e.g. Reolink publishes its native Baichuan streams
8273
- * as `autoEligible: true` (the recommended path) and its RTSP / RTMP
8274
- * mirrors as `autoEligible: false` (still pickable per slot, just not
8275
- * the auto choice). Manual `assignProfile` calls remain valid for
8276
- * non-eligible streams.
8277
- */
8278
- autoEligible: boolean().optional(),
8279
- /**
8280
- * The profile slot the PUBLISHER says this stream is: `low` for a stream
8281
- * whose only sensible role is the low-bandwidth one, `high` for a main
8282
- * stream. Consulted by the broker's automatic ranking
8283
- * (`rankInitialAssignment`) BEFORE the pixel-count heuristic, and only there:
8284
- * an operator's manual assignment still wins, and an absent hint is the
8285
- * pixel-ranked default every camera had before this field existed.
8286
- *
8287
- * Why it exists: a camera with ONE stream ranked to `mid` — the slot no
8288
- * storage class is written for on its own (`recordings` is high|mid,
8289
- * `recordingsLow` is low) and the one profile scrub does not read. The robot
8290
- * camera's single 720p relay is a `low` in every respect except the slot the
8291
- * heuristic put it in.
8292
- */
8293
- profileHint: CamProfileSchema.optional(),
8294
- /**
8295
- * Transport-specific opaque metadata. The broker passes it through to
8296
- * the source reader without inspecting it. Currently used by
8297
- * `pull-rfc4571` streams to carry the upstream SDP (so the reader can
8298
- * route RTP packets to the right depacketizer without an in-band
8299
- * DESCRIBE phase). Other kinds typically leave it undefined.
8300
- */
8301
- metadata: record(string(), unknown()).optional()
8302
- });
8303
- var ProfileSlotStatusSchema = _enum([
8304
- "unassigned",
8305
- "idle",
8306
- "connecting",
8307
- "streaming",
8308
- "error"
8309
- ]);
8310
- var ProfileSlotSchema = object({
8311
- deviceId: number().int().nonnegative(),
8312
- profile: CamProfileSchema,
8313
- /** Broker id the rest of the system addresses: `${deviceId}/${profile}`. */
8314
- brokerId: string(),
8315
- /** `null` when the profile is unassigned. */
8316
- sourceCamStreamId: string().nullable(),
8317
- status: ProfileSlotStatusSchema,
8318
- resolution: CamStreamResolutionSchema.optional(),
8319
- codec: string().optional(),
8320
- preBufferSec: number().nonnegative().optional(),
8321
- errorMessage: string().optional()
8322
- });
8323
- /**
8324
- * Zod schema for StreamSourceEntry — the canonical stream descriptor
8325
- * exposed by ICameraDevice.getStreamSources() and consumed by the broker.
8326
- */
8327
- var StreamSourceEntrySchema$1 = object({
8328
- id: string(),
8329
- label: string(),
8330
- protocol: _enum([
8331
- "rtsp",
8332
- "rtmp",
8333
- "annexb",
8334
- "http-mjpeg",
8335
- "webrtc",
8336
- "custom"
8337
- ]),
8338
- url: string().optional(),
8339
- resolution: object({
8340
- width: number(),
8341
- height: number()
8342
- }).readonly().optional(),
8343
- fps: number().optional(),
8344
- bitrate: number().optional(),
8345
- codec: string().optional(),
8346
- profileHint: CamProfileSchema.optional()
8347
- });
8348
- object({
8349
- type: string(),
8350
- url: string(),
8351
- videoCodec: string().optional(),
8352
- audioCodec: string().optional(),
8353
- metadata: record(string(), unknown()).readonly().optional()
8354
- });
8355
- /**
8356
- * Wire schema for `FrameHandle` — a zero-pixel, serialisable reference to a
8357
- * frame in a shared-memory ring (Phase 5 / D9). Mirrors the `FrameHandle` TS
8358
- * interface in `interfaces/frame-handle.ts` field-for-field so a cap method
8359
- * (the runner's native-crop and pipeline-executor inputs) can carry handles
8360
- * over tRPC / Moleculer.
8361
- *
8362
- * The `satisfies` assertion below pins this schema to the interface: if a
8363
- * field is added to / removed from `FrameHandle` without a matching schema
8364
- * edit, `z.infer<typeof FrameHandleSchema>` stops being assignable to
8365
- * `FrameHandle` and the build fails — no silent drift.
8366
- */
8367
- var FrameHandleSchema = object({
8368
- shmId: string(),
8369
- slot: number().int().nonnegative(),
8370
- seq: number().int().nonnegative(),
8371
- width: number().int().positive(),
8372
- height: number().int().positive(),
8373
- format: _enum([
8374
- "jpeg",
8375
- "rgb",
8376
- "bgr",
8377
- "yuv420",
8378
- "gray"
8379
- ]),
8380
- pts: number(),
8381
- byteLength: number().int().nonnegative(),
8382
- nodeId: string(),
8383
- slotCount: number().int().positive()
8384
- });
8385
- /**
8386
- * Wire schema for a decoded audio chunk (Phase 5 / D9). Mirrors the
8387
- * `DecodedAudioChunk` TS interface in `interfaces/stream-broker.ts`: PCM
8388
- * sample bytes plus the track parameters a consumer needs to interpret them.
8389
- *
8390
- * Audio chunks are tiny (a ~500ms AudioCodecSession window is a few KB of
8391
- * Float32 PCM), so unlike video frames they ship their bytes INLINE over
8392
- * tRPC / Moleculer — no shared-memory plane. `data` is typed `Uint8Array`
8393
- * (the wire-serialisable supertype of `Buffer`); a `Buffer` is assignable to
8394
- * it.
8395
- */
8396
- var AudioChunkFormatSchema = _enum(AUDIO_CHUNK_FORMATS);
8397
- var DecodedAudioChunkSchema = object({
8398
- data: _instanceof(Uint8Array),
8399
- sampleRate: number().int().positive(),
8400
- channels: number().int().positive(),
8401
- timestamp: number(),
8402
- /**
8403
- * Byte format of `data`. ABSENT MEANS `f32le` — today's bytes, byte for
8404
- * byte, for any peer that never heard of this field. A coded window
8405
- * (`pcmu` / `pcma`, one byte per sample) is only ever emitted to a
8406
- * subscription that DECLARED it accepts one, so absence can never mean
8407
- * "coded bytes a consumer will read as floats" (D455).
8408
- */
8409
- format: AudioChunkFormatSchema.optional()
8410
- });
8411
- /**
8412
- * Input for `stream-broker.subscribeAudioChunks` (Phase 5 / D9). Audio ships
8413
- * its bytes inline: a consumer asks the broker for the decoded audio-chunk
8414
- * stream of one broker; the broker keeps a per-subscription bounded FIFO queue
8415
- * the consumer drains via `pullAudioChunks`.
8416
- */
8417
- var SubscribeAudioChunksInputSchema = object({
8418
- brokerId: string(),
8419
- /** Short caller-identity tag (`audio-analyzer`, …) for `listClients`. */
8420
- tag: string().optional(),
8421
- /**
8422
- * Byte formats this subscriber can READ, best first. The broker serves the
8423
- * chunk's own format when it is in this list and expands to `f32le`
8424
- * otherwise, so a subscriber is never handed bytes it cannot interpret.
8425
- *
8426
- * Absent (or without the source format) means `f32le` — the behaviour every
8427
- * subscriber had before D455, unchanged. This is the negotiation half of
8428
- * the source-bytes lever: it is what lets the broker and its consumers
8429
- * deploy one at a time across three nodes.
8430
- */
8431
- accept: array(AudioChunkFormatSchema).readonly().optional()
8432
- });
8433
- /** Result of `stream-broker.subscribeAudioChunks`. */
8434
- var SubscribeAudioChunksResultSchema = object({
8435
- /** Opaque id passed to `pullAudioChunks` / `unsubscribeAudioChunks`. */
8436
- subscriptionId: string() });
8437
- var BrokerStatsSchema = object({
8438
- status: _enum([
8439
- "idle",
8440
- "connecting",
8441
- "streaming",
8442
- "error",
8443
- "stopped"
8444
- ]),
8445
- inputFps: number(),
8446
- encodedSubscribers: number(),
8447
- uptimeMs: number(),
8448
- bitrateKbps: number(),
8449
- idrIntervalMs: number(),
8450
- codec: string().optional(),
8451
- totalBytes: number(),
8452
- packetCount: number(),
8453
- rtspClients: number(),
8454
- pipeClients: number(),
8455
- preBufferSec: number(),
8456
- preBufferMs: number(),
8457
- preBufferPackets: number(),
8458
- /**
8459
- * Detected audio track parameters from the RTSP DESCRIBE / SDP. `null`
8460
- * when the stream has no audio track or the broker is in cold start.
8461
- * `supported = false` means the codec was detected but the local
8462
- * decoder pipeline cannot produce PCM chunks (e.g. AAC without the
8463
- * AAC pipeline wired). Surfaced in the UI device-overview so operators
8464
- * can pre-pick the audio-analysis model that matches the codec.
8465
- */
8466
- audio: object({
8467
- codec: string(),
8468
- sampleRate: number(),
8469
- channels: number(),
8470
- supported: boolean()
8471
- }).nullable().optional(),
8472
- /**
8473
- * BROKER-SIDE AUDIO MUTE (D83). `true` = this broker is deliberately
8474
- * distributing none of the device's audio, on live or recording.
8475
- *
8476
- * Present so a silent camera can be told apart from a broken one on the
8477
- * stream panel itself, without cross-referencing the switch group: a
8478
- * broker holding an `audio` track descriptor while `audioMuted` is true is
8479
- * working exactly as asked. `audioMutedDropped` counts the audio units
8480
- * thrown away since the current dial — it is how you confirm from stats
8481
- * alone that the mute is on the packet path and not merely persisted.
8482
- */
8483
- audioMuted: boolean().optional(),
8484
- audioMutedDropped: number().optional()
8485
- });
8486
- /**
8487
- * Exporter-facing "profile restream" entry. Returned by
8488
- * `cameraStreams.getProfileRtspEntries` — one per ASSIGNED profile slot
8489
- * (high/mid/low). `brokerId` is the PROFILE-keyed broker id
8490
- * (`${deviceId}/${profile}`, e.g. `15/high`); its `url` is a broker
8491
- * RTSP restream that aliases the profile's assigned source broker, so
8492
- * every consumer dialling a profile converges on the same single pull.
8493
- * `codec`/`resolution` describe the assigned SOURCE camStream (for the
8494
- * `auto` resolution-closest pick). Distinct from `RtspRestreamEntry`
8495
- * (raw per-camStream, live-view only).
8496
- */
8497
- var ProfileRtspEntrySchema = object({
8498
- profile: CamProfileSchema,
8499
- /** Profile-keyed broker id, format `${deviceId}/${profile}` (e.g. `"15/high"`). */
8500
- brokerId: string(),
8501
- url: string(),
8502
- mutedUrl: string(),
8503
- enabled: boolean(),
8504
- codec: string().optional(),
8505
- resolution: CamStreamResolutionSchema.optional()
8506
- });
8507
- /**
8508
- * The model weight formats a catalog entry can ship. Schema-free on purpose:
8509
- * `types/models.ts` builds Zod schemas from it, but a runner-closure consumer
8510
- * (the model downloader in `@camstack/system`) needs only the list and must
8511
- * reach it through `@camstack/types/addon` without building the cap schema
8512
- * graph (D28).
8513
- */
8514
- var MODEL_FORMATS = [
8515
- "onnx",
8516
- "coreml",
8517
- "openvino",
8518
- "tflite",
8519
- "pt",
8520
- "gguf"
8521
- ];
8522
8635
  object({
8523
8636
  field: string().min(1),
8524
8637
  on: literal("advance")
@@ -8611,6 +8724,407 @@ function optionalMethod(input, output, options) {
8611
8724
  function event(data) {
8612
8725
  return { data };
8613
8726
  }
8727
+ var READING_QUANTITIES = [
8728
+ "temperature",
8729
+ "pressure",
8730
+ "speed",
8731
+ "precipitation",
8732
+ "precipitation-rate",
8733
+ "length",
8734
+ "area",
8735
+ "illuminance",
8736
+ "irradiance",
8737
+ "power",
8738
+ "energy",
8739
+ "voltage",
8740
+ "current",
8741
+ "apparent-power",
8742
+ "humidity",
8743
+ "percentage",
8744
+ "concentration-volume",
8745
+ "concentration-mass",
8746
+ "concentration-ppb",
8747
+ "volume",
8748
+ "duration",
8749
+ "duration-ms",
8750
+ "information",
8751
+ "sound-level",
8752
+ "signal-level",
8753
+ "colour-temperature",
8754
+ "angle",
8755
+ "bitrate",
8756
+ "rate",
8757
+ "mass",
8758
+ "index",
8759
+ "ratio",
8760
+ "count",
8761
+ "none",
8762
+ "device"
8763
+ ];
8764
+ var ReadingQuantitySchema = _enum(READING_QUANTITIES);
8765
+ new Set(READING_QUANTITIES);
8766
+ /**
8767
+ * Stored unit symbol for a quantity. `none` means the number is dimensionless
8768
+ * (or an index / ratio / count). `device` means the cap has no single unit.
8769
+ */
8770
+ var QUANTITY_BASE = {
8771
+ temperature: "°C",
8772
+ pressure: "hPa",
8773
+ speed: "m/s",
8774
+ precipitation: "mm",
8775
+ "precipitation-rate": "mm/h",
8776
+ length: "m",
8777
+ area: "m²",
8778
+ illuminance: "lx",
8779
+ irradiance: "W/m²",
8780
+ power: "W",
8781
+ energy: "kWh",
8782
+ voltage: "V",
8783
+ current: "A",
8784
+ "apparent-power": "VA",
8785
+ humidity: "%",
8786
+ percentage: "%",
8787
+ "concentration-volume": "ppm",
8788
+ "concentration-mass": "µg/m³",
8789
+ "concentration-ppb": "ppb",
8790
+ volume: "L",
8791
+ duration: "s",
8792
+ "duration-ms": "ms",
8793
+ information: "B",
8794
+ "sound-level": "dBFS",
8795
+ "signal-level": "dBm",
8796
+ "colour-temperature": "mired",
8797
+ angle: "°",
8798
+ bitrate: "kbps",
8799
+ rate: "fps",
8800
+ mass: "kg",
8801
+ index: "none",
8802
+ ratio: "none",
8803
+ count: "none",
8804
+ none: "none",
8805
+ device: "device"
8806
+ };
8807
+ /** The stored unit, or `undefined` when the quantity has no unit symbol. */
8808
+ function baseUnitOf(quantity) {
8809
+ const symbol = QUANTITY_BASE[quantity];
8810
+ if (symbol === "none" || symbol === "device") return void 0;
8811
+ return symbol;
8812
+ }
8813
+ function readingMeta(quantity, extras) {
8814
+ return {
8815
+ quantity,
8816
+ ...extras?.precision !== void 0 ? { precision: extras.precision } : {},
8817
+ ...extras?.label !== void 0 ? { label: extras.label } : {}
8818
+ };
8819
+ }
8820
+ /** Shared Zod schemas used across streaming capabilities. */
8821
+ var CamProfileSchema = _enum([
8822
+ "high",
8823
+ "mid",
8824
+ "low"
8825
+ ]);
8826
+ /**
8827
+ * Which rung a per-camera consumer listens on: an explicit profile, or `auto`.
8828
+ *
8829
+ * `auto` is a REAL value, not an absent one — it means "resolve it by the rule
8830
+ * that consumer owns". It lives beside `CamProfile` because a rung is the only
8831
+ * operator-facing way to name a stream: a cam-stream id is broker-internal and
8832
+ * moves when a slot is reassigned, so a stored id silently stops meaning what
8833
+ * the operator picked. Today's one user is the audio analyser, whose rule is
8834
+ * "the lowest AVAILABLE native rung" (D537).
8835
+ */
8836
+ var AudioStreamPickSchema = union([CamProfileSchema, literal("auto")]);
8837
+ var CamStreamKindSchema = _enum([
8838
+ "pull-rtsp",
8839
+ "pull-rtmp",
8840
+ "pull-http",
8841
+ "pull-flv",
8842
+ "pull-rfc4571",
8843
+ "push-annexb",
8844
+ "derived"
8845
+ ]);
8846
+ var CamStreamResolutionSchema = object({
8847
+ width: number().int().positive().meta(readingMeta("none")),
8848
+ height: number().int().positive().meta(readingMeta("none"))
8849
+ });
8850
+ var CameraStreamSchema = object({
8851
+ /** Stable, provider-assigned id unique within the (deviceId) scope. */
8852
+ camStreamId: string().min(1),
8853
+ deviceId: number().int().nonnegative(),
8854
+ kind: CamStreamKindSchema,
8855
+ /** Required for pull-* kinds. Ignored for push-annexb. */
8856
+ url: string().optional(),
8857
+ codec: string().optional(),
8858
+ resolution: CamStreamResolutionSchema.optional(),
8859
+ fps: number().positive().optional(),
8860
+ /** Human label surfaced in the Admin UI "Camera Stream" dropdown. */
8861
+ label: string().optional(),
8862
+ /**
8863
+ * Device-level features the publisher advertised (e.g. `battery-operated`).
8864
+ * The broker, snapshot orchestrator, and prebuffer manager all consult
8865
+ * this list to derive policy — relaxed stall watchdog for battery
8866
+ * cams, prebuffer off by default, longer snapshot rate-limit, etc.
8867
+ *
8868
+ * Single source of truth replacing per-stream flags like the
8869
+ * historical `allowStall`: if the publisher knows the camera is
8870
+ * battery-powered, every downstream service derives the right policy
8871
+ * from this list.
8872
+ */
8873
+ deviceFeatures: array(string()).optional(),
8874
+ /**
8875
+ * Whether this stream participates in the broker's automatic profile
8876
+ * assignment (`computeInitialAssignment`). Defaults to `true`. Publishers
8877
+ * use `false` when they want a stream to be SELECTABLE in the UI but not
8878
+ * picked by default — e.g. Reolink publishes its native Baichuan streams
8879
+ * as `autoEligible: true` (the recommended path) and its RTSP / RTMP
8880
+ * mirrors as `autoEligible: false` (still pickable per slot, just not
8881
+ * the auto choice). Manual `assignProfile` calls remain valid for
8882
+ * non-eligible streams.
8883
+ */
8884
+ autoEligible: boolean().optional(),
8885
+ /**
8886
+ * The profile slot the PUBLISHER says this stream is: `low` for a stream
8887
+ * whose only sensible role is the low-bandwidth one, `high` for a main
8888
+ * stream. Consulted by the broker's automatic ranking
8889
+ * (`rankInitialAssignment`) BEFORE the pixel-count heuristic, and only there:
8890
+ * an operator's manual assignment still wins, and an absent hint is the
8891
+ * pixel-ranked default every camera had before this field existed.
8892
+ *
8893
+ * Why it exists: a camera with ONE stream ranked to `mid` — the slot no
8894
+ * storage class is written for on its own (`recordings` is high|mid,
8895
+ * `recordingsLow` is low) and the one profile scrub does not read. The robot
8896
+ * camera's single 720p relay is a `low` in every respect except the slot the
8897
+ * heuristic put it in.
8898
+ */
8899
+ profileHint: CamProfileSchema.optional(),
8900
+ /**
8901
+ * Transport-specific opaque metadata. The broker passes it through to
8902
+ * the source reader without inspecting it. Currently used by
8903
+ * `pull-rfc4571` streams to carry the upstream SDP (so the reader can
8904
+ * route RTP packets to the right depacketizer without an in-band
8905
+ * DESCRIBE phase). Other kinds typically leave it undefined.
8906
+ */
8907
+ metadata: record(string(), unknown()).optional()
8908
+ });
8909
+ var ProfileSlotStatusSchema = _enum([
8910
+ "unassigned",
8911
+ "idle",
8912
+ "connecting",
8913
+ "streaming",
8914
+ "error"
8915
+ ]);
8916
+ var ProfileSlotSchema = object({
8917
+ deviceId: number().int().nonnegative(),
8918
+ profile: CamProfileSchema,
8919
+ /** Broker id the rest of the system addresses: `${deviceId}/${profile}`. */
8920
+ brokerId: string(),
8921
+ /** `null` when the profile is unassigned. */
8922
+ sourceCamStreamId: string().nullable(),
8923
+ status: ProfileSlotStatusSchema,
8924
+ resolution: CamStreamResolutionSchema.optional(),
8925
+ codec: string().optional(),
8926
+ preBufferSec: number().nonnegative().optional(),
8927
+ errorMessage: string().optional()
8928
+ });
8929
+ /**
8930
+ * Zod schema for StreamSourceEntry — the canonical stream descriptor
8931
+ * exposed by ICameraDevice.getStreamSources() and consumed by the broker.
8932
+ */
8933
+ var StreamSourceEntrySchema$1 = object({
8934
+ id: string(),
8935
+ label: string(),
8936
+ protocol: _enum([
8937
+ "rtsp",
8938
+ "rtmp",
8939
+ "annexb",
8940
+ "http-mjpeg",
8941
+ "webrtc",
8942
+ "custom"
8943
+ ]),
8944
+ url: string().optional(),
8945
+ resolution: object({
8946
+ width: number(),
8947
+ height: number()
8948
+ }).readonly().optional(),
8949
+ fps: number().optional(),
8950
+ bitrate: number().optional(),
8951
+ codec: string().optional(),
8952
+ profileHint: CamProfileSchema.optional()
8953
+ });
8954
+ object({
8955
+ type: string(),
8956
+ url: string(),
8957
+ videoCodec: string().optional(),
8958
+ audioCodec: string().optional(),
8959
+ metadata: record(string(), unknown()).readonly().optional()
8960
+ });
8961
+ /**
8962
+ * Wire schema for `FrameHandle` — a zero-pixel, serialisable reference to a
8963
+ * frame in a shared-memory ring (Phase 5 / D9). Mirrors the `FrameHandle` TS
8964
+ * interface in `interfaces/frame-handle.ts` field-for-field so a cap method
8965
+ * (the runner's native-crop and pipeline-executor inputs) can carry handles
8966
+ * over tRPC / Moleculer.
8967
+ *
8968
+ * The `satisfies` assertion below pins this schema to the interface: if a
8969
+ * field is added to / removed from `FrameHandle` without a matching schema
8970
+ * edit, `z.infer<typeof FrameHandleSchema>` stops being assignable to
8971
+ * `FrameHandle` and the build fails — no silent drift.
8972
+ */
8973
+ var FrameHandleSchema = object({
8974
+ shmId: string(),
8975
+ slot: number().int().nonnegative(),
8976
+ seq: number().int().nonnegative(),
8977
+ width: number().int().positive(),
8978
+ height: number().int().positive(),
8979
+ format: _enum([
8980
+ "jpeg",
8981
+ "rgb",
8982
+ "bgr",
8983
+ "yuv420",
8984
+ "gray"
8985
+ ]),
8986
+ pts: number(),
8987
+ byteLength: number().int().nonnegative(),
8988
+ nodeId: string(),
8989
+ slotCount: number().int().positive()
8990
+ });
8991
+ /**
8992
+ * Wire schema for a decoded audio chunk (Phase 5 / D9). Mirrors the
8993
+ * `DecodedAudioChunk` TS interface in `interfaces/stream-broker.ts`: PCM
8994
+ * sample bytes plus the track parameters a consumer needs to interpret them.
8995
+ *
8996
+ * Audio chunks are tiny (a ~500ms AudioCodecSession window is a few KB of
8997
+ * Float32 PCM), so unlike video frames they ship their bytes INLINE over
8998
+ * tRPC / Moleculer — no shared-memory plane. `data` is typed `Uint8Array`
8999
+ * (the wire-serialisable supertype of `Buffer`); a `Buffer` is assignable to
9000
+ * it.
9001
+ */
9002
+ var AudioChunkFormatSchema = _enum(AUDIO_CHUNK_FORMATS);
9003
+ var DecodedAudioChunkSchema = object({
9004
+ data: _instanceof(Uint8Array),
9005
+ sampleRate: number().int().positive(),
9006
+ channels: number().int().positive(),
9007
+ timestamp: number(),
9008
+ /**
9009
+ * Byte format of `data`. ABSENT MEANS `f32le` — today's bytes, byte for
9010
+ * byte, for any peer that never heard of this field. A coded window
9011
+ * (`pcmu` / `pcma`, one byte per sample) is only ever emitted to a
9012
+ * subscription that DECLARED it accepts one, so absence can never mean
9013
+ * "coded bytes a consumer will read as floats" (D455).
9014
+ */
9015
+ format: AudioChunkFormatSchema.optional()
9016
+ });
9017
+ /**
9018
+ * Input for `stream-broker.subscribeAudioChunks` (Phase 5 / D9). Audio ships
9019
+ * its bytes inline: a consumer asks the broker for the decoded audio-chunk
9020
+ * stream of one broker; the broker keeps a per-subscription bounded FIFO queue
9021
+ * the consumer drains via `pullAudioChunks`.
9022
+ */
9023
+ var SubscribeAudioChunksInputSchema = object({
9024
+ brokerId: string(),
9025
+ /** Short caller-identity tag (`audio-analyzer`, …) for `listClients`. */
9026
+ tag: string().optional(),
9027
+ /**
9028
+ * Byte formats this subscriber can READ, best first. The broker serves the
9029
+ * chunk's own format when it is in this list and expands to `f32le`
9030
+ * otherwise, so a subscriber is never handed bytes it cannot interpret.
9031
+ *
9032
+ * Absent (or without the source format) means `f32le` — the behaviour every
9033
+ * subscriber had before D455, unchanged. This is the negotiation half of
9034
+ * the source-bytes lever: it is what lets the broker and its consumers
9035
+ * deploy one at a time across three nodes.
9036
+ */
9037
+ accept: array(AudioChunkFormatSchema).readonly().optional()
9038
+ });
9039
+ /** Result of `stream-broker.subscribeAudioChunks`. */
9040
+ var SubscribeAudioChunksResultSchema = object({
9041
+ /** Opaque id passed to `pullAudioChunks` / `unsubscribeAudioChunks`. */
9042
+ subscriptionId: string() });
9043
+ var BrokerStatsSchema = object({
9044
+ status: _enum([
9045
+ "idle",
9046
+ "connecting",
9047
+ "streaming",
9048
+ "error",
9049
+ "stopped"
9050
+ ]),
9051
+ inputFps: number(),
9052
+ encodedSubscribers: number(),
9053
+ uptimeMs: number(),
9054
+ bitrateKbps: number(),
9055
+ idrIntervalMs: number(),
9056
+ codec: string().optional(),
9057
+ totalBytes: number(),
9058
+ packetCount: number(),
9059
+ rtspClients: number(),
9060
+ pipeClients: number(),
9061
+ preBufferSec: number(),
9062
+ preBufferMs: number(),
9063
+ preBufferPackets: number(),
9064
+ /**
9065
+ * Detected audio track parameters from the RTSP DESCRIBE / SDP. `null`
9066
+ * when the stream has no audio track or the broker is in cold start.
9067
+ * `supported = false` means the codec was detected but the local
9068
+ * decoder pipeline cannot produce PCM chunks (e.g. AAC without the
9069
+ * AAC pipeline wired). Surfaced in the UI device-overview so operators
9070
+ * can pre-pick the audio-analysis model that matches the codec.
9071
+ */
9072
+ audio: object({
9073
+ codec: string(),
9074
+ sampleRate: number(),
9075
+ channels: number(),
9076
+ supported: boolean()
9077
+ }).nullable().optional(),
9078
+ /**
9079
+ * BROKER-SIDE AUDIO MUTE (D83). `true` = this broker is deliberately
9080
+ * distributing none of the device's audio, on live or recording.
9081
+ *
9082
+ * Present so a silent camera can be told apart from a broken one on the
9083
+ * stream panel itself, without cross-referencing the switch group: a
9084
+ * broker holding an `audio` track descriptor while `audioMuted` is true is
9085
+ * working exactly as asked. `audioMutedDropped` counts the audio units
9086
+ * thrown away since the current dial — it is how you confirm from stats
9087
+ * alone that the mute is on the packet path and not merely persisted.
9088
+ */
9089
+ audioMuted: boolean().optional(),
9090
+ audioMutedDropped: number().optional()
9091
+ });
9092
+ /**
9093
+ * Exporter-facing "profile restream" entry. Returned by
9094
+ * `cameraStreams.getProfileRtspEntries` — one per ASSIGNED profile slot
9095
+ * (high/mid/low). `brokerId` is the PROFILE-keyed broker id
9096
+ * (`${deviceId}/${profile}`, e.g. `15/high`); its `url` is a broker
9097
+ * RTSP restream that aliases the profile's assigned source broker, so
9098
+ * every consumer dialling a profile converges on the same single pull.
9099
+ * `codec`/`resolution` describe the assigned SOURCE camStream (for the
9100
+ * `auto` resolution-closest pick). Distinct from `RtspRestreamEntry`
9101
+ * (raw per-camStream, live-view only).
9102
+ */
9103
+ var ProfileRtspEntrySchema = object({
9104
+ profile: CamProfileSchema,
9105
+ /** Profile-keyed broker id, format `${deviceId}/${profile}` (e.g. `"15/high"`). */
9106
+ brokerId: string(),
9107
+ url: string(),
9108
+ mutedUrl: string(),
9109
+ enabled: boolean(),
9110
+ codec: string().optional(),
9111
+ resolution: CamStreamResolutionSchema.optional()
9112
+ });
9113
+ /**
9114
+ * The model weight formats a catalog entry can ship. Schema-free on purpose:
9115
+ * `types/models.ts` builds Zod schemas from it, but a runner-closure consumer
9116
+ * (the model downloader in `@camstack/server/system`) needs only the list and must
9117
+ * reach it through `@camstack/types/addon` without building the cap schema
9118
+ * graph (D28).
9119
+ */
9120
+ var MODEL_FORMATS = [
9121
+ "onnx",
9122
+ "coreml",
9123
+ "openvino",
9124
+ "tflite",
9125
+ "pt",
9126
+ "gguf"
9127
+ ];
8614
9128
  /**
8615
9129
  * `addon-pages-source` — collection cap exposing per-provider raw page
8616
9130
  * declarations. Every addon that contributes a UI page registers a
@@ -8852,6 +9366,7 @@ var CAP_NAMES = Object.freeze([
8852
9366
  "mesh-network",
8853
9367
  "meter",
8854
9368
  "metrics-provider",
9369
+ "migrations",
8855
9370
  "model-convert",
8856
9371
  "model-distributor",
8857
9372
  "motion",
@@ -8884,6 +9399,7 @@ var CAP_NAMES = Object.freeze([
8884
9399
  "plate-gallery",
8885
9400
  "platform-probe",
8886
9401
  "power-meter",
9402
+ "power-source",
8887
9403
  "presence",
8888
9404
  "pressure-sensor",
8889
9405
  "privacy-mask",
@@ -9847,7 +10363,8 @@ Object.freeze({
9847
10363
  "unlocked",
9848
10364
  "locking",
9849
10365
  "unlocking",
9850
- "jammed"
10366
+ "jammed",
10367
+ "door-open"
9851
10368
  ]
9852
10369
  }],
9853
10370
  "media-player": [
@@ -10234,6 +10751,13 @@ Object.freeze({
10234
10751
  "valueType": "number"
10235
10752
  }
10236
10753
  ],
10754
+ "power-source": [{
10755
+ "path": "switchOn",
10756
+ "valueType": "boolean"
10757
+ }, {
10758
+ "path": "offSeconds",
10759
+ "valueType": "number"
10760
+ }],
10237
10761
  "presence": [
10238
10762
  {
10239
10763
  "path": "state",
@@ -10745,379 +11269,7 @@ object({
10745
11269
  params: record(string(), union([string(), number()])).optional()
10746
11270
  });
10747
11271
  //#endregion
10748
- //#region ../types/dist/err-msg-BD5buxvT.mjs
10749
- var DeviceType = /* @__PURE__ */ function(DeviceType) {
10750
- DeviceType["Camera"] = "camera";
10751
- DeviceType["Hub"] = "hub";
10752
- DeviceType["Light"] = "light";
10753
- DeviceType["Siren"] = "siren";
10754
- DeviceType["Switch"] = "switch";
10755
- DeviceType["Sensor"] = "sensor";
10756
- DeviceType["Thermostat"] = "thermostat";
10757
- /** Air-conditioner / heat-pump climate device (HVAC) — shares the
10758
- * `climate-control` cap surface with `Thermostat` but renders a
10759
- * dedicated AC-appropriate control UI (mode chips, fan speed,
10760
- * independent vertical/horizontal swing). Sources: native Gree, and
10761
- * reusable by other AC integrations. */
10762
- DeviceType["Climate"] = "climate";
10763
- DeviceType["Button"] = "button";
10764
- /** Generic stateless event emitter — carries a device's EXACT declared
10765
- * event vocabulary verbatim (no normalization). Installed with the
10766
- * `event-emitter` cap. Sources: HA `event.*` entities (structured) and
10767
- * HA bus events (e.g. `zha_event`, generic). */
10768
- DeviceType["EventEmitter"] = "event-emitter";
10769
- /** Firmware/software update entity — current vs available version,
10770
- * updatable flag, update state, and an install action. Installed with
10771
- * the `update` cap. Sources: Homematic firmware-update channels (and
10772
- * reusable by other providers, e.g. HA `update.*` entities). */
10773
- DeviceType["Update"] = "update";
10774
- DeviceType["Generic"] = "generic";
10775
- /** Generic notification delivery target (HA `notify.<service>`, future
10776
- * Telegram / Discord / ntfy / SMTP, …). One device per delivery
10777
- * endpoint; the `notifier` cap defines the send surface. */
10778
- DeviceType["Notifier"] = "notifier";
10779
- /** Pre-recorded action sequence with optional parameters
10780
- * (HA `script.*`). Runnable via `script-runner` cap. */
10781
- DeviceType["Script"] = "script";
10782
- /** Automation rule (HA `automation.*`) — enable/disable + manual
10783
- * trigger surface exposed via `automation-control` cap. */
10784
- DeviceType["Automation"] = "automation";
10785
- /** Door / smart lock device (HA `lock.*`). `lock-control` cap. */
10786
- DeviceType["Lock"] = "lock";
10787
- /** Window covering, blinds, garage door, valve, etc. (HA `cover.*`,
10788
- * `valve.*`). `cover` cap with sub-roles for variant. */
10789
- DeviceType["Cover"] = "cover";
10790
- /** Pipe / water / gas valve with open/close/stop and optional
10791
- * position (HA `valve.*`). `valve` cap — a cover-sibling actuator
10792
- * modelled on the same open/closed lifecycle. */
10793
- DeviceType["Valve"] = "valve";
10794
- /** Humidifier / dehumidifier with on/off + target humidity + mode
10795
- * (HA `humidifier.*`). `humidifier` cap — a climate-family actuator
10796
- * modelled on the same target / mode lifecycle. */
10797
- DeviceType["Humidifier"] = "humidifier";
10798
- /** Water heater / boiler with target temperature + operation mode +
10799
- * away mode (HA `water_heater.*`). `water-heater` cap — a
10800
- * climate-family actuator. */
10801
- DeviceType["WaterHeater"] = "water-heater";
10802
- /** Ceiling / standing / exhaust fan (HA `fan.*`). `fan-control` cap. */
10803
- DeviceType["Fan"] = "fan";
10804
- /** Audio / video playback endpoint (HA `media_player.*`). Disjoint from
10805
- * the camera surface — those use `Camera`. `media-player` cap. */
10806
- DeviceType["MediaPlayer"] = "media-player";
10807
- /** Security panel / alarm system (HA `alarm_control_panel.*`).
10808
- * `alarm-panel` cap. */
10809
- DeviceType["AlarmPanel"] = "alarm-panel";
10810
- /** Generic user-settable input (HA `number` / `input_number` / `select`
10811
- * / `input_select` / `text` / `input_text` / `input_datetime`).
10812
- * Sub-type via `DeviceRole`: NumericControl / SelectControl /
10813
- * TextControl / DateTimeControl. */
10814
- DeviceType["Control"] = "control";
10815
- /** Person / device-tracker presence (HA `person.*`, `device_tracker.*`).
10816
- * `presence` cap. */
10817
- DeviceType["Presence"] = "presence";
10818
- /** Weather provider (HA `weather.*`). Tier-3, low MVP priority.
10819
- * `weather` cap. */
10820
- DeviceType["Weather"] = "weather";
10821
- /** Robot vacuum (HA `vacuum.*`). Tier-3. `vacuum-control` cap. */
10822
- DeviceType["Vacuum"] = "vacuum";
10823
- /** Robotic lawn mower (HA `lawn_mower.*`). Tier-3.
10824
- * `lawn-mower-control` cap. */
10825
- DeviceType["LawnMower"] = "lawn-mower";
10826
- /** Physical HA device group — parent container for entity-children
10827
- * adopted from a single HA device entry. Not renderable as a
10828
- * standalone device; exists only to anchor child entities. */
10829
- DeviceType["Container"] = "container";
10830
- /** Single still-image entity (HA `image.*`). Read-only display of an
10831
- * `entity_picture` signed URL the browser loads directly. `image` cap. */
10832
- DeviceType["Image"] = "image";
10833
- /** Smart pet feeder — cloud-connected food dispenser with a bowl food
10834
- * level, battery, desiccant life, feeding state and manual-feed /
10835
- * call-pet / maintenance actions. Installed with the `pet-feeder` cap;
10836
- * dual-hopper models (D4S/D4SH) expose per-hopper portions. Sources:
10837
- * native PetKit (`nodepetkit` `FeederDevice`), reusable by other feeder
10838
- * integrations sharing the same food/desiccant/hopper surface. */
10839
- DeviceType["PetFeeder"] = "pet-feeder";
10840
- /** Pet water fountain — tank level, water drunk, pump and its duty cycle.
10841
- * Installed with the `pet-fountain` cap. Reaches CamStack as loose entities
10842
- * of a container and is BUILT as a composed device (D967). */
10843
- DeviceType["PetFountain"] = "pet-fountain";
10844
- /** Built-in dishwasher — program lifecycle, door / remote-start gates,
10845
- * salt and rinse-aid levels. Installed with the `dishwasher` cap.
10846
- * Sources: native Bosch Home Connect, reusable by similar integrations.
10847
- * A plug-built dishwasher carries running-state and power instead. */
10848
- DeviceType["Dishwasher"] = "dishwasher";
10849
- /** Washing machine. A plug-built one is running while its power stays
10850
- * above a threshold. No program cap — that surface is the dishwasher's. */
10851
- DeviceType["Washer"] = "washer";
10852
- /** Tumble dryer. Same plug-built running surface as {@link Washer}. */
10853
- DeviceType["Dryer"] = "dryer";
10854
- /** The physical bridge box of a vendor ecosystem (Velux KLF-200, and later
10855
- * other radio-to-IP bridges). It carries the box's own facts (firmware,
10856
- * address, connection state) and the actions the box really supports
10857
- * (`reboot`, `device-admin-link`). Products are NOT its children — they are
10858
- * adopted beside it — and its connection settings stay on the integration. */
10859
- DeviceType["Gateway"] = "gateway";
10860
- return DeviceType;
10861
- }({});
10862
- var DeviceFeature = /* @__PURE__ */ function(DeviceFeature) {
10863
- DeviceFeature["BatteryOperated"] = "battery-operated";
10864
- DeviceFeature["Rebootable"] = "rebootable";
10865
- /**
10866
- * Device supports an on-demand re-sync of its derived spec with its
10867
- * upstream source — drives the generic Re-sync button. The owning
10868
- * provider implements the action via the `device-adoption.resync` cap.
10869
- */
10870
- DeviceFeature["Resyncable"] = "resyncable";
10871
- DeviceFeature["NativeSnapshot"] = "native-snapshot";
10872
- DeviceFeature["DoorbellButton"] = "doorbell-button";
10873
- DeviceFeature["TwoWayAudio"] = "two-way-audio";
10874
- DeviceFeature["PanTiltZoom"] = "pan-tilt-zoom";
10875
- /**
10876
- * Camera supports the on-firmware autotrack subsystem (subject-
10877
- * following). Distinct from `PanTiltZoom` because not every PTZ
10878
- * camera ships autotrack — the admin UI uses this flag to gate
10879
- * the autotrack toggle / settings card without re-deriving from
10880
- * the cap registry. Mirrors `ptz-autotrack` cap registration:
10881
- * driver sets this feature when probe confirms the firmware
10882
- * surface, and registers the cap in the same code path.
10883
- */
10884
- DeviceFeature["PtzAutotrack"] = "ptz-autotrack";
10885
- /**
10886
- * Accessory exposes a "trigger on motion" toggle — the parent camera's
10887
- * motion detection automatically activates this device. Mirrors
10888
- * `motion-trigger` cap registration: drivers set this feature in the
10889
- * same code path that calls `ctx.registerNativeCap(motionTriggerCapability, ...)`.
10890
- *
10891
- * Used by admin UI (gate the in-hero `MotionTriggerToggle` against a
10892
- * fast scalar without binding fetch), notifier rules, and `listAll`
10893
- * filters that want "all devices with on-motion behaviour".
10894
- */
10895
- DeviceFeature["MotionTrigger"] = "motion-trigger";
10896
- /** Light supports rgb-triplet color via `color` cap. */
10897
- DeviceFeature["LightColorRgb"] = "light-color-rgb";
10898
- /** Light supports HSV color via `color` cap. */
10899
- DeviceFeature["LightColorHsv"] = "light-color-hsv";
10900
- /** Light supports color-temperature (mired) via `color` cap. */
10901
- DeviceFeature["LightColorMired"] = "light-color-mired";
10902
- /** Thermostat supports a `heat_cool` dual setpoint (targetLow +
10903
- * targetHigh). Gates the range slider UI. */
10904
- DeviceFeature["ClimateDualSetpoint"] = "climate-dual-setpoint";
10905
- /** Thermostat exposes target humidity and/or current humidity
10906
- * readings. Gates the humidity controls. */
10907
- DeviceFeature["ClimateHumidity"] = "climate-humidity";
10908
- /** Thermostat exposes a fan-mode selector. */
10909
- DeviceFeature["ClimateFanMode"] = "climate-fan-mode";
10910
- /** Thermostat exposes preset modes (eco / away / sleep / vendor). */
10911
- DeviceFeature["ClimatePreset"] = "climate-preset";
10912
- /** Thermostat exposes a vertical louver swing toggle. Gates the
10913
- * vertical-swing switch in the climate UI. Independent of horizontal. */
10914
- DeviceFeature["ClimateSwingVertical"] = "climate-swing-vertical";
10915
- /** Thermostat exposes a horizontal louver swing toggle. Gates the
10916
- * horizontal-swing switch in the climate UI. Independent of vertical. */
10917
- DeviceFeature["ClimateSwingHorizontal"] = "climate-swing-horizontal";
10918
- /** Cover exposes intermediate position control (0..100). Gates the
10919
- * position slider UI. */
10920
- DeviceFeature["CoverPositionable"] = "cover-positionable";
10921
- /** Cover exposes slat-tilt control. Gates the tilt slider UI. */
10922
- DeviceFeature["CoverTilt"] = "cover-tilt";
10923
- /** Valve exposes intermediate position control (0..100). Gates the
10924
- * position slider / drag surface UI. */
10925
- DeviceFeature["ValvePositionable"] = "valve-positionable";
10926
- /** Fan exposes a speed-percentage setter. Gates the speed slider UI. */
10927
- DeviceFeature["FanSpeed"] = "fan-speed";
10928
- /** Fan exposes a preset mode selector. */
10929
- DeviceFeature["FanPreset"] = "fan-preset";
10930
- /** Fan exposes blade direction (forward/reverse) — typical of
10931
- * ceiling fans. */
10932
- DeviceFeature["FanDirection"] = "fan-direction";
10933
- /** Fan exposes an oscillation toggle. */
10934
- DeviceFeature["FanOscillating"] = "fan-oscillating";
10935
- /** Lock requires a PIN code on lock/unlock. Gates the code-entry
10936
- * field on the UI lock-controls panel. */
10937
- DeviceFeature["LockPinRequired"] = "lock-pin-required";
10938
- /** Lock supports a latch-release ("open door") action distinct from
10939
- * unlock. Mirrors HA `LockEntityFeature.OPEN` (bit 1) in
10940
- * `supported_features`. Gates the Open Door button in the UI. */
10941
- DeviceFeature["LockOpen"] = "lock-open";
10942
- /** Media player exposes a seek-to-position surface. */
10943
- DeviceFeature["MediaPlayerSeek"] = "media-player-seek";
10944
- /** Media player exposes a volume-level setter. */
10945
- DeviceFeature["MediaPlayerVolume"] = "media-player-volume";
10946
- /** Media player exposes a mute toggle distinct from volume=0. */
10947
- DeviceFeature["MediaPlayerMute"] = "media-player-mute";
10948
- /** Media player exposes a shuffle toggle. */
10949
- DeviceFeature["MediaPlayerShuffle"] = "media-player-shuffle";
10950
- /** Media player exposes a repeat mode (off / all / one). */
10951
- DeviceFeature["MediaPlayerRepeat"] = "media-player-repeat";
10952
- /** Media player exposes a source / input selector. */
10953
- DeviceFeature["MediaPlayerSelectSource"] = "media-player-select-source";
10954
- /** Media player exposes a play-arbitrary-media surface (URL / id). */
10955
- DeviceFeature["MediaPlayerPlayMedia"] = "media-player-play-media";
10956
- /** Media player exposes next-track. */
10957
- DeviceFeature["MediaPlayerNext"] = "media-player-next";
10958
- /** Media player exposes previous-track. */
10959
- DeviceFeature["MediaPlayerPrevious"] = "media-player-previous";
10960
- /** Media player exposes stop distinct from pause. */
10961
- DeviceFeature["MediaPlayerStop"] = "media-player-stop";
10962
- /** Alarm panel requires a PIN code on arm/disarm. */
10963
- DeviceFeature["AlarmPinRequired"] = "alarm-pin-required";
10964
- /** Presence device carries GPS coordinates (lat/lng/accuracy) in
10965
- * addition to a textual location. */
10966
- DeviceFeature["PresenceGps"] = "presence-gps";
10967
- /** Notifier accepts an inline / URL image attachment. */
10968
- DeviceFeature["NotifierImage"] = "notifier-image";
10969
- /** Notifier accepts a priority hint (high/normal/low). */
10970
- DeviceFeature["NotifierPriority"] = "notifier-priority";
10971
- /** Notifier accepts a free-form `data` payload for platform-specific
10972
- * fields. */
10973
- DeviceFeature["NotifierData"] = "notifier-data";
10974
- /** Notifier supports interactive action buttons / callbacks. */
10975
- DeviceFeature["NotifierActions"] = "notifier-actions";
10976
- /** Notifier supports per-call recipient targeting (multi-user). */
10977
- DeviceFeature["NotifierRecipients"] = "notifier-recipients";
10978
- /** Script runner accepts a variables map on each run invocation. */
10979
- DeviceFeature["ScriptVariables"] = "script-variables";
10980
- /** Automation `trigger` accepts a skipCondition flag — fires the
10981
- * automation's actions while bypassing its condition block. */
10982
- DeviceFeature["AutomationSkipCondition"] = "automation-skip-condition";
10983
- /** Robot vacuum exposes a live cleaning map (image child). Gates the
10984
- * map tile in the vacuum UI. */
10985
- DeviceFeature["VacuumHasMap"] = "vacuum-has-map";
10986
- /** Robot vacuum exposes AI obstacle-detection toggles. Gates the AI
10987
- * switches group. */
10988
- DeviceFeature["VacuumHasAi"] = "vacuum-has-ai";
10989
- /** Robot mower exposes a live mowing map (SVG image child). Gates the
10990
- * mower map tile. */
10991
- DeviceFeature["MowerHasMap"] = "mower-has-map";
10992
- /** Robot mower exposes targeted mowing (all-area / zones / edges /
10993
- * spots) — gates the Mow action + map selectors. */
10994
- DeviceFeature["MowerHasTargetedMowing"] = "mower-has-targeted-mowing";
10995
- return DeviceFeature;
10996
- }({});
10997
- /**
10998
- * Semantic role a device plays within its parent. Populated by driver
10999
- * addons when creating accessory devices (Reolink siren/floodlight/
11000
- * PIR/chime/autotrack/doorbell, ONVIF relay outputs, …). Used by the
11001
- * admin UI to pick icons, labels, and widgets — a `Switch` with
11002
- * `role: Floodlight` renders as a bulb with a brightness slider,
11003
- * whereas a `Switch` with `role: Siren` renders as a klaxon.
11004
- *
11005
- * Undefined for top-level devices (cameras, NVRs, hubs). Persisted in
11006
- * sqlite as a nullable TEXT column — old rows keep working unchanged.
11007
- */
11008
- var DeviceRole = /* @__PURE__ */ function(DeviceRole) {
11009
- DeviceRole["Siren"] = "siren";
11010
- DeviceRole["Floodlight"] = "floodlight";
11011
- DeviceRole["Spotlight"] = "spotlight";
11012
- DeviceRole["PirSensor"] = "pir-sensor";
11013
- DeviceRole["Chime"] = "chime";
11014
- DeviceRole["Autotrack"] = "autotrack";
11015
- DeviceRole["Nightvision"] = "nightvision";
11016
- DeviceRole["PrivacyMask"] = "privacy-mask";
11017
- DeviceRole["Doorbell"] = "doorbell";
11018
- /** Interior blind / shade / roller blind. HA `device_class: blind|shade`. */
11019
- DeviceRole["CoverBlind"] = "cover-blind";
11020
- /** Exterior roller shutter. HA `device_class: shutter`; Homematic BROLL. */
11021
- DeviceRole["CoverShutter"] = "cover-shutter";
11022
- /** Exterior awning. HA `device_class: awning`. */
11023
- DeviceRole["CoverAwning"] = "cover-awning";
11024
- /** Curtain / drape. HA `device_class: curtain`. */
11025
- DeviceRole["CoverCurtain"] = "cover-curtain";
11026
- /** Garage door. HA `device_class: garage`. A security actuator: exports gate it. */
11027
- DeviceRole["CoverGarage"] = "cover-garage";
11028
- /** Gate. HA `device_class: gate`. A security actuator: exports gate it. */
11029
- DeviceRole["CoverGate"] = "cover-gate";
11030
- /** Door. HA `device_class: door`. A security actuator: exports gate it. */
11031
- DeviceRole["CoverDoor"] = "cover-door";
11032
- /** Window. HA `device_class: window`. */
11033
- DeviceRole["CoverWindow"] = "cover-window";
11034
- /** Virtual HA toggle (input_boolean.*) — distinguishable from a
11035
- * real Switch device for UI rendering / export adapters. */
11036
- DeviceRole["BinaryHelper"] = "binary-helper";
11037
- /** A smart plug: a `switch` whose source says it is an outlet (HA
11038
- * `device_class: outlet`). The Alexa export shows it as a SMARTPLUG. */
11039
- DeviceRole["Outlet"] = "outlet";
11040
- /** Generic motion / occupancy / moving event source. Distinct from
11041
- * the camera accessory PirSensor role: that one is a camera child;
11042
- * this is a standalone HA / 3rd-party motion sensor. */
11043
- DeviceRole["MotionSensor"] = "motion-sensor";
11044
- DeviceRole["ContactSensor"] = "contact-sensor";
11045
- DeviceRole["LeakSensor"] = "leak-sensor";
11046
- DeviceRole["SmokeSensor"] = "smoke-sensor";
11047
- DeviceRole["COSensor"] = "co-sensor";
11048
- DeviceRole["GasSensor"] = "gas-sensor";
11049
- DeviceRole["TamperSensor"] = "tamper-sensor";
11050
- DeviceRole["VibrationSensor"] = "vibration-sensor";
11051
- DeviceRole["ConnectivitySensor"] = "connectivity-sensor";
11052
- DeviceRole["SoundSensor"] = "sound-sensor";
11053
- /** Fallback for `binary_sensor` without a known `device_class`. */
11054
- DeviceRole["BinarySensor"] = "binary-sensor";
11055
- DeviceRole["TemperatureSensor"] = "temperature-sensor";
11056
- DeviceRole["HumiditySensor"] = "humidity-sensor";
11057
- DeviceRole["AmbientLightSensor"] = "ambient-light-sensor";
11058
- DeviceRole["PressureSensor"] = "pressure-sensor";
11059
- /** Wind speed or direction (weather-station `wind-sensor` cap). */
11060
- DeviceRole["WindSensor"] = "wind-sensor";
11061
- /** Rain accumulation or rate (weather-station `rain-sensor` cap). */
11062
- DeviceRole["RainSensor"] = "rain-sensor";
11063
- /** UV index (weather-station `uv-sensor` cap). */
11064
- DeviceRole["UvSensor"] = "uv-sensor";
11065
- /** Solar irradiance W/m² (weather-station `solar-radiation-sensor` cap).
11066
- * Distinct from AmbientLightSensor (lux). */
11067
- DeviceRole["SolarRadiationSensor"] = "solar-radiation-sensor";
11068
- /** Soil moisture % (garden/weather `soil-moisture-sensor` cap). */
11069
- DeviceRole["SoilMoistureSensor"] = "soil-moisture-sensor";
11070
- DeviceRole["PowerSensor"] = "power-sensor";
11071
- DeviceRole["EnergySensor"] = "energy-sensor";
11072
- DeviceRole["VoltageSensor"] = "voltage-sensor";
11073
- DeviceRole["CurrentSensor"] = "current-sensor";
11074
- DeviceRole["AirQualitySensor"] = "air-quality-sensor";
11075
- /** Battery level (numeric % via `sensor` OR low-bool via
11076
- * `binary_sensor` — the cap distinguishes via the value type). */
11077
- DeviceRole["BatterySensor"] = "battery-sensor";
11078
- /** Fallback for `sensor` numeric without a known `device_class`. */
11079
- DeviceRole["NumericSensor"] = "numeric-sensor";
11080
- /** String / enum state (HA `sensor` with `state_class: enum` or
11081
- * `attributes.options`). */
11082
- DeviceRole["EnumSensor"] = "enum-sensor";
11083
- /** Date / timestamp state (HA `sensor` with `device_class: timestamp`
11084
- * or `date`). The slice carries the raw ISO string verbatim (hosted on
11085
- * the `enum-sensor` cap); the UI renders it locale-formatted. */
11086
- DeviceRole["DateTimeSensor"] = "datetime-sensor";
11087
- /** Last-resort fallback when nothing else matches. */
11088
- DeviceRole["GenericSensor"] = "generic-sensor";
11089
- DeviceRole["NumericControl"] = "numeric-control";
11090
- DeviceRole["SelectControl"] = "select-control";
11091
- DeviceRole["TextControl"] = "text-control";
11092
- DeviceRole["DateTimeControl"] = "datetime-control";
11093
- /** Mobile push notifier (HA `notify.mobile_app_*`) — supports
11094
- * rich features (image, priority, channel routing). */
11095
- DeviceRole["MobilePushNotifier"] = "mobile-push-notifier";
11096
- /** Chat / messaging service (HA `notify.telegram_*`,
11097
- * `notify.discord_*`, etc.). */
11098
- DeviceRole["MessagingNotifier"] = "messaging-notifier";
11099
- /** Email-based delivery (HA `notify.smtp`, etc.). */
11100
- DeviceRole["EmailNotifier"] = "email-notifier";
11101
- /** Fallback when the notifier service name doesn't match a known
11102
- * pattern. */
11103
- DeviceRole["GenericNotifier"] = "generic-notifier";
11104
- /** Emits a pulse train (infrared or RF) on request — the `ir-transmitter`
11105
- * cap. A `DeviceType.Generic` device. */
11106
- DeviceRole["IrTransmitter"] = "ir-transmitter";
11107
- return DeviceRole;
11108
- }({});
11109
- /**
11110
- import { errMsg } from '@camstack/types'
11111
- * Extract a human-readable message from an unknown error value.
11112
- * Replaces the ubiquitous `errMsg(err)` pattern.
11113
- */
11114
- function errMsg(err) {
11115
- if (err instanceof Error) return err.message;
11116
- if (typeof err === "string") return err;
11117
- return String(err);
11118
- }
11119
- //#endregion
11120
- //#region ../types/dist/composition-C7WdThmP.mjs
11272
+ //#region ../types/dist/composition-BBzVf2xd.mjs
11121
11273
  /**
11122
11274
  * The ffmpeg build every CamStack node downloads, per platform and arch.
11123
11275
  *
@@ -12335,6 +12487,90 @@ var EventOwnerTypeSchema = _enum(EVENT_OWNER_TYPES);
12335
12487
  /** The same list as a Zod enum, for the cap input that carries it. */
12336
12488
  var MediaPresenceOwnerKindSchema = _enum([...EVENT_OWNER_TYPES, "track"]);
12337
12489
  new Set(EVENT_OWNER_TYPES);
12490
+ /**
12491
+ * What a published package needs from the rest of the install (D989).
12492
+ *
12493
+ * One published framework package (`@camstack/server`) carries the kernel, the
12494
+ * builtins and the capability contracts, so "which addon runs on which
12495
+ * framework" is a question of version, and the answer is DECLARED by the
12496
+ * package that knows it:
12497
+ *
12498
+ * - an addon says the oldest server it runs on (`requires.server: ">=X"`);
12499
+ * - the server says the oldest addon it works with
12500
+ * (`requires.addons: { "@camstack/addon-x": ">=Y" }`) and the oldest shell
12501
+ * around it (`requires.wrapper: { desktop, image }`);
12502
+ * - a wrapper (the desktop app, the container starter) says which server
12503
+ * range it can host, and reports its own kind and version.
12504
+ *
12505
+ * The publish workflow STAMPS these after the version bump
12506
+ * (`scripts/ci/stamp-requires.mjs`); nobody types a number by hand.
12507
+ *
12508
+ * A requirement that cannot be checked is NOT met: a shell that reports no
12509
+ * version counts as below the minimum, and an unreadable running version is
12510
+ * named in the result (a failed read is never a pass).
12511
+ */
12512
+ /** `>=MAJOR.MINOR.PATCH[-prerelease]` — the only range shape a requirement uses. */
12513
+ var MinVersionSchema = string().regex(/^>=\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/, "a minimum is written \">=X.Y.Z\"");
12514
+ object({
12515
+ /** The oldest `@camstack/server` this package runs on. */
12516
+ server: MinVersionSchema.optional(),
12517
+ /** The oldest installed version of each named addon package. */
12518
+ addons: record(string(), MinVersionSchema).optional(),
12519
+ /** The oldest shell around the server, per shell kind. */
12520
+ wrapper: object({
12521
+ desktop: MinVersionSchema.optional(),
12522
+ image: MinVersionSchema.optional()
12523
+ }).optional()
12524
+ });
12525
+ /** One unmet requirement; each names the versions on both sides. */
12526
+ var UnmetRequirementSchema = discriminatedUnion("kind", [
12527
+ object({
12528
+ kind: literal("server-too-old"),
12529
+ required: string(),
12530
+ /** `null` — the running server version could not be read. */
12531
+ running: string().nullable()
12532
+ }),
12533
+ object({
12534
+ kind: literal("addon-too-old"),
12535
+ packageName: string(),
12536
+ required: string(),
12537
+ /** `null` — the addon is not installed (or its version is unreadable). */
12538
+ running: string().nullable()
12539
+ }),
12540
+ object({
12541
+ kind: literal("wrapper-too-old"),
12542
+ wrapper: _enum(["desktop", "image"]),
12543
+ required: string(),
12544
+ /** `null` — the wrapper reported no version. */
12545
+ running: string().nullable()
12546
+ })
12547
+ ]);
12548
+ var RequiredAddonUpdateSchema = object({
12549
+ packageName: string(),
12550
+ /** The minimum the target server declares. */
12551
+ required: string(),
12552
+ /** What this node has; `null` when the addon is not installed. */
12553
+ installed: string().nullable()
12554
+ });
12555
+ /**
12556
+ * Which integration instance a settings form is for. An addon that serves several instances
12557
+ * (`instanceMode: multiple`) can show a different form per instance — an NVR integration
12558
+ * needs its connection, the default integration needs the addon's own sections.
12559
+ *
12560
+ * - `integrationId` is `null` in the create wizard: nothing exists yet, the operator is
12561
+ * setting up a NEW instance.
12562
+ * - `mode` is the wizard mode of the integration type (`getAvailableTypes`).
12563
+ * - `candidateStableId` is the discovery candidate a wizard was seeded from, if any.
12564
+ */
12565
+ var IntegrationInstanceRefSchema = object({
12566
+ integrationId: string().nullable(),
12567
+ mode: _enum([
12568
+ "standalone",
12569
+ "account",
12570
+ "broker"
12571
+ ]),
12572
+ candidateStableId: string().optional()
12573
+ });
12338
12574
  var EncodeProfileSchema = object({
12339
12575
  video: object({
12340
12576
  codec: _enum([
@@ -12791,6 +13027,613 @@ var CameraSwitchGroupSchema = object({
12791
13027
  fetchedAt: number()
12792
13028
  });
12793
13029
  /**
13030
+ * Query filter for settings-store collections.
13031
+ */
13032
+ var QueryFilterSchema = object({
13033
+ where: record(string(), unknown()).optional(),
13034
+ whereIn: record(string(), array(unknown())).optional(),
13035
+ whereBetween: record(string(), tuple([unknown(), unknown()])).optional(),
13036
+ /** NULL-safe exclusion: matches rows whose field is NULL OR != the value. */
13037
+ whereNot: record(string(), unknown()).optional(),
13038
+ /** NULL-safe exclusion of a SET — `whereNot` for more than one value. An
13039
+ * empty list excludes nothing. See `QueryFilter.whereNotIn`. */
13040
+ whereNotIn: record(string(), array(unknown())).optional(),
13041
+ orderBy: object({
13042
+ field: string(),
13043
+ direction: _enum(["asc", "desc"])
13044
+ }).optional(),
13045
+ limit: number().optional(),
13046
+ offset: number().optional()
13047
+ });
13048
+ /**
13049
+ * The predicate half of a filter, for BULK MUTATIONS.
13050
+ *
13051
+ * Deliberately not `QueryFilterSchema`: `orderBy` / `limit` / `offset` have no
13052
+ * meaning for a statement that rewrites a set, and accepting them would invite
13053
+ * a caller to believe `limit` bounds the damage. Every field is optional here
13054
+ * only so the shape stays composable — the implementation REJECTS a filter
13055
+ * that compiles to no predicate, because that is the whole collection.
13056
+ */
13057
+ var MutationFilterSchema = object({
13058
+ where: record(string(), unknown()).optional(),
13059
+ whereIn: record(string(), array(unknown())).optional(),
13060
+ whereBetween: record(string(), tuple([unknown(), unknown()])).optional(),
13061
+ whereNot: record(string(), unknown()).optional(),
13062
+ whereNotIn: record(string(), array(unknown())).optional()
13063
+ });
13064
+ /**
13065
+ * One scalar an {@link settingsStoreCapability.methods.aggregate} call asks for.
13066
+ *
13067
+ * `as` names the slot in the result, so the SAME column may be asked twice with
13068
+ * two operations (`MIN(startMs)` and `MAX(startMs)` in one round trip) — which
13069
+ * a `Record<column, op>` shape could not express.
13070
+ */
13071
+ var AggregateFieldSchema = object({
13072
+ /** Result key. */
13073
+ as: string().min(1),
13074
+ /** Column to aggregate. Must be a real column of a declared collection. */
13075
+ field: string().min(1),
13076
+ op: _enum([
13077
+ "sum",
13078
+ "min",
13079
+ "max"
13080
+ ])
13081
+ });
13082
+ /**
13083
+ * `COUNT(*)` plus one number per requested field.
13084
+ *
13085
+ * `null` means NO ROW MATCHED, never zero: a `SUM` over an empty set and a sum
13086
+ * that really is 0 are different facts, and an accounting caller that renders
13087
+ * "0 bytes, oldest = 0" for "nothing here" reports a lie about a disk.
13088
+ */
13089
+ var AggregateResultSchema = object({
13090
+ count: number().int(),
13091
+ values: record(string(), number().nullable())
13092
+ });
13093
+ /** A single stored record: `{ id, data }`. */
13094
+ var SettingsRecordSchema = object({
13095
+ id: string(),
13096
+ data: record(string(), unknown())
13097
+ });
13098
+ /**
13099
+ * One record of a BULK insert — {@link SettingsRecordSchema} with the id made
13100
+ * optional.
13101
+ *
13102
+ * A separate schema rather than loosening the shared one: every other method
13103
+ * on this cap addresses a row BY its id, and making that field optional
13104
+ * everywhere would turn a forgotten key into a silently generated one on
13105
+ * `update` and `delete` as well.
13106
+ */
13107
+ var BulkRecordSchema = object({
13108
+ id: string().optional(),
13109
+ data: record(string(), unknown())
13110
+ });
13111
+ /**
13112
+ * The database FILES one `sqlite-settings` engine process owns (D851).
13113
+ * Closed on purpose: a file is a thing the engine opens, backs up and gives a
13114
+ * serving thread, so a new one is a code change, never a string a caller
13115
+ * invents. `settings` is `camstack.db` and the default for every collection
13116
+ * that names no file.
13117
+ */
13118
+ var DataStoreDatabaseSchema = _enum([
13119
+ "settings",
13120
+ "events",
13121
+ "history"
13122
+ ]);
13123
+ DataStoreDatabaseSchema.options;
13124
+ /**
13125
+ * One write inside a {@link settingsStoreCapability.methods.transact} call.
13126
+ * The three that a "replace this set, bump that revision" write needs, plus
13127
+ * the batched upsert a rollup writer needs.
13128
+ */
13129
+ var TransactOpSchema = discriminatedUnion("op", [
13130
+ object({
13131
+ op: literal("deleteWhere"),
13132
+ collection: string(),
13133
+ filter: MutationFilterSchema
13134
+ }),
13135
+ object({
13136
+ op: literal("insertMany"),
13137
+ collection: string(),
13138
+ records: array(BulkRecordSchema).readonly()
13139
+ }),
13140
+ object({
13141
+ op: literal("update"),
13142
+ collection: string(),
13143
+ id: string(),
13144
+ data: record(string(), unknown())
13145
+ }),
13146
+ object({
13147
+ op: literal("upsertMany"),
13148
+ collection: string(),
13149
+ conflict: array(string()).readonly(),
13150
+ records: array(BulkRecordSchema).readonly()
13151
+ })
13152
+ ]);
13153
+ /**
13154
+ * The precondition of a transaction: the row `key` of `collection` must hold
13155
+ * `equals` in `column`, read INSIDE the transaction.
13156
+ */
13157
+ var TransactGuardSchema = object({
13158
+ collection: string(),
13159
+ key: string(),
13160
+ column: string(),
13161
+ equals: unknown()
13162
+ });
13163
+ /**
13164
+ * Column declaration for a structured (SQL-backed) collection.
13165
+ *
13166
+ * Logical types — the backend translates each to the matching SQLite
13167
+ * storage class and handles per-type marshaling:
13168
+ * - `TEXT` / `INTEGER` / `REAL` — native SQLite types, pass-through
13169
+ * - `JSON` — TEXT under the hood; serialised on write, parsed on read
13170
+ * - `BOOLEAN` — INTEGER 0/1 under the hood; coerced both directions
13171
+ */
13172
+ var CollectionColumnSchema = object({
13173
+ name: string(),
13174
+ type: _enum([
13175
+ "TEXT",
13176
+ "INTEGER",
13177
+ "REAL",
13178
+ "JSON",
13179
+ "BOOLEAN"
13180
+ ]),
13181
+ primaryKey: boolean().optional(),
13182
+ notNull: boolean().optional(),
13183
+ unique: boolean().optional(),
13184
+ /**
13185
+ * Column DEFAULT. Required for B2: the four `ensureTable` consumers declare
13186
+ * columns like `enabled INTEGER NOT NULL DEFAULT 1`, and without this the
13187
+ * collection surface simply cannot express their existing tables — which is
13188
+ * why they were still on the uncapped `table*` API.
13189
+ */
13190
+ defaultValue: union([
13191
+ string(),
13192
+ number(),
13193
+ boolean()
13194
+ ]).optional()
13195
+ });
13196
+ var CollectionIndexSchema = object({
13197
+ name: string(),
13198
+ columns: array(string()).readonly(),
13199
+ unique: boolean().optional()
13200
+ });
13201
+ /**
13202
+ * The input of `declareCollection` — ONE schema for the door and for the
13203
+ * engine behind it, so the two surfaces cannot drift (the dispatcher forwards
13204
+ * the payload untouched).
13205
+ *
13206
+ * - `database` — the file the collection lives in (D851). Absent means
13207
+ * `settings`. A declaration naming a file other than the one the table is
13208
+ * already in is REFUSED by the engine, never moved.
13209
+ * - `primaryKey` — a COMPOSITE key, as column names. A single name is the same
13210
+ * as `primaryKey: true` on that column. A composite-keyed collection has no
13211
+ * `id`: it is written with `insertMany` / `upsertMany` / `transact` and read
13212
+ * with `query` / `count` / `aggregate` / `deleteWhere`; the calls that
13213
+ * address one row by id are refused.
13214
+ * - `withoutRowid` — emit `WITHOUT ROWID`. Requires a primary key.
13215
+ */
13216
+ var DeclareCollectionInputSchema = object({
13217
+ namespace: string().optional(),
13218
+ collection: string(),
13219
+ columns: array(CollectionColumnSchema).readonly(),
13220
+ indexes: array(CollectionIndexSchema).readonly().optional(),
13221
+ database: DataStoreDatabaseSchema.optional(),
13222
+ primaryKey: array(string()).readonly().optional(),
13223
+ withoutRowid: boolean().optional()
13224
+ });
13225
+ method(object({
13226
+ namespace: string().optional(),
13227
+ collection: string(),
13228
+ key: string()
13229
+ }), unknown(), { auth: "admin" }), method(object({
13230
+ namespace: string().optional(),
13231
+ collection: string(),
13232
+ key: string(),
13233
+ value: unknown()
13234
+ }), _void(), {
13235
+ kind: "mutation",
13236
+ auth: "admin"
13237
+ }), method(object({
13238
+ namespace: string().optional(),
13239
+ collection: string(),
13240
+ filter: QueryFilterSchema.optional(),
13241
+ columns: array(string()).readonly().optional()
13242
+ }), array(SettingsRecordSchema).readonly(), { auth: "admin" }), method(object({
13243
+ namespace: string().optional(),
13244
+ collection: string(),
13245
+ /** Declared columns identifying the slot; must be covered by a UNIQUE
13246
+ * index on the collection. Never empty. */
13247
+ conflict: array(string()).readonly(),
13248
+ record: BulkRecordSchema
13249
+ }), object({
13250
+ /** The id the row HAS: assigned by SQLite on an insert, or the existing
13251
+ * row's own id on an update. */
13252
+ id: union([string(), number()]) }), {
13253
+ kind: "mutation",
13254
+ auth: "admin"
13255
+ }), method(object({
13256
+ namespace: string().optional(),
13257
+ collection: string(),
13258
+ conflict: array(string()).readonly(),
13259
+ records: array(BulkRecordSchema).readonly()
13260
+ }), object({ upserted: number().int() }), {
13261
+ kind: "mutation",
13262
+ auth: "admin"
13263
+ }), method(object({
13264
+ namespace: string().optional(),
13265
+ collection: string(),
13266
+ record: object({
13267
+ id: string().optional(),
13268
+ data: record(string(), unknown())
13269
+ })
13270
+ }), object({
13271
+ /**
13272
+ * The id the row ACTUALLY got (D473): the one supplied, the UUID minted
13273
+ * for an absent one, or the ROWID SQLite assigned on an `INTEGER`
13274
+ * primary key — which is the only place an auto key is knowable.
13275
+ */
13276
+ id: union([string(), number()]) }), {
13277
+ kind: "mutation",
13278
+ auth: "admin"
13279
+ }), method(object({
13280
+ namespace: string().optional(),
13281
+ collection: string(),
13282
+ records: array(BulkRecordSchema).readonly()
13283
+ }), object({ inserted: number().int() }), {
13284
+ kind: "mutation",
13285
+ auth: "admin"
13286
+ }), method(object({
13287
+ namespace: string().optional(),
13288
+ guard: TransactGuardSchema.optional(),
13289
+ ops: array(TransactOpSchema).readonly()
13290
+ }), object({
13291
+ applied: boolean(),
13292
+ current: unknown().optional()
13293
+ }), {
13294
+ kind: "mutation",
13295
+ auth: "admin"
13296
+ }), method(object({
13297
+ namespace: string().optional(),
13298
+ collection: string(),
13299
+ id: string(),
13300
+ data: record(string(), unknown())
13301
+ }), _void(), {
13302
+ kind: "mutation",
13303
+ auth: "admin"
13304
+ }), method(object({
13305
+ namespace: string().optional(),
13306
+ collection: string(),
13307
+ key: string()
13308
+ }), _void(), {
13309
+ kind: "mutation",
13310
+ auth: "admin"
13311
+ }), method(object({
13312
+ namespace: string().optional(),
13313
+ collection: string(),
13314
+ filter: MutationFilterSchema
13315
+ }), object({ deleted: number().int() }), {
13316
+ kind: "mutation",
13317
+ auth: "admin"
13318
+ }), method(object({
13319
+ namespace: string().optional(),
13320
+ collection: string(),
13321
+ filter: MutationFilterSchema,
13322
+ data: record(string(), unknown())
13323
+ }), object({ updated: number().int() }), {
13324
+ kind: "mutation",
13325
+ auth: "admin"
13326
+ }), method(object({
13327
+ namespace: string().optional(),
13328
+ collection: string(),
13329
+ filter: QueryFilterSchema.optional()
13330
+ }), number(), { auth: "admin" }), method(object({
13331
+ namespace: string().optional(),
13332
+ collection: string(),
13333
+ fields: array(AggregateFieldSchema).readonly(),
13334
+ filter: QueryFilterSchema.optional()
13335
+ }), AggregateResultSchema, { auth: "admin" }), method(object({
13336
+ namespace: string().optional(),
13337
+ collection: string(),
13338
+ field: string(),
13339
+ bucketSize: number().int().positive(),
13340
+ origin: number().int(),
13341
+ filter: QueryFilterSchema.optional()
13342
+ }), array(object({
13343
+ bucket: number().int(),
13344
+ count: number().int()
13345
+ })).readonly(), { auth: "admin" }), method(object({
13346
+ namespace: string().optional(),
13347
+ collection: string()
13348
+ }), boolean(), { auth: "admin" }), method(DeclareCollectionInputSchema, _void(), {
13349
+ kind: "mutation",
13350
+ auth: "admin"
13351
+ });
13352
+ 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>\"");
13353
+ var MigrationLevelSchema = _enum([
13354
+ "hub-store",
13355
+ "addon-state",
13356
+ "node-config",
13357
+ "viewer",
13358
+ "addon-internal"
13359
+ ]);
13360
+ var MigrationStoreSchema = _enum([
13361
+ "settings",
13362
+ "events",
13363
+ "history",
13364
+ "node-local",
13365
+ "addon-files"
13366
+ ]);
13367
+ var MigrationScopeSchema = _enum(["cluster", "per-node"]);
13368
+ /** What depends on a migration while it is pending: `block-owner` is the default. */
13369
+ var MigrationOnFailureSchema = _enum(["block-owner", "tolerate"]);
13370
+ /**
13371
+ * `sql` is a hub-only body over the owner's connection. `plan` returns edits the
13372
+ * engine applies inside the migration's own transaction (hub-owned: computed in
13373
+ * that transaction; addon-owned: computed by the owner and shipped to the engine).
13374
+ * `procedure` is addon-owned work that is not SQL (keychain, integration registry,
13375
+ * device tombstones); the ledger records its outcome and it must be idempotent.
13376
+ */
13377
+ var MigrationBodyKindSchema = _enum([
13378
+ "sql",
13379
+ "plan",
13380
+ "procedure"
13381
+ ]);
13382
+ var MigrationMetaSchema = object({
13383
+ id: MigrationIdSchema,
13384
+ /** `hub`, an addon id, `node` or `viewer`. */
13385
+ owner: string().regex(/^[a-z0-9-]+$/),
13386
+ level: MigrationLevelSchema,
13387
+ /** Strictly increasing per owner: the order within an owner. */
13388
+ seq: number().int().positive(),
13389
+ /** Ids of any owner: the order across owners. */
13390
+ dependsOn: array(MigrationIdSchema),
13391
+ store: MigrationStoreSchema,
13392
+ scope: MigrationScopeSchema,
13393
+ onFailure: MigrationOnFailureSchema,
13394
+ bodyKind: MigrationBodyKindSchema,
13395
+ description: string().min(1),
13396
+ /** The marked field the migration's spec must assert survived. */
13397
+ fixtureMark: string().min(1)
13398
+ }).refine((m) => m.id.startsWith(`${m.owner}/`), {
13399
+ message: "the id must start with \"<owner>/\"",
13400
+ path: ["id"]
13401
+ });
13402
+ /** A cycle in the dependency graph, named by the ids on it. */
13403
+ var MigrationCycleError = class extends Error {
13404
+ ids;
13405
+ constructor(ids) {
13406
+ super(`migration dependsOn cycle: ${ids.join(" -> ")}`);
13407
+ this.ids = ids;
13408
+ this.name = "MigrationCycleError";
13409
+ }
13410
+ };
13411
+ /**
13412
+ * Topological order over `dependsOn` plus the implicit edge "a lower `seq` of an
13413
+ * owner runs first". Ties are broken by `(owner, seq)`. Throws
13414
+ * {@link MigrationCycleError} on a cycle. A `dependsOn` id that is not in the list
13415
+ * is ignored here — the catalogue schema is what refuses it.
13416
+ */
13417
+ function orderMigrations(items) {
13418
+ const byId = new Map(items.map((item) => [item.id, item]));
13419
+ const deps = /* @__PURE__ */ new Map();
13420
+ const ownerSorted = /* @__PURE__ */ new Map();
13421
+ for (const item of items) {
13422
+ const list = ownerSorted.get(item.owner) ?? [];
13423
+ list.push(item);
13424
+ ownerSorted.set(item.owner, list);
13425
+ }
13426
+ for (const item of items) deps.set(item.id, new Set(item.dependsOn.filter((id) => byId.has(id))));
13427
+ for (const list of ownerSorted.values()) {
13428
+ list.sort((a, b) => a.seq - b.seq);
13429
+ for (let i = 1; i < list.length; i++) {
13430
+ const previous = list[i - 1];
13431
+ const current = list[i];
13432
+ if (previous !== void 0 && current !== void 0) deps.get(current.id)?.add(previous.id);
13433
+ }
13434
+ }
13435
+ const tie = (a, b) => a.owner < b.owner ? -1 : a.owner > b.owner ? 1 : a.seq - b.seq;
13436
+ const done = /* @__PURE__ */ new Set();
13437
+ const ordered = [];
13438
+ let remaining = [...items];
13439
+ while (remaining.length > 0) {
13440
+ const ready = remaining.filter((item) => [...deps.get(item.id) ?? []].every((dep) => done.has(dep)));
13441
+ if (ready.length === 0) throw new MigrationCycleError(remaining.map((item) => item.id).sort());
13442
+ ready.sort(tie);
13443
+ const next = ready[0];
13444
+ if (next === void 0) break;
13445
+ ordered.push(next);
13446
+ done.add(next.id);
13447
+ remaining = remaining.filter((item) => item.id !== next.id);
13448
+ }
13449
+ return ordered;
13450
+ }
13451
+ /** Every way a list of migrations (in declaration order) is not a catalogue. */
13452
+ function catalogueIssues(items) {
13453
+ const issues = [];
13454
+ const seen = /* @__PURE__ */ new Set();
13455
+ const known = new Set(items.map((item) => item.id));
13456
+ const lastSeq = /* @__PURE__ */ new Map();
13457
+ const seqUsed = /* @__PURE__ */ new Map();
13458
+ for (const item of items) {
13459
+ if (seen.has(item.id)) issues.push({
13460
+ code: "duplicate-id",
13461
+ ids: [item.id],
13462
+ message: `duplicate id ${item.id}`
13463
+ });
13464
+ seen.add(item.id);
13465
+ const used = seqUsed.get(item.owner) ?? /* @__PURE__ */ new Set();
13466
+ if (used.has(item.seq)) issues.push({
13467
+ code: "duplicate-seq",
13468
+ ids: [item.id],
13469
+ message: `owner ${item.owner} has two migrations with seq ${item.seq}`
13470
+ });
13471
+ else if (item.seq < (lastSeq.get(item.owner) ?? 0)) issues.push({
13472
+ code: "seq-order",
13473
+ ids: [item.id],
13474
+ message: `seq ${item.seq} of ${item.id} does not increase in declaration order (owner ${item.owner})`
13475
+ });
13476
+ used.add(item.seq);
13477
+ seqUsed.set(item.owner, used);
13478
+ lastSeq.set(item.owner, Math.max(item.seq, lastSeq.get(item.owner) ?? 0));
13479
+ for (const dep of item.dependsOn) if (!known.has(dep)) issues.push({
13480
+ code: "unknown-dependency",
13481
+ ids: [item.id, dep],
13482
+ message: `${item.id} dependsOn unknown id ${dep}`
13483
+ });
13484
+ }
13485
+ try {
13486
+ orderMigrations(items);
13487
+ } catch (err) {
13488
+ if (err instanceof MigrationCycleError) issues.push({
13489
+ code: "cycle",
13490
+ ids: err.ids,
13491
+ message: err.message
13492
+ });
13493
+ else throw err;
13494
+ }
13495
+ return issues;
13496
+ }
13497
+ array(MigrationMetaSchema).superRefine((items, ctx) => {
13498
+ for (const issue of catalogueIssues(items)) ctx.addIssue({
13499
+ code: "custom",
13500
+ message: `${issue.code}: ${issue.message}`
13501
+ });
13502
+ });
13503
+ /**
13504
+ * The canonical JSON (`preconditionOf`) of the value a plan read; the apply step
13505
+ * re-reads, and a plan whose read no longer matches is refused (the migration is
13506
+ * `failed`, nothing it edited is kept).
13507
+ */
13508
+ var PreconditionSchema = string().min(1).optional();
13509
+ var MigrationPlanSchema = object({ edits: array(discriminatedUnion("op", [
13510
+ object({
13511
+ op: literal("setKeys"),
13512
+ scope: string().min(1),
13513
+ values: record(string(), unknown()),
13514
+ precondition: PreconditionSchema
13515
+ }),
13516
+ object({
13517
+ op: literal("deleteKeys"),
13518
+ scope: string().min(1),
13519
+ keys: array(string().min(1)).min(1),
13520
+ precondition: PreconditionSchema
13521
+ }),
13522
+ object({
13523
+ op: literal("putRow"),
13524
+ table: string().min(1),
13525
+ key: record(string(), unknown()),
13526
+ row: record(string(), unknown()),
13527
+ precondition: PreconditionSchema
13528
+ }),
13529
+ object({
13530
+ op: literal("deleteRow"),
13531
+ table: string().min(1),
13532
+ key: record(string(), unknown()),
13533
+ precondition: PreconditionSchema
13534
+ }),
13535
+ object({
13536
+ op: literal("upsertRecord"),
13537
+ kind: string().min(1),
13538
+ recordKey: string().min(1),
13539
+ value: unknown(),
13540
+ precondition: PreconditionSchema
13541
+ }),
13542
+ object({
13543
+ op: literal("deleteRecord"),
13544
+ kind: string().min(1),
13545
+ recordKey: string().min(1),
13546
+ precondition: PreconditionSchema
13547
+ })
13548
+ ])) });
13549
+ /**
13550
+ * What a `procedure` body reports. `already-done` means the rerun observed the
13551
+ * work finished (a crash between the work and the ledger row, or a manual fix)
13552
+ * and did none of it again; the ledger records `applied` either way.
13553
+ */
13554
+ var MigrationProcedureResultSchema = object({
13555
+ outcome: _enum(["done", "already-done"]),
13556
+ summary: record(string(), unknown())
13557
+ });
13558
+ /** What an owner hands the engine when one of its migrations ran. */
13559
+ var MigrationOwnerResultSchema = discriminatedUnion("kind", [
13560
+ object({
13561
+ kind: literal("plan"),
13562
+ plan: MigrationPlanSchema
13563
+ }),
13564
+ object({
13565
+ kind: literal("procedure"),
13566
+ result: MigrationProcedureResultSchema
13567
+ }),
13568
+ object({
13569
+ kind: literal("failed"),
13570
+ error: string().min(1)
13571
+ })
13572
+ ]);
13573
+ /**
13574
+ * `held`: a restore of a pre-migration snapshot undid this id; it does not rerun
13575
+ * until the operator retries or a new code version ships.
13576
+ * `skipped-newer`: the database knows an id this code does not.
13577
+ */
13578
+ var MigrationStatusSchema = _enum([
13579
+ "applied",
13580
+ "failed",
13581
+ "blocked",
13582
+ "running",
13583
+ "held",
13584
+ "skipped-newer"
13585
+ ]);
13586
+ var MigrationLedgerEntrySchema = object({
13587
+ id: string(),
13588
+ database: DataStoreDatabaseSchema,
13589
+ owner: string(),
13590
+ level: MigrationLevelSchema,
13591
+ seq: number().int().nonnegative(),
13592
+ status: MigrationStatusSchema,
13593
+ attempts: number().int().nonnegative(),
13594
+ appliedAt: number().nullable(),
13595
+ codeVersion: string().nullable(),
13596
+ /** What the apply changed: rows per table, as the engine counted them. */
13597
+ planSummary: record(string(), unknown()).nullable(),
13598
+ progress: record(string(), unknown()).nullable(),
13599
+ error: string().nullable(),
13600
+ /** Why it is `blocked` or `held` (`dependency-failed`, `snapshot-refused`, …). */
13601
+ reason: string().nullable(),
13602
+ nodeId: string().nullable(),
13603
+ bootId: string().nullable()
13604
+ });
13605
+ /** A catalogued migration that is not applied, with where it stands. */
13606
+ var MigrationPendingEntrySchema = object({
13607
+ meta: MigrationMetaSchema,
13608
+ database: DataStoreDatabaseSchema,
13609
+ state: _enum([
13610
+ "pending",
13611
+ "failed",
13612
+ "blocked",
13613
+ "held"
13614
+ ]),
13615
+ reason: string().nullable()
13616
+ });
13617
+ /** Rows per table before and after a dry run, and what it would have changed. */
13618
+ var MigrationDryRunResultSchema = object({
13619
+ id: string(),
13620
+ ok: boolean(),
13621
+ error: string().nullable(),
13622
+ rowsChanged: number().int().nonnegative(),
13623
+ tables: array(object({
13624
+ table: string(),
13625
+ before: number().int().nonnegative(),
13626
+ after: number().int().nonnegative()
13627
+ }))
13628
+ });
13629
+ /** One of an owner's migrations, as the engine tells the owner to treat it this boot. */
13630
+ var MigrationOwnerStepSchema = object({
13631
+ id: MigrationIdSchema,
13632
+ action: _enum(["run", "skip"]),
13633
+ /** The ledger state the engine found: `pending` has no row yet. */
13634
+ state: union([literal("pending"), MigrationStatusSchema])
13635
+ });
13636
+ /**
12794
13637
  * The trainer contract — what the hub hands the cloud trainer and what it
12795
13638
  * gets back (cloud training, D805+).
12796
13639
  *
@@ -16193,6 +17036,41 @@ var ACTUATOR_CAP_VOCABULARY = {
16193
17036
  icon: "droplets"
16194
17037
  }]
16195
17038
  },
17039
+ "power-source": {
17040
+ scope: "device",
17041
+ noun: t("Power source", "Alimentazione"),
17042
+ actions: [
17043
+ {
17044
+ id: "powerSource.on",
17045
+ method: "powerOn",
17046
+ label: t("Power on", "Dai corrente"),
17047
+ phrase: t("Power on {device}", "Dai corrente a {device}"),
17048
+ icon: "power"
17049
+ },
17050
+ {
17051
+ id: "powerSource.off",
17052
+ method: "powerOff",
17053
+ label: t("Power off", "Togli corrente"),
17054
+ phrase: t("Cut the power of {device}", "Togli la corrente a {device}"),
17055
+ icon: "power-off"
17056
+ },
17057
+ {
17058
+ id: "powerSource.forceRestart",
17059
+ method: "forceRestart",
17060
+ label: t("Force restart", "Riavvio forzato"),
17061
+ phrase: t("Force restart {device} by cutting its power", "Riavvia {device} togliendo la corrente"),
17062
+ icon: "refresh-cw"
17063
+ }
17064
+ ],
17065
+ state: [{
17066
+ kind: "boolean",
17067
+ field: "switchOn",
17068
+ label: t("Power switch on", "Presa accesa"),
17069
+ whenTrue: t("When the power of {device} comes on", "Quando arriva la corrente a {device}"),
17070
+ whenFalse: t("When the power of {device} is cut", "Quando manca la corrente a {device}"),
17071
+ icon: "power"
17072
+ }]
17073
+ },
16196
17074
  "lock-control": {
16197
17075
  scope: "device",
16198
17076
  noun: t("Lock", "Serratura"),
@@ -16274,6 +17152,11 @@ var ACTUATOR_CAP_VOCABULARY = {
16274
17152
  value: "jammed",
16275
17153
  label: t("Jammed", "Inceppata"),
16276
17154
  phrase: t("When {device} jams", "Quando {device} si inceppa")
17155
+ },
17156
+ {
17157
+ value: "door-open",
17158
+ label: t("Door open", "Porta aperta"),
17159
+ phrase: t("When the door of {device} opens", "Quando la porta di {device} si apre")
16277
17160
  }
16278
17161
  ],
16279
17162
  icon: "lock"
@@ -16663,7 +17546,7 @@ var ACTUATOR_CAP_VOCABULARY = {
16663
17546
  method: "setDirection",
16664
17547
  label: t("Spin forward", "Rotazione avanti"),
16665
17548
  phrase: t("Spin {device} forward", "Fai girare {device} in avanti"),
16666
- icon: "rotate-cw",
17549
+ icon: "refresh-cw",
16667
17550
  effect: {
16668
17551
  field: "direction",
16669
17552
  value: "forward"
@@ -19741,6 +20624,7 @@ var SYSTEM_CAP_VOCABULARY = {
19741
20624
  "login-method": system(IDENTITY),
19742
20625
  "mesh-network": system(NETWORK),
19743
20626
  "metrics-provider": system(CLUSTER),
20627
+ migrations: system(INTERNAL),
19744
20628
  "model-convert": system(ML),
19745
20629
  "model-distributor": system(ML),
19746
20630
  "mqtt-broker": system(NETWORK),
@@ -42084,7 +42968,7 @@ method(object({}), array(TargetKindSchema)), method(object({}), array(TargetSche
42084
42968
  * rebuilds without manual reload.
42085
42969
  *
42086
42970
  * The hub-local builtin `addon-pages-aggregator` (see
42087
- * `@camstack/system/builtins/addon-pages-aggregator`) registers the
42971
+ * `@camstack/server/system/builtins/addon-pages-aggregator`) registers the
42088
42972
  * provider. Splitting the public aggregator from the raw collection
42089
42973
  * keeps both ends in codegen — there's no hand-written
42090
42974
  * `addon-pages.router.ts` wrapper anymore.
@@ -42210,6 +43094,10 @@ method(object({
42210
43094
  }), method(object({
42211
43095
  addonId: string(),
42212
43096
  nodeId: string().optional()
43097
+ }), SettingsSchemaWithValuesSchema.nullable()), method(object({
43098
+ addonId: string(),
43099
+ nodeId: string().optional(),
43100
+ instance: IntegrationInstanceRefSchema
42213
43101
  }), SettingsSchemaWithValuesSchema.nullable()), method(object({
42214
43102
  addonId: string(),
42215
43103
  nodeId: string().optional()
@@ -42380,7 +43268,7 @@ method(_void(), array(WidgetMetadataSchema).readonly(), { auth: "admin" });
42380
43268
  * manual reload — same scheme used by `addon-pages`.
42381
43269
  *
42382
43270
  * The hub-local builtin `addon-widgets-aggregator` (see
42383
- * `@camstack/system/builtins/addon-widgets-aggregator`) registers the
43271
+ * `@camstack/server/system/builtins/addon-widgets-aggregator`) registers the
42384
43272
  * provider. Splitting the public aggregator from the raw collection
42385
43273
  * keeps both ends in codegen — there's no hand-written wrapper.
42386
43274
  */
@@ -42741,329 +43629,6 @@ method(object({
42741
43629
  password: string()
42742
43630
  }), AuthResultSchema.nullable(), { kind: "mutation" }), method(object({ state: string() }), string()), method(record(string(), string()), AuthResultSchema, { kind: "mutation" }), method(object({ token: string() }), AuthResultSchema.nullable());
42743
43631
  /**
42744
- * Query filter for settings-store collections.
42745
- */
42746
- var QueryFilterSchema = object({
42747
- where: record(string(), unknown()).optional(),
42748
- whereIn: record(string(), array(unknown())).optional(),
42749
- whereBetween: record(string(), tuple([unknown(), unknown()])).optional(),
42750
- /** NULL-safe exclusion: matches rows whose field is NULL OR != the value. */
42751
- whereNot: record(string(), unknown()).optional(),
42752
- /** NULL-safe exclusion of a SET — `whereNot` for more than one value. An
42753
- * empty list excludes nothing. See `QueryFilter.whereNotIn`. */
42754
- whereNotIn: record(string(), array(unknown())).optional(),
42755
- orderBy: object({
42756
- field: string(),
42757
- direction: _enum(["asc", "desc"])
42758
- }).optional(),
42759
- limit: number().optional(),
42760
- offset: number().optional()
42761
- });
42762
- /**
42763
- * The predicate half of a filter, for BULK MUTATIONS.
42764
- *
42765
- * Deliberately not `QueryFilterSchema`: `orderBy` / `limit` / `offset` have no
42766
- * meaning for a statement that rewrites a set, and accepting them would invite
42767
- * a caller to believe `limit` bounds the damage. Every field is optional here
42768
- * only so the shape stays composable — the implementation REJECTS a filter
42769
- * that compiles to no predicate, because that is the whole collection.
42770
- */
42771
- var MutationFilterSchema = object({
42772
- where: record(string(), unknown()).optional(),
42773
- whereIn: record(string(), array(unknown())).optional(),
42774
- whereBetween: record(string(), tuple([unknown(), unknown()])).optional(),
42775
- whereNot: record(string(), unknown()).optional(),
42776
- whereNotIn: record(string(), array(unknown())).optional()
42777
- });
42778
- /**
42779
- * One scalar an {@link settingsStoreCapability.methods.aggregate} call asks for.
42780
- *
42781
- * `as` names the slot in the result, so the SAME column may be asked twice with
42782
- * two operations (`MIN(startMs)` and `MAX(startMs)` in one round trip) — which
42783
- * a `Record<column, op>` shape could not express.
42784
- */
42785
- var AggregateFieldSchema = object({
42786
- /** Result key. */
42787
- as: string().min(1),
42788
- /** Column to aggregate. Must be a real column of a declared collection. */
42789
- field: string().min(1),
42790
- op: _enum([
42791
- "sum",
42792
- "min",
42793
- "max"
42794
- ])
42795
- });
42796
- /**
42797
- * `COUNT(*)` plus one number per requested field.
42798
- *
42799
- * `null` means NO ROW MATCHED, never zero: a `SUM` over an empty set and a sum
42800
- * that really is 0 are different facts, and an accounting caller that renders
42801
- * "0 bytes, oldest = 0" for "nothing here" reports a lie about a disk.
42802
- */
42803
- var AggregateResultSchema = object({
42804
- count: number().int(),
42805
- values: record(string(), number().nullable())
42806
- });
42807
- /** A single stored record: `{ id, data }`. */
42808
- var SettingsRecordSchema = object({
42809
- id: string(),
42810
- data: record(string(), unknown())
42811
- });
42812
- /**
42813
- * One record of a BULK insert — {@link SettingsRecordSchema} with the id made
42814
- * optional.
42815
- *
42816
- * A separate schema rather than loosening the shared one: every other method
42817
- * on this cap addresses a row BY its id, and making that field optional
42818
- * everywhere would turn a forgotten key into a silently generated one on
42819
- * `update` and `delete` as well.
42820
- */
42821
- var BulkRecordSchema = object({
42822
- id: string().optional(),
42823
- data: record(string(), unknown())
42824
- });
42825
- /**
42826
- * The database FILES one `sqlite-settings` engine process owns (D851).
42827
- * Closed on purpose: a file is a thing the engine opens, backs up and gives a
42828
- * serving thread, so a new one is a code change, never a string a caller
42829
- * invents. `settings` is `camstack.db` and the default for every collection
42830
- * that names no file.
42831
- */
42832
- var DataStoreDatabaseSchema = _enum([
42833
- "settings",
42834
- "events",
42835
- "history"
42836
- ]);
42837
- DataStoreDatabaseSchema.options;
42838
- /**
42839
- * One write inside a {@link settingsStoreCapability.methods.transact} call.
42840
- * The three that a "replace this set, bump that revision" write needs, plus
42841
- * the batched upsert a rollup writer needs.
42842
- */
42843
- var TransactOpSchema = discriminatedUnion("op", [
42844
- object({
42845
- op: literal("deleteWhere"),
42846
- collection: string(),
42847
- filter: MutationFilterSchema
42848
- }),
42849
- object({
42850
- op: literal("insertMany"),
42851
- collection: string(),
42852
- records: array(BulkRecordSchema).readonly()
42853
- }),
42854
- object({
42855
- op: literal("update"),
42856
- collection: string(),
42857
- id: string(),
42858
- data: record(string(), unknown())
42859
- }),
42860
- object({
42861
- op: literal("upsertMany"),
42862
- collection: string(),
42863
- conflict: array(string()).readonly(),
42864
- records: array(BulkRecordSchema).readonly()
42865
- })
42866
- ]);
42867
- /**
42868
- * The precondition of a transaction: the row `key` of `collection` must hold
42869
- * `equals` in `column`, read INSIDE the transaction.
42870
- */
42871
- var TransactGuardSchema = object({
42872
- collection: string(),
42873
- key: string(),
42874
- column: string(),
42875
- equals: unknown()
42876
- });
42877
- /**
42878
- * Column declaration for a structured (SQL-backed) collection.
42879
- *
42880
- * Logical types — the backend translates each to the matching SQLite
42881
- * storage class and handles per-type marshaling:
42882
- * - `TEXT` / `INTEGER` / `REAL` — native SQLite types, pass-through
42883
- * - `JSON` — TEXT under the hood; serialised on write, parsed on read
42884
- * - `BOOLEAN` — INTEGER 0/1 under the hood; coerced both directions
42885
- */
42886
- var CollectionColumnSchema = object({
42887
- name: string(),
42888
- type: _enum([
42889
- "TEXT",
42890
- "INTEGER",
42891
- "REAL",
42892
- "JSON",
42893
- "BOOLEAN"
42894
- ]),
42895
- primaryKey: boolean().optional(),
42896
- notNull: boolean().optional(),
42897
- unique: boolean().optional(),
42898
- /**
42899
- * Column DEFAULT. Required for B2: the four `ensureTable` consumers declare
42900
- * columns like `enabled INTEGER NOT NULL DEFAULT 1`, and without this the
42901
- * collection surface simply cannot express their existing tables — which is
42902
- * why they were still on the uncapped `table*` API.
42903
- */
42904
- defaultValue: union([
42905
- string(),
42906
- number(),
42907
- boolean()
42908
- ]).optional()
42909
- });
42910
- var CollectionIndexSchema = object({
42911
- name: string(),
42912
- columns: array(string()).readonly(),
42913
- unique: boolean().optional()
42914
- });
42915
- /**
42916
- * The input of `declareCollection` — ONE schema for the door and for the
42917
- * engine behind it, so the two surfaces cannot drift (the dispatcher forwards
42918
- * the payload untouched).
42919
- *
42920
- * - `database` — the file the collection lives in (D851). Absent means
42921
- * `settings`. A declaration naming a file other than the one the table is
42922
- * already in is REFUSED by the engine, never moved.
42923
- * - `primaryKey` — a COMPOSITE key, as column names. A single name is the same
42924
- * as `primaryKey: true` on that column. A composite-keyed collection has no
42925
- * `id`: it is written with `insertMany` / `upsertMany` / `transact` and read
42926
- * with `query` / `count` / `aggregate` / `deleteWhere`; the calls that
42927
- * address one row by id are refused.
42928
- * - `withoutRowid` — emit `WITHOUT ROWID`. Requires a primary key.
42929
- */
42930
- var DeclareCollectionInputSchema = object({
42931
- namespace: string().optional(),
42932
- collection: string(),
42933
- columns: array(CollectionColumnSchema).readonly(),
42934
- indexes: array(CollectionIndexSchema).readonly().optional(),
42935
- database: DataStoreDatabaseSchema.optional(),
42936
- primaryKey: array(string()).readonly().optional(),
42937
- withoutRowid: boolean().optional()
42938
- });
42939
- method(object({
42940
- namespace: string().optional(),
42941
- collection: string(),
42942
- key: string()
42943
- }), unknown(), { auth: "admin" }), method(object({
42944
- namespace: string().optional(),
42945
- collection: string(),
42946
- key: string(),
42947
- value: unknown()
42948
- }), _void(), {
42949
- kind: "mutation",
42950
- auth: "admin"
42951
- }), method(object({
42952
- namespace: string().optional(),
42953
- collection: string(),
42954
- filter: QueryFilterSchema.optional(),
42955
- columns: array(string()).readonly().optional()
42956
- }), array(SettingsRecordSchema).readonly(), { auth: "admin" }), method(object({
42957
- namespace: string().optional(),
42958
- collection: string(),
42959
- /** Declared columns identifying the slot; must be covered by a UNIQUE
42960
- * index on the collection. Never empty. */
42961
- conflict: array(string()).readonly(),
42962
- record: BulkRecordSchema
42963
- }), object({
42964
- /** The id the row HAS: assigned by SQLite on an insert, or the existing
42965
- * row's own id on an update. */
42966
- id: union([string(), number()]) }), {
42967
- kind: "mutation",
42968
- auth: "admin"
42969
- }), method(object({
42970
- namespace: string().optional(),
42971
- collection: string(),
42972
- conflict: array(string()).readonly(),
42973
- records: array(BulkRecordSchema).readonly()
42974
- }), object({ upserted: number().int() }), {
42975
- kind: "mutation",
42976
- auth: "admin"
42977
- }), method(object({
42978
- namespace: string().optional(),
42979
- collection: string(),
42980
- record: object({
42981
- id: string().optional(),
42982
- data: record(string(), unknown())
42983
- })
42984
- }), object({
42985
- /**
42986
- * The id the row ACTUALLY got (D473): the one supplied, the UUID minted
42987
- * for an absent one, or the ROWID SQLite assigned on an `INTEGER`
42988
- * primary key — which is the only place an auto key is knowable.
42989
- */
42990
- id: union([string(), number()]) }), {
42991
- kind: "mutation",
42992
- auth: "admin"
42993
- }), method(object({
42994
- namespace: string().optional(),
42995
- collection: string(),
42996
- records: array(BulkRecordSchema).readonly()
42997
- }), object({ inserted: number().int() }), {
42998
- kind: "mutation",
42999
- auth: "admin"
43000
- }), method(object({
43001
- namespace: string().optional(),
43002
- guard: TransactGuardSchema.optional(),
43003
- ops: array(TransactOpSchema).readonly()
43004
- }), object({
43005
- applied: boolean(),
43006
- current: unknown().optional()
43007
- }), {
43008
- kind: "mutation",
43009
- auth: "admin"
43010
- }), method(object({
43011
- namespace: string().optional(),
43012
- collection: string(),
43013
- id: string(),
43014
- data: record(string(), unknown())
43015
- }), _void(), {
43016
- kind: "mutation",
43017
- auth: "admin"
43018
- }), method(object({
43019
- namespace: string().optional(),
43020
- collection: string(),
43021
- key: string()
43022
- }), _void(), {
43023
- kind: "mutation",
43024
- auth: "admin"
43025
- }), method(object({
43026
- namespace: string().optional(),
43027
- collection: string(),
43028
- filter: MutationFilterSchema
43029
- }), object({ deleted: number().int() }), {
43030
- kind: "mutation",
43031
- auth: "admin"
43032
- }), method(object({
43033
- namespace: string().optional(),
43034
- collection: string(),
43035
- filter: MutationFilterSchema,
43036
- data: record(string(), unknown())
43037
- }), object({ updated: number().int() }), {
43038
- kind: "mutation",
43039
- auth: "admin"
43040
- }), method(object({
43041
- namespace: string().optional(),
43042
- collection: string(),
43043
- filter: QueryFilterSchema.optional()
43044
- }), number(), { auth: "admin" }), method(object({
43045
- namespace: string().optional(),
43046
- collection: string(),
43047
- fields: array(AggregateFieldSchema).readonly(),
43048
- filter: QueryFilterSchema.optional()
43049
- }), AggregateResultSchema, { auth: "admin" }), method(object({
43050
- namespace: string().optional(),
43051
- collection: string(),
43052
- field: string(),
43053
- bucketSize: number().int().positive(),
43054
- origin: number().int(),
43055
- filter: QueryFilterSchema.optional()
43056
- }), array(object({
43057
- bucket: number().int(),
43058
- count: number().int()
43059
- })).readonly(), { auth: "admin" }), method(object({
43060
- namespace: string().optional(),
43061
- collection: string()
43062
- }), boolean(), { auth: "admin" }), method(DeclareCollectionInputSchema, _void(), {
43063
- kind: "mutation",
43064
- auth: "admin"
43065
- });
43066
- /**
43067
43632
  * How long one snapshot may take end to end: the pinned copy of the whole
43068
43633
  * database plus `PRAGMA integrity_check` over the copy, on the hub's slowest
43069
43634
  * disk. ~800 MB at a pessimistic 10 MB/s is under 3 minutes; the bound is
@@ -44977,6 +45542,7 @@ method(object({
44977
45542
  kind: "mutation",
44978
45543
  auth: "admin"
44979
45544
  }), method(object({ blockIds: array(string()) }), object({ states: array(CompositionBlockStateSchema) }), { auth: "admin" });
45545
+ 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;
44980
45546
  var NUMBER = {
44981
45547
  kind: "fixed",
44982
45548
  result: ["number"]
@@ -45987,6 +46553,57 @@ id: union([string(), number()]) }), {
45987
46553
  kind: "mutation",
45988
46554
  auth: "admin"
45989
46555
  });
46556
+ 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({
46557
+ id: string().min(1),
46558
+ error: string().min(1)
46559
+ }), MigrationLedgerEntrySchema, {
46560
+ kind: "mutation",
46561
+ auth: "admin"
46562
+ }), method(object({ id: string().min(1) }), object({ entries: array(MigrationLedgerEntrySchema) }), {
46563
+ kind: "mutation",
46564
+ auth: "admin"
46565
+ }), method(object({
46566
+ database: DataStoreDatabaseSchema,
46567
+ snapshotId: string().min(1)
46568
+ }), object({
46569
+ scheduled: boolean(),
46570
+ held: array(string()),
46571
+ restartRequired: boolean()
46572
+ }), {
46573
+ kind: "mutation",
46574
+ auth: "admin"
46575
+ }), method(object({
46576
+ owner: string().min(1),
46577
+ metas: array(MigrationMetaSchema)
46578
+ }), object({
46579
+ bootId: string(),
46580
+ snapshot: _enum([
46581
+ "none",
46582
+ "written",
46583
+ "refused"
46584
+ ]),
46585
+ steps: array(MigrationOwnerStepSchema)
46586
+ }), {
46587
+ kind: "mutation",
46588
+ auth: "admin"
46589
+ }), method(object({ id: string().min(1) }), object({
46590
+ proceed: boolean(),
46591
+ state: union([literal("pending"), MigrationStatusSchema]),
46592
+ reason: string().nullable()
46593
+ }), {
46594
+ kind: "mutation",
46595
+ auth: "admin"
46596
+ }), method(object({
46597
+ id: string().min(1),
46598
+ result: MigrationOwnerResultSchema
46599
+ }), object({
46600
+ status: MigrationStatusSchema,
46601
+ error: string().nullable(),
46602
+ rowsChanged: number().int().nonnegative()
46603
+ }), {
46604
+ kind: "mutation",
46605
+ auth: "admin"
46606
+ });
45990
46607
  /**
45991
46608
  * records - an addon's own facts and documents (D881): ledgers, audit rows,
45992
46609
  * config documents, mirrors. The third storage API next to `ctx.settings`
@@ -60851,7 +61468,18 @@ var ServerUpdateActionResultSchema = object({
60851
61468
  * store (D877), present when the pre-flight refused the update
60852
61469
  * (`reason: 'addon-would-refuse'`). Optional for version skew.
60853
61470
  */
60854
- blockers: array(UpdateBlockerSchema).optional()
61471
+ blockers: array(UpdateBlockerSchema).optional(),
61472
+ /**
61473
+ * The minimums the target server declares and this node does not meet
61474
+ * (D989), present when the update was refused with
61475
+ * `reason: 'requirements-unmet'`. Each names the versions on both sides.
61476
+ */
61477
+ unmet: array(UnmetRequirementSchema).optional(),
61478
+ /**
61479
+ * The addons this node has below the minimum the staged server declares —
61480
+ * the ones to update WITH the server. Present on an accepted update.
61481
+ */
61482
+ requiredAddonUpdates: array(RequiredAddonUpdateSchema).optional()
60855
61483
  });
60856
61484
  var LaunchOverridesSchema = object({
60857
61485
  /** The persisted heap ceiling in MB, or `null` when none is set. */
@@ -63395,7 +64023,9 @@ var PackageUpdateSchema = object({
63395
64023
  latestVersion: string(),
63396
64024
  category: _enum(["addon", "core"]),
63397
64025
  requiresRestart: boolean(),
63398
- isSystem: boolean()
64026
+ isSystem: boolean(),
64027
+ /** Held: what the newer version needs and this node does not meet (D989). */
64028
+ blockedBy: array(UnmetRequirementSchema).readonly().optional()
63399
64029
  });
63400
64030
  var PackageVersionInfoSchema = object({
63401
64031
  version: string(),
@@ -64851,19 +65481,19 @@ var RemoteSetConfigResultSchema = discriminatedUnion("status", [object({ status:
64851
65481
  reason: _enum(["unknown-template", "not-a-media-player"]),
64852
65482
  detail: string()
64853
65483
  })]);
64854
- var deviceInput = object({ deviceId: number().int().nonnegative() });
65484
+ var deviceInput$1 = object({ deviceId: number().int().nonnegative() });
64855
65485
  DeviceType.MediaPlayer;
64856
- method(deviceInput, RemoteConfigSchema, { auth: "admin" }), method(deviceInput.extend({ config: RemoteConfigSchema }), RemoteSetConfigResultSchema, {
65486
+ method(deviceInput$1, RemoteConfigSchema, { auth: "admin" }), method(deviceInput$1.extend({ config: RemoteConfigSchema }), RemoteSetConfigResultSchema, {
64857
65487
  kind: "mutation",
64858
65488
  auth: "admin"
64859
- }), method(deviceInput, RemoteCommandListSchema, { auth: "protected" }), method(deviceInput.extend({
65489
+ }), method(deviceInput$1, RemoteCommandListSchema, { auth: "protected" }), method(deviceInput$1.extend({
64860
65490
  role: RemoteRoleSchema,
64861
65491
  /** Sends in a row (volume steps). */
64862
65492
  repeat: number().int().min(1).max(50).default(1)
64863
65493
  }), RemoteSendResultSchema, {
64864
65494
  kind: "mutation",
64865
65495
  auth: "protected"
64866
- }), method(deviceInput.extend({
65496
+ }), method(deviceInput$1.extend({
64867
65497
  command: RemoteMediaCommandSchema,
64868
65498
  muted: boolean().optional(),
64869
65499
  source: string().optional(),
@@ -68202,6 +68832,8 @@ var IntegrationWithStateSchema = object({
68202
68832
  name: string(),
68203
68833
  enabled: boolean(),
68204
68834
  info: record(string(), unknown()).readonly(),
68835
+ /** The discovery candidate this integration was set up from (D984). */
68836
+ originStableId: string().optional(),
68205
68837
  createdAt: number(),
68206
68838
  updatedAt: number(),
68207
68839
  processState: string()
@@ -68212,6 +68844,8 @@ var IntegrationLiteSchema = object({
68212
68844
  name: string(),
68213
68845
  enabled: boolean(),
68214
68846
  info: record(string(), unknown()).readonly(),
68847
+ /** The discovery candidate this integration was set up from (D984). */
68848
+ originStableId: string().optional(),
68215
68849
  createdAt: number(),
68216
68850
  updatedAt: number()
68217
68851
  });
@@ -68309,6 +68943,8 @@ var CreateIntegrationInputSchema = object({
68309
68943
  name: string(),
68310
68944
  enabled: boolean().optional(),
68311
68945
  info: record(string(), unknown()).optional(),
68946
+ /** `stableId` of the discovery candidate the form was seeded from (D984). */
68947
+ originStableId: string().optional(),
68312
68948
  settings: record(string(), unknown()).optional(),
68313
68949
  /** Keys of `settings` the form's schema declares secret (by type) — kept in
68314
68950
  * the keychain, not the table (D787). */
@@ -68680,7 +69316,7 @@ var lawnMowerControlCapability = {
68680
69316
  * clients — they reverse-connect to the hub. Exposing their interfaces
68681
69317
  * via the same surface would leak internal topology with no upside.
68682
69318
  *
68683
- * Implementation in `@camstack/system/builtins/local-network/`.
69319
+ * Implementation in `@camstack/server/system/builtins/local-network/`.
68684
69320
  */
68685
69321
  /** Coarse classification derived from the interface name + IP range. */
68686
69322
  var InterfaceKindEnum = _enum([
@@ -69077,11 +69713,44 @@ var LockControlStatusSchema = object({
69077
69713
  "unlocked",
69078
69714
  "locking",
69079
69715
  "unlocking",
69080
- "jammed"
69716
+ "jammed",
69717
+ "door-open"
69081
69718
  ]),
69082
69719
  /** Ms epoch when the slice was last updated. */
69083
69720
  lastChangedAt: number()
69084
69721
  });
69722
+ /**
69723
+ * "Door contact" Customize extension (D984): the ONE extension that replaces a
69724
+ * native field. `state` is listed in `overridable` for this target, so the
69725
+ * validator and the guard accept it and nothing else.
69726
+ */
69727
+ var lockControlCustomize = {
69728
+ version: 1,
69729
+ overridable: [{
69730
+ path: "state",
69731
+ by: "door-contact"
69732
+ }],
69733
+ targets: [{
69734
+ id: "door-contact",
69735
+ cardinality: "scalar",
69736
+ vocabulary: {
69737
+ cap: "lock-control",
69738
+ stateField: "state"
69739
+ },
69740
+ deviceTypes: [DeviceType.Lock],
69741
+ apply: "replace",
69742
+ path: "state",
69743
+ override: {
69744
+ whenSourceTrue: "door-open",
69745
+ unlessNative: ["jammed"]
69746
+ },
69747
+ sources: [{
69748
+ kind: "binary",
69749
+ transforms: [{ kind: "identity" }]
69750
+ }],
69751
+ sourceReport: { status: "assignment-status" }
69752
+ }]
69753
+ };
69085
69754
  var lockControlCapability = {
69086
69755
  name: "lock-control",
69087
69756
  scope: "device",
@@ -69127,6 +69796,7 @@ var lockControlCapability = {
69127
69796
  * transitions.
69128
69797
  */
69129
69798
  runtimeState: LockControlStatusSchema,
69799
+ customize: lockControlCustomize,
69130
69800
  /**
69131
69801
  * Runtime-state durability: **restored** — a lock left locked must still read locked.
69132
69802
  *
@@ -69143,6 +69813,117 @@ var lockControlCapability = {
69143
69813
  * whether persisting is worth a SQLite commit. */
69144
69814
  volatileStateFields: ["lastChangedAt"]
69145
69815
  };
69816
+ var PowerSourceRefusalReasonSchema = _enum([
69817
+ "no-power-source",
69818
+ "switch-missing",
69819
+ "switch-offline",
69820
+ "switch-unknown",
69821
+ "restart-in-progress",
69822
+ "switch-call-failed"
69823
+ ]);
69824
+ var PowerSourceActionResultSchema = discriminatedUnion("accepted", [object({ accepted: literal(true) }), object({
69825
+ accepted: literal(false),
69826
+ reason: PowerSourceRefusalReasonSchema,
69827
+ /** One line for the operator, naming the switch. */
69828
+ message: string()
69829
+ })]);
69830
+ var PowerSourceInfoSchema = object({
69831
+ /** False when the device has no power source linked; the rest is then empty. */
69832
+ linked: boolean(),
69833
+ switchDeviceId: number().nullable(),
69834
+ switchName: string().nullable(),
69835
+ /** The switch's on/off state, or null when it is unknown. Never false for unknown (D393). */
69836
+ switchOn: boolean().nullable(),
69837
+ switchOnline: boolean().nullable(),
69838
+ restartInProgress: boolean(),
69839
+ offSeconds: number().int()
69840
+ });
69841
+ /** The composed slice: the switch state the link mirrors, plus the stored off time. */
69842
+ var PowerSourceStateSchema = object({
69843
+ /** Null while the switch has not reported: unknown is never `false` (D393). */
69844
+ switchOn: boolean().nullable(),
69845
+ offSeconds: number().int(),
69846
+ lastFetchedAt: number()
69847
+ });
69848
+ var POWER_SOURCE_DEVICE_TYPES = [
69849
+ DeviceType.Gateway,
69850
+ DeviceType.Hub,
69851
+ DeviceType.Camera,
69852
+ DeviceType.Sensor,
69853
+ DeviceType.MediaPlayer,
69854
+ DeviceType.Vacuum,
69855
+ DeviceType.Generic,
69856
+ DeviceType.Washer,
69857
+ DeviceType.Dryer,
69858
+ DeviceType.Dishwasher,
69859
+ DeviceType.PetFountain,
69860
+ DeviceType.Fan,
69861
+ DeviceType.Humidifier,
69862
+ DeviceType.Climate,
69863
+ DeviceType.WaterHeater
69864
+ ];
69865
+ var powerSourceCustomize = {
69866
+ version: 1,
69867
+ targets: [{
69868
+ id: "switch",
69869
+ cardinality: "scalar",
69870
+ vocabulary: {
69871
+ cap: "power-source",
69872
+ stateField: "switchOn"
69873
+ },
69874
+ deviceTypes: POWER_SOURCE_DEVICE_TYPES,
69875
+ apply: "add",
69876
+ path: "switchOn",
69877
+ sources: [{
69878
+ kind: "switch",
69879
+ transforms: [{ kind: "identity" }]
69880
+ }],
69881
+ companions: [{
69882
+ kind: "constant",
69883
+ path: "offSeconds",
69884
+ value: 10,
69885
+ when: "add"
69886
+ }, {
69887
+ kind: "clock",
69888
+ path: "lastFetchedAt",
69889
+ when: "always"
69890
+ }],
69891
+ sourceReport: { status: "assignment-status" }
69892
+ }]
69893
+ };
69894
+ var deviceInput = object({ deviceId: number().int().nonnegative() });
69895
+ var powerSourceCapability = {
69896
+ name: "power-source",
69897
+ scope: "device",
69898
+ mode: "singleton",
69899
+ deviceTypes: POWER_SOURCE_DEVICE_TYPES,
69900
+ methods: {
69901
+ getStatus: systemMethod(deviceInput, PowerSourceInfoSchema, { auth: "admin" }),
69902
+ powerOn: systemMethod(deviceInput, PowerSourceActionResultSchema, {
69903
+ kind: "mutation",
69904
+ auth: "admin"
69905
+ }),
69906
+ powerOff: systemMethod(deviceInput, PowerSourceActionResultSchema, {
69907
+ kind: "mutation",
69908
+ auth: "admin"
69909
+ }),
69910
+ forceRestart: systemMethod(deviceInput, PowerSourceActionResultSchema, {
69911
+ kind: "mutation",
69912
+ auth: "admin"
69913
+ })
69914
+ },
69915
+ runtimeState: PowerSourceStateSchema,
69916
+ customize: powerSourceCustomize,
69917
+ /**
69918
+ * Runtime-state durability: **session** — mirrored from the linked switch on
69919
+ * its next report; a restored value would outlive the reading it names.
69920
+ *
69921
+ * See `RuntimeStateDurability`. Enforced by
69922
+ * `scripts/check-runtime-state-durability.ts`.
69923
+ */
69924
+ durability: "session",
69925
+ history: "none"
69926
+ };
69146
69927
  /**
69147
69928
  * A monotonic total between resets (D944). Energy uses `power-meter.energy`
69148
69929
  * so dashboards keep one series; every other instantaneous reading lands here.
@@ -70423,7 +71204,7 @@ onStatusChanged: { data: object({
70423
71204
  * jitter, and observed/peak bandwidth per device + per client.
70424
71205
  *
70425
71206
  * Implementation lives in the server's `NetworkQualityService` (thin
70426
- * wrapper over the shared `NetworkQualityTracker` from `@camstack/system`).
71207
+ * wrapper over the shared `NetworkQualityTracker` from `@camstack/server/system`).
70427
71208
  * The provider is registered from `trpc.router.ts` against the existing
70428
71209
  * service instance — no addon owns this state.
70429
71210
  *
@@ -72574,7 +73355,7 @@ var ptzAutotrackCapability = {
72574
73355
  * - **Native**: the camera's own firmware autotrack, registered per device by
72575
73356
  * the driver when the probe says it exists (Reolink: yes; Hikvision:
72576
73357
  * model-dependent).
72577
- * - **Wrapper**: the emulated implementation (`@camstack/system`'s `autotrack` builtin),
73358
+ * - **Wrapper**: the emulated implementation (`@camstack/server/system`'s `autotrack` builtin),
72578
73359
  * which follows a subject with `ptz.move` pulses driven by the detection
72579
73360
  * pipeline. `defaultActive: false` — the operator binds it per camera in
72580
73361
  * the bindings UI.
@@ -77531,7 +78312,6 @@ discriminatedUnion("kind", [object({
77531
78312
  })]);
77532
78313
  DeviceFeature.FanSpeed, DeviceFeature.FanPreset, DeviceFeature.FanDirection, DeviceFeature.FanOscillating, DeviceFeature.CoverPositionable, DeviceFeature.CoverTilt, DeviceFeature.ValvePositionable, DeviceFeature.MediaPlayerVolume, DeviceFeature.ClimateFanMode;
77533
78314
  SOURCE_CAPS.filter((cap) => cap !== "switch" && cap !== "motion");
77534
- 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;
77535
78315
  String.raw`rose\(active(\d+), changed(\d+), (\d+(?:\.\d+)?)\)`;
77536
78316
  DeviceRole.BinarySensor, DeviceRole.MotionSensor;
77537
78317
  new Set(CAP_NAMES);
@@ -77884,6 +78664,7 @@ var DEVICE_LOCAL_STATE_CAPS = {
77884
78664
  petFeeder: petFeederCapability,
77885
78665
  petFountain: petFountainCapability,
77886
78666
  powerMeter: powerMeterCapability,
78667
+ powerSource: powerSourceCapability,
77887
78668
  presence: presenceCapability,
77888
78669
  pressureSensor: pressureSensorCapability,
77889
78670
  privacyMask: privacyMaskCapability,
@@ -79553,6 +80334,13 @@ Object.freeze({
79553
80334
  access: "view",
79554
80335
  role: "admin"
79555
80336
  },
80337
+ "addonSettings.getIntegrationFormSchema": {
80338
+ capName: "addon-settings",
80339
+ capScope: "system",
80340
+ addonId: null,
80341
+ access: "view",
80342
+ role: "admin"
80343
+ },
79556
80344
  "addonSettings.getIntegrationSettings": {
79557
80345
  capName: "addon-settings",
79558
80346
  capScope: "system",
@@ -83641,6 +84429,69 @@ Object.freeze({
83641
84429
  access: "view",
83642
84430
  role: "admin"
83643
84431
  },
84432
+ "migrations.declareOwner": {
84433
+ capName: "migrations",
84434
+ capScope: "system",
84435
+ addonId: null,
84436
+ access: "create",
84437
+ role: "admin"
84438
+ },
84439
+ "migrations.dryRun": {
84440
+ capName: "migrations",
84441
+ capScope: "system",
84442
+ addonId: null,
84443
+ access: "view",
84444
+ role: "admin"
84445
+ },
84446
+ "migrations.finishMigration": {
84447
+ capName: "migrations",
84448
+ capScope: "system",
84449
+ addonId: null,
84450
+ access: "create",
84451
+ role: "admin"
84452
+ },
84453
+ "migrations.listLedger": {
84454
+ capName: "migrations",
84455
+ capScope: "system",
84456
+ addonId: null,
84457
+ access: "view",
84458
+ role: "admin"
84459
+ },
84460
+ "migrations.listPending": {
84461
+ capName: "migrations",
84462
+ capScope: "system",
84463
+ addonId: null,
84464
+ access: "view",
84465
+ role: "admin"
84466
+ },
84467
+ "migrations.markFailed": {
84468
+ capName: "migrations",
84469
+ capScope: "system",
84470
+ addonId: null,
84471
+ access: "create",
84472
+ role: "admin"
84473
+ },
84474
+ "migrations.restorePreMigration": {
84475
+ capName: "migrations",
84476
+ capScope: "system",
84477
+ addonId: null,
84478
+ access: "create",
84479
+ role: "admin"
84480
+ },
84481
+ "migrations.retry": {
84482
+ capName: "migrations",
84483
+ capScope: "system",
84484
+ addonId: null,
84485
+ access: "create",
84486
+ role: "admin"
84487
+ },
84488
+ "migrations.startMigration": {
84489
+ capName: "migrations",
84490
+ capScope: "system",
84491
+ addonId: null,
84492
+ access: "create",
84493
+ role: "admin"
84494
+ },
83644
84495
  "modelConvert.convert": {
83645
84496
  capName: "model-convert",
83646
84497
  capScope: "system",
@@ -86273,6 +87124,34 @@ Object.freeze({
86273
87124
  access: "view",
86274
87125
  role: "viewer"
86275
87126
  },
87127
+ "powerSource.forceRestart": {
87128
+ capName: "power-source",
87129
+ capScope: "device",
87130
+ addonId: null,
87131
+ access: "create",
87132
+ role: "admin"
87133
+ },
87134
+ "powerSource.getStatus": {
87135
+ capName: "power-source",
87136
+ capScope: "device",
87137
+ addonId: null,
87138
+ access: "view",
87139
+ role: "admin"
87140
+ },
87141
+ "powerSource.powerOff": {
87142
+ capName: "power-source",
87143
+ capScope: "device",
87144
+ addonId: null,
87145
+ access: "create",
87146
+ role: "admin"
87147
+ },
87148
+ "powerSource.powerOn": {
87149
+ capName: "power-source",
87150
+ capScope: "device",
87151
+ addonId: null,
87152
+ access: "create",
87153
+ role: "admin"
87154
+ },
86276
87155
  "presence.getStatus": {
86277
87156
  capName: "presence",
86278
87157
  capScope: "device",
@@ -91421,6 +92300,26 @@ Object.freeze({
91421
92300
  form: "single",
91422
92301
  optional: false
91423
92302
  }],
92303
+ "powerSource.forceRestart": [{
92304
+ name: "deviceId",
92305
+ form: "single",
92306
+ optional: false
92307
+ }],
92308
+ "powerSource.getStatus": [{
92309
+ name: "deviceId",
92310
+ form: "single",
92311
+ optional: false
92312
+ }],
92313
+ "powerSource.powerOff": [{
92314
+ name: "deviceId",
92315
+ form: "single",
92316
+ optional: false
92317
+ }],
92318
+ "powerSource.powerOn": [{
92319
+ name: "deviceId",
92320
+ form: "single",
92321
+ optional: false
92322
+ }],
91424
92323
  "presence.getStatus": [{
91425
92324
  name: "deviceId",
91426
92325
  form: "single",
@@ -93087,6 +93986,7 @@ new Set([
93087
93986
  "locking",
93088
93987
  "unlocking",
93089
93988
  "jammed",
93989
+ "door-open",
93090
93990
  "on",
93091
93991
  "off",
93092
93992
  "home",
@@ -93599,6 +94499,60 @@ object({
93599
94499
  commands: record(string(), unknown())
93600
94500
  });
93601
94501
  //#endregion
94502
+ //#region src/rademacher-reconciler.ts
94503
+ /**
94504
+ * Keeps one live client per enabled integration. Devices are removed ONLY on an explicit
94505
+ * lifecycle event naming the integration (D49): a list that failed, an empty list or an
94506
+ * unreadable connection changes nothing about devices and is logged, never read as "gone".
94507
+ */
94508
+ var RademacherReconciler = class {
94509
+ #deps;
94510
+ constructor(deps) {
94511
+ this.#deps = deps;
94512
+ }
94513
+ /** Upsert a client per usable enabled integration; drop the CLIENT (never devices) of one the list answered is gone. */
94514
+ async reconcile() {
94515
+ const { logger, clients } = this.#deps;
94516
+ let all;
94517
+ try {
94518
+ all = await this.#deps.listIntegrations();
94519
+ } catch (err) {
94520
+ logger.warn("Rademacher integration reconcile skipped — the integration list did not answer", { meta: { error: errMsg(err) } });
94521
+ return;
94522
+ }
94523
+ const enabled = all.filter((i) => i.addonId === this.#deps.addonId && i.enabled);
94524
+ for (const integration of enabled) try {
94525
+ if (!await this.#deps.applySettings(integration)) logger.warn("Rademacher integration has no usable connection — left as it is", { meta: { integrationId: integration.id } });
94526
+ } catch (err) {
94527
+ logger.warn("Rademacher reconcile failed for integration — left as it is", { meta: {
94528
+ integrationId: integration.id,
94529
+ error: errMsg(err)
94530
+ } });
94531
+ }
94532
+ const enabledIds = new Set(enabled.map((i) => i.id));
94533
+ for (const id of clients.list()) if (!enabledIds.has(id)) await clients.remove(id);
94534
+ }
94535
+ /** An explicit lifecycle event: only `deleted` / `disabled` of a named integration remove devices. */
94536
+ async onLifecycle(kind, integrationId) {
94537
+ await this.reconcile();
94538
+ if (kind === "enabled" || integrationId === null) return;
94539
+ try {
94540
+ const removed = await this.#deps.removeDevices(integrationId);
94541
+ this.#deps.logger.info("Rademacher integration devices removed", { meta: {
94542
+ integrationId,
94543
+ kind,
94544
+ removed
94545
+ } });
94546
+ } catch (err) {
94547
+ this.#deps.logger.warn("Rademacher integration device removal failed", { meta: {
94548
+ integrationId,
94549
+ kind,
94550
+ error: errMsg(err)
94551
+ } });
94552
+ }
94553
+ }
94554
+ };
94555
+ //#endregion
93602
94556
  //#region src/config.ts
93603
94557
  /**
93604
94558
  * Operator-supplied connection to ONE Rademacher HomePilot / Start2Smart hub on
@@ -94102,8 +95056,8 @@ function resolveNativeId(integrationId, covers, childNativeId) {
94102
95056
  /**
94103
95057
  * The adoption candidates of one integration, pure and in a fixed order: the hub
94104
95058
  * box ({@link DeviceType.Gateway}), then one {@link DeviceType.Cover} per roller
94105
- * shutter. Each cover names the gateway through `parentStableId`, so the
94106
- * adoption modal nests it under the hub.
95059
+ * shutter. The gateway is a sibling of the covers, never their parent: no
95060
+ * cover names a `parentStableId`.
94107
95061
  */
94108
95062
  function buildRademacherCandidates(input) {
94109
95063
  const gatewayId = gatewayNativeId(input.integrationId);
@@ -94135,8 +95089,7 @@ function buildRademacherCandidates(input) {
94135
95089
  },
94136
95090
  alreadyAdopted: adoptedId !== null,
94137
95091
  adoptedDeviceId: adoptedId,
94138
- capabilities: [coverCapability.name],
94139
- parentStableId: gatewayId
95092
+ capabilities: [coverCapability.name]
94140
95093
  });
94141
95094
  }
94142
95095
  return out;
@@ -95751,6 +96704,7 @@ var RademacherProviderAddon = class extends BaseDeviceProvider {
95751
96704
  [DeviceType.Gateway]: RademacherGatewayDevice,
95752
96705
  [DeviceType.Cover]: RademacherCoverDevice
95753
96706
  };
96707
+ integrationReconciler = null;
95754
96708
  clients = null;
95755
96709
  constructor() {
95756
96710
  super({});
@@ -95785,6 +96739,7 @@ var RademacherProviderAddon = class extends BaseDeviceProvider {
95785
96739
  this.ctx.logger.warn("Rademacher: shutdown error", { meta: { error: errMsg(err) } });
95786
96740
  }
95787
96741
  this.clients = null;
96742
+ this.integrationReconciler = null;
95788
96743
  await super.onShutdown();
95789
96744
  }
95790
96745
  globalSettingsSchema() {
@@ -95809,47 +96764,48 @@ var RademacherProviderAddon = class extends BaseDeviceProvider {
95809
96764
  if (!this.clients) throw new Error("Rademacher provider not initialised");
95810
96765
  return this.clients;
95811
96766
  }
95812
- async reconcileIntegrations() {
95813
- const reg = this.requireClients();
95814
- try {
95815
- const mine = (await this.ctx.api.integrations.list.query()).filter((i) => i.addonId === this.ctx.id && i.enabled);
95816
- const surviving = /* @__PURE__ */ new Set();
95817
- for (const integration of mine) try {
95818
- const connection = connectionFromSettings(await this.ctx.api.integrations.getSettings.query({ id: integration.id }));
95819
- if (!connection) {
95820
- this.ctx.logger.warn("Rademacher reconcile: host missing or invalid, no session opened", { tags: { integrationId: integration.id } });
95821
- continue;
96767
+ reconciler() {
96768
+ this.integrationReconciler ??= new RademacherReconciler({
96769
+ addonId: this.ctx.id,
96770
+ logger: this.ctx.logger,
96771
+ clients: {
96772
+ list: () => this.requireClients().list(),
96773
+ remove: async (id) => {
96774
+ await this.requireClients().remove(id);
95822
96775
  }
95823
- await reg.upsert(integration.id, integration.name, connection);
95824
- surviving.add(integration.id);
95825
- } catch (err) {
95826
- this.ctx.logger.warn("Rademacher reconcile: settings read failed", {
95827
- tags: { integrationId: integration.id },
95828
- meta: { error: errMsg(err) }
95829
- });
95830
- }
95831
- const vanished = reg.list().filter((id) => !surviving.has(id));
95832
- await reg.retain(surviving);
95833
- for (const integrationId of vanished) await this.cascadeRemoveDevicesForIntegration(integrationId);
95834
- } catch (err) {
95835
- this.ctx.logger.warn("Rademacher integration reconcile failed", { meta: { error: errMsg(err) } });
95836
- }
96776
+ },
96777
+ listIntegrations: () => this.ctx.api.integrations.list.query(),
96778
+ applySettings: async (integration) => {
96779
+ const connection = connectionFromSettings(await this.ctx.api.integrations.getSettings.query({ id: integration.id }));
96780
+ if (!connection) return false;
96781
+ await this.requireClients().upsert(integration.id, integration.name, connection);
96782
+ return true;
96783
+ },
96784
+ removeDevices: (integrationId) => this.cascadeRemoveDevicesForIntegration(integrationId)
96785
+ });
96786
+ return this.integrationReconciler;
96787
+ }
96788
+ /** Upsert one client per enabled integration; never removes a device (D49). */
96789
+ async reconcileIntegrations() {
96790
+ await this.reconciler().reconcile();
95837
96791
  }
96792
+ /** Lifecycle events: only an explicit Deleted/Disabled naming an integration removes devices. */
95838
96793
  subscribeIntegrationLifecycle() {
95839
- const handler = (event) => {
96794
+ const on = (kind) => (event) => {
95840
96795
  const addonId = event.data["addonId"];
95841
96796
  if (typeof addonId === "string" && addonId !== this.ctx.id) return;
95842
- this.reconcileIntegrations();
96797
+ const integrationId = event.data["integrationId"];
96798
+ this.reconciler().onLifecycle(kind, typeof integrationId === "string" ? integrationId : null);
95843
96799
  };
95844
- this.ctx.eventBus.subscribe({ category: EventCategory.IntegrationEnabled }, handler);
95845
- this.ctx.eventBus.subscribe({ category: EventCategory.IntegrationDisabled }, handler);
95846
- this.ctx.eventBus.subscribe({ category: EventCategory.IntegrationDeleted }, handler);
96800
+ this.ctx.eventBus.subscribe({ category: EventCategory.IntegrationEnabled }, on("enabled"));
96801
+ this.ctx.eventBus.subscribe({ category: EventCategory.IntegrationDisabled }, on("disabled"));
96802
+ this.ctx.eventBus.subscribe({ category: EventCategory.IntegrationDeleted }, on("deleted"));
95847
96803
  }
95848
96804
  async cascadeRemoveDevicesForIntegration(integrationId) {
95849
96805
  const reg = this.ctx.kernel.deviceRegistry;
95850
96806
  const devices = this.ctx.kernel.devices;
95851
- if (!reg || !devices) return;
95852
- await cascadeRemoveDevicesForIntegration({
96807
+ if (!reg || !devices) return 0;
96808
+ return await cascadeRemoveDevicesForIntegration({
95853
96809
  reg,
95854
96810
  devices,
95855
96811
  addonId: this.addonId,