@camstack/addon-provider-enpal 0.1.5 → 0.1.7

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.
package/dist/addon.mjs CHANGED
@@ -1,4 +1,4 @@
1
- //#region ../types/dist/event-category-DmZDPDf_.mjs
1
+ //#region ../types/dist/event-category-CaURIwQA.mjs
2
2
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
3
3
  EventCategory["SystemBoot"] = "system.boot";
4
4
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -714,6 +714,10 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
714
714
  EventCategory["NotificationRuleBroken"] = "notification.rule-broken";
715
715
  /** The clear of {@link NotificationRuleBroken}: the rule loads and names nothing deleted. */
716
716
  EventCategory["NotificationRuleRepaired"] = "notification.rule-repaired";
717
+ /** A migration failed or was blocked; the Alert Center re-reads the ledger. */
718
+ EventCategory["MigrationFailed"] = "migration.failed";
719
+ /** A migration applied or a failure cleared; the Alert Center re-reads the ledger. */
720
+ EventCategory["MigrationApplied"] = "migration.applied";
717
721
  EventCategory["DeviceUpdated"] = "device.updated";
718
722
  /**
719
723
  * Transport-level connectivity. Emitted by the device driver when
@@ -776,6 +780,378 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
776
780
  return EventCategory;
777
781
  }({});
778
782
  //#endregion
783
+ //#region ../types/dist/device-type-CEqjmUnR.mjs
784
+ /**
785
+ import { errMsg } from '@camstack/types'
786
+ * Extract a human-readable message from an unknown error value.
787
+ * Replaces the ubiquitous `errMsg(err)` pattern.
788
+ */
789
+ function errMsg(err) {
790
+ if (err instanceof Error) return err.message;
791
+ if (typeof err === "string") return err;
792
+ return String(err);
793
+ }
794
+ var DeviceType = /* @__PURE__ */ function(DeviceType) {
795
+ DeviceType["Camera"] = "camera";
796
+ DeviceType["Hub"] = "hub";
797
+ DeviceType["Light"] = "light";
798
+ DeviceType["Siren"] = "siren";
799
+ DeviceType["Switch"] = "switch";
800
+ DeviceType["Sensor"] = "sensor";
801
+ DeviceType["Thermostat"] = "thermostat";
802
+ /** Air-conditioner / heat-pump climate device (HVAC) — shares the
803
+ * `climate-control` cap surface with `Thermostat` but renders a
804
+ * dedicated AC-appropriate control UI (mode chips, fan speed,
805
+ * independent vertical/horizontal swing). Sources: native Gree, and
806
+ * reusable by other AC integrations. */
807
+ DeviceType["Climate"] = "climate";
808
+ DeviceType["Button"] = "button";
809
+ /** Generic stateless event emitter — carries a device's EXACT declared
810
+ * event vocabulary verbatim (no normalization). Installed with the
811
+ * `event-emitter` cap. Sources: HA `event.*` entities (structured) and
812
+ * HA bus events (e.g. `zha_event`, generic). */
813
+ DeviceType["EventEmitter"] = "event-emitter";
814
+ /** Firmware/software update entity — current vs available version,
815
+ * updatable flag, update state, and an install action. Installed with
816
+ * the `update` cap. Sources: Homematic firmware-update channels (and
817
+ * reusable by other providers, e.g. HA `update.*` entities). */
818
+ DeviceType["Update"] = "update";
819
+ DeviceType["Generic"] = "generic";
820
+ /** Generic notification delivery target (HA `notify.<service>`, future
821
+ * Telegram / Discord / ntfy / SMTP, …). One device per delivery
822
+ * endpoint; the `notifier` cap defines the send surface. */
823
+ DeviceType["Notifier"] = "notifier";
824
+ /** Pre-recorded action sequence with optional parameters
825
+ * (HA `script.*`). Runnable via `script-runner` cap. */
826
+ DeviceType["Script"] = "script";
827
+ /** Automation rule (HA `automation.*`) — enable/disable + manual
828
+ * trigger surface exposed via `automation-control` cap. */
829
+ DeviceType["Automation"] = "automation";
830
+ /** Door / smart lock device (HA `lock.*`). `lock-control` cap. */
831
+ DeviceType["Lock"] = "lock";
832
+ /** Window covering, blinds, garage door, valve, etc. (HA `cover.*`,
833
+ * `valve.*`). `cover` cap with sub-roles for variant. */
834
+ DeviceType["Cover"] = "cover";
835
+ /** Pipe / water / gas valve with open/close/stop and optional
836
+ * position (HA `valve.*`). `valve` cap — a cover-sibling actuator
837
+ * modelled on the same open/closed lifecycle. */
838
+ DeviceType["Valve"] = "valve";
839
+ /** Humidifier / dehumidifier with on/off + target humidity + mode
840
+ * (HA `humidifier.*`). `humidifier` cap — a climate-family actuator
841
+ * modelled on the same target / mode lifecycle. */
842
+ DeviceType["Humidifier"] = "humidifier";
843
+ /** Water heater / boiler with target temperature + operation mode +
844
+ * away mode (HA `water_heater.*`). `water-heater` cap — a
845
+ * climate-family actuator. */
846
+ DeviceType["WaterHeater"] = "water-heater";
847
+ /** Ceiling / standing / exhaust fan (HA `fan.*`). `fan-control` cap. */
848
+ DeviceType["Fan"] = "fan";
849
+ /** Audio / video playback endpoint (HA `media_player.*`). Disjoint from
850
+ * the camera surface — those use `Camera`. `media-player` cap. */
851
+ DeviceType["MediaPlayer"] = "media-player";
852
+ /** Security panel / alarm system (HA `alarm_control_panel.*`).
853
+ * `alarm-panel` cap. */
854
+ DeviceType["AlarmPanel"] = "alarm-panel";
855
+ /** Generic user-settable input (HA `number` / `input_number` / `select`
856
+ * / `input_select` / `text` / `input_text` / `input_datetime`).
857
+ * Sub-type via `DeviceRole`: NumericControl / SelectControl /
858
+ * TextControl / DateTimeControl. */
859
+ DeviceType["Control"] = "control";
860
+ /** Person / device-tracker presence (HA `person.*`, `device_tracker.*`).
861
+ * `presence` cap. */
862
+ DeviceType["Presence"] = "presence";
863
+ /** Weather provider (HA `weather.*`). Tier-3, low MVP priority.
864
+ * `weather` cap. */
865
+ DeviceType["Weather"] = "weather";
866
+ /** Robot vacuum (HA `vacuum.*`). Tier-3. `vacuum-control` cap. */
867
+ DeviceType["Vacuum"] = "vacuum";
868
+ /** Robotic lawn mower (HA `lawn_mower.*`). Tier-3.
869
+ * `lawn-mower-control` cap. */
870
+ DeviceType["LawnMower"] = "lawn-mower";
871
+ /** Physical HA device group — parent container for entity-children
872
+ * adopted from a single HA device entry. Not renderable as a
873
+ * standalone device; exists only to anchor child entities. */
874
+ DeviceType["Container"] = "container";
875
+ /** Single still-image entity (HA `image.*`). Read-only display of an
876
+ * `entity_picture` signed URL the browser loads directly. `image` cap. */
877
+ DeviceType["Image"] = "image";
878
+ /** Smart pet feeder — cloud-connected food dispenser with a bowl food
879
+ * level, battery, desiccant life, feeding state and manual-feed /
880
+ * call-pet / maintenance actions. Installed with the `pet-feeder` cap;
881
+ * dual-hopper models (D4S/D4SH) expose per-hopper portions. Sources:
882
+ * native PetKit (`nodepetkit` `FeederDevice`), reusable by other feeder
883
+ * integrations sharing the same food/desiccant/hopper surface. */
884
+ DeviceType["PetFeeder"] = "pet-feeder";
885
+ /** Pet water fountain — tank level, water drunk, pump and its duty cycle.
886
+ * Installed with the `pet-fountain` cap. Reaches CamStack as loose entities
887
+ * of a container and is BUILT as a composed device (D967). */
888
+ DeviceType["PetFountain"] = "pet-fountain";
889
+ /** Built-in dishwasher — program lifecycle, door / remote-start gates,
890
+ * salt and rinse-aid levels. Installed with the `dishwasher` cap.
891
+ * Sources: native Bosch Home Connect, reusable by similar integrations.
892
+ * A plug-built dishwasher carries running-state and power instead. */
893
+ DeviceType["Dishwasher"] = "dishwasher";
894
+ /** Washing machine. A plug-built one is running while its power stays
895
+ * above a threshold. No program cap — that surface is the dishwasher's. */
896
+ DeviceType["Washer"] = "washer";
897
+ /** Tumble dryer. Same plug-built running surface as {@link Washer}. */
898
+ DeviceType["Dryer"] = "dryer";
899
+ /** The physical bridge box of a vendor ecosystem (Velux KLF-200, and later
900
+ * other radio-to-IP bridges). It carries the box's own facts (firmware,
901
+ * address, connection state) and the actions the box really supports
902
+ * (`reboot`, `device-admin-link`). Products are NOT its children — they are
903
+ * adopted beside it — and its connection settings stay on the integration. */
904
+ DeviceType["Gateway"] = "gateway";
905
+ return DeviceType;
906
+ }({});
907
+ var DeviceFeature = /* @__PURE__ */ function(DeviceFeature) {
908
+ DeviceFeature["BatteryOperated"] = "battery-operated";
909
+ DeviceFeature["Rebootable"] = "rebootable";
910
+ /**
911
+ * Device supports an on-demand re-sync of its derived spec with its
912
+ * upstream source — drives the generic Re-sync button. The owning
913
+ * provider implements the action via the `device-adoption.resync` cap.
914
+ */
915
+ DeviceFeature["Resyncable"] = "resyncable";
916
+ DeviceFeature["NativeSnapshot"] = "native-snapshot";
917
+ DeviceFeature["DoorbellButton"] = "doorbell-button";
918
+ DeviceFeature["TwoWayAudio"] = "two-way-audio";
919
+ DeviceFeature["PanTiltZoom"] = "pan-tilt-zoom";
920
+ /**
921
+ * Camera supports the on-firmware autotrack subsystem (subject-
922
+ * following). Distinct from `PanTiltZoom` because not every PTZ
923
+ * camera ships autotrack — the admin UI uses this flag to gate
924
+ * the autotrack toggle / settings card without re-deriving from
925
+ * the cap registry. Mirrors `ptz-autotrack` cap registration:
926
+ * driver sets this feature when probe confirms the firmware
927
+ * surface, and registers the cap in the same code path.
928
+ */
929
+ DeviceFeature["PtzAutotrack"] = "ptz-autotrack";
930
+ /**
931
+ * Accessory exposes a "trigger on motion" toggle — the parent camera's
932
+ * motion detection automatically activates this device. Mirrors
933
+ * `motion-trigger` cap registration: drivers set this feature in the
934
+ * same code path that calls `ctx.registerNativeCap(motionTriggerCapability, ...)`.
935
+ *
936
+ * Used by admin UI (gate the in-hero `MotionTriggerToggle` against a
937
+ * fast scalar without binding fetch), notifier rules, and `listAll`
938
+ * filters that want "all devices with on-motion behaviour".
939
+ */
940
+ DeviceFeature["MotionTrigger"] = "motion-trigger";
941
+ /** Light supports rgb-triplet color via `color` cap. */
942
+ DeviceFeature["LightColorRgb"] = "light-color-rgb";
943
+ /** Light supports HSV color via `color` cap. */
944
+ DeviceFeature["LightColorHsv"] = "light-color-hsv";
945
+ /** Light supports color-temperature (mired) via `color` cap. */
946
+ DeviceFeature["LightColorMired"] = "light-color-mired";
947
+ /** Thermostat supports a `heat_cool` dual setpoint (targetLow +
948
+ * targetHigh). Gates the range slider UI. */
949
+ DeviceFeature["ClimateDualSetpoint"] = "climate-dual-setpoint";
950
+ /** Thermostat exposes target humidity and/or current humidity
951
+ * readings. Gates the humidity controls. */
952
+ DeviceFeature["ClimateHumidity"] = "climate-humidity";
953
+ /** Thermostat exposes a fan-mode selector. */
954
+ DeviceFeature["ClimateFanMode"] = "climate-fan-mode";
955
+ /** Thermostat exposes preset modes (eco / away / sleep / vendor). */
956
+ DeviceFeature["ClimatePreset"] = "climate-preset";
957
+ /** Thermostat exposes a vertical louver swing toggle. Gates the
958
+ * vertical-swing switch in the climate UI. Independent of horizontal. */
959
+ DeviceFeature["ClimateSwingVertical"] = "climate-swing-vertical";
960
+ /** Thermostat exposes a horizontal louver swing toggle. Gates the
961
+ * horizontal-swing switch in the climate UI. Independent of vertical. */
962
+ DeviceFeature["ClimateSwingHorizontal"] = "climate-swing-horizontal";
963
+ /** Cover exposes intermediate position control (0..100). Gates the
964
+ * position slider UI. */
965
+ DeviceFeature["CoverPositionable"] = "cover-positionable";
966
+ /** Cover exposes slat-tilt control. Gates the tilt slider UI. */
967
+ DeviceFeature["CoverTilt"] = "cover-tilt";
968
+ /** Valve exposes intermediate position control (0..100). Gates the
969
+ * position slider / drag surface UI. */
970
+ DeviceFeature["ValvePositionable"] = "valve-positionable";
971
+ /** Fan exposes a speed-percentage setter. Gates the speed slider UI. */
972
+ DeviceFeature["FanSpeed"] = "fan-speed";
973
+ /** Fan exposes a preset mode selector. */
974
+ DeviceFeature["FanPreset"] = "fan-preset";
975
+ /** Fan exposes blade direction (forward/reverse) — typical of
976
+ * ceiling fans. */
977
+ DeviceFeature["FanDirection"] = "fan-direction";
978
+ /** Fan exposes an oscillation toggle. */
979
+ DeviceFeature["FanOscillating"] = "fan-oscillating";
980
+ /** Lock requires a PIN code on lock/unlock. Gates the code-entry
981
+ * field on the UI lock-controls panel. */
982
+ DeviceFeature["LockPinRequired"] = "lock-pin-required";
983
+ /** Lock supports a latch-release ("open door") action distinct from
984
+ * unlock. Mirrors HA `LockEntityFeature.OPEN` (bit 1) in
985
+ * `supported_features`. Gates the Open Door button in the UI. */
986
+ DeviceFeature["LockOpen"] = "lock-open";
987
+ /** Media player exposes a seek-to-position surface. */
988
+ DeviceFeature["MediaPlayerSeek"] = "media-player-seek";
989
+ /** Media player exposes a volume-level setter. */
990
+ DeviceFeature["MediaPlayerVolume"] = "media-player-volume";
991
+ /** Media player exposes a mute toggle distinct from volume=0. */
992
+ DeviceFeature["MediaPlayerMute"] = "media-player-mute";
993
+ /** Media player exposes a shuffle toggle. */
994
+ DeviceFeature["MediaPlayerShuffle"] = "media-player-shuffle";
995
+ /** Media player exposes a repeat mode (off / all / one). */
996
+ DeviceFeature["MediaPlayerRepeat"] = "media-player-repeat";
997
+ /** Media player exposes a source / input selector. */
998
+ DeviceFeature["MediaPlayerSelectSource"] = "media-player-select-source";
999
+ /** Media player exposes a play-arbitrary-media surface (URL / id). */
1000
+ DeviceFeature["MediaPlayerPlayMedia"] = "media-player-play-media";
1001
+ /** Media player exposes next-track. */
1002
+ DeviceFeature["MediaPlayerNext"] = "media-player-next";
1003
+ /** Media player exposes previous-track. */
1004
+ DeviceFeature["MediaPlayerPrevious"] = "media-player-previous";
1005
+ /** Media player exposes stop distinct from pause. */
1006
+ DeviceFeature["MediaPlayerStop"] = "media-player-stop";
1007
+ /** Alarm panel requires a PIN code on arm/disarm. */
1008
+ DeviceFeature["AlarmPinRequired"] = "alarm-pin-required";
1009
+ /** Presence device carries GPS coordinates (lat/lng/accuracy) in
1010
+ * addition to a textual location. */
1011
+ DeviceFeature["PresenceGps"] = "presence-gps";
1012
+ /** Notifier accepts an inline / URL image attachment. */
1013
+ DeviceFeature["NotifierImage"] = "notifier-image";
1014
+ /** Notifier accepts a priority hint (high/normal/low). */
1015
+ DeviceFeature["NotifierPriority"] = "notifier-priority";
1016
+ /** Notifier accepts a free-form `data` payload for platform-specific
1017
+ * fields. */
1018
+ DeviceFeature["NotifierData"] = "notifier-data";
1019
+ /** Notifier supports interactive action buttons / callbacks. */
1020
+ DeviceFeature["NotifierActions"] = "notifier-actions";
1021
+ /** Notifier supports per-call recipient targeting (multi-user). */
1022
+ DeviceFeature["NotifierRecipients"] = "notifier-recipients";
1023
+ /** Script runner accepts a variables map on each run invocation. */
1024
+ DeviceFeature["ScriptVariables"] = "script-variables";
1025
+ /** Automation `trigger` accepts a skipCondition flag — fires the
1026
+ * automation's actions while bypassing its condition block. */
1027
+ DeviceFeature["AutomationSkipCondition"] = "automation-skip-condition";
1028
+ /** Robot vacuum exposes a live cleaning map (image child). Gates the
1029
+ * map tile in the vacuum UI. */
1030
+ DeviceFeature["VacuumHasMap"] = "vacuum-has-map";
1031
+ /** Robot vacuum exposes AI obstacle-detection toggles. Gates the AI
1032
+ * switches group. */
1033
+ DeviceFeature["VacuumHasAi"] = "vacuum-has-ai";
1034
+ /** Robot mower exposes a live mowing map (SVG image child). Gates the
1035
+ * mower map tile. */
1036
+ DeviceFeature["MowerHasMap"] = "mower-has-map";
1037
+ /** Robot mower exposes targeted mowing (all-area / zones / edges /
1038
+ * spots) — gates the Mow action + map selectors. */
1039
+ DeviceFeature["MowerHasTargetedMowing"] = "mower-has-targeted-mowing";
1040
+ return DeviceFeature;
1041
+ }({});
1042
+ /**
1043
+ * Semantic role a device plays within its parent. Populated by driver
1044
+ * addons when creating accessory devices (Reolink siren/floodlight/
1045
+ * PIR/chime/autotrack/doorbell, ONVIF relay outputs, …). Used by the
1046
+ * admin UI to pick icons, labels, and widgets — a `Switch` with
1047
+ * `role: Floodlight` renders as a bulb with a brightness slider,
1048
+ * whereas a `Switch` with `role: Siren` renders as a klaxon.
1049
+ *
1050
+ * Undefined for top-level devices (cameras, NVRs, hubs). Persisted in
1051
+ * sqlite as a nullable TEXT column — old rows keep working unchanged.
1052
+ */
1053
+ var DeviceRole = /* @__PURE__ */ function(DeviceRole) {
1054
+ DeviceRole["Siren"] = "siren";
1055
+ DeviceRole["Floodlight"] = "floodlight";
1056
+ DeviceRole["Spotlight"] = "spotlight";
1057
+ DeviceRole["PirSensor"] = "pir-sensor";
1058
+ DeviceRole["Chime"] = "chime";
1059
+ DeviceRole["Autotrack"] = "autotrack";
1060
+ DeviceRole["Nightvision"] = "nightvision";
1061
+ DeviceRole["PrivacyMask"] = "privacy-mask";
1062
+ DeviceRole["Doorbell"] = "doorbell";
1063
+ /** Interior blind / shade / roller blind. HA `device_class: blind|shade`. */
1064
+ DeviceRole["CoverBlind"] = "cover-blind";
1065
+ /** Exterior roller shutter. HA `device_class: shutter`; Homematic BROLL. */
1066
+ DeviceRole["CoverShutter"] = "cover-shutter";
1067
+ /** Exterior awning. HA `device_class: awning`. */
1068
+ DeviceRole["CoverAwning"] = "cover-awning";
1069
+ /** Curtain / drape. HA `device_class: curtain`. */
1070
+ DeviceRole["CoverCurtain"] = "cover-curtain";
1071
+ /** Garage door. HA `device_class: garage`. A security actuator: exports gate it. */
1072
+ DeviceRole["CoverGarage"] = "cover-garage";
1073
+ /** Gate. HA `device_class: gate`. A security actuator: exports gate it. */
1074
+ DeviceRole["CoverGate"] = "cover-gate";
1075
+ /** Door. HA `device_class: door`. A security actuator: exports gate it. */
1076
+ DeviceRole["CoverDoor"] = "cover-door";
1077
+ /** Window. HA `device_class: window`. */
1078
+ DeviceRole["CoverWindow"] = "cover-window";
1079
+ /** Virtual HA toggle (input_boolean.*) — distinguishable from a
1080
+ * real Switch device for UI rendering / export adapters. */
1081
+ DeviceRole["BinaryHelper"] = "binary-helper";
1082
+ /** A smart plug: a `switch` whose source says it is an outlet (HA
1083
+ * `device_class: outlet`). The Alexa export shows it as a SMARTPLUG. */
1084
+ DeviceRole["Outlet"] = "outlet";
1085
+ /** Generic motion / occupancy / moving event source. Distinct from
1086
+ * the camera accessory PirSensor role: that one is a camera child;
1087
+ * this is a standalone HA / 3rd-party motion sensor. */
1088
+ DeviceRole["MotionSensor"] = "motion-sensor";
1089
+ DeviceRole["ContactSensor"] = "contact-sensor";
1090
+ DeviceRole["LeakSensor"] = "leak-sensor";
1091
+ DeviceRole["SmokeSensor"] = "smoke-sensor";
1092
+ DeviceRole["COSensor"] = "co-sensor";
1093
+ DeviceRole["GasSensor"] = "gas-sensor";
1094
+ DeviceRole["TamperSensor"] = "tamper-sensor";
1095
+ DeviceRole["VibrationSensor"] = "vibration-sensor";
1096
+ DeviceRole["ConnectivitySensor"] = "connectivity-sensor";
1097
+ DeviceRole["SoundSensor"] = "sound-sensor";
1098
+ /** Fallback for `binary_sensor` without a known `device_class`. */
1099
+ DeviceRole["BinarySensor"] = "binary-sensor";
1100
+ DeviceRole["TemperatureSensor"] = "temperature-sensor";
1101
+ DeviceRole["HumiditySensor"] = "humidity-sensor";
1102
+ DeviceRole["AmbientLightSensor"] = "ambient-light-sensor";
1103
+ DeviceRole["PressureSensor"] = "pressure-sensor";
1104
+ /** Wind speed or direction (weather-station `wind-sensor` cap). */
1105
+ DeviceRole["WindSensor"] = "wind-sensor";
1106
+ /** Rain accumulation or rate (weather-station `rain-sensor` cap). */
1107
+ DeviceRole["RainSensor"] = "rain-sensor";
1108
+ /** UV index (weather-station `uv-sensor` cap). */
1109
+ DeviceRole["UvSensor"] = "uv-sensor";
1110
+ /** Solar irradiance W/m² (weather-station `solar-radiation-sensor` cap).
1111
+ * Distinct from AmbientLightSensor (lux). */
1112
+ DeviceRole["SolarRadiationSensor"] = "solar-radiation-sensor";
1113
+ /** Soil moisture % (garden/weather `soil-moisture-sensor` cap). */
1114
+ DeviceRole["SoilMoistureSensor"] = "soil-moisture-sensor";
1115
+ DeviceRole["PowerSensor"] = "power-sensor";
1116
+ DeviceRole["EnergySensor"] = "energy-sensor";
1117
+ DeviceRole["VoltageSensor"] = "voltage-sensor";
1118
+ DeviceRole["CurrentSensor"] = "current-sensor";
1119
+ DeviceRole["AirQualitySensor"] = "air-quality-sensor";
1120
+ /** Battery level (numeric % via `sensor` OR low-bool via
1121
+ * `binary_sensor` — the cap distinguishes via the value type). */
1122
+ DeviceRole["BatterySensor"] = "battery-sensor";
1123
+ /** Fallback for `sensor` numeric without a known `device_class`. */
1124
+ DeviceRole["NumericSensor"] = "numeric-sensor";
1125
+ /** String / enum state (HA `sensor` with `state_class: enum` or
1126
+ * `attributes.options`). */
1127
+ DeviceRole["EnumSensor"] = "enum-sensor";
1128
+ /** Date / timestamp state (HA `sensor` with `device_class: timestamp`
1129
+ * or `date`). The slice carries the raw ISO string verbatim (hosted on
1130
+ * the `enum-sensor` cap); the UI renders it locale-formatted. */
1131
+ DeviceRole["DateTimeSensor"] = "datetime-sensor";
1132
+ /** Last-resort fallback when nothing else matches. */
1133
+ DeviceRole["GenericSensor"] = "generic-sensor";
1134
+ DeviceRole["NumericControl"] = "numeric-control";
1135
+ DeviceRole["SelectControl"] = "select-control";
1136
+ DeviceRole["TextControl"] = "text-control";
1137
+ DeviceRole["DateTimeControl"] = "datetime-control";
1138
+ /** Mobile push notifier (HA `notify.mobile_app_*`) — supports
1139
+ * rich features (image, priority, channel routing). */
1140
+ DeviceRole["MobilePushNotifier"] = "mobile-push-notifier";
1141
+ /** Chat / messaging service (HA `notify.telegram_*`,
1142
+ * `notify.discord_*`, etc.). */
1143
+ DeviceRole["MessagingNotifier"] = "messaging-notifier";
1144
+ /** Email-based delivery (HA `notify.smtp`, etc.). */
1145
+ DeviceRole["EmailNotifier"] = "email-notifier";
1146
+ /** Fallback when the notifier service name doesn't match a known
1147
+ * pattern. */
1148
+ DeviceRole["GenericNotifier"] = "generic-notifier";
1149
+ /** Emits a pulse train (infrared or RF) on request — the `ir-transmitter`
1150
+ * cap. A `DeviceType.Generic` device. */
1151
+ DeviceRole["IrTransmitter"] = "ir-transmitter";
1152
+ return DeviceRole;
1153
+ }({});
1154
+ //#endregion
779
1155
  //#region ../../node_modules/zod/v4/core/core.js
780
1156
  var _a$1;
781
1157
  function $constructor(name, initializer, params) {
@@ -5454,7 +5830,7 @@ var ZodIssueCode = {
5454
5830
  var ZodFirstPartyTypeKind;
5455
5831
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5456
5832
  //#endregion
5457
- //#region ../types/dist/sleep-DSVWZfJC.mjs
5833
+ //#region ../types/dist/sleep-CSYuxssh.mjs
5458
5834
  /**
5459
5835
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5460
5836
  * window to float samples (D455).
@@ -5974,6 +6350,102 @@ function emitReadiness(bus, params) {
5974
6350
  ts
5975
6351
  }));
5976
6352
  }
6353
+ var NOT_RUN = {
6354
+ declared: false,
6355
+ snapshot: null,
6356
+ applied: [],
6357
+ failed: [],
6358
+ blocked: [],
6359
+ skipped: [],
6360
+ blockOwner: []
6361
+ };
6362
+ /**
6363
+ * Offer `migrations` to the engine and run what it says to run, in its order. Never
6364
+ * throws: an unreachable or refusing engine is logged and reported `declared: false`,
6365
+ * because the hub (and this addon's boot) must not hang on a migration.
6366
+ */
6367
+ async function runAddonMigrations(host, migrations, logger) {
6368
+ const first = migrations[0];
6369
+ if (first === void 0) return NOT_RUN;
6370
+ const owner = first.meta.owner;
6371
+ const log = logger.withTags({ migrationOwner: owner });
6372
+ const byId = new Map(migrations.map((m) => [m.meta.id, m]));
6373
+ let declaration;
6374
+ try {
6375
+ declaration = await host.declareOwner({
6376
+ owner,
6377
+ metas: migrations.map((m) => m.meta)
6378
+ });
6379
+ } catch (err) {
6380
+ log.error("the migration engine refused or could not take this addon catalogue — none ran", { meta: { error: errMsg(err) } });
6381
+ return {
6382
+ ...NOT_RUN,
6383
+ blockOwner: migrations.filter((m) => m.meta.onFailure === "block-owner").map((m) => m.meta.id)
6384
+ };
6385
+ }
6386
+ const applied = [];
6387
+ const failed = [];
6388
+ const blocked = [];
6389
+ const skipped = [];
6390
+ const settle = (id, status) => {
6391
+ if (status === "applied") applied.push(id);
6392
+ else if (status === "failed") failed.push(id);
6393
+ else if (status === "blocked") blocked.push(id);
6394
+ else skipped.push(id);
6395
+ };
6396
+ for (const step of declaration.steps) {
6397
+ const bound = byId.get(step.id);
6398
+ if (bound === void 0 || step.action === "skip") {
6399
+ skipped.push(step.id);
6400
+ continue;
6401
+ }
6402
+ const tags = { migrationId: step.id };
6403
+ try {
6404
+ const gate = await host.startMigration({ id: step.id });
6405
+ if (!gate.proceed) {
6406
+ log.warn("migration not run", {
6407
+ tags,
6408
+ meta: {
6409
+ state: gate.state,
6410
+ reason: gate.reason
6411
+ }
6412
+ });
6413
+ settle(step.id, gate.state === "pending" ? "blocked" : gate.state);
6414
+ continue;
6415
+ }
6416
+ const result = await bound.execute();
6417
+ const outcome = await host.finishMigration({
6418
+ id: step.id,
6419
+ result
6420
+ });
6421
+ if (outcome.status === "applied") log.info("migration applied", {
6422
+ tags,
6423
+ meta: { rowsChanged: outcome.rowsChanged }
6424
+ });
6425
+ else log.error("migration failed", {
6426
+ tags,
6427
+ meta: { error: outcome.error }
6428
+ });
6429
+ settle(step.id, outcome.status);
6430
+ } catch (err) {
6431
+ log.error("migration could not be completed with the engine", {
6432
+ tags,
6433
+ meta: { error: errMsg(err) }
6434
+ });
6435
+ failed.push(step.id);
6436
+ }
6437
+ }
6438
+ const blockOwner = [...failed, ...blocked].filter((id) => byId.get(id)?.meta.onFailure === "block-owner");
6439
+ return {
6440
+ declared: true,
6441
+ snapshot: declaration.snapshot,
6442
+ applied,
6443
+ failed,
6444
+ blocked,
6445
+ skipped,
6446
+ blockOwner
6447
+ };
6448
+ }
5977
6449
  /**
5978
6450
  * Build a {@link DurableState} over a single store key. Transport-agnostic:
5979
6451
  * `read`/`write` are the addon-store or device-store accessors. The whole
@@ -7029,6 +7501,28 @@ var BaseAddon = class {
7029
7501
  return normalized;
7030
7502
  }
7031
7503
  /**
7504
+ * Run this addon's migration catalogue (D987 M3) through the hub's `migrations`
7505
+ * engine. Call it at the point of the boot where the data must already have the new
7506
+ * shape — `onInitialize`, or `onRestoreDevices` for a migration that needs the
7507
+ * restored devices — with the catalogue bound to the addon's ports
7508
+ * (`defineAddonMigrations(ADDON_MIGRATIONS, ports)`).
7509
+ *
7510
+ * An engine that cannot be reached or refuses the catalogue is logged and runs
7511
+ * nothing; the boot continues. A migration that FAILED (or was blocked by one)
7512
+ * and declares `onFailure: 'block-owner'` makes this throw, so the addon does not
7513
+ * serve data whose shape it cannot trust; `tolerate` migrations never do.
7514
+ */
7515
+ async runMigrations(migrations) {
7516
+ const api = this.ctx.api.migrations;
7517
+ const report = await runAddonMigrations({
7518
+ declareOwner: (input) => api.declareOwner.mutate(input),
7519
+ startMigration: (input) => api.startMigration.mutate(input),
7520
+ finishMigration: (input) => api.finishMigration.mutate(input)
7521
+ }, migrations, this.ctx.logger);
7522
+ if (report.blockOwner.length > 0) throw new Error(`migration(s) ${report.blockOwner.join(", ")} did not apply and block this addon — fix or retry them (migrations.retry), then restart it`);
7523
+ return report;
7524
+ }
7525
+ /**
7032
7526
  * Called by the isolated-process runner AFTER `broker.start()`.
7033
7527
  * In-process addons never need this because the hub broker is already
7034
7528
  * running when `initialize()` fires. For forked children the broker
@@ -8138,6 +8632,98 @@ var RecordKindDeclarationSchema = object({
8138
8632
  }).strict();
8139
8633
  /** The kind names: lower-case, dash separated, short enough for an index name. */
8140
8634
  var RecordKindNameSchema = string().regex(/^[a-z][a-z0-9-]{0,47}$/);
8635
+ object({
8636
+ field: string().min(1),
8637
+ on: literal("advance")
8638
+ });
8639
+ /**
8640
+ * Output schema shared by the contribution + live methods.
8641
+ *
8642
+ * Mirrors the `ConfigUISchemaWithValues` shape (sections[] + optional
8643
+ * tabs[]) without importing from `../interfaces/config-ui.js` — a
8644
+ * concrete-but-lenient Zod object keeps tRPC output inference happy
8645
+ * (using `z.unknown()` here collapses unrelated router branches to
8646
+ * `unknown` when the generator re-inlines the huge AppRouter type).
8647
+ *
8648
+ * `.passthrough()` on sections/fields accepts whatever FormBuilder
8649
+ * extensions the caller adds (showWhen, displayScale, …) without
8650
+ * rebuilding every time a new field kind is introduced.
8651
+ */
8652
+ var ContributionSectionSchema = object({
8653
+ id: string(),
8654
+ title: string(),
8655
+ description: string().optional(),
8656
+ style: _enum(["card", "accordion"]).optional(),
8657
+ defaultCollapsed: boolean().optional(),
8658
+ columns: union([
8659
+ literal(1),
8660
+ literal(2),
8661
+ literal(3),
8662
+ literal(4)
8663
+ ]).optional(),
8664
+ tab: string().optional(),
8665
+ location: _enum([
8666
+ "settings",
8667
+ "top-tab",
8668
+ "hero"
8669
+ ]).optional(),
8670
+ order: number().optional(),
8671
+ fields: array(any())
8672
+ });
8673
+ object({
8674
+ tabs: array(object({
8675
+ id: string(),
8676
+ label: string(),
8677
+ icon: string(),
8678
+ order: number().optional()
8679
+ })).optional(),
8680
+ sections: array(ContributionSectionSchema)
8681
+ }).nullable();
8682
+ object({ deviceId: number() }), object({ deviceId: number() }), object({
8683
+ deviceId: number(),
8684
+ patch: record(string(), unknown())
8685
+ }), object({ success: literal(true) });
8686
+ object({ deviceId: number() }), unknown().nullable();
8687
+ function method(input, output, options) {
8688
+ return {
8689
+ input,
8690
+ output,
8691
+ kind: options?.kind ?? "query",
8692
+ ...options?.auth !== void 0 ? { auth: options.auth } : {},
8693
+ ...options?.access !== void 0 ? { access: options.access } : {},
8694
+ ...options?.role !== void 0 ? { role: options.role } : {},
8695
+ ...options?.caller !== void 0 ? { caller: options.caller } : {},
8696
+ timeoutMs: options?.timeoutMs
8697
+ };
8698
+ }
8699
+ /**
8700
+ * A wrapper/system-only method: served exclusively by the cap's system-level
8701
+ * provider (`InferProvider`), and OPTIONAL on `InferNativeProvider` so per-device
8702
+ * driver natives don't stub out a wrapper concern (e.g. a cross-device cache
8703
+ * overview). The `systemOnly: true` literal is what `InferNativeProvider` keys on.
8704
+ */
8705
+ function systemMethod(input, output, options) {
8706
+ return {
8707
+ ...method(input, output, options),
8708
+ systemOnly: true
8709
+ };
8710
+ }
8711
+ /**
8712
+ * A method a SOURCE of a collection cap may legitimately not serve — OPTIONAL
8713
+ * on `InferProvider`. The `providerOptional: true` literal is what
8714
+ * `InferProvider` keys on; see {@link CapabilityMethodSchema.providerOptional}
8715
+ * for when this is the honest answer and when it is a soft stub.
8716
+ */
8717
+ function optionalMethod(input, output, options) {
8718
+ return {
8719
+ ...method(input, output, options),
8720
+ providerOptional: true
8721
+ };
8722
+ }
8723
+ /** Shorthand to define an event schema */
8724
+ function event(data) {
8725
+ return { data };
8726
+ }
8141
8727
  var READING_QUANTITIES = [
8142
8728
  "temperature",
8143
8729
  "pressure",
@@ -8527,7 +9113,7 @@ var ProfileRtspEntrySchema = object({
8527
9113
  /**
8528
9114
  * The model weight formats a catalog entry can ship. Schema-free on purpose:
8529
9115
  * `types/models.ts` builds Zod schemas from it, but a runner-closure consumer
8530
- * (the model downloader in `@camstack/system`) needs only the list and must
9116
+ * (the model downloader in `@camstack/server/system`) needs only the list and must
8531
9117
  * reach it through `@camstack/types/addon` without building the cap schema
8532
9118
  * graph (D28).
8533
9119
  */
@@ -8539,98 +9125,6 @@ var MODEL_FORMATS = [
8539
9125
  "pt",
8540
9126
  "gguf"
8541
9127
  ];
8542
- object({
8543
- field: string().min(1),
8544
- on: literal("advance")
8545
- });
8546
- /**
8547
- * Output schema shared by the contribution + live methods.
8548
- *
8549
- * Mirrors the `ConfigUISchemaWithValues` shape (sections[] + optional
8550
- * tabs[]) without importing from `../interfaces/config-ui.js` — a
8551
- * concrete-but-lenient Zod object keeps tRPC output inference happy
8552
- * (using `z.unknown()` here collapses unrelated router branches to
8553
- * `unknown` when the generator re-inlines the huge AppRouter type).
8554
- *
8555
- * `.passthrough()` on sections/fields accepts whatever FormBuilder
8556
- * extensions the caller adds (showWhen, displayScale, …) without
8557
- * rebuilding every time a new field kind is introduced.
8558
- */
8559
- var ContributionSectionSchema = object({
8560
- id: string(),
8561
- title: string(),
8562
- description: string().optional(),
8563
- style: _enum(["card", "accordion"]).optional(),
8564
- defaultCollapsed: boolean().optional(),
8565
- columns: union([
8566
- literal(1),
8567
- literal(2),
8568
- literal(3),
8569
- literal(4)
8570
- ]).optional(),
8571
- tab: string().optional(),
8572
- location: _enum([
8573
- "settings",
8574
- "top-tab",
8575
- "hero"
8576
- ]).optional(),
8577
- order: number().optional(),
8578
- fields: array(any())
8579
- });
8580
- object({
8581
- tabs: array(object({
8582
- id: string(),
8583
- label: string(),
8584
- icon: string(),
8585
- order: number().optional()
8586
- })).optional(),
8587
- sections: array(ContributionSectionSchema)
8588
- }).nullable();
8589
- object({ deviceId: number() }), object({ deviceId: number() }), object({
8590
- deviceId: number(),
8591
- patch: record(string(), unknown())
8592
- }), object({ success: literal(true) });
8593
- object({ deviceId: number() }), unknown().nullable();
8594
- function method(input, output, options) {
8595
- return {
8596
- input,
8597
- output,
8598
- kind: options?.kind ?? "query",
8599
- ...options?.auth !== void 0 ? { auth: options.auth } : {},
8600
- ...options?.access !== void 0 ? { access: options.access } : {},
8601
- ...options?.role !== void 0 ? { role: options.role } : {},
8602
- ...options?.caller !== void 0 ? { caller: options.caller } : {},
8603
- timeoutMs: options?.timeoutMs
8604
- };
8605
- }
8606
- /**
8607
- * A wrapper/system-only method: served exclusively by the cap's system-level
8608
- * provider (`InferProvider`), and OPTIONAL on `InferNativeProvider` so per-device
8609
- * driver natives don't stub out a wrapper concern (e.g. a cross-device cache
8610
- * overview). The `systemOnly: true` literal is what `InferNativeProvider` keys on.
8611
- */
8612
- function systemMethod(input, output, options) {
8613
- return {
8614
- ...method(input, output, options),
8615
- systemOnly: true
8616
- };
8617
- }
8618
- /**
8619
- * A method a SOURCE of a collection cap may legitimately not serve — OPTIONAL
8620
- * on `InferProvider`. The `providerOptional: true` literal is what
8621
- * `InferProvider` keys on; see {@link CapabilityMethodSchema.providerOptional}
8622
- * for when this is the honest answer and when it is a soft stub.
8623
- */
8624
- function optionalMethod(input, output, options) {
8625
- return {
8626
- ...method(input, output, options),
8627
- providerOptional: true
8628
- };
8629
- }
8630
- /** Shorthand to define an event schema */
8631
- function event(data) {
8632
- return { data };
8633
- }
8634
9128
  /**
8635
9129
  * `addon-pages-source` — collection cap exposing per-provider raw page
8636
9130
  * declarations. Every addon that contributes a UI page registers a
@@ -8872,6 +9366,7 @@ var CAP_NAMES = Object.freeze([
8872
9366
  "mesh-network",
8873
9367
  "meter",
8874
9368
  "metrics-provider",
9369
+ "migrations",
8875
9370
  "model-convert",
8876
9371
  "model-distributor",
8877
9372
  "motion",
@@ -8904,6 +9399,7 @@ var CAP_NAMES = Object.freeze([
8904
9399
  "plate-gallery",
8905
9400
  "platform-probe",
8906
9401
  "power-meter",
9402
+ "power-source",
8907
9403
  "presence",
8908
9404
  "pressure-sensor",
8909
9405
  "privacy-mask",
@@ -10255,6 +10751,13 @@ Object.freeze({
10255
10751
  "valueType": "number"
10256
10752
  }
10257
10753
  ],
10754
+ "power-source": [{
10755
+ "path": "switchOn",
10756
+ "valueType": "boolean"
10757
+ }, {
10758
+ "path": "offSeconds",
10759
+ "valueType": "number"
10760
+ }],
10258
10761
  "presence": [
10259
10762
  {
10260
10763
  "path": "state",
@@ -10766,379 +11269,7 @@ object({
10766
11269
  params: record(string(), union([string(), number()])).optional()
10767
11270
  });
10768
11271
  //#endregion
10769
- //#region ../types/dist/err-msg-BD5buxvT.mjs
10770
- var DeviceType = /* @__PURE__ */ function(DeviceType) {
10771
- DeviceType["Camera"] = "camera";
10772
- DeviceType["Hub"] = "hub";
10773
- DeviceType["Light"] = "light";
10774
- DeviceType["Siren"] = "siren";
10775
- DeviceType["Switch"] = "switch";
10776
- DeviceType["Sensor"] = "sensor";
10777
- DeviceType["Thermostat"] = "thermostat";
10778
- /** Air-conditioner / heat-pump climate device (HVAC) — shares the
10779
- * `climate-control` cap surface with `Thermostat` but renders a
10780
- * dedicated AC-appropriate control UI (mode chips, fan speed,
10781
- * independent vertical/horizontal swing). Sources: native Gree, and
10782
- * reusable by other AC integrations. */
10783
- DeviceType["Climate"] = "climate";
10784
- DeviceType["Button"] = "button";
10785
- /** Generic stateless event emitter — carries a device's EXACT declared
10786
- * event vocabulary verbatim (no normalization). Installed with the
10787
- * `event-emitter` cap. Sources: HA `event.*` entities (structured) and
10788
- * HA bus events (e.g. `zha_event`, generic). */
10789
- DeviceType["EventEmitter"] = "event-emitter";
10790
- /** Firmware/software update entity — current vs available version,
10791
- * updatable flag, update state, and an install action. Installed with
10792
- * the `update` cap. Sources: Homematic firmware-update channels (and
10793
- * reusable by other providers, e.g. HA `update.*` entities). */
10794
- DeviceType["Update"] = "update";
10795
- DeviceType["Generic"] = "generic";
10796
- /** Generic notification delivery target (HA `notify.<service>`, future
10797
- * Telegram / Discord / ntfy / SMTP, …). One device per delivery
10798
- * endpoint; the `notifier` cap defines the send surface. */
10799
- DeviceType["Notifier"] = "notifier";
10800
- /** Pre-recorded action sequence with optional parameters
10801
- * (HA `script.*`). Runnable via `script-runner` cap. */
10802
- DeviceType["Script"] = "script";
10803
- /** Automation rule (HA `automation.*`) — enable/disable + manual
10804
- * trigger surface exposed via `automation-control` cap. */
10805
- DeviceType["Automation"] = "automation";
10806
- /** Door / smart lock device (HA `lock.*`). `lock-control` cap. */
10807
- DeviceType["Lock"] = "lock";
10808
- /** Window covering, blinds, garage door, valve, etc. (HA `cover.*`,
10809
- * `valve.*`). `cover` cap with sub-roles for variant. */
10810
- DeviceType["Cover"] = "cover";
10811
- /** Pipe / water / gas valve with open/close/stop and optional
10812
- * position (HA `valve.*`). `valve` cap — a cover-sibling actuator
10813
- * modelled on the same open/closed lifecycle. */
10814
- DeviceType["Valve"] = "valve";
10815
- /** Humidifier / dehumidifier with on/off + target humidity + mode
10816
- * (HA `humidifier.*`). `humidifier` cap — a climate-family actuator
10817
- * modelled on the same target / mode lifecycle. */
10818
- DeviceType["Humidifier"] = "humidifier";
10819
- /** Water heater / boiler with target temperature + operation mode +
10820
- * away mode (HA `water_heater.*`). `water-heater` cap — a
10821
- * climate-family actuator. */
10822
- DeviceType["WaterHeater"] = "water-heater";
10823
- /** Ceiling / standing / exhaust fan (HA `fan.*`). `fan-control` cap. */
10824
- DeviceType["Fan"] = "fan";
10825
- /** Audio / video playback endpoint (HA `media_player.*`). Disjoint from
10826
- * the camera surface — those use `Camera`. `media-player` cap. */
10827
- DeviceType["MediaPlayer"] = "media-player";
10828
- /** Security panel / alarm system (HA `alarm_control_panel.*`).
10829
- * `alarm-panel` cap. */
10830
- DeviceType["AlarmPanel"] = "alarm-panel";
10831
- /** Generic user-settable input (HA `number` / `input_number` / `select`
10832
- * / `input_select` / `text` / `input_text` / `input_datetime`).
10833
- * Sub-type via `DeviceRole`: NumericControl / SelectControl /
10834
- * TextControl / DateTimeControl. */
10835
- DeviceType["Control"] = "control";
10836
- /** Person / device-tracker presence (HA `person.*`, `device_tracker.*`).
10837
- * `presence` cap. */
10838
- DeviceType["Presence"] = "presence";
10839
- /** Weather provider (HA `weather.*`). Tier-3, low MVP priority.
10840
- * `weather` cap. */
10841
- DeviceType["Weather"] = "weather";
10842
- /** Robot vacuum (HA `vacuum.*`). Tier-3. `vacuum-control` cap. */
10843
- DeviceType["Vacuum"] = "vacuum";
10844
- /** Robotic lawn mower (HA `lawn_mower.*`). Tier-3.
10845
- * `lawn-mower-control` cap. */
10846
- DeviceType["LawnMower"] = "lawn-mower";
10847
- /** Physical HA device group — parent container for entity-children
10848
- * adopted from a single HA device entry. Not renderable as a
10849
- * standalone device; exists only to anchor child entities. */
10850
- DeviceType["Container"] = "container";
10851
- /** Single still-image entity (HA `image.*`). Read-only display of an
10852
- * `entity_picture` signed URL the browser loads directly. `image` cap. */
10853
- DeviceType["Image"] = "image";
10854
- /** Smart pet feeder — cloud-connected food dispenser with a bowl food
10855
- * level, battery, desiccant life, feeding state and manual-feed /
10856
- * call-pet / maintenance actions. Installed with the `pet-feeder` cap;
10857
- * dual-hopper models (D4S/D4SH) expose per-hopper portions. Sources:
10858
- * native PetKit (`nodepetkit` `FeederDevice`), reusable by other feeder
10859
- * integrations sharing the same food/desiccant/hopper surface. */
10860
- DeviceType["PetFeeder"] = "pet-feeder";
10861
- /** Pet water fountain — tank level, water drunk, pump and its duty cycle.
10862
- * Installed with the `pet-fountain` cap. Reaches CamStack as loose entities
10863
- * of a container and is BUILT as a composed device (D967). */
10864
- DeviceType["PetFountain"] = "pet-fountain";
10865
- /** Built-in dishwasher — program lifecycle, door / remote-start gates,
10866
- * salt and rinse-aid levels. Installed with the `dishwasher` cap.
10867
- * Sources: native Bosch Home Connect, reusable by similar integrations.
10868
- * A plug-built dishwasher carries running-state and power instead. */
10869
- DeviceType["Dishwasher"] = "dishwasher";
10870
- /** Washing machine. A plug-built one is running while its power stays
10871
- * above a threshold. No program cap — that surface is the dishwasher's. */
10872
- DeviceType["Washer"] = "washer";
10873
- /** Tumble dryer. Same plug-built running surface as {@link Washer}. */
10874
- DeviceType["Dryer"] = "dryer";
10875
- /** The physical bridge box of a vendor ecosystem (Velux KLF-200, and later
10876
- * other radio-to-IP bridges). It carries the box's own facts (firmware,
10877
- * address, connection state) and the actions the box really supports
10878
- * (`reboot`, `device-admin-link`). Products are NOT its children — they are
10879
- * adopted beside it — and its connection settings stay on the integration. */
10880
- DeviceType["Gateway"] = "gateway";
10881
- return DeviceType;
10882
- }({});
10883
- var DeviceFeature = /* @__PURE__ */ function(DeviceFeature) {
10884
- DeviceFeature["BatteryOperated"] = "battery-operated";
10885
- DeviceFeature["Rebootable"] = "rebootable";
10886
- /**
10887
- * Device supports an on-demand re-sync of its derived spec with its
10888
- * upstream source — drives the generic Re-sync button. The owning
10889
- * provider implements the action via the `device-adoption.resync` cap.
10890
- */
10891
- DeviceFeature["Resyncable"] = "resyncable";
10892
- DeviceFeature["NativeSnapshot"] = "native-snapshot";
10893
- DeviceFeature["DoorbellButton"] = "doorbell-button";
10894
- DeviceFeature["TwoWayAudio"] = "two-way-audio";
10895
- DeviceFeature["PanTiltZoom"] = "pan-tilt-zoom";
10896
- /**
10897
- * Camera supports the on-firmware autotrack subsystem (subject-
10898
- * following). Distinct from `PanTiltZoom` because not every PTZ
10899
- * camera ships autotrack — the admin UI uses this flag to gate
10900
- * the autotrack toggle / settings card without re-deriving from
10901
- * the cap registry. Mirrors `ptz-autotrack` cap registration:
10902
- * driver sets this feature when probe confirms the firmware
10903
- * surface, and registers the cap in the same code path.
10904
- */
10905
- DeviceFeature["PtzAutotrack"] = "ptz-autotrack";
10906
- /**
10907
- * Accessory exposes a "trigger on motion" toggle — the parent camera's
10908
- * motion detection automatically activates this device. Mirrors
10909
- * `motion-trigger` cap registration: drivers set this feature in the
10910
- * same code path that calls `ctx.registerNativeCap(motionTriggerCapability, ...)`.
10911
- *
10912
- * Used by admin UI (gate the in-hero `MotionTriggerToggle` against a
10913
- * fast scalar without binding fetch), notifier rules, and `listAll`
10914
- * filters that want "all devices with on-motion behaviour".
10915
- */
10916
- DeviceFeature["MotionTrigger"] = "motion-trigger";
10917
- /** Light supports rgb-triplet color via `color` cap. */
10918
- DeviceFeature["LightColorRgb"] = "light-color-rgb";
10919
- /** Light supports HSV color via `color` cap. */
10920
- DeviceFeature["LightColorHsv"] = "light-color-hsv";
10921
- /** Light supports color-temperature (mired) via `color` cap. */
10922
- DeviceFeature["LightColorMired"] = "light-color-mired";
10923
- /** Thermostat supports a `heat_cool` dual setpoint (targetLow +
10924
- * targetHigh). Gates the range slider UI. */
10925
- DeviceFeature["ClimateDualSetpoint"] = "climate-dual-setpoint";
10926
- /** Thermostat exposes target humidity and/or current humidity
10927
- * readings. Gates the humidity controls. */
10928
- DeviceFeature["ClimateHumidity"] = "climate-humidity";
10929
- /** Thermostat exposes a fan-mode selector. */
10930
- DeviceFeature["ClimateFanMode"] = "climate-fan-mode";
10931
- /** Thermostat exposes preset modes (eco / away / sleep / vendor). */
10932
- DeviceFeature["ClimatePreset"] = "climate-preset";
10933
- /** Thermostat exposes a vertical louver swing toggle. Gates the
10934
- * vertical-swing switch in the climate UI. Independent of horizontal. */
10935
- DeviceFeature["ClimateSwingVertical"] = "climate-swing-vertical";
10936
- /** Thermostat exposes a horizontal louver swing toggle. Gates the
10937
- * horizontal-swing switch in the climate UI. Independent of vertical. */
10938
- DeviceFeature["ClimateSwingHorizontal"] = "climate-swing-horizontal";
10939
- /** Cover exposes intermediate position control (0..100). Gates the
10940
- * position slider UI. */
10941
- DeviceFeature["CoverPositionable"] = "cover-positionable";
10942
- /** Cover exposes slat-tilt control. Gates the tilt slider UI. */
10943
- DeviceFeature["CoverTilt"] = "cover-tilt";
10944
- /** Valve exposes intermediate position control (0..100). Gates the
10945
- * position slider / drag surface UI. */
10946
- DeviceFeature["ValvePositionable"] = "valve-positionable";
10947
- /** Fan exposes a speed-percentage setter. Gates the speed slider UI. */
10948
- DeviceFeature["FanSpeed"] = "fan-speed";
10949
- /** Fan exposes a preset mode selector. */
10950
- DeviceFeature["FanPreset"] = "fan-preset";
10951
- /** Fan exposes blade direction (forward/reverse) — typical of
10952
- * ceiling fans. */
10953
- DeviceFeature["FanDirection"] = "fan-direction";
10954
- /** Fan exposes an oscillation toggle. */
10955
- DeviceFeature["FanOscillating"] = "fan-oscillating";
10956
- /** Lock requires a PIN code on lock/unlock. Gates the code-entry
10957
- * field on the UI lock-controls panel. */
10958
- DeviceFeature["LockPinRequired"] = "lock-pin-required";
10959
- /** Lock supports a latch-release ("open door") action distinct from
10960
- * unlock. Mirrors HA `LockEntityFeature.OPEN` (bit 1) in
10961
- * `supported_features`. Gates the Open Door button in the UI. */
10962
- DeviceFeature["LockOpen"] = "lock-open";
10963
- /** Media player exposes a seek-to-position surface. */
10964
- DeviceFeature["MediaPlayerSeek"] = "media-player-seek";
10965
- /** Media player exposes a volume-level setter. */
10966
- DeviceFeature["MediaPlayerVolume"] = "media-player-volume";
10967
- /** Media player exposes a mute toggle distinct from volume=0. */
10968
- DeviceFeature["MediaPlayerMute"] = "media-player-mute";
10969
- /** Media player exposes a shuffle toggle. */
10970
- DeviceFeature["MediaPlayerShuffle"] = "media-player-shuffle";
10971
- /** Media player exposes a repeat mode (off / all / one). */
10972
- DeviceFeature["MediaPlayerRepeat"] = "media-player-repeat";
10973
- /** Media player exposes a source / input selector. */
10974
- DeviceFeature["MediaPlayerSelectSource"] = "media-player-select-source";
10975
- /** Media player exposes a play-arbitrary-media surface (URL / id). */
10976
- DeviceFeature["MediaPlayerPlayMedia"] = "media-player-play-media";
10977
- /** Media player exposes next-track. */
10978
- DeviceFeature["MediaPlayerNext"] = "media-player-next";
10979
- /** Media player exposes previous-track. */
10980
- DeviceFeature["MediaPlayerPrevious"] = "media-player-previous";
10981
- /** Media player exposes stop distinct from pause. */
10982
- DeviceFeature["MediaPlayerStop"] = "media-player-stop";
10983
- /** Alarm panel requires a PIN code on arm/disarm. */
10984
- DeviceFeature["AlarmPinRequired"] = "alarm-pin-required";
10985
- /** Presence device carries GPS coordinates (lat/lng/accuracy) in
10986
- * addition to a textual location. */
10987
- DeviceFeature["PresenceGps"] = "presence-gps";
10988
- /** Notifier accepts an inline / URL image attachment. */
10989
- DeviceFeature["NotifierImage"] = "notifier-image";
10990
- /** Notifier accepts a priority hint (high/normal/low). */
10991
- DeviceFeature["NotifierPriority"] = "notifier-priority";
10992
- /** Notifier accepts a free-form `data` payload for platform-specific
10993
- * fields. */
10994
- DeviceFeature["NotifierData"] = "notifier-data";
10995
- /** Notifier supports interactive action buttons / callbacks. */
10996
- DeviceFeature["NotifierActions"] = "notifier-actions";
10997
- /** Notifier supports per-call recipient targeting (multi-user). */
10998
- DeviceFeature["NotifierRecipients"] = "notifier-recipients";
10999
- /** Script runner accepts a variables map on each run invocation. */
11000
- DeviceFeature["ScriptVariables"] = "script-variables";
11001
- /** Automation `trigger` accepts a skipCondition flag — fires the
11002
- * automation's actions while bypassing its condition block. */
11003
- DeviceFeature["AutomationSkipCondition"] = "automation-skip-condition";
11004
- /** Robot vacuum exposes a live cleaning map (image child). Gates the
11005
- * map tile in the vacuum UI. */
11006
- DeviceFeature["VacuumHasMap"] = "vacuum-has-map";
11007
- /** Robot vacuum exposes AI obstacle-detection toggles. Gates the AI
11008
- * switches group. */
11009
- DeviceFeature["VacuumHasAi"] = "vacuum-has-ai";
11010
- /** Robot mower exposes a live mowing map (SVG image child). Gates the
11011
- * mower map tile. */
11012
- DeviceFeature["MowerHasMap"] = "mower-has-map";
11013
- /** Robot mower exposes targeted mowing (all-area / zones / edges /
11014
- * spots) — gates the Mow action + map selectors. */
11015
- DeviceFeature["MowerHasTargetedMowing"] = "mower-has-targeted-mowing";
11016
- return DeviceFeature;
11017
- }({});
11018
- /**
11019
- * Semantic role a device plays within its parent. Populated by driver
11020
- * addons when creating accessory devices (Reolink siren/floodlight/
11021
- * PIR/chime/autotrack/doorbell, ONVIF relay outputs, …). Used by the
11022
- * admin UI to pick icons, labels, and widgets — a `Switch` with
11023
- * `role: Floodlight` renders as a bulb with a brightness slider,
11024
- * whereas a `Switch` with `role: Siren` renders as a klaxon.
11025
- *
11026
- * Undefined for top-level devices (cameras, NVRs, hubs). Persisted in
11027
- * sqlite as a nullable TEXT column — old rows keep working unchanged.
11028
- */
11029
- var DeviceRole = /* @__PURE__ */ function(DeviceRole) {
11030
- DeviceRole["Siren"] = "siren";
11031
- DeviceRole["Floodlight"] = "floodlight";
11032
- DeviceRole["Spotlight"] = "spotlight";
11033
- DeviceRole["PirSensor"] = "pir-sensor";
11034
- DeviceRole["Chime"] = "chime";
11035
- DeviceRole["Autotrack"] = "autotrack";
11036
- DeviceRole["Nightvision"] = "nightvision";
11037
- DeviceRole["PrivacyMask"] = "privacy-mask";
11038
- DeviceRole["Doorbell"] = "doorbell";
11039
- /** Interior blind / shade / roller blind. HA `device_class: blind|shade`. */
11040
- DeviceRole["CoverBlind"] = "cover-blind";
11041
- /** Exterior roller shutter. HA `device_class: shutter`; Homematic BROLL. */
11042
- DeviceRole["CoverShutter"] = "cover-shutter";
11043
- /** Exterior awning. HA `device_class: awning`. */
11044
- DeviceRole["CoverAwning"] = "cover-awning";
11045
- /** Curtain / drape. HA `device_class: curtain`. */
11046
- DeviceRole["CoverCurtain"] = "cover-curtain";
11047
- /** Garage door. HA `device_class: garage`. A security actuator: exports gate it. */
11048
- DeviceRole["CoverGarage"] = "cover-garage";
11049
- /** Gate. HA `device_class: gate`. A security actuator: exports gate it. */
11050
- DeviceRole["CoverGate"] = "cover-gate";
11051
- /** Door. HA `device_class: door`. A security actuator: exports gate it. */
11052
- DeviceRole["CoverDoor"] = "cover-door";
11053
- /** Window. HA `device_class: window`. */
11054
- DeviceRole["CoverWindow"] = "cover-window";
11055
- /** Virtual HA toggle (input_boolean.*) — distinguishable from a
11056
- * real Switch device for UI rendering / export adapters. */
11057
- DeviceRole["BinaryHelper"] = "binary-helper";
11058
- /** A smart plug: a `switch` whose source says it is an outlet (HA
11059
- * `device_class: outlet`). The Alexa export shows it as a SMARTPLUG. */
11060
- DeviceRole["Outlet"] = "outlet";
11061
- /** Generic motion / occupancy / moving event source. Distinct from
11062
- * the camera accessory PirSensor role: that one is a camera child;
11063
- * this is a standalone HA / 3rd-party motion sensor. */
11064
- DeviceRole["MotionSensor"] = "motion-sensor";
11065
- DeviceRole["ContactSensor"] = "contact-sensor";
11066
- DeviceRole["LeakSensor"] = "leak-sensor";
11067
- DeviceRole["SmokeSensor"] = "smoke-sensor";
11068
- DeviceRole["COSensor"] = "co-sensor";
11069
- DeviceRole["GasSensor"] = "gas-sensor";
11070
- DeviceRole["TamperSensor"] = "tamper-sensor";
11071
- DeviceRole["VibrationSensor"] = "vibration-sensor";
11072
- DeviceRole["ConnectivitySensor"] = "connectivity-sensor";
11073
- DeviceRole["SoundSensor"] = "sound-sensor";
11074
- /** Fallback for `binary_sensor` without a known `device_class`. */
11075
- DeviceRole["BinarySensor"] = "binary-sensor";
11076
- DeviceRole["TemperatureSensor"] = "temperature-sensor";
11077
- DeviceRole["HumiditySensor"] = "humidity-sensor";
11078
- DeviceRole["AmbientLightSensor"] = "ambient-light-sensor";
11079
- DeviceRole["PressureSensor"] = "pressure-sensor";
11080
- /** Wind speed or direction (weather-station `wind-sensor` cap). */
11081
- DeviceRole["WindSensor"] = "wind-sensor";
11082
- /** Rain accumulation or rate (weather-station `rain-sensor` cap). */
11083
- DeviceRole["RainSensor"] = "rain-sensor";
11084
- /** UV index (weather-station `uv-sensor` cap). */
11085
- DeviceRole["UvSensor"] = "uv-sensor";
11086
- /** Solar irradiance W/m² (weather-station `solar-radiation-sensor` cap).
11087
- * Distinct from AmbientLightSensor (lux). */
11088
- DeviceRole["SolarRadiationSensor"] = "solar-radiation-sensor";
11089
- /** Soil moisture % (garden/weather `soil-moisture-sensor` cap). */
11090
- DeviceRole["SoilMoistureSensor"] = "soil-moisture-sensor";
11091
- DeviceRole["PowerSensor"] = "power-sensor";
11092
- DeviceRole["EnergySensor"] = "energy-sensor";
11093
- DeviceRole["VoltageSensor"] = "voltage-sensor";
11094
- DeviceRole["CurrentSensor"] = "current-sensor";
11095
- DeviceRole["AirQualitySensor"] = "air-quality-sensor";
11096
- /** Battery level (numeric % via `sensor` OR low-bool via
11097
- * `binary_sensor` — the cap distinguishes via the value type). */
11098
- DeviceRole["BatterySensor"] = "battery-sensor";
11099
- /** Fallback for `sensor` numeric without a known `device_class`. */
11100
- DeviceRole["NumericSensor"] = "numeric-sensor";
11101
- /** String / enum state (HA `sensor` with `state_class: enum` or
11102
- * `attributes.options`). */
11103
- DeviceRole["EnumSensor"] = "enum-sensor";
11104
- /** Date / timestamp state (HA `sensor` with `device_class: timestamp`
11105
- * or `date`). The slice carries the raw ISO string verbatim (hosted on
11106
- * the `enum-sensor` cap); the UI renders it locale-formatted. */
11107
- DeviceRole["DateTimeSensor"] = "datetime-sensor";
11108
- /** Last-resort fallback when nothing else matches. */
11109
- DeviceRole["GenericSensor"] = "generic-sensor";
11110
- DeviceRole["NumericControl"] = "numeric-control";
11111
- DeviceRole["SelectControl"] = "select-control";
11112
- DeviceRole["TextControl"] = "text-control";
11113
- DeviceRole["DateTimeControl"] = "datetime-control";
11114
- /** Mobile push notifier (HA `notify.mobile_app_*`) — supports
11115
- * rich features (image, priority, channel routing). */
11116
- DeviceRole["MobilePushNotifier"] = "mobile-push-notifier";
11117
- /** Chat / messaging service (HA `notify.telegram_*`,
11118
- * `notify.discord_*`, etc.). */
11119
- DeviceRole["MessagingNotifier"] = "messaging-notifier";
11120
- /** Email-based delivery (HA `notify.smtp`, etc.). */
11121
- DeviceRole["EmailNotifier"] = "email-notifier";
11122
- /** Fallback when the notifier service name doesn't match a known
11123
- * pattern. */
11124
- DeviceRole["GenericNotifier"] = "generic-notifier";
11125
- /** Emits a pulse train (infrared or RF) on request — the `ir-transmitter`
11126
- * cap. A `DeviceType.Generic` device. */
11127
- DeviceRole["IrTransmitter"] = "ir-transmitter";
11128
- return DeviceRole;
11129
- }({});
11130
- /**
11131
- import { errMsg } from '@camstack/types'
11132
- * Extract a human-readable message from an unknown error value.
11133
- * Replaces the ubiquitous `errMsg(err)` pattern.
11134
- */
11135
- function errMsg(err) {
11136
- if (err instanceof Error) return err.message;
11137
- if (typeof err === "string") return err;
11138
- return String(err);
11139
- }
11140
- //#endregion
11141
- //#region ../types/dist/composition-C7WdThmP.mjs
11272
+ //#region ../types/dist/composition-BBzVf2xd.mjs
11142
11273
  /**
11143
11274
  * The ffmpeg build every CamStack node downloads, per platform and arch.
11144
11275
  *
@@ -12357,6 +12488,71 @@ var EventOwnerTypeSchema = _enum(EVENT_OWNER_TYPES);
12357
12488
  var MediaPresenceOwnerKindSchema = _enum([...EVENT_OWNER_TYPES, "track"]);
12358
12489
  new Set(EVENT_OWNER_TYPES);
12359
12490
  /**
12491
+ * What a published package needs from the rest of the install (D989).
12492
+ *
12493
+ * One published framework package (`@camstack/server`) carries the kernel, the
12494
+ * builtins and the capability contracts, so "which addon runs on which
12495
+ * framework" is a question of version, and the answer is DECLARED by the
12496
+ * package that knows it:
12497
+ *
12498
+ * - an addon says the oldest server it runs on (`requires.server: ">=X"`);
12499
+ * - the server says the oldest addon it works with
12500
+ * (`requires.addons: { "@camstack/addon-x": ">=Y" }`) and the oldest shell
12501
+ * around it (`requires.wrapper: { desktop, image }`);
12502
+ * - a wrapper (the desktop app, the container starter) says which server
12503
+ * range it can host, and reports its own kind and version.
12504
+ *
12505
+ * The publish workflow STAMPS these after the version bump
12506
+ * (`scripts/ci/stamp-requires.mjs`); nobody types a number by hand.
12507
+ *
12508
+ * A requirement that cannot be checked is NOT met: a shell that reports no
12509
+ * version counts as below the minimum, and an unreadable running version is
12510
+ * named in the result (a failed read is never a pass).
12511
+ */
12512
+ /** `>=MAJOR.MINOR.PATCH[-prerelease]` — the only range shape a requirement uses. */
12513
+ var MinVersionSchema = string().regex(/^>=\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/, "a minimum is written \">=X.Y.Z\"");
12514
+ object({
12515
+ /** The oldest `@camstack/server` this package runs on. */
12516
+ server: MinVersionSchema.optional(),
12517
+ /** The oldest installed version of each named addon package. */
12518
+ addons: record(string(), MinVersionSchema).optional(),
12519
+ /** The oldest shell around the server, per shell kind. */
12520
+ wrapper: object({
12521
+ desktop: MinVersionSchema.optional(),
12522
+ image: MinVersionSchema.optional()
12523
+ }).optional()
12524
+ });
12525
+ /** One unmet requirement; each names the versions on both sides. */
12526
+ var UnmetRequirementSchema = discriminatedUnion("kind", [
12527
+ object({
12528
+ kind: literal("server-too-old"),
12529
+ required: string(),
12530
+ /** `null` — the running server version could not be read. */
12531
+ running: string().nullable()
12532
+ }),
12533
+ object({
12534
+ kind: literal("addon-too-old"),
12535
+ packageName: string(),
12536
+ required: string(),
12537
+ /** `null` — the addon is not installed (or its version is unreadable). */
12538
+ running: string().nullable()
12539
+ }),
12540
+ object({
12541
+ kind: literal("wrapper-too-old"),
12542
+ wrapper: _enum(["desktop", "image"]),
12543
+ required: string(),
12544
+ /** `null` — the wrapper reported no version. */
12545
+ running: string().nullable()
12546
+ })
12547
+ ]);
12548
+ var RequiredAddonUpdateSchema = object({
12549
+ packageName: string(),
12550
+ /** The minimum the target server declares. */
12551
+ required: string(),
12552
+ /** What this node has; `null` when the addon is not installed. */
12553
+ installed: string().nullable()
12554
+ });
12555
+ /**
12360
12556
  * Which integration instance a settings form is for. An addon that serves several instances
12361
12557
  * (`instanceMode: multiple`) can show a different form per instance — an NVR integration
12362
12558
  * needs its connection, the default integration needs the addon's own sections.
@@ -12831,6 +13027,613 @@ var CameraSwitchGroupSchema = object({
12831
13027
  fetchedAt: number()
12832
13028
  });
12833
13029
  /**
13030
+ * Query filter for settings-store collections.
13031
+ */
13032
+ var QueryFilterSchema = object({
13033
+ where: record(string(), unknown()).optional(),
13034
+ whereIn: record(string(), array(unknown())).optional(),
13035
+ whereBetween: record(string(), tuple([unknown(), unknown()])).optional(),
13036
+ /** NULL-safe exclusion: matches rows whose field is NULL OR != the value. */
13037
+ whereNot: record(string(), unknown()).optional(),
13038
+ /** NULL-safe exclusion of a SET — `whereNot` for more than one value. An
13039
+ * empty list excludes nothing. See `QueryFilter.whereNotIn`. */
13040
+ whereNotIn: record(string(), array(unknown())).optional(),
13041
+ orderBy: object({
13042
+ field: string(),
13043
+ direction: _enum(["asc", "desc"])
13044
+ }).optional(),
13045
+ limit: number().optional(),
13046
+ offset: number().optional()
13047
+ });
13048
+ /**
13049
+ * The predicate half of a filter, for BULK MUTATIONS.
13050
+ *
13051
+ * Deliberately not `QueryFilterSchema`: `orderBy` / `limit` / `offset` have no
13052
+ * meaning for a statement that rewrites a set, and accepting them would invite
13053
+ * a caller to believe `limit` bounds the damage. Every field is optional here
13054
+ * only so the shape stays composable — the implementation REJECTS a filter
13055
+ * that compiles to no predicate, because that is the whole collection.
13056
+ */
13057
+ var MutationFilterSchema = object({
13058
+ where: record(string(), unknown()).optional(),
13059
+ whereIn: record(string(), array(unknown())).optional(),
13060
+ whereBetween: record(string(), tuple([unknown(), unknown()])).optional(),
13061
+ whereNot: record(string(), unknown()).optional(),
13062
+ whereNotIn: record(string(), array(unknown())).optional()
13063
+ });
13064
+ /**
13065
+ * One scalar an {@link settingsStoreCapability.methods.aggregate} call asks for.
13066
+ *
13067
+ * `as` names the slot in the result, so the SAME column may be asked twice with
13068
+ * two operations (`MIN(startMs)` and `MAX(startMs)` in one round trip) — which
13069
+ * a `Record<column, op>` shape could not express.
13070
+ */
13071
+ var AggregateFieldSchema = object({
13072
+ /** Result key. */
13073
+ as: string().min(1),
13074
+ /** Column to aggregate. Must be a real column of a declared collection. */
13075
+ field: string().min(1),
13076
+ op: _enum([
13077
+ "sum",
13078
+ "min",
13079
+ "max"
13080
+ ])
13081
+ });
13082
+ /**
13083
+ * `COUNT(*)` plus one number per requested field.
13084
+ *
13085
+ * `null` means NO ROW MATCHED, never zero: a `SUM` over an empty set and a sum
13086
+ * that really is 0 are different facts, and an accounting caller that renders
13087
+ * "0 bytes, oldest = 0" for "nothing here" reports a lie about a disk.
13088
+ */
13089
+ var AggregateResultSchema = object({
13090
+ count: number().int(),
13091
+ values: record(string(), number().nullable())
13092
+ });
13093
+ /** A single stored record: `{ id, data }`. */
13094
+ var SettingsRecordSchema = object({
13095
+ id: string(),
13096
+ data: record(string(), unknown())
13097
+ });
13098
+ /**
13099
+ * One record of a BULK insert — {@link SettingsRecordSchema} with the id made
13100
+ * optional.
13101
+ *
13102
+ * A separate schema rather than loosening the shared one: every other method
13103
+ * on this cap addresses a row BY its id, and making that field optional
13104
+ * everywhere would turn a forgotten key into a silently generated one on
13105
+ * `update` and `delete` as well.
13106
+ */
13107
+ var BulkRecordSchema = object({
13108
+ id: string().optional(),
13109
+ data: record(string(), unknown())
13110
+ });
13111
+ /**
13112
+ * The database FILES one `sqlite-settings` engine process owns (D851).
13113
+ * Closed on purpose: a file is a thing the engine opens, backs up and gives a
13114
+ * serving thread, so a new one is a code change, never a string a caller
13115
+ * invents. `settings` is `camstack.db` and the default for every collection
13116
+ * that names no file.
13117
+ */
13118
+ var DataStoreDatabaseSchema = _enum([
13119
+ "settings",
13120
+ "events",
13121
+ "history"
13122
+ ]);
13123
+ DataStoreDatabaseSchema.options;
13124
+ /**
13125
+ * One write inside a {@link settingsStoreCapability.methods.transact} call.
13126
+ * The three that a "replace this set, bump that revision" write needs, plus
13127
+ * the batched upsert a rollup writer needs.
13128
+ */
13129
+ var TransactOpSchema = discriminatedUnion("op", [
13130
+ object({
13131
+ op: literal("deleteWhere"),
13132
+ collection: string(),
13133
+ filter: MutationFilterSchema
13134
+ }),
13135
+ object({
13136
+ op: literal("insertMany"),
13137
+ collection: string(),
13138
+ records: array(BulkRecordSchema).readonly()
13139
+ }),
13140
+ object({
13141
+ op: literal("update"),
13142
+ collection: string(),
13143
+ id: string(),
13144
+ data: record(string(), unknown())
13145
+ }),
13146
+ object({
13147
+ op: literal("upsertMany"),
13148
+ collection: string(),
13149
+ conflict: array(string()).readonly(),
13150
+ records: array(BulkRecordSchema).readonly()
13151
+ })
13152
+ ]);
13153
+ /**
13154
+ * The precondition of a transaction: the row `key` of `collection` must hold
13155
+ * `equals` in `column`, read INSIDE the transaction.
13156
+ */
13157
+ var TransactGuardSchema = object({
13158
+ collection: string(),
13159
+ key: string(),
13160
+ column: string(),
13161
+ equals: unknown()
13162
+ });
13163
+ /**
13164
+ * Column declaration for a structured (SQL-backed) collection.
13165
+ *
13166
+ * Logical types — the backend translates each to the matching SQLite
13167
+ * storage class and handles per-type marshaling:
13168
+ * - `TEXT` / `INTEGER` / `REAL` — native SQLite types, pass-through
13169
+ * - `JSON` — TEXT under the hood; serialised on write, parsed on read
13170
+ * - `BOOLEAN` — INTEGER 0/1 under the hood; coerced both directions
13171
+ */
13172
+ var CollectionColumnSchema = object({
13173
+ name: string(),
13174
+ type: _enum([
13175
+ "TEXT",
13176
+ "INTEGER",
13177
+ "REAL",
13178
+ "JSON",
13179
+ "BOOLEAN"
13180
+ ]),
13181
+ primaryKey: boolean().optional(),
13182
+ notNull: boolean().optional(),
13183
+ unique: boolean().optional(),
13184
+ /**
13185
+ * Column DEFAULT. Required for B2: the four `ensureTable` consumers declare
13186
+ * columns like `enabled INTEGER NOT NULL DEFAULT 1`, and without this the
13187
+ * collection surface simply cannot express their existing tables — which is
13188
+ * why they were still on the uncapped `table*` API.
13189
+ */
13190
+ defaultValue: union([
13191
+ string(),
13192
+ number(),
13193
+ boolean()
13194
+ ]).optional()
13195
+ });
13196
+ var CollectionIndexSchema = object({
13197
+ name: string(),
13198
+ columns: array(string()).readonly(),
13199
+ unique: boolean().optional()
13200
+ });
13201
+ /**
13202
+ * The input of `declareCollection` — ONE schema for the door and for the
13203
+ * engine behind it, so the two surfaces cannot drift (the dispatcher forwards
13204
+ * the payload untouched).
13205
+ *
13206
+ * - `database` — the file the collection lives in (D851). Absent means
13207
+ * `settings`. A declaration naming a file other than the one the table is
13208
+ * already in is REFUSED by the engine, never moved.
13209
+ * - `primaryKey` — a COMPOSITE key, as column names. A single name is the same
13210
+ * as `primaryKey: true` on that column. A composite-keyed collection has no
13211
+ * `id`: it is written with `insertMany` / `upsertMany` / `transact` and read
13212
+ * with `query` / `count` / `aggregate` / `deleteWhere`; the calls that
13213
+ * address one row by id are refused.
13214
+ * - `withoutRowid` — emit `WITHOUT ROWID`. Requires a primary key.
13215
+ */
13216
+ var DeclareCollectionInputSchema = object({
13217
+ namespace: string().optional(),
13218
+ collection: string(),
13219
+ columns: array(CollectionColumnSchema).readonly(),
13220
+ indexes: array(CollectionIndexSchema).readonly().optional(),
13221
+ database: DataStoreDatabaseSchema.optional(),
13222
+ primaryKey: array(string()).readonly().optional(),
13223
+ withoutRowid: boolean().optional()
13224
+ });
13225
+ method(object({
13226
+ namespace: string().optional(),
13227
+ collection: string(),
13228
+ key: string()
13229
+ }), unknown(), { auth: "admin" }), method(object({
13230
+ namespace: string().optional(),
13231
+ collection: string(),
13232
+ key: string(),
13233
+ value: unknown()
13234
+ }), _void(), {
13235
+ kind: "mutation",
13236
+ auth: "admin"
13237
+ }), method(object({
13238
+ namespace: string().optional(),
13239
+ collection: string(),
13240
+ filter: QueryFilterSchema.optional(),
13241
+ columns: array(string()).readonly().optional()
13242
+ }), array(SettingsRecordSchema).readonly(), { auth: "admin" }), method(object({
13243
+ namespace: string().optional(),
13244
+ collection: string(),
13245
+ /** Declared columns identifying the slot; must be covered by a UNIQUE
13246
+ * index on the collection. Never empty. */
13247
+ conflict: array(string()).readonly(),
13248
+ record: BulkRecordSchema
13249
+ }), object({
13250
+ /** The id the row HAS: assigned by SQLite on an insert, or the existing
13251
+ * row's own id on an update. */
13252
+ id: union([string(), number()]) }), {
13253
+ kind: "mutation",
13254
+ auth: "admin"
13255
+ }), method(object({
13256
+ namespace: string().optional(),
13257
+ collection: string(),
13258
+ conflict: array(string()).readonly(),
13259
+ records: array(BulkRecordSchema).readonly()
13260
+ }), object({ upserted: number().int() }), {
13261
+ kind: "mutation",
13262
+ auth: "admin"
13263
+ }), method(object({
13264
+ namespace: string().optional(),
13265
+ collection: string(),
13266
+ record: object({
13267
+ id: string().optional(),
13268
+ data: record(string(), unknown())
13269
+ })
13270
+ }), object({
13271
+ /**
13272
+ * The id the row ACTUALLY got (D473): the one supplied, the UUID minted
13273
+ * for an absent one, or the ROWID SQLite assigned on an `INTEGER`
13274
+ * primary key — which is the only place an auto key is knowable.
13275
+ */
13276
+ id: union([string(), number()]) }), {
13277
+ kind: "mutation",
13278
+ auth: "admin"
13279
+ }), method(object({
13280
+ namespace: string().optional(),
13281
+ collection: string(),
13282
+ records: array(BulkRecordSchema).readonly()
13283
+ }), object({ inserted: number().int() }), {
13284
+ kind: "mutation",
13285
+ auth: "admin"
13286
+ }), method(object({
13287
+ namespace: string().optional(),
13288
+ guard: TransactGuardSchema.optional(),
13289
+ ops: array(TransactOpSchema).readonly()
13290
+ }), object({
13291
+ applied: boolean(),
13292
+ current: unknown().optional()
13293
+ }), {
13294
+ kind: "mutation",
13295
+ auth: "admin"
13296
+ }), method(object({
13297
+ namespace: string().optional(),
13298
+ collection: string(),
13299
+ id: string(),
13300
+ data: record(string(), unknown())
13301
+ }), _void(), {
13302
+ kind: "mutation",
13303
+ auth: "admin"
13304
+ }), method(object({
13305
+ namespace: string().optional(),
13306
+ collection: string(),
13307
+ key: string()
13308
+ }), _void(), {
13309
+ kind: "mutation",
13310
+ auth: "admin"
13311
+ }), method(object({
13312
+ namespace: string().optional(),
13313
+ collection: string(),
13314
+ filter: MutationFilterSchema
13315
+ }), object({ deleted: number().int() }), {
13316
+ kind: "mutation",
13317
+ auth: "admin"
13318
+ }), method(object({
13319
+ namespace: string().optional(),
13320
+ collection: string(),
13321
+ filter: MutationFilterSchema,
13322
+ data: record(string(), unknown())
13323
+ }), object({ updated: number().int() }), {
13324
+ kind: "mutation",
13325
+ auth: "admin"
13326
+ }), method(object({
13327
+ namespace: string().optional(),
13328
+ collection: string(),
13329
+ filter: QueryFilterSchema.optional()
13330
+ }), number(), { auth: "admin" }), method(object({
13331
+ namespace: string().optional(),
13332
+ collection: string(),
13333
+ fields: array(AggregateFieldSchema).readonly(),
13334
+ filter: QueryFilterSchema.optional()
13335
+ }), AggregateResultSchema, { auth: "admin" }), method(object({
13336
+ namespace: string().optional(),
13337
+ collection: string(),
13338
+ field: string(),
13339
+ bucketSize: number().int().positive(),
13340
+ origin: number().int(),
13341
+ filter: QueryFilterSchema.optional()
13342
+ }), array(object({
13343
+ bucket: number().int(),
13344
+ count: number().int()
13345
+ })).readonly(), { auth: "admin" }), method(object({
13346
+ namespace: string().optional(),
13347
+ collection: string()
13348
+ }), boolean(), { auth: "admin" }), method(DeclareCollectionInputSchema, _void(), {
13349
+ kind: "mutation",
13350
+ auth: "admin"
13351
+ });
13352
+ var MigrationIdSchema = string().regex(/^[a-z0-9-]+\/\d{4}-\d{2}-\d{2}-[a-z0-9-]+$/, "a migration id is \"<owner>/<yyyy-mm-dd>-<slug>\"");
13353
+ var MigrationLevelSchema = _enum([
13354
+ "hub-store",
13355
+ "addon-state",
13356
+ "node-config",
13357
+ "viewer",
13358
+ "addon-internal"
13359
+ ]);
13360
+ var MigrationStoreSchema = _enum([
13361
+ "settings",
13362
+ "events",
13363
+ "history",
13364
+ "node-local",
13365
+ "addon-files"
13366
+ ]);
13367
+ var MigrationScopeSchema = _enum(["cluster", "per-node"]);
13368
+ /** What depends on a migration while it is pending: `block-owner` is the default. */
13369
+ var MigrationOnFailureSchema = _enum(["block-owner", "tolerate"]);
13370
+ /**
13371
+ * `sql` is a hub-only body over the owner's connection. `plan` returns edits the
13372
+ * engine applies inside the migration's own transaction (hub-owned: computed in
13373
+ * that transaction; addon-owned: computed by the owner and shipped to the engine).
13374
+ * `procedure` is addon-owned work that is not SQL (keychain, integration registry,
13375
+ * device tombstones); the ledger records its outcome and it must be idempotent.
13376
+ */
13377
+ var MigrationBodyKindSchema = _enum([
13378
+ "sql",
13379
+ "plan",
13380
+ "procedure"
13381
+ ]);
13382
+ var MigrationMetaSchema = object({
13383
+ id: MigrationIdSchema,
13384
+ /** `hub`, an addon id, `node` or `viewer`. */
13385
+ owner: string().regex(/^[a-z0-9-]+$/),
13386
+ level: MigrationLevelSchema,
13387
+ /** Strictly increasing per owner: the order within an owner. */
13388
+ seq: number().int().positive(),
13389
+ /** Ids of any owner: the order across owners. */
13390
+ dependsOn: array(MigrationIdSchema),
13391
+ store: MigrationStoreSchema,
13392
+ scope: MigrationScopeSchema,
13393
+ onFailure: MigrationOnFailureSchema,
13394
+ bodyKind: MigrationBodyKindSchema,
13395
+ description: string().min(1),
13396
+ /** The marked field the migration's spec must assert survived. */
13397
+ fixtureMark: string().min(1)
13398
+ }).refine((m) => m.id.startsWith(`${m.owner}/`), {
13399
+ message: "the id must start with \"<owner>/\"",
13400
+ path: ["id"]
13401
+ });
13402
+ /** A cycle in the dependency graph, named by the ids on it. */
13403
+ var MigrationCycleError = class extends Error {
13404
+ ids;
13405
+ constructor(ids) {
13406
+ super(`migration dependsOn cycle: ${ids.join(" -> ")}`);
13407
+ this.ids = ids;
13408
+ this.name = "MigrationCycleError";
13409
+ }
13410
+ };
13411
+ /**
13412
+ * Topological order over `dependsOn` plus the implicit edge "a lower `seq` of an
13413
+ * owner runs first". Ties are broken by `(owner, seq)`. Throws
13414
+ * {@link MigrationCycleError} on a cycle. A `dependsOn` id that is not in the list
13415
+ * is ignored here — the catalogue schema is what refuses it.
13416
+ */
13417
+ function orderMigrations(items) {
13418
+ const byId = new Map(items.map((item) => [item.id, item]));
13419
+ const deps = /* @__PURE__ */ new Map();
13420
+ const ownerSorted = /* @__PURE__ */ new Map();
13421
+ for (const item of items) {
13422
+ const list = ownerSorted.get(item.owner) ?? [];
13423
+ list.push(item);
13424
+ ownerSorted.set(item.owner, list);
13425
+ }
13426
+ for (const item of items) deps.set(item.id, new Set(item.dependsOn.filter((id) => byId.has(id))));
13427
+ for (const list of ownerSorted.values()) {
13428
+ list.sort((a, b) => a.seq - b.seq);
13429
+ for (let i = 1; i < list.length; i++) {
13430
+ const previous = list[i - 1];
13431
+ const current = list[i];
13432
+ if (previous !== void 0 && current !== void 0) deps.get(current.id)?.add(previous.id);
13433
+ }
13434
+ }
13435
+ const tie = (a, b) => a.owner < b.owner ? -1 : a.owner > b.owner ? 1 : a.seq - b.seq;
13436
+ const done = /* @__PURE__ */ new Set();
13437
+ const ordered = [];
13438
+ let remaining = [...items];
13439
+ while (remaining.length > 0) {
13440
+ const ready = remaining.filter((item) => [...deps.get(item.id) ?? []].every((dep) => done.has(dep)));
13441
+ if (ready.length === 0) throw new MigrationCycleError(remaining.map((item) => item.id).sort());
13442
+ ready.sort(tie);
13443
+ const next = ready[0];
13444
+ if (next === void 0) break;
13445
+ ordered.push(next);
13446
+ done.add(next.id);
13447
+ remaining = remaining.filter((item) => item.id !== next.id);
13448
+ }
13449
+ return ordered;
13450
+ }
13451
+ /** Every way a list of migrations (in declaration order) is not a catalogue. */
13452
+ function catalogueIssues(items) {
13453
+ const issues = [];
13454
+ const seen = /* @__PURE__ */ new Set();
13455
+ const known = new Set(items.map((item) => item.id));
13456
+ const lastSeq = /* @__PURE__ */ new Map();
13457
+ const seqUsed = /* @__PURE__ */ new Map();
13458
+ for (const item of items) {
13459
+ if (seen.has(item.id)) issues.push({
13460
+ code: "duplicate-id",
13461
+ ids: [item.id],
13462
+ message: `duplicate id ${item.id}`
13463
+ });
13464
+ seen.add(item.id);
13465
+ const used = seqUsed.get(item.owner) ?? /* @__PURE__ */ new Set();
13466
+ if (used.has(item.seq)) issues.push({
13467
+ code: "duplicate-seq",
13468
+ ids: [item.id],
13469
+ message: `owner ${item.owner} has two migrations with seq ${item.seq}`
13470
+ });
13471
+ else if (item.seq < (lastSeq.get(item.owner) ?? 0)) issues.push({
13472
+ code: "seq-order",
13473
+ ids: [item.id],
13474
+ message: `seq ${item.seq} of ${item.id} does not increase in declaration order (owner ${item.owner})`
13475
+ });
13476
+ used.add(item.seq);
13477
+ seqUsed.set(item.owner, used);
13478
+ lastSeq.set(item.owner, Math.max(item.seq, lastSeq.get(item.owner) ?? 0));
13479
+ for (const dep of item.dependsOn) if (!known.has(dep)) issues.push({
13480
+ code: "unknown-dependency",
13481
+ ids: [item.id, dep],
13482
+ message: `${item.id} dependsOn unknown id ${dep}`
13483
+ });
13484
+ }
13485
+ try {
13486
+ orderMigrations(items);
13487
+ } catch (err) {
13488
+ if (err instanceof MigrationCycleError) issues.push({
13489
+ code: "cycle",
13490
+ ids: err.ids,
13491
+ message: err.message
13492
+ });
13493
+ else throw err;
13494
+ }
13495
+ return issues;
13496
+ }
13497
+ array(MigrationMetaSchema).superRefine((items, ctx) => {
13498
+ for (const issue of catalogueIssues(items)) ctx.addIssue({
13499
+ code: "custom",
13500
+ message: `${issue.code}: ${issue.message}`
13501
+ });
13502
+ });
13503
+ /**
13504
+ * The canonical JSON (`preconditionOf`) of the value a plan read; the apply step
13505
+ * re-reads, and a plan whose read no longer matches is refused (the migration is
13506
+ * `failed`, nothing it edited is kept).
13507
+ */
13508
+ var PreconditionSchema = string().min(1).optional();
13509
+ var MigrationPlanSchema = object({ edits: array(discriminatedUnion("op", [
13510
+ object({
13511
+ op: literal("setKeys"),
13512
+ scope: string().min(1),
13513
+ values: record(string(), unknown()),
13514
+ precondition: PreconditionSchema
13515
+ }),
13516
+ object({
13517
+ op: literal("deleteKeys"),
13518
+ scope: string().min(1),
13519
+ keys: array(string().min(1)).min(1),
13520
+ precondition: PreconditionSchema
13521
+ }),
13522
+ object({
13523
+ op: literal("putRow"),
13524
+ table: string().min(1),
13525
+ key: record(string(), unknown()),
13526
+ row: record(string(), unknown()),
13527
+ precondition: PreconditionSchema
13528
+ }),
13529
+ object({
13530
+ op: literal("deleteRow"),
13531
+ table: string().min(1),
13532
+ key: record(string(), unknown()),
13533
+ precondition: PreconditionSchema
13534
+ }),
13535
+ object({
13536
+ op: literal("upsertRecord"),
13537
+ kind: string().min(1),
13538
+ recordKey: string().min(1),
13539
+ value: unknown(),
13540
+ precondition: PreconditionSchema
13541
+ }),
13542
+ object({
13543
+ op: literal("deleteRecord"),
13544
+ kind: string().min(1),
13545
+ recordKey: string().min(1),
13546
+ precondition: PreconditionSchema
13547
+ })
13548
+ ])) });
13549
+ /**
13550
+ * What a `procedure` body reports. `already-done` means the rerun observed the
13551
+ * work finished (a crash between the work and the ledger row, or a manual fix)
13552
+ * and did none of it again; the ledger records `applied` either way.
13553
+ */
13554
+ var MigrationProcedureResultSchema = object({
13555
+ outcome: _enum(["done", "already-done"]),
13556
+ summary: record(string(), unknown())
13557
+ });
13558
+ /** What an owner hands the engine when one of its migrations ran. */
13559
+ var MigrationOwnerResultSchema = discriminatedUnion("kind", [
13560
+ object({
13561
+ kind: literal("plan"),
13562
+ plan: MigrationPlanSchema
13563
+ }),
13564
+ object({
13565
+ kind: literal("procedure"),
13566
+ result: MigrationProcedureResultSchema
13567
+ }),
13568
+ object({
13569
+ kind: literal("failed"),
13570
+ error: string().min(1)
13571
+ })
13572
+ ]);
13573
+ /**
13574
+ * `held`: a restore of a pre-migration snapshot undid this id; it does not rerun
13575
+ * until the operator retries or a new code version ships.
13576
+ * `skipped-newer`: the database knows an id this code does not.
13577
+ */
13578
+ var MigrationStatusSchema = _enum([
13579
+ "applied",
13580
+ "failed",
13581
+ "blocked",
13582
+ "running",
13583
+ "held",
13584
+ "skipped-newer"
13585
+ ]);
13586
+ var MigrationLedgerEntrySchema = object({
13587
+ id: string(),
13588
+ database: DataStoreDatabaseSchema,
13589
+ owner: string(),
13590
+ level: MigrationLevelSchema,
13591
+ seq: number().int().nonnegative(),
13592
+ status: MigrationStatusSchema,
13593
+ attempts: number().int().nonnegative(),
13594
+ appliedAt: number().nullable(),
13595
+ codeVersion: string().nullable(),
13596
+ /** What the apply changed: rows per table, as the engine counted them. */
13597
+ planSummary: record(string(), unknown()).nullable(),
13598
+ progress: record(string(), unknown()).nullable(),
13599
+ error: string().nullable(),
13600
+ /** Why it is `blocked` or `held` (`dependency-failed`, `snapshot-refused`, …). */
13601
+ reason: string().nullable(),
13602
+ nodeId: string().nullable(),
13603
+ bootId: string().nullable()
13604
+ });
13605
+ /** A catalogued migration that is not applied, with where it stands. */
13606
+ var MigrationPendingEntrySchema = object({
13607
+ meta: MigrationMetaSchema,
13608
+ database: DataStoreDatabaseSchema,
13609
+ state: _enum([
13610
+ "pending",
13611
+ "failed",
13612
+ "blocked",
13613
+ "held"
13614
+ ]),
13615
+ reason: string().nullable()
13616
+ });
13617
+ /** Rows per table before and after a dry run, and what it would have changed. */
13618
+ var MigrationDryRunResultSchema = object({
13619
+ id: string(),
13620
+ ok: boolean(),
13621
+ error: string().nullable(),
13622
+ rowsChanged: number().int().nonnegative(),
13623
+ tables: array(object({
13624
+ table: string(),
13625
+ before: number().int().nonnegative(),
13626
+ after: number().int().nonnegative()
13627
+ }))
13628
+ });
13629
+ /** One of an owner's migrations, as the engine tells the owner to treat it this boot. */
13630
+ var MigrationOwnerStepSchema = object({
13631
+ id: MigrationIdSchema,
13632
+ action: _enum(["run", "skip"]),
13633
+ /** The ledger state the engine found: `pending` has no row yet. */
13634
+ state: union([literal("pending"), MigrationStatusSchema])
13635
+ });
13636
+ /**
12834
13637
  * The trainer contract — what the hub hands the cloud trainer and what it
12835
13638
  * gets back (cloud training, D805+).
12836
13639
  *
@@ -16233,6 +17036,41 @@ var ACTUATOR_CAP_VOCABULARY = {
16233
17036
  icon: "droplets"
16234
17037
  }]
16235
17038
  },
17039
+ "power-source": {
17040
+ scope: "device",
17041
+ noun: t("Power source", "Alimentazione"),
17042
+ actions: [
17043
+ {
17044
+ id: "powerSource.on",
17045
+ method: "powerOn",
17046
+ label: t("Power on", "Dai corrente"),
17047
+ phrase: t("Power on {device}", "Dai corrente a {device}"),
17048
+ icon: "power"
17049
+ },
17050
+ {
17051
+ id: "powerSource.off",
17052
+ method: "powerOff",
17053
+ label: t("Power off", "Togli corrente"),
17054
+ phrase: t("Cut the power of {device}", "Togli la corrente a {device}"),
17055
+ icon: "power-off"
17056
+ },
17057
+ {
17058
+ id: "powerSource.forceRestart",
17059
+ method: "forceRestart",
17060
+ label: t("Force restart", "Riavvio forzato"),
17061
+ phrase: t("Force restart {device} by cutting its power", "Riavvia {device} togliendo la corrente"),
17062
+ icon: "refresh-cw"
17063
+ }
17064
+ ],
17065
+ state: [{
17066
+ kind: "boolean",
17067
+ field: "switchOn",
17068
+ label: t("Power switch on", "Presa accesa"),
17069
+ whenTrue: t("When the power of {device} comes on", "Quando arriva la corrente a {device}"),
17070
+ whenFalse: t("When the power of {device} is cut", "Quando manca la corrente a {device}"),
17071
+ icon: "power"
17072
+ }]
17073
+ },
16236
17074
  "lock-control": {
16237
17075
  scope: "device",
16238
17076
  noun: t("Lock", "Serratura"),
@@ -16708,7 +17546,7 @@ var ACTUATOR_CAP_VOCABULARY = {
16708
17546
  method: "setDirection",
16709
17547
  label: t("Spin forward", "Rotazione avanti"),
16710
17548
  phrase: t("Spin {device} forward", "Fai girare {device} in avanti"),
16711
- icon: "rotate-cw",
17549
+ icon: "refresh-cw",
16712
17550
  effect: {
16713
17551
  field: "direction",
16714
17552
  value: "forward"
@@ -19786,6 +20624,7 @@ var SYSTEM_CAP_VOCABULARY = {
19786
20624
  "login-method": system(IDENTITY),
19787
20625
  "mesh-network": system(NETWORK),
19788
20626
  "metrics-provider": system(CLUSTER),
20627
+ migrations: system(INTERNAL),
19789
20628
  "model-convert": system(ML),
19790
20629
  "model-distributor": system(ML),
19791
20630
  "mqtt-broker": system(NETWORK),
@@ -39988,7 +40827,8 @@ function normalizeUnit(raw) {
39988
40827
  if (lower === "dbm") return "dBm";
39989
40828
  if (lower === "dbfs") return "dBFS";
39990
40829
  if (lower === "inhg") return "inHg";
39991
- if (lower === "mm/h") return "mm/h";
40830
+ if (lower === "mm/h" || lower === "mm/hr") return "mm/h";
40831
+ if (lower === "in/h" || lower === "in/hr") return "in/h";
39992
40832
  if (lower === "w/m²" || lower === "w/m2") return "W/m²";
39993
40833
  if (lower === "°" || lower === "deg" || lower === "degree" || lower === "degrees") return "°";
39994
40834
  if (lower === "kbps") return "kbps";
@@ -42207,7 +43047,7 @@ method(object({}), array(TargetKindSchema)), method(object({}), array(TargetSche
42207
43047
  * rebuilds without manual reload.
42208
43048
  *
42209
43049
  * The hub-local builtin `addon-pages-aggregator` (see
42210
- * `@camstack/system/builtins/addon-pages-aggregator`) registers the
43050
+ * `@camstack/server/system/builtins/addon-pages-aggregator`) registers the
42211
43051
  * provider. Splitting the public aggregator from the raw collection
42212
43052
  * keeps both ends in codegen — there's no hand-written
42213
43053
  * `addon-pages.router.ts` wrapper anymore.
@@ -42507,7 +43347,7 @@ method(_void(), array(WidgetMetadataSchema).readonly(), { auth: "admin" });
42507
43347
  * manual reload — same scheme used by `addon-pages`.
42508
43348
  *
42509
43349
  * The hub-local builtin `addon-widgets-aggregator` (see
42510
- * `@camstack/system/builtins/addon-widgets-aggregator`) registers the
43350
+ * `@camstack/server/system/builtins/addon-widgets-aggregator`) registers the
42511
43351
  * provider. Splitting the public aggregator from the raw collection
42512
43352
  * keeps both ends in codegen — there's no hand-written wrapper.
42513
43353
  */
@@ -42868,329 +43708,6 @@ method(object({
42868
43708
  password: string()
42869
43709
  }), AuthResultSchema.nullable(), { kind: "mutation" }), method(object({ state: string() }), string()), method(record(string(), string()), AuthResultSchema, { kind: "mutation" }), method(object({ token: string() }), AuthResultSchema.nullable());
42870
43710
  /**
42871
- * Query filter for settings-store collections.
42872
- */
42873
- var QueryFilterSchema = object({
42874
- where: record(string(), unknown()).optional(),
42875
- whereIn: record(string(), array(unknown())).optional(),
42876
- whereBetween: record(string(), tuple([unknown(), unknown()])).optional(),
42877
- /** NULL-safe exclusion: matches rows whose field is NULL OR != the value. */
42878
- whereNot: record(string(), unknown()).optional(),
42879
- /** NULL-safe exclusion of a SET — `whereNot` for more than one value. An
42880
- * empty list excludes nothing. See `QueryFilter.whereNotIn`. */
42881
- whereNotIn: record(string(), array(unknown())).optional(),
42882
- orderBy: object({
42883
- field: string(),
42884
- direction: _enum(["asc", "desc"])
42885
- }).optional(),
42886
- limit: number().optional(),
42887
- offset: number().optional()
42888
- });
42889
- /**
42890
- * The predicate half of a filter, for BULK MUTATIONS.
42891
- *
42892
- * Deliberately not `QueryFilterSchema`: `orderBy` / `limit` / `offset` have no
42893
- * meaning for a statement that rewrites a set, and accepting them would invite
42894
- * a caller to believe `limit` bounds the damage. Every field is optional here
42895
- * only so the shape stays composable — the implementation REJECTS a filter
42896
- * that compiles to no predicate, because that is the whole collection.
42897
- */
42898
- var MutationFilterSchema = object({
42899
- where: record(string(), unknown()).optional(),
42900
- whereIn: record(string(), array(unknown())).optional(),
42901
- whereBetween: record(string(), tuple([unknown(), unknown()])).optional(),
42902
- whereNot: record(string(), unknown()).optional(),
42903
- whereNotIn: record(string(), array(unknown())).optional()
42904
- });
42905
- /**
42906
- * One scalar an {@link settingsStoreCapability.methods.aggregate} call asks for.
42907
- *
42908
- * `as` names the slot in the result, so the SAME column may be asked twice with
42909
- * two operations (`MIN(startMs)` and `MAX(startMs)` in one round trip) — which
42910
- * a `Record<column, op>` shape could not express.
42911
- */
42912
- var AggregateFieldSchema = object({
42913
- /** Result key. */
42914
- as: string().min(1),
42915
- /** Column to aggregate. Must be a real column of a declared collection. */
42916
- field: string().min(1),
42917
- op: _enum([
42918
- "sum",
42919
- "min",
42920
- "max"
42921
- ])
42922
- });
42923
- /**
42924
- * `COUNT(*)` plus one number per requested field.
42925
- *
42926
- * `null` means NO ROW MATCHED, never zero: a `SUM` over an empty set and a sum
42927
- * that really is 0 are different facts, and an accounting caller that renders
42928
- * "0 bytes, oldest = 0" for "nothing here" reports a lie about a disk.
42929
- */
42930
- var AggregateResultSchema = object({
42931
- count: number().int(),
42932
- values: record(string(), number().nullable())
42933
- });
42934
- /** A single stored record: `{ id, data }`. */
42935
- var SettingsRecordSchema = object({
42936
- id: string(),
42937
- data: record(string(), unknown())
42938
- });
42939
- /**
42940
- * One record of a BULK insert — {@link SettingsRecordSchema} with the id made
42941
- * optional.
42942
- *
42943
- * A separate schema rather than loosening the shared one: every other method
42944
- * on this cap addresses a row BY its id, and making that field optional
42945
- * everywhere would turn a forgotten key into a silently generated one on
42946
- * `update` and `delete` as well.
42947
- */
42948
- var BulkRecordSchema = object({
42949
- id: string().optional(),
42950
- data: record(string(), unknown())
42951
- });
42952
- /**
42953
- * The database FILES one `sqlite-settings` engine process owns (D851).
42954
- * Closed on purpose: a file is a thing the engine opens, backs up and gives a
42955
- * serving thread, so a new one is a code change, never a string a caller
42956
- * invents. `settings` is `camstack.db` and the default for every collection
42957
- * that names no file.
42958
- */
42959
- var DataStoreDatabaseSchema = _enum([
42960
- "settings",
42961
- "events",
42962
- "history"
42963
- ]);
42964
- DataStoreDatabaseSchema.options;
42965
- /**
42966
- * One write inside a {@link settingsStoreCapability.methods.transact} call.
42967
- * The three that a "replace this set, bump that revision" write needs, plus
42968
- * the batched upsert a rollup writer needs.
42969
- */
42970
- var TransactOpSchema = discriminatedUnion("op", [
42971
- object({
42972
- op: literal("deleteWhere"),
42973
- collection: string(),
42974
- filter: MutationFilterSchema
42975
- }),
42976
- object({
42977
- op: literal("insertMany"),
42978
- collection: string(),
42979
- records: array(BulkRecordSchema).readonly()
42980
- }),
42981
- object({
42982
- op: literal("update"),
42983
- collection: string(),
42984
- id: string(),
42985
- data: record(string(), unknown())
42986
- }),
42987
- object({
42988
- op: literal("upsertMany"),
42989
- collection: string(),
42990
- conflict: array(string()).readonly(),
42991
- records: array(BulkRecordSchema).readonly()
42992
- })
42993
- ]);
42994
- /**
42995
- * The precondition of a transaction: the row `key` of `collection` must hold
42996
- * `equals` in `column`, read INSIDE the transaction.
42997
- */
42998
- var TransactGuardSchema = object({
42999
- collection: string(),
43000
- key: string(),
43001
- column: string(),
43002
- equals: unknown()
43003
- });
43004
- /**
43005
- * Column declaration for a structured (SQL-backed) collection.
43006
- *
43007
- * Logical types — the backend translates each to the matching SQLite
43008
- * storage class and handles per-type marshaling:
43009
- * - `TEXT` / `INTEGER` / `REAL` — native SQLite types, pass-through
43010
- * - `JSON` — TEXT under the hood; serialised on write, parsed on read
43011
- * - `BOOLEAN` — INTEGER 0/1 under the hood; coerced both directions
43012
- */
43013
- var CollectionColumnSchema = object({
43014
- name: string(),
43015
- type: _enum([
43016
- "TEXT",
43017
- "INTEGER",
43018
- "REAL",
43019
- "JSON",
43020
- "BOOLEAN"
43021
- ]),
43022
- primaryKey: boolean().optional(),
43023
- notNull: boolean().optional(),
43024
- unique: boolean().optional(),
43025
- /**
43026
- * Column DEFAULT. Required for B2: the four `ensureTable` consumers declare
43027
- * columns like `enabled INTEGER NOT NULL DEFAULT 1`, and without this the
43028
- * collection surface simply cannot express their existing tables — which is
43029
- * why they were still on the uncapped `table*` API.
43030
- */
43031
- defaultValue: union([
43032
- string(),
43033
- number(),
43034
- boolean()
43035
- ]).optional()
43036
- });
43037
- var CollectionIndexSchema = object({
43038
- name: string(),
43039
- columns: array(string()).readonly(),
43040
- unique: boolean().optional()
43041
- });
43042
- /**
43043
- * The input of `declareCollection` — ONE schema for the door and for the
43044
- * engine behind it, so the two surfaces cannot drift (the dispatcher forwards
43045
- * the payload untouched).
43046
- *
43047
- * - `database` — the file the collection lives in (D851). Absent means
43048
- * `settings`. A declaration naming a file other than the one the table is
43049
- * already in is REFUSED by the engine, never moved.
43050
- * - `primaryKey` — a COMPOSITE key, as column names. A single name is the same
43051
- * as `primaryKey: true` on that column. A composite-keyed collection has no
43052
- * `id`: it is written with `insertMany` / `upsertMany` / `transact` and read
43053
- * with `query` / `count` / `aggregate` / `deleteWhere`; the calls that
43054
- * address one row by id are refused.
43055
- * - `withoutRowid` — emit `WITHOUT ROWID`. Requires a primary key.
43056
- */
43057
- var DeclareCollectionInputSchema = object({
43058
- namespace: string().optional(),
43059
- collection: string(),
43060
- columns: array(CollectionColumnSchema).readonly(),
43061
- indexes: array(CollectionIndexSchema).readonly().optional(),
43062
- database: DataStoreDatabaseSchema.optional(),
43063
- primaryKey: array(string()).readonly().optional(),
43064
- withoutRowid: boolean().optional()
43065
- });
43066
- method(object({
43067
- namespace: string().optional(),
43068
- collection: string(),
43069
- key: string()
43070
- }), unknown(), { auth: "admin" }), method(object({
43071
- namespace: string().optional(),
43072
- collection: string(),
43073
- key: string(),
43074
- value: unknown()
43075
- }), _void(), {
43076
- kind: "mutation",
43077
- auth: "admin"
43078
- }), method(object({
43079
- namespace: string().optional(),
43080
- collection: string(),
43081
- filter: QueryFilterSchema.optional(),
43082
- columns: array(string()).readonly().optional()
43083
- }), array(SettingsRecordSchema).readonly(), { auth: "admin" }), method(object({
43084
- namespace: string().optional(),
43085
- collection: string(),
43086
- /** Declared columns identifying the slot; must be covered by a UNIQUE
43087
- * index on the collection. Never empty. */
43088
- conflict: array(string()).readonly(),
43089
- record: BulkRecordSchema
43090
- }), object({
43091
- /** The id the row HAS: assigned by SQLite on an insert, or the existing
43092
- * row's own id on an update. */
43093
- id: union([string(), number()]) }), {
43094
- kind: "mutation",
43095
- auth: "admin"
43096
- }), method(object({
43097
- namespace: string().optional(),
43098
- collection: string(),
43099
- conflict: array(string()).readonly(),
43100
- records: array(BulkRecordSchema).readonly()
43101
- }), object({ upserted: number().int() }), {
43102
- kind: "mutation",
43103
- auth: "admin"
43104
- }), method(object({
43105
- namespace: string().optional(),
43106
- collection: string(),
43107
- record: object({
43108
- id: string().optional(),
43109
- data: record(string(), unknown())
43110
- })
43111
- }), object({
43112
- /**
43113
- * The id the row ACTUALLY got (D473): the one supplied, the UUID minted
43114
- * for an absent one, or the ROWID SQLite assigned on an `INTEGER`
43115
- * primary key — which is the only place an auto key is knowable.
43116
- */
43117
- id: union([string(), number()]) }), {
43118
- kind: "mutation",
43119
- auth: "admin"
43120
- }), method(object({
43121
- namespace: string().optional(),
43122
- collection: string(),
43123
- records: array(BulkRecordSchema).readonly()
43124
- }), object({ inserted: number().int() }), {
43125
- kind: "mutation",
43126
- auth: "admin"
43127
- }), method(object({
43128
- namespace: string().optional(),
43129
- guard: TransactGuardSchema.optional(),
43130
- ops: array(TransactOpSchema).readonly()
43131
- }), object({
43132
- applied: boolean(),
43133
- current: unknown().optional()
43134
- }), {
43135
- kind: "mutation",
43136
- auth: "admin"
43137
- }), method(object({
43138
- namespace: string().optional(),
43139
- collection: string(),
43140
- id: string(),
43141
- data: record(string(), unknown())
43142
- }), _void(), {
43143
- kind: "mutation",
43144
- auth: "admin"
43145
- }), method(object({
43146
- namespace: string().optional(),
43147
- collection: string(),
43148
- key: string()
43149
- }), _void(), {
43150
- kind: "mutation",
43151
- auth: "admin"
43152
- }), method(object({
43153
- namespace: string().optional(),
43154
- collection: string(),
43155
- filter: MutationFilterSchema
43156
- }), object({ deleted: number().int() }), {
43157
- kind: "mutation",
43158
- auth: "admin"
43159
- }), method(object({
43160
- namespace: string().optional(),
43161
- collection: string(),
43162
- filter: MutationFilterSchema,
43163
- data: record(string(), unknown())
43164
- }), object({ updated: number().int() }), {
43165
- kind: "mutation",
43166
- auth: "admin"
43167
- }), method(object({
43168
- namespace: string().optional(),
43169
- collection: string(),
43170
- filter: QueryFilterSchema.optional()
43171
- }), number(), { auth: "admin" }), method(object({
43172
- namespace: string().optional(),
43173
- collection: string(),
43174
- fields: array(AggregateFieldSchema).readonly(),
43175
- filter: QueryFilterSchema.optional()
43176
- }), AggregateResultSchema, { auth: "admin" }), method(object({
43177
- namespace: string().optional(),
43178
- collection: string(),
43179
- field: string(),
43180
- bucketSize: number().int().positive(),
43181
- origin: number().int(),
43182
- filter: QueryFilterSchema.optional()
43183
- }), array(object({
43184
- bucket: number().int(),
43185
- count: number().int()
43186
- })).readonly(), { auth: "admin" }), method(object({
43187
- namespace: string().optional(),
43188
- collection: string()
43189
- }), boolean(), { auth: "admin" }), method(DeclareCollectionInputSchema, _void(), {
43190
- kind: "mutation",
43191
- auth: "admin"
43192
- });
43193
- /**
43194
43711
  * How long one snapshot may take end to end: the pinned copy of the whole
43195
43712
  * database plus `PRAGMA integrity_check` over the copy, on the hub's slowest
43196
43713
  * disk. ~800 MB at a pessimistic 10 MB/s is under 3 minutes; the bound is
@@ -45104,7 +45621,7 @@ method(object({
45104
45621
  kind: "mutation",
45105
45622
  auth: "admin"
45106
45623
  }), method(object({ blockIds: array(string()) }), object({ states: array(CompositionBlockStateSchema) }), { auth: "admin" });
45107
- 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;
45624
+ 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;
45108
45625
  var NUMBER = {
45109
45626
  kind: "fixed",
45110
45627
  result: ["number"]
@@ -46115,6 +46632,57 @@ id: union([string(), number()]) }), {
46115
46632
  kind: "mutation",
46116
46633
  auth: "admin"
46117
46634
  });
46635
+ 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({
46636
+ id: string().min(1),
46637
+ error: string().min(1)
46638
+ }), MigrationLedgerEntrySchema, {
46639
+ kind: "mutation",
46640
+ auth: "admin"
46641
+ }), method(object({ id: string().min(1) }), object({ entries: array(MigrationLedgerEntrySchema) }), {
46642
+ kind: "mutation",
46643
+ auth: "admin"
46644
+ }), method(object({
46645
+ database: DataStoreDatabaseSchema,
46646
+ snapshotId: string().min(1)
46647
+ }), object({
46648
+ scheduled: boolean(),
46649
+ held: array(string()),
46650
+ restartRequired: boolean()
46651
+ }), {
46652
+ kind: "mutation",
46653
+ auth: "admin"
46654
+ }), method(object({
46655
+ owner: string().min(1),
46656
+ metas: array(MigrationMetaSchema)
46657
+ }), object({
46658
+ bootId: string(),
46659
+ snapshot: _enum([
46660
+ "none",
46661
+ "written",
46662
+ "refused"
46663
+ ]),
46664
+ steps: array(MigrationOwnerStepSchema)
46665
+ }), {
46666
+ kind: "mutation",
46667
+ auth: "admin"
46668
+ }), method(object({ id: string().min(1) }), object({
46669
+ proceed: boolean(),
46670
+ state: union([literal("pending"), MigrationStatusSchema]),
46671
+ reason: string().nullable()
46672
+ }), {
46673
+ kind: "mutation",
46674
+ auth: "admin"
46675
+ }), method(object({
46676
+ id: string().min(1),
46677
+ result: MigrationOwnerResultSchema
46678
+ }), object({
46679
+ status: MigrationStatusSchema,
46680
+ error: string().nullable(),
46681
+ rowsChanged: number().int().nonnegative()
46682
+ }), {
46683
+ kind: "mutation",
46684
+ auth: "admin"
46685
+ });
46118
46686
  /**
46119
46687
  * records - an addon's own facts and documents (D881): ledgers, audit rows,
46120
46688
  * config documents, mirrors. The third storage API next to `ctx.settings`
@@ -60979,7 +61547,18 @@ var ServerUpdateActionResultSchema = object({
60979
61547
  * store (D877), present when the pre-flight refused the update
60980
61548
  * (`reason: 'addon-would-refuse'`). Optional for version skew.
60981
61549
  */
60982
- blockers: array(UpdateBlockerSchema).optional()
61550
+ blockers: array(UpdateBlockerSchema).optional(),
61551
+ /**
61552
+ * The minimums the target server declares and this node does not meet
61553
+ * (D989), present when the update was refused with
61554
+ * `reason: 'requirements-unmet'`. Each names the versions on both sides.
61555
+ */
61556
+ unmet: array(UnmetRequirementSchema).optional(),
61557
+ /**
61558
+ * The addons this node has below the minimum the staged server declares —
61559
+ * the ones to update WITH the server. Present on an accepted update.
61560
+ */
61561
+ requiredAddonUpdates: array(RequiredAddonUpdateSchema).optional()
60983
61562
  });
60984
61563
  var LaunchOverridesSchema = object({
60985
61564
  /** The persisted heap ceiling in MB, or `null` when none is set. */
@@ -63523,7 +64102,9 @@ var PackageUpdateSchema = object({
63523
64102
  latestVersion: string(),
63524
64103
  category: _enum(["addon", "core"]),
63525
64104
  requiresRestart: boolean(),
63526
- isSystem: boolean()
64105
+ isSystem: boolean(),
64106
+ /** Held: what the newer version needs and this node does not meet (D989). */
64107
+ blockedBy: array(UnmetRequirementSchema).readonly().optional()
63527
64108
  });
63528
64109
  var PackageVersionInfoSchema = object({
63529
64110
  version: string(),
@@ -64979,19 +65560,19 @@ var RemoteSetConfigResultSchema = discriminatedUnion("status", [object({ status:
64979
65560
  reason: _enum(["unknown-template", "not-a-media-player"]),
64980
65561
  detail: string()
64981
65562
  })]);
64982
- var deviceInput = object({ deviceId: number().int().nonnegative() });
65563
+ var deviceInput$1 = object({ deviceId: number().int().nonnegative() });
64983
65564
  DeviceType.MediaPlayer;
64984
- method(deviceInput, RemoteConfigSchema, { auth: "admin" }), method(deviceInput.extend({ config: RemoteConfigSchema }), RemoteSetConfigResultSchema, {
65565
+ method(deviceInput$1, RemoteConfigSchema, { auth: "admin" }), method(deviceInput$1.extend({ config: RemoteConfigSchema }), RemoteSetConfigResultSchema, {
64985
65566
  kind: "mutation",
64986
65567
  auth: "admin"
64987
- }), method(deviceInput, RemoteCommandListSchema, { auth: "protected" }), method(deviceInput.extend({
65568
+ }), method(deviceInput$1, RemoteCommandListSchema, { auth: "protected" }), method(deviceInput$1.extend({
64988
65569
  role: RemoteRoleSchema,
64989
65570
  /** Sends in a row (volume steps). */
64990
65571
  repeat: number().int().min(1).max(50).default(1)
64991
65572
  }), RemoteSendResultSchema, {
64992
65573
  kind: "mutation",
64993
65574
  auth: "protected"
64994
- }), method(deviceInput.extend({
65575
+ }), method(deviceInput$1.extend({
64995
65576
  command: RemoteMediaCommandSchema,
64996
65577
  muted: boolean().optional(),
64997
65578
  source: string().optional(),
@@ -68806,7 +69387,7 @@ var lawnMowerControlCapability = {
68806
69387
  * clients — they reverse-connect to the hub. Exposing their interfaces
68807
69388
  * via the same surface would leak internal topology with no upside.
68808
69389
  *
68809
- * Implementation in `@camstack/system/builtins/local-network/`.
69390
+ * Implementation in `@camstack/server/system/builtins/local-network/`.
68810
69391
  */
68811
69392
  /** Coarse classification derived from the interface name + IP range. */
68812
69393
  var InterfaceKindEnum = _enum([
@@ -69303,6 +69884,117 @@ var lockControlCapability = {
69303
69884
  * whether persisting is worth a SQLite commit. */
69304
69885
  volatileStateFields: ["lastChangedAt"]
69305
69886
  };
69887
+ var PowerSourceRefusalReasonSchema = _enum([
69888
+ "no-power-source",
69889
+ "switch-missing",
69890
+ "switch-offline",
69891
+ "switch-unknown",
69892
+ "restart-in-progress",
69893
+ "switch-call-failed"
69894
+ ]);
69895
+ var PowerSourceActionResultSchema = discriminatedUnion("accepted", [object({ accepted: literal(true) }), object({
69896
+ accepted: literal(false),
69897
+ reason: PowerSourceRefusalReasonSchema,
69898
+ /** One line for the operator, naming the switch. */
69899
+ message: string()
69900
+ })]);
69901
+ var PowerSourceInfoSchema = object({
69902
+ /** False when the device has no power source linked; the rest is then empty. */
69903
+ linked: boolean(),
69904
+ switchDeviceId: number().nullable(),
69905
+ switchName: string().nullable(),
69906
+ /** The switch's on/off state, or null when it is unknown. Never false for unknown (D393). */
69907
+ switchOn: boolean().nullable(),
69908
+ switchOnline: boolean().nullable(),
69909
+ restartInProgress: boolean(),
69910
+ offSeconds: number().int()
69911
+ });
69912
+ /** The composed slice: the switch state the link mirrors, plus the stored off time. */
69913
+ var PowerSourceStateSchema = object({
69914
+ /** Null while the switch has not reported: unknown is never `false` (D393). */
69915
+ switchOn: boolean().nullable(),
69916
+ offSeconds: number().int(),
69917
+ lastFetchedAt: number()
69918
+ });
69919
+ var POWER_SOURCE_DEVICE_TYPES = [
69920
+ DeviceType.Gateway,
69921
+ DeviceType.Hub,
69922
+ DeviceType.Camera,
69923
+ DeviceType.Sensor,
69924
+ DeviceType.MediaPlayer,
69925
+ DeviceType.Vacuum,
69926
+ DeviceType.Generic,
69927
+ DeviceType.Washer,
69928
+ DeviceType.Dryer,
69929
+ DeviceType.Dishwasher,
69930
+ DeviceType.PetFountain,
69931
+ DeviceType.Fan,
69932
+ DeviceType.Humidifier,
69933
+ DeviceType.Climate,
69934
+ DeviceType.WaterHeater
69935
+ ];
69936
+ var powerSourceCustomize = {
69937
+ version: 1,
69938
+ targets: [{
69939
+ id: "switch",
69940
+ cardinality: "scalar",
69941
+ vocabulary: {
69942
+ cap: "power-source",
69943
+ stateField: "switchOn"
69944
+ },
69945
+ deviceTypes: POWER_SOURCE_DEVICE_TYPES,
69946
+ apply: "add",
69947
+ path: "switchOn",
69948
+ sources: [{
69949
+ kind: "switch",
69950
+ transforms: [{ kind: "identity" }]
69951
+ }],
69952
+ companions: [{
69953
+ kind: "constant",
69954
+ path: "offSeconds",
69955
+ value: 10,
69956
+ when: "add"
69957
+ }, {
69958
+ kind: "clock",
69959
+ path: "lastFetchedAt",
69960
+ when: "always"
69961
+ }],
69962
+ sourceReport: { status: "assignment-status" }
69963
+ }]
69964
+ };
69965
+ var deviceInput = object({ deviceId: number().int().nonnegative() });
69966
+ var powerSourceCapability = {
69967
+ name: "power-source",
69968
+ scope: "device",
69969
+ mode: "singleton",
69970
+ deviceTypes: POWER_SOURCE_DEVICE_TYPES,
69971
+ methods: {
69972
+ getStatus: systemMethod(deviceInput, PowerSourceInfoSchema, { auth: "admin" }),
69973
+ powerOn: systemMethod(deviceInput, PowerSourceActionResultSchema, {
69974
+ kind: "mutation",
69975
+ auth: "admin"
69976
+ }),
69977
+ powerOff: systemMethod(deviceInput, PowerSourceActionResultSchema, {
69978
+ kind: "mutation",
69979
+ auth: "admin"
69980
+ }),
69981
+ forceRestart: systemMethod(deviceInput, PowerSourceActionResultSchema, {
69982
+ kind: "mutation",
69983
+ auth: "admin"
69984
+ })
69985
+ },
69986
+ runtimeState: PowerSourceStateSchema,
69987
+ customize: powerSourceCustomize,
69988
+ /**
69989
+ * Runtime-state durability: **session** — mirrored from the linked switch on
69990
+ * its next report; a restored value would outlive the reading it names.
69991
+ *
69992
+ * See `RuntimeStateDurability`. Enforced by
69993
+ * `scripts/check-runtime-state-durability.ts`.
69994
+ */
69995
+ durability: "session",
69996
+ history: "none"
69997
+ };
69306
69998
  /**
69307
69999
  * A monotonic total between resets (D944). Energy uses `power-meter.energy`
69308
70000
  * so dashboards keep one series; every other instantaneous reading lands here.
@@ -70583,7 +71275,7 @@ onStatusChanged: { data: object({
70583
71275
  * jitter, and observed/peak bandwidth per device + per client.
70584
71276
  *
70585
71277
  * Implementation lives in the server's `NetworkQualityService` (thin
70586
- * wrapper over the shared `NetworkQualityTracker` from `@camstack/system`).
71278
+ * wrapper over the shared `NetworkQualityTracker` from `@camstack/server/system`).
70587
71279
  * The provider is registered from `trpc.router.ts` against the existing
70588
71280
  * service instance — no addon owns this state.
70589
71281
  *
@@ -72734,7 +73426,7 @@ var ptzAutotrackCapability = {
72734
73426
  * - **Native**: the camera's own firmware autotrack, registered per device by
72735
73427
  * the driver when the probe says it exists (Reolink: yes; Hikvision:
72736
73428
  * model-dependent).
72737
- * - **Wrapper**: the emulated implementation (`@camstack/system`'s `autotrack` builtin),
73429
+ * - **Wrapper**: the emulated implementation (`@camstack/server/system`'s `autotrack` builtin),
72738
73430
  * which follows a subject with `ptz.move` pulses driven by the detection
72739
73431
  * pipeline. `defaultActive: false` — the operator binds it per camera in
72740
73432
  * the bindings UI.
@@ -78043,6 +78735,7 @@ var DEVICE_LOCAL_STATE_CAPS = {
78043
78735
  petFeeder: petFeederCapability,
78044
78736
  petFountain: petFountainCapability,
78045
78737
  powerMeter: powerMeterCapability,
78738
+ powerSource: powerSourceCapability,
78046
78739
  presence: presenceCapability,
78047
78740
  pressureSensor: pressureSensorCapability,
78048
78741
  privacyMask: privacyMaskCapability,
@@ -83807,6 +84500,69 @@ Object.freeze({
83807
84500
  access: "view",
83808
84501
  role: "admin"
83809
84502
  },
84503
+ "migrations.declareOwner": {
84504
+ capName: "migrations",
84505
+ capScope: "system",
84506
+ addonId: null,
84507
+ access: "create",
84508
+ role: "admin"
84509
+ },
84510
+ "migrations.dryRun": {
84511
+ capName: "migrations",
84512
+ capScope: "system",
84513
+ addonId: null,
84514
+ access: "view",
84515
+ role: "admin"
84516
+ },
84517
+ "migrations.finishMigration": {
84518
+ capName: "migrations",
84519
+ capScope: "system",
84520
+ addonId: null,
84521
+ access: "create",
84522
+ role: "admin"
84523
+ },
84524
+ "migrations.listLedger": {
84525
+ capName: "migrations",
84526
+ capScope: "system",
84527
+ addonId: null,
84528
+ access: "view",
84529
+ role: "admin"
84530
+ },
84531
+ "migrations.listPending": {
84532
+ capName: "migrations",
84533
+ capScope: "system",
84534
+ addonId: null,
84535
+ access: "view",
84536
+ role: "admin"
84537
+ },
84538
+ "migrations.markFailed": {
84539
+ capName: "migrations",
84540
+ capScope: "system",
84541
+ addonId: null,
84542
+ access: "create",
84543
+ role: "admin"
84544
+ },
84545
+ "migrations.restorePreMigration": {
84546
+ capName: "migrations",
84547
+ capScope: "system",
84548
+ addonId: null,
84549
+ access: "create",
84550
+ role: "admin"
84551
+ },
84552
+ "migrations.retry": {
84553
+ capName: "migrations",
84554
+ capScope: "system",
84555
+ addonId: null,
84556
+ access: "create",
84557
+ role: "admin"
84558
+ },
84559
+ "migrations.startMigration": {
84560
+ capName: "migrations",
84561
+ capScope: "system",
84562
+ addonId: null,
84563
+ access: "create",
84564
+ role: "admin"
84565
+ },
83810
84566
  "modelConvert.convert": {
83811
84567
  capName: "model-convert",
83812
84568
  capScope: "system",
@@ -86439,6 +87195,34 @@ Object.freeze({
86439
87195
  access: "view",
86440
87196
  role: "viewer"
86441
87197
  },
87198
+ "powerSource.forceRestart": {
87199
+ capName: "power-source",
87200
+ capScope: "device",
87201
+ addonId: null,
87202
+ access: "create",
87203
+ role: "admin"
87204
+ },
87205
+ "powerSource.getStatus": {
87206
+ capName: "power-source",
87207
+ capScope: "device",
87208
+ addonId: null,
87209
+ access: "view",
87210
+ role: "admin"
87211
+ },
87212
+ "powerSource.powerOff": {
87213
+ capName: "power-source",
87214
+ capScope: "device",
87215
+ addonId: null,
87216
+ access: "create",
87217
+ role: "admin"
87218
+ },
87219
+ "powerSource.powerOn": {
87220
+ capName: "power-source",
87221
+ capScope: "device",
87222
+ addonId: null,
87223
+ access: "create",
87224
+ role: "admin"
87225
+ },
86442
87226
  "presence.getStatus": {
86443
87227
  capName: "presence",
86444
87228
  capScope: "device",
@@ -91587,6 +92371,26 @@ Object.freeze({
91587
92371
  form: "single",
91588
92372
  optional: false
91589
92373
  }],
92374
+ "powerSource.forceRestart": [{
92375
+ name: "deviceId",
92376
+ form: "single",
92377
+ optional: false
92378
+ }],
92379
+ "powerSource.getStatus": [{
92380
+ name: "deviceId",
92381
+ form: "single",
92382
+ optional: false
92383
+ }],
92384
+ "powerSource.powerOff": [{
92385
+ name: "deviceId",
92386
+ form: "single",
92387
+ optional: false
92388
+ }],
92389
+ "powerSource.powerOn": [{
92390
+ name: "deviceId",
92391
+ form: "single",
92392
+ optional: false
92393
+ }],
91590
92394
  "presence.getStatus": [{
91591
92395
  name: "deviceId",
91592
92396
  form: "single",
@@ -93797,6 +94601,88 @@ function ingestReading(input) {
93797
94601
  value: converted
93798
94602
  };
93799
94603
  }
94604
+ /**
94605
+ * Closes the gap D49 leaves: an integration deleted while its provider was down never emits an
94606
+ * event the provider sees. An integration id absent from an ANSWERED list is only a suspect; its
94607
+ * devices go (through the provider's own removeDevices) when a SECOND answered read, at least
94608
+ * `minIntervalMs` later, still lacks it. A read that threw changes nothing; reappearing clears.
94609
+ */
94610
+ var OrphanIntegrationSweep = class {
94611
+ #logger;
94612
+ #minIntervalMs;
94613
+ #now;
94614
+ #suspects = /* @__PURE__ */ new Map();
94615
+ constructor(options) {
94616
+ this.#logger = options.logger;
94617
+ this.#minIntervalMs = options.minIntervalMs ?? 6e4;
94618
+ this.#now = options.now ?? Date.now;
94619
+ }
94620
+ get minIntervalMs() {
94621
+ return this.#minIntervalMs;
94622
+ }
94623
+ async sweep(input) {
94624
+ if (input.listedIds === null) return {
94625
+ newSuspects: 0,
94626
+ removed: []
94627
+ };
94628
+ const listed = new Set(input.listedIds);
94629
+ const owners = new Set(input.ownerIds);
94630
+ const now = this.#now();
94631
+ let newSuspects = 0;
94632
+ const removed = [];
94633
+ for (const id of [...this.#suspects.keys()]) if (listed.has(id) || !owners.has(id)) {
94634
+ this.#suspects.delete(id);
94635
+ this.#logger.info("Orphan integration suspect cleared", { meta: {
94636
+ integrationId: id,
94637
+ reason: listed.has(id) ? "listed again" : "owns no devices"
94638
+ } });
94639
+ }
94640
+ for (const id of owners) {
94641
+ if (listed.has(id)) continue;
94642
+ const since = this.#suspects.get(id);
94643
+ if (since === void 0) {
94644
+ this.#suspects.set(id, now);
94645
+ newSuspects += 1;
94646
+ this.#logger.info("Integration owns devices but is absent from the list — suspect", { meta: {
94647
+ integrationId: id,
94648
+ minIntervalMs: this.#minIntervalMs
94649
+ } });
94650
+ continue;
94651
+ }
94652
+ if (now - since < this.#minIntervalMs) continue;
94653
+ try {
94654
+ const count = await input.removeDevices(id);
94655
+ this.#suspects.delete(id);
94656
+ removed.push(id);
94657
+ this.#logger.warn("Orphan integration devices removed — absent from two answered lists", { meta: {
94658
+ integrationId: id,
94659
+ removed: count,
94660
+ firstMissingAt: since
94661
+ } });
94662
+ } catch (err) {
94663
+ this.#logger.warn("Orphan integration device removal failed — kept as suspect", { meta: {
94664
+ integrationId: id,
94665
+ error: errMsg(err)
94666
+ } });
94667
+ }
94668
+ }
94669
+ return {
94670
+ newSuspects,
94671
+ removed
94672
+ };
94673
+ }
94674
+ };
94675
+ /** The integration ids that own top-level devices of one provider (a config that cannot be read is skipped). */
94676
+ async function collectOwnerIntegrationIds(input) {
94677
+ const ids = /* @__PURE__ */ new Set();
94678
+ for (const d of input.reg.getAllForAddon(input.addonId)) {
94679
+ if (d.parentDeviceId !== null) continue;
94680
+ const cfg = await input.devices.loadConfig(d.id).catch(() => ({}));
94681
+ const owner = input.ownerOf(cfg);
94682
+ if (owner !== null) ids.add(owner);
94683
+ }
94684
+ return [...ids];
94685
+ }
93800
94686
  String.raw`(?<=^|["'(\s>,\[{])`;
93801
94687
  var MB = 1024 * 1024;
93802
94688
  1024 * MB, 3072 * MB;
@@ -95303,6 +96189,97 @@ var EnpalClientRegistry = class {
95303
96189
  }
95304
96190
  };
95305
96191
  //#endregion
96192
+ //#region src/enpal-reconciler.ts
96193
+ /**
96194
+ * Keeps one live client per enabled integration. Devices are removed ONLY on an explicit
96195
+ * lifecycle event naming the integration (D49): a list that failed, an empty list or an
96196
+ * unreadable connection changes nothing about devices and is logged, never read as "gone".
96197
+ */
96198
+ var EnpalReconciler = class {
96199
+ #deps;
96200
+ #sweep;
96201
+ #rerun = null;
96202
+ constructor(deps) {
96203
+ this.#deps = deps;
96204
+ this.#sweep = new OrphanIntegrationSweep({
96205
+ logger: deps.logger,
96206
+ ...deps.sweepOptions
96207
+ });
96208
+ }
96209
+ /** Upsert a client per usable enabled integration; drop the CLIENT (never devices) of one the list answered is gone. */
96210
+ async reconcile() {
96211
+ const { logger, clients } = this.#deps;
96212
+ let all;
96213
+ try {
96214
+ all = await this.#deps.listIntegrations();
96215
+ } catch (err) {
96216
+ logger.warn("Enpal integration reconcile skipped — the integration list did not answer", { meta: { error: errMsg(err) } });
96217
+ return;
96218
+ }
96219
+ const enabled = all.filter((i) => i.addonId === this.#deps.addonId && i.enabled);
96220
+ for (const integration of enabled) try {
96221
+ if (!await this.#deps.applySettings(integration)) logger.warn("Enpal integration has no usable connection — left as it is", { meta: { integrationId: integration.id } });
96222
+ } catch (err) {
96223
+ logger.warn("Enpal reconcile failed for integration — left as it is", { meta: {
96224
+ integrationId: integration.id,
96225
+ error: errMsg(err)
96226
+ } });
96227
+ }
96228
+ const enabledIds = new Set(enabled.map((i) => i.id));
96229
+ for (const id of clients.list()) if (!enabledIds.has(id)) await clients.remove(id);
96230
+ await this.#sweepOrphans(all.map((i) => i.id));
96231
+ }
96232
+ /** Stop the scheduled second read (addon stop). */
96233
+ dispose() {
96234
+ if (this.#rerun !== null) clearTimeout(this.#rerun);
96235
+ this.#rerun = null;
96236
+ }
96237
+ /**
96238
+ * An integration deleted while this addon was down emits nothing it sees: its id owns devices
96239
+ * yet is absent from the ANSWERED list. Removal needs a second answered read later, so one
96240
+ * re-run is scheduled when a suspect appears (bounded: one timer, no retry loop).
96241
+ */
96242
+ async #sweepOrphans(listedIds) {
96243
+ const { ownerIntegrationIds, removeDevices, logger } = this.#deps;
96244
+ if (ownerIntegrationIds === void 0) return;
96245
+ try {
96246
+ const ownerIds = await ownerIntegrationIds();
96247
+ if ((await this.#sweep.sweep({
96248
+ ownerIds,
96249
+ listedIds,
96250
+ removeDevices
96251
+ })).newSuspects > 0 && this.#rerun === null) {
96252
+ this.#rerun = setTimeout(() => {
96253
+ this.#rerun = null;
96254
+ this.reconcile();
96255
+ }, this.#sweep.minIntervalMs + 1e3);
96256
+ this.#rerun.unref();
96257
+ }
96258
+ } catch (err) {
96259
+ logger.warn("Orphan integration sweep failed — devices left as they are", { meta: { error: errMsg(err) } });
96260
+ }
96261
+ }
96262
+ /** An explicit lifecycle event: only `deleted` / `disabled` of a named integration remove devices. */
96263
+ async onLifecycle(kind, integrationId) {
96264
+ await this.reconcile();
96265
+ if (kind === "enabled" || integrationId === null) return;
96266
+ try {
96267
+ const removed = await this.#deps.removeDevices(integrationId);
96268
+ this.#deps.logger.info("Enpal integration devices removed", { meta: {
96269
+ integrationId,
96270
+ kind,
96271
+ removed
96272
+ } });
96273
+ } catch (err) {
96274
+ this.#deps.logger.warn("Enpal integration device removal failed", { meta: {
96275
+ integrationId,
96276
+ kind,
96277
+ error: errMsg(err)
96278
+ } });
96279
+ }
96280
+ }
96281
+ };
96282
+ //#endregion
95306
96283
  //#region src/enpal-integration-device-cascade.ts
95307
96284
  /**
95308
96285
  * Remove every equipment Container of an integration that was deleted or disabled; the
@@ -95426,6 +96403,7 @@ var EnpalProviderAddon = class extends BaseDeviceProvider {
95426
96403
  [DeviceType.Container]: EnpalEquipmentDevice,
95427
96404
  [DeviceType.Gateway]: EnpalGatewayDevice
95428
96405
  };
96406
+ integrationReconciler = null;
95429
96407
  clients = null;
95430
96408
  constructor() {
95431
96409
  super({});
@@ -95450,6 +96428,8 @@ var EnpalProviderAddon = class extends BaseDeviceProvider {
95450
96428
  this.ctx.logger.warn("Enpal shutdown error", { meta: { error: errMsg(err) } });
95451
96429
  }
95452
96430
  this.clients = null;
96431
+ this.integrationReconciler?.dispose();
96432
+ this.integrationReconciler = null;
95453
96433
  enpalConnections.clear();
95454
96434
  await super.onShutdown();
95455
96435
  }
@@ -95466,51 +96446,63 @@ var EnpalProviderAddon = class extends BaseDeviceProvider {
95466
96446
  return `enpal:${integrationId}:${equipment}`;
95467
96447
  }
95468
96448
  /**
95469
- * Rebuild the pollers from the live integration list: upsert one per enabled integration
95470
- * with a host, drop the ones that vanished and cascade-remove their devices. Idempotent;
95471
- * runs at boot and on every integration lifecycle event.
96449
+ * Upsert one client per enabled integration and drop the client of one the list ANSWERED is
96450
+ * gone. Never removes a device (D49): that follows only an explicit lifecycle event.
96451
+ * Idempotent; runs at boot and on every integration lifecycle event.
95472
96452
  */
95473
- async reconcileIntegrations() {
95474
- const reg = this.requireClients();
95475
- try {
95476
- const mine = (await this.ctx.api.integrations.list.query()).filter((i) => i.addonId === this.ctx.id && i.enabled);
95477
- const surviving = /* @__PURE__ */ new Set();
95478
- for (const integration of mine) try {
96453
+ reconciler() {
96454
+ this.integrationReconciler ??= new EnpalReconciler({
96455
+ addonId: this.ctx.id,
96456
+ logger: this.ctx.logger,
96457
+ clients: this.requireClients(),
96458
+ listIntegrations: () => this.ctx.api.integrations.list.query(),
96459
+ applySettings: async (integration) => {
96460
+ const reg = this.requireClients();
95479
96461
  const read = connectionFromSettings(await this.ctx.api.integrations.getSettings.query({ id: integration.id }));
95480
- if (read === null) {
95481
- this.ctx.logger.warn("Enpal integration has no box host — skipping", { meta: { integrationId: integration.id } });
95482
- continue;
95483
- }
96462
+ if (read === null) return false;
95484
96463
  await reg.upsert(integration.id, integration.name, read.connection, read.tokenLocked);
95485
- surviving.add(integration.id);
95486
- } catch (err) {
95487
- this.ctx.logger.warn("Enpal reconcile failed for integration", { meta: {
95488
- integrationId: integration.id,
95489
- error: errMsg(err)
95490
- } });
95491
- }
95492
- const vanished = reg.list().filter((id) => !surviving.has(id));
95493
- await reg.retain(surviving);
95494
- for (const integrationId of vanished) await this.cascadeRemoveDevicesForIntegration(integrationId);
95495
- } catch (err) {
95496
- this.ctx.logger.warn("Enpal integration reconcile failed", { meta: { error: errMsg(err) } });
95497
- }
96464
+ return true;
96465
+ },
96466
+ removeDevices: (integrationId) => this.cascadeRemoveDevicesForIntegration(integrationId),
96467
+ ownerIntegrationIds: () => this.ownerIntegrationIds()
96468
+ });
96469
+ return this.integrationReconciler;
96470
+ }
96471
+ async reconcileIntegrations() {
96472
+ await this.reconciler().reconcile();
95498
96473
  }
96474
+ /** Lifecycle events: only an explicit Deleted/Disabled naming an integration removes devices. */
95499
96475
  subscribeIntegrationLifecycle() {
95500
- const handler = (event) => {
96476
+ const on = (kind) => (event) => {
95501
96477
  const addonId = event.data["addonId"];
95502
96478
  if (typeof addonId === "string" && addonId !== this.ctx.id) return;
95503
- this.reconcileIntegrations();
96479
+ const integrationId = event.data["integrationId"];
96480
+ this.reconciler().onLifecycle(kind, typeof integrationId === "string" ? integrationId : null);
95504
96481
  };
95505
- this.ctx.eventBus.subscribe({ category: EventCategory.IntegrationEnabled }, handler);
95506
- this.ctx.eventBus.subscribe({ category: EventCategory.IntegrationDisabled }, handler);
95507
- this.ctx.eventBus.subscribe({ category: EventCategory.IntegrationDeleted }, handler);
96482
+ this.ctx.eventBus.subscribe({ category: EventCategory.IntegrationEnabled }, on("enabled"));
96483
+ this.ctx.eventBus.subscribe({ category: EventCategory.IntegrationDisabled }, on("disabled"));
96484
+ this.ctx.eventBus.subscribe({ category: EventCategory.IntegrationDeleted }, on("deleted"));
96485
+ }
96486
+ /** The integrations that own devices here, for the orphan sweep. */
96487
+ async ownerIntegrationIds() {
96488
+ const reg = this.ctx.kernel.deviceRegistry;
96489
+ const devices = this.ctx.kernel.devices;
96490
+ if (!reg || !devices) return [];
96491
+ return await collectOwnerIntegrationIds({
96492
+ reg,
96493
+ devices,
96494
+ addonId: this.addonId,
96495
+ ownerOf: (cfg) => {
96496
+ const owner = cfg["integrationId"];
96497
+ return cfg["system"] === "enpal" && typeof owner === "string" ? owner : null;
96498
+ }
96499
+ });
95508
96500
  }
95509
96501
  async cascadeRemoveDevicesForIntegration(integrationId) {
95510
96502
  const reg = this.ctx.kernel.deviceRegistry;
95511
96503
  const devices = this.ctx.kernel.devices;
95512
- if (!reg || !devices) return;
95513
- await cascadeRemoveDevicesForIntegration({
96504
+ if (!reg || !devices) return 0;
96505
+ return await cascadeRemoveDevicesForIntegration({
95514
96506
  reg,
95515
96507
  devices,
95516
96508
  addonId: this.addonId,