@camstack/addon-provider-enpal 0.1.5 → 0.1.6

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 +7 -0
  2. package/dist/addon.js +2002 -1146
  3. package/dist/addon.mjs +2002 -1146
  4. package/package.json +5 -2
package/dist/addon.js CHANGED
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- //#region ../types/dist/event-category-DmZDPDf_.mjs
2
+ //#region ../types/dist/event-category-CaURIwQA.mjs
3
3
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
4
4
  EventCategory["SystemBoot"] = "system.boot";
5
5
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -715,6 +715,10 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
715
715
  EventCategory["NotificationRuleBroken"] = "notification.rule-broken";
716
716
  /** The clear of {@link NotificationRuleBroken}: the rule loads and names nothing deleted. */
717
717
  EventCategory["NotificationRuleRepaired"] = "notification.rule-repaired";
718
+ /** A migration failed or was blocked; the Alert Center re-reads the ledger. */
719
+ EventCategory["MigrationFailed"] = "migration.failed";
720
+ /** A migration applied or a failure cleared; the Alert Center re-reads the ledger. */
721
+ EventCategory["MigrationApplied"] = "migration.applied";
718
722
  EventCategory["DeviceUpdated"] = "device.updated";
719
723
  /**
720
724
  * Transport-level connectivity. Emitted by the device driver when
@@ -777,6 +781,378 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
777
781
  return EventCategory;
778
782
  }({});
779
783
  //#endregion
784
+ //#region ../types/dist/device-type-CEqjmUnR.mjs
785
+ /**
786
+ import { errMsg } from '@camstack/types'
787
+ * Extract a human-readable message from an unknown error value.
788
+ * Replaces the ubiquitous `errMsg(err)` pattern.
789
+ */
790
+ function errMsg(err) {
791
+ if (err instanceof Error) return err.message;
792
+ if (typeof err === "string") return err;
793
+ return String(err);
794
+ }
795
+ var DeviceType = /* @__PURE__ */ function(DeviceType) {
796
+ DeviceType["Camera"] = "camera";
797
+ DeviceType["Hub"] = "hub";
798
+ DeviceType["Light"] = "light";
799
+ DeviceType["Siren"] = "siren";
800
+ DeviceType["Switch"] = "switch";
801
+ DeviceType["Sensor"] = "sensor";
802
+ DeviceType["Thermostat"] = "thermostat";
803
+ /** Air-conditioner / heat-pump climate device (HVAC) — shares the
804
+ * `climate-control` cap surface with `Thermostat` but renders a
805
+ * dedicated AC-appropriate control UI (mode chips, fan speed,
806
+ * independent vertical/horizontal swing). Sources: native Gree, and
807
+ * reusable by other AC integrations. */
808
+ DeviceType["Climate"] = "climate";
809
+ DeviceType["Button"] = "button";
810
+ /** Generic stateless event emitter — carries a device's EXACT declared
811
+ * event vocabulary verbatim (no normalization). Installed with the
812
+ * `event-emitter` cap. Sources: HA `event.*` entities (structured) and
813
+ * HA bus events (e.g. `zha_event`, generic). */
814
+ DeviceType["EventEmitter"] = "event-emitter";
815
+ /** Firmware/software update entity — current vs available version,
816
+ * updatable flag, update state, and an install action. Installed with
817
+ * the `update` cap. Sources: Homematic firmware-update channels (and
818
+ * reusable by other providers, e.g. HA `update.*` entities). */
819
+ DeviceType["Update"] = "update";
820
+ DeviceType["Generic"] = "generic";
821
+ /** Generic notification delivery target (HA `notify.<service>`, future
822
+ * Telegram / Discord / ntfy / SMTP, …). One device per delivery
823
+ * endpoint; the `notifier` cap defines the send surface. */
824
+ DeviceType["Notifier"] = "notifier";
825
+ /** Pre-recorded action sequence with optional parameters
826
+ * (HA `script.*`). Runnable via `script-runner` cap. */
827
+ DeviceType["Script"] = "script";
828
+ /** Automation rule (HA `automation.*`) — enable/disable + manual
829
+ * trigger surface exposed via `automation-control` cap. */
830
+ DeviceType["Automation"] = "automation";
831
+ /** Door / smart lock device (HA `lock.*`). `lock-control` cap. */
832
+ DeviceType["Lock"] = "lock";
833
+ /** Window covering, blinds, garage door, valve, etc. (HA `cover.*`,
834
+ * `valve.*`). `cover` cap with sub-roles for variant. */
835
+ DeviceType["Cover"] = "cover";
836
+ /** Pipe / water / gas valve with open/close/stop and optional
837
+ * position (HA `valve.*`). `valve` cap — a cover-sibling actuator
838
+ * modelled on the same open/closed lifecycle. */
839
+ DeviceType["Valve"] = "valve";
840
+ /** Humidifier / dehumidifier with on/off + target humidity + mode
841
+ * (HA `humidifier.*`). `humidifier` cap — a climate-family actuator
842
+ * modelled on the same target / mode lifecycle. */
843
+ DeviceType["Humidifier"] = "humidifier";
844
+ /** Water heater / boiler with target temperature + operation mode +
845
+ * away mode (HA `water_heater.*`). `water-heater` cap — a
846
+ * climate-family actuator. */
847
+ DeviceType["WaterHeater"] = "water-heater";
848
+ /** Ceiling / standing / exhaust fan (HA `fan.*`). `fan-control` cap. */
849
+ DeviceType["Fan"] = "fan";
850
+ /** Audio / video playback endpoint (HA `media_player.*`). Disjoint from
851
+ * the camera surface — those use `Camera`. `media-player` cap. */
852
+ DeviceType["MediaPlayer"] = "media-player";
853
+ /** Security panel / alarm system (HA `alarm_control_panel.*`).
854
+ * `alarm-panel` cap. */
855
+ DeviceType["AlarmPanel"] = "alarm-panel";
856
+ /** Generic user-settable input (HA `number` / `input_number` / `select`
857
+ * / `input_select` / `text` / `input_text` / `input_datetime`).
858
+ * Sub-type via `DeviceRole`: NumericControl / SelectControl /
859
+ * TextControl / DateTimeControl. */
860
+ DeviceType["Control"] = "control";
861
+ /** Person / device-tracker presence (HA `person.*`, `device_tracker.*`).
862
+ * `presence` cap. */
863
+ DeviceType["Presence"] = "presence";
864
+ /** Weather provider (HA `weather.*`). Tier-3, low MVP priority.
865
+ * `weather` cap. */
866
+ DeviceType["Weather"] = "weather";
867
+ /** Robot vacuum (HA `vacuum.*`). Tier-3. `vacuum-control` cap. */
868
+ DeviceType["Vacuum"] = "vacuum";
869
+ /** Robotic lawn mower (HA `lawn_mower.*`). Tier-3.
870
+ * `lawn-mower-control` cap. */
871
+ DeviceType["LawnMower"] = "lawn-mower";
872
+ /** Physical HA device group — parent container for entity-children
873
+ * adopted from a single HA device entry. Not renderable as a
874
+ * standalone device; exists only to anchor child entities. */
875
+ DeviceType["Container"] = "container";
876
+ /** Single still-image entity (HA `image.*`). Read-only display of an
877
+ * `entity_picture` signed URL the browser loads directly. `image` cap. */
878
+ DeviceType["Image"] = "image";
879
+ /** Smart pet feeder — cloud-connected food dispenser with a bowl food
880
+ * level, battery, desiccant life, feeding state and manual-feed /
881
+ * call-pet / maintenance actions. Installed with the `pet-feeder` cap;
882
+ * dual-hopper models (D4S/D4SH) expose per-hopper portions. Sources:
883
+ * native PetKit (`nodepetkit` `FeederDevice`), reusable by other feeder
884
+ * integrations sharing the same food/desiccant/hopper surface. */
885
+ DeviceType["PetFeeder"] = "pet-feeder";
886
+ /** Pet water fountain — tank level, water drunk, pump and its duty cycle.
887
+ * Installed with the `pet-fountain` cap. Reaches CamStack as loose entities
888
+ * of a container and is BUILT as a composed device (D967). */
889
+ DeviceType["PetFountain"] = "pet-fountain";
890
+ /** Built-in dishwasher — program lifecycle, door / remote-start gates,
891
+ * salt and rinse-aid levels. Installed with the `dishwasher` cap.
892
+ * Sources: native Bosch Home Connect, reusable by similar integrations.
893
+ * A plug-built dishwasher carries running-state and power instead. */
894
+ DeviceType["Dishwasher"] = "dishwasher";
895
+ /** Washing machine. A plug-built one is running while its power stays
896
+ * above a threshold. No program cap — that surface is the dishwasher's. */
897
+ DeviceType["Washer"] = "washer";
898
+ /** Tumble dryer. Same plug-built running surface as {@link Washer}. */
899
+ DeviceType["Dryer"] = "dryer";
900
+ /** The physical bridge box of a vendor ecosystem (Velux KLF-200, and later
901
+ * other radio-to-IP bridges). It carries the box's own facts (firmware,
902
+ * address, connection state) and the actions the box really supports
903
+ * (`reboot`, `device-admin-link`). Products are NOT its children — they are
904
+ * adopted beside it — and its connection settings stay on the integration. */
905
+ DeviceType["Gateway"] = "gateway";
906
+ return DeviceType;
907
+ }({});
908
+ var DeviceFeature = /* @__PURE__ */ function(DeviceFeature) {
909
+ DeviceFeature["BatteryOperated"] = "battery-operated";
910
+ DeviceFeature["Rebootable"] = "rebootable";
911
+ /**
912
+ * Device supports an on-demand re-sync of its derived spec with its
913
+ * upstream source — drives the generic Re-sync button. The owning
914
+ * provider implements the action via the `device-adoption.resync` cap.
915
+ */
916
+ DeviceFeature["Resyncable"] = "resyncable";
917
+ DeviceFeature["NativeSnapshot"] = "native-snapshot";
918
+ DeviceFeature["DoorbellButton"] = "doorbell-button";
919
+ DeviceFeature["TwoWayAudio"] = "two-way-audio";
920
+ DeviceFeature["PanTiltZoom"] = "pan-tilt-zoom";
921
+ /**
922
+ * Camera supports the on-firmware autotrack subsystem (subject-
923
+ * following). Distinct from `PanTiltZoom` because not every PTZ
924
+ * camera ships autotrack — the admin UI uses this flag to gate
925
+ * the autotrack toggle / settings card without re-deriving from
926
+ * the cap registry. Mirrors `ptz-autotrack` cap registration:
927
+ * driver sets this feature when probe confirms the firmware
928
+ * surface, and registers the cap in the same code path.
929
+ */
930
+ DeviceFeature["PtzAutotrack"] = "ptz-autotrack";
931
+ /**
932
+ * Accessory exposes a "trigger on motion" toggle — the parent camera's
933
+ * motion detection automatically activates this device. Mirrors
934
+ * `motion-trigger` cap registration: drivers set this feature in the
935
+ * same code path that calls `ctx.registerNativeCap(motionTriggerCapability, ...)`.
936
+ *
937
+ * Used by admin UI (gate the in-hero `MotionTriggerToggle` against a
938
+ * fast scalar without binding fetch), notifier rules, and `listAll`
939
+ * filters that want "all devices with on-motion behaviour".
940
+ */
941
+ DeviceFeature["MotionTrigger"] = "motion-trigger";
942
+ /** Light supports rgb-triplet color via `color` cap. */
943
+ DeviceFeature["LightColorRgb"] = "light-color-rgb";
944
+ /** Light supports HSV color via `color` cap. */
945
+ DeviceFeature["LightColorHsv"] = "light-color-hsv";
946
+ /** Light supports color-temperature (mired) via `color` cap. */
947
+ DeviceFeature["LightColorMired"] = "light-color-mired";
948
+ /** Thermostat supports a `heat_cool` dual setpoint (targetLow +
949
+ * targetHigh). Gates the range slider UI. */
950
+ DeviceFeature["ClimateDualSetpoint"] = "climate-dual-setpoint";
951
+ /** Thermostat exposes target humidity and/or current humidity
952
+ * readings. Gates the humidity controls. */
953
+ DeviceFeature["ClimateHumidity"] = "climate-humidity";
954
+ /** Thermostat exposes a fan-mode selector. */
955
+ DeviceFeature["ClimateFanMode"] = "climate-fan-mode";
956
+ /** Thermostat exposes preset modes (eco / away / sleep / vendor). */
957
+ DeviceFeature["ClimatePreset"] = "climate-preset";
958
+ /** Thermostat exposes a vertical louver swing toggle. Gates the
959
+ * vertical-swing switch in the climate UI. Independent of horizontal. */
960
+ DeviceFeature["ClimateSwingVertical"] = "climate-swing-vertical";
961
+ /** Thermostat exposes a horizontal louver swing toggle. Gates the
962
+ * horizontal-swing switch in the climate UI. Independent of vertical. */
963
+ DeviceFeature["ClimateSwingHorizontal"] = "climate-swing-horizontal";
964
+ /** Cover exposes intermediate position control (0..100). Gates the
965
+ * position slider UI. */
966
+ DeviceFeature["CoverPositionable"] = "cover-positionable";
967
+ /** Cover exposes slat-tilt control. Gates the tilt slider UI. */
968
+ DeviceFeature["CoverTilt"] = "cover-tilt";
969
+ /** Valve exposes intermediate position control (0..100). Gates the
970
+ * position slider / drag surface UI. */
971
+ DeviceFeature["ValvePositionable"] = "valve-positionable";
972
+ /** Fan exposes a speed-percentage setter. Gates the speed slider UI. */
973
+ DeviceFeature["FanSpeed"] = "fan-speed";
974
+ /** Fan exposes a preset mode selector. */
975
+ DeviceFeature["FanPreset"] = "fan-preset";
976
+ /** Fan exposes blade direction (forward/reverse) — typical of
977
+ * ceiling fans. */
978
+ DeviceFeature["FanDirection"] = "fan-direction";
979
+ /** Fan exposes an oscillation toggle. */
980
+ DeviceFeature["FanOscillating"] = "fan-oscillating";
981
+ /** Lock requires a PIN code on lock/unlock. Gates the code-entry
982
+ * field on the UI lock-controls panel. */
983
+ DeviceFeature["LockPinRequired"] = "lock-pin-required";
984
+ /** Lock supports a latch-release ("open door") action distinct from
985
+ * unlock. Mirrors HA `LockEntityFeature.OPEN` (bit 1) in
986
+ * `supported_features`. Gates the Open Door button in the UI. */
987
+ DeviceFeature["LockOpen"] = "lock-open";
988
+ /** Media player exposes a seek-to-position surface. */
989
+ DeviceFeature["MediaPlayerSeek"] = "media-player-seek";
990
+ /** Media player exposes a volume-level setter. */
991
+ DeviceFeature["MediaPlayerVolume"] = "media-player-volume";
992
+ /** Media player exposes a mute toggle distinct from volume=0. */
993
+ DeviceFeature["MediaPlayerMute"] = "media-player-mute";
994
+ /** Media player exposes a shuffle toggle. */
995
+ DeviceFeature["MediaPlayerShuffle"] = "media-player-shuffle";
996
+ /** Media player exposes a repeat mode (off / all / one). */
997
+ DeviceFeature["MediaPlayerRepeat"] = "media-player-repeat";
998
+ /** Media player exposes a source / input selector. */
999
+ DeviceFeature["MediaPlayerSelectSource"] = "media-player-select-source";
1000
+ /** Media player exposes a play-arbitrary-media surface (URL / id). */
1001
+ DeviceFeature["MediaPlayerPlayMedia"] = "media-player-play-media";
1002
+ /** Media player exposes next-track. */
1003
+ DeviceFeature["MediaPlayerNext"] = "media-player-next";
1004
+ /** Media player exposes previous-track. */
1005
+ DeviceFeature["MediaPlayerPrevious"] = "media-player-previous";
1006
+ /** Media player exposes stop distinct from pause. */
1007
+ DeviceFeature["MediaPlayerStop"] = "media-player-stop";
1008
+ /** Alarm panel requires a PIN code on arm/disarm. */
1009
+ DeviceFeature["AlarmPinRequired"] = "alarm-pin-required";
1010
+ /** Presence device carries GPS coordinates (lat/lng/accuracy) in
1011
+ * addition to a textual location. */
1012
+ DeviceFeature["PresenceGps"] = "presence-gps";
1013
+ /** Notifier accepts an inline / URL image attachment. */
1014
+ DeviceFeature["NotifierImage"] = "notifier-image";
1015
+ /** Notifier accepts a priority hint (high/normal/low). */
1016
+ DeviceFeature["NotifierPriority"] = "notifier-priority";
1017
+ /** Notifier accepts a free-form `data` payload for platform-specific
1018
+ * fields. */
1019
+ DeviceFeature["NotifierData"] = "notifier-data";
1020
+ /** Notifier supports interactive action buttons / callbacks. */
1021
+ DeviceFeature["NotifierActions"] = "notifier-actions";
1022
+ /** Notifier supports per-call recipient targeting (multi-user). */
1023
+ DeviceFeature["NotifierRecipients"] = "notifier-recipients";
1024
+ /** Script runner accepts a variables map on each run invocation. */
1025
+ DeviceFeature["ScriptVariables"] = "script-variables";
1026
+ /** Automation `trigger` accepts a skipCondition flag — fires the
1027
+ * automation's actions while bypassing its condition block. */
1028
+ DeviceFeature["AutomationSkipCondition"] = "automation-skip-condition";
1029
+ /** Robot vacuum exposes a live cleaning map (image child). Gates the
1030
+ * map tile in the vacuum UI. */
1031
+ DeviceFeature["VacuumHasMap"] = "vacuum-has-map";
1032
+ /** Robot vacuum exposes AI obstacle-detection toggles. Gates the AI
1033
+ * switches group. */
1034
+ DeviceFeature["VacuumHasAi"] = "vacuum-has-ai";
1035
+ /** Robot mower exposes a live mowing map (SVG image child). Gates the
1036
+ * mower map tile. */
1037
+ DeviceFeature["MowerHasMap"] = "mower-has-map";
1038
+ /** Robot mower exposes targeted mowing (all-area / zones / edges /
1039
+ * spots) — gates the Mow action + map selectors. */
1040
+ DeviceFeature["MowerHasTargetedMowing"] = "mower-has-targeted-mowing";
1041
+ return DeviceFeature;
1042
+ }({});
1043
+ /**
1044
+ * Semantic role a device plays within its parent. Populated by driver
1045
+ * addons when creating accessory devices (Reolink siren/floodlight/
1046
+ * PIR/chime/autotrack/doorbell, ONVIF relay outputs, …). Used by the
1047
+ * admin UI to pick icons, labels, and widgets — a `Switch` with
1048
+ * `role: Floodlight` renders as a bulb with a brightness slider,
1049
+ * whereas a `Switch` with `role: Siren` renders as a klaxon.
1050
+ *
1051
+ * Undefined for top-level devices (cameras, NVRs, hubs). Persisted in
1052
+ * sqlite as a nullable TEXT column — old rows keep working unchanged.
1053
+ */
1054
+ var DeviceRole = /* @__PURE__ */ function(DeviceRole) {
1055
+ DeviceRole["Siren"] = "siren";
1056
+ DeviceRole["Floodlight"] = "floodlight";
1057
+ DeviceRole["Spotlight"] = "spotlight";
1058
+ DeviceRole["PirSensor"] = "pir-sensor";
1059
+ DeviceRole["Chime"] = "chime";
1060
+ DeviceRole["Autotrack"] = "autotrack";
1061
+ DeviceRole["Nightvision"] = "nightvision";
1062
+ DeviceRole["PrivacyMask"] = "privacy-mask";
1063
+ DeviceRole["Doorbell"] = "doorbell";
1064
+ /** Interior blind / shade / roller blind. HA `device_class: blind|shade`. */
1065
+ DeviceRole["CoverBlind"] = "cover-blind";
1066
+ /** Exterior roller shutter. HA `device_class: shutter`; Homematic BROLL. */
1067
+ DeviceRole["CoverShutter"] = "cover-shutter";
1068
+ /** Exterior awning. HA `device_class: awning`. */
1069
+ DeviceRole["CoverAwning"] = "cover-awning";
1070
+ /** Curtain / drape. HA `device_class: curtain`. */
1071
+ DeviceRole["CoverCurtain"] = "cover-curtain";
1072
+ /** Garage door. HA `device_class: garage`. A security actuator: exports gate it. */
1073
+ DeviceRole["CoverGarage"] = "cover-garage";
1074
+ /** Gate. HA `device_class: gate`. A security actuator: exports gate it. */
1075
+ DeviceRole["CoverGate"] = "cover-gate";
1076
+ /** Door. HA `device_class: door`. A security actuator: exports gate it. */
1077
+ DeviceRole["CoverDoor"] = "cover-door";
1078
+ /** Window. HA `device_class: window`. */
1079
+ DeviceRole["CoverWindow"] = "cover-window";
1080
+ /** Virtual HA toggle (input_boolean.*) — distinguishable from a
1081
+ * real Switch device for UI rendering / export adapters. */
1082
+ DeviceRole["BinaryHelper"] = "binary-helper";
1083
+ /** A smart plug: a `switch` whose source says it is an outlet (HA
1084
+ * `device_class: outlet`). The Alexa export shows it as a SMARTPLUG. */
1085
+ DeviceRole["Outlet"] = "outlet";
1086
+ /** Generic motion / occupancy / moving event source. Distinct from
1087
+ * the camera accessory PirSensor role: that one is a camera child;
1088
+ * this is a standalone HA / 3rd-party motion sensor. */
1089
+ DeviceRole["MotionSensor"] = "motion-sensor";
1090
+ DeviceRole["ContactSensor"] = "contact-sensor";
1091
+ DeviceRole["LeakSensor"] = "leak-sensor";
1092
+ DeviceRole["SmokeSensor"] = "smoke-sensor";
1093
+ DeviceRole["COSensor"] = "co-sensor";
1094
+ DeviceRole["GasSensor"] = "gas-sensor";
1095
+ DeviceRole["TamperSensor"] = "tamper-sensor";
1096
+ DeviceRole["VibrationSensor"] = "vibration-sensor";
1097
+ DeviceRole["ConnectivitySensor"] = "connectivity-sensor";
1098
+ DeviceRole["SoundSensor"] = "sound-sensor";
1099
+ /** Fallback for `binary_sensor` without a known `device_class`. */
1100
+ DeviceRole["BinarySensor"] = "binary-sensor";
1101
+ DeviceRole["TemperatureSensor"] = "temperature-sensor";
1102
+ DeviceRole["HumiditySensor"] = "humidity-sensor";
1103
+ DeviceRole["AmbientLightSensor"] = "ambient-light-sensor";
1104
+ DeviceRole["PressureSensor"] = "pressure-sensor";
1105
+ /** Wind speed or direction (weather-station `wind-sensor` cap). */
1106
+ DeviceRole["WindSensor"] = "wind-sensor";
1107
+ /** Rain accumulation or rate (weather-station `rain-sensor` cap). */
1108
+ DeviceRole["RainSensor"] = "rain-sensor";
1109
+ /** UV index (weather-station `uv-sensor` cap). */
1110
+ DeviceRole["UvSensor"] = "uv-sensor";
1111
+ /** Solar irradiance W/m² (weather-station `solar-radiation-sensor` cap).
1112
+ * Distinct from AmbientLightSensor (lux). */
1113
+ DeviceRole["SolarRadiationSensor"] = "solar-radiation-sensor";
1114
+ /** Soil moisture % (garden/weather `soil-moisture-sensor` cap). */
1115
+ DeviceRole["SoilMoistureSensor"] = "soil-moisture-sensor";
1116
+ DeviceRole["PowerSensor"] = "power-sensor";
1117
+ DeviceRole["EnergySensor"] = "energy-sensor";
1118
+ DeviceRole["VoltageSensor"] = "voltage-sensor";
1119
+ DeviceRole["CurrentSensor"] = "current-sensor";
1120
+ DeviceRole["AirQualitySensor"] = "air-quality-sensor";
1121
+ /** Battery level (numeric % via `sensor` OR low-bool via
1122
+ * `binary_sensor` — the cap distinguishes via the value type). */
1123
+ DeviceRole["BatterySensor"] = "battery-sensor";
1124
+ /** Fallback for `sensor` numeric without a known `device_class`. */
1125
+ DeviceRole["NumericSensor"] = "numeric-sensor";
1126
+ /** String / enum state (HA `sensor` with `state_class: enum` or
1127
+ * `attributes.options`). */
1128
+ DeviceRole["EnumSensor"] = "enum-sensor";
1129
+ /** Date / timestamp state (HA `sensor` with `device_class: timestamp`
1130
+ * or `date`). The slice carries the raw ISO string verbatim (hosted on
1131
+ * the `enum-sensor` cap); the UI renders it locale-formatted. */
1132
+ DeviceRole["DateTimeSensor"] = "datetime-sensor";
1133
+ /** Last-resort fallback when nothing else matches. */
1134
+ DeviceRole["GenericSensor"] = "generic-sensor";
1135
+ DeviceRole["NumericControl"] = "numeric-control";
1136
+ DeviceRole["SelectControl"] = "select-control";
1137
+ DeviceRole["TextControl"] = "text-control";
1138
+ DeviceRole["DateTimeControl"] = "datetime-control";
1139
+ /** Mobile push notifier (HA `notify.mobile_app_*`) — supports
1140
+ * rich features (image, priority, channel routing). */
1141
+ DeviceRole["MobilePushNotifier"] = "mobile-push-notifier";
1142
+ /** Chat / messaging service (HA `notify.telegram_*`,
1143
+ * `notify.discord_*`, etc.). */
1144
+ DeviceRole["MessagingNotifier"] = "messaging-notifier";
1145
+ /** Email-based delivery (HA `notify.smtp`, etc.). */
1146
+ DeviceRole["EmailNotifier"] = "email-notifier";
1147
+ /** Fallback when the notifier service name doesn't match a known
1148
+ * pattern. */
1149
+ DeviceRole["GenericNotifier"] = "generic-notifier";
1150
+ /** Emits a pulse train (infrared or RF) on request — the `ir-transmitter`
1151
+ * cap. A `DeviceType.Generic` device. */
1152
+ DeviceRole["IrTransmitter"] = "ir-transmitter";
1153
+ return DeviceRole;
1154
+ }({});
1155
+ //#endregion
780
1156
  //#region ../../node_modules/zod/v4/core/core.js
781
1157
  var _a$1;
782
1158
  function $constructor(name, initializer, params) {
@@ -5455,7 +5831,7 @@ var ZodIssueCode = {
5455
5831
  var ZodFirstPartyTypeKind;
5456
5832
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5457
5833
  //#endregion
5458
- //#region ../types/dist/sleep-DSVWZfJC.mjs
5834
+ //#region ../types/dist/sleep-CSYuxssh.mjs
5459
5835
  /**
5460
5836
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5461
5837
  * window to float samples (D455).
@@ -5975,6 +6351,102 @@ function emitReadiness(bus, params) {
5975
6351
  ts
5976
6352
  }));
5977
6353
  }
6354
+ var NOT_RUN = {
6355
+ declared: false,
6356
+ snapshot: null,
6357
+ applied: [],
6358
+ failed: [],
6359
+ blocked: [],
6360
+ skipped: [],
6361
+ blockOwner: []
6362
+ };
6363
+ /**
6364
+ * Offer `migrations` to the engine and run what it says to run, in its order. Never
6365
+ * throws: an unreachable or refusing engine is logged and reported `declared: false`,
6366
+ * because the hub (and this addon's boot) must not hang on a migration.
6367
+ */
6368
+ async function runAddonMigrations(host, migrations, logger) {
6369
+ const first = migrations[0];
6370
+ if (first === void 0) return NOT_RUN;
6371
+ const owner = first.meta.owner;
6372
+ const log = logger.withTags({ migrationOwner: owner });
6373
+ const byId = new Map(migrations.map((m) => [m.meta.id, m]));
6374
+ let declaration;
6375
+ try {
6376
+ declaration = await host.declareOwner({
6377
+ owner,
6378
+ metas: migrations.map((m) => m.meta)
6379
+ });
6380
+ } catch (err) {
6381
+ log.error("the migration engine refused or could not take this addon catalogue — none ran", { meta: { error: errMsg(err) } });
6382
+ return {
6383
+ ...NOT_RUN,
6384
+ blockOwner: migrations.filter((m) => m.meta.onFailure === "block-owner").map((m) => m.meta.id)
6385
+ };
6386
+ }
6387
+ const applied = [];
6388
+ const failed = [];
6389
+ const blocked = [];
6390
+ const skipped = [];
6391
+ const settle = (id, status) => {
6392
+ if (status === "applied") applied.push(id);
6393
+ else if (status === "failed") failed.push(id);
6394
+ else if (status === "blocked") blocked.push(id);
6395
+ else skipped.push(id);
6396
+ };
6397
+ for (const step of declaration.steps) {
6398
+ const bound = byId.get(step.id);
6399
+ if (bound === void 0 || step.action === "skip") {
6400
+ skipped.push(step.id);
6401
+ continue;
6402
+ }
6403
+ const tags = { migrationId: step.id };
6404
+ try {
6405
+ const gate = await host.startMigration({ id: step.id });
6406
+ if (!gate.proceed) {
6407
+ log.warn("migration not run", {
6408
+ tags,
6409
+ meta: {
6410
+ state: gate.state,
6411
+ reason: gate.reason
6412
+ }
6413
+ });
6414
+ settle(step.id, gate.state === "pending" ? "blocked" : gate.state);
6415
+ continue;
6416
+ }
6417
+ const result = await bound.execute();
6418
+ const outcome = await host.finishMigration({
6419
+ id: step.id,
6420
+ result
6421
+ });
6422
+ if (outcome.status === "applied") log.info("migration applied", {
6423
+ tags,
6424
+ meta: { rowsChanged: outcome.rowsChanged }
6425
+ });
6426
+ else log.error("migration failed", {
6427
+ tags,
6428
+ meta: { error: outcome.error }
6429
+ });
6430
+ settle(step.id, outcome.status);
6431
+ } catch (err) {
6432
+ log.error("migration could not be completed with the engine", {
6433
+ tags,
6434
+ meta: { error: errMsg(err) }
6435
+ });
6436
+ failed.push(step.id);
6437
+ }
6438
+ }
6439
+ const blockOwner = [...failed, ...blocked].filter((id) => byId.get(id)?.meta.onFailure === "block-owner");
6440
+ return {
6441
+ declared: true,
6442
+ snapshot: declaration.snapshot,
6443
+ applied,
6444
+ failed,
6445
+ blocked,
6446
+ skipped,
6447
+ blockOwner
6448
+ };
6449
+ }
5978
6450
  /**
5979
6451
  * Build a {@link DurableState} over a single store key. Transport-agnostic:
5980
6452
  * `read`/`write` are the addon-store or device-store accessors. The whole
@@ -7030,6 +7502,28 @@ var BaseAddon = class {
7030
7502
  return normalized;
7031
7503
  }
7032
7504
  /**
7505
+ * Run this addon's migration catalogue (D987 M3) through the hub's `migrations`
7506
+ * engine. Call it at the point of the boot where the data must already have the new
7507
+ * shape — `onInitialize`, or `onRestoreDevices` for a migration that needs the
7508
+ * restored devices — with the catalogue bound to the addon's ports
7509
+ * (`defineAddonMigrations(ADDON_MIGRATIONS, ports)`).
7510
+ *
7511
+ * An engine that cannot be reached or refuses the catalogue is logged and runs
7512
+ * nothing; the boot continues. A migration that FAILED (or was blocked by one)
7513
+ * and declares `onFailure: 'block-owner'` makes this throw, so the addon does not
7514
+ * serve data whose shape it cannot trust; `tolerate` migrations never do.
7515
+ */
7516
+ async runMigrations(migrations) {
7517
+ const api = this.ctx.api.migrations;
7518
+ const report = await runAddonMigrations({
7519
+ declareOwner: (input) => api.declareOwner.mutate(input),
7520
+ startMigration: (input) => api.startMigration.mutate(input),
7521
+ finishMigration: (input) => api.finishMigration.mutate(input)
7522
+ }, migrations, this.ctx.logger);
7523
+ 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`);
7524
+ return report;
7525
+ }
7526
+ /**
7033
7527
  * Called by the isolated-process runner AFTER `broker.start()`.
7034
7528
  * In-process addons never need this because the hub broker is already
7035
7529
  * running when `initialize()` fires. For forked children the broker
@@ -8139,407 +8633,6 @@ var RecordKindDeclarationSchema = object({
8139
8633
  }).strict();
8140
8634
  /** The kind names: lower-case, dash separated, short enough for an index name. */
8141
8635
  var RecordKindNameSchema = string().regex(/^[a-z][a-z0-9-]{0,47}$/);
8142
- var READING_QUANTITIES = [
8143
- "temperature",
8144
- "pressure",
8145
- "speed",
8146
- "precipitation",
8147
- "precipitation-rate",
8148
- "length",
8149
- "area",
8150
- "illuminance",
8151
- "irradiance",
8152
- "power",
8153
- "energy",
8154
- "voltage",
8155
- "current",
8156
- "apparent-power",
8157
- "humidity",
8158
- "percentage",
8159
- "concentration-volume",
8160
- "concentration-mass",
8161
- "concentration-ppb",
8162
- "volume",
8163
- "duration",
8164
- "duration-ms",
8165
- "information",
8166
- "sound-level",
8167
- "signal-level",
8168
- "colour-temperature",
8169
- "angle",
8170
- "bitrate",
8171
- "rate",
8172
- "mass",
8173
- "index",
8174
- "ratio",
8175
- "count",
8176
- "none",
8177
- "device"
8178
- ];
8179
- var ReadingQuantitySchema = _enum(READING_QUANTITIES);
8180
- new Set(READING_QUANTITIES);
8181
- /**
8182
- * Stored unit symbol for a quantity. `none` means the number is dimensionless
8183
- * (or an index / ratio / count). `device` means the cap has no single unit.
8184
- */
8185
- var QUANTITY_BASE = {
8186
- temperature: "°C",
8187
- pressure: "hPa",
8188
- speed: "m/s",
8189
- precipitation: "mm",
8190
- "precipitation-rate": "mm/h",
8191
- length: "m",
8192
- area: "m²",
8193
- illuminance: "lx",
8194
- irradiance: "W/m²",
8195
- power: "W",
8196
- energy: "kWh",
8197
- voltage: "V",
8198
- current: "A",
8199
- "apparent-power": "VA",
8200
- humidity: "%",
8201
- percentage: "%",
8202
- "concentration-volume": "ppm",
8203
- "concentration-mass": "µg/m³",
8204
- "concentration-ppb": "ppb",
8205
- volume: "L",
8206
- duration: "s",
8207
- "duration-ms": "ms",
8208
- information: "B",
8209
- "sound-level": "dBFS",
8210
- "signal-level": "dBm",
8211
- "colour-temperature": "mired",
8212
- angle: "°",
8213
- bitrate: "kbps",
8214
- rate: "fps",
8215
- mass: "kg",
8216
- index: "none",
8217
- ratio: "none",
8218
- count: "none",
8219
- none: "none",
8220
- device: "device"
8221
- };
8222
- /** The stored unit, or `undefined` when the quantity has no unit symbol. */
8223
- function baseUnitOf(quantity) {
8224
- const symbol = QUANTITY_BASE[quantity];
8225
- if (symbol === "none" || symbol === "device") return void 0;
8226
- return symbol;
8227
- }
8228
- function readingMeta(quantity, extras) {
8229
- return {
8230
- quantity,
8231
- ...extras?.precision !== void 0 ? { precision: extras.precision } : {},
8232
- ...extras?.label !== void 0 ? { label: extras.label } : {}
8233
- };
8234
- }
8235
- /** Shared Zod schemas used across streaming capabilities. */
8236
- var CamProfileSchema = _enum([
8237
- "high",
8238
- "mid",
8239
- "low"
8240
- ]);
8241
- /**
8242
- * Which rung a per-camera consumer listens on: an explicit profile, or `auto`.
8243
- *
8244
- * `auto` is a REAL value, not an absent one — it means "resolve it by the rule
8245
- * that consumer owns". It lives beside `CamProfile` because a rung is the only
8246
- * operator-facing way to name a stream: a cam-stream id is broker-internal and
8247
- * moves when a slot is reassigned, so a stored id silently stops meaning what
8248
- * the operator picked. Today's one user is the audio analyser, whose rule is
8249
- * "the lowest AVAILABLE native rung" (D537).
8250
- */
8251
- var AudioStreamPickSchema = union([CamProfileSchema, literal("auto")]);
8252
- var CamStreamKindSchema = _enum([
8253
- "pull-rtsp",
8254
- "pull-rtmp",
8255
- "pull-http",
8256
- "pull-flv",
8257
- "pull-rfc4571",
8258
- "push-annexb",
8259
- "derived"
8260
- ]);
8261
- var CamStreamResolutionSchema = object({
8262
- width: number().int().positive().meta(readingMeta("none")),
8263
- height: number().int().positive().meta(readingMeta("none"))
8264
- });
8265
- var CameraStreamSchema = object({
8266
- /** Stable, provider-assigned id unique within the (deviceId) scope. */
8267
- camStreamId: string().min(1),
8268
- deviceId: number().int().nonnegative(),
8269
- kind: CamStreamKindSchema,
8270
- /** Required for pull-* kinds. Ignored for push-annexb. */
8271
- url: string().optional(),
8272
- codec: string().optional(),
8273
- resolution: CamStreamResolutionSchema.optional(),
8274
- fps: number().positive().optional(),
8275
- /** Human label surfaced in the Admin UI "Camera Stream" dropdown. */
8276
- label: string().optional(),
8277
- /**
8278
- * Device-level features the publisher advertised (e.g. `battery-operated`).
8279
- * The broker, snapshot orchestrator, and prebuffer manager all consult
8280
- * this list to derive policy — relaxed stall watchdog for battery
8281
- * cams, prebuffer off by default, longer snapshot rate-limit, etc.
8282
- *
8283
- * Single source of truth replacing per-stream flags like the
8284
- * historical `allowStall`: if the publisher knows the camera is
8285
- * battery-powered, every downstream service derives the right policy
8286
- * from this list.
8287
- */
8288
- deviceFeatures: array(string()).optional(),
8289
- /**
8290
- * Whether this stream participates in the broker's automatic profile
8291
- * assignment (`computeInitialAssignment`). Defaults to `true`. Publishers
8292
- * use `false` when they want a stream to be SELECTABLE in the UI but not
8293
- * picked by default — e.g. Reolink publishes its native Baichuan streams
8294
- * as `autoEligible: true` (the recommended path) and its RTSP / RTMP
8295
- * mirrors as `autoEligible: false` (still pickable per slot, just not
8296
- * the auto choice). Manual `assignProfile` calls remain valid for
8297
- * non-eligible streams.
8298
- */
8299
- autoEligible: boolean().optional(),
8300
- /**
8301
- * The profile slot the PUBLISHER says this stream is: `low` for a stream
8302
- * whose only sensible role is the low-bandwidth one, `high` for a main
8303
- * stream. Consulted by the broker's automatic ranking
8304
- * (`rankInitialAssignment`) BEFORE the pixel-count heuristic, and only there:
8305
- * an operator's manual assignment still wins, and an absent hint is the
8306
- * pixel-ranked default every camera had before this field existed.
8307
- *
8308
- * Why it exists: a camera with ONE stream ranked to `mid` — the slot no
8309
- * storage class is written for on its own (`recordings` is high|mid,
8310
- * `recordingsLow` is low) and the one profile scrub does not read. The robot
8311
- * camera's single 720p relay is a `low` in every respect except the slot the
8312
- * heuristic put it in.
8313
- */
8314
- profileHint: CamProfileSchema.optional(),
8315
- /**
8316
- * Transport-specific opaque metadata. The broker passes it through to
8317
- * the source reader without inspecting it. Currently used by
8318
- * `pull-rfc4571` streams to carry the upstream SDP (so the reader can
8319
- * route RTP packets to the right depacketizer without an in-band
8320
- * DESCRIBE phase). Other kinds typically leave it undefined.
8321
- */
8322
- metadata: record(string(), unknown()).optional()
8323
- });
8324
- var ProfileSlotStatusSchema = _enum([
8325
- "unassigned",
8326
- "idle",
8327
- "connecting",
8328
- "streaming",
8329
- "error"
8330
- ]);
8331
- var ProfileSlotSchema = object({
8332
- deviceId: number().int().nonnegative(),
8333
- profile: CamProfileSchema,
8334
- /** Broker id the rest of the system addresses: `${deviceId}/${profile}`. */
8335
- brokerId: string(),
8336
- /** `null` when the profile is unassigned. */
8337
- sourceCamStreamId: string().nullable(),
8338
- status: ProfileSlotStatusSchema,
8339
- resolution: CamStreamResolutionSchema.optional(),
8340
- codec: string().optional(),
8341
- preBufferSec: number().nonnegative().optional(),
8342
- errorMessage: string().optional()
8343
- });
8344
- /**
8345
- * Zod schema for StreamSourceEntry — the canonical stream descriptor
8346
- * exposed by ICameraDevice.getStreamSources() and consumed by the broker.
8347
- */
8348
- var StreamSourceEntrySchema$1 = object({
8349
- id: string(),
8350
- label: string(),
8351
- protocol: _enum([
8352
- "rtsp",
8353
- "rtmp",
8354
- "annexb",
8355
- "http-mjpeg",
8356
- "webrtc",
8357
- "custom"
8358
- ]),
8359
- url: string().optional(),
8360
- resolution: object({
8361
- width: number(),
8362
- height: number()
8363
- }).readonly().optional(),
8364
- fps: number().optional(),
8365
- bitrate: number().optional(),
8366
- codec: string().optional(),
8367
- profileHint: CamProfileSchema.optional()
8368
- });
8369
- object({
8370
- type: string(),
8371
- url: string(),
8372
- videoCodec: string().optional(),
8373
- audioCodec: string().optional(),
8374
- metadata: record(string(), unknown()).readonly().optional()
8375
- });
8376
- /**
8377
- * Wire schema for `FrameHandle` — a zero-pixel, serialisable reference to a
8378
- * frame in a shared-memory ring (Phase 5 / D9). Mirrors the `FrameHandle` TS
8379
- * interface in `interfaces/frame-handle.ts` field-for-field so a cap method
8380
- * (the runner's native-crop and pipeline-executor inputs) can carry handles
8381
- * over tRPC / Moleculer.
8382
- *
8383
- * The `satisfies` assertion below pins this schema to the interface: if a
8384
- * field is added to / removed from `FrameHandle` without a matching schema
8385
- * edit, `z.infer<typeof FrameHandleSchema>` stops being assignable to
8386
- * `FrameHandle` and the build fails — no silent drift.
8387
- */
8388
- var FrameHandleSchema = object({
8389
- shmId: string(),
8390
- slot: number().int().nonnegative(),
8391
- seq: number().int().nonnegative(),
8392
- width: number().int().positive(),
8393
- height: number().int().positive(),
8394
- format: _enum([
8395
- "jpeg",
8396
- "rgb",
8397
- "bgr",
8398
- "yuv420",
8399
- "gray"
8400
- ]),
8401
- pts: number(),
8402
- byteLength: number().int().nonnegative(),
8403
- nodeId: string(),
8404
- slotCount: number().int().positive()
8405
- });
8406
- /**
8407
- * Wire schema for a decoded audio chunk (Phase 5 / D9). Mirrors the
8408
- * `DecodedAudioChunk` TS interface in `interfaces/stream-broker.ts`: PCM
8409
- * sample bytes plus the track parameters a consumer needs to interpret them.
8410
- *
8411
- * Audio chunks are tiny (a ~500ms AudioCodecSession window is a few KB of
8412
- * Float32 PCM), so unlike video frames they ship their bytes INLINE over
8413
- * tRPC / Moleculer — no shared-memory plane. `data` is typed `Uint8Array`
8414
- * (the wire-serialisable supertype of `Buffer`); a `Buffer` is assignable to
8415
- * it.
8416
- */
8417
- var AudioChunkFormatSchema = _enum(AUDIO_CHUNK_FORMATS);
8418
- var DecodedAudioChunkSchema = object({
8419
- data: _instanceof(Uint8Array),
8420
- sampleRate: number().int().positive(),
8421
- channels: number().int().positive(),
8422
- timestamp: number(),
8423
- /**
8424
- * Byte format of `data`. ABSENT MEANS `f32le` — today's bytes, byte for
8425
- * byte, for any peer that never heard of this field. A coded window
8426
- * (`pcmu` / `pcma`, one byte per sample) is only ever emitted to a
8427
- * subscription that DECLARED it accepts one, so absence can never mean
8428
- * "coded bytes a consumer will read as floats" (D455).
8429
- */
8430
- format: AudioChunkFormatSchema.optional()
8431
- });
8432
- /**
8433
- * Input for `stream-broker.subscribeAudioChunks` (Phase 5 / D9). Audio ships
8434
- * its bytes inline: a consumer asks the broker for the decoded audio-chunk
8435
- * stream of one broker; the broker keeps a per-subscription bounded FIFO queue
8436
- * the consumer drains via `pullAudioChunks`.
8437
- */
8438
- var SubscribeAudioChunksInputSchema = object({
8439
- brokerId: string(),
8440
- /** Short caller-identity tag (`audio-analyzer`, …) for `listClients`. */
8441
- tag: string().optional(),
8442
- /**
8443
- * Byte formats this subscriber can READ, best first. The broker serves the
8444
- * chunk's own format when it is in this list and expands to `f32le`
8445
- * otherwise, so a subscriber is never handed bytes it cannot interpret.
8446
- *
8447
- * Absent (or without the source format) means `f32le` — the behaviour every
8448
- * subscriber had before D455, unchanged. This is the negotiation half of
8449
- * the source-bytes lever: it is what lets the broker and its consumers
8450
- * deploy one at a time across three nodes.
8451
- */
8452
- accept: array(AudioChunkFormatSchema).readonly().optional()
8453
- });
8454
- /** Result of `stream-broker.subscribeAudioChunks`. */
8455
- var SubscribeAudioChunksResultSchema = object({
8456
- /** Opaque id passed to `pullAudioChunks` / `unsubscribeAudioChunks`. */
8457
- subscriptionId: string() });
8458
- var BrokerStatsSchema = object({
8459
- status: _enum([
8460
- "idle",
8461
- "connecting",
8462
- "streaming",
8463
- "error",
8464
- "stopped"
8465
- ]),
8466
- inputFps: number(),
8467
- encodedSubscribers: number(),
8468
- uptimeMs: number(),
8469
- bitrateKbps: number(),
8470
- idrIntervalMs: number(),
8471
- codec: string().optional(),
8472
- totalBytes: number(),
8473
- packetCount: number(),
8474
- rtspClients: number(),
8475
- pipeClients: number(),
8476
- preBufferSec: number(),
8477
- preBufferMs: number(),
8478
- preBufferPackets: number(),
8479
- /**
8480
- * Detected audio track parameters from the RTSP DESCRIBE / SDP. `null`
8481
- * when the stream has no audio track or the broker is in cold start.
8482
- * `supported = false` means the codec was detected but the local
8483
- * decoder pipeline cannot produce PCM chunks (e.g. AAC without the
8484
- * AAC pipeline wired). Surfaced in the UI device-overview so operators
8485
- * can pre-pick the audio-analysis model that matches the codec.
8486
- */
8487
- audio: object({
8488
- codec: string(),
8489
- sampleRate: number(),
8490
- channels: number(),
8491
- supported: boolean()
8492
- }).nullable().optional(),
8493
- /**
8494
- * BROKER-SIDE AUDIO MUTE (D83). `true` = this broker is deliberately
8495
- * distributing none of the device's audio, on live or recording.
8496
- *
8497
- * Present so a silent camera can be told apart from a broken one on the
8498
- * stream panel itself, without cross-referencing the switch group: a
8499
- * broker holding an `audio` track descriptor while `audioMuted` is true is
8500
- * working exactly as asked. `audioMutedDropped` counts the audio units
8501
- * thrown away since the current dial — it is how you confirm from stats
8502
- * alone that the mute is on the packet path and not merely persisted.
8503
- */
8504
- audioMuted: boolean().optional(),
8505
- audioMutedDropped: number().optional()
8506
- });
8507
- /**
8508
- * Exporter-facing "profile restream" entry. Returned by
8509
- * `cameraStreams.getProfileRtspEntries` — one per ASSIGNED profile slot
8510
- * (high/mid/low). `brokerId` is the PROFILE-keyed broker id
8511
- * (`${deviceId}/${profile}`, e.g. `15/high`); its `url` is a broker
8512
- * RTSP restream that aliases the profile's assigned source broker, so
8513
- * every consumer dialling a profile converges on the same single pull.
8514
- * `codec`/`resolution` describe the assigned SOURCE camStream (for the
8515
- * `auto` resolution-closest pick). Distinct from `RtspRestreamEntry`
8516
- * (raw per-camStream, live-view only).
8517
- */
8518
- var ProfileRtspEntrySchema = object({
8519
- profile: CamProfileSchema,
8520
- /** Profile-keyed broker id, format `${deviceId}/${profile}` (e.g. `"15/high"`). */
8521
- brokerId: string(),
8522
- url: string(),
8523
- mutedUrl: string(),
8524
- enabled: boolean(),
8525
- codec: string().optional(),
8526
- resolution: CamStreamResolutionSchema.optional()
8527
- });
8528
- /**
8529
- * The model weight formats a catalog entry can ship. Schema-free on purpose:
8530
- * `types/models.ts` builds Zod schemas from it, but a runner-closure consumer
8531
- * (the model downloader in `@camstack/system`) needs only the list and must
8532
- * reach it through `@camstack/types/addon` without building the cap schema
8533
- * graph (D28).
8534
- */
8535
- var MODEL_FORMATS = [
8536
- "onnx",
8537
- "coreml",
8538
- "openvino",
8539
- "tflite",
8540
- "pt",
8541
- "gguf"
8542
- ];
8543
8636
  object({
8544
8637
  field: string().min(1),
8545
8638
  on: literal("advance")
@@ -8632,6 +8725,407 @@ function optionalMethod(input, output, options) {
8632
8725
  function event(data) {
8633
8726
  return { data };
8634
8727
  }
8728
+ var READING_QUANTITIES = [
8729
+ "temperature",
8730
+ "pressure",
8731
+ "speed",
8732
+ "precipitation",
8733
+ "precipitation-rate",
8734
+ "length",
8735
+ "area",
8736
+ "illuminance",
8737
+ "irradiance",
8738
+ "power",
8739
+ "energy",
8740
+ "voltage",
8741
+ "current",
8742
+ "apparent-power",
8743
+ "humidity",
8744
+ "percentage",
8745
+ "concentration-volume",
8746
+ "concentration-mass",
8747
+ "concentration-ppb",
8748
+ "volume",
8749
+ "duration",
8750
+ "duration-ms",
8751
+ "information",
8752
+ "sound-level",
8753
+ "signal-level",
8754
+ "colour-temperature",
8755
+ "angle",
8756
+ "bitrate",
8757
+ "rate",
8758
+ "mass",
8759
+ "index",
8760
+ "ratio",
8761
+ "count",
8762
+ "none",
8763
+ "device"
8764
+ ];
8765
+ var ReadingQuantitySchema = _enum(READING_QUANTITIES);
8766
+ new Set(READING_QUANTITIES);
8767
+ /**
8768
+ * Stored unit symbol for a quantity. `none` means the number is dimensionless
8769
+ * (or an index / ratio / count). `device` means the cap has no single unit.
8770
+ */
8771
+ var QUANTITY_BASE = {
8772
+ temperature: "°C",
8773
+ pressure: "hPa",
8774
+ speed: "m/s",
8775
+ precipitation: "mm",
8776
+ "precipitation-rate": "mm/h",
8777
+ length: "m",
8778
+ area: "m²",
8779
+ illuminance: "lx",
8780
+ irradiance: "W/m²",
8781
+ power: "W",
8782
+ energy: "kWh",
8783
+ voltage: "V",
8784
+ current: "A",
8785
+ "apparent-power": "VA",
8786
+ humidity: "%",
8787
+ percentage: "%",
8788
+ "concentration-volume": "ppm",
8789
+ "concentration-mass": "µg/m³",
8790
+ "concentration-ppb": "ppb",
8791
+ volume: "L",
8792
+ duration: "s",
8793
+ "duration-ms": "ms",
8794
+ information: "B",
8795
+ "sound-level": "dBFS",
8796
+ "signal-level": "dBm",
8797
+ "colour-temperature": "mired",
8798
+ angle: "°",
8799
+ bitrate: "kbps",
8800
+ rate: "fps",
8801
+ mass: "kg",
8802
+ index: "none",
8803
+ ratio: "none",
8804
+ count: "none",
8805
+ none: "none",
8806
+ device: "device"
8807
+ };
8808
+ /** The stored unit, or `undefined` when the quantity has no unit symbol. */
8809
+ function baseUnitOf(quantity) {
8810
+ const symbol = QUANTITY_BASE[quantity];
8811
+ if (symbol === "none" || symbol === "device") return void 0;
8812
+ return symbol;
8813
+ }
8814
+ function readingMeta(quantity, extras) {
8815
+ return {
8816
+ quantity,
8817
+ ...extras?.precision !== void 0 ? { precision: extras.precision } : {},
8818
+ ...extras?.label !== void 0 ? { label: extras.label } : {}
8819
+ };
8820
+ }
8821
+ /** Shared Zod schemas used across streaming capabilities. */
8822
+ var CamProfileSchema = _enum([
8823
+ "high",
8824
+ "mid",
8825
+ "low"
8826
+ ]);
8827
+ /**
8828
+ * Which rung a per-camera consumer listens on: an explicit profile, or `auto`.
8829
+ *
8830
+ * `auto` is a REAL value, not an absent one — it means "resolve it by the rule
8831
+ * that consumer owns". It lives beside `CamProfile` because a rung is the only
8832
+ * operator-facing way to name a stream: a cam-stream id is broker-internal and
8833
+ * moves when a slot is reassigned, so a stored id silently stops meaning what
8834
+ * the operator picked. Today's one user is the audio analyser, whose rule is
8835
+ * "the lowest AVAILABLE native rung" (D537).
8836
+ */
8837
+ var AudioStreamPickSchema = union([CamProfileSchema, literal("auto")]);
8838
+ var CamStreamKindSchema = _enum([
8839
+ "pull-rtsp",
8840
+ "pull-rtmp",
8841
+ "pull-http",
8842
+ "pull-flv",
8843
+ "pull-rfc4571",
8844
+ "push-annexb",
8845
+ "derived"
8846
+ ]);
8847
+ var CamStreamResolutionSchema = object({
8848
+ width: number().int().positive().meta(readingMeta("none")),
8849
+ height: number().int().positive().meta(readingMeta("none"))
8850
+ });
8851
+ var CameraStreamSchema = object({
8852
+ /** Stable, provider-assigned id unique within the (deviceId) scope. */
8853
+ camStreamId: string().min(1),
8854
+ deviceId: number().int().nonnegative(),
8855
+ kind: CamStreamKindSchema,
8856
+ /** Required for pull-* kinds. Ignored for push-annexb. */
8857
+ url: string().optional(),
8858
+ codec: string().optional(),
8859
+ resolution: CamStreamResolutionSchema.optional(),
8860
+ fps: number().positive().optional(),
8861
+ /** Human label surfaced in the Admin UI "Camera Stream" dropdown. */
8862
+ label: string().optional(),
8863
+ /**
8864
+ * Device-level features the publisher advertised (e.g. `battery-operated`).
8865
+ * The broker, snapshot orchestrator, and prebuffer manager all consult
8866
+ * this list to derive policy — relaxed stall watchdog for battery
8867
+ * cams, prebuffer off by default, longer snapshot rate-limit, etc.
8868
+ *
8869
+ * Single source of truth replacing per-stream flags like the
8870
+ * historical `allowStall`: if the publisher knows the camera is
8871
+ * battery-powered, every downstream service derives the right policy
8872
+ * from this list.
8873
+ */
8874
+ deviceFeatures: array(string()).optional(),
8875
+ /**
8876
+ * Whether this stream participates in the broker's automatic profile
8877
+ * assignment (`computeInitialAssignment`). Defaults to `true`. Publishers
8878
+ * use `false` when they want a stream to be SELECTABLE in the UI but not
8879
+ * picked by default — e.g. Reolink publishes its native Baichuan streams
8880
+ * as `autoEligible: true` (the recommended path) and its RTSP / RTMP
8881
+ * mirrors as `autoEligible: false` (still pickable per slot, just not
8882
+ * the auto choice). Manual `assignProfile` calls remain valid for
8883
+ * non-eligible streams.
8884
+ */
8885
+ autoEligible: boolean().optional(),
8886
+ /**
8887
+ * The profile slot the PUBLISHER says this stream is: `low` for a stream
8888
+ * whose only sensible role is the low-bandwidth one, `high` for a main
8889
+ * stream. Consulted by the broker's automatic ranking
8890
+ * (`rankInitialAssignment`) BEFORE the pixel-count heuristic, and only there:
8891
+ * an operator's manual assignment still wins, and an absent hint is the
8892
+ * pixel-ranked default every camera had before this field existed.
8893
+ *
8894
+ * Why it exists: a camera with ONE stream ranked to `mid` — the slot no
8895
+ * storage class is written for on its own (`recordings` is high|mid,
8896
+ * `recordingsLow` is low) and the one profile scrub does not read. The robot
8897
+ * camera's single 720p relay is a `low` in every respect except the slot the
8898
+ * heuristic put it in.
8899
+ */
8900
+ profileHint: CamProfileSchema.optional(),
8901
+ /**
8902
+ * Transport-specific opaque metadata. The broker passes it through to
8903
+ * the source reader without inspecting it. Currently used by
8904
+ * `pull-rfc4571` streams to carry the upstream SDP (so the reader can
8905
+ * route RTP packets to the right depacketizer without an in-band
8906
+ * DESCRIBE phase). Other kinds typically leave it undefined.
8907
+ */
8908
+ metadata: record(string(), unknown()).optional()
8909
+ });
8910
+ var ProfileSlotStatusSchema = _enum([
8911
+ "unassigned",
8912
+ "idle",
8913
+ "connecting",
8914
+ "streaming",
8915
+ "error"
8916
+ ]);
8917
+ var ProfileSlotSchema = object({
8918
+ deviceId: number().int().nonnegative(),
8919
+ profile: CamProfileSchema,
8920
+ /** Broker id the rest of the system addresses: `${deviceId}/${profile}`. */
8921
+ brokerId: string(),
8922
+ /** `null` when the profile is unassigned. */
8923
+ sourceCamStreamId: string().nullable(),
8924
+ status: ProfileSlotStatusSchema,
8925
+ resolution: CamStreamResolutionSchema.optional(),
8926
+ codec: string().optional(),
8927
+ preBufferSec: number().nonnegative().optional(),
8928
+ errorMessage: string().optional()
8929
+ });
8930
+ /**
8931
+ * Zod schema for StreamSourceEntry — the canonical stream descriptor
8932
+ * exposed by ICameraDevice.getStreamSources() and consumed by the broker.
8933
+ */
8934
+ var StreamSourceEntrySchema$1 = object({
8935
+ id: string(),
8936
+ label: string(),
8937
+ protocol: _enum([
8938
+ "rtsp",
8939
+ "rtmp",
8940
+ "annexb",
8941
+ "http-mjpeg",
8942
+ "webrtc",
8943
+ "custom"
8944
+ ]),
8945
+ url: string().optional(),
8946
+ resolution: object({
8947
+ width: number(),
8948
+ height: number()
8949
+ }).readonly().optional(),
8950
+ fps: number().optional(),
8951
+ bitrate: number().optional(),
8952
+ codec: string().optional(),
8953
+ profileHint: CamProfileSchema.optional()
8954
+ });
8955
+ object({
8956
+ type: string(),
8957
+ url: string(),
8958
+ videoCodec: string().optional(),
8959
+ audioCodec: string().optional(),
8960
+ metadata: record(string(), unknown()).readonly().optional()
8961
+ });
8962
+ /**
8963
+ * Wire schema for `FrameHandle` — a zero-pixel, serialisable reference to a
8964
+ * frame in a shared-memory ring (Phase 5 / D9). Mirrors the `FrameHandle` TS
8965
+ * interface in `interfaces/frame-handle.ts` field-for-field so a cap method
8966
+ * (the runner's native-crop and pipeline-executor inputs) can carry handles
8967
+ * over tRPC / Moleculer.
8968
+ *
8969
+ * The `satisfies` assertion below pins this schema to the interface: if a
8970
+ * field is added to / removed from `FrameHandle` without a matching schema
8971
+ * edit, `z.infer<typeof FrameHandleSchema>` stops being assignable to
8972
+ * `FrameHandle` and the build fails — no silent drift.
8973
+ */
8974
+ var FrameHandleSchema = object({
8975
+ shmId: string(),
8976
+ slot: number().int().nonnegative(),
8977
+ seq: number().int().nonnegative(),
8978
+ width: number().int().positive(),
8979
+ height: number().int().positive(),
8980
+ format: _enum([
8981
+ "jpeg",
8982
+ "rgb",
8983
+ "bgr",
8984
+ "yuv420",
8985
+ "gray"
8986
+ ]),
8987
+ pts: number(),
8988
+ byteLength: number().int().nonnegative(),
8989
+ nodeId: string(),
8990
+ slotCount: number().int().positive()
8991
+ });
8992
+ /**
8993
+ * Wire schema for a decoded audio chunk (Phase 5 / D9). Mirrors the
8994
+ * `DecodedAudioChunk` TS interface in `interfaces/stream-broker.ts`: PCM
8995
+ * sample bytes plus the track parameters a consumer needs to interpret them.
8996
+ *
8997
+ * Audio chunks are tiny (a ~500ms AudioCodecSession window is a few KB of
8998
+ * Float32 PCM), so unlike video frames they ship their bytes INLINE over
8999
+ * tRPC / Moleculer — no shared-memory plane. `data` is typed `Uint8Array`
9000
+ * (the wire-serialisable supertype of `Buffer`); a `Buffer` is assignable to
9001
+ * it.
9002
+ */
9003
+ var AudioChunkFormatSchema = _enum(AUDIO_CHUNK_FORMATS);
9004
+ var DecodedAudioChunkSchema = object({
9005
+ data: _instanceof(Uint8Array),
9006
+ sampleRate: number().int().positive(),
9007
+ channels: number().int().positive(),
9008
+ timestamp: number(),
9009
+ /**
9010
+ * Byte format of `data`. ABSENT MEANS `f32le` — today's bytes, byte for
9011
+ * byte, for any peer that never heard of this field. A coded window
9012
+ * (`pcmu` / `pcma`, one byte per sample) is only ever emitted to a
9013
+ * subscription that DECLARED it accepts one, so absence can never mean
9014
+ * "coded bytes a consumer will read as floats" (D455).
9015
+ */
9016
+ format: AudioChunkFormatSchema.optional()
9017
+ });
9018
+ /**
9019
+ * Input for `stream-broker.subscribeAudioChunks` (Phase 5 / D9). Audio ships
9020
+ * its bytes inline: a consumer asks the broker for the decoded audio-chunk
9021
+ * stream of one broker; the broker keeps a per-subscription bounded FIFO queue
9022
+ * the consumer drains via `pullAudioChunks`.
9023
+ */
9024
+ var SubscribeAudioChunksInputSchema = object({
9025
+ brokerId: string(),
9026
+ /** Short caller-identity tag (`audio-analyzer`, …) for `listClients`. */
9027
+ tag: string().optional(),
9028
+ /**
9029
+ * Byte formats this subscriber can READ, best first. The broker serves the
9030
+ * chunk's own format when it is in this list and expands to `f32le`
9031
+ * otherwise, so a subscriber is never handed bytes it cannot interpret.
9032
+ *
9033
+ * Absent (or without the source format) means `f32le` — the behaviour every
9034
+ * subscriber had before D455, unchanged. This is the negotiation half of
9035
+ * the source-bytes lever: it is what lets the broker and its consumers
9036
+ * deploy one at a time across three nodes.
9037
+ */
9038
+ accept: array(AudioChunkFormatSchema).readonly().optional()
9039
+ });
9040
+ /** Result of `stream-broker.subscribeAudioChunks`. */
9041
+ var SubscribeAudioChunksResultSchema = object({
9042
+ /** Opaque id passed to `pullAudioChunks` / `unsubscribeAudioChunks`. */
9043
+ subscriptionId: string() });
9044
+ var BrokerStatsSchema = object({
9045
+ status: _enum([
9046
+ "idle",
9047
+ "connecting",
9048
+ "streaming",
9049
+ "error",
9050
+ "stopped"
9051
+ ]),
9052
+ inputFps: number(),
9053
+ encodedSubscribers: number(),
9054
+ uptimeMs: number(),
9055
+ bitrateKbps: number(),
9056
+ idrIntervalMs: number(),
9057
+ codec: string().optional(),
9058
+ totalBytes: number(),
9059
+ packetCount: number(),
9060
+ rtspClients: number(),
9061
+ pipeClients: number(),
9062
+ preBufferSec: number(),
9063
+ preBufferMs: number(),
9064
+ preBufferPackets: number(),
9065
+ /**
9066
+ * Detected audio track parameters from the RTSP DESCRIBE / SDP. `null`
9067
+ * when the stream has no audio track or the broker is in cold start.
9068
+ * `supported = false` means the codec was detected but the local
9069
+ * decoder pipeline cannot produce PCM chunks (e.g. AAC without the
9070
+ * AAC pipeline wired). Surfaced in the UI device-overview so operators
9071
+ * can pre-pick the audio-analysis model that matches the codec.
9072
+ */
9073
+ audio: object({
9074
+ codec: string(),
9075
+ sampleRate: number(),
9076
+ channels: number(),
9077
+ supported: boolean()
9078
+ }).nullable().optional(),
9079
+ /**
9080
+ * BROKER-SIDE AUDIO MUTE (D83). `true` = this broker is deliberately
9081
+ * distributing none of the device's audio, on live or recording.
9082
+ *
9083
+ * Present so a silent camera can be told apart from a broken one on the
9084
+ * stream panel itself, without cross-referencing the switch group: a
9085
+ * broker holding an `audio` track descriptor while `audioMuted` is true is
9086
+ * working exactly as asked. `audioMutedDropped` counts the audio units
9087
+ * thrown away since the current dial — it is how you confirm from stats
9088
+ * alone that the mute is on the packet path and not merely persisted.
9089
+ */
9090
+ audioMuted: boolean().optional(),
9091
+ audioMutedDropped: number().optional()
9092
+ });
9093
+ /**
9094
+ * Exporter-facing "profile restream" entry. Returned by
9095
+ * `cameraStreams.getProfileRtspEntries` — one per ASSIGNED profile slot
9096
+ * (high/mid/low). `brokerId` is the PROFILE-keyed broker id
9097
+ * (`${deviceId}/${profile}`, e.g. `15/high`); its `url` is a broker
9098
+ * RTSP restream that aliases the profile's assigned source broker, so
9099
+ * every consumer dialling a profile converges on the same single pull.
9100
+ * `codec`/`resolution` describe the assigned SOURCE camStream (for the
9101
+ * `auto` resolution-closest pick). Distinct from `RtspRestreamEntry`
9102
+ * (raw per-camStream, live-view only).
9103
+ */
9104
+ var ProfileRtspEntrySchema = object({
9105
+ profile: CamProfileSchema,
9106
+ /** Profile-keyed broker id, format `${deviceId}/${profile}` (e.g. `"15/high"`). */
9107
+ brokerId: string(),
9108
+ url: string(),
9109
+ mutedUrl: string(),
9110
+ enabled: boolean(),
9111
+ codec: string().optional(),
9112
+ resolution: CamStreamResolutionSchema.optional()
9113
+ });
9114
+ /**
9115
+ * The model weight formats a catalog entry can ship. Schema-free on purpose:
9116
+ * `types/models.ts` builds Zod schemas from it, but a runner-closure consumer
9117
+ * (the model downloader in `@camstack/server/system`) needs only the list and must
9118
+ * reach it through `@camstack/types/addon` without building the cap schema
9119
+ * graph (D28).
9120
+ */
9121
+ var MODEL_FORMATS = [
9122
+ "onnx",
9123
+ "coreml",
9124
+ "openvino",
9125
+ "tflite",
9126
+ "pt",
9127
+ "gguf"
9128
+ ];
8635
9129
  /**
8636
9130
  * `addon-pages-source` — collection cap exposing per-provider raw page
8637
9131
  * declarations. Every addon that contributes a UI page registers a
@@ -8873,6 +9367,7 @@ var CAP_NAMES = Object.freeze([
8873
9367
  "mesh-network",
8874
9368
  "meter",
8875
9369
  "metrics-provider",
9370
+ "migrations",
8876
9371
  "model-convert",
8877
9372
  "model-distributor",
8878
9373
  "motion",
@@ -8905,6 +9400,7 @@ var CAP_NAMES = Object.freeze([
8905
9400
  "plate-gallery",
8906
9401
  "platform-probe",
8907
9402
  "power-meter",
9403
+ "power-source",
8908
9404
  "presence",
8909
9405
  "pressure-sensor",
8910
9406
  "privacy-mask",
@@ -10256,6 +10752,13 @@ Object.freeze({
10256
10752
  "valueType": "number"
10257
10753
  }
10258
10754
  ],
10755
+ "power-source": [{
10756
+ "path": "switchOn",
10757
+ "valueType": "boolean"
10758
+ }, {
10759
+ "path": "offSeconds",
10760
+ "valueType": "number"
10761
+ }],
10259
10762
  "presence": [
10260
10763
  {
10261
10764
  "path": "state",
@@ -10767,379 +11270,7 @@ object({
10767
11270
  params: record(string(), union([string(), number()])).optional()
10768
11271
  });
10769
11272
  //#endregion
10770
- //#region ../types/dist/err-msg-BD5buxvT.mjs
10771
- var DeviceType = /* @__PURE__ */ function(DeviceType) {
10772
- DeviceType["Camera"] = "camera";
10773
- DeviceType["Hub"] = "hub";
10774
- DeviceType["Light"] = "light";
10775
- DeviceType["Siren"] = "siren";
10776
- DeviceType["Switch"] = "switch";
10777
- DeviceType["Sensor"] = "sensor";
10778
- DeviceType["Thermostat"] = "thermostat";
10779
- /** Air-conditioner / heat-pump climate device (HVAC) — shares the
10780
- * `climate-control` cap surface with `Thermostat` but renders a
10781
- * dedicated AC-appropriate control UI (mode chips, fan speed,
10782
- * independent vertical/horizontal swing). Sources: native Gree, and
10783
- * reusable by other AC integrations. */
10784
- DeviceType["Climate"] = "climate";
10785
- DeviceType["Button"] = "button";
10786
- /** Generic stateless event emitter — carries a device's EXACT declared
10787
- * event vocabulary verbatim (no normalization). Installed with the
10788
- * `event-emitter` cap. Sources: HA `event.*` entities (structured) and
10789
- * HA bus events (e.g. `zha_event`, generic). */
10790
- DeviceType["EventEmitter"] = "event-emitter";
10791
- /** Firmware/software update entity — current vs available version,
10792
- * updatable flag, update state, and an install action. Installed with
10793
- * the `update` cap. Sources: Homematic firmware-update channels (and
10794
- * reusable by other providers, e.g. HA `update.*` entities). */
10795
- DeviceType["Update"] = "update";
10796
- DeviceType["Generic"] = "generic";
10797
- /** Generic notification delivery target (HA `notify.<service>`, future
10798
- * Telegram / Discord / ntfy / SMTP, …). One device per delivery
10799
- * endpoint; the `notifier` cap defines the send surface. */
10800
- DeviceType["Notifier"] = "notifier";
10801
- /** Pre-recorded action sequence with optional parameters
10802
- * (HA `script.*`). Runnable via `script-runner` cap. */
10803
- DeviceType["Script"] = "script";
10804
- /** Automation rule (HA `automation.*`) — enable/disable + manual
10805
- * trigger surface exposed via `automation-control` cap. */
10806
- DeviceType["Automation"] = "automation";
10807
- /** Door / smart lock device (HA `lock.*`). `lock-control` cap. */
10808
- DeviceType["Lock"] = "lock";
10809
- /** Window covering, blinds, garage door, valve, etc. (HA `cover.*`,
10810
- * `valve.*`). `cover` cap with sub-roles for variant. */
10811
- DeviceType["Cover"] = "cover";
10812
- /** Pipe / water / gas valve with open/close/stop and optional
10813
- * position (HA `valve.*`). `valve` cap — a cover-sibling actuator
10814
- * modelled on the same open/closed lifecycle. */
10815
- DeviceType["Valve"] = "valve";
10816
- /** Humidifier / dehumidifier with on/off + target humidity + mode
10817
- * (HA `humidifier.*`). `humidifier` cap — a climate-family actuator
10818
- * modelled on the same target / mode lifecycle. */
10819
- DeviceType["Humidifier"] = "humidifier";
10820
- /** Water heater / boiler with target temperature + operation mode +
10821
- * away mode (HA `water_heater.*`). `water-heater` cap — a
10822
- * climate-family actuator. */
10823
- DeviceType["WaterHeater"] = "water-heater";
10824
- /** Ceiling / standing / exhaust fan (HA `fan.*`). `fan-control` cap. */
10825
- DeviceType["Fan"] = "fan";
10826
- /** Audio / video playback endpoint (HA `media_player.*`). Disjoint from
10827
- * the camera surface — those use `Camera`. `media-player` cap. */
10828
- DeviceType["MediaPlayer"] = "media-player";
10829
- /** Security panel / alarm system (HA `alarm_control_panel.*`).
10830
- * `alarm-panel` cap. */
10831
- DeviceType["AlarmPanel"] = "alarm-panel";
10832
- /** Generic user-settable input (HA `number` / `input_number` / `select`
10833
- * / `input_select` / `text` / `input_text` / `input_datetime`).
10834
- * Sub-type via `DeviceRole`: NumericControl / SelectControl /
10835
- * TextControl / DateTimeControl. */
10836
- DeviceType["Control"] = "control";
10837
- /** Person / device-tracker presence (HA `person.*`, `device_tracker.*`).
10838
- * `presence` cap. */
10839
- DeviceType["Presence"] = "presence";
10840
- /** Weather provider (HA `weather.*`). Tier-3, low MVP priority.
10841
- * `weather` cap. */
10842
- DeviceType["Weather"] = "weather";
10843
- /** Robot vacuum (HA `vacuum.*`). Tier-3. `vacuum-control` cap. */
10844
- DeviceType["Vacuum"] = "vacuum";
10845
- /** Robotic lawn mower (HA `lawn_mower.*`). Tier-3.
10846
- * `lawn-mower-control` cap. */
10847
- DeviceType["LawnMower"] = "lawn-mower";
10848
- /** Physical HA device group — parent container for entity-children
10849
- * adopted from a single HA device entry. Not renderable as a
10850
- * standalone device; exists only to anchor child entities. */
10851
- DeviceType["Container"] = "container";
10852
- /** Single still-image entity (HA `image.*`). Read-only display of an
10853
- * `entity_picture` signed URL the browser loads directly. `image` cap. */
10854
- DeviceType["Image"] = "image";
10855
- /** Smart pet feeder — cloud-connected food dispenser with a bowl food
10856
- * level, battery, desiccant life, feeding state and manual-feed /
10857
- * call-pet / maintenance actions. Installed with the `pet-feeder` cap;
10858
- * dual-hopper models (D4S/D4SH) expose per-hopper portions. Sources:
10859
- * native PetKit (`nodepetkit` `FeederDevice`), reusable by other feeder
10860
- * integrations sharing the same food/desiccant/hopper surface. */
10861
- DeviceType["PetFeeder"] = "pet-feeder";
10862
- /** Pet water fountain — tank level, water drunk, pump and its duty cycle.
10863
- * Installed with the `pet-fountain` cap. Reaches CamStack as loose entities
10864
- * of a container and is BUILT as a composed device (D967). */
10865
- DeviceType["PetFountain"] = "pet-fountain";
10866
- /** Built-in dishwasher — program lifecycle, door / remote-start gates,
10867
- * salt and rinse-aid levels. Installed with the `dishwasher` cap.
10868
- * Sources: native Bosch Home Connect, reusable by similar integrations.
10869
- * A plug-built dishwasher carries running-state and power instead. */
10870
- DeviceType["Dishwasher"] = "dishwasher";
10871
- /** Washing machine. A plug-built one is running while its power stays
10872
- * above a threshold. No program cap — that surface is the dishwasher's. */
10873
- DeviceType["Washer"] = "washer";
10874
- /** Tumble dryer. Same plug-built running surface as {@link Washer}. */
10875
- DeviceType["Dryer"] = "dryer";
10876
- /** The physical bridge box of a vendor ecosystem (Velux KLF-200, and later
10877
- * other radio-to-IP bridges). It carries the box's own facts (firmware,
10878
- * address, connection state) and the actions the box really supports
10879
- * (`reboot`, `device-admin-link`). Products are NOT its children — they are
10880
- * adopted beside it — and its connection settings stay on the integration. */
10881
- DeviceType["Gateway"] = "gateway";
10882
- return DeviceType;
10883
- }({});
10884
- var DeviceFeature = /* @__PURE__ */ function(DeviceFeature) {
10885
- DeviceFeature["BatteryOperated"] = "battery-operated";
10886
- DeviceFeature["Rebootable"] = "rebootable";
10887
- /**
10888
- * Device supports an on-demand re-sync of its derived spec with its
10889
- * upstream source — drives the generic Re-sync button. The owning
10890
- * provider implements the action via the `device-adoption.resync` cap.
10891
- */
10892
- DeviceFeature["Resyncable"] = "resyncable";
10893
- DeviceFeature["NativeSnapshot"] = "native-snapshot";
10894
- DeviceFeature["DoorbellButton"] = "doorbell-button";
10895
- DeviceFeature["TwoWayAudio"] = "two-way-audio";
10896
- DeviceFeature["PanTiltZoom"] = "pan-tilt-zoom";
10897
- /**
10898
- * Camera supports the on-firmware autotrack subsystem (subject-
10899
- * following). Distinct from `PanTiltZoom` because not every PTZ
10900
- * camera ships autotrack — the admin UI uses this flag to gate
10901
- * the autotrack toggle / settings card without re-deriving from
10902
- * the cap registry. Mirrors `ptz-autotrack` cap registration:
10903
- * driver sets this feature when probe confirms the firmware
10904
- * surface, and registers the cap in the same code path.
10905
- */
10906
- DeviceFeature["PtzAutotrack"] = "ptz-autotrack";
10907
- /**
10908
- * Accessory exposes a "trigger on motion" toggle — the parent camera's
10909
- * motion detection automatically activates this device. Mirrors
10910
- * `motion-trigger` cap registration: drivers set this feature in the
10911
- * same code path that calls `ctx.registerNativeCap(motionTriggerCapability, ...)`.
10912
- *
10913
- * Used by admin UI (gate the in-hero `MotionTriggerToggle` against a
10914
- * fast scalar without binding fetch), notifier rules, and `listAll`
10915
- * filters that want "all devices with on-motion behaviour".
10916
- */
10917
- DeviceFeature["MotionTrigger"] = "motion-trigger";
10918
- /** Light supports rgb-triplet color via `color` cap. */
10919
- DeviceFeature["LightColorRgb"] = "light-color-rgb";
10920
- /** Light supports HSV color via `color` cap. */
10921
- DeviceFeature["LightColorHsv"] = "light-color-hsv";
10922
- /** Light supports color-temperature (mired) via `color` cap. */
10923
- DeviceFeature["LightColorMired"] = "light-color-mired";
10924
- /** Thermostat supports a `heat_cool` dual setpoint (targetLow +
10925
- * targetHigh). Gates the range slider UI. */
10926
- DeviceFeature["ClimateDualSetpoint"] = "climate-dual-setpoint";
10927
- /** Thermostat exposes target humidity and/or current humidity
10928
- * readings. Gates the humidity controls. */
10929
- DeviceFeature["ClimateHumidity"] = "climate-humidity";
10930
- /** Thermostat exposes a fan-mode selector. */
10931
- DeviceFeature["ClimateFanMode"] = "climate-fan-mode";
10932
- /** Thermostat exposes preset modes (eco / away / sleep / vendor). */
10933
- DeviceFeature["ClimatePreset"] = "climate-preset";
10934
- /** Thermostat exposes a vertical louver swing toggle. Gates the
10935
- * vertical-swing switch in the climate UI. Independent of horizontal. */
10936
- DeviceFeature["ClimateSwingVertical"] = "climate-swing-vertical";
10937
- /** Thermostat exposes a horizontal louver swing toggle. Gates the
10938
- * horizontal-swing switch in the climate UI. Independent of vertical. */
10939
- DeviceFeature["ClimateSwingHorizontal"] = "climate-swing-horizontal";
10940
- /** Cover exposes intermediate position control (0..100). Gates the
10941
- * position slider UI. */
10942
- DeviceFeature["CoverPositionable"] = "cover-positionable";
10943
- /** Cover exposes slat-tilt control. Gates the tilt slider UI. */
10944
- DeviceFeature["CoverTilt"] = "cover-tilt";
10945
- /** Valve exposes intermediate position control (0..100). Gates the
10946
- * position slider / drag surface UI. */
10947
- DeviceFeature["ValvePositionable"] = "valve-positionable";
10948
- /** Fan exposes a speed-percentage setter. Gates the speed slider UI. */
10949
- DeviceFeature["FanSpeed"] = "fan-speed";
10950
- /** Fan exposes a preset mode selector. */
10951
- DeviceFeature["FanPreset"] = "fan-preset";
10952
- /** Fan exposes blade direction (forward/reverse) — typical of
10953
- * ceiling fans. */
10954
- DeviceFeature["FanDirection"] = "fan-direction";
10955
- /** Fan exposes an oscillation toggle. */
10956
- DeviceFeature["FanOscillating"] = "fan-oscillating";
10957
- /** Lock requires a PIN code on lock/unlock. Gates the code-entry
10958
- * field on the UI lock-controls panel. */
10959
- DeviceFeature["LockPinRequired"] = "lock-pin-required";
10960
- /** Lock supports a latch-release ("open door") action distinct from
10961
- * unlock. Mirrors HA `LockEntityFeature.OPEN` (bit 1) in
10962
- * `supported_features`. Gates the Open Door button in the UI. */
10963
- DeviceFeature["LockOpen"] = "lock-open";
10964
- /** Media player exposes a seek-to-position surface. */
10965
- DeviceFeature["MediaPlayerSeek"] = "media-player-seek";
10966
- /** Media player exposes a volume-level setter. */
10967
- DeviceFeature["MediaPlayerVolume"] = "media-player-volume";
10968
- /** Media player exposes a mute toggle distinct from volume=0. */
10969
- DeviceFeature["MediaPlayerMute"] = "media-player-mute";
10970
- /** Media player exposes a shuffle toggle. */
10971
- DeviceFeature["MediaPlayerShuffle"] = "media-player-shuffle";
10972
- /** Media player exposes a repeat mode (off / all / one). */
10973
- DeviceFeature["MediaPlayerRepeat"] = "media-player-repeat";
10974
- /** Media player exposes a source / input selector. */
10975
- DeviceFeature["MediaPlayerSelectSource"] = "media-player-select-source";
10976
- /** Media player exposes a play-arbitrary-media surface (URL / id). */
10977
- DeviceFeature["MediaPlayerPlayMedia"] = "media-player-play-media";
10978
- /** Media player exposes next-track. */
10979
- DeviceFeature["MediaPlayerNext"] = "media-player-next";
10980
- /** Media player exposes previous-track. */
10981
- DeviceFeature["MediaPlayerPrevious"] = "media-player-previous";
10982
- /** Media player exposes stop distinct from pause. */
10983
- DeviceFeature["MediaPlayerStop"] = "media-player-stop";
10984
- /** Alarm panel requires a PIN code on arm/disarm. */
10985
- DeviceFeature["AlarmPinRequired"] = "alarm-pin-required";
10986
- /** Presence device carries GPS coordinates (lat/lng/accuracy) in
10987
- * addition to a textual location. */
10988
- DeviceFeature["PresenceGps"] = "presence-gps";
10989
- /** Notifier accepts an inline / URL image attachment. */
10990
- DeviceFeature["NotifierImage"] = "notifier-image";
10991
- /** Notifier accepts a priority hint (high/normal/low). */
10992
- DeviceFeature["NotifierPriority"] = "notifier-priority";
10993
- /** Notifier accepts a free-form `data` payload for platform-specific
10994
- * fields. */
10995
- DeviceFeature["NotifierData"] = "notifier-data";
10996
- /** Notifier supports interactive action buttons / callbacks. */
10997
- DeviceFeature["NotifierActions"] = "notifier-actions";
10998
- /** Notifier supports per-call recipient targeting (multi-user). */
10999
- DeviceFeature["NotifierRecipients"] = "notifier-recipients";
11000
- /** Script runner accepts a variables map on each run invocation. */
11001
- DeviceFeature["ScriptVariables"] = "script-variables";
11002
- /** Automation `trigger` accepts a skipCondition flag — fires the
11003
- * automation's actions while bypassing its condition block. */
11004
- DeviceFeature["AutomationSkipCondition"] = "automation-skip-condition";
11005
- /** Robot vacuum exposes a live cleaning map (image child). Gates the
11006
- * map tile in the vacuum UI. */
11007
- DeviceFeature["VacuumHasMap"] = "vacuum-has-map";
11008
- /** Robot vacuum exposes AI obstacle-detection toggles. Gates the AI
11009
- * switches group. */
11010
- DeviceFeature["VacuumHasAi"] = "vacuum-has-ai";
11011
- /** Robot mower exposes a live mowing map (SVG image child). Gates the
11012
- * mower map tile. */
11013
- DeviceFeature["MowerHasMap"] = "mower-has-map";
11014
- /** Robot mower exposes targeted mowing (all-area / zones / edges /
11015
- * spots) — gates the Mow action + map selectors. */
11016
- DeviceFeature["MowerHasTargetedMowing"] = "mower-has-targeted-mowing";
11017
- return DeviceFeature;
11018
- }({});
11019
- /**
11020
- * Semantic role a device plays within its parent. Populated by driver
11021
- * addons when creating accessory devices (Reolink siren/floodlight/
11022
- * PIR/chime/autotrack/doorbell, ONVIF relay outputs, …). Used by the
11023
- * admin UI to pick icons, labels, and widgets — a `Switch` with
11024
- * `role: Floodlight` renders as a bulb with a brightness slider,
11025
- * whereas a `Switch` with `role: Siren` renders as a klaxon.
11026
- *
11027
- * Undefined for top-level devices (cameras, NVRs, hubs). Persisted in
11028
- * sqlite as a nullable TEXT column — old rows keep working unchanged.
11029
- */
11030
- var DeviceRole = /* @__PURE__ */ function(DeviceRole) {
11031
- DeviceRole["Siren"] = "siren";
11032
- DeviceRole["Floodlight"] = "floodlight";
11033
- DeviceRole["Spotlight"] = "spotlight";
11034
- DeviceRole["PirSensor"] = "pir-sensor";
11035
- DeviceRole["Chime"] = "chime";
11036
- DeviceRole["Autotrack"] = "autotrack";
11037
- DeviceRole["Nightvision"] = "nightvision";
11038
- DeviceRole["PrivacyMask"] = "privacy-mask";
11039
- DeviceRole["Doorbell"] = "doorbell";
11040
- /** Interior blind / shade / roller blind. HA `device_class: blind|shade`. */
11041
- DeviceRole["CoverBlind"] = "cover-blind";
11042
- /** Exterior roller shutter. HA `device_class: shutter`; Homematic BROLL. */
11043
- DeviceRole["CoverShutter"] = "cover-shutter";
11044
- /** Exterior awning. HA `device_class: awning`. */
11045
- DeviceRole["CoverAwning"] = "cover-awning";
11046
- /** Curtain / drape. HA `device_class: curtain`. */
11047
- DeviceRole["CoverCurtain"] = "cover-curtain";
11048
- /** Garage door. HA `device_class: garage`. A security actuator: exports gate it. */
11049
- DeviceRole["CoverGarage"] = "cover-garage";
11050
- /** Gate. HA `device_class: gate`. A security actuator: exports gate it. */
11051
- DeviceRole["CoverGate"] = "cover-gate";
11052
- /** Door. HA `device_class: door`. A security actuator: exports gate it. */
11053
- DeviceRole["CoverDoor"] = "cover-door";
11054
- /** Window. HA `device_class: window`. */
11055
- DeviceRole["CoverWindow"] = "cover-window";
11056
- /** Virtual HA toggle (input_boolean.*) — distinguishable from a
11057
- * real Switch device for UI rendering / export adapters. */
11058
- DeviceRole["BinaryHelper"] = "binary-helper";
11059
- /** A smart plug: a `switch` whose source says it is an outlet (HA
11060
- * `device_class: outlet`). The Alexa export shows it as a SMARTPLUG. */
11061
- DeviceRole["Outlet"] = "outlet";
11062
- /** Generic motion / occupancy / moving event source. Distinct from
11063
- * the camera accessory PirSensor role: that one is a camera child;
11064
- * this is a standalone HA / 3rd-party motion sensor. */
11065
- DeviceRole["MotionSensor"] = "motion-sensor";
11066
- DeviceRole["ContactSensor"] = "contact-sensor";
11067
- DeviceRole["LeakSensor"] = "leak-sensor";
11068
- DeviceRole["SmokeSensor"] = "smoke-sensor";
11069
- DeviceRole["COSensor"] = "co-sensor";
11070
- DeviceRole["GasSensor"] = "gas-sensor";
11071
- DeviceRole["TamperSensor"] = "tamper-sensor";
11072
- DeviceRole["VibrationSensor"] = "vibration-sensor";
11073
- DeviceRole["ConnectivitySensor"] = "connectivity-sensor";
11074
- DeviceRole["SoundSensor"] = "sound-sensor";
11075
- /** Fallback for `binary_sensor` without a known `device_class`. */
11076
- DeviceRole["BinarySensor"] = "binary-sensor";
11077
- DeviceRole["TemperatureSensor"] = "temperature-sensor";
11078
- DeviceRole["HumiditySensor"] = "humidity-sensor";
11079
- DeviceRole["AmbientLightSensor"] = "ambient-light-sensor";
11080
- DeviceRole["PressureSensor"] = "pressure-sensor";
11081
- /** Wind speed or direction (weather-station `wind-sensor` cap). */
11082
- DeviceRole["WindSensor"] = "wind-sensor";
11083
- /** Rain accumulation or rate (weather-station `rain-sensor` cap). */
11084
- DeviceRole["RainSensor"] = "rain-sensor";
11085
- /** UV index (weather-station `uv-sensor` cap). */
11086
- DeviceRole["UvSensor"] = "uv-sensor";
11087
- /** Solar irradiance W/m² (weather-station `solar-radiation-sensor` cap).
11088
- * Distinct from AmbientLightSensor (lux). */
11089
- DeviceRole["SolarRadiationSensor"] = "solar-radiation-sensor";
11090
- /** Soil moisture % (garden/weather `soil-moisture-sensor` cap). */
11091
- DeviceRole["SoilMoistureSensor"] = "soil-moisture-sensor";
11092
- DeviceRole["PowerSensor"] = "power-sensor";
11093
- DeviceRole["EnergySensor"] = "energy-sensor";
11094
- DeviceRole["VoltageSensor"] = "voltage-sensor";
11095
- DeviceRole["CurrentSensor"] = "current-sensor";
11096
- DeviceRole["AirQualitySensor"] = "air-quality-sensor";
11097
- /** Battery level (numeric % via `sensor` OR low-bool via
11098
- * `binary_sensor` — the cap distinguishes via the value type). */
11099
- DeviceRole["BatterySensor"] = "battery-sensor";
11100
- /** Fallback for `sensor` numeric without a known `device_class`. */
11101
- DeviceRole["NumericSensor"] = "numeric-sensor";
11102
- /** String / enum state (HA `sensor` with `state_class: enum` or
11103
- * `attributes.options`). */
11104
- DeviceRole["EnumSensor"] = "enum-sensor";
11105
- /** Date / timestamp state (HA `sensor` with `device_class: timestamp`
11106
- * or `date`). The slice carries the raw ISO string verbatim (hosted on
11107
- * the `enum-sensor` cap); the UI renders it locale-formatted. */
11108
- DeviceRole["DateTimeSensor"] = "datetime-sensor";
11109
- /** Last-resort fallback when nothing else matches. */
11110
- DeviceRole["GenericSensor"] = "generic-sensor";
11111
- DeviceRole["NumericControl"] = "numeric-control";
11112
- DeviceRole["SelectControl"] = "select-control";
11113
- DeviceRole["TextControl"] = "text-control";
11114
- DeviceRole["DateTimeControl"] = "datetime-control";
11115
- /** Mobile push notifier (HA `notify.mobile_app_*`) — supports
11116
- * rich features (image, priority, channel routing). */
11117
- DeviceRole["MobilePushNotifier"] = "mobile-push-notifier";
11118
- /** Chat / messaging service (HA `notify.telegram_*`,
11119
- * `notify.discord_*`, etc.). */
11120
- DeviceRole["MessagingNotifier"] = "messaging-notifier";
11121
- /** Email-based delivery (HA `notify.smtp`, etc.). */
11122
- DeviceRole["EmailNotifier"] = "email-notifier";
11123
- /** Fallback when the notifier service name doesn't match a known
11124
- * pattern. */
11125
- DeviceRole["GenericNotifier"] = "generic-notifier";
11126
- /** Emits a pulse train (infrared or RF) on request — the `ir-transmitter`
11127
- * cap. A `DeviceType.Generic` device. */
11128
- DeviceRole["IrTransmitter"] = "ir-transmitter";
11129
- return DeviceRole;
11130
- }({});
11131
- /**
11132
- import { errMsg } from '@camstack/types'
11133
- * Extract a human-readable message from an unknown error value.
11134
- * Replaces the ubiquitous `errMsg(err)` pattern.
11135
- */
11136
- function errMsg(err) {
11137
- if (err instanceof Error) return err.message;
11138
- if (typeof err === "string") return err;
11139
- return String(err);
11140
- }
11141
- //#endregion
11142
- //#region ../types/dist/composition-C7WdThmP.mjs
11273
+ //#region ../types/dist/composition-BBzVf2xd.mjs
11143
11274
  /**
11144
11275
  * The ffmpeg build every CamStack node downloads, per platform and arch.
11145
11276
  *
@@ -12358,6 +12489,71 @@ var EventOwnerTypeSchema = _enum(EVENT_OWNER_TYPES);
12358
12489
  var MediaPresenceOwnerKindSchema = _enum([...EVENT_OWNER_TYPES, "track"]);
12359
12490
  new Set(EVENT_OWNER_TYPES);
12360
12491
  /**
12492
+ * What a published package needs from the rest of the install (D989).
12493
+ *
12494
+ * One published framework package (`@camstack/server`) carries the kernel, the
12495
+ * builtins and the capability contracts, so "which addon runs on which
12496
+ * framework" is a question of version, and the answer is DECLARED by the
12497
+ * package that knows it:
12498
+ *
12499
+ * - an addon says the oldest server it runs on (`requires.server: ">=X"`);
12500
+ * - the server says the oldest addon it works with
12501
+ * (`requires.addons: { "@camstack/addon-x": ">=Y" }`) and the oldest shell
12502
+ * around it (`requires.wrapper: { desktop, image }`);
12503
+ * - a wrapper (the desktop app, the container starter) says which server
12504
+ * range it can host, and reports its own kind and version.
12505
+ *
12506
+ * The publish workflow STAMPS these after the version bump
12507
+ * (`scripts/ci/stamp-requires.mjs`); nobody types a number by hand.
12508
+ *
12509
+ * A requirement that cannot be checked is NOT met: a shell that reports no
12510
+ * version counts as below the minimum, and an unreadable running version is
12511
+ * named in the result (a failed read is never a pass).
12512
+ */
12513
+ /** `>=MAJOR.MINOR.PATCH[-prerelease]` — the only range shape a requirement uses. */
12514
+ var MinVersionSchema = string().regex(/^>=\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/, "a minimum is written \">=X.Y.Z\"");
12515
+ object({
12516
+ /** The oldest `@camstack/server` this package runs on. */
12517
+ server: MinVersionSchema.optional(),
12518
+ /** The oldest installed version of each named addon package. */
12519
+ addons: record(string(), MinVersionSchema).optional(),
12520
+ /** The oldest shell around the server, per shell kind. */
12521
+ wrapper: object({
12522
+ desktop: MinVersionSchema.optional(),
12523
+ image: MinVersionSchema.optional()
12524
+ }).optional()
12525
+ });
12526
+ /** One unmet requirement; each names the versions on both sides. */
12527
+ var UnmetRequirementSchema = discriminatedUnion("kind", [
12528
+ object({
12529
+ kind: literal("server-too-old"),
12530
+ required: string(),
12531
+ /** `null` — the running server version could not be read. */
12532
+ running: string().nullable()
12533
+ }),
12534
+ object({
12535
+ kind: literal("addon-too-old"),
12536
+ packageName: string(),
12537
+ required: string(),
12538
+ /** `null` — the addon is not installed (or its version is unreadable). */
12539
+ running: string().nullable()
12540
+ }),
12541
+ object({
12542
+ kind: literal("wrapper-too-old"),
12543
+ wrapper: _enum(["desktop", "image"]),
12544
+ required: string(),
12545
+ /** `null` — the wrapper reported no version. */
12546
+ running: string().nullable()
12547
+ })
12548
+ ]);
12549
+ var RequiredAddonUpdateSchema = object({
12550
+ packageName: string(),
12551
+ /** The minimum the target server declares. */
12552
+ required: string(),
12553
+ /** What this node has; `null` when the addon is not installed. */
12554
+ installed: string().nullable()
12555
+ });
12556
+ /**
12361
12557
  * Which integration instance a settings form is for. An addon that serves several instances
12362
12558
  * (`instanceMode: multiple`) can show a different form per instance — an NVR integration
12363
12559
  * needs its connection, the default integration needs the addon's own sections.
@@ -12832,6 +13028,613 @@ var CameraSwitchGroupSchema = object({
12832
13028
  fetchedAt: number()
12833
13029
  });
12834
13030
  /**
13031
+ * Query filter for settings-store collections.
13032
+ */
13033
+ var QueryFilterSchema = object({
13034
+ where: record(string(), unknown()).optional(),
13035
+ whereIn: record(string(), array(unknown())).optional(),
13036
+ whereBetween: record(string(), tuple([unknown(), unknown()])).optional(),
13037
+ /** NULL-safe exclusion: matches rows whose field is NULL OR != the value. */
13038
+ whereNot: record(string(), unknown()).optional(),
13039
+ /** NULL-safe exclusion of a SET — `whereNot` for more than one value. An
13040
+ * empty list excludes nothing. See `QueryFilter.whereNotIn`. */
13041
+ whereNotIn: record(string(), array(unknown())).optional(),
13042
+ orderBy: object({
13043
+ field: string(),
13044
+ direction: _enum(["asc", "desc"])
13045
+ }).optional(),
13046
+ limit: number().optional(),
13047
+ offset: number().optional()
13048
+ });
13049
+ /**
13050
+ * The predicate half of a filter, for BULK MUTATIONS.
13051
+ *
13052
+ * Deliberately not `QueryFilterSchema`: `orderBy` / `limit` / `offset` have no
13053
+ * meaning for a statement that rewrites a set, and accepting them would invite
13054
+ * a caller to believe `limit` bounds the damage. Every field is optional here
13055
+ * only so the shape stays composable — the implementation REJECTS a filter
13056
+ * that compiles to no predicate, because that is the whole collection.
13057
+ */
13058
+ var MutationFilterSchema = object({
13059
+ where: record(string(), unknown()).optional(),
13060
+ whereIn: record(string(), array(unknown())).optional(),
13061
+ whereBetween: record(string(), tuple([unknown(), unknown()])).optional(),
13062
+ whereNot: record(string(), unknown()).optional(),
13063
+ whereNotIn: record(string(), array(unknown())).optional()
13064
+ });
13065
+ /**
13066
+ * One scalar an {@link settingsStoreCapability.methods.aggregate} call asks for.
13067
+ *
13068
+ * `as` names the slot in the result, so the SAME column may be asked twice with
13069
+ * two operations (`MIN(startMs)` and `MAX(startMs)` in one round trip) — which
13070
+ * a `Record<column, op>` shape could not express.
13071
+ */
13072
+ var AggregateFieldSchema = object({
13073
+ /** Result key. */
13074
+ as: string().min(1),
13075
+ /** Column to aggregate. Must be a real column of a declared collection. */
13076
+ field: string().min(1),
13077
+ op: _enum([
13078
+ "sum",
13079
+ "min",
13080
+ "max"
13081
+ ])
13082
+ });
13083
+ /**
13084
+ * `COUNT(*)` plus one number per requested field.
13085
+ *
13086
+ * `null` means NO ROW MATCHED, never zero: a `SUM` over an empty set and a sum
13087
+ * that really is 0 are different facts, and an accounting caller that renders
13088
+ * "0 bytes, oldest = 0" for "nothing here" reports a lie about a disk.
13089
+ */
13090
+ var AggregateResultSchema = object({
13091
+ count: number().int(),
13092
+ values: record(string(), number().nullable())
13093
+ });
13094
+ /** A single stored record: `{ id, data }`. */
13095
+ var SettingsRecordSchema = object({
13096
+ id: string(),
13097
+ data: record(string(), unknown())
13098
+ });
13099
+ /**
13100
+ * One record of a BULK insert — {@link SettingsRecordSchema} with the id made
13101
+ * optional.
13102
+ *
13103
+ * A separate schema rather than loosening the shared one: every other method
13104
+ * on this cap addresses a row BY its id, and making that field optional
13105
+ * everywhere would turn a forgotten key into a silently generated one on
13106
+ * `update` and `delete` as well.
13107
+ */
13108
+ var BulkRecordSchema = object({
13109
+ id: string().optional(),
13110
+ data: record(string(), unknown())
13111
+ });
13112
+ /**
13113
+ * The database FILES one `sqlite-settings` engine process owns (D851).
13114
+ * Closed on purpose: a file is a thing the engine opens, backs up and gives a
13115
+ * serving thread, so a new one is a code change, never a string a caller
13116
+ * invents. `settings` is `camstack.db` and the default for every collection
13117
+ * that names no file.
13118
+ */
13119
+ var DataStoreDatabaseSchema = _enum([
13120
+ "settings",
13121
+ "events",
13122
+ "history"
13123
+ ]);
13124
+ DataStoreDatabaseSchema.options;
13125
+ /**
13126
+ * One write inside a {@link settingsStoreCapability.methods.transact} call.
13127
+ * The three that a "replace this set, bump that revision" write needs, plus
13128
+ * the batched upsert a rollup writer needs.
13129
+ */
13130
+ var TransactOpSchema = discriminatedUnion("op", [
13131
+ object({
13132
+ op: literal("deleteWhere"),
13133
+ collection: string(),
13134
+ filter: MutationFilterSchema
13135
+ }),
13136
+ object({
13137
+ op: literal("insertMany"),
13138
+ collection: string(),
13139
+ records: array(BulkRecordSchema).readonly()
13140
+ }),
13141
+ object({
13142
+ op: literal("update"),
13143
+ collection: string(),
13144
+ id: string(),
13145
+ data: record(string(), unknown())
13146
+ }),
13147
+ object({
13148
+ op: literal("upsertMany"),
13149
+ collection: string(),
13150
+ conflict: array(string()).readonly(),
13151
+ records: array(BulkRecordSchema).readonly()
13152
+ })
13153
+ ]);
13154
+ /**
13155
+ * The precondition of a transaction: the row `key` of `collection` must hold
13156
+ * `equals` in `column`, read INSIDE the transaction.
13157
+ */
13158
+ var TransactGuardSchema = object({
13159
+ collection: string(),
13160
+ key: string(),
13161
+ column: string(),
13162
+ equals: unknown()
13163
+ });
13164
+ /**
13165
+ * Column declaration for a structured (SQL-backed) collection.
13166
+ *
13167
+ * Logical types — the backend translates each to the matching SQLite
13168
+ * storage class and handles per-type marshaling:
13169
+ * - `TEXT` / `INTEGER` / `REAL` — native SQLite types, pass-through
13170
+ * - `JSON` — TEXT under the hood; serialised on write, parsed on read
13171
+ * - `BOOLEAN` — INTEGER 0/1 under the hood; coerced both directions
13172
+ */
13173
+ var CollectionColumnSchema = object({
13174
+ name: string(),
13175
+ type: _enum([
13176
+ "TEXT",
13177
+ "INTEGER",
13178
+ "REAL",
13179
+ "JSON",
13180
+ "BOOLEAN"
13181
+ ]),
13182
+ primaryKey: boolean().optional(),
13183
+ notNull: boolean().optional(),
13184
+ unique: boolean().optional(),
13185
+ /**
13186
+ * Column DEFAULT. Required for B2: the four `ensureTable` consumers declare
13187
+ * columns like `enabled INTEGER NOT NULL DEFAULT 1`, and without this the
13188
+ * collection surface simply cannot express their existing tables — which is
13189
+ * why they were still on the uncapped `table*` API.
13190
+ */
13191
+ defaultValue: union([
13192
+ string(),
13193
+ number(),
13194
+ boolean()
13195
+ ]).optional()
13196
+ });
13197
+ var CollectionIndexSchema = object({
13198
+ name: string(),
13199
+ columns: array(string()).readonly(),
13200
+ unique: boolean().optional()
13201
+ });
13202
+ /**
13203
+ * The input of `declareCollection` — ONE schema for the door and for the
13204
+ * engine behind it, so the two surfaces cannot drift (the dispatcher forwards
13205
+ * the payload untouched).
13206
+ *
13207
+ * - `database` — the file the collection lives in (D851). Absent means
13208
+ * `settings`. A declaration naming a file other than the one the table is
13209
+ * already in is REFUSED by the engine, never moved.
13210
+ * - `primaryKey` — a COMPOSITE key, as column names. A single name is the same
13211
+ * as `primaryKey: true` on that column. A composite-keyed collection has no
13212
+ * `id`: it is written with `insertMany` / `upsertMany` / `transact` and read
13213
+ * with `query` / `count` / `aggregate` / `deleteWhere`; the calls that
13214
+ * address one row by id are refused.
13215
+ * - `withoutRowid` — emit `WITHOUT ROWID`. Requires a primary key.
13216
+ */
13217
+ var DeclareCollectionInputSchema = object({
13218
+ namespace: string().optional(),
13219
+ collection: string(),
13220
+ columns: array(CollectionColumnSchema).readonly(),
13221
+ indexes: array(CollectionIndexSchema).readonly().optional(),
13222
+ database: DataStoreDatabaseSchema.optional(),
13223
+ primaryKey: array(string()).readonly().optional(),
13224
+ withoutRowid: boolean().optional()
13225
+ });
13226
+ method(object({
13227
+ namespace: string().optional(),
13228
+ collection: string(),
13229
+ key: string()
13230
+ }), unknown(), { auth: "admin" }), method(object({
13231
+ namespace: string().optional(),
13232
+ collection: string(),
13233
+ key: string(),
13234
+ value: unknown()
13235
+ }), _void(), {
13236
+ kind: "mutation",
13237
+ auth: "admin"
13238
+ }), method(object({
13239
+ namespace: string().optional(),
13240
+ collection: string(),
13241
+ filter: QueryFilterSchema.optional(),
13242
+ columns: array(string()).readonly().optional()
13243
+ }), array(SettingsRecordSchema).readonly(), { auth: "admin" }), method(object({
13244
+ namespace: string().optional(),
13245
+ collection: string(),
13246
+ /** Declared columns identifying the slot; must be covered by a UNIQUE
13247
+ * index on the collection. Never empty. */
13248
+ conflict: array(string()).readonly(),
13249
+ record: BulkRecordSchema
13250
+ }), object({
13251
+ /** The id the row HAS: assigned by SQLite on an insert, or the existing
13252
+ * row's own id on an update. */
13253
+ id: union([string(), number()]) }), {
13254
+ kind: "mutation",
13255
+ auth: "admin"
13256
+ }), method(object({
13257
+ namespace: string().optional(),
13258
+ collection: string(),
13259
+ conflict: array(string()).readonly(),
13260
+ records: array(BulkRecordSchema).readonly()
13261
+ }), object({ upserted: number().int() }), {
13262
+ kind: "mutation",
13263
+ auth: "admin"
13264
+ }), method(object({
13265
+ namespace: string().optional(),
13266
+ collection: string(),
13267
+ record: object({
13268
+ id: string().optional(),
13269
+ data: record(string(), unknown())
13270
+ })
13271
+ }), object({
13272
+ /**
13273
+ * The id the row ACTUALLY got (D473): the one supplied, the UUID minted
13274
+ * for an absent one, or the ROWID SQLite assigned on an `INTEGER`
13275
+ * primary key — which is the only place an auto key is knowable.
13276
+ */
13277
+ id: union([string(), number()]) }), {
13278
+ kind: "mutation",
13279
+ auth: "admin"
13280
+ }), method(object({
13281
+ namespace: string().optional(),
13282
+ collection: string(),
13283
+ records: array(BulkRecordSchema).readonly()
13284
+ }), object({ inserted: number().int() }), {
13285
+ kind: "mutation",
13286
+ auth: "admin"
13287
+ }), method(object({
13288
+ namespace: string().optional(),
13289
+ guard: TransactGuardSchema.optional(),
13290
+ ops: array(TransactOpSchema).readonly()
13291
+ }), object({
13292
+ applied: boolean(),
13293
+ current: unknown().optional()
13294
+ }), {
13295
+ kind: "mutation",
13296
+ auth: "admin"
13297
+ }), method(object({
13298
+ namespace: string().optional(),
13299
+ collection: string(),
13300
+ id: string(),
13301
+ data: record(string(), unknown())
13302
+ }), _void(), {
13303
+ kind: "mutation",
13304
+ auth: "admin"
13305
+ }), method(object({
13306
+ namespace: string().optional(),
13307
+ collection: string(),
13308
+ key: string()
13309
+ }), _void(), {
13310
+ kind: "mutation",
13311
+ auth: "admin"
13312
+ }), method(object({
13313
+ namespace: string().optional(),
13314
+ collection: string(),
13315
+ filter: MutationFilterSchema
13316
+ }), object({ deleted: number().int() }), {
13317
+ kind: "mutation",
13318
+ auth: "admin"
13319
+ }), method(object({
13320
+ namespace: string().optional(),
13321
+ collection: string(),
13322
+ filter: MutationFilterSchema,
13323
+ data: record(string(), unknown())
13324
+ }), object({ updated: number().int() }), {
13325
+ kind: "mutation",
13326
+ auth: "admin"
13327
+ }), method(object({
13328
+ namespace: string().optional(),
13329
+ collection: string(),
13330
+ filter: QueryFilterSchema.optional()
13331
+ }), number(), { auth: "admin" }), method(object({
13332
+ namespace: string().optional(),
13333
+ collection: string(),
13334
+ fields: array(AggregateFieldSchema).readonly(),
13335
+ filter: QueryFilterSchema.optional()
13336
+ }), AggregateResultSchema, { auth: "admin" }), method(object({
13337
+ namespace: string().optional(),
13338
+ collection: string(),
13339
+ field: string(),
13340
+ bucketSize: number().int().positive(),
13341
+ origin: number().int(),
13342
+ filter: QueryFilterSchema.optional()
13343
+ }), array(object({
13344
+ bucket: number().int(),
13345
+ count: number().int()
13346
+ })).readonly(), { auth: "admin" }), method(object({
13347
+ namespace: string().optional(),
13348
+ collection: string()
13349
+ }), boolean(), { auth: "admin" }), method(DeclareCollectionInputSchema, _void(), {
13350
+ kind: "mutation",
13351
+ auth: "admin"
13352
+ });
13353
+ 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>\"");
13354
+ var MigrationLevelSchema = _enum([
13355
+ "hub-store",
13356
+ "addon-state",
13357
+ "node-config",
13358
+ "viewer",
13359
+ "addon-internal"
13360
+ ]);
13361
+ var MigrationStoreSchema = _enum([
13362
+ "settings",
13363
+ "events",
13364
+ "history",
13365
+ "node-local",
13366
+ "addon-files"
13367
+ ]);
13368
+ var MigrationScopeSchema = _enum(["cluster", "per-node"]);
13369
+ /** What depends on a migration while it is pending: `block-owner` is the default. */
13370
+ var MigrationOnFailureSchema = _enum(["block-owner", "tolerate"]);
13371
+ /**
13372
+ * `sql` is a hub-only body over the owner's connection. `plan` returns edits the
13373
+ * engine applies inside the migration's own transaction (hub-owned: computed in
13374
+ * that transaction; addon-owned: computed by the owner and shipped to the engine).
13375
+ * `procedure` is addon-owned work that is not SQL (keychain, integration registry,
13376
+ * device tombstones); the ledger records its outcome and it must be idempotent.
13377
+ */
13378
+ var MigrationBodyKindSchema = _enum([
13379
+ "sql",
13380
+ "plan",
13381
+ "procedure"
13382
+ ]);
13383
+ var MigrationMetaSchema = object({
13384
+ id: MigrationIdSchema,
13385
+ /** `hub`, an addon id, `node` or `viewer`. */
13386
+ owner: string().regex(/^[a-z0-9-]+$/),
13387
+ level: MigrationLevelSchema,
13388
+ /** Strictly increasing per owner: the order within an owner. */
13389
+ seq: number().int().positive(),
13390
+ /** Ids of any owner: the order across owners. */
13391
+ dependsOn: array(MigrationIdSchema),
13392
+ store: MigrationStoreSchema,
13393
+ scope: MigrationScopeSchema,
13394
+ onFailure: MigrationOnFailureSchema,
13395
+ bodyKind: MigrationBodyKindSchema,
13396
+ description: string().min(1),
13397
+ /** The marked field the migration's spec must assert survived. */
13398
+ fixtureMark: string().min(1)
13399
+ }).refine((m) => m.id.startsWith(`${m.owner}/`), {
13400
+ message: "the id must start with \"<owner>/\"",
13401
+ path: ["id"]
13402
+ });
13403
+ /** A cycle in the dependency graph, named by the ids on it. */
13404
+ var MigrationCycleError = class extends Error {
13405
+ ids;
13406
+ constructor(ids) {
13407
+ super(`migration dependsOn cycle: ${ids.join(" -> ")}`);
13408
+ this.ids = ids;
13409
+ this.name = "MigrationCycleError";
13410
+ }
13411
+ };
13412
+ /**
13413
+ * Topological order over `dependsOn` plus the implicit edge "a lower `seq` of an
13414
+ * owner runs first". Ties are broken by `(owner, seq)`. Throws
13415
+ * {@link MigrationCycleError} on a cycle. A `dependsOn` id that is not in the list
13416
+ * is ignored here — the catalogue schema is what refuses it.
13417
+ */
13418
+ function orderMigrations(items) {
13419
+ const byId = new Map(items.map((item) => [item.id, item]));
13420
+ const deps = /* @__PURE__ */ new Map();
13421
+ const ownerSorted = /* @__PURE__ */ new Map();
13422
+ for (const item of items) {
13423
+ const list = ownerSorted.get(item.owner) ?? [];
13424
+ list.push(item);
13425
+ ownerSorted.set(item.owner, list);
13426
+ }
13427
+ for (const item of items) deps.set(item.id, new Set(item.dependsOn.filter((id) => byId.has(id))));
13428
+ for (const list of ownerSorted.values()) {
13429
+ list.sort((a, b) => a.seq - b.seq);
13430
+ for (let i = 1; i < list.length; i++) {
13431
+ const previous = list[i - 1];
13432
+ const current = list[i];
13433
+ if (previous !== void 0 && current !== void 0) deps.get(current.id)?.add(previous.id);
13434
+ }
13435
+ }
13436
+ const tie = (a, b) => a.owner < b.owner ? -1 : a.owner > b.owner ? 1 : a.seq - b.seq;
13437
+ const done = /* @__PURE__ */ new Set();
13438
+ const ordered = [];
13439
+ let remaining = [...items];
13440
+ while (remaining.length > 0) {
13441
+ const ready = remaining.filter((item) => [...deps.get(item.id) ?? []].every((dep) => done.has(dep)));
13442
+ if (ready.length === 0) throw new MigrationCycleError(remaining.map((item) => item.id).sort());
13443
+ ready.sort(tie);
13444
+ const next = ready[0];
13445
+ if (next === void 0) break;
13446
+ ordered.push(next);
13447
+ done.add(next.id);
13448
+ remaining = remaining.filter((item) => item.id !== next.id);
13449
+ }
13450
+ return ordered;
13451
+ }
13452
+ /** Every way a list of migrations (in declaration order) is not a catalogue. */
13453
+ function catalogueIssues(items) {
13454
+ const issues = [];
13455
+ const seen = /* @__PURE__ */ new Set();
13456
+ const known = new Set(items.map((item) => item.id));
13457
+ const lastSeq = /* @__PURE__ */ new Map();
13458
+ const seqUsed = /* @__PURE__ */ new Map();
13459
+ for (const item of items) {
13460
+ if (seen.has(item.id)) issues.push({
13461
+ code: "duplicate-id",
13462
+ ids: [item.id],
13463
+ message: `duplicate id ${item.id}`
13464
+ });
13465
+ seen.add(item.id);
13466
+ const used = seqUsed.get(item.owner) ?? /* @__PURE__ */ new Set();
13467
+ if (used.has(item.seq)) issues.push({
13468
+ code: "duplicate-seq",
13469
+ ids: [item.id],
13470
+ message: `owner ${item.owner} has two migrations with seq ${item.seq}`
13471
+ });
13472
+ else if (item.seq < (lastSeq.get(item.owner) ?? 0)) issues.push({
13473
+ code: "seq-order",
13474
+ ids: [item.id],
13475
+ message: `seq ${item.seq} of ${item.id} does not increase in declaration order (owner ${item.owner})`
13476
+ });
13477
+ used.add(item.seq);
13478
+ seqUsed.set(item.owner, used);
13479
+ lastSeq.set(item.owner, Math.max(item.seq, lastSeq.get(item.owner) ?? 0));
13480
+ for (const dep of item.dependsOn) if (!known.has(dep)) issues.push({
13481
+ code: "unknown-dependency",
13482
+ ids: [item.id, dep],
13483
+ message: `${item.id} dependsOn unknown id ${dep}`
13484
+ });
13485
+ }
13486
+ try {
13487
+ orderMigrations(items);
13488
+ } catch (err) {
13489
+ if (err instanceof MigrationCycleError) issues.push({
13490
+ code: "cycle",
13491
+ ids: err.ids,
13492
+ message: err.message
13493
+ });
13494
+ else throw err;
13495
+ }
13496
+ return issues;
13497
+ }
13498
+ array(MigrationMetaSchema).superRefine((items, ctx) => {
13499
+ for (const issue of catalogueIssues(items)) ctx.addIssue({
13500
+ code: "custom",
13501
+ message: `${issue.code}: ${issue.message}`
13502
+ });
13503
+ });
13504
+ /**
13505
+ * The canonical JSON (`preconditionOf`) of the value a plan read; the apply step
13506
+ * re-reads, and a plan whose read no longer matches is refused (the migration is
13507
+ * `failed`, nothing it edited is kept).
13508
+ */
13509
+ var PreconditionSchema = string().min(1).optional();
13510
+ var MigrationPlanSchema = object({ edits: array(discriminatedUnion("op", [
13511
+ object({
13512
+ op: literal("setKeys"),
13513
+ scope: string().min(1),
13514
+ values: record(string(), unknown()),
13515
+ precondition: PreconditionSchema
13516
+ }),
13517
+ object({
13518
+ op: literal("deleteKeys"),
13519
+ scope: string().min(1),
13520
+ keys: array(string().min(1)).min(1),
13521
+ precondition: PreconditionSchema
13522
+ }),
13523
+ object({
13524
+ op: literal("putRow"),
13525
+ table: string().min(1),
13526
+ key: record(string(), unknown()),
13527
+ row: record(string(), unknown()),
13528
+ precondition: PreconditionSchema
13529
+ }),
13530
+ object({
13531
+ op: literal("deleteRow"),
13532
+ table: string().min(1),
13533
+ key: record(string(), unknown()),
13534
+ precondition: PreconditionSchema
13535
+ }),
13536
+ object({
13537
+ op: literal("upsertRecord"),
13538
+ kind: string().min(1),
13539
+ recordKey: string().min(1),
13540
+ value: unknown(),
13541
+ precondition: PreconditionSchema
13542
+ }),
13543
+ object({
13544
+ op: literal("deleteRecord"),
13545
+ kind: string().min(1),
13546
+ recordKey: string().min(1),
13547
+ precondition: PreconditionSchema
13548
+ })
13549
+ ])) });
13550
+ /**
13551
+ * What a `procedure` body reports. `already-done` means the rerun observed the
13552
+ * work finished (a crash between the work and the ledger row, or a manual fix)
13553
+ * and did none of it again; the ledger records `applied` either way.
13554
+ */
13555
+ var MigrationProcedureResultSchema = object({
13556
+ outcome: _enum(["done", "already-done"]),
13557
+ summary: record(string(), unknown())
13558
+ });
13559
+ /** What an owner hands the engine when one of its migrations ran. */
13560
+ var MigrationOwnerResultSchema = discriminatedUnion("kind", [
13561
+ object({
13562
+ kind: literal("plan"),
13563
+ plan: MigrationPlanSchema
13564
+ }),
13565
+ object({
13566
+ kind: literal("procedure"),
13567
+ result: MigrationProcedureResultSchema
13568
+ }),
13569
+ object({
13570
+ kind: literal("failed"),
13571
+ error: string().min(1)
13572
+ })
13573
+ ]);
13574
+ /**
13575
+ * `held`: a restore of a pre-migration snapshot undid this id; it does not rerun
13576
+ * until the operator retries or a new code version ships.
13577
+ * `skipped-newer`: the database knows an id this code does not.
13578
+ */
13579
+ var MigrationStatusSchema = _enum([
13580
+ "applied",
13581
+ "failed",
13582
+ "blocked",
13583
+ "running",
13584
+ "held",
13585
+ "skipped-newer"
13586
+ ]);
13587
+ var MigrationLedgerEntrySchema = object({
13588
+ id: string(),
13589
+ database: DataStoreDatabaseSchema,
13590
+ owner: string(),
13591
+ level: MigrationLevelSchema,
13592
+ seq: number().int().nonnegative(),
13593
+ status: MigrationStatusSchema,
13594
+ attempts: number().int().nonnegative(),
13595
+ appliedAt: number().nullable(),
13596
+ codeVersion: string().nullable(),
13597
+ /** What the apply changed: rows per table, as the engine counted them. */
13598
+ planSummary: record(string(), unknown()).nullable(),
13599
+ progress: record(string(), unknown()).nullable(),
13600
+ error: string().nullable(),
13601
+ /** Why it is `blocked` or `held` (`dependency-failed`, `snapshot-refused`, …). */
13602
+ reason: string().nullable(),
13603
+ nodeId: string().nullable(),
13604
+ bootId: string().nullable()
13605
+ });
13606
+ /** A catalogued migration that is not applied, with where it stands. */
13607
+ var MigrationPendingEntrySchema = object({
13608
+ meta: MigrationMetaSchema,
13609
+ database: DataStoreDatabaseSchema,
13610
+ state: _enum([
13611
+ "pending",
13612
+ "failed",
13613
+ "blocked",
13614
+ "held"
13615
+ ]),
13616
+ reason: string().nullable()
13617
+ });
13618
+ /** Rows per table before and after a dry run, and what it would have changed. */
13619
+ var MigrationDryRunResultSchema = object({
13620
+ id: string(),
13621
+ ok: boolean(),
13622
+ error: string().nullable(),
13623
+ rowsChanged: number().int().nonnegative(),
13624
+ tables: array(object({
13625
+ table: string(),
13626
+ before: number().int().nonnegative(),
13627
+ after: number().int().nonnegative()
13628
+ }))
13629
+ });
13630
+ /** One of an owner's migrations, as the engine tells the owner to treat it this boot. */
13631
+ var MigrationOwnerStepSchema = object({
13632
+ id: MigrationIdSchema,
13633
+ action: _enum(["run", "skip"]),
13634
+ /** The ledger state the engine found: `pending` has no row yet. */
13635
+ state: union([literal("pending"), MigrationStatusSchema])
13636
+ });
13637
+ /**
12835
13638
  * The trainer contract — what the hub hands the cloud trainer and what it
12836
13639
  * gets back (cloud training, D805+).
12837
13640
  *
@@ -16234,6 +17037,41 @@ var ACTUATOR_CAP_VOCABULARY = {
16234
17037
  icon: "droplets"
16235
17038
  }]
16236
17039
  },
17040
+ "power-source": {
17041
+ scope: "device",
17042
+ noun: t("Power source", "Alimentazione"),
17043
+ actions: [
17044
+ {
17045
+ id: "powerSource.on",
17046
+ method: "powerOn",
17047
+ label: t("Power on", "Dai corrente"),
17048
+ phrase: t("Power on {device}", "Dai corrente a {device}"),
17049
+ icon: "power"
17050
+ },
17051
+ {
17052
+ id: "powerSource.off",
17053
+ method: "powerOff",
17054
+ label: t("Power off", "Togli corrente"),
17055
+ phrase: t("Cut the power of {device}", "Togli la corrente a {device}"),
17056
+ icon: "power-off"
17057
+ },
17058
+ {
17059
+ id: "powerSource.forceRestart",
17060
+ method: "forceRestart",
17061
+ label: t("Force restart", "Riavvio forzato"),
17062
+ phrase: t("Force restart {device} by cutting its power", "Riavvia {device} togliendo la corrente"),
17063
+ icon: "refresh-cw"
17064
+ }
17065
+ ],
17066
+ state: [{
17067
+ kind: "boolean",
17068
+ field: "switchOn",
17069
+ label: t("Power switch on", "Presa accesa"),
17070
+ whenTrue: t("When the power of {device} comes on", "Quando arriva la corrente a {device}"),
17071
+ whenFalse: t("When the power of {device} is cut", "Quando manca la corrente a {device}"),
17072
+ icon: "power"
17073
+ }]
17074
+ },
16237
17075
  "lock-control": {
16238
17076
  scope: "device",
16239
17077
  noun: t("Lock", "Serratura"),
@@ -16709,7 +17547,7 @@ var ACTUATOR_CAP_VOCABULARY = {
16709
17547
  method: "setDirection",
16710
17548
  label: t("Spin forward", "Rotazione avanti"),
16711
17549
  phrase: t("Spin {device} forward", "Fai girare {device} in avanti"),
16712
- icon: "rotate-cw",
17550
+ icon: "refresh-cw",
16713
17551
  effect: {
16714
17552
  field: "direction",
16715
17553
  value: "forward"
@@ -19787,6 +20625,7 @@ var SYSTEM_CAP_VOCABULARY = {
19787
20625
  "login-method": system(IDENTITY),
19788
20626
  "mesh-network": system(NETWORK),
19789
20627
  "metrics-provider": system(CLUSTER),
20628
+ migrations: system(INTERNAL),
19790
20629
  "model-convert": system(ML),
19791
20630
  "model-distributor": system(ML),
19792
20631
  "mqtt-broker": system(NETWORK),
@@ -39989,7 +40828,8 @@ function normalizeUnit(raw) {
39989
40828
  if (lower === "dbm") return "dBm";
39990
40829
  if (lower === "dbfs") return "dBFS";
39991
40830
  if (lower === "inhg") return "inHg";
39992
- if (lower === "mm/h") return "mm/h";
40831
+ if (lower === "mm/h" || lower === "mm/hr") return "mm/h";
40832
+ if (lower === "in/h" || lower === "in/hr") return "in/h";
39993
40833
  if (lower === "w/m²" || lower === "w/m2") return "W/m²";
39994
40834
  if (lower === "°" || lower === "deg" || lower === "degree" || lower === "degrees") return "°";
39995
40835
  if (lower === "kbps") return "kbps";
@@ -42208,7 +43048,7 @@ method(object({}), array(TargetKindSchema)), method(object({}), array(TargetSche
42208
43048
  * rebuilds without manual reload.
42209
43049
  *
42210
43050
  * The hub-local builtin `addon-pages-aggregator` (see
42211
- * `@camstack/system/builtins/addon-pages-aggregator`) registers the
43051
+ * `@camstack/server/system/builtins/addon-pages-aggregator`) registers the
42212
43052
  * provider. Splitting the public aggregator from the raw collection
42213
43053
  * keeps both ends in codegen — there's no hand-written
42214
43054
  * `addon-pages.router.ts` wrapper anymore.
@@ -42508,7 +43348,7 @@ method(_void(), array(WidgetMetadataSchema).readonly(), { auth: "admin" });
42508
43348
  * manual reload — same scheme used by `addon-pages`.
42509
43349
  *
42510
43350
  * The hub-local builtin `addon-widgets-aggregator` (see
42511
- * `@camstack/system/builtins/addon-widgets-aggregator`) registers the
43351
+ * `@camstack/server/system/builtins/addon-widgets-aggregator`) registers the
42512
43352
  * provider. Splitting the public aggregator from the raw collection
42513
43353
  * keeps both ends in codegen — there's no hand-written wrapper.
42514
43354
  */
@@ -42869,329 +43709,6 @@ method(object({
42869
43709
  password: string()
42870
43710
  }), AuthResultSchema.nullable(), { kind: "mutation" }), method(object({ state: string() }), string()), method(record(string(), string()), AuthResultSchema, { kind: "mutation" }), method(object({ token: string() }), AuthResultSchema.nullable());
42871
43711
  /**
42872
- * Query filter for settings-store collections.
42873
- */
42874
- var QueryFilterSchema = object({
42875
- where: record(string(), unknown()).optional(),
42876
- whereIn: record(string(), array(unknown())).optional(),
42877
- whereBetween: record(string(), tuple([unknown(), unknown()])).optional(),
42878
- /** NULL-safe exclusion: matches rows whose field is NULL OR != the value. */
42879
- whereNot: record(string(), unknown()).optional(),
42880
- /** NULL-safe exclusion of a SET — `whereNot` for more than one value. An
42881
- * empty list excludes nothing. See `QueryFilter.whereNotIn`. */
42882
- whereNotIn: record(string(), array(unknown())).optional(),
42883
- orderBy: object({
42884
- field: string(),
42885
- direction: _enum(["asc", "desc"])
42886
- }).optional(),
42887
- limit: number().optional(),
42888
- offset: number().optional()
42889
- });
42890
- /**
42891
- * The predicate half of a filter, for BULK MUTATIONS.
42892
- *
42893
- * Deliberately not `QueryFilterSchema`: `orderBy` / `limit` / `offset` have no
42894
- * meaning for a statement that rewrites a set, and accepting them would invite
42895
- * a caller to believe `limit` bounds the damage. Every field is optional here
42896
- * only so the shape stays composable — the implementation REJECTS a filter
42897
- * that compiles to no predicate, because that is the whole collection.
42898
- */
42899
- var MutationFilterSchema = object({
42900
- where: record(string(), unknown()).optional(),
42901
- whereIn: record(string(), array(unknown())).optional(),
42902
- whereBetween: record(string(), tuple([unknown(), unknown()])).optional(),
42903
- whereNot: record(string(), unknown()).optional(),
42904
- whereNotIn: record(string(), array(unknown())).optional()
42905
- });
42906
- /**
42907
- * One scalar an {@link settingsStoreCapability.methods.aggregate} call asks for.
42908
- *
42909
- * `as` names the slot in the result, so the SAME column may be asked twice with
42910
- * two operations (`MIN(startMs)` and `MAX(startMs)` in one round trip) — which
42911
- * a `Record<column, op>` shape could not express.
42912
- */
42913
- var AggregateFieldSchema = object({
42914
- /** Result key. */
42915
- as: string().min(1),
42916
- /** Column to aggregate. Must be a real column of a declared collection. */
42917
- field: string().min(1),
42918
- op: _enum([
42919
- "sum",
42920
- "min",
42921
- "max"
42922
- ])
42923
- });
42924
- /**
42925
- * `COUNT(*)` plus one number per requested field.
42926
- *
42927
- * `null` means NO ROW MATCHED, never zero: a `SUM` over an empty set and a sum
42928
- * that really is 0 are different facts, and an accounting caller that renders
42929
- * "0 bytes, oldest = 0" for "nothing here" reports a lie about a disk.
42930
- */
42931
- var AggregateResultSchema = object({
42932
- count: number().int(),
42933
- values: record(string(), number().nullable())
42934
- });
42935
- /** A single stored record: `{ id, data }`. */
42936
- var SettingsRecordSchema = object({
42937
- id: string(),
42938
- data: record(string(), unknown())
42939
- });
42940
- /**
42941
- * One record of a BULK insert — {@link SettingsRecordSchema} with the id made
42942
- * optional.
42943
- *
42944
- * A separate schema rather than loosening the shared one: every other method
42945
- * on this cap addresses a row BY its id, and making that field optional
42946
- * everywhere would turn a forgotten key into a silently generated one on
42947
- * `update` and `delete` as well.
42948
- */
42949
- var BulkRecordSchema = object({
42950
- id: string().optional(),
42951
- data: record(string(), unknown())
42952
- });
42953
- /**
42954
- * The database FILES one `sqlite-settings` engine process owns (D851).
42955
- * Closed on purpose: a file is a thing the engine opens, backs up and gives a
42956
- * serving thread, so a new one is a code change, never a string a caller
42957
- * invents. `settings` is `camstack.db` and the default for every collection
42958
- * that names no file.
42959
- */
42960
- var DataStoreDatabaseSchema = _enum([
42961
- "settings",
42962
- "events",
42963
- "history"
42964
- ]);
42965
- DataStoreDatabaseSchema.options;
42966
- /**
42967
- * One write inside a {@link settingsStoreCapability.methods.transact} call.
42968
- * The three that a "replace this set, bump that revision" write needs, plus
42969
- * the batched upsert a rollup writer needs.
42970
- */
42971
- var TransactOpSchema = discriminatedUnion("op", [
42972
- object({
42973
- op: literal("deleteWhere"),
42974
- collection: string(),
42975
- filter: MutationFilterSchema
42976
- }),
42977
- object({
42978
- op: literal("insertMany"),
42979
- collection: string(),
42980
- records: array(BulkRecordSchema).readonly()
42981
- }),
42982
- object({
42983
- op: literal("update"),
42984
- collection: string(),
42985
- id: string(),
42986
- data: record(string(), unknown())
42987
- }),
42988
- object({
42989
- op: literal("upsertMany"),
42990
- collection: string(),
42991
- conflict: array(string()).readonly(),
42992
- records: array(BulkRecordSchema).readonly()
42993
- })
42994
- ]);
42995
- /**
42996
- * The precondition of a transaction: the row `key` of `collection` must hold
42997
- * `equals` in `column`, read INSIDE the transaction.
42998
- */
42999
- var TransactGuardSchema = object({
43000
- collection: string(),
43001
- key: string(),
43002
- column: string(),
43003
- equals: unknown()
43004
- });
43005
- /**
43006
- * Column declaration for a structured (SQL-backed) collection.
43007
- *
43008
- * Logical types — the backend translates each to the matching SQLite
43009
- * storage class and handles per-type marshaling:
43010
- * - `TEXT` / `INTEGER` / `REAL` — native SQLite types, pass-through
43011
- * - `JSON` — TEXT under the hood; serialised on write, parsed on read
43012
- * - `BOOLEAN` — INTEGER 0/1 under the hood; coerced both directions
43013
- */
43014
- var CollectionColumnSchema = object({
43015
- name: string(),
43016
- type: _enum([
43017
- "TEXT",
43018
- "INTEGER",
43019
- "REAL",
43020
- "JSON",
43021
- "BOOLEAN"
43022
- ]),
43023
- primaryKey: boolean().optional(),
43024
- notNull: boolean().optional(),
43025
- unique: boolean().optional(),
43026
- /**
43027
- * Column DEFAULT. Required for B2: the four `ensureTable` consumers declare
43028
- * columns like `enabled INTEGER NOT NULL DEFAULT 1`, and without this the
43029
- * collection surface simply cannot express their existing tables — which is
43030
- * why they were still on the uncapped `table*` API.
43031
- */
43032
- defaultValue: union([
43033
- string(),
43034
- number(),
43035
- boolean()
43036
- ]).optional()
43037
- });
43038
- var CollectionIndexSchema = object({
43039
- name: string(),
43040
- columns: array(string()).readonly(),
43041
- unique: boolean().optional()
43042
- });
43043
- /**
43044
- * The input of `declareCollection` — ONE schema for the door and for the
43045
- * engine behind it, so the two surfaces cannot drift (the dispatcher forwards
43046
- * the payload untouched).
43047
- *
43048
- * - `database` — the file the collection lives in (D851). Absent means
43049
- * `settings`. A declaration naming a file other than the one the table is
43050
- * already in is REFUSED by the engine, never moved.
43051
- * - `primaryKey` — a COMPOSITE key, as column names. A single name is the same
43052
- * as `primaryKey: true` on that column. A composite-keyed collection has no
43053
- * `id`: it is written with `insertMany` / `upsertMany` / `transact` and read
43054
- * with `query` / `count` / `aggregate` / `deleteWhere`; the calls that
43055
- * address one row by id are refused.
43056
- * - `withoutRowid` — emit `WITHOUT ROWID`. Requires a primary key.
43057
- */
43058
- var DeclareCollectionInputSchema = object({
43059
- namespace: string().optional(),
43060
- collection: string(),
43061
- columns: array(CollectionColumnSchema).readonly(),
43062
- indexes: array(CollectionIndexSchema).readonly().optional(),
43063
- database: DataStoreDatabaseSchema.optional(),
43064
- primaryKey: array(string()).readonly().optional(),
43065
- withoutRowid: boolean().optional()
43066
- });
43067
- method(object({
43068
- namespace: string().optional(),
43069
- collection: string(),
43070
- key: string()
43071
- }), unknown(), { auth: "admin" }), method(object({
43072
- namespace: string().optional(),
43073
- collection: string(),
43074
- key: string(),
43075
- value: unknown()
43076
- }), _void(), {
43077
- kind: "mutation",
43078
- auth: "admin"
43079
- }), method(object({
43080
- namespace: string().optional(),
43081
- collection: string(),
43082
- filter: QueryFilterSchema.optional(),
43083
- columns: array(string()).readonly().optional()
43084
- }), array(SettingsRecordSchema).readonly(), { auth: "admin" }), method(object({
43085
- namespace: string().optional(),
43086
- collection: string(),
43087
- /** Declared columns identifying the slot; must be covered by a UNIQUE
43088
- * index on the collection. Never empty. */
43089
- conflict: array(string()).readonly(),
43090
- record: BulkRecordSchema
43091
- }), object({
43092
- /** The id the row HAS: assigned by SQLite on an insert, or the existing
43093
- * row's own id on an update. */
43094
- id: union([string(), number()]) }), {
43095
- kind: "mutation",
43096
- auth: "admin"
43097
- }), method(object({
43098
- namespace: string().optional(),
43099
- collection: string(),
43100
- conflict: array(string()).readonly(),
43101
- records: array(BulkRecordSchema).readonly()
43102
- }), object({ upserted: number().int() }), {
43103
- kind: "mutation",
43104
- auth: "admin"
43105
- }), method(object({
43106
- namespace: string().optional(),
43107
- collection: string(),
43108
- record: object({
43109
- id: string().optional(),
43110
- data: record(string(), unknown())
43111
- })
43112
- }), object({
43113
- /**
43114
- * The id the row ACTUALLY got (D473): the one supplied, the UUID minted
43115
- * for an absent one, or the ROWID SQLite assigned on an `INTEGER`
43116
- * primary key — which is the only place an auto key is knowable.
43117
- */
43118
- id: union([string(), number()]) }), {
43119
- kind: "mutation",
43120
- auth: "admin"
43121
- }), method(object({
43122
- namespace: string().optional(),
43123
- collection: string(),
43124
- records: array(BulkRecordSchema).readonly()
43125
- }), object({ inserted: number().int() }), {
43126
- kind: "mutation",
43127
- auth: "admin"
43128
- }), method(object({
43129
- namespace: string().optional(),
43130
- guard: TransactGuardSchema.optional(),
43131
- ops: array(TransactOpSchema).readonly()
43132
- }), object({
43133
- applied: boolean(),
43134
- current: unknown().optional()
43135
- }), {
43136
- kind: "mutation",
43137
- auth: "admin"
43138
- }), method(object({
43139
- namespace: string().optional(),
43140
- collection: string(),
43141
- id: string(),
43142
- data: record(string(), unknown())
43143
- }), _void(), {
43144
- kind: "mutation",
43145
- auth: "admin"
43146
- }), method(object({
43147
- namespace: string().optional(),
43148
- collection: string(),
43149
- key: string()
43150
- }), _void(), {
43151
- kind: "mutation",
43152
- auth: "admin"
43153
- }), method(object({
43154
- namespace: string().optional(),
43155
- collection: string(),
43156
- filter: MutationFilterSchema
43157
- }), object({ deleted: number().int() }), {
43158
- kind: "mutation",
43159
- auth: "admin"
43160
- }), method(object({
43161
- namespace: string().optional(),
43162
- collection: string(),
43163
- filter: MutationFilterSchema,
43164
- data: record(string(), unknown())
43165
- }), object({ updated: number().int() }), {
43166
- kind: "mutation",
43167
- auth: "admin"
43168
- }), method(object({
43169
- namespace: string().optional(),
43170
- collection: string(),
43171
- filter: QueryFilterSchema.optional()
43172
- }), number(), { auth: "admin" }), method(object({
43173
- namespace: string().optional(),
43174
- collection: string(),
43175
- fields: array(AggregateFieldSchema).readonly(),
43176
- filter: QueryFilterSchema.optional()
43177
- }), AggregateResultSchema, { auth: "admin" }), method(object({
43178
- namespace: string().optional(),
43179
- collection: string(),
43180
- field: string(),
43181
- bucketSize: number().int().positive(),
43182
- origin: number().int(),
43183
- filter: QueryFilterSchema.optional()
43184
- }), array(object({
43185
- bucket: number().int(),
43186
- count: number().int()
43187
- })).readonly(), { auth: "admin" }), method(object({
43188
- namespace: string().optional(),
43189
- collection: string()
43190
- }), boolean(), { auth: "admin" }), method(DeclareCollectionInputSchema, _void(), {
43191
- kind: "mutation",
43192
- auth: "admin"
43193
- });
43194
- /**
43195
43712
  * How long one snapshot may take end to end: the pinned copy of the whole
43196
43713
  * database plus `PRAGMA integrity_check` over the copy, on the hub's slowest
43197
43714
  * disk. ~800 MB at a pessimistic 10 MB/s is under 3 minutes; the bound is
@@ -45105,7 +45622,7 @@ method(object({
45105
45622
  kind: "mutation",
45106
45623
  auth: "admin"
45107
45624
  }), method(object({ blockIds: array(string()) }), object({ states: array(CompositionBlockStateSchema) }), { auth: "admin" });
45108
- DeviceType.Camera, DeviceType.Sensor, DeviceType.Button, DeviceType.Switch, DeviceType.PetFeeder, DeviceType.Container, DeviceType.Thermostat, DeviceType.EventEmitter, DeviceType.Camera, DeviceType.Hub, DeviceType.Light, DeviceType.Siren, DeviceType.Switch, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Fan, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder, DeviceType.PetFountain, DeviceType.Dishwasher, DeviceType.Washer, DeviceType.Dryer, DeviceType.Gateway, DeviceType.Camera, DeviceType.Lock, DeviceType.Camera, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.MediaPlayer, DeviceType.WaterHeater, DeviceType.Humidifier, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.PetFeeder;
45625
+ 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;
45109
45626
  var NUMBER = {
45110
45627
  kind: "fixed",
45111
45628
  result: ["number"]
@@ -46116,6 +46633,57 @@ id: union([string(), number()]) }), {
46116
46633
  kind: "mutation",
46117
46634
  auth: "admin"
46118
46635
  });
46636
+ 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({
46637
+ id: string().min(1),
46638
+ error: string().min(1)
46639
+ }), MigrationLedgerEntrySchema, {
46640
+ kind: "mutation",
46641
+ auth: "admin"
46642
+ }), method(object({ id: string().min(1) }), object({ entries: array(MigrationLedgerEntrySchema) }), {
46643
+ kind: "mutation",
46644
+ auth: "admin"
46645
+ }), method(object({
46646
+ database: DataStoreDatabaseSchema,
46647
+ snapshotId: string().min(1)
46648
+ }), object({
46649
+ scheduled: boolean(),
46650
+ held: array(string()),
46651
+ restartRequired: boolean()
46652
+ }), {
46653
+ kind: "mutation",
46654
+ auth: "admin"
46655
+ }), method(object({
46656
+ owner: string().min(1),
46657
+ metas: array(MigrationMetaSchema)
46658
+ }), object({
46659
+ bootId: string(),
46660
+ snapshot: _enum([
46661
+ "none",
46662
+ "written",
46663
+ "refused"
46664
+ ]),
46665
+ steps: array(MigrationOwnerStepSchema)
46666
+ }), {
46667
+ kind: "mutation",
46668
+ auth: "admin"
46669
+ }), method(object({ id: string().min(1) }), object({
46670
+ proceed: boolean(),
46671
+ state: union([literal("pending"), MigrationStatusSchema]),
46672
+ reason: string().nullable()
46673
+ }), {
46674
+ kind: "mutation",
46675
+ auth: "admin"
46676
+ }), method(object({
46677
+ id: string().min(1),
46678
+ result: MigrationOwnerResultSchema
46679
+ }), object({
46680
+ status: MigrationStatusSchema,
46681
+ error: string().nullable(),
46682
+ rowsChanged: number().int().nonnegative()
46683
+ }), {
46684
+ kind: "mutation",
46685
+ auth: "admin"
46686
+ });
46119
46687
  /**
46120
46688
  * records - an addon's own facts and documents (D881): ledgers, audit rows,
46121
46689
  * config documents, mirrors. The third storage API next to `ctx.settings`
@@ -60980,7 +61548,18 @@ var ServerUpdateActionResultSchema = object({
60980
61548
  * store (D877), present when the pre-flight refused the update
60981
61549
  * (`reason: 'addon-would-refuse'`). Optional for version skew.
60982
61550
  */
60983
- blockers: array(UpdateBlockerSchema).optional()
61551
+ blockers: array(UpdateBlockerSchema).optional(),
61552
+ /**
61553
+ * The minimums the target server declares and this node does not meet
61554
+ * (D989), present when the update was refused with
61555
+ * `reason: 'requirements-unmet'`. Each names the versions on both sides.
61556
+ */
61557
+ unmet: array(UnmetRequirementSchema).optional(),
61558
+ /**
61559
+ * The addons this node has below the minimum the staged server declares —
61560
+ * the ones to update WITH the server. Present on an accepted update.
61561
+ */
61562
+ requiredAddonUpdates: array(RequiredAddonUpdateSchema).optional()
60984
61563
  });
60985
61564
  var LaunchOverridesSchema = object({
60986
61565
  /** The persisted heap ceiling in MB, or `null` when none is set. */
@@ -63524,7 +64103,9 @@ var PackageUpdateSchema = object({
63524
64103
  latestVersion: string(),
63525
64104
  category: _enum(["addon", "core"]),
63526
64105
  requiresRestart: boolean(),
63527
- isSystem: boolean()
64106
+ isSystem: boolean(),
64107
+ /** Held: what the newer version needs and this node does not meet (D989). */
64108
+ blockedBy: array(UnmetRequirementSchema).readonly().optional()
63528
64109
  });
63529
64110
  var PackageVersionInfoSchema = object({
63530
64111
  version: string(),
@@ -64980,19 +65561,19 @@ var RemoteSetConfigResultSchema = discriminatedUnion("status", [object({ status:
64980
65561
  reason: _enum(["unknown-template", "not-a-media-player"]),
64981
65562
  detail: string()
64982
65563
  })]);
64983
- var deviceInput = object({ deviceId: number().int().nonnegative() });
65564
+ var deviceInput$1 = object({ deviceId: number().int().nonnegative() });
64984
65565
  DeviceType.MediaPlayer;
64985
- method(deviceInput, RemoteConfigSchema, { auth: "admin" }), method(deviceInput.extend({ config: RemoteConfigSchema }), RemoteSetConfigResultSchema, {
65566
+ method(deviceInput$1, RemoteConfigSchema, { auth: "admin" }), method(deviceInput$1.extend({ config: RemoteConfigSchema }), RemoteSetConfigResultSchema, {
64986
65567
  kind: "mutation",
64987
65568
  auth: "admin"
64988
- }), method(deviceInput, RemoteCommandListSchema, { auth: "protected" }), method(deviceInput.extend({
65569
+ }), method(deviceInput$1, RemoteCommandListSchema, { auth: "protected" }), method(deviceInput$1.extend({
64989
65570
  role: RemoteRoleSchema,
64990
65571
  /** Sends in a row (volume steps). */
64991
65572
  repeat: number().int().min(1).max(50).default(1)
64992
65573
  }), RemoteSendResultSchema, {
64993
65574
  kind: "mutation",
64994
65575
  auth: "protected"
64995
- }), method(deviceInput.extend({
65576
+ }), method(deviceInput$1.extend({
64996
65577
  command: RemoteMediaCommandSchema,
64997
65578
  muted: boolean().optional(),
64998
65579
  source: string().optional(),
@@ -68807,7 +69388,7 @@ var lawnMowerControlCapability = {
68807
69388
  * clients — they reverse-connect to the hub. Exposing their interfaces
68808
69389
  * via the same surface would leak internal topology with no upside.
68809
69390
  *
68810
- * Implementation in `@camstack/system/builtins/local-network/`.
69391
+ * Implementation in `@camstack/server/system/builtins/local-network/`.
68811
69392
  */
68812
69393
  /** Coarse classification derived from the interface name + IP range. */
68813
69394
  var InterfaceKindEnum = _enum([
@@ -69304,6 +69885,117 @@ var lockControlCapability = {
69304
69885
  * whether persisting is worth a SQLite commit. */
69305
69886
  volatileStateFields: ["lastChangedAt"]
69306
69887
  };
69888
+ var PowerSourceRefusalReasonSchema = _enum([
69889
+ "no-power-source",
69890
+ "switch-missing",
69891
+ "switch-offline",
69892
+ "switch-unknown",
69893
+ "restart-in-progress",
69894
+ "switch-call-failed"
69895
+ ]);
69896
+ var PowerSourceActionResultSchema = discriminatedUnion("accepted", [object({ accepted: literal(true) }), object({
69897
+ accepted: literal(false),
69898
+ reason: PowerSourceRefusalReasonSchema,
69899
+ /** One line for the operator, naming the switch. */
69900
+ message: string()
69901
+ })]);
69902
+ var PowerSourceInfoSchema = object({
69903
+ /** False when the device has no power source linked; the rest is then empty. */
69904
+ linked: boolean(),
69905
+ switchDeviceId: number().nullable(),
69906
+ switchName: string().nullable(),
69907
+ /** The switch's on/off state, or null when it is unknown. Never false for unknown (D393). */
69908
+ switchOn: boolean().nullable(),
69909
+ switchOnline: boolean().nullable(),
69910
+ restartInProgress: boolean(),
69911
+ offSeconds: number().int()
69912
+ });
69913
+ /** The composed slice: the switch state the link mirrors, plus the stored off time. */
69914
+ var PowerSourceStateSchema = object({
69915
+ /** Null while the switch has not reported: unknown is never `false` (D393). */
69916
+ switchOn: boolean().nullable(),
69917
+ offSeconds: number().int(),
69918
+ lastFetchedAt: number()
69919
+ });
69920
+ var POWER_SOURCE_DEVICE_TYPES = [
69921
+ DeviceType.Gateway,
69922
+ DeviceType.Hub,
69923
+ DeviceType.Camera,
69924
+ DeviceType.Sensor,
69925
+ DeviceType.MediaPlayer,
69926
+ DeviceType.Vacuum,
69927
+ DeviceType.Generic,
69928
+ DeviceType.Washer,
69929
+ DeviceType.Dryer,
69930
+ DeviceType.Dishwasher,
69931
+ DeviceType.PetFountain,
69932
+ DeviceType.Fan,
69933
+ DeviceType.Humidifier,
69934
+ DeviceType.Climate,
69935
+ DeviceType.WaterHeater
69936
+ ];
69937
+ var powerSourceCustomize = {
69938
+ version: 1,
69939
+ targets: [{
69940
+ id: "switch",
69941
+ cardinality: "scalar",
69942
+ vocabulary: {
69943
+ cap: "power-source",
69944
+ stateField: "switchOn"
69945
+ },
69946
+ deviceTypes: POWER_SOURCE_DEVICE_TYPES,
69947
+ apply: "add",
69948
+ path: "switchOn",
69949
+ sources: [{
69950
+ kind: "switch",
69951
+ transforms: [{ kind: "identity" }]
69952
+ }],
69953
+ companions: [{
69954
+ kind: "constant",
69955
+ path: "offSeconds",
69956
+ value: 10,
69957
+ when: "add"
69958
+ }, {
69959
+ kind: "clock",
69960
+ path: "lastFetchedAt",
69961
+ when: "always"
69962
+ }],
69963
+ sourceReport: { status: "assignment-status" }
69964
+ }]
69965
+ };
69966
+ var deviceInput = object({ deviceId: number().int().nonnegative() });
69967
+ var powerSourceCapability = {
69968
+ name: "power-source",
69969
+ scope: "device",
69970
+ mode: "singleton",
69971
+ deviceTypes: POWER_SOURCE_DEVICE_TYPES,
69972
+ methods: {
69973
+ getStatus: systemMethod(deviceInput, PowerSourceInfoSchema, { auth: "admin" }),
69974
+ powerOn: systemMethod(deviceInput, PowerSourceActionResultSchema, {
69975
+ kind: "mutation",
69976
+ auth: "admin"
69977
+ }),
69978
+ powerOff: systemMethod(deviceInput, PowerSourceActionResultSchema, {
69979
+ kind: "mutation",
69980
+ auth: "admin"
69981
+ }),
69982
+ forceRestart: systemMethod(deviceInput, PowerSourceActionResultSchema, {
69983
+ kind: "mutation",
69984
+ auth: "admin"
69985
+ })
69986
+ },
69987
+ runtimeState: PowerSourceStateSchema,
69988
+ customize: powerSourceCustomize,
69989
+ /**
69990
+ * Runtime-state durability: **session** — mirrored from the linked switch on
69991
+ * its next report; a restored value would outlive the reading it names.
69992
+ *
69993
+ * See `RuntimeStateDurability`. Enforced by
69994
+ * `scripts/check-runtime-state-durability.ts`.
69995
+ */
69996
+ durability: "session",
69997
+ history: "none"
69998
+ };
69307
69999
  /**
69308
70000
  * A monotonic total between resets (D944). Energy uses `power-meter.energy`
69309
70001
  * so dashboards keep one series; every other instantaneous reading lands here.
@@ -70584,7 +71276,7 @@ onStatusChanged: { data: object({
70584
71276
  * jitter, and observed/peak bandwidth per device + per client.
70585
71277
  *
70586
71278
  * Implementation lives in the server's `NetworkQualityService` (thin
70587
- * wrapper over the shared `NetworkQualityTracker` from `@camstack/system`).
71279
+ * wrapper over the shared `NetworkQualityTracker` from `@camstack/server/system`).
70588
71280
  * The provider is registered from `trpc.router.ts` against the existing
70589
71281
  * service instance — no addon owns this state.
70590
71282
  *
@@ -72735,7 +73427,7 @@ var ptzAutotrackCapability = {
72735
73427
  * - **Native**: the camera's own firmware autotrack, registered per device by
72736
73428
  * the driver when the probe says it exists (Reolink: yes; Hikvision:
72737
73429
  * model-dependent).
72738
- * - **Wrapper**: the emulated implementation (`@camstack/system`'s `autotrack` builtin),
73430
+ * - **Wrapper**: the emulated implementation (`@camstack/server/system`'s `autotrack` builtin),
72739
73431
  * which follows a subject with `ptz.move` pulses driven by the detection
72740
73432
  * pipeline. `defaultActive: false` — the operator binds it per camera in
72741
73433
  * the bindings UI.
@@ -78044,6 +78736,7 @@ var DEVICE_LOCAL_STATE_CAPS = {
78044
78736
  petFeeder: petFeederCapability,
78045
78737
  petFountain: petFountainCapability,
78046
78738
  powerMeter: powerMeterCapability,
78739
+ powerSource: powerSourceCapability,
78047
78740
  presence: presenceCapability,
78048
78741
  pressureSensor: pressureSensorCapability,
78049
78742
  privacyMask: privacyMaskCapability,
@@ -83808,6 +84501,69 @@ Object.freeze({
83808
84501
  access: "view",
83809
84502
  role: "admin"
83810
84503
  },
84504
+ "migrations.declareOwner": {
84505
+ capName: "migrations",
84506
+ capScope: "system",
84507
+ addonId: null,
84508
+ access: "create",
84509
+ role: "admin"
84510
+ },
84511
+ "migrations.dryRun": {
84512
+ capName: "migrations",
84513
+ capScope: "system",
84514
+ addonId: null,
84515
+ access: "view",
84516
+ role: "admin"
84517
+ },
84518
+ "migrations.finishMigration": {
84519
+ capName: "migrations",
84520
+ capScope: "system",
84521
+ addonId: null,
84522
+ access: "create",
84523
+ role: "admin"
84524
+ },
84525
+ "migrations.listLedger": {
84526
+ capName: "migrations",
84527
+ capScope: "system",
84528
+ addonId: null,
84529
+ access: "view",
84530
+ role: "admin"
84531
+ },
84532
+ "migrations.listPending": {
84533
+ capName: "migrations",
84534
+ capScope: "system",
84535
+ addonId: null,
84536
+ access: "view",
84537
+ role: "admin"
84538
+ },
84539
+ "migrations.markFailed": {
84540
+ capName: "migrations",
84541
+ capScope: "system",
84542
+ addonId: null,
84543
+ access: "create",
84544
+ role: "admin"
84545
+ },
84546
+ "migrations.restorePreMigration": {
84547
+ capName: "migrations",
84548
+ capScope: "system",
84549
+ addonId: null,
84550
+ access: "create",
84551
+ role: "admin"
84552
+ },
84553
+ "migrations.retry": {
84554
+ capName: "migrations",
84555
+ capScope: "system",
84556
+ addonId: null,
84557
+ access: "create",
84558
+ role: "admin"
84559
+ },
84560
+ "migrations.startMigration": {
84561
+ capName: "migrations",
84562
+ capScope: "system",
84563
+ addonId: null,
84564
+ access: "create",
84565
+ role: "admin"
84566
+ },
83811
84567
  "modelConvert.convert": {
83812
84568
  capName: "model-convert",
83813
84569
  capScope: "system",
@@ -86440,6 +87196,34 @@ Object.freeze({
86440
87196
  access: "view",
86441
87197
  role: "viewer"
86442
87198
  },
87199
+ "powerSource.forceRestart": {
87200
+ capName: "power-source",
87201
+ capScope: "device",
87202
+ addonId: null,
87203
+ access: "create",
87204
+ role: "admin"
87205
+ },
87206
+ "powerSource.getStatus": {
87207
+ capName: "power-source",
87208
+ capScope: "device",
87209
+ addonId: null,
87210
+ access: "view",
87211
+ role: "admin"
87212
+ },
87213
+ "powerSource.powerOff": {
87214
+ capName: "power-source",
87215
+ capScope: "device",
87216
+ addonId: null,
87217
+ access: "create",
87218
+ role: "admin"
87219
+ },
87220
+ "powerSource.powerOn": {
87221
+ capName: "power-source",
87222
+ capScope: "device",
87223
+ addonId: null,
87224
+ access: "create",
87225
+ role: "admin"
87226
+ },
86443
87227
  "presence.getStatus": {
86444
87228
  capName: "presence",
86445
87229
  capScope: "device",
@@ -91588,6 +92372,26 @@ Object.freeze({
91588
92372
  form: "single",
91589
92373
  optional: false
91590
92374
  }],
92375
+ "powerSource.forceRestart": [{
92376
+ name: "deviceId",
92377
+ form: "single",
92378
+ optional: false
92379
+ }],
92380
+ "powerSource.getStatus": [{
92381
+ name: "deviceId",
92382
+ form: "single",
92383
+ optional: false
92384
+ }],
92385
+ "powerSource.powerOff": [{
92386
+ name: "deviceId",
92387
+ form: "single",
92388
+ optional: false
92389
+ }],
92390
+ "powerSource.powerOn": [{
92391
+ name: "deviceId",
92392
+ form: "single",
92393
+ optional: false
92394
+ }],
91591
92395
  "presence.getStatus": [{
91592
92396
  name: "deviceId",
91593
92397
  form: "single",
@@ -95304,6 +96108,60 @@ var EnpalClientRegistry = class {
95304
96108
  }
95305
96109
  };
95306
96110
  //#endregion
96111
+ //#region src/enpal-reconciler.ts
96112
+ /**
96113
+ * Keeps one live client per enabled integration. Devices are removed ONLY on an explicit
96114
+ * lifecycle event naming the integration (D49): a list that failed, an empty list or an
96115
+ * unreadable connection changes nothing about devices and is logged, never read as "gone".
96116
+ */
96117
+ var EnpalReconciler = class {
96118
+ #deps;
96119
+ constructor(deps) {
96120
+ this.#deps = deps;
96121
+ }
96122
+ /** Upsert a client per usable enabled integration; drop the CLIENT (never devices) of one the list answered is gone. */
96123
+ async reconcile() {
96124
+ const { logger, clients } = this.#deps;
96125
+ let all;
96126
+ try {
96127
+ all = await this.#deps.listIntegrations();
96128
+ } catch (err) {
96129
+ logger.warn("Enpal integration reconcile skipped — the integration list did not answer", { meta: { error: errMsg(err) } });
96130
+ return;
96131
+ }
96132
+ const enabled = all.filter((i) => i.addonId === this.#deps.addonId && i.enabled);
96133
+ for (const integration of enabled) try {
96134
+ if (!await this.#deps.applySettings(integration)) logger.warn("Enpal integration has no usable connection — left as it is", { meta: { integrationId: integration.id } });
96135
+ } catch (err) {
96136
+ logger.warn("Enpal reconcile failed for integration — left as it is", { meta: {
96137
+ integrationId: integration.id,
96138
+ error: errMsg(err)
96139
+ } });
96140
+ }
96141
+ const enabledIds = new Set(enabled.map((i) => i.id));
96142
+ for (const id of clients.list()) if (!enabledIds.has(id)) await clients.remove(id);
96143
+ }
96144
+ /** An explicit lifecycle event: only `deleted` / `disabled` of a named integration remove devices. */
96145
+ async onLifecycle(kind, integrationId) {
96146
+ await this.reconcile();
96147
+ if (kind === "enabled" || integrationId === null) return;
96148
+ try {
96149
+ const removed = await this.#deps.removeDevices(integrationId);
96150
+ this.#deps.logger.info("Enpal integration devices removed", { meta: {
96151
+ integrationId,
96152
+ kind,
96153
+ removed
96154
+ } });
96155
+ } catch (err) {
96156
+ this.#deps.logger.warn("Enpal integration device removal failed", { meta: {
96157
+ integrationId,
96158
+ kind,
96159
+ error: errMsg(err)
96160
+ } });
96161
+ }
96162
+ }
96163
+ };
96164
+ //#endregion
95307
96165
  //#region src/enpal-integration-device-cascade.ts
95308
96166
  /**
95309
96167
  * Remove every equipment Container of an integration that was deleted or disabled; the
@@ -95427,6 +96285,7 @@ var EnpalProviderAddon = class extends BaseDeviceProvider {
95427
96285
  [DeviceType.Container]: EnpalEquipmentDevice,
95428
96286
  [DeviceType.Gateway]: EnpalGatewayDevice
95429
96287
  };
96288
+ integrationReconciler = null;
95430
96289
  clients = null;
95431
96290
  constructor() {
95432
96291
  super({});
@@ -95451,6 +96310,7 @@ var EnpalProviderAddon = class extends BaseDeviceProvider {
95451
96310
  this.ctx.logger.warn("Enpal shutdown error", { meta: { error: errMsg(err) } });
95452
96311
  }
95453
96312
  this.clients = null;
96313
+ this.integrationReconciler = null;
95454
96314
  enpalConnections.clear();
95455
96315
  await super.onShutdown();
95456
96316
  }
@@ -95467,51 +96327,47 @@ var EnpalProviderAddon = class extends BaseDeviceProvider {
95467
96327
  return `enpal:${integrationId}:${equipment}`;
95468
96328
  }
95469
96329
  /**
95470
- * Rebuild the pollers from the live integration list: upsert one per enabled integration
95471
- * with a host, drop the ones that vanished and cascade-remove their devices. Idempotent;
95472
- * runs at boot and on every integration lifecycle event.
96330
+ * Upsert one client per enabled integration and drop the client of one the list ANSWERED is
96331
+ * gone. Never removes a device (D49): that follows only an explicit lifecycle event.
96332
+ * Idempotent; runs at boot and on every integration lifecycle event.
95473
96333
  */
95474
- async reconcileIntegrations() {
95475
- const reg = this.requireClients();
95476
- try {
95477
- const mine = (await this.ctx.api.integrations.list.query()).filter((i) => i.addonId === this.ctx.id && i.enabled);
95478
- const surviving = /* @__PURE__ */ new Set();
95479
- for (const integration of mine) try {
96334
+ reconciler() {
96335
+ this.integrationReconciler ??= new EnpalReconciler({
96336
+ addonId: this.ctx.id,
96337
+ logger: this.ctx.logger,
96338
+ clients: this.requireClients(),
96339
+ listIntegrations: () => this.ctx.api.integrations.list.query(),
96340
+ applySettings: async (integration) => {
96341
+ const reg = this.requireClients();
95480
96342
  const read = connectionFromSettings(await this.ctx.api.integrations.getSettings.query({ id: integration.id }));
95481
- if (read === null) {
95482
- this.ctx.logger.warn("Enpal integration has no box host — skipping", { meta: { integrationId: integration.id } });
95483
- continue;
95484
- }
96343
+ if (read === null) return false;
95485
96344
  await reg.upsert(integration.id, integration.name, read.connection, read.tokenLocked);
95486
- surviving.add(integration.id);
95487
- } catch (err) {
95488
- this.ctx.logger.warn("Enpal reconcile failed for integration", { meta: {
95489
- integrationId: integration.id,
95490
- error: errMsg(err)
95491
- } });
95492
- }
95493
- const vanished = reg.list().filter((id) => !surviving.has(id));
95494
- await reg.retain(surviving);
95495
- for (const integrationId of vanished) await this.cascadeRemoveDevicesForIntegration(integrationId);
95496
- } catch (err) {
95497
- this.ctx.logger.warn("Enpal integration reconcile failed", { meta: { error: errMsg(err) } });
95498
- }
96345
+ return true;
96346
+ },
96347
+ removeDevices: (integrationId) => this.cascadeRemoveDevicesForIntegration(integrationId)
96348
+ });
96349
+ return this.integrationReconciler;
96350
+ }
96351
+ async reconcileIntegrations() {
96352
+ await this.reconciler().reconcile();
95499
96353
  }
96354
+ /** Lifecycle events: only an explicit Deleted/Disabled naming an integration removes devices. */
95500
96355
  subscribeIntegrationLifecycle() {
95501
- const handler = (event) => {
96356
+ const on = (kind) => (event) => {
95502
96357
  const addonId = event.data["addonId"];
95503
96358
  if (typeof addonId === "string" && addonId !== this.ctx.id) return;
95504
- this.reconcileIntegrations();
96359
+ const integrationId = event.data["integrationId"];
96360
+ this.reconciler().onLifecycle(kind, typeof integrationId === "string" ? integrationId : null);
95505
96361
  };
95506
- this.ctx.eventBus.subscribe({ category: EventCategory.IntegrationEnabled }, handler);
95507
- this.ctx.eventBus.subscribe({ category: EventCategory.IntegrationDisabled }, handler);
95508
- this.ctx.eventBus.subscribe({ category: EventCategory.IntegrationDeleted }, handler);
96362
+ this.ctx.eventBus.subscribe({ category: EventCategory.IntegrationEnabled }, on("enabled"));
96363
+ this.ctx.eventBus.subscribe({ category: EventCategory.IntegrationDisabled }, on("disabled"));
96364
+ this.ctx.eventBus.subscribe({ category: EventCategory.IntegrationDeleted }, on("deleted"));
95509
96365
  }
95510
96366
  async cascadeRemoveDevicesForIntegration(integrationId) {
95511
96367
  const reg = this.ctx.kernel.deviceRegistry;
95512
96368
  const devices = this.ctx.kernel.devices;
95513
- if (!reg || !devices) return;
95514
- await cascadeRemoveDevicesForIntegration({
96369
+ if (!reg || !devices) return 0;
96370
+ return await cascadeRemoveDevicesForIntegration({
95515
96371
  reg,
95516
96372
  devices,
95517
96373
  addonId: this.addonId,