@camstack/addon-provider-rademacher 0.2.154 → 0.2.155

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/dist/addon.js +2003 -1143
  3. package/dist/addon.mjs +2003 -1143
  4. package/package.json +5 -2
package/dist/addon.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import { createHash } from "crypto";
2
2
  import { EventEmitter } from "events";
3
3
  import { networkInterfaces } from "os";
4
- //#region ../types/dist/event-category-DmZDPDf_.mjs
4
+ //#region ../types/dist/event-category-CaURIwQA.mjs
5
5
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
6
6
  EventCategory["SystemBoot"] = "system.boot";
7
7
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -717,6 +717,10 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
717
717
  EventCategory["NotificationRuleBroken"] = "notification.rule-broken";
718
718
  /** The clear of {@link NotificationRuleBroken}: the rule loads and names nothing deleted. */
719
719
  EventCategory["NotificationRuleRepaired"] = "notification.rule-repaired";
720
+ /** A migration failed or was blocked; the Alert Center re-reads the ledger. */
721
+ EventCategory["MigrationFailed"] = "migration.failed";
722
+ /** A migration applied or a failure cleared; the Alert Center re-reads the ledger. */
723
+ EventCategory["MigrationApplied"] = "migration.applied";
720
724
  EventCategory["DeviceUpdated"] = "device.updated";
721
725
  /**
722
726
  * Transport-level connectivity. Emitted by the device driver when
@@ -779,6 +783,378 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
779
783
  return EventCategory;
780
784
  }({});
781
785
  //#endregion
786
+ //#region ../types/dist/device-type-CEqjmUnR.mjs
787
+ /**
788
+ import { errMsg } from '@camstack/types'
789
+ * Extract a human-readable message from an unknown error value.
790
+ * Replaces the ubiquitous `errMsg(err)` pattern.
791
+ */
792
+ function errMsg(err) {
793
+ if (err instanceof Error) return err.message;
794
+ if (typeof err === "string") return err;
795
+ return String(err);
796
+ }
797
+ var DeviceType = /* @__PURE__ */ function(DeviceType) {
798
+ DeviceType["Camera"] = "camera";
799
+ DeviceType["Hub"] = "hub";
800
+ DeviceType["Light"] = "light";
801
+ DeviceType["Siren"] = "siren";
802
+ DeviceType["Switch"] = "switch";
803
+ DeviceType["Sensor"] = "sensor";
804
+ DeviceType["Thermostat"] = "thermostat";
805
+ /** Air-conditioner / heat-pump climate device (HVAC) — shares the
806
+ * `climate-control` cap surface with `Thermostat` but renders a
807
+ * dedicated AC-appropriate control UI (mode chips, fan speed,
808
+ * independent vertical/horizontal swing). Sources: native Gree, and
809
+ * reusable by other AC integrations. */
810
+ DeviceType["Climate"] = "climate";
811
+ DeviceType["Button"] = "button";
812
+ /** Generic stateless event emitter — carries a device's EXACT declared
813
+ * event vocabulary verbatim (no normalization). Installed with the
814
+ * `event-emitter` cap. Sources: HA `event.*` entities (structured) and
815
+ * HA bus events (e.g. `zha_event`, generic). */
816
+ DeviceType["EventEmitter"] = "event-emitter";
817
+ /** Firmware/software update entity — current vs available version,
818
+ * updatable flag, update state, and an install action. Installed with
819
+ * the `update` cap. Sources: Homematic firmware-update channels (and
820
+ * reusable by other providers, e.g. HA `update.*` entities). */
821
+ DeviceType["Update"] = "update";
822
+ DeviceType["Generic"] = "generic";
823
+ /** Generic notification delivery target (HA `notify.<service>`, future
824
+ * Telegram / Discord / ntfy / SMTP, …). One device per delivery
825
+ * endpoint; the `notifier` cap defines the send surface. */
826
+ DeviceType["Notifier"] = "notifier";
827
+ /** Pre-recorded action sequence with optional parameters
828
+ * (HA `script.*`). Runnable via `script-runner` cap. */
829
+ DeviceType["Script"] = "script";
830
+ /** Automation rule (HA `automation.*`) — enable/disable + manual
831
+ * trigger surface exposed via `automation-control` cap. */
832
+ DeviceType["Automation"] = "automation";
833
+ /** Door / smart lock device (HA `lock.*`). `lock-control` cap. */
834
+ DeviceType["Lock"] = "lock";
835
+ /** Window covering, blinds, garage door, valve, etc. (HA `cover.*`,
836
+ * `valve.*`). `cover` cap with sub-roles for variant. */
837
+ DeviceType["Cover"] = "cover";
838
+ /** Pipe / water / gas valve with open/close/stop and optional
839
+ * position (HA `valve.*`). `valve` cap — a cover-sibling actuator
840
+ * modelled on the same open/closed lifecycle. */
841
+ DeviceType["Valve"] = "valve";
842
+ /** Humidifier / dehumidifier with on/off + target humidity + mode
843
+ * (HA `humidifier.*`). `humidifier` cap — a climate-family actuator
844
+ * modelled on the same target / mode lifecycle. */
845
+ DeviceType["Humidifier"] = "humidifier";
846
+ /** Water heater / boiler with target temperature + operation mode +
847
+ * away mode (HA `water_heater.*`). `water-heater` cap — a
848
+ * climate-family actuator. */
849
+ DeviceType["WaterHeater"] = "water-heater";
850
+ /** Ceiling / standing / exhaust fan (HA `fan.*`). `fan-control` cap. */
851
+ DeviceType["Fan"] = "fan";
852
+ /** Audio / video playback endpoint (HA `media_player.*`). Disjoint from
853
+ * the camera surface — those use `Camera`. `media-player` cap. */
854
+ DeviceType["MediaPlayer"] = "media-player";
855
+ /** Security panel / alarm system (HA `alarm_control_panel.*`).
856
+ * `alarm-panel` cap. */
857
+ DeviceType["AlarmPanel"] = "alarm-panel";
858
+ /** Generic user-settable input (HA `number` / `input_number` / `select`
859
+ * / `input_select` / `text` / `input_text` / `input_datetime`).
860
+ * Sub-type via `DeviceRole`: NumericControl / SelectControl /
861
+ * TextControl / DateTimeControl. */
862
+ DeviceType["Control"] = "control";
863
+ /** Person / device-tracker presence (HA `person.*`, `device_tracker.*`).
864
+ * `presence` cap. */
865
+ DeviceType["Presence"] = "presence";
866
+ /** Weather provider (HA `weather.*`). Tier-3, low MVP priority.
867
+ * `weather` cap. */
868
+ DeviceType["Weather"] = "weather";
869
+ /** Robot vacuum (HA `vacuum.*`). Tier-3. `vacuum-control` cap. */
870
+ DeviceType["Vacuum"] = "vacuum";
871
+ /** Robotic lawn mower (HA `lawn_mower.*`). Tier-3.
872
+ * `lawn-mower-control` cap. */
873
+ DeviceType["LawnMower"] = "lawn-mower";
874
+ /** Physical HA device group — parent container for entity-children
875
+ * adopted from a single HA device entry. Not renderable as a
876
+ * standalone device; exists only to anchor child entities. */
877
+ DeviceType["Container"] = "container";
878
+ /** Single still-image entity (HA `image.*`). Read-only display of an
879
+ * `entity_picture` signed URL the browser loads directly. `image` cap. */
880
+ DeviceType["Image"] = "image";
881
+ /** Smart pet feeder — cloud-connected food dispenser with a bowl food
882
+ * level, battery, desiccant life, feeding state and manual-feed /
883
+ * call-pet / maintenance actions. Installed with the `pet-feeder` cap;
884
+ * dual-hopper models (D4S/D4SH) expose per-hopper portions. Sources:
885
+ * native PetKit (`nodepetkit` `FeederDevice`), reusable by other feeder
886
+ * integrations sharing the same food/desiccant/hopper surface. */
887
+ DeviceType["PetFeeder"] = "pet-feeder";
888
+ /** Pet water fountain — tank level, water drunk, pump and its duty cycle.
889
+ * Installed with the `pet-fountain` cap. Reaches CamStack as loose entities
890
+ * of a container and is BUILT as a composed device (D967). */
891
+ DeviceType["PetFountain"] = "pet-fountain";
892
+ /** Built-in dishwasher — program lifecycle, door / remote-start gates,
893
+ * salt and rinse-aid levels. Installed with the `dishwasher` cap.
894
+ * Sources: native Bosch Home Connect, reusable by similar integrations.
895
+ * A plug-built dishwasher carries running-state and power instead. */
896
+ DeviceType["Dishwasher"] = "dishwasher";
897
+ /** Washing machine. A plug-built one is running while its power stays
898
+ * above a threshold. No program cap — that surface is the dishwasher's. */
899
+ DeviceType["Washer"] = "washer";
900
+ /** Tumble dryer. Same plug-built running surface as {@link Washer}. */
901
+ DeviceType["Dryer"] = "dryer";
902
+ /** The physical bridge box of a vendor ecosystem (Velux KLF-200, and later
903
+ * other radio-to-IP bridges). It carries the box's own facts (firmware,
904
+ * address, connection state) and the actions the box really supports
905
+ * (`reboot`, `device-admin-link`). Products are NOT its children — they are
906
+ * adopted beside it — and its connection settings stay on the integration. */
907
+ DeviceType["Gateway"] = "gateway";
908
+ return DeviceType;
909
+ }({});
910
+ var DeviceFeature = /* @__PURE__ */ function(DeviceFeature) {
911
+ DeviceFeature["BatteryOperated"] = "battery-operated";
912
+ DeviceFeature["Rebootable"] = "rebootable";
913
+ /**
914
+ * Device supports an on-demand re-sync of its derived spec with its
915
+ * upstream source — drives the generic Re-sync button. The owning
916
+ * provider implements the action via the `device-adoption.resync` cap.
917
+ */
918
+ DeviceFeature["Resyncable"] = "resyncable";
919
+ DeviceFeature["NativeSnapshot"] = "native-snapshot";
920
+ DeviceFeature["DoorbellButton"] = "doorbell-button";
921
+ DeviceFeature["TwoWayAudio"] = "two-way-audio";
922
+ DeviceFeature["PanTiltZoom"] = "pan-tilt-zoom";
923
+ /**
924
+ * Camera supports the on-firmware autotrack subsystem (subject-
925
+ * following). Distinct from `PanTiltZoom` because not every PTZ
926
+ * camera ships autotrack — the admin UI uses this flag to gate
927
+ * the autotrack toggle / settings card without re-deriving from
928
+ * the cap registry. Mirrors `ptz-autotrack` cap registration:
929
+ * driver sets this feature when probe confirms the firmware
930
+ * surface, and registers the cap in the same code path.
931
+ */
932
+ DeviceFeature["PtzAutotrack"] = "ptz-autotrack";
933
+ /**
934
+ * Accessory exposes a "trigger on motion" toggle — the parent camera's
935
+ * motion detection automatically activates this device. Mirrors
936
+ * `motion-trigger` cap registration: drivers set this feature in the
937
+ * same code path that calls `ctx.registerNativeCap(motionTriggerCapability, ...)`.
938
+ *
939
+ * Used by admin UI (gate the in-hero `MotionTriggerToggle` against a
940
+ * fast scalar without binding fetch), notifier rules, and `listAll`
941
+ * filters that want "all devices with on-motion behaviour".
942
+ */
943
+ DeviceFeature["MotionTrigger"] = "motion-trigger";
944
+ /** Light supports rgb-triplet color via `color` cap. */
945
+ DeviceFeature["LightColorRgb"] = "light-color-rgb";
946
+ /** Light supports HSV color via `color` cap. */
947
+ DeviceFeature["LightColorHsv"] = "light-color-hsv";
948
+ /** Light supports color-temperature (mired) via `color` cap. */
949
+ DeviceFeature["LightColorMired"] = "light-color-mired";
950
+ /** Thermostat supports a `heat_cool` dual setpoint (targetLow +
951
+ * targetHigh). Gates the range slider UI. */
952
+ DeviceFeature["ClimateDualSetpoint"] = "climate-dual-setpoint";
953
+ /** Thermostat exposes target humidity and/or current humidity
954
+ * readings. Gates the humidity controls. */
955
+ DeviceFeature["ClimateHumidity"] = "climate-humidity";
956
+ /** Thermostat exposes a fan-mode selector. */
957
+ DeviceFeature["ClimateFanMode"] = "climate-fan-mode";
958
+ /** Thermostat exposes preset modes (eco / away / sleep / vendor). */
959
+ DeviceFeature["ClimatePreset"] = "climate-preset";
960
+ /** Thermostat exposes a vertical louver swing toggle. Gates the
961
+ * vertical-swing switch in the climate UI. Independent of horizontal. */
962
+ DeviceFeature["ClimateSwingVertical"] = "climate-swing-vertical";
963
+ /** Thermostat exposes a horizontal louver swing toggle. Gates the
964
+ * horizontal-swing switch in the climate UI. Independent of vertical. */
965
+ DeviceFeature["ClimateSwingHorizontal"] = "climate-swing-horizontal";
966
+ /** Cover exposes intermediate position control (0..100). Gates the
967
+ * position slider UI. */
968
+ DeviceFeature["CoverPositionable"] = "cover-positionable";
969
+ /** Cover exposes slat-tilt control. Gates the tilt slider UI. */
970
+ DeviceFeature["CoverTilt"] = "cover-tilt";
971
+ /** Valve exposes intermediate position control (0..100). Gates the
972
+ * position slider / drag surface UI. */
973
+ DeviceFeature["ValvePositionable"] = "valve-positionable";
974
+ /** Fan exposes a speed-percentage setter. Gates the speed slider UI. */
975
+ DeviceFeature["FanSpeed"] = "fan-speed";
976
+ /** Fan exposes a preset mode selector. */
977
+ DeviceFeature["FanPreset"] = "fan-preset";
978
+ /** Fan exposes blade direction (forward/reverse) — typical of
979
+ * ceiling fans. */
980
+ DeviceFeature["FanDirection"] = "fan-direction";
981
+ /** Fan exposes an oscillation toggle. */
982
+ DeviceFeature["FanOscillating"] = "fan-oscillating";
983
+ /** Lock requires a PIN code on lock/unlock. Gates the code-entry
984
+ * field on the UI lock-controls panel. */
985
+ DeviceFeature["LockPinRequired"] = "lock-pin-required";
986
+ /** Lock supports a latch-release ("open door") action distinct from
987
+ * unlock. Mirrors HA `LockEntityFeature.OPEN` (bit 1) in
988
+ * `supported_features`. Gates the Open Door button in the UI. */
989
+ DeviceFeature["LockOpen"] = "lock-open";
990
+ /** Media player exposes a seek-to-position surface. */
991
+ DeviceFeature["MediaPlayerSeek"] = "media-player-seek";
992
+ /** Media player exposes a volume-level setter. */
993
+ DeviceFeature["MediaPlayerVolume"] = "media-player-volume";
994
+ /** Media player exposes a mute toggle distinct from volume=0. */
995
+ DeviceFeature["MediaPlayerMute"] = "media-player-mute";
996
+ /** Media player exposes a shuffle toggle. */
997
+ DeviceFeature["MediaPlayerShuffle"] = "media-player-shuffle";
998
+ /** Media player exposes a repeat mode (off / all / one). */
999
+ DeviceFeature["MediaPlayerRepeat"] = "media-player-repeat";
1000
+ /** Media player exposes a source / input selector. */
1001
+ DeviceFeature["MediaPlayerSelectSource"] = "media-player-select-source";
1002
+ /** Media player exposes a play-arbitrary-media surface (URL / id). */
1003
+ DeviceFeature["MediaPlayerPlayMedia"] = "media-player-play-media";
1004
+ /** Media player exposes next-track. */
1005
+ DeviceFeature["MediaPlayerNext"] = "media-player-next";
1006
+ /** Media player exposes previous-track. */
1007
+ DeviceFeature["MediaPlayerPrevious"] = "media-player-previous";
1008
+ /** Media player exposes stop distinct from pause. */
1009
+ DeviceFeature["MediaPlayerStop"] = "media-player-stop";
1010
+ /** Alarm panel requires a PIN code on arm/disarm. */
1011
+ DeviceFeature["AlarmPinRequired"] = "alarm-pin-required";
1012
+ /** Presence device carries GPS coordinates (lat/lng/accuracy) in
1013
+ * addition to a textual location. */
1014
+ DeviceFeature["PresenceGps"] = "presence-gps";
1015
+ /** Notifier accepts an inline / URL image attachment. */
1016
+ DeviceFeature["NotifierImage"] = "notifier-image";
1017
+ /** Notifier accepts a priority hint (high/normal/low). */
1018
+ DeviceFeature["NotifierPriority"] = "notifier-priority";
1019
+ /** Notifier accepts a free-form `data` payload for platform-specific
1020
+ * fields. */
1021
+ DeviceFeature["NotifierData"] = "notifier-data";
1022
+ /** Notifier supports interactive action buttons / callbacks. */
1023
+ DeviceFeature["NotifierActions"] = "notifier-actions";
1024
+ /** Notifier supports per-call recipient targeting (multi-user). */
1025
+ DeviceFeature["NotifierRecipients"] = "notifier-recipients";
1026
+ /** Script runner accepts a variables map on each run invocation. */
1027
+ DeviceFeature["ScriptVariables"] = "script-variables";
1028
+ /** Automation `trigger` accepts a skipCondition flag — fires the
1029
+ * automation's actions while bypassing its condition block. */
1030
+ DeviceFeature["AutomationSkipCondition"] = "automation-skip-condition";
1031
+ /** Robot vacuum exposes a live cleaning map (image child). Gates the
1032
+ * map tile in the vacuum UI. */
1033
+ DeviceFeature["VacuumHasMap"] = "vacuum-has-map";
1034
+ /** Robot vacuum exposes AI obstacle-detection toggles. Gates the AI
1035
+ * switches group. */
1036
+ DeviceFeature["VacuumHasAi"] = "vacuum-has-ai";
1037
+ /** Robot mower exposes a live mowing map (SVG image child). Gates the
1038
+ * mower map tile. */
1039
+ DeviceFeature["MowerHasMap"] = "mower-has-map";
1040
+ /** Robot mower exposes targeted mowing (all-area / zones / edges /
1041
+ * spots) — gates the Mow action + map selectors. */
1042
+ DeviceFeature["MowerHasTargetedMowing"] = "mower-has-targeted-mowing";
1043
+ return DeviceFeature;
1044
+ }({});
1045
+ /**
1046
+ * Semantic role a device plays within its parent. Populated by driver
1047
+ * addons when creating accessory devices (Reolink siren/floodlight/
1048
+ * PIR/chime/autotrack/doorbell, ONVIF relay outputs, …). Used by the
1049
+ * admin UI to pick icons, labels, and widgets — a `Switch` with
1050
+ * `role: Floodlight` renders as a bulb with a brightness slider,
1051
+ * whereas a `Switch` with `role: Siren` renders as a klaxon.
1052
+ *
1053
+ * Undefined for top-level devices (cameras, NVRs, hubs). Persisted in
1054
+ * sqlite as a nullable TEXT column — old rows keep working unchanged.
1055
+ */
1056
+ var DeviceRole = /* @__PURE__ */ function(DeviceRole) {
1057
+ DeviceRole["Siren"] = "siren";
1058
+ DeviceRole["Floodlight"] = "floodlight";
1059
+ DeviceRole["Spotlight"] = "spotlight";
1060
+ DeviceRole["PirSensor"] = "pir-sensor";
1061
+ DeviceRole["Chime"] = "chime";
1062
+ DeviceRole["Autotrack"] = "autotrack";
1063
+ DeviceRole["Nightvision"] = "nightvision";
1064
+ DeviceRole["PrivacyMask"] = "privacy-mask";
1065
+ DeviceRole["Doorbell"] = "doorbell";
1066
+ /** Interior blind / shade / roller blind. HA `device_class: blind|shade`. */
1067
+ DeviceRole["CoverBlind"] = "cover-blind";
1068
+ /** Exterior roller shutter. HA `device_class: shutter`; Homematic BROLL. */
1069
+ DeviceRole["CoverShutter"] = "cover-shutter";
1070
+ /** Exterior awning. HA `device_class: awning`. */
1071
+ DeviceRole["CoverAwning"] = "cover-awning";
1072
+ /** Curtain / drape. HA `device_class: curtain`. */
1073
+ DeviceRole["CoverCurtain"] = "cover-curtain";
1074
+ /** Garage door. HA `device_class: garage`. A security actuator: exports gate it. */
1075
+ DeviceRole["CoverGarage"] = "cover-garage";
1076
+ /** Gate. HA `device_class: gate`. A security actuator: exports gate it. */
1077
+ DeviceRole["CoverGate"] = "cover-gate";
1078
+ /** Door. HA `device_class: door`. A security actuator: exports gate it. */
1079
+ DeviceRole["CoverDoor"] = "cover-door";
1080
+ /** Window. HA `device_class: window`. */
1081
+ DeviceRole["CoverWindow"] = "cover-window";
1082
+ /** Virtual HA toggle (input_boolean.*) — distinguishable from a
1083
+ * real Switch device for UI rendering / export adapters. */
1084
+ DeviceRole["BinaryHelper"] = "binary-helper";
1085
+ /** A smart plug: a `switch` whose source says it is an outlet (HA
1086
+ * `device_class: outlet`). The Alexa export shows it as a SMARTPLUG. */
1087
+ DeviceRole["Outlet"] = "outlet";
1088
+ /** Generic motion / occupancy / moving event source. Distinct from
1089
+ * the camera accessory PirSensor role: that one is a camera child;
1090
+ * this is a standalone HA / 3rd-party motion sensor. */
1091
+ DeviceRole["MotionSensor"] = "motion-sensor";
1092
+ DeviceRole["ContactSensor"] = "contact-sensor";
1093
+ DeviceRole["LeakSensor"] = "leak-sensor";
1094
+ DeviceRole["SmokeSensor"] = "smoke-sensor";
1095
+ DeviceRole["COSensor"] = "co-sensor";
1096
+ DeviceRole["GasSensor"] = "gas-sensor";
1097
+ DeviceRole["TamperSensor"] = "tamper-sensor";
1098
+ DeviceRole["VibrationSensor"] = "vibration-sensor";
1099
+ DeviceRole["ConnectivitySensor"] = "connectivity-sensor";
1100
+ DeviceRole["SoundSensor"] = "sound-sensor";
1101
+ /** Fallback for `binary_sensor` without a known `device_class`. */
1102
+ DeviceRole["BinarySensor"] = "binary-sensor";
1103
+ DeviceRole["TemperatureSensor"] = "temperature-sensor";
1104
+ DeviceRole["HumiditySensor"] = "humidity-sensor";
1105
+ DeviceRole["AmbientLightSensor"] = "ambient-light-sensor";
1106
+ DeviceRole["PressureSensor"] = "pressure-sensor";
1107
+ /** Wind speed or direction (weather-station `wind-sensor` cap). */
1108
+ DeviceRole["WindSensor"] = "wind-sensor";
1109
+ /** Rain accumulation or rate (weather-station `rain-sensor` cap). */
1110
+ DeviceRole["RainSensor"] = "rain-sensor";
1111
+ /** UV index (weather-station `uv-sensor` cap). */
1112
+ DeviceRole["UvSensor"] = "uv-sensor";
1113
+ /** Solar irradiance W/m² (weather-station `solar-radiation-sensor` cap).
1114
+ * Distinct from AmbientLightSensor (lux). */
1115
+ DeviceRole["SolarRadiationSensor"] = "solar-radiation-sensor";
1116
+ /** Soil moisture % (garden/weather `soil-moisture-sensor` cap). */
1117
+ DeviceRole["SoilMoistureSensor"] = "soil-moisture-sensor";
1118
+ DeviceRole["PowerSensor"] = "power-sensor";
1119
+ DeviceRole["EnergySensor"] = "energy-sensor";
1120
+ DeviceRole["VoltageSensor"] = "voltage-sensor";
1121
+ DeviceRole["CurrentSensor"] = "current-sensor";
1122
+ DeviceRole["AirQualitySensor"] = "air-quality-sensor";
1123
+ /** Battery level (numeric % via `sensor` OR low-bool via
1124
+ * `binary_sensor` — the cap distinguishes via the value type). */
1125
+ DeviceRole["BatterySensor"] = "battery-sensor";
1126
+ /** Fallback for `sensor` numeric without a known `device_class`. */
1127
+ DeviceRole["NumericSensor"] = "numeric-sensor";
1128
+ /** String / enum state (HA `sensor` with `state_class: enum` or
1129
+ * `attributes.options`). */
1130
+ DeviceRole["EnumSensor"] = "enum-sensor";
1131
+ /** Date / timestamp state (HA `sensor` with `device_class: timestamp`
1132
+ * or `date`). The slice carries the raw ISO string verbatim (hosted on
1133
+ * the `enum-sensor` cap); the UI renders it locale-formatted. */
1134
+ DeviceRole["DateTimeSensor"] = "datetime-sensor";
1135
+ /** Last-resort fallback when nothing else matches. */
1136
+ DeviceRole["GenericSensor"] = "generic-sensor";
1137
+ DeviceRole["NumericControl"] = "numeric-control";
1138
+ DeviceRole["SelectControl"] = "select-control";
1139
+ DeviceRole["TextControl"] = "text-control";
1140
+ DeviceRole["DateTimeControl"] = "datetime-control";
1141
+ /** Mobile push notifier (HA `notify.mobile_app_*`) — supports
1142
+ * rich features (image, priority, channel routing). */
1143
+ DeviceRole["MobilePushNotifier"] = "mobile-push-notifier";
1144
+ /** Chat / messaging service (HA `notify.telegram_*`,
1145
+ * `notify.discord_*`, etc.). */
1146
+ DeviceRole["MessagingNotifier"] = "messaging-notifier";
1147
+ /** Email-based delivery (HA `notify.smtp`, etc.). */
1148
+ DeviceRole["EmailNotifier"] = "email-notifier";
1149
+ /** Fallback when the notifier service name doesn't match a known
1150
+ * pattern. */
1151
+ DeviceRole["GenericNotifier"] = "generic-notifier";
1152
+ /** Emits a pulse train (infrared or RF) on request — the `ir-transmitter`
1153
+ * cap. A `DeviceType.Generic` device. */
1154
+ DeviceRole["IrTransmitter"] = "ir-transmitter";
1155
+ return DeviceRole;
1156
+ }({});
1157
+ //#endregion
782
1158
  //#region ../../node_modules/zod/v4/core/core.js
783
1159
  var _a$1;
784
1160
  function $constructor(name, initializer, params) {
@@ -5457,7 +5833,7 @@ var ZodIssueCode = {
5457
5833
  var ZodFirstPartyTypeKind;
5458
5834
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5459
5835
  //#endregion
5460
- //#region ../types/dist/sleep-DSVWZfJC.mjs
5836
+ //#region ../types/dist/sleep-CSYuxssh.mjs
5461
5837
  /**
5462
5838
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5463
5839
  * window to float samples (D455).
@@ -5973,6 +6349,102 @@ function emitReadiness(bus, params) {
5973
6349
  ts
5974
6350
  }));
5975
6351
  }
6352
+ var NOT_RUN = {
6353
+ declared: false,
6354
+ snapshot: null,
6355
+ applied: [],
6356
+ failed: [],
6357
+ blocked: [],
6358
+ skipped: [],
6359
+ blockOwner: []
6360
+ };
6361
+ /**
6362
+ * Offer `migrations` to the engine and run what it says to run, in its order. Never
6363
+ * throws: an unreachable or refusing engine is logged and reported `declared: false`,
6364
+ * because the hub (and this addon's boot) must not hang on a migration.
6365
+ */
6366
+ async function runAddonMigrations(host, migrations, logger) {
6367
+ const first = migrations[0];
6368
+ if (first === void 0) return NOT_RUN;
6369
+ const owner = first.meta.owner;
6370
+ const log = logger.withTags({ migrationOwner: owner });
6371
+ const byId = new Map(migrations.map((m) => [m.meta.id, m]));
6372
+ let declaration;
6373
+ try {
6374
+ declaration = await host.declareOwner({
6375
+ owner,
6376
+ metas: migrations.map((m) => m.meta)
6377
+ });
6378
+ } catch (err) {
6379
+ log.error("the migration engine refused or could not take this addon catalogue — none ran", { meta: { error: errMsg(err) } });
6380
+ return {
6381
+ ...NOT_RUN,
6382
+ blockOwner: migrations.filter((m) => m.meta.onFailure === "block-owner").map((m) => m.meta.id)
6383
+ };
6384
+ }
6385
+ const applied = [];
6386
+ const failed = [];
6387
+ const blocked = [];
6388
+ const skipped = [];
6389
+ const settle = (id, status) => {
6390
+ if (status === "applied") applied.push(id);
6391
+ else if (status === "failed") failed.push(id);
6392
+ else if (status === "blocked") blocked.push(id);
6393
+ else skipped.push(id);
6394
+ };
6395
+ for (const step of declaration.steps) {
6396
+ const bound = byId.get(step.id);
6397
+ if (bound === void 0 || step.action === "skip") {
6398
+ skipped.push(step.id);
6399
+ continue;
6400
+ }
6401
+ const tags = { migrationId: step.id };
6402
+ try {
6403
+ const gate = await host.startMigration({ id: step.id });
6404
+ if (!gate.proceed) {
6405
+ log.warn("migration not run", {
6406
+ tags,
6407
+ meta: {
6408
+ state: gate.state,
6409
+ reason: gate.reason
6410
+ }
6411
+ });
6412
+ settle(step.id, gate.state === "pending" ? "blocked" : gate.state);
6413
+ continue;
6414
+ }
6415
+ const result = await bound.execute();
6416
+ const outcome = await host.finishMigration({
6417
+ id: step.id,
6418
+ result
6419
+ });
6420
+ if (outcome.status === "applied") log.info("migration applied", {
6421
+ tags,
6422
+ meta: { rowsChanged: outcome.rowsChanged }
6423
+ });
6424
+ else log.error("migration failed", {
6425
+ tags,
6426
+ meta: { error: outcome.error }
6427
+ });
6428
+ settle(step.id, outcome.status);
6429
+ } catch (err) {
6430
+ log.error("migration could not be completed with the engine", {
6431
+ tags,
6432
+ meta: { error: errMsg(err) }
6433
+ });
6434
+ failed.push(step.id);
6435
+ }
6436
+ }
6437
+ const blockOwner = [...failed, ...blocked].filter((id) => byId.get(id)?.meta.onFailure === "block-owner");
6438
+ return {
6439
+ declared: true,
6440
+ snapshot: declaration.snapshot,
6441
+ applied,
6442
+ failed,
6443
+ blocked,
6444
+ skipped,
6445
+ blockOwner
6446
+ };
6447
+ }
5976
6448
  /**
5977
6449
  * Build a {@link DurableState} over a single store key. Transport-agnostic:
5978
6450
  * `read`/`write` are the addon-store or device-store accessors. The whole
@@ -7028,6 +7500,28 @@ var BaseAddon = class {
7028
7500
  return normalized;
7029
7501
  }
7030
7502
  /**
7503
+ * Run this addon's migration catalogue (D987 M3) through the hub's `migrations`
7504
+ * engine. Call it at the point of the boot where the data must already have the new
7505
+ * shape — `onInitialize`, or `onRestoreDevices` for a migration that needs the
7506
+ * restored devices — with the catalogue bound to the addon's ports
7507
+ * (`defineAddonMigrations(ADDON_MIGRATIONS, ports)`).
7508
+ *
7509
+ * An engine that cannot be reached or refuses the catalogue is logged and runs
7510
+ * nothing; the boot continues. A migration that FAILED (or was blocked by one)
7511
+ * and declares `onFailure: 'block-owner'` makes this throw, so the addon does not
7512
+ * serve data whose shape it cannot trust; `tolerate` migrations never do.
7513
+ */
7514
+ async runMigrations(migrations) {
7515
+ const api = this.ctx.api.migrations;
7516
+ const report = await runAddonMigrations({
7517
+ declareOwner: (input) => api.declareOwner.mutate(input),
7518
+ startMigration: (input) => api.startMigration.mutate(input),
7519
+ finishMigration: (input) => api.finishMigration.mutate(input)
7520
+ }, migrations, this.ctx.logger);
7521
+ 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`);
7522
+ return report;
7523
+ }
7524
+ /**
7031
7525
  * Called by the isolated-process runner AFTER `broker.start()`.
7032
7526
  * In-process addons never need this because the hub broker is already
7033
7527
  * running when `initialize()` fires. For forked children the broker
@@ -8137,407 +8631,6 @@ var RecordKindDeclarationSchema = object({
8137
8631
  }).strict();
8138
8632
  /** The kind names: lower-case, dash separated, short enough for an index name. */
8139
8633
  var RecordKindNameSchema = string().regex(/^[a-z][a-z0-9-]{0,47}$/);
8140
- var READING_QUANTITIES = [
8141
- "temperature",
8142
- "pressure",
8143
- "speed",
8144
- "precipitation",
8145
- "precipitation-rate",
8146
- "length",
8147
- "area",
8148
- "illuminance",
8149
- "irradiance",
8150
- "power",
8151
- "energy",
8152
- "voltage",
8153
- "current",
8154
- "apparent-power",
8155
- "humidity",
8156
- "percentage",
8157
- "concentration-volume",
8158
- "concentration-mass",
8159
- "concentration-ppb",
8160
- "volume",
8161
- "duration",
8162
- "duration-ms",
8163
- "information",
8164
- "sound-level",
8165
- "signal-level",
8166
- "colour-temperature",
8167
- "angle",
8168
- "bitrate",
8169
- "rate",
8170
- "mass",
8171
- "index",
8172
- "ratio",
8173
- "count",
8174
- "none",
8175
- "device"
8176
- ];
8177
- var ReadingQuantitySchema = _enum(READING_QUANTITIES);
8178
- new Set(READING_QUANTITIES);
8179
- /**
8180
- * Stored unit symbol for a quantity. `none` means the number is dimensionless
8181
- * (or an index / ratio / count). `device` means the cap has no single unit.
8182
- */
8183
- var QUANTITY_BASE = {
8184
- temperature: "°C",
8185
- pressure: "hPa",
8186
- speed: "m/s",
8187
- precipitation: "mm",
8188
- "precipitation-rate": "mm/h",
8189
- length: "m",
8190
- area: "m²",
8191
- illuminance: "lx",
8192
- irradiance: "W/m²",
8193
- power: "W",
8194
- energy: "kWh",
8195
- voltage: "V",
8196
- current: "A",
8197
- "apparent-power": "VA",
8198
- humidity: "%",
8199
- percentage: "%",
8200
- "concentration-volume": "ppm",
8201
- "concentration-mass": "µg/m³",
8202
- "concentration-ppb": "ppb",
8203
- volume: "L",
8204
- duration: "s",
8205
- "duration-ms": "ms",
8206
- information: "B",
8207
- "sound-level": "dBFS",
8208
- "signal-level": "dBm",
8209
- "colour-temperature": "mired",
8210
- angle: "°",
8211
- bitrate: "kbps",
8212
- rate: "fps",
8213
- mass: "kg",
8214
- index: "none",
8215
- ratio: "none",
8216
- count: "none",
8217
- none: "none",
8218
- device: "device"
8219
- };
8220
- /** The stored unit, or `undefined` when the quantity has no unit symbol. */
8221
- function baseUnitOf(quantity) {
8222
- const symbol = QUANTITY_BASE[quantity];
8223
- if (symbol === "none" || symbol === "device") return void 0;
8224
- return symbol;
8225
- }
8226
- function readingMeta(quantity, extras) {
8227
- return {
8228
- quantity,
8229
- ...extras?.precision !== void 0 ? { precision: extras.precision } : {},
8230
- ...extras?.label !== void 0 ? { label: extras.label } : {}
8231
- };
8232
- }
8233
- /** Shared Zod schemas used across streaming capabilities. */
8234
- var CamProfileSchema = _enum([
8235
- "high",
8236
- "mid",
8237
- "low"
8238
- ]);
8239
- /**
8240
- * Which rung a per-camera consumer listens on: an explicit profile, or `auto`.
8241
- *
8242
- * `auto` is a REAL value, not an absent one — it means "resolve it by the rule
8243
- * that consumer owns". It lives beside `CamProfile` because a rung is the only
8244
- * operator-facing way to name a stream: a cam-stream id is broker-internal and
8245
- * moves when a slot is reassigned, so a stored id silently stops meaning what
8246
- * the operator picked. Today's one user is the audio analyser, whose rule is
8247
- * "the lowest AVAILABLE native rung" (D537).
8248
- */
8249
- var AudioStreamPickSchema = union([CamProfileSchema, literal("auto")]);
8250
- var CamStreamKindSchema = _enum([
8251
- "pull-rtsp",
8252
- "pull-rtmp",
8253
- "pull-http",
8254
- "pull-flv",
8255
- "pull-rfc4571",
8256
- "push-annexb",
8257
- "derived"
8258
- ]);
8259
- var CamStreamResolutionSchema = object({
8260
- width: number().int().positive().meta(readingMeta("none")),
8261
- height: number().int().positive().meta(readingMeta("none"))
8262
- });
8263
- var CameraStreamSchema = object({
8264
- /** Stable, provider-assigned id unique within the (deviceId) scope. */
8265
- camStreamId: string().min(1),
8266
- deviceId: number().int().nonnegative(),
8267
- kind: CamStreamKindSchema,
8268
- /** Required for pull-* kinds. Ignored for push-annexb. */
8269
- url: string().optional(),
8270
- codec: string().optional(),
8271
- resolution: CamStreamResolutionSchema.optional(),
8272
- fps: number().positive().optional(),
8273
- /** Human label surfaced in the Admin UI "Camera Stream" dropdown. */
8274
- label: string().optional(),
8275
- /**
8276
- * Device-level features the publisher advertised (e.g. `battery-operated`).
8277
- * The broker, snapshot orchestrator, and prebuffer manager all consult
8278
- * this list to derive policy — relaxed stall watchdog for battery
8279
- * cams, prebuffer off by default, longer snapshot rate-limit, etc.
8280
- *
8281
- * Single source of truth replacing per-stream flags like the
8282
- * historical `allowStall`: if the publisher knows the camera is
8283
- * battery-powered, every downstream service derives the right policy
8284
- * from this list.
8285
- */
8286
- deviceFeatures: array(string()).optional(),
8287
- /**
8288
- * Whether this stream participates in the broker's automatic profile
8289
- * assignment (`computeInitialAssignment`). Defaults to `true`. Publishers
8290
- * use `false` when they want a stream to be SELECTABLE in the UI but not
8291
- * picked by default — e.g. Reolink publishes its native Baichuan streams
8292
- * as `autoEligible: true` (the recommended path) and its RTSP / RTMP
8293
- * mirrors as `autoEligible: false` (still pickable per slot, just not
8294
- * the auto choice). Manual `assignProfile` calls remain valid for
8295
- * non-eligible streams.
8296
- */
8297
- autoEligible: boolean().optional(),
8298
- /**
8299
- * The profile slot the PUBLISHER says this stream is: `low` for a stream
8300
- * whose only sensible role is the low-bandwidth one, `high` for a main
8301
- * stream. Consulted by the broker's automatic ranking
8302
- * (`rankInitialAssignment`) BEFORE the pixel-count heuristic, and only there:
8303
- * an operator's manual assignment still wins, and an absent hint is the
8304
- * pixel-ranked default every camera had before this field existed.
8305
- *
8306
- * Why it exists: a camera with ONE stream ranked to `mid` — the slot no
8307
- * storage class is written for on its own (`recordings` is high|mid,
8308
- * `recordingsLow` is low) and the one profile scrub does not read. The robot
8309
- * camera's single 720p relay is a `low` in every respect except the slot the
8310
- * heuristic put it in.
8311
- */
8312
- profileHint: CamProfileSchema.optional(),
8313
- /**
8314
- * Transport-specific opaque metadata. The broker passes it through to
8315
- * the source reader without inspecting it. Currently used by
8316
- * `pull-rfc4571` streams to carry the upstream SDP (so the reader can
8317
- * route RTP packets to the right depacketizer without an in-band
8318
- * DESCRIBE phase). Other kinds typically leave it undefined.
8319
- */
8320
- metadata: record(string(), unknown()).optional()
8321
- });
8322
- var ProfileSlotStatusSchema = _enum([
8323
- "unassigned",
8324
- "idle",
8325
- "connecting",
8326
- "streaming",
8327
- "error"
8328
- ]);
8329
- var ProfileSlotSchema = object({
8330
- deviceId: number().int().nonnegative(),
8331
- profile: CamProfileSchema,
8332
- /** Broker id the rest of the system addresses: `${deviceId}/${profile}`. */
8333
- brokerId: string(),
8334
- /** `null` when the profile is unassigned. */
8335
- sourceCamStreamId: string().nullable(),
8336
- status: ProfileSlotStatusSchema,
8337
- resolution: CamStreamResolutionSchema.optional(),
8338
- codec: string().optional(),
8339
- preBufferSec: number().nonnegative().optional(),
8340
- errorMessage: string().optional()
8341
- });
8342
- /**
8343
- * Zod schema for StreamSourceEntry — the canonical stream descriptor
8344
- * exposed by ICameraDevice.getStreamSources() and consumed by the broker.
8345
- */
8346
- var StreamSourceEntrySchema$1 = object({
8347
- id: string(),
8348
- label: string(),
8349
- protocol: _enum([
8350
- "rtsp",
8351
- "rtmp",
8352
- "annexb",
8353
- "http-mjpeg",
8354
- "webrtc",
8355
- "custom"
8356
- ]),
8357
- url: string().optional(),
8358
- resolution: object({
8359
- width: number(),
8360
- height: number()
8361
- }).readonly().optional(),
8362
- fps: number().optional(),
8363
- bitrate: number().optional(),
8364
- codec: string().optional(),
8365
- profileHint: CamProfileSchema.optional()
8366
- });
8367
- object({
8368
- type: string(),
8369
- url: string(),
8370
- videoCodec: string().optional(),
8371
- audioCodec: string().optional(),
8372
- metadata: record(string(), unknown()).readonly().optional()
8373
- });
8374
- /**
8375
- * Wire schema for `FrameHandle` — a zero-pixel, serialisable reference to a
8376
- * frame in a shared-memory ring (Phase 5 / D9). Mirrors the `FrameHandle` TS
8377
- * interface in `interfaces/frame-handle.ts` field-for-field so a cap method
8378
- * (the runner's native-crop and pipeline-executor inputs) can carry handles
8379
- * over tRPC / Moleculer.
8380
- *
8381
- * The `satisfies` assertion below pins this schema to the interface: if a
8382
- * field is added to / removed from `FrameHandle` without a matching schema
8383
- * edit, `z.infer<typeof FrameHandleSchema>` stops being assignable to
8384
- * `FrameHandle` and the build fails — no silent drift.
8385
- */
8386
- var FrameHandleSchema = object({
8387
- shmId: string(),
8388
- slot: number().int().nonnegative(),
8389
- seq: number().int().nonnegative(),
8390
- width: number().int().positive(),
8391
- height: number().int().positive(),
8392
- format: _enum([
8393
- "jpeg",
8394
- "rgb",
8395
- "bgr",
8396
- "yuv420",
8397
- "gray"
8398
- ]),
8399
- pts: number(),
8400
- byteLength: number().int().nonnegative(),
8401
- nodeId: string(),
8402
- slotCount: number().int().positive()
8403
- });
8404
- /**
8405
- * Wire schema for a decoded audio chunk (Phase 5 / D9). Mirrors the
8406
- * `DecodedAudioChunk` TS interface in `interfaces/stream-broker.ts`: PCM
8407
- * sample bytes plus the track parameters a consumer needs to interpret them.
8408
- *
8409
- * Audio chunks are tiny (a ~500ms AudioCodecSession window is a few KB of
8410
- * Float32 PCM), so unlike video frames they ship their bytes INLINE over
8411
- * tRPC / Moleculer — no shared-memory plane. `data` is typed `Uint8Array`
8412
- * (the wire-serialisable supertype of `Buffer`); a `Buffer` is assignable to
8413
- * it.
8414
- */
8415
- var AudioChunkFormatSchema = _enum(AUDIO_CHUNK_FORMATS);
8416
- var DecodedAudioChunkSchema = object({
8417
- data: _instanceof(Uint8Array),
8418
- sampleRate: number().int().positive(),
8419
- channels: number().int().positive(),
8420
- timestamp: number(),
8421
- /**
8422
- * Byte format of `data`. ABSENT MEANS `f32le` — today's bytes, byte for
8423
- * byte, for any peer that never heard of this field. A coded window
8424
- * (`pcmu` / `pcma`, one byte per sample) is only ever emitted to a
8425
- * subscription that DECLARED it accepts one, so absence can never mean
8426
- * "coded bytes a consumer will read as floats" (D455).
8427
- */
8428
- format: AudioChunkFormatSchema.optional()
8429
- });
8430
- /**
8431
- * Input for `stream-broker.subscribeAudioChunks` (Phase 5 / D9). Audio ships
8432
- * its bytes inline: a consumer asks the broker for the decoded audio-chunk
8433
- * stream of one broker; the broker keeps a per-subscription bounded FIFO queue
8434
- * the consumer drains via `pullAudioChunks`.
8435
- */
8436
- var SubscribeAudioChunksInputSchema = object({
8437
- brokerId: string(),
8438
- /** Short caller-identity tag (`audio-analyzer`, …) for `listClients`. */
8439
- tag: string().optional(),
8440
- /**
8441
- * Byte formats this subscriber can READ, best first. The broker serves the
8442
- * chunk's own format when it is in this list and expands to `f32le`
8443
- * otherwise, so a subscriber is never handed bytes it cannot interpret.
8444
- *
8445
- * Absent (or without the source format) means `f32le` — the behaviour every
8446
- * subscriber had before D455, unchanged. This is the negotiation half of
8447
- * the source-bytes lever: it is what lets the broker and its consumers
8448
- * deploy one at a time across three nodes.
8449
- */
8450
- accept: array(AudioChunkFormatSchema).readonly().optional()
8451
- });
8452
- /** Result of `stream-broker.subscribeAudioChunks`. */
8453
- var SubscribeAudioChunksResultSchema = object({
8454
- /** Opaque id passed to `pullAudioChunks` / `unsubscribeAudioChunks`. */
8455
- subscriptionId: string() });
8456
- var BrokerStatsSchema = object({
8457
- status: _enum([
8458
- "idle",
8459
- "connecting",
8460
- "streaming",
8461
- "error",
8462
- "stopped"
8463
- ]),
8464
- inputFps: number(),
8465
- encodedSubscribers: number(),
8466
- uptimeMs: number(),
8467
- bitrateKbps: number(),
8468
- idrIntervalMs: number(),
8469
- codec: string().optional(),
8470
- totalBytes: number(),
8471
- packetCount: number(),
8472
- rtspClients: number(),
8473
- pipeClients: number(),
8474
- preBufferSec: number(),
8475
- preBufferMs: number(),
8476
- preBufferPackets: number(),
8477
- /**
8478
- * Detected audio track parameters from the RTSP DESCRIBE / SDP. `null`
8479
- * when the stream has no audio track or the broker is in cold start.
8480
- * `supported = false` means the codec was detected but the local
8481
- * decoder pipeline cannot produce PCM chunks (e.g. AAC without the
8482
- * AAC pipeline wired). Surfaced in the UI device-overview so operators
8483
- * can pre-pick the audio-analysis model that matches the codec.
8484
- */
8485
- audio: object({
8486
- codec: string(),
8487
- sampleRate: number(),
8488
- channels: number(),
8489
- supported: boolean()
8490
- }).nullable().optional(),
8491
- /**
8492
- * BROKER-SIDE AUDIO MUTE (D83). `true` = this broker is deliberately
8493
- * distributing none of the device's audio, on live or recording.
8494
- *
8495
- * Present so a silent camera can be told apart from a broken one on the
8496
- * stream panel itself, without cross-referencing the switch group: a
8497
- * broker holding an `audio` track descriptor while `audioMuted` is true is
8498
- * working exactly as asked. `audioMutedDropped` counts the audio units
8499
- * thrown away since the current dial — it is how you confirm from stats
8500
- * alone that the mute is on the packet path and not merely persisted.
8501
- */
8502
- audioMuted: boolean().optional(),
8503
- audioMutedDropped: number().optional()
8504
- });
8505
- /**
8506
- * Exporter-facing "profile restream" entry. Returned by
8507
- * `cameraStreams.getProfileRtspEntries` — one per ASSIGNED profile slot
8508
- * (high/mid/low). `brokerId` is the PROFILE-keyed broker id
8509
- * (`${deviceId}/${profile}`, e.g. `15/high`); its `url` is a broker
8510
- * RTSP restream that aliases the profile's assigned source broker, so
8511
- * every consumer dialling a profile converges on the same single pull.
8512
- * `codec`/`resolution` describe the assigned SOURCE camStream (for the
8513
- * `auto` resolution-closest pick). Distinct from `RtspRestreamEntry`
8514
- * (raw per-camStream, live-view only).
8515
- */
8516
- var ProfileRtspEntrySchema = object({
8517
- profile: CamProfileSchema,
8518
- /** Profile-keyed broker id, format `${deviceId}/${profile}` (e.g. `"15/high"`). */
8519
- brokerId: string(),
8520
- url: string(),
8521
- mutedUrl: string(),
8522
- enabled: boolean(),
8523
- codec: string().optional(),
8524
- resolution: CamStreamResolutionSchema.optional()
8525
- });
8526
- /**
8527
- * The model weight formats a catalog entry can ship. Schema-free on purpose:
8528
- * `types/models.ts` builds Zod schemas from it, but a runner-closure consumer
8529
- * (the model downloader in `@camstack/system`) needs only the list and must
8530
- * reach it through `@camstack/types/addon` without building the cap schema
8531
- * graph (D28).
8532
- */
8533
- var MODEL_FORMATS = [
8534
- "onnx",
8535
- "coreml",
8536
- "openvino",
8537
- "tflite",
8538
- "pt",
8539
- "gguf"
8540
- ];
8541
8634
  object({
8542
8635
  field: string().min(1),
8543
8636
  on: literal("advance")
@@ -8630,6 +8723,407 @@ function optionalMethod(input, output, options) {
8630
8723
  function event(data) {
8631
8724
  return { data };
8632
8725
  }
8726
+ var READING_QUANTITIES = [
8727
+ "temperature",
8728
+ "pressure",
8729
+ "speed",
8730
+ "precipitation",
8731
+ "precipitation-rate",
8732
+ "length",
8733
+ "area",
8734
+ "illuminance",
8735
+ "irradiance",
8736
+ "power",
8737
+ "energy",
8738
+ "voltage",
8739
+ "current",
8740
+ "apparent-power",
8741
+ "humidity",
8742
+ "percentage",
8743
+ "concentration-volume",
8744
+ "concentration-mass",
8745
+ "concentration-ppb",
8746
+ "volume",
8747
+ "duration",
8748
+ "duration-ms",
8749
+ "information",
8750
+ "sound-level",
8751
+ "signal-level",
8752
+ "colour-temperature",
8753
+ "angle",
8754
+ "bitrate",
8755
+ "rate",
8756
+ "mass",
8757
+ "index",
8758
+ "ratio",
8759
+ "count",
8760
+ "none",
8761
+ "device"
8762
+ ];
8763
+ var ReadingQuantitySchema = _enum(READING_QUANTITIES);
8764
+ new Set(READING_QUANTITIES);
8765
+ /**
8766
+ * Stored unit symbol for a quantity. `none` means the number is dimensionless
8767
+ * (or an index / ratio / count). `device` means the cap has no single unit.
8768
+ */
8769
+ var QUANTITY_BASE = {
8770
+ temperature: "°C",
8771
+ pressure: "hPa",
8772
+ speed: "m/s",
8773
+ precipitation: "mm",
8774
+ "precipitation-rate": "mm/h",
8775
+ length: "m",
8776
+ area: "m²",
8777
+ illuminance: "lx",
8778
+ irradiance: "W/m²",
8779
+ power: "W",
8780
+ energy: "kWh",
8781
+ voltage: "V",
8782
+ current: "A",
8783
+ "apparent-power": "VA",
8784
+ humidity: "%",
8785
+ percentage: "%",
8786
+ "concentration-volume": "ppm",
8787
+ "concentration-mass": "µg/m³",
8788
+ "concentration-ppb": "ppb",
8789
+ volume: "L",
8790
+ duration: "s",
8791
+ "duration-ms": "ms",
8792
+ information: "B",
8793
+ "sound-level": "dBFS",
8794
+ "signal-level": "dBm",
8795
+ "colour-temperature": "mired",
8796
+ angle: "°",
8797
+ bitrate: "kbps",
8798
+ rate: "fps",
8799
+ mass: "kg",
8800
+ index: "none",
8801
+ ratio: "none",
8802
+ count: "none",
8803
+ none: "none",
8804
+ device: "device"
8805
+ };
8806
+ /** The stored unit, or `undefined` when the quantity has no unit symbol. */
8807
+ function baseUnitOf(quantity) {
8808
+ const symbol = QUANTITY_BASE[quantity];
8809
+ if (symbol === "none" || symbol === "device") return void 0;
8810
+ return symbol;
8811
+ }
8812
+ function readingMeta(quantity, extras) {
8813
+ return {
8814
+ quantity,
8815
+ ...extras?.precision !== void 0 ? { precision: extras.precision } : {},
8816
+ ...extras?.label !== void 0 ? { label: extras.label } : {}
8817
+ };
8818
+ }
8819
+ /** Shared Zod schemas used across streaming capabilities. */
8820
+ var CamProfileSchema = _enum([
8821
+ "high",
8822
+ "mid",
8823
+ "low"
8824
+ ]);
8825
+ /**
8826
+ * Which rung a per-camera consumer listens on: an explicit profile, or `auto`.
8827
+ *
8828
+ * `auto` is a REAL value, not an absent one — it means "resolve it by the rule
8829
+ * that consumer owns". It lives beside `CamProfile` because a rung is the only
8830
+ * operator-facing way to name a stream: a cam-stream id is broker-internal and
8831
+ * moves when a slot is reassigned, so a stored id silently stops meaning what
8832
+ * the operator picked. Today's one user is the audio analyser, whose rule is
8833
+ * "the lowest AVAILABLE native rung" (D537).
8834
+ */
8835
+ var AudioStreamPickSchema = union([CamProfileSchema, literal("auto")]);
8836
+ var CamStreamKindSchema = _enum([
8837
+ "pull-rtsp",
8838
+ "pull-rtmp",
8839
+ "pull-http",
8840
+ "pull-flv",
8841
+ "pull-rfc4571",
8842
+ "push-annexb",
8843
+ "derived"
8844
+ ]);
8845
+ var CamStreamResolutionSchema = object({
8846
+ width: number().int().positive().meta(readingMeta("none")),
8847
+ height: number().int().positive().meta(readingMeta("none"))
8848
+ });
8849
+ var CameraStreamSchema = object({
8850
+ /** Stable, provider-assigned id unique within the (deviceId) scope. */
8851
+ camStreamId: string().min(1),
8852
+ deviceId: number().int().nonnegative(),
8853
+ kind: CamStreamKindSchema,
8854
+ /** Required for pull-* kinds. Ignored for push-annexb. */
8855
+ url: string().optional(),
8856
+ codec: string().optional(),
8857
+ resolution: CamStreamResolutionSchema.optional(),
8858
+ fps: number().positive().optional(),
8859
+ /** Human label surfaced in the Admin UI "Camera Stream" dropdown. */
8860
+ label: string().optional(),
8861
+ /**
8862
+ * Device-level features the publisher advertised (e.g. `battery-operated`).
8863
+ * The broker, snapshot orchestrator, and prebuffer manager all consult
8864
+ * this list to derive policy — relaxed stall watchdog for battery
8865
+ * cams, prebuffer off by default, longer snapshot rate-limit, etc.
8866
+ *
8867
+ * Single source of truth replacing per-stream flags like the
8868
+ * historical `allowStall`: if the publisher knows the camera is
8869
+ * battery-powered, every downstream service derives the right policy
8870
+ * from this list.
8871
+ */
8872
+ deviceFeatures: array(string()).optional(),
8873
+ /**
8874
+ * Whether this stream participates in the broker's automatic profile
8875
+ * assignment (`computeInitialAssignment`). Defaults to `true`. Publishers
8876
+ * use `false` when they want a stream to be SELECTABLE in the UI but not
8877
+ * picked by default — e.g. Reolink publishes its native Baichuan streams
8878
+ * as `autoEligible: true` (the recommended path) and its RTSP / RTMP
8879
+ * mirrors as `autoEligible: false` (still pickable per slot, just not
8880
+ * the auto choice). Manual `assignProfile` calls remain valid for
8881
+ * non-eligible streams.
8882
+ */
8883
+ autoEligible: boolean().optional(),
8884
+ /**
8885
+ * The profile slot the PUBLISHER says this stream is: `low` for a stream
8886
+ * whose only sensible role is the low-bandwidth one, `high` for a main
8887
+ * stream. Consulted by the broker's automatic ranking
8888
+ * (`rankInitialAssignment`) BEFORE the pixel-count heuristic, and only there:
8889
+ * an operator's manual assignment still wins, and an absent hint is the
8890
+ * pixel-ranked default every camera had before this field existed.
8891
+ *
8892
+ * Why it exists: a camera with ONE stream ranked to `mid` — the slot no
8893
+ * storage class is written for on its own (`recordings` is high|mid,
8894
+ * `recordingsLow` is low) and the one profile scrub does not read. The robot
8895
+ * camera's single 720p relay is a `low` in every respect except the slot the
8896
+ * heuristic put it in.
8897
+ */
8898
+ profileHint: CamProfileSchema.optional(),
8899
+ /**
8900
+ * Transport-specific opaque metadata. The broker passes it through to
8901
+ * the source reader without inspecting it. Currently used by
8902
+ * `pull-rfc4571` streams to carry the upstream SDP (so the reader can
8903
+ * route RTP packets to the right depacketizer without an in-band
8904
+ * DESCRIBE phase). Other kinds typically leave it undefined.
8905
+ */
8906
+ metadata: record(string(), unknown()).optional()
8907
+ });
8908
+ var ProfileSlotStatusSchema = _enum([
8909
+ "unassigned",
8910
+ "idle",
8911
+ "connecting",
8912
+ "streaming",
8913
+ "error"
8914
+ ]);
8915
+ var ProfileSlotSchema = object({
8916
+ deviceId: number().int().nonnegative(),
8917
+ profile: CamProfileSchema,
8918
+ /** Broker id the rest of the system addresses: `${deviceId}/${profile}`. */
8919
+ brokerId: string(),
8920
+ /** `null` when the profile is unassigned. */
8921
+ sourceCamStreamId: string().nullable(),
8922
+ status: ProfileSlotStatusSchema,
8923
+ resolution: CamStreamResolutionSchema.optional(),
8924
+ codec: string().optional(),
8925
+ preBufferSec: number().nonnegative().optional(),
8926
+ errorMessage: string().optional()
8927
+ });
8928
+ /**
8929
+ * Zod schema for StreamSourceEntry — the canonical stream descriptor
8930
+ * exposed by ICameraDevice.getStreamSources() and consumed by the broker.
8931
+ */
8932
+ var StreamSourceEntrySchema$1 = object({
8933
+ id: string(),
8934
+ label: string(),
8935
+ protocol: _enum([
8936
+ "rtsp",
8937
+ "rtmp",
8938
+ "annexb",
8939
+ "http-mjpeg",
8940
+ "webrtc",
8941
+ "custom"
8942
+ ]),
8943
+ url: string().optional(),
8944
+ resolution: object({
8945
+ width: number(),
8946
+ height: number()
8947
+ }).readonly().optional(),
8948
+ fps: number().optional(),
8949
+ bitrate: number().optional(),
8950
+ codec: string().optional(),
8951
+ profileHint: CamProfileSchema.optional()
8952
+ });
8953
+ object({
8954
+ type: string(),
8955
+ url: string(),
8956
+ videoCodec: string().optional(),
8957
+ audioCodec: string().optional(),
8958
+ metadata: record(string(), unknown()).readonly().optional()
8959
+ });
8960
+ /**
8961
+ * Wire schema for `FrameHandle` — a zero-pixel, serialisable reference to a
8962
+ * frame in a shared-memory ring (Phase 5 / D9). Mirrors the `FrameHandle` TS
8963
+ * interface in `interfaces/frame-handle.ts` field-for-field so a cap method
8964
+ * (the runner's native-crop and pipeline-executor inputs) can carry handles
8965
+ * over tRPC / Moleculer.
8966
+ *
8967
+ * The `satisfies` assertion below pins this schema to the interface: if a
8968
+ * field is added to / removed from `FrameHandle` without a matching schema
8969
+ * edit, `z.infer<typeof FrameHandleSchema>` stops being assignable to
8970
+ * `FrameHandle` and the build fails — no silent drift.
8971
+ */
8972
+ var FrameHandleSchema = object({
8973
+ shmId: string(),
8974
+ slot: number().int().nonnegative(),
8975
+ seq: number().int().nonnegative(),
8976
+ width: number().int().positive(),
8977
+ height: number().int().positive(),
8978
+ format: _enum([
8979
+ "jpeg",
8980
+ "rgb",
8981
+ "bgr",
8982
+ "yuv420",
8983
+ "gray"
8984
+ ]),
8985
+ pts: number(),
8986
+ byteLength: number().int().nonnegative(),
8987
+ nodeId: string(),
8988
+ slotCount: number().int().positive()
8989
+ });
8990
+ /**
8991
+ * Wire schema for a decoded audio chunk (Phase 5 / D9). Mirrors the
8992
+ * `DecodedAudioChunk` TS interface in `interfaces/stream-broker.ts`: PCM
8993
+ * sample bytes plus the track parameters a consumer needs to interpret them.
8994
+ *
8995
+ * Audio chunks are tiny (a ~500ms AudioCodecSession window is a few KB of
8996
+ * Float32 PCM), so unlike video frames they ship their bytes INLINE over
8997
+ * tRPC / Moleculer — no shared-memory plane. `data` is typed `Uint8Array`
8998
+ * (the wire-serialisable supertype of `Buffer`); a `Buffer` is assignable to
8999
+ * it.
9000
+ */
9001
+ var AudioChunkFormatSchema = _enum(AUDIO_CHUNK_FORMATS);
9002
+ var DecodedAudioChunkSchema = object({
9003
+ data: _instanceof(Uint8Array),
9004
+ sampleRate: number().int().positive(),
9005
+ channels: number().int().positive(),
9006
+ timestamp: number(),
9007
+ /**
9008
+ * Byte format of `data`. ABSENT MEANS `f32le` — today's bytes, byte for
9009
+ * byte, for any peer that never heard of this field. A coded window
9010
+ * (`pcmu` / `pcma`, one byte per sample) is only ever emitted to a
9011
+ * subscription that DECLARED it accepts one, so absence can never mean
9012
+ * "coded bytes a consumer will read as floats" (D455).
9013
+ */
9014
+ format: AudioChunkFormatSchema.optional()
9015
+ });
9016
+ /**
9017
+ * Input for `stream-broker.subscribeAudioChunks` (Phase 5 / D9). Audio ships
9018
+ * its bytes inline: a consumer asks the broker for the decoded audio-chunk
9019
+ * stream of one broker; the broker keeps a per-subscription bounded FIFO queue
9020
+ * the consumer drains via `pullAudioChunks`.
9021
+ */
9022
+ var SubscribeAudioChunksInputSchema = object({
9023
+ brokerId: string(),
9024
+ /** Short caller-identity tag (`audio-analyzer`, …) for `listClients`. */
9025
+ tag: string().optional(),
9026
+ /**
9027
+ * Byte formats this subscriber can READ, best first. The broker serves the
9028
+ * chunk's own format when it is in this list and expands to `f32le`
9029
+ * otherwise, so a subscriber is never handed bytes it cannot interpret.
9030
+ *
9031
+ * Absent (or without the source format) means `f32le` — the behaviour every
9032
+ * subscriber had before D455, unchanged. This is the negotiation half of
9033
+ * the source-bytes lever: it is what lets the broker and its consumers
9034
+ * deploy one at a time across three nodes.
9035
+ */
9036
+ accept: array(AudioChunkFormatSchema).readonly().optional()
9037
+ });
9038
+ /** Result of `stream-broker.subscribeAudioChunks`. */
9039
+ var SubscribeAudioChunksResultSchema = object({
9040
+ /** Opaque id passed to `pullAudioChunks` / `unsubscribeAudioChunks`. */
9041
+ subscriptionId: string() });
9042
+ var BrokerStatsSchema = object({
9043
+ status: _enum([
9044
+ "idle",
9045
+ "connecting",
9046
+ "streaming",
9047
+ "error",
9048
+ "stopped"
9049
+ ]),
9050
+ inputFps: number(),
9051
+ encodedSubscribers: number(),
9052
+ uptimeMs: number(),
9053
+ bitrateKbps: number(),
9054
+ idrIntervalMs: number(),
9055
+ codec: string().optional(),
9056
+ totalBytes: number(),
9057
+ packetCount: number(),
9058
+ rtspClients: number(),
9059
+ pipeClients: number(),
9060
+ preBufferSec: number(),
9061
+ preBufferMs: number(),
9062
+ preBufferPackets: number(),
9063
+ /**
9064
+ * Detected audio track parameters from the RTSP DESCRIBE / SDP. `null`
9065
+ * when the stream has no audio track or the broker is in cold start.
9066
+ * `supported = false` means the codec was detected but the local
9067
+ * decoder pipeline cannot produce PCM chunks (e.g. AAC without the
9068
+ * AAC pipeline wired). Surfaced in the UI device-overview so operators
9069
+ * can pre-pick the audio-analysis model that matches the codec.
9070
+ */
9071
+ audio: object({
9072
+ codec: string(),
9073
+ sampleRate: number(),
9074
+ channels: number(),
9075
+ supported: boolean()
9076
+ }).nullable().optional(),
9077
+ /**
9078
+ * BROKER-SIDE AUDIO MUTE (D83). `true` = this broker is deliberately
9079
+ * distributing none of the device's audio, on live or recording.
9080
+ *
9081
+ * Present so a silent camera can be told apart from a broken one on the
9082
+ * stream panel itself, without cross-referencing the switch group: a
9083
+ * broker holding an `audio` track descriptor while `audioMuted` is true is
9084
+ * working exactly as asked. `audioMutedDropped` counts the audio units
9085
+ * thrown away since the current dial — it is how you confirm from stats
9086
+ * alone that the mute is on the packet path and not merely persisted.
9087
+ */
9088
+ audioMuted: boolean().optional(),
9089
+ audioMutedDropped: number().optional()
9090
+ });
9091
+ /**
9092
+ * Exporter-facing "profile restream" entry. Returned by
9093
+ * `cameraStreams.getProfileRtspEntries` — one per ASSIGNED profile slot
9094
+ * (high/mid/low). `brokerId` is the PROFILE-keyed broker id
9095
+ * (`${deviceId}/${profile}`, e.g. `15/high`); its `url` is a broker
9096
+ * RTSP restream that aliases the profile's assigned source broker, so
9097
+ * every consumer dialling a profile converges on the same single pull.
9098
+ * `codec`/`resolution` describe the assigned SOURCE camStream (for the
9099
+ * `auto` resolution-closest pick). Distinct from `RtspRestreamEntry`
9100
+ * (raw per-camStream, live-view only).
9101
+ */
9102
+ var ProfileRtspEntrySchema = object({
9103
+ profile: CamProfileSchema,
9104
+ /** Profile-keyed broker id, format `${deviceId}/${profile}` (e.g. `"15/high"`). */
9105
+ brokerId: string(),
9106
+ url: string(),
9107
+ mutedUrl: string(),
9108
+ enabled: boolean(),
9109
+ codec: string().optional(),
9110
+ resolution: CamStreamResolutionSchema.optional()
9111
+ });
9112
+ /**
9113
+ * The model weight formats a catalog entry can ship. Schema-free on purpose:
9114
+ * `types/models.ts` builds Zod schemas from it, but a runner-closure consumer
9115
+ * (the model downloader in `@camstack/server/system`) needs only the list and must
9116
+ * reach it through `@camstack/types/addon` without building the cap schema
9117
+ * graph (D28).
9118
+ */
9119
+ var MODEL_FORMATS = [
9120
+ "onnx",
9121
+ "coreml",
9122
+ "openvino",
9123
+ "tflite",
9124
+ "pt",
9125
+ "gguf"
9126
+ ];
8633
9127
  /**
8634
9128
  * `addon-pages-source` — collection cap exposing per-provider raw page
8635
9129
  * declarations. Every addon that contributes a UI page registers a
@@ -8871,6 +9365,7 @@ var CAP_NAMES = Object.freeze([
8871
9365
  "mesh-network",
8872
9366
  "meter",
8873
9367
  "metrics-provider",
9368
+ "migrations",
8874
9369
  "model-convert",
8875
9370
  "model-distributor",
8876
9371
  "motion",
@@ -8903,6 +9398,7 @@ var CAP_NAMES = Object.freeze([
8903
9398
  "plate-gallery",
8904
9399
  "platform-probe",
8905
9400
  "power-meter",
9401
+ "power-source",
8906
9402
  "presence",
8907
9403
  "pressure-sensor",
8908
9404
  "privacy-mask",
@@ -10254,6 +10750,13 @@ Object.freeze({
10254
10750
  "valueType": "number"
10255
10751
  }
10256
10752
  ],
10753
+ "power-source": [{
10754
+ "path": "switchOn",
10755
+ "valueType": "boolean"
10756
+ }, {
10757
+ "path": "offSeconds",
10758
+ "valueType": "number"
10759
+ }],
10257
10760
  "presence": [
10258
10761
  {
10259
10762
  "path": "state",
@@ -10765,379 +11268,7 @@ object({
10765
11268
  params: record(string(), union([string(), number()])).optional()
10766
11269
  });
10767
11270
  //#endregion
10768
- //#region ../types/dist/err-msg-BD5buxvT.mjs
10769
- var DeviceType = /* @__PURE__ */ function(DeviceType) {
10770
- DeviceType["Camera"] = "camera";
10771
- DeviceType["Hub"] = "hub";
10772
- DeviceType["Light"] = "light";
10773
- DeviceType["Siren"] = "siren";
10774
- DeviceType["Switch"] = "switch";
10775
- DeviceType["Sensor"] = "sensor";
10776
- DeviceType["Thermostat"] = "thermostat";
10777
- /** Air-conditioner / heat-pump climate device (HVAC) — shares the
10778
- * `climate-control` cap surface with `Thermostat` but renders a
10779
- * dedicated AC-appropriate control UI (mode chips, fan speed,
10780
- * independent vertical/horizontal swing). Sources: native Gree, and
10781
- * reusable by other AC integrations. */
10782
- DeviceType["Climate"] = "climate";
10783
- DeviceType["Button"] = "button";
10784
- /** Generic stateless event emitter — carries a device's EXACT declared
10785
- * event vocabulary verbatim (no normalization). Installed with the
10786
- * `event-emitter` cap. Sources: HA `event.*` entities (structured) and
10787
- * HA bus events (e.g. `zha_event`, generic). */
10788
- DeviceType["EventEmitter"] = "event-emitter";
10789
- /** Firmware/software update entity — current vs available version,
10790
- * updatable flag, update state, and an install action. Installed with
10791
- * the `update` cap. Sources: Homematic firmware-update channels (and
10792
- * reusable by other providers, e.g. HA `update.*` entities). */
10793
- DeviceType["Update"] = "update";
10794
- DeviceType["Generic"] = "generic";
10795
- /** Generic notification delivery target (HA `notify.<service>`, future
10796
- * Telegram / Discord / ntfy / SMTP, …). One device per delivery
10797
- * endpoint; the `notifier` cap defines the send surface. */
10798
- DeviceType["Notifier"] = "notifier";
10799
- /** Pre-recorded action sequence with optional parameters
10800
- * (HA `script.*`). Runnable via `script-runner` cap. */
10801
- DeviceType["Script"] = "script";
10802
- /** Automation rule (HA `automation.*`) — enable/disable + manual
10803
- * trigger surface exposed via `automation-control` cap. */
10804
- DeviceType["Automation"] = "automation";
10805
- /** Door / smart lock device (HA `lock.*`). `lock-control` cap. */
10806
- DeviceType["Lock"] = "lock";
10807
- /** Window covering, blinds, garage door, valve, etc. (HA `cover.*`,
10808
- * `valve.*`). `cover` cap with sub-roles for variant. */
10809
- DeviceType["Cover"] = "cover";
10810
- /** Pipe / water / gas valve with open/close/stop and optional
10811
- * position (HA `valve.*`). `valve` cap — a cover-sibling actuator
10812
- * modelled on the same open/closed lifecycle. */
10813
- DeviceType["Valve"] = "valve";
10814
- /** Humidifier / dehumidifier with on/off + target humidity + mode
10815
- * (HA `humidifier.*`). `humidifier` cap — a climate-family actuator
10816
- * modelled on the same target / mode lifecycle. */
10817
- DeviceType["Humidifier"] = "humidifier";
10818
- /** Water heater / boiler with target temperature + operation mode +
10819
- * away mode (HA `water_heater.*`). `water-heater` cap — a
10820
- * climate-family actuator. */
10821
- DeviceType["WaterHeater"] = "water-heater";
10822
- /** Ceiling / standing / exhaust fan (HA `fan.*`). `fan-control` cap. */
10823
- DeviceType["Fan"] = "fan";
10824
- /** Audio / video playback endpoint (HA `media_player.*`). Disjoint from
10825
- * the camera surface — those use `Camera`. `media-player` cap. */
10826
- DeviceType["MediaPlayer"] = "media-player";
10827
- /** Security panel / alarm system (HA `alarm_control_panel.*`).
10828
- * `alarm-panel` cap. */
10829
- DeviceType["AlarmPanel"] = "alarm-panel";
10830
- /** Generic user-settable input (HA `number` / `input_number` / `select`
10831
- * / `input_select` / `text` / `input_text` / `input_datetime`).
10832
- * Sub-type via `DeviceRole`: NumericControl / SelectControl /
10833
- * TextControl / DateTimeControl. */
10834
- DeviceType["Control"] = "control";
10835
- /** Person / device-tracker presence (HA `person.*`, `device_tracker.*`).
10836
- * `presence` cap. */
10837
- DeviceType["Presence"] = "presence";
10838
- /** Weather provider (HA `weather.*`). Tier-3, low MVP priority.
10839
- * `weather` cap. */
10840
- DeviceType["Weather"] = "weather";
10841
- /** Robot vacuum (HA `vacuum.*`). Tier-3. `vacuum-control` cap. */
10842
- DeviceType["Vacuum"] = "vacuum";
10843
- /** Robotic lawn mower (HA `lawn_mower.*`). Tier-3.
10844
- * `lawn-mower-control` cap. */
10845
- DeviceType["LawnMower"] = "lawn-mower";
10846
- /** Physical HA device group — parent container for entity-children
10847
- * adopted from a single HA device entry. Not renderable as a
10848
- * standalone device; exists only to anchor child entities. */
10849
- DeviceType["Container"] = "container";
10850
- /** Single still-image entity (HA `image.*`). Read-only display of an
10851
- * `entity_picture` signed URL the browser loads directly. `image` cap. */
10852
- DeviceType["Image"] = "image";
10853
- /** Smart pet feeder — cloud-connected food dispenser with a bowl food
10854
- * level, battery, desiccant life, feeding state and manual-feed /
10855
- * call-pet / maintenance actions. Installed with the `pet-feeder` cap;
10856
- * dual-hopper models (D4S/D4SH) expose per-hopper portions. Sources:
10857
- * native PetKit (`nodepetkit` `FeederDevice`), reusable by other feeder
10858
- * integrations sharing the same food/desiccant/hopper surface. */
10859
- DeviceType["PetFeeder"] = "pet-feeder";
10860
- /** Pet water fountain — tank level, water drunk, pump and its duty cycle.
10861
- * Installed with the `pet-fountain` cap. Reaches CamStack as loose entities
10862
- * of a container and is BUILT as a composed device (D967). */
10863
- DeviceType["PetFountain"] = "pet-fountain";
10864
- /** Built-in dishwasher — program lifecycle, door / remote-start gates,
10865
- * salt and rinse-aid levels. Installed with the `dishwasher` cap.
10866
- * Sources: native Bosch Home Connect, reusable by similar integrations.
10867
- * A plug-built dishwasher carries running-state and power instead. */
10868
- DeviceType["Dishwasher"] = "dishwasher";
10869
- /** Washing machine. A plug-built one is running while its power stays
10870
- * above a threshold. No program cap — that surface is the dishwasher's. */
10871
- DeviceType["Washer"] = "washer";
10872
- /** Tumble dryer. Same plug-built running surface as {@link Washer}. */
10873
- DeviceType["Dryer"] = "dryer";
10874
- /** The physical bridge box of a vendor ecosystem (Velux KLF-200, and later
10875
- * other radio-to-IP bridges). It carries the box's own facts (firmware,
10876
- * address, connection state) and the actions the box really supports
10877
- * (`reboot`, `device-admin-link`). Products are NOT its children — they are
10878
- * adopted beside it — and its connection settings stay on the integration. */
10879
- DeviceType["Gateway"] = "gateway";
10880
- return DeviceType;
10881
- }({});
10882
- var DeviceFeature = /* @__PURE__ */ function(DeviceFeature) {
10883
- DeviceFeature["BatteryOperated"] = "battery-operated";
10884
- DeviceFeature["Rebootable"] = "rebootable";
10885
- /**
10886
- * Device supports an on-demand re-sync of its derived spec with its
10887
- * upstream source — drives the generic Re-sync button. The owning
10888
- * provider implements the action via the `device-adoption.resync` cap.
10889
- */
10890
- DeviceFeature["Resyncable"] = "resyncable";
10891
- DeviceFeature["NativeSnapshot"] = "native-snapshot";
10892
- DeviceFeature["DoorbellButton"] = "doorbell-button";
10893
- DeviceFeature["TwoWayAudio"] = "two-way-audio";
10894
- DeviceFeature["PanTiltZoom"] = "pan-tilt-zoom";
10895
- /**
10896
- * Camera supports the on-firmware autotrack subsystem (subject-
10897
- * following). Distinct from `PanTiltZoom` because not every PTZ
10898
- * camera ships autotrack — the admin UI uses this flag to gate
10899
- * the autotrack toggle / settings card without re-deriving from
10900
- * the cap registry. Mirrors `ptz-autotrack` cap registration:
10901
- * driver sets this feature when probe confirms the firmware
10902
- * surface, and registers the cap in the same code path.
10903
- */
10904
- DeviceFeature["PtzAutotrack"] = "ptz-autotrack";
10905
- /**
10906
- * Accessory exposes a "trigger on motion" toggle — the parent camera's
10907
- * motion detection automatically activates this device. Mirrors
10908
- * `motion-trigger` cap registration: drivers set this feature in the
10909
- * same code path that calls `ctx.registerNativeCap(motionTriggerCapability, ...)`.
10910
- *
10911
- * Used by admin UI (gate the in-hero `MotionTriggerToggle` against a
10912
- * fast scalar without binding fetch), notifier rules, and `listAll`
10913
- * filters that want "all devices with on-motion behaviour".
10914
- */
10915
- DeviceFeature["MotionTrigger"] = "motion-trigger";
10916
- /** Light supports rgb-triplet color via `color` cap. */
10917
- DeviceFeature["LightColorRgb"] = "light-color-rgb";
10918
- /** Light supports HSV color via `color` cap. */
10919
- DeviceFeature["LightColorHsv"] = "light-color-hsv";
10920
- /** Light supports color-temperature (mired) via `color` cap. */
10921
- DeviceFeature["LightColorMired"] = "light-color-mired";
10922
- /** Thermostat supports a `heat_cool` dual setpoint (targetLow +
10923
- * targetHigh). Gates the range slider UI. */
10924
- DeviceFeature["ClimateDualSetpoint"] = "climate-dual-setpoint";
10925
- /** Thermostat exposes target humidity and/or current humidity
10926
- * readings. Gates the humidity controls. */
10927
- DeviceFeature["ClimateHumidity"] = "climate-humidity";
10928
- /** Thermostat exposes a fan-mode selector. */
10929
- DeviceFeature["ClimateFanMode"] = "climate-fan-mode";
10930
- /** Thermostat exposes preset modes (eco / away / sleep / vendor). */
10931
- DeviceFeature["ClimatePreset"] = "climate-preset";
10932
- /** Thermostat exposes a vertical louver swing toggle. Gates the
10933
- * vertical-swing switch in the climate UI. Independent of horizontal. */
10934
- DeviceFeature["ClimateSwingVertical"] = "climate-swing-vertical";
10935
- /** Thermostat exposes a horizontal louver swing toggle. Gates the
10936
- * horizontal-swing switch in the climate UI. Independent of vertical. */
10937
- DeviceFeature["ClimateSwingHorizontal"] = "climate-swing-horizontal";
10938
- /** Cover exposes intermediate position control (0..100). Gates the
10939
- * position slider UI. */
10940
- DeviceFeature["CoverPositionable"] = "cover-positionable";
10941
- /** Cover exposes slat-tilt control. Gates the tilt slider UI. */
10942
- DeviceFeature["CoverTilt"] = "cover-tilt";
10943
- /** Valve exposes intermediate position control (0..100). Gates the
10944
- * position slider / drag surface UI. */
10945
- DeviceFeature["ValvePositionable"] = "valve-positionable";
10946
- /** Fan exposes a speed-percentage setter. Gates the speed slider UI. */
10947
- DeviceFeature["FanSpeed"] = "fan-speed";
10948
- /** Fan exposes a preset mode selector. */
10949
- DeviceFeature["FanPreset"] = "fan-preset";
10950
- /** Fan exposes blade direction (forward/reverse) — typical of
10951
- * ceiling fans. */
10952
- DeviceFeature["FanDirection"] = "fan-direction";
10953
- /** Fan exposes an oscillation toggle. */
10954
- DeviceFeature["FanOscillating"] = "fan-oscillating";
10955
- /** Lock requires a PIN code on lock/unlock. Gates the code-entry
10956
- * field on the UI lock-controls panel. */
10957
- DeviceFeature["LockPinRequired"] = "lock-pin-required";
10958
- /** Lock supports a latch-release ("open door") action distinct from
10959
- * unlock. Mirrors HA `LockEntityFeature.OPEN` (bit 1) in
10960
- * `supported_features`. Gates the Open Door button in the UI. */
10961
- DeviceFeature["LockOpen"] = "lock-open";
10962
- /** Media player exposes a seek-to-position surface. */
10963
- DeviceFeature["MediaPlayerSeek"] = "media-player-seek";
10964
- /** Media player exposes a volume-level setter. */
10965
- DeviceFeature["MediaPlayerVolume"] = "media-player-volume";
10966
- /** Media player exposes a mute toggle distinct from volume=0. */
10967
- DeviceFeature["MediaPlayerMute"] = "media-player-mute";
10968
- /** Media player exposes a shuffle toggle. */
10969
- DeviceFeature["MediaPlayerShuffle"] = "media-player-shuffle";
10970
- /** Media player exposes a repeat mode (off / all / one). */
10971
- DeviceFeature["MediaPlayerRepeat"] = "media-player-repeat";
10972
- /** Media player exposes a source / input selector. */
10973
- DeviceFeature["MediaPlayerSelectSource"] = "media-player-select-source";
10974
- /** Media player exposes a play-arbitrary-media surface (URL / id). */
10975
- DeviceFeature["MediaPlayerPlayMedia"] = "media-player-play-media";
10976
- /** Media player exposes next-track. */
10977
- DeviceFeature["MediaPlayerNext"] = "media-player-next";
10978
- /** Media player exposes previous-track. */
10979
- DeviceFeature["MediaPlayerPrevious"] = "media-player-previous";
10980
- /** Media player exposes stop distinct from pause. */
10981
- DeviceFeature["MediaPlayerStop"] = "media-player-stop";
10982
- /** Alarm panel requires a PIN code on arm/disarm. */
10983
- DeviceFeature["AlarmPinRequired"] = "alarm-pin-required";
10984
- /** Presence device carries GPS coordinates (lat/lng/accuracy) in
10985
- * addition to a textual location. */
10986
- DeviceFeature["PresenceGps"] = "presence-gps";
10987
- /** Notifier accepts an inline / URL image attachment. */
10988
- DeviceFeature["NotifierImage"] = "notifier-image";
10989
- /** Notifier accepts a priority hint (high/normal/low). */
10990
- DeviceFeature["NotifierPriority"] = "notifier-priority";
10991
- /** Notifier accepts a free-form `data` payload for platform-specific
10992
- * fields. */
10993
- DeviceFeature["NotifierData"] = "notifier-data";
10994
- /** Notifier supports interactive action buttons / callbacks. */
10995
- DeviceFeature["NotifierActions"] = "notifier-actions";
10996
- /** Notifier supports per-call recipient targeting (multi-user). */
10997
- DeviceFeature["NotifierRecipients"] = "notifier-recipients";
10998
- /** Script runner accepts a variables map on each run invocation. */
10999
- DeviceFeature["ScriptVariables"] = "script-variables";
11000
- /** Automation `trigger` accepts a skipCondition flag — fires the
11001
- * automation's actions while bypassing its condition block. */
11002
- DeviceFeature["AutomationSkipCondition"] = "automation-skip-condition";
11003
- /** Robot vacuum exposes a live cleaning map (image child). Gates the
11004
- * map tile in the vacuum UI. */
11005
- DeviceFeature["VacuumHasMap"] = "vacuum-has-map";
11006
- /** Robot vacuum exposes AI obstacle-detection toggles. Gates the AI
11007
- * switches group. */
11008
- DeviceFeature["VacuumHasAi"] = "vacuum-has-ai";
11009
- /** Robot mower exposes a live mowing map (SVG image child). Gates the
11010
- * mower map tile. */
11011
- DeviceFeature["MowerHasMap"] = "mower-has-map";
11012
- /** Robot mower exposes targeted mowing (all-area / zones / edges /
11013
- * spots) — gates the Mow action + map selectors. */
11014
- DeviceFeature["MowerHasTargetedMowing"] = "mower-has-targeted-mowing";
11015
- return DeviceFeature;
11016
- }({});
11017
- /**
11018
- * Semantic role a device plays within its parent. Populated by driver
11019
- * addons when creating accessory devices (Reolink siren/floodlight/
11020
- * PIR/chime/autotrack/doorbell, ONVIF relay outputs, …). Used by the
11021
- * admin UI to pick icons, labels, and widgets — a `Switch` with
11022
- * `role: Floodlight` renders as a bulb with a brightness slider,
11023
- * whereas a `Switch` with `role: Siren` renders as a klaxon.
11024
- *
11025
- * Undefined for top-level devices (cameras, NVRs, hubs). Persisted in
11026
- * sqlite as a nullable TEXT column — old rows keep working unchanged.
11027
- */
11028
- var DeviceRole = /* @__PURE__ */ function(DeviceRole) {
11029
- DeviceRole["Siren"] = "siren";
11030
- DeviceRole["Floodlight"] = "floodlight";
11031
- DeviceRole["Spotlight"] = "spotlight";
11032
- DeviceRole["PirSensor"] = "pir-sensor";
11033
- DeviceRole["Chime"] = "chime";
11034
- DeviceRole["Autotrack"] = "autotrack";
11035
- DeviceRole["Nightvision"] = "nightvision";
11036
- DeviceRole["PrivacyMask"] = "privacy-mask";
11037
- DeviceRole["Doorbell"] = "doorbell";
11038
- /** Interior blind / shade / roller blind. HA `device_class: blind|shade`. */
11039
- DeviceRole["CoverBlind"] = "cover-blind";
11040
- /** Exterior roller shutter. HA `device_class: shutter`; Homematic BROLL. */
11041
- DeviceRole["CoverShutter"] = "cover-shutter";
11042
- /** Exterior awning. HA `device_class: awning`. */
11043
- DeviceRole["CoverAwning"] = "cover-awning";
11044
- /** Curtain / drape. HA `device_class: curtain`. */
11045
- DeviceRole["CoverCurtain"] = "cover-curtain";
11046
- /** Garage door. HA `device_class: garage`. A security actuator: exports gate it. */
11047
- DeviceRole["CoverGarage"] = "cover-garage";
11048
- /** Gate. HA `device_class: gate`. A security actuator: exports gate it. */
11049
- DeviceRole["CoverGate"] = "cover-gate";
11050
- /** Door. HA `device_class: door`. A security actuator: exports gate it. */
11051
- DeviceRole["CoverDoor"] = "cover-door";
11052
- /** Window. HA `device_class: window`. */
11053
- DeviceRole["CoverWindow"] = "cover-window";
11054
- /** Virtual HA toggle (input_boolean.*) — distinguishable from a
11055
- * real Switch device for UI rendering / export adapters. */
11056
- DeviceRole["BinaryHelper"] = "binary-helper";
11057
- /** A smart plug: a `switch` whose source says it is an outlet (HA
11058
- * `device_class: outlet`). The Alexa export shows it as a SMARTPLUG. */
11059
- DeviceRole["Outlet"] = "outlet";
11060
- /** Generic motion / occupancy / moving event source. Distinct from
11061
- * the camera accessory PirSensor role: that one is a camera child;
11062
- * this is a standalone HA / 3rd-party motion sensor. */
11063
- DeviceRole["MotionSensor"] = "motion-sensor";
11064
- DeviceRole["ContactSensor"] = "contact-sensor";
11065
- DeviceRole["LeakSensor"] = "leak-sensor";
11066
- DeviceRole["SmokeSensor"] = "smoke-sensor";
11067
- DeviceRole["COSensor"] = "co-sensor";
11068
- DeviceRole["GasSensor"] = "gas-sensor";
11069
- DeviceRole["TamperSensor"] = "tamper-sensor";
11070
- DeviceRole["VibrationSensor"] = "vibration-sensor";
11071
- DeviceRole["ConnectivitySensor"] = "connectivity-sensor";
11072
- DeviceRole["SoundSensor"] = "sound-sensor";
11073
- /** Fallback for `binary_sensor` without a known `device_class`. */
11074
- DeviceRole["BinarySensor"] = "binary-sensor";
11075
- DeviceRole["TemperatureSensor"] = "temperature-sensor";
11076
- DeviceRole["HumiditySensor"] = "humidity-sensor";
11077
- DeviceRole["AmbientLightSensor"] = "ambient-light-sensor";
11078
- DeviceRole["PressureSensor"] = "pressure-sensor";
11079
- /** Wind speed or direction (weather-station `wind-sensor` cap). */
11080
- DeviceRole["WindSensor"] = "wind-sensor";
11081
- /** Rain accumulation or rate (weather-station `rain-sensor` cap). */
11082
- DeviceRole["RainSensor"] = "rain-sensor";
11083
- /** UV index (weather-station `uv-sensor` cap). */
11084
- DeviceRole["UvSensor"] = "uv-sensor";
11085
- /** Solar irradiance W/m² (weather-station `solar-radiation-sensor` cap).
11086
- * Distinct from AmbientLightSensor (lux). */
11087
- DeviceRole["SolarRadiationSensor"] = "solar-radiation-sensor";
11088
- /** Soil moisture % (garden/weather `soil-moisture-sensor` cap). */
11089
- DeviceRole["SoilMoistureSensor"] = "soil-moisture-sensor";
11090
- DeviceRole["PowerSensor"] = "power-sensor";
11091
- DeviceRole["EnergySensor"] = "energy-sensor";
11092
- DeviceRole["VoltageSensor"] = "voltage-sensor";
11093
- DeviceRole["CurrentSensor"] = "current-sensor";
11094
- DeviceRole["AirQualitySensor"] = "air-quality-sensor";
11095
- /** Battery level (numeric % via `sensor` OR low-bool via
11096
- * `binary_sensor` — the cap distinguishes via the value type). */
11097
- DeviceRole["BatterySensor"] = "battery-sensor";
11098
- /** Fallback for `sensor` numeric without a known `device_class`. */
11099
- DeviceRole["NumericSensor"] = "numeric-sensor";
11100
- /** String / enum state (HA `sensor` with `state_class: enum` or
11101
- * `attributes.options`). */
11102
- DeviceRole["EnumSensor"] = "enum-sensor";
11103
- /** Date / timestamp state (HA `sensor` with `device_class: timestamp`
11104
- * or `date`). The slice carries the raw ISO string verbatim (hosted on
11105
- * the `enum-sensor` cap); the UI renders it locale-formatted. */
11106
- DeviceRole["DateTimeSensor"] = "datetime-sensor";
11107
- /** Last-resort fallback when nothing else matches. */
11108
- DeviceRole["GenericSensor"] = "generic-sensor";
11109
- DeviceRole["NumericControl"] = "numeric-control";
11110
- DeviceRole["SelectControl"] = "select-control";
11111
- DeviceRole["TextControl"] = "text-control";
11112
- DeviceRole["DateTimeControl"] = "datetime-control";
11113
- /** Mobile push notifier (HA `notify.mobile_app_*`) — supports
11114
- * rich features (image, priority, channel routing). */
11115
- DeviceRole["MobilePushNotifier"] = "mobile-push-notifier";
11116
- /** Chat / messaging service (HA `notify.telegram_*`,
11117
- * `notify.discord_*`, etc.). */
11118
- DeviceRole["MessagingNotifier"] = "messaging-notifier";
11119
- /** Email-based delivery (HA `notify.smtp`, etc.). */
11120
- DeviceRole["EmailNotifier"] = "email-notifier";
11121
- /** Fallback when the notifier service name doesn't match a known
11122
- * pattern. */
11123
- DeviceRole["GenericNotifier"] = "generic-notifier";
11124
- /** Emits a pulse train (infrared or RF) on request — the `ir-transmitter`
11125
- * cap. A `DeviceType.Generic` device. */
11126
- DeviceRole["IrTransmitter"] = "ir-transmitter";
11127
- return DeviceRole;
11128
- }({});
11129
- /**
11130
- import { errMsg } from '@camstack/types'
11131
- * Extract a human-readable message from an unknown error value.
11132
- * Replaces the ubiquitous `errMsg(err)` pattern.
11133
- */
11134
- function errMsg(err) {
11135
- if (err instanceof Error) return err.message;
11136
- if (typeof err === "string") return err;
11137
- return String(err);
11138
- }
11139
- //#endregion
11140
- //#region ../types/dist/composition-C7WdThmP.mjs
11271
+ //#region ../types/dist/composition-BBzVf2xd.mjs
11141
11272
  /**
11142
11273
  * The ffmpeg build every CamStack node downloads, per platform and arch.
11143
11274
  *
@@ -12356,6 +12487,71 @@ var EventOwnerTypeSchema = _enum(EVENT_OWNER_TYPES);
12356
12487
  var MediaPresenceOwnerKindSchema = _enum([...EVENT_OWNER_TYPES, "track"]);
12357
12488
  new Set(EVENT_OWNER_TYPES);
12358
12489
  /**
12490
+ * What a published package needs from the rest of the install (D989).
12491
+ *
12492
+ * One published framework package (`@camstack/server`) carries the kernel, the
12493
+ * builtins and the capability contracts, so "which addon runs on which
12494
+ * framework" is a question of version, and the answer is DECLARED by the
12495
+ * package that knows it:
12496
+ *
12497
+ * - an addon says the oldest server it runs on (`requires.server: ">=X"`);
12498
+ * - the server says the oldest addon it works with
12499
+ * (`requires.addons: { "@camstack/addon-x": ">=Y" }`) and the oldest shell
12500
+ * around it (`requires.wrapper: { desktop, image }`);
12501
+ * - a wrapper (the desktop app, the container starter) says which server
12502
+ * range it can host, and reports its own kind and version.
12503
+ *
12504
+ * The publish workflow STAMPS these after the version bump
12505
+ * (`scripts/ci/stamp-requires.mjs`); nobody types a number by hand.
12506
+ *
12507
+ * A requirement that cannot be checked is NOT met: a shell that reports no
12508
+ * version counts as below the minimum, and an unreadable running version is
12509
+ * named in the result (a failed read is never a pass).
12510
+ */
12511
+ /** `>=MAJOR.MINOR.PATCH[-prerelease]` — the only range shape a requirement uses. */
12512
+ var MinVersionSchema = string().regex(/^>=\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/, "a minimum is written \">=X.Y.Z\"");
12513
+ object({
12514
+ /** The oldest `@camstack/server` this package runs on. */
12515
+ server: MinVersionSchema.optional(),
12516
+ /** The oldest installed version of each named addon package. */
12517
+ addons: record(string(), MinVersionSchema).optional(),
12518
+ /** The oldest shell around the server, per shell kind. */
12519
+ wrapper: object({
12520
+ desktop: MinVersionSchema.optional(),
12521
+ image: MinVersionSchema.optional()
12522
+ }).optional()
12523
+ });
12524
+ /** One unmet requirement; each names the versions on both sides. */
12525
+ var UnmetRequirementSchema = discriminatedUnion("kind", [
12526
+ object({
12527
+ kind: literal("server-too-old"),
12528
+ required: string(),
12529
+ /** `null` — the running server version could not be read. */
12530
+ running: string().nullable()
12531
+ }),
12532
+ object({
12533
+ kind: literal("addon-too-old"),
12534
+ packageName: string(),
12535
+ required: string(),
12536
+ /** `null` — the addon is not installed (or its version is unreadable). */
12537
+ running: string().nullable()
12538
+ }),
12539
+ object({
12540
+ kind: literal("wrapper-too-old"),
12541
+ wrapper: _enum(["desktop", "image"]),
12542
+ required: string(),
12543
+ /** `null` — the wrapper reported no version. */
12544
+ running: string().nullable()
12545
+ })
12546
+ ]);
12547
+ var RequiredAddonUpdateSchema = object({
12548
+ packageName: string(),
12549
+ /** The minimum the target server declares. */
12550
+ required: string(),
12551
+ /** What this node has; `null` when the addon is not installed. */
12552
+ installed: string().nullable()
12553
+ });
12554
+ /**
12359
12555
  * Which integration instance a settings form is for. An addon that serves several instances
12360
12556
  * (`instanceMode: multiple`) can show a different form per instance — an NVR integration
12361
12557
  * needs its connection, the default integration needs the addon's own sections.
@@ -12830,6 +13026,613 @@ var CameraSwitchGroupSchema = object({
12830
13026
  fetchedAt: number()
12831
13027
  });
12832
13028
  /**
13029
+ * Query filter for settings-store collections.
13030
+ */
13031
+ var QueryFilterSchema = object({
13032
+ where: record(string(), unknown()).optional(),
13033
+ whereIn: record(string(), array(unknown())).optional(),
13034
+ whereBetween: record(string(), tuple([unknown(), unknown()])).optional(),
13035
+ /** NULL-safe exclusion: matches rows whose field is NULL OR != the value. */
13036
+ whereNot: record(string(), unknown()).optional(),
13037
+ /** NULL-safe exclusion of a SET — `whereNot` for more than one value. An
13038
+ * empty list excludes nothing. See `QueryFilter.whereNotIn`. */
13039
+ whereNotIn: record(string(), array(unknown())).optional(),
13040
+ orderBy: object({
13041
+ field: string(),
13042
+ direction: _enum(["asc", "desc"])
13043
+ }).optional(),
13044
+ limit: number().optional(),
13045
+ offset: number().optional()
13046
+ });
13047
+ /**
13048
+ * The predicate half of a filter, for BULK MUTATIONS.
13049
+ *
13050
+ * Deliberately not `QueryFilterSchema`: `orderBy` / `limit` / `offset` have no
13051
+ * meaning for a statement that rewrites a set, and accepting them would invite
13052
+ * a caller to believe `limit` bounds the damage. Every field is optional here
13053
+ * only so the shape stays composable — the implementation REJECTS a filter
13054
+ * that compiles to no predicate, because that is the whole collection.
13055
+ */
13056
+ var MutationFilterSchema = object({
13057
+ where: record(string(), unknown()).optional(),
13058
+ whereIn: record(string(), array(unknown())).optional(),
13059
+ whereBetween: record(string(), tuple([unknown(), unknown()])).optional(),
13060
+ whereNot: record(string(), unknown()).optional(),
13061
+ whereNotIn: record(string(), array(unknown())).optional()
13062
+ });
13063
+ /**
13064
+ * One scalar an {@link settingsStoreCapability.methods.aggregate} call asks for.
13065
+ *
13066
+ * `as` names the slot in the result, so the SAME column may be asked twice with
13067
+ * two operations (`MIN(startMs)` and `MAX(startMs)` in one round trip) — which
13068
+ * a `Record<column, op>` shape could not express.
13069
+ */
13070
+ var AggregateFieldSchema = object({
13071
+ /** Result key. */
13072
+ as: string().min(1),
13073
+ /** Column to aggregate. Must be a real column of a declared collection. */
13074
+ field: string().min(1),
13075
+ op: _enum([
13076
+ "sum",
13077
+ "min",
13078
+ "max"
13079
+ ])
13080
+ });
13081
+ /**
13082
+ * `COUNT(*)` plus one number per requested field.
13083
+ *
13084
+ * `null` means NO ROW MATCHED, never zero: a `SUM` over an empty set and a sum
13085
+ * that really is 0 are different facts, and an accounting caller that renders
13086
+ * "0 bytes, oldest = 0" for "nothing here" reports a lie about a disk.
13087
+ */
13088
+ var AggregateResultSchema = object({
13089
+ count: number().int(),
13090
+ values: record(string(), number().nullable())
13091
+ });
13092
+ /** A single stored record: `{ id, data }`. */
13093
+ var SettingsRecordSchema = object({
13094
+ id: string(),
13095
+ data: record(string(), unknown())
13096
+ });
13097
+ /**
13098
+ * One record of a BULK insert — {@link SettingsRecordSchema} with the id made
13099
+ * optional.
13100
+ *
13101
+ * A separate schema rather than loosening the shared one: every other method
13102
+ * on this cap addresses a row BY its id, and making that field optional
13103
+ * everywhere would turn a forgotten key into a silently generated one on
13104
+ * `update` and `delete` as well.
13105
+ */
13106
+ var BulkRecordSchema = object({
13107
+ id: string().optional(),
13108
+ data: record(string(), unknown())
13109
+ });
13110
+ /**
13111
+ * The database FILES one `sqlite-settings` engine process owns (D851).
13112
+ * Closed on purpose: a file is a thing the engine opens, backs up and gives a
13113
+ * serving thread, so a new one is a code change, never a string a caller
13114
+ * invents. `settings` is `camstack.db` and the default for every collection
13115
+ * that names no file.
13116
+ */
13117
+ var DataStoreDatabaseSchema = _enum([
13118
+ "settings",
13119
+ "events",
13120
+ "history"
13121
+ ]);
13122
+ DataStoreDatabaseSchema.options;
13123
+ /**
13124
+ * One write inside a {@link settingsStoreCapability.methods.transact} call.
13125
+ * The three that a "replace this set, bump that revision" write needs, plus
13126
+ * the batched upsert a rollup writer needs.
13127
+ */
13128
+ var TransactOpSchema = discriminatedUnion("op", [
13129
+ object({
13130
+ op: literal("deleteWhere"),
13131
+ collection: string(),
13132
+ filter: MutationFilterSchema
13133
+ }),
13134
+ object({
13135
+ op: literal("insertMany"),
13136
+ collection: string(),
13137
+ records: array(BulkRecordSchema).readonly()
13138
+ }),
13139
+ object({
13140
+ op: literal("update"),
13141
+ collection: string(),
13142
+ id: string(),
13143
+ data: record(string(), unknown())
13144
+ }),
13145
+ object({
13146
+ op: literal("upsertMany"),
13147
+ collection: string(),
13148
+ conflict: array(string()).readonly(),
13149
+ records: array(BulkRecordSchema).readonly()
13150
+ })
13151
+ ]);
13152
+ /**
13153
+ * The precondition of a transaction: the row `key` of `collection` must hold
13154
+ * `equals` in `column`, read INSIDE the transaction.
13155
+ */
13156
+ var TransactGuardSchema = object({
13157
+ collection: string(),
13158
+ key: string(),
13159
+ column: string(),
13160
+ equals: unknown()
13161
+ });
13162
+ /**
13163
+ * Column declaration for a structured (SQL-backed) collection.
13164
+ *
13165
+ * Logical types — the backend translates each to the matching SQLite
13166
+ * storage class and handles per-type marshaling:
13167
+ * - `TEXT` / `INTEGER` / `REAL` — native SQLite types, pass-through
13168
+ * - `JSON` — TEXT under the hood; serialised on write, parsed on read
13169
+ * - `BOOLEAN` — INTEGER 0/1 under the hood; coerced both directions
13170
+ */
13171
+ var CollectionColumnSchema = object({
13172
+ name: string(),
13173
+ type: _enum([
13174
+ "TEXT",
13175
+ "INTEGER",
13176
+ "REAL",
13177
+ "JSON",
13178
+ "BOOLEAN"
13179
+ ]),
13180
+ primaryKey: boolean().optional(),
13181
+ notNull: boolean().optional(),
13182
+ unique: boolean().optional(),
13183
+ /**
13184
+ * Column DEFAULT. Required for B2: the four `ensureTable` consumers declare
13185
+ * columns like `enabled INTEGER NOT NULL DEFAULT 1`, and without this the
13186
+ * collection surface simply cannot express their existing tables — which is
13187
+ * why they were still on the uncapped `table*` API.
13188
+ */
13189
+ defaultValue: union([
13190
+ string(),
13191
+ number(),
13192
+ boolean()
13193
+ ]).optional()
13194
+ });
13195
+ var CollectionIndexSchema = object({
13196
+ name: string(),
13197
+ columns: array(string()).readonly(),
13198
+ unique: boolean().optional()
13199
+ });
13200
+ /**
13201
+ * The input of `declareCollection` — ONE schema for the door and for the
13202
+ * engine behind it, so the two surfaces cannot drift (the dispatcher forwards
13203
+ * the payload untouched).
13204
+ *
13205
+ * - `database` — the file the collection lives in (D851). Absent means
13206
+ * `settings`. A declaration naming a file other than the one the table is
13207
+ * already in is REFUSED by the engine, never moved.
13208
+ * - `primaryKey` — a COMPOSITE key, as column names. A single name is the same
13209
+ * as `primaryKey: true` on that column. A composite-keyed collection has no
13210
+ * `id`: it is written with `insertMany` / `upsertMany` / `transact` and read
13211
+ * with `query` / `count` / `aggregate` / `deleteWhere`; the calls that
13212
+ * address one row by id are refused.
13213
+ * - `withoutRowid` — emit `WITHOUT ROWID`. Requires a primary key.
13214
+ */
13215
+ var DeclareCollectionInputSchema = object({
13216
+ namespace: string().optional(),
13217
+ collection: string(),
13218
+ columns: array(CollectionColumnSchema).readonly(),
13219
+ indexes: array(CollectionIndexSchema).readonly().optional(),
13220
+ database: DataStoreDatabaseSchema.optional(),
13221
+ primaryKey: array(string()).readonly().optional(),
13222
+ withoutRowid: boolean().optional()
13223
+ });
13224
+ method(object({
13225
+ namespace: string().optional(),
13226
+ collection: string(),
13227
+ key: string()
13228
+ }), unknown(), { auth: "admin" }), method(object({
13229
+ namespace: string().optional(),
13230
+ collection: string(),
13231
+ key: string(),
13232
+ value: unknown()
13233
+ }), _void(), {
13234
+ kind: "mutation",
13235
+ auth: "admin"
13236
+ }), method(object({
13237
+ namespace: string().optional(),
13238
+ collection: string(),
13239
+ filter: QueryFilterSchema.optional(),
13240
+ columns: array(string()).readonly().optional()
13241
+ }), array(SettingsRecordSchema).readonly(), { auth: "admin" }), method(object({
13242
+ namespace: string().optional(),
13243
+ collection: string(),
13244
+ /** Declared columns identifying the slot; must be covered by a UNIQUE
13245
+ * index on the collection. Never empty. */
13246
+ conflict: array(string()).readonly(),
13247
+ record: BulkRecordSchema
13248
+ }), object({
13249
+ /** The id the row HAS: assigned by SQLite on an insert, or the existing
13250
+ * row's own id on an update. */
13251
+ id: union([string(), number()]) }), {
13252
+ kind: "mutation",
13253
+ auth: "admin"
13254
+ }), method(object({
13255
+ namespace: string().optional(),
13256
+ collection: string(),
13257
+ conflict: array(string()).readonly(),
13258
+ records: array(BulkRecordSchema).readonly()
13259
+ }), object({ upserted: number().int() }), {
13260
+ kind: "mutation",
13261
+ auth: "admin"
13262
+ }), method(object({
13263
+ namespace: string().optional(),
13264
+ collection: string(),
13265
+ record: object({
13266
+ id: string().optional(),
13267
+ data: record(string(), unknown())
13268
+ })
13269
+ }), object({
13270
+ /**
13271
+ * The id the row ACTUALLY got (D473): the one supplied, the UUID minted
13272
+ * for an absent one, or the ROWID SQLite assigned on an `INTEGER`
13273
+ * primary key — which is the only place an auto key is knowable.
13274
+ */
13275
+ id: union([string(), number()]) }), {
13276
+ kind: "mutation",
13277
+ auth: "admin"
13278
+ }), method(object({
13279
+ namespace: string().optional(),
13280
+ collection: string(),
13281
+ records: array(BulkRecordSchema).readonly()
13282
+ }), object({ inserted: number().int() }), {
13283
+ kind: "mutation",
13284
+ auth: "admin"
13285
+ }), method(object({
13286
+ namespace: string().optional(),
13287
+ guard: TransactGuardSchema.optional(),
13288
+ ops: array(TransactOpSchema).readonly()
13289
+ }), object({
13290
+ applied: boolean(),
13291
+ current: unknown().optional()
13292
+ }), {
13293
+ kind: "mutation",
13294
+ auth: "admin"
13295
+ }), method(object({
13296
+ namespace: string().optional(),
13297
+ collection: string(),
13298
+ id: string(),
13299
+ data: record(string(), unknown())
13300
+ }), _void(), {
13301
+ kind: "mutation",
13302
+ auth: "admin"
13303
+ }), method(object({
13304
+ namespace: string().optional(),
13305
+ collection: string(),
13306
+ key: string()
13307
+ }), _void(), {
13308
+ kind: "mutation",
13309
+ auth: "admin"
13310
+ }), method(object({
13311
+ namespace: string().optional(),
13312
+ collection: string(),
13313
+ filter: MutationFilterSchema
13314
+ }), object({ deleted: number().int() }), {
13315
+ kind: "mutation",
13316
+ auth: "admin"
13317
+ }), method(object({
13318
+ namespace: string().optional(),
13319
+ collection: string(),
13320
+ filter: MutationFilterSchema,
13321
+ data: record(string(), unknown())
13322
+ }), object({ updated: number().int() }), {
13323
+ kind: "mutation",
13324
+ auth: "admin"
13325
+ }), method(object({
13326
+ namespace: string().optional(),
13327
+ collection: string(),
13328
+ filter: QueryFilterSchema.optional()
13329
+ }), number(), { auth: "admin" }), method(object({
13330
+ namespace: string().optional(),
13331
+ collection: string(),
13332
+ fields: array(AggregateFieldSchema).readonly(),
13333
+ filter: QueryFilterSchema.optional()
13334
+ }), AggregateResultSchema, { auth: "admin" }), method(object({
13335
+ namespace: string().optional(),
13336
+ collection: string(),
13337
+ field: string(),
13338
+ bucketSize: number().int().positive(),
13339
+ origin: number().int(),
13340
+ filter: QueryFilterSchema.optional()
13341
+ }), array(object({
13342
+ bucket: number().int(),
13343
+ count: number().int()
13344
+ })).readonly(), { auth: "admin" }), method(object({
13345
+ namespace: string().optional(),
13346
+ collection: string()
13347
+ }), boolean(), { auth: "admin" }), method(DeclareCollectionInputSchema, _void(), {
13348
+ kind: "mutation",
13349
+ auth: "admin"
13350
+ });
13351
+ 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>\"");
13352
+ var MigrationLevelSchema = _enum([
13353
+ "hub-store",
13354
+ "addon-state",
13355
+ "node-config",
13356
+ "viewer",
13357
+ "addon-internal"
13358
+ ]);
13359
+ var MigrationStoreSchema = _enum([
13360
+ "settings",
13361
+ "events",
13362
+ "history",
13363
+ "node-local",
13364
+ "addon-files"
13365
+ ]);
13366
+ var MigrationScopeSchema = _enum(["cluster", "per-node"]);
13367
+ /** What depends on a migration while it is pending: `block-owner` is the default. */
13368
+ var MigrationOnFailureSchema = _enum(["block-owner", "tolerate"]);
13369
+ /**
13370
+ * `sql` is a hub-only body over the owner's connection. `plan` returns edits the
13371
+ * engine applies inside the migration's own transaction (hub-owned: computed in
13372
+ * that transaction; addon-owned: computed by the owner and shipped to the engine).
13373
+ * `procedure` is addon-owned work that is not SQL (keychain, integration registry,
13374
+ * device tombstones); the ledger records its outcome and it must be idempotent.
13375
+ */
13376
+ var MigrationBodyKindSchema = _enum([
13377
+ "sql",
13378
+ "plan",
13379
+ "procedure"
13380
+ ]);
13381
+ var MigrationMetaSchema = object({
13382
+ id: MigrationIdSchema,
13383
+ /** `hub`, an addon id, `node` or `viewer`. */
13384
+ owner: string().regex(/^[a-z0-9-]+$/),
13385
+ level: MigrationLevelSchema,
13386
+ /** Strictly increasing per owner: the order within an owner. */
13387
+ seq: number().int().positive(),
13388
+ /** Ids of any owner: the order across owners. */
13389
+ dependsOn: array(MigrationIdSchema),
13390
+ store: MigrationStoreSchema,
13391
+ scope: MigrationScopeSchema,
13392
+ onFailure: MigrationOnFailureSchema,
13393
+ bodyKind: MigrationBodyKindSchema,
13394
+ description: string().min(1),
13395
+ /** The marked field the migration's spec must assert survived. */
13396
+ fixtureMark: string().min(1)
13397
+ }).refine((m) => m.id.startsWith(`${m.owner}/`), {
13398
+ message: "the id must start with \"<owner>/\"",
13399
+ path: ["id"]
13400
+ });
13401
+ /** A cycle in the dependency graph, named by the ids on it. */
13402
+ var MigrationCycleError = class extends Error {
13403
+ ids;
13404
+ constructor(ids) {
13405
+ super(`migration dependsOn cycle: ${ids.join(" -> ")}`);
13406
+ this.ids = ids;
13407
+ this.name = "MigrationCycleError";
13408
+ }
13409
+ };
13410
+ /**
13411
+ * Topological order over `dependsOn` plus the implicit edge "a lower `seq` of an
13412
+ * owner runs first". Ties are broken by `(owner, seq)`. Throws
13413
+ * {@link MigrationCycleError} on a cycle. A `dependsOn` id that is not in the list
13414
+ * is ignored here — the catalogue schema is what refuses it.
13415
+ */
13416
+ function orderMigrations(items) {
13417
+ const byId = new Map(items.map((item) => [item.id, item]));
13418
+ const deps = /* @__PURE__ */ new Map();
13419
+ const ownerSorted = /* @__PURE__ */ new Map();
13420
+ for (const item of items) {
13421
+ const list = ownerSorted.get(item.owner) ?? [];
13422
+ list.push(item);
13423
+ ownerSorted.set(item.owner, list);
13424
+ }
13425
+ for (const item of items) deps.set(item.id, new Set(item.dependsOn.filter((id) => byId.has(id))));
13426
+ for (const list of ownerSorted.values()) {
13427
+ list.sort((a, b) => a.seq - b.seq);
13428
+ for (let i = 1; i < list.length; i++) {
13429
+ const previous = list[i - 1];
13430
+ const current = list[i];
13431
+ if (previous !== void 0 && current !== void 0) deps.get(current.id)?.add(previous.id);
13432
+ }
13433
+ }
13434
+ const tie = (a, b) => a.owner < b.owner ? -1 : a.owner > b.owner ? 1 : a.seq - b.seq;
13435
+ const done = /* @__PURE__ */ new Set();
13436
+ const ordered = [];
13437
+ let remaining = [...items];
13438
+ while (remaining.length > 0) {
13439
+ const ready = remaining.filter((item) => [...deps.get(item.id) ?? []].every((dep) => done.has(dep)));
13440
+ if (ready.length === 0) throw new MigrationCycleError(remaining.map((item) => item.id).sort());
13441
+ ready.sort(tie);
13442
+ const next = ready[0];
13443
+ if (next === void 0) break;
13444
+ ordered.push(next);
13445
+ done.add(next.id);
13446
+ remaining = remaining.filter((item) => item.id !== next.id);
13447
+ }
13448
+ return ordered;
13449
+ }
13450
+ /** Every way a list of migrations (in declaration order) is not a catalogue. */
13451
+ function catalogueIssues(items) {
13452
+ const issues = [];
13453
+ const seen = /* @__PURE__ */ new Set();
13454
+ const known = new Set(items.map((item) => item.id));
13455
+ const lastSeq = /* @__PURE__ */ new Map();
13456
+ const seqUsed = /* @__PURE__ */ new Map();
13457
+ for (const item of items) {
13458
+ if (seen.has(item.id)) issues.push({
13459
+ code: "duplicate-id",
13460
+ ids: [item.id],
13461
+ message: `duplicate id ${item.id}`
13462
+ });
13463
+ seen.add(item.id);
13464
+ const used = seqUsed.get(item.owner) ?? /* @__PURE__ */ new Set();
13465
+ if (used.has(item.seq)) issues.push({
13466
+ code: "duplicate-seq",
13467
+ ids: [item.id],
13468
+ message: `owner ${item.owner} has two migrations with seq ${item.seq}`
13469
+ });
13470
+ else if (item.seq < (lastSeq.get(item.owner) ?? 0)) issues.push({
13471
+ code: "seq-order",
13472
+ ids: [item.id],
13473
+ message: `seq ${item.seq} of ${item.id} does not increase in declaration order (owner ${item.owner})`
13474
+ });
13475
+ used.add(item.seq);
13476
+ seqUsed.set(item.owner, used);
13477
+ lastSeq.set(item.owner, Math.max(item.seq, lastSeq.get(item.owner) ?? 0));
13478
+ for (const dep of item.dependsOn) if (!known.has(dep)) issues.push({
13479
+ code: "unknown-dependency",
13480
+ ids: [item.id, dep],
13481
+ message: `${item.id} dependsOn unknown id ${dep}`
13482
+ });
13483
+ }
13484
+ try {
13485
+ orderMigrations(items);
13486
+ } catch (err) {
13487
+ if (err instanceof MigrationCycleError) issues.push({
13488
+ code: "cycle",
13489
+ ids: err.ids,
13490
+ message: err.message
13491
+ });
13492
+ else throw err;
13493
+ }
13494
+ return issues;
13495
+ }
13496
+ array(MigrationMetaSchema).superRefine((items, ctx) => {
13497
+ for (const issue of catalogueIssues(items)) ctx.addIssue({
13498
+ code: "custom",
13499
+ message: `${issue.code}: ${issue.message}`
13500
+ });
13501
+ });
13502
+ /**
13503
+ * The canonical JSON (`preconditionOf`) of the value a plan read; the apply step
13504
+ * re-reads, and a plan whose read no longer matches is refused (the migration is
13505
+ * `failed`, nothing it edited is kept).
13506
+ */
13507
+ var PreconditionSchema = string().min(1).optional();
13508
+ var MigrationPlanSchema = object({ edits: array(discriminatedUnion("op", [
13509
+ object({
13510
+ op: literal("setKeys"),
13511
+ scope: string().min(1),
13512
+ values: record(string(), unknown()),
13513
+ precondition: PreconditionSchema
13514
+ }),
13515
+ object({
13516
+ op: literal("deleteKeys"),
13517
+ scope: string().min(1),
13518
+ keys: array(string().min(1)).min(1),
13519
+ precondition: PreconditionSchema
13520
+ }),
13521
+ object({
13522
+ op: literal("putRow"),
13523
+ table: string().min(1),
13524
+ key: record(string(), unknown()),
13525
+ row: record(string(), unknown()),
13526
+ precondition: PreconditionSchema
13527
+ }),
13528
+ object({
13529
+ op: literal("deleteRow"),
13530
+ table: string().min(1),
13531
+ key: record(string(), unknown()),
13532
+ precondition: PreconditionSchema
13533
+ }),
13534
+ object({
13535
+ op: literal("upsertRecord"),
13536
+ kind: string().min(1),
13537
+ recordKey: string().min(1),
13538
+ value: unknown(),
13539
+ precondition: PreconditionSchema
13540
+ }),
13541
+ object({
13542
+ op: literal("deleteRecord"),
13543
+ kind: string().min(1),
13544
+ recordKey: string().min(1),
13545
+ precondition: PreconditionSchema
13546
+ })
13547
+ ])) });
13548
+ /**
13549
+ * What a `procedure` body reports. `already-done` means the rerun observed the
13550
+ * work finished (a crash between the work and the ledger row, or a manual fix)
13551
+ * and did none of it again; the ledger records `applied` either way.
13552
+ */
13553
+ var MigrationProcedureResultSchema = object({
13554
+ outcome: _enum(["done", "already-done"]),
13555
+ summary: record(string(), unknown())
13556
+ });
13557
+ /** What an owner hands the engine when one of its migrations ran. */
13558
+ var MigrationOwnerResultSchema = discriminatedUnion("kind", [
13559
+ object({
13560
+ kind: literal("plan"),
13561
+ plan: MigrationPlanSchema
13562
+ }),
13563
+ object({
13564
+ kind: literal("procedure"),
13565
+ result: MigrationProcedureResultSchema
13566
+ }),
13567
+ object({
13568
+ kind: literal("failed"),
13569
+ error: string().min(1)
13570
+ })
13571
+ ]);
13572
+ /**
13573
+ * `held`: a restore of a pre-migration snapshot undid this id; it does not rerun
13574
+ * until the operator retries or a new code version ships.
13575
+ * `skipped-newer`: the database knows an id this code does not.
13576
+ */
13577
+ var MigrationStatusSchema = _enum([
13578
+ "applied",
13579
+ "failed",
13580
+ "blocked",
13581
+ "running",
13582
+ "held",
13583
+ "skipped-newer"
13584
+ ]);
13585
+ var MigrationLedgerEntrySchema = object({
13586
+ id: string(),
13587
+ database: DataStoreDatabaseSchema,
13588
+ owner: string(),
13589
+ level: MigrationLevelSchema,
13590
+ seq: number().int().nonnegative(),
13591
+ status: MigrationStatusSchema,
13592
+ attempts: number().int().nonnegative(),
13593
+ appliedAt: number().nullable(),
13594
+ codeVersion: string().nullable(),
13595
+ /** What the apply changed: rows per table, as the engine counted them. */
13596
+ planSummary: record(string(), unknown()).nullable(),
13597
+ progress: record(string(), unknown()).nullable(),
13598
+ error: string().nullable(),
13599
+ /** Why it is `blocked` or `held` (`dependency-failed`, `snapshot-refused`, …). */
13600
+ reason: string().nullable(),
13601
+ nodeId: string().nullable(),
13602
+ bootId: string().nullable()
13603
+ });
13604
+ /** A catalogued migration that is not applied, with where it stands. */
13605
+ var MigrationPendingEntrySchema = object({
13606
+ meta: MigrationMetaSchema,
13607
+ database: DataStoreDatabaseSchema,
13608
+ state: _enum([
13609
+ "pending",
13610
+ "failed",
13611
+ "blocked",
13612
+ "held"
13613
+ ]),
13614
+ reason: string().nullable()
13615
+ });
13616
+ /** Rows per table before and after a dry run, and what it would have changed. */
13617
+ var MigrationDryRunResultSchema = object({
13618
+ id: string(),
13619
+ ok: boolean(),
13620
+ error: string().nullable(),
13621
+ rowsChanged: number().int().nonnegative(),
13622
+ tables: array(object({
13623
+ table: string(),
13624
+ before: number().int().nonnegative(),
13625
+ after: number().int().nonnegative()
13626
+ }))
13627
+ });
13628
+ /** One of an owner's migrations, as the engine tells the owner to treat it this boot. */
13629
+ var MigrationOwnerStepSchema = object({
13630
+ id: MigrationIdSchema,
13631
+ action: _enum(["run", "skip"]),
13632
+ /** The ledger state the engine found: `pending` has no row yet. */
13633
+ state: union([literal("pending"), MigrationStatusSchema])
13634
+ });
13635
+ /**
12833
13636
  * The trainer contract — what the hub hands the cloud trainer and what it
12834
13637
  * gets back (cloud training, D805+).
12835
13638
  *
@@ -16232,6 +17035,41 @@ var ACTUATOR_CAP_VOCABULARY = {
16232
17035
  icon: "droplets"
16233
17036
  }]
16234
17037
  },
17038
+ "power-source": {
17039
+ scope: "device",
17040
+ noun: t("Power source", "Alimentazione"),
17041
+ actions: [
17042
+ {
17043
+ id: "powerSource.on",
17044
+ method: "powerOn",
17045
+ label: t("Power on", "Dai corrente"),
17046
+ phrase: t("Power on {device}", "Dai corrente a {device}"),
17047
+ icon: "power"
17048
+ },
17049
+ {
17050
+ id: "powerSource.off",
17051
+ method: "powerOff",
17052
+ label: t("Power off", "Togli corrente"),
17053
+ phrase: t("Cut the power of {device}", "Togli la corrente a {device}"),
17054
+ icon: "power-off"
17055
+ },
17056
+ {
17057
+ id: "powerSource.forceRestart",
17058
+ method: "forceRestart",
17059
+ label: t("Force restart", "Riavvio forzato"),
17060
+ phrase: t("Force restart {device} by cutting its power", "Riavvia {device} togliendo la corrente"),
17061
+ icon: "refresh-cw"
17062
+ }
17063
+ ],
17064
+ state: [{
17065
+ kind: "boolean",
17066
+ field: "switchOn",
17067
+ label: t("Power switch on", "Presa accesa"),
17068
+ whenTrue: t("When the power of {device} comes on", "Quando arriva la corrente a {device}"),
17069
+ whenFalse: t("When the power of {device} is cut", "Quando manca la corrente a {device}"),
17070
+ icon: "power"
17071
+ }]
17072
+ },
16235
17073
  "lock-control": {
16236
17074
  scope: "device",
16237
17075
  noun: t("Lock", "Serratura"),
@@ -16707,7 +17545,7 @@ var ACTUATOR_CAP_VOCABULARY = {
16707
17545
  method: "setDirection",
16708
17546
  label: t("Spin forward", "Rotazione avanti"),
16709
17547
  phrase: t("Spin {device} forward", "Fai girare {device} in avanti"),
16710
- icon: "rotate-cw",
17548
+ icon: "refresh-cw",
16711
17549
  effect: {
16712
17550
  field: "direction",
16713
17551
  value: "forward"
@@ -19785,6 +20623,7 @@ var SYSTEM_CAP_VOCABULARY = {
19785
20623
  "login-method": system(IDENTITY),
19786
20624
  "mesh-network": system(NETWORK),
19787
20625
  "metrics-provider": system(CLUSTER),
20626
+ migrations: system(INTERNAL),
19788
20627
  "model-convert": system(ML),
19789
20628
  "model-distributor": system(ML),
19790
20629
  "mqtt-broker": system(NETWORK),
@@ -42128,7 +42967,7 @@ method(object({}), array(TargetKindSchema)), method(object({}), array(TargetSche
42128
42967
  * rebuilds without manual reload.
42129
42968
  *
42130
42969
  * The hub-local builtin `addon-pages-aggregator` (see
42131
- * `@camstack/system/builtins/addon-pages-aggregator`) registers the
42970
+ * `@camstack/server/system/builtins/addon-pages-aggregator`) registers the
42132
42971
  * provider. Splitting the public aggregator from the raw collection
42133
42972
  * keeps both ends in codegen — there's no hand-written
42134
42973
  * `addon-pages.router.ts` wrapper anymore.
@@ -42428,7 +43267,7 @@ method(_void(), array(WidgetMetadataSchema).readonly(), { auth: "admin" });
42428
43267
  * manual reload — same scheme used by `addon-pages`.
42429
43268
  *
42430
43269
  * The hub-local builtin `addon-widgets-aggregator` (see
42431
- * `@camstack/system/builtins/addon-widgets-aggregator`) registers the
43270
+ * `@camstack/server/system/builtins/addon-widgets-aggregator`) registers the
42432
43271
  * provider. Splitting the public aggregator from the raw collection
42433
43272
  * keeps both ends in codegen — there's no hand-written wrapper.
42434
43273
  */
@@ -42789,329 +43628,6 @@ method(object({
42789
43628
  password: string()
42790
43629
  }), AuthResultSchema.nullable(), { kind: "mutation" }), method(object({ state: string() }), string()), method(record(string(), string()), AuthResultSchema, { kind: "mutation" }), method(object({ token: string() }), AuthResultSchema.nullable());
42791
43630
  /**
42792
- * Query filter for settings-store collections.
42793
- */
42794
- var QueryFilterSchema = object({
42795
- where: record(string(), unknown()).optional(),
42796
- whereIn: record(string(), array(unknown())).optional(),
42797
- whereBetween: record(string(), tuple([unknown(), unknown()])).optional(),
42798
- /** NULL-safe exclusion: matches rows whose field is NULL OR != the value. */
42799
- whereNot: record(string(), unknown()).optional(),
42800
- /** NULL-safe exclusion of a SET — `whereNot` for more than one value. An
42801
- * empty list excludes nothing. See `QueryFilter.whereNotIn`. */
42802
- whereNotIn: record(string(), array(unknown())).optional(),
42803
- orderBy: object({
42804
- field: string(),
42805
- direction: _enum(["asc", "desc"])
42806
- }).optional(),
42807
- limit: number().optional(),
42808
- offset: number().optional()
42809
- });
42810
- /**
42811
- * The predicate half of a filter, for BULK MUTATIONS.
42812
- *
42813
- * Deliberately not `QueryFilterSchema`: `orderBy` / `limit` / `offset` have no
42814
- * meaning for a statement that rewrites a set, and accepting them would invite
42815
- * a caller to believe `limit` bounds the damage. Every field is optional here
42816
- * only so the shape stays composable — the implementation REJECTS a filter
42817
- * that compiles to no predicate, because that is the whole collection.
42818
- */
42819
- var MutationFilterSchema = object({
42820
- where: record(string(), unknown()).optional(),
42821
- whereIn: record(string(), array(unknown())).optional(),
42822
- whereBetween: record(string(), tuple([unknown(), unknown()])).optional(),
42823
- whereNot: record(string(), unknown()).optional(),
42824
- whereNotIn: record(string(), array(unknown())).optional()
42825
- });
42826
- /**
42827
- * One scalar an {@link settingsStoreCapability.methods.aggregate} call asks for.
42828
- *
42829
- * `as` names the slot in the result, so the SAME column may be asked twice with
42830
- * two operations (`MIN(startMs)` and `MAX(startMs)` in one round trip) — which
42831
- * a `Record<column, op>` shape could not express.
42832
- */
42833
- var AggregateFieldSchema = object({
42834
- /** Result key. */
42835
- as: string().min(1),
42836
- /** Column to aggregate. Must be a real column of a declared collection. */
42837
- field: string().min(1),
42838
- op: _enum([
42839
- "sum",
42840
- "min",
42841
- "max"
42842
- ])
42843
- });
42844
- /**
42845
- * `COUNT(*)` plus one number per requested field.
42846
- *
42847
- * `null` means NO ROW MATCHED, never zero: a `SUM` over an empty set and a sum
42848
- * that really is 0 are different facts, and an accounting caller that renders
42849
- * "0 bytes, oldest = 0" for "nothing here" reports a lie about a disk.
42850
- */
42851
- var AggregateResultSchema = object({
42852
- count: number().int(),
42853
- values: record(string(), number().nullable())
42854
- });
42855
- /** A single stored record: `{ id, data }`. */
42856
- var SettingsRecordSchema = object({
42857
- id: string(),
42858
- data: record(string(), unknown())
42859
- });
42860
- /**
42861
- * One record of a BULK insert — {@link SettingsRecordSchema} with the id made
42862
- * optional.
42863
- *
42864
- * A separate schema rather than loosening the shared one: every other method
42865
- * on this cap addresses a row BY its id, and making that field optional
42866
- * everywhere would turn a forgotten key into a silently generated one on
42867
- * `update` and `delete` as well.
42868
- */
42869
- var BulkRecordSchema = object({
42870
- id: string().optional(),
42871
- data: record(string(), unknown())
42872
- });
42873
- /**
42874
- * The database FILES one `sqlite-settings` engine process owns (D851).
42875
- * Closed on purpose: a file is a thing the engine opens, backs up and gives a
42876
- * serving thread, so a new one is a code change, never a string a caller
42877
- * invents. `settings` is `camstack.db` and the default for every collection
42878
- * that names no file.
42879
- */
42880
- var DataStoreDatabaseSchema = _enum([
42881
- "settings",
42882
- "events",
42883
- "history"
42884
- ]);
42885
- DataStoreDatabaseSchema.options;
42886
- /**
42887
- * One write inside a {@link settingsStoreCapability.methods.transact} call.
42888
- * The three that a "replace this set, bump that revision" write needs, plus
42889
- * the batched upsert a rollup writer needs.
42890
- */
42891
- var TransactOpSchema = discriminatedUnion("op", [
42892
- object({
42893
- op: literal("deleteWhere"),
42894
- collection: string(),
42895
- filter: MutationFilterSchema
42896
- }),
42897
- object({
42898
- op: literal("insertMany"),
42899
- collection: string(),
42900
- records: array(BulkRecordSchema).readonly()
42901
- }),
42902
- object({
42903
- op: literal("update"),
42904
- collection: string(),
42905
- id: string(),
42906
- data: record(string(), unknown())
42907
- }),
42908
- object({
42909
- op: literal("upsertMany"),
42910
- collection: string(),
42911
- conflict: array(string()).readonly(),
42912
- records: array(BulkRecordSchema).readonly()
42913
- })
42914
- ]);
42915
- /**
42916
- * The precondition of a transaction: the row `key` of `collection` must hold
42917
- * `equals` in `column`, read INSIDE the transaction.
42918
- */
42919
- var TransactGuardSchema = object({
42920
- collection: string(),
42921
- key: string(),
42922
- column: string(),
42923
- equals: unknown()
42924
- });
42925
- /**
42926
- * Column declaration for a structured (SQL-backed) collection.
42927
- *
42928
- * Logical types — the backend translates each to the matching SQLite
42929
- * storage class and handles per-type marshaling:
42930
- * - `TEXT` / `INTEGER` / `REAL` — native SQLite types, pass-through
42931
- * - `JSON` — TEXT under the hood; serialised on write, parsed on read
42932
- * - `BOOLEAN` — INTEGER 0/1 under the hood; coerced both directions
42933
- */
42934
- var CollectionColumnSchema = object({
42935
- name: string(),
42936
- type: _enum([
42937
- "TEXT",
42938
- "INTEGER",
42939
- "REAL",
42940
- "JSON",
42941
- "BOOLEAN"
42942
- ]),
42943
- primaryKey: boolean().optional(),
42944
- notNull: boolean().optional(),
42945
- unique: boolean().optional(),
42946
- /**
42947
- * Column DEFAULT. Required for B2: the four `ensureTable` consumers declare
42948
- * columns like `enabled INTEGER NOT NULL DEFAULT 1`, and without this the
42949
- * collection surface simply cannot express their existing tables — which is
42950
- * why they were still on the uncapped `table*` API.
42951
- */
42952
- defaultValue: union([
42953
- string(),
42954
- number(),
42955
- boolean()
42956
- ]).optional()
42957
- });
42958
- var CollectionIndexSchema = object({
42959
- name: string(),
42960
- columns: array(string()).readonly(),
42961
- unique: boolean().optional()
42962
- });
42963
- /**
42964
- * The input of `declareCollection` — ONE schema for the door and for the
42965
- * engine behind it, so the two surfaces cannot drift (the dispatcher forwards
42966
- * the payload untouched).
42967
- *
42968
- * - `database` — the file the collection lives in (D851). Absent means
42969
- * `settings`. A declaration naming a file other than the one the table is
42970
- * already in is REFUSED by the engine, never moved.
42971
- * - `primaryKey` — a COMPOSITE key, as column names. A single name is the same
42972
- * as `primaryKey: true` on that column. A composite-keyed collection has no
42973
- * `id`: it is written with `insertMany` / `upsertMany` / `transact` and read
42974
- * with `query` / `count` / `aggregate` / `deleteWhere`; the calls that
42975
- * address one row by id are refused.
42976
- * - `withoutRowid` — emit `WITHOUT ROWID`. Requires a primary key.
42977
- */
42978
- var DeclareCollectionInputSchema = object({
42979
- namespace: string().optional(),
42980
- collection: string(),
42981
- columns: array(CollectionColumnSchema).readonly(),
42982
- indexes: array(CollectionIndexSchema).readonly().optional(),
42983
- database: DataStoreDatabaseSchema.optional(),
42984
- primaryKey: array(string()).readonly().optional(),
42985
- withoutRowid: boolean().optional()
42986
- });
42987
- method(object({
42988
- namespace: string().optional(),
42989
- collection: string(),
42990
- key: string()
42991
- }), unknown(), { auth: "admin" }), method(object({
42992
- namespace: string().optional(),
42993
- collection: string(),
42994
- key: string(),
42995
- value: unknown()
42996
- }), _void(), {
42997
- kind: "mutation",
42998
- auth: "admin"
42999
- }), method(object({
43000
- namespace: string().optional(),
43001
- collection: string(),
43002
- filter: QueryFilterSchema.optional(),
43003
- columns: array(string()).readonly().optional()
43004
- }), array(SettingsRecordSchema).readonly(), { auth: "admin" }), method(object({
43005
- namespace: string().optional(),
43006
- collection: string(),
43007
- /** Declared columns identifying the slot; must be covered by a UNIQUE
43008
- * index on the collection. Never empty. */
43009
- conflict: array(string()).readonly(),
43010
- record: BulkRecordSchema
43011
- }), object({
43012
- /** The id the row HAS: assigned by SQLite on an insert, or the existing
43013
- * row's own id on an update. */
43014
- id: union([string(), number()]) }), {
43015
- kind: "mutation",
43016
- auth: "admin"
43017
- }), method(object({
43018
- namespace: string().optional(),
43019
- collection: string(),
43020
- conflict: array(string()).readonly(),
43021
- records: array(BulkRecordSchema).readonly()
43022
- }), object({ upserted: number().int() }), {
43023
- kind: "mutation",
43024
- auth: "admin"
43025
- }), method(object({
43026
- namespace: string().optional(),
43027
- collection: string(),
43028
- record: object({
43029
- id: string().optional(),
43030
- data: record(string(), unknown())
43031
- })
43032
- }), object({
43033
- /**
43034
- * The id the row ACTUALLY got (D473): the one supplied, the UUID minted
43035
- * for an absent one, or the ROWID SQLite assigned on an `INTEGER`
43036
- * primary key — which is the only place an auto key is knowable.
43037
- */
43038
- id: union([string(), number()]) }), {
43039
- kind: "mutation",
43040
- auth: "admin"
43041
- }), method(object({
43042
- namespace: string().optional(),
43043
- collection: string(),
43044
- records: array(BulkRecordSchema).readonly()
43045
- }), object({ inserted: number().int() }), {
43046
- kind: "mutation",
43047
- auth: "admin"
43048
- }), method(object({
43049
- namespace: string().optional(),
43050
- guard: TransactGuardSchema.optional(),
43051
- ops: array(TransactOpSchema).readonly()
43052
- }), object({
43053
- applied: boolean(),
43054
- current: unknown().optional()
43055
- }), {
43056
- kind: "mutation",
43057
- auth: "admin"
43058
- }), method(object({
43059
- namespace: string().optional(),
43060
- collection: string(),
43061
- id: string(),
43062
- data: record(string(), unknown())
43063
- }), _void(), {
43064
- kind: "mutation",
43065
- auth: "admin"
43066
- }), method(object({
43067
- namespace: string().optional(),
43068
- collection: string(),
43069
- key: string()
43070
- }), _void(), {
43071
- kind: "mutation",
43072
- auth: "admin"
43073
- }), method(object({
43074
- namespace: string().optional(),
43075
- collection: string(),
43076
- filter: MutationFilterSchema
43077
- }), object({ deleted: number().int() }), {
43078
- kind: "mutation",
43079
- auth: "admin"
43080
- }), method(object({
43081
- namespace: string().optional(),
43082
- collection: string(),
43083
- filter: MutationFilterSchema,
43084
- data: record(string(), unknown())
43085
- }), object({ updated: number().int() }), {
43086
- kind: "mutation",
43087
- auth: "admin"
43088
- }), method(object({
43089
- namespace: string().optional(),
43090
- collection: string(),
43091
- filter: QueryFilterSchema.optional()
43092
- }), number(), { auth: "admin" }), method(object({
43093
- namespace: string().optional(),
43094
- collection: string(),
43095
- fields: array(AggregateFieldSchema).readonly(),
43096
- filter: QueryFilterSchema.optional()
43097
- }), AggregateResultSchema, { auth: "admin" }), method(object({
43098
- namespace: string().optional(),
43099
- collection: string(),
43100
- field: string(),
43101
- bucketSize: number().int().positive(),
43102
- origin: number().int(),
43103
- filter: QueryFilterSchema.optional()
43104
- }), array(object({
43105
- bucket: number().int(),
43106
- count: number().int()
43107
- })).readonly(), { auth: "admin" }), method(object({
43108
- namespace: string().optional(),
43109
- collection: string()
43110
- }), boolean(), { auth: "admin" }), method(DeclareCollectionInputSchema, _void(), {
43111
- kind: "mutation",
43112
- auth: "admin"
43113
- });
43114
- /**
43115
43631
  * How long one snapshot may take end to end: the pinned copy of the whole
43116
43632
  * database plus `PRAGMA integrity_check` over the copy, on the hub's slowest
43117
43633
  * disk. ~800 MB at a pessimistic 10 MB/s is under 3 minutes; the bound is
@@ -45025,7 +45541,7 @@ method(object({
45025
45541
  kind: "mutation",
45026
45542
  auth: "admin"
45027
45543
  }), method(object({ blockIds: array(string()) }), object({ states: array(CompositionBlockStateSchema) }), { auth: "admin" });
45028
- 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;
45544
+ 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;
45029
45545
  var NUMBER = {
45030
45546
  kind: "fixed",
45031
45547
  result: ["number"]
@@ -46036,6 +46552,57 @@ id: union([string(), number()]) }), {
46036
46552
  kind: "mutation",
46037
46553
  auth: "admin"
46038
46554
  });
46555
+ 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({
46556
+ id: string().min(1),
46557
+ error: string().min(1)
46558
+ }), MigrationLedgerEntrySchema, {
46559
+ kind: "mutation",
46560
+ auth: "admin"
46561
+ }), method(object({ id: string().min(1) }), object({ entries: array(MigrationLedgerEntrySchema) }), {
46562
+ kind: "mutation",
46563
+ auth: "admin"
46564
+ }), method(object({
46565
+ database: DataStoreDatabaseSchema,
46566
+ snapshotId: string().min(1)
46567
+ }), object({
46568
+ scheduled: boolean(),
46569
+ held: array(string()),
46570
+ restartRequired: boolean()
46571
+ }), {
46572
+ kind: "mutation",
46573
+ auth: "admin"
46574
+ }), method(object({
46575
+ owner: string().min(1),
46576
+ metas: array(MigrationMetaSchema)
46577
+ }), object({
46578
+ bootId: string(),
46579
+ snapshot: _enum([
46580
+ "none",
46581
+ "written",
46582
+ "refused"
46583
+ ]),
46584
+ steps: array(MigrationOwnerStepSchema)
46585
+ }), {
46586
+ kind: "mutation",
46587
+ auth: "admin"
46588
+ }), method(object({ id: string().min(1) }), object({
46589
+ proceed: boolean(),
46590
+ state: union([literal("pending"), MigrationStatusSchema]),
46591
+ reason: string().nullable()
46592
+ }), {
46593
+ kind: "mutation",
46594
+ auth: "admin"
46595
+ }), method(object({
46596
+ id: string().min(1),
46597
+ result: MigrationOwnerResultSchema
46598
+ }), object({
46599
+ status: MigrationStatusSchema,
46600
+ error: string().nullable(),
46601
+ rowsChanged: number().int().nonnegative()
46602
+ }), {
46603
+ kind: "mutation",
46604
+ auth: "admin"
46605
+ });
46039
46606
  /**
46040
46607
  * records - an addon's own facts and documents (D881): ledgers, audit rows,
46041
46608
  * config documents, mirrors. The third storage API next to `ctx.settings`
@@ -60900,7 +61467,18 @@ var ServerUpdateActionResultSchema = object({
60900
61467
  * store (D877), present when the pre-flight refused the update
60901
61468
  * (`reason: 'addon-would-refuse'`). Optional for version skew.
60902
61469
  */
60903
- blockers: array(UpdateBlockerSchema).optional()
61470
+ blockers: array(UpdateBlockerSchema).optional(),
61471
+ /**
61472
+ * The minimums the target server declares and this node does not meet
61473
+ * (D989), present when the update was refused with
61474
+ * `reason: 'requirements-unmet'`. Each names the versions on both sides.
61475
+ */
61476
+ unmet: array(UnmetRequirementSchema).optional(),
61477
+ /**
61478
+ * The addons this node has below the minimum the staged server declares —
61479
+ * the ones to update WITH the server. Present on an accepted update.
61480
+ */
61481
+ requiredAddonUpdates: array(RequiredAddonUpdateSchema).optional()
60904
61482
  });
60905
61483
  var LaunchOverridesSchema = object({
60906
61484
  /** The persisted heap ceiling in MB, or `null` when none is set. */
@@ -63444,7 +64022,9 @@ var PackageUpdateSchema = object({
63444
64022
  latestVersion: string(),
63445
64023
  category: _enum(["addon", "core"]),
63446
64024
  requiresRestart: boolean(),
63447
- isSystem: boolean()
64025
+ isSystem: boolean(),
64026
+ /** Held: what the newer version needs and this node does not meet (D989). */
64027
+ blockedBy: array(UnmetRequirementSchema).readonly().optional()
63448
64028
  });
63449
64029
  var PackageVersionInfoSchema = object({
63450
64030
  version: string(),
@@ -64900,19 +65480,19 @@ var RemoteSetConfigResultSchema = discriminatedUnion("status", [object({ status:
64900
65480
  reason: _enum(["unknown-template", "not-a-media-player"]),
64901
65481
  detail: string()
64902
65482
  })]);
64903
- var deviceInput = object({ deviceId: number().int().nonnegative() });
65483
+ var deviceInput$1 = object({ deviceId: number().int().nonnegative() });
64904
65484
  DeviceType.MediaPlayer;
64905
- method(deviceInput, RemoteConfigSchema, { auth: "admin" }), method(deviceInput.extend({ config: RemoteConfigSchema }), RemoteSetConfigResultSchema, {
65485
+ method(deviceInput$1, RemoteConfigSchema, { auth: "admin" }), method(deviceInput$1.extend({ config: RemoteConfigSchema }), RemoteSetConfigResultSchema, {
64906
65486
  kind: "mutation",
64907
65487
  auth: "admin"
64908
- }), method(deviceInput, RemoteCommandListSchema, { auth: "protected" }), method(deviceInput.extend({
65488
+ }), method(deviceInput$1, RemoteCommandListSchema, { auth: "protected" }), method(deviceInput$1.extend({
64909
65489
  role: RemoteRoleSchema,
64910
65490
  /** Sends in a row (volume steps). */
64911
65491
  repeat: number().int().min(1).max(50).default(1)
64912
65492
  }), RemoteSendResultSchema, {
64913
65493
  kind: "mutation",
64914
65494
  auth: "protected"
64915
- }), method(deviceInput.extend({
65495
+ }), method(deviceInput$1.extend({
64916
65496
  command: RemoteMediaCommandSchema,
64917
65497
  muted: boolean().optional(),
64918
65498
  source: string().optional(),
@@ -68735,7 +69315,7 @@ var lawnMowerControlCapability = {
68735
69315
  * clients — they reverse-connect to the hub. Exposing their interfaces
68736
69316
  * via the same surface would leak internal topology with no upside.
68737
69317
  *
68738
- * Implementation in `@camstack/system/builtins/local-network/`.
69318
+ * Implementation in `@camstack/server/system/builtins/local-network/`.
68739
69319
  */
68740
69320
  /** Coarse classification derived from the interface name + IP range. */
68741
69321
  var InterfaceKindEnum = _enum([
@@ -69232,6 +69812,117 @@ var lockControlCapability = {
69232
69812
  * whether persisting is worth a SQLite commit. */
69233
69813
  volatileStateFields: ["lastChangedAt"]
69234
69814
  };
69815
+ var PowerSourceRefusalReasonSchema = _enum([
69816
+ "no-power-source",
69817
+ "switch-missing",
69818
+ "switch-offline",
69819
+ "switch-unknown",
69820
+ "restart-in-progress",
69821
+ "switch-call-failed"
69822
+ ]);
69823
+ var PowerSourceActionResultSchema = discriminatedUnion("accepted", [object({ accepted: literal(true) }), object({
69824
+ accepted: literal(false),
69825
+ reason: PowerSourceRefusalReasonSchema,
69826
+ /** One line for the operator, naming the switch. */
69827
+ message: string()
69828
+ })]);
69829
+ var PowerSourceInfoSchema = object({
69830
+ /** False when the device has no power source linked; the rest is then empty. */
69831
+ linked: boolean(),
69832
+ switchDeviceId: number().nullable(),
69833
+ switchName: string().nullable(),
69834
+ /** The switch's on/off state, or null when it is unknown. Never false for unknown (D393). */
69835
+ switchOn: boolean().nullable(),
69836
+ switchOnline: boolean().nullable(),
69837
+ restartInProgress: boolean(),
69838
+ offSeconds: number().int()
69839
+ });
69840
+ /** The composed slice: the switch state the link mirrors, plus the stored off time. */
69841
+ var PowerSourceStateSchema = object({
69842
+ /** Null while the switch has not reported: unknown is never `false` (D393). */
69843
+ switchOn: boolean().nullable(),
69844
+ offSeconds: number().int(),
69845
+ lastFetchedAt: number()
69846
+ });
69847
+ var POWER_SOURCE_DEVICE_TYPES = [
69848
+ DeviceType.Gateway,
69849
+ DeviceType.Hub,
69850
+ DeviceType.Camera,
69851
+ DeviceType.Sensor,
69852
+ DeviceType.MediaPlayer,
69853
+ DeviceType.Vacuum,
69854
+ DeviceType.Generic,
69855
+ DeviceType.Washer,
69856
+ DeviceType.Dryer,
69857
+ DeviceType.Dishwasher,
69858
+ DeviceType.PetFountain,
69859
+ DeviceType.Fan,
69860
+ DeviceType.Humidifier,
69861
+ DeviceType.Climate,
69862
+ DeviceType.WaterHeater
69863
+ ];
69864
+ var powerSourceCustomize = {
69865
+ version: 1,
69866
+ targets: [{
69867
+ id: "switch",
69868
+ cardinality: "scalar",
69869
+ vocabulary: {
69870
+ cap: "power-source",
69871
+ stateField: "switchOn"
69872
+ },
69873
+ deviceTypes: POWER_SOURCE_DEVICE_TYPES,
69874
+ apply: "add",
69875
+ path: "switchOn",
69876
+ sources: [{
69877
+ kind: "switch",
69878
+ transforms: [{ kind: "identity" }]
69879
+ }],
69880
+ companions: [{
69881
+ kind: "constant",
69882
+ path: "offSeconds",
69883
+ value: 10,
69884
+ when: "add"
69885
+ }, {
69886
+ kind: "clock",
69887
+ path: "lastFetchedAt",
69888
+ when: "always"
69889
+ }],
69890
+ sourceReport: { status: "assignment-status" }
69891
+ }]
69892
+ };
69893
+ var deviceInput = object({ deviceId: number().int().nonnegative() });
69894
+ var powerSourceCapability = {
69895
+ name: "power-source",
69896
+ scope: "device",
69897
+ mode: "singleton",
69898
+ deviceTypes: POWER_SOURCE_DEVICE_TYPES,
69899
+ methods: {
69900
+ getStatus: systemMethod(deviceInput, PowerSourceInfoSchema, { auth: "admin" }),
69901
+ powerOn: systemMethod(deviceInput, PowerSourceActionResultSchema, {
69902
+ kind: "mutation",
69903
+ auth: "admin"
69904
+ }),
69905
+ powerOff: systemMethod(deviceInput, PowerSourceActionResultSchema, {
69906
+ kind: "mutation",
69907
+ auth: "admin"
69908
+ }),
69909
+ forceRestart: systemMethod(deviceInput, PowerSourceActionResultSchema, {
69910
+ kind: "mutation",
69911
+ auth: "admin"
69912
+ })
69913
+ },
69914
+ runtimeState: PowerSourceStateSchema,
69915
+ customize: powerSourceCustomize,
69916
+ /**
69917
+ * Runtime-state durability: **session** — mirrored from the linked switch on
69918
+ * its next report; a restored value would outlive the reading it names.
69919
+ *
69920
+ * See `RuntimeStateDurability`. Enforced by
69921
+ * `scripts/check-runtime-state-durability.ts`.
69922
+ */
69923
+ durability: "session",
69924
+ history: "none"
69925
+ };
69235
69926
  /**
69236
69927
  * A monotonic total between resets (D944). Energy uses `power-meter.energy`
69237
69928
  * so dashboards keep one series; every other instantaneous reading lands here.
@@ -70512,7 +71203,7 @@ onStatusChanged: { data: object({
70512
71203
  * jitter, and observed/peak bandwidth per device + per client.
70513
71204
  *
70514
71205
  * Implementation lives in the server's `NetworkQualityService` (thin
70515
- * wrapper over the shared `NetworkQualityTracker` from `@camstack/system`).
71206
+ * wrapper over the shared `NetworkQualityTracker` from `@camstack/server/system`).
70516
71207
  * The provider is registered from `trpc.router.ts` against the existing
70517
71208
  * service instance — no addon owns this state.
70518
71209
  *
@@ -72663,7 +73354,7 @@ var ptzAutotrackCapability = {
72663
73354
  * - **Native**: the camera's own firmware autotrack, registered per device by
72664
73355
  * the driver when the probe says it exists (Reolink: yes; Hikvision:
72665
73356
  * model-dependent).
72666
- * - **Wrapper**: the emulated implementation (`@camstack/system`'s `autotrack` builtin),
73357
+ * - **Wrapper**: the emulated implementation (`@camstack/server/system`'s `autotrack` builtin),
72667
73358
  * which follows a subject with `ptz.move` pulses driven by the detection
72668
73359
  * pipeline. `defaultActive: false` — the operator binds it per camera in
72669
73360
  * the bindings UI.
@@ -77972,6 +78663,7 @@ var DEVICE_LOCAL_STATE_CAPS = {
77972
78663
  petFeeder: petFeederCapability,
77973
78664
  petFountain: petFountainCapability,
77974
78665
  powerMeter: powerMeterCapability,
78666
+ powerSource: powerSourceCapability,
77975
78667
  presence: presenceCapability,
77976
78668
  pressureSensor: pressureSensorCapability,
77977
78669
  privacyMask: privacyMaskCapability,
@@ -83736,6 +84428,69 @@ Object.freeze({
83736
84428
  access: "view",
83737
84429
  role: "admin"
83738
84430
  },
84431
+ "migrations.declareOwner": {
84432
+ capName: "migrations",
84433
+ capScope: "system",
84434
+ addonId: null,
84435
+ access: "create",
84436
+ role: "admin"
84437
+ },
84438
+ "migrations.dryRun": {
84439
+ capName: "migrations",
84440
+ capScope: "system",
84441
+ addonId: null,
84442
+ access: "view",
84443
+ role: "admin"
84444
+ },
84445
+ "migrations.finishMigration": {
84446
+ capName: "migrations",
84447
+ capScope: "system",
84448
+ addonId: null,
84449
+ access: "create",
84450
+ role: "admin"
84451
+ },
84452
+ "migrations.listLedger": {
84453
+ capName: "migrations",
84454
+ capScope: "system",
84455
+ addonId: null,
84456
+ access: "view",
84457
+ role: "admin"
84458
+ },
84459
+ "migrations.listPending": {
84460
+ capName: "migrations",
84461
+ capScope: "system",
84462
+ addonId: null,
84463
+ access: "view",
84464
+ role: "admin"
84465
+ },
84466
+ "migrations.markFailed": {
84467
+ capName: "migrations",
84468
+ capScope: "system",
84469
+ addonId: null,
84470
+ access: "create",
84471
+ role: "admin"
84472
+ },
84473
+ "migrations.restorePreMigration": {
84474
+ capName: "migrations",
84475
+ capScope: "system",
84476
+ addonId: null,
84477
+ access: "create",
84478
+ role: "admin"
84479
+ },
84480
+ "migrations.retry": {
84481
+ capName: "migrations",
84482
+ capScope: "system",
84483
+ addonId: null,
84484
+ access: "create",
84485
+ role: "admin"
84486
+ },
84487
+ "migrations.startMigration": {
84488
+ capName: "migrations",
84489
+ capScope: "system",
84490
+ addonId: null,
84491
+ access: "create",
84492
+ role: "admin"
84493
+ },
83739
84494
  "modelConvert.convert": {
83740
84495
  capName: "model-convert",
83741
84496
  capScope: "system",
@@ -86368,6 +87123,34 @@ Object.freeze({
86368
87123
  access: "view",
86369
87124
  role: "viewer"
86370
87125
  },
87126
+ "powerSource.forceRestart": {
87127
+ capName: "power-source",
87128
+ capScope: "device",
87129
+ addonId: null,
87130
+ access: "create",
87131
+ role: "admin"
87132
+ },
87133
+ "powerSource.getStatus": {
87134
+ capName: "power-source",
87135
+ capScope: "device",
87136
+ addonId: null,
87137
+ access: "view",
87138
+ role: "admin"
87139
+ },
87140
+ "powerSource.powerOff": {
87141
+ capName: "power-source",
87142
+ capScope: "device",
87143
+ addonId: null,
87144
+ access: "create",
87145
+ role: "admin"
87146
+ },
87147
+ "powerSource.powerOn": {
87148
+ capName: "power-source",
87149
+ capScope: "device",
87150
+ addonId: null,
87151
+ access: "create",
87152
+ role: "admin"
87153
+ },
86371
87154
  "presence.getStatus": {
86372
87155
  capName: "presence",
86373
87156
  capScope: "device",
@@ -91516,6 +92299,26 @@ Object.freeze({
91516
92299
  form: "single",
91517
92300
  optional: false
91518
92301
  }],
92302
+ "powerSource.forceRestart": [{
92303
+ name: "deviceId",
92304
+ form: "single",
92305
+ optional: false
92306
+ }],
92307
+ "powerSource.getStatus": [{
92308
+ name: "deviceId",
92309
+ form: "single",
92310
+ optional: false
92311
+ }],
92312
+ "powerSource.powerOff": [{
92313
+ name: "deviceId",
92314
+ form: "single",
92315
+ optional: false
92316
+ }],
92317
+ "powerSource.powerOn": [{
92318
+ name: "deviceId",
92319
+ form: "single",
92320
+ optional: false
92321
+ }],
91519
92322
  "presence.getStatus": [{
91520
92323
  name: "deviceId",
91521
92324
  form: "single",
@@ -93695,6 +94498,60 @@ object({
93695
94498
  commands: record(string(), unknown())
93696
94499
  });
93697
94500
  //#endregion
94501
+ //#region src/rademacher-reconciler.ts
94502
+ /**
94503
+ * Keeps one live client per enabled integration. Devices are removed ONLY on an explicit
94504
+ * lifecycle event naming the integration (D49): a list that failed, an empty list or an
94505
+ * unreadable connection changes nothing about devices and is logged, never read as "gone".
94506
+ */
94507
+ var RademacherReconciler = class {
94508
+ #deps;
94509
+ constructor(deps) {
94510
+ this.#deps = deps;
94511
+ }
94512
+ /** Upsert a client per usable enabled integration; drop the CLIENT (never devices) of one the list answered is gone. */
94513
+ async reconcile() {
94514
+ const { logger, clients } = this.#deps;
94515
+ let all;
94516
+ try {
94517
+ all = await this.#deps.listIntegrations();
94518
+ } catch (err) {
94519
+ logger.warn("Rademacher integration reconcile skipped — the integration list did not answer", { meta: { error: errMsg(err) } });
94520
+ return;
94521
+ }
94522
+ const enabled = all.filter((i) => i.addonId === this.#deps.addonId && i.enabled);
94523
+ for (const integration of enabled) try {
94524
+ if (!await this.#deps.applySettings(integration)) logger.warn("Rademacher integration has no usable connection — left as it is", { meta: { integrationId: integration.id } });
94525
+ } catch (err) {
94526
+ logger.warn("Rademacher reconcile failed for integration — left as it is", { meta: {
94527
+ integrationId: integration.id,
94528
+ error: errMsg(err)
94529
+ } });
94530
+ }
94531
+ const enabledIds = new Set(enabled.map((i) => i.id));
94532
+ for (const id of clients.list()) if (!enabledIds.has(id)) await clients.remove(id);
94533
+ }
94534
+ /** An explicit lifecycle event: only `deleted` / `disabled` of a named integration remove devices. */
94535
+ async onLifecycle(kind, integrationId) {
94536
+ await this.reconcile();
94537
+ if (kind === "enabled" || integrationId === null) return;
94538
+ try {
94539
+ const removed = await this.#deps.removeDevices(integrationId);
94540
+ this.#deps.logger.info("Rademacher integration devices removed", { meta: {
94541
+ integrationId,
94542
+ kind,
94543
+ removed
94544
+ } });
94545
+ } catch (err) {
94546
+ this.#deps.logger.warn("Rademacher integration device removal failed", { meta: {
94547
+ integrationId,
94548
+ kind,
94549
+ error: errMsg(err)
94550
+ } });
94551
+ }
94552
+ }
94553
+ };
94554
+ //#endregion
93698
94555
  //#region src/config.ts
93699
94556
  /**
93700
94557
  * Operator-supplied connection to ONE Rademacher HomePilot / Start2Smart hub on
@@ -95846,6 +96703,7 @@ var RademacherProviderAddon = class extends BaseDeviceProvider {
95846
96703
  [DeviceType.Gateway]: RademacherGatewayDevice,
95847
96704
  [DeviceType.Cover]: RademacherCoverDevice
95848
96705
  };
96706
+ integrationReconciler = null;
95849
96707
  clients = null;
95850
96708
  constructor() {
95851
96709
  super({});
@@ -95880,6 +96738,7 @@ var RademacherProviderAddon = class extends BaseDeviceProvider {
95880
96738
  this.ctx.logger.warn("Rademacher: shutdown error", { meta: { error: errMsg(err) } });
95881
96739
  }
95882
96740
  this.clients = null;
96741
+ this.integrationReconciler = null;
95883
96742
  await super.onShutdown();
95884
96743
  }
95885
96744
  globalSettingsSchema() {
@@ -95904,47 +96763,48 @@ var RademacherProviderAddon = class extends BaseDeviceProvider {
95904
96763
  if (!this.clients) throw new Error("Rademacher provider not initialised");
95905
96764
  return this.clients;
95906
96765
  }
95907
- async reconcileIntegrations() {
95908
- const reg = this.requireClients();
95909
- try {
95910
- const mine = (await this.ctx.api.integrations.list.query()).filter((i) => i.addonId === this.ctx.id && i.enabled);
95911
- const surviving = /* @__PURE__ */ new Set();
95912
- for (const integration of mine) try {
95913
- const connection = connectionFromSettings(await this.ctx.api.integrations.getSettings.query({ id: integration.id }));
95914
- if (!connection) {
95915
- this.ctx.logger.warn("Rademacher reconcile: host missing or invalid, no session opened", { tags: { integrationId: integration.id } });
95916
- continue;
96766
+ reconciler() {
96767
+ this.integrationReconciler ??= new RademacherReconciler({
96768
+ addonId: this.ctx.id,
96769
+ logger: this.ctx.logger,
96770
+ clients: {
96771
+ list: () => this.requireClients().list(),
96772
+ remove: async (id) => {
96773
+ await this.requireClients().remove(id);
95917
96774
  }
95918
- await reg.upsert(integration.id, integration.name, connection);
95919
- surviving.add(integration.id);
95920
- } catch (err) {
95921
- this.ctx.logger.warn("Rademacher reconcile: settings read failed", {
95922
- tags: { integrationId: integration.id },
95923
- meta: { error: errMsg(err) }
95924
- });
95925
- }
95926
- const vanished = reg.list().filter((id) => !surviving.has(id));
95927
- await reg.retain(surviving);
95928
- for (const integrationId of vanished) await this.cascadeRemoveDevicesForIntegration(integrationId);
95929
- } catch (err) {
95930
- this.ctx.logger.warn("Rademacher integration reconcile failed", { meta: { error: errMsg(err) } });
95931
- }
96775
+ },
96776
+ listIntegrations: () => this.ctx.api.integrations.list.query(),
96777
+ applySettings: async (integration) => {
96778
+ const connection = connectionFromSettings(await this.ctx.api.integrations.getSettings.query({ id: integration.id }));
96779
+ if (!connection) return false;
96780
+ await this.requireClients().upsert(integration.id, integration.name, connection);
96781
+ return true;
96782
+ },
96783
+ removeDevices: (integrationId) => this.cascadeRemoveDevicesForIntegration(integrationId)
96784
+ });
96785
+ return this.integrationReconciler;
96786
+ }
96787
+ /** Upsert one client per enabled integration; never removes a device (D49). */
96788
+ async reconcileIntegrations() {
96789
+ await this.reconciler().reconcile();
95932
96790
  }
96791
+ /** Lifecycle events: only an explicit Deleted/Disabled naming an integration removes devices. */
95933
96792
  subscribeIntegrationLifecycle() {
95934
- const handler = (event) => {
96793
+ const on = (kind) => (event) => {
95935
96794
  const addonId = event.data["addonId"];
95936
96795
  if (typeof addonId === "string" && addonId !== this.ctx.id) return;
95937
- this.reconcileIntegrations();
96796
+ const integrationId = event.data["integrationId"];
96797
+ this.reconciler().onLifecycle(kind, typeof integrationId === "string" ? integrationId : null);
95938
96798
  };
95939
- this.ctx.eventBus.subscribe({ category: EventCategory.IntegrationEnabled }, handler);
95940
- this.ctx.eventBus.subscribe({ category: EventCategory.IntegrationDisabled }, handler);
95941
- this.ctx.eventBus.subscribe({ category: EventCategory.IntegrationDeleted }, handler);
96799
+ this.ctx.eventBus.subscribe({ category: EventCategory.IntegrationEnabled }, on("enabled"));
96800
+ this.ctx.eventBus.subscribe({ category: EventCategory.IntegrationDisabled }, on("disabled"));
96801
+ this.ctx.eventBus.subscribe({ category: EventCategory.IntegrationDeleted }, on("deleted"));
95942
96802
  }
95943
96803
  async cascadeRemoveDevicesForIntegration(integrationId) {
95944
96804
  const reg = this.ctx.kernel.deviceRegistry;
95945
96805
  const devices = this.ctx.kernel.devices;
95946
- if (!reg || !devices) return;
95947
- await cascadeRemoveDevicesForIntegration({
96806
+ if (!reg || !devices) return 0;
96807
+ return await cascadeRemoveDevicesForIntegration({
95948
96808
  reg,
95949
96809
  devices,
95950
96810
  addonId: this.addonId,