when-does-my-quota-refresh 1.1.0 → 1.2.0
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/accounts-FH6AKZT7.js +27 -0
- package/dist/{auth-XYJBJA2Q.js → auth-GX4ZIG5O.js} +2 -2
- package/dist/auth-GX4ZIG5O.js.map +1 -0
- package/dist/chunk-H7BEO7IF.js +2 -0
- package/dist/chunk-H7BEO7IF.js.map +1 -0
- package/dist/{chunk-ST6OOHQI.js → chunk-VLP22VAR.js} +254 -169
- package/dist/chunk-VLP22VAR.js.map +1 -0
- package/dist/cli/index.js +218 -28
- package/dist/cli/index.js.map +1 -1
- package/dist/sdk/index.d.ts +4 -0
- package/dist/sdk/index.js +399 -175
- package/dist/sdk/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-ST6OOHQI.js.map +0 -1
- /package/dist/{auth-XYJBJA2Q.js.map → accounts-FH6AKZT7.js.map} +0 -0
package/dist/cli/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import "../chunk-H7BEO7IF.js";
|
|
2
3
|
import {
|
|
3
4
|
OAuthClient,
|
|
4
5
|
SourceUnavailableError,
|
|
@@ -15,7 +16,7 @@ import {
|
|
|
15
16
|
saveConfig,
|
|
16
17
|
setActiveAccount,
|
|
17
18
|
setVerbose
|
|
18
|
-
} from "../chunk-
|
|
19
|
+
} from "../chunk-VLP22VAR.js";
|
|
19
20
|
|
|
20
21
|
// src/cli/index.ts
|
|
21
22
|
import { Command } from "commander";
|
|
@@ -570,11 +571,11 @@ var ConnectClient = class {
|
|
|
570
571
|
};
|
|
571
572
|
|
|
572
573
|
// src/sources/local/parser.ts
|
|
573
|
-
function parseQuotaSummary(summary, email) {
|
|
574
|
+
function parseQuotaSummary(summary, email, method = "local") {
|
|
574
575
|
debug("parser", `Parsing quota summary: ${summary.groups.length} groups`);
|
|
575
576
|
return {
|
|
576
577
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
577
|
-
method
|
|
578
|
+
method,
|
|
578
579
|
email,
|
|
579
580
|
groups: summary.groups.map((g) => ({
|
|
580
581
|
displayName: g.displayName,
|
|
@@ -589,11 +590,11 @@ function parseQuotaSummary(summary, email) {
|
|
|
589
590
|
}))
|
|
590
591
|
};
|
|
591
592
|
}
|
|
592
|
-
function parseUserStatus(userStatus) {
|
|
593
|
+
function parseUserStatus(userStatus, method = "local") {
|
|
593
594
|
debug("parser", "Parsing GetUserStatus fallback");
|
|
594
595
|
const snapshot = {
|
|
595
596
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
596
|
-
method
|
|
597
|
+
method,
|
|
597
598
|
email: userStatus.email,
|
|
598
599
|
groups: []
|
|
599
600
|
};
|
|
@@ -667,7 +668,7 @@ function isGeminiModel(modelId) {
|
|
|
667
668
|
}
|
|
668
669
|
function detectAutocomplete(modelId, label) {
|
|
669
670
|
const text = `${modelId} ${label || ""}`.toLowerCase();
|
|
670
|
-
return text.includes("autocomplete")
|
|
671
|
+
return text.includes("autocomplete");
|
|
671
672
|
}
|
|
672
673
|
function clampFraction(value) {
|
|
673
674
|
if (value === void 0 || value === null) return 1;
|
|
@@ -841,10 +842,10 @@ var CliSource = class {
|
|
|
841
842
|
const summary = await client.fetchQuotaSummary();
|
|
842
843
|
if (summary && summary.groups.length > 0) {
|
|
843
844
|
const email = (await client.fetchUserStatus())?.email;
|
|
844
|
-
return parseQuotaSummary(summary, email);
|
|
845
|
+
return parseQuotaSummary(summary, email, "cli");
|
|
845
846
|
}
|
|
846
847
|
const userStatus = await client.fetchUserStatus();
|
|
847
|
-
if (userStatus) return parseUserStatus(userStatus);
|
|
848
|
+
if (userStatus) return parseUserStatus(userStatus, "cli");
|
|
848
849
|
} catch {
|
|
849
850
|
}
|
|
850
851
|
}
|
|
@@ -903,20 +904,25 @@ var OAuthSource = class {
|
|
|
903
904
|
}
|
|
904
905
|
async fetchQuota() {
|
|
905
906
|
debug("oauth", "Fetching quota via Cloud Code API");
|
|
907
|
+
let email;
|
|
908
|
+
try {
|
|
909
|
+
const userInfo = await this.client.fetchUserInfo();
|
|
910
|
+
email = userInfo.email;
|
|
911
|
+
} catch {
|
|
912
|
+
debug("oauth", "Could not fetch user info for email");
|
|
913
|
+
}
|
|
906
914
|
try {
|
|
907
915
|
const summaryRaw = await this.client.fetchQuotaSummary();
|
|
908
916
|
const summary = this.parseRemoteSummary(summaryRaw);
|
|
909
917
|
if (summary && summary.groups.length > 0) {
|
|
910
|
-
const statusRaw2 = await this.client.fetchUserStatus();
|
|
911
|
-
const email = this.extractEmail(statusRaw2);
|
|
912
918
|
debug("oauth", "Got quota summary via OAuth");
|
|
913
|
-
return parseQuotaSummary(summary, email);
|
|
919
|
+
return parseQuotaSummary(summary, email, "oauth");
|
|
914
920
|
}
|
|
915
921
|
const statusRaw = await this.client.fetchUserStatus();
|
|
916
922
|
const status = this.parseRemoteStatus(statusRaw);
|
|
917
923
|
if (status) {
|
|
918
924
|
debug("oauth", "Got user status via OAuth");
|
|
919
|
-
return parseUserStatus(status);
|
|
925
|
+
return parseUserStatus(status, "oauth");
|
|
920
926
|
}
|
|
921
927
|
} catch (err) {
|
|
922
928
|
debug("oauth", `Fetch failed: ${err}`);
|
|
@@ -950,13 +956,12 @@ var OAuthSource = class {
|
|
|
950
956
|
*/
|
|
951
957
|
parseRemoteStatus(raw) {
|
|
952
958
|
if (typeof raw !== "object" || raw === null) return null;
|
|
953
|
-
return { raw };
|
|
954
|
-
}
|
|
955
|
-
extractEmail(raw) {
|
|
956
|
-
if (typeof raw !== "object" || raw === null) return void 0;
|
|
957
959
|
const data = raw;
|
|
958
|
-
const userStatus = data.userStatus;
|
|
959
|
-
return
|
|
960
|
+
const userStatus = data.userStatus || data;
|
|
961
|
+
return {
|
|
962
|
+
email: userStatus.email,
|
|
963
|
+
raw
|
|
964
|
+
};
|
|
960
965
|
}
|
|
961
966
|
};
|
|
962
967
|
|
|
@@ -1230,17 +1235,160 @@ function closeStore() {
|
|
|
1230
1235
|
}
|
|
1231
1236
|
}
|
|
1232
1237
|
|
|
1238
|
+
// src/plugins/builtin/notifications.ts
|
|
1239
|
+
var NotificationPlugin = class {
|
|
1240
|
+
name = "notifications";
|
|
1241
|
+
version = "1.0.0";
|
|
1242
|
+
notifier = null;
|
|
1243
|
+
notifiedLow = /* @__PURE__ */ new Set();
|
|
1244
|
+
async onInit(ctx) {
|
|
1245
|
+
try {
|
|
1246
|
+
const { default: notifier } = await import("node-notifier");
|
|
1247
|
+
this.notifier = notifier;
|
|
1248
|
+
debug("notifications", "Notification plugin initialized");
|
|
1249
|
+
} catch {
|
|
1250
|
+
debug("notifications", "node-notifier not available, notifications disabled");
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
async onQuotaUpdate(snapshot, ctx) {
|
|
1254
|
+
for (const group of snapshot.groups) {
|
|
1255
|
+
for (const bucket of group.buckets) {
|
|
1256
|
+
if (bucket.remainingFraction > 0.8) {
|
|
1257
|
+
this.notifiedLow.delete(`${group.displayName}:${bucket.displayName}`);
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
async onQuotaLow(group, bucket, remaining, ctx) {
|
|
1263
|
+
const key = `${group}:${bucket}`;
|
|
1264
|
+
if (this.notifiedLow.has(key)) return;
|
|
1265
|
+
const pct = Math.round(remaining * 100);
|
|
1266
|
+
const title = "when-does-my-quota-refresh";
|
|
1267
|
+
const message = `\u26A0\uFE0F Low quota: ${group} \u2014 ${bucket} at ${pct}%`;
|
|
1268
|
+
ctx.log(message);
|
|
1269
|
+
this.notify(title, message);
|
|
1270
|
+
this.notifiedLow.add(key);
|
|
1271
|
+
}
|
|
1272
|
+
async onReset(group, nextReset, ctx) {
|
|
1273
|
+
const title = "when-does-my-quota-refresh";
|
|
1274
|
+
const message = `\u2705 ${group} quota has reset!`;
|
|
1275
|
+
ctx.log(message);
|
|
1276
|
+
this.notify(title, message);
|
|
1277
|
+
}
|
|
1278
|
+
notify(title, message) {
|
|
1279
|
+
if (!this.notifier) return;
|
|
1280
|
+
try {
|
|
1281
|
+
this.notifier.notify({ title, message, timeout: 5 });
|
|
1282
|
+
} catch (err) {
|
|
1283
|
+
debug("notifications", `Notification failed: ${err}`);
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
};
|
|
1287
|
+
|
|
1288
|
+
// src/plugins/index.ts
|
|
1289
|
+
var PluginRegistry = class {
|
|
1290
|
+
plugins = [];
|
|
1291
|
+
context;
|
|
1292
|
+
constructor(context) {
|
|
1293
|
+
this.context = {
|
|
1294
|
+
notify: context?.notify || (() => {
|
|
1295
|
+
}),
|
|
1296
|
+
log: context?.log || ((msg) => console.log(msg))
|
|
1297
|
+
};
|
|
1298
|
+
}
|
|
1299
|
+
/**
|
|
1300
|
+
* Register a plugin
|
|
1301
|
+
*/
|
|
1302
|
+
async register(plugin) {
|
|
1303
|
+
this.plugins.push(plugin);
|
|
1304
|
+
if (plugin.onInit) {
|
|
1305
|
+
await plugin.onInit(this.context);
|
|
1306
|
+
}
|
|
1307
|
+
debug("plugins", `Registered: ${plugin.name}@${plugin.version}`);
|
|
1308
|
+
}
|
|
1309
|
+
/**
|
|
1310
|
+
* Load built-in plugins
|
|
1311
|
+
*/
|
|
1312
|
+
async loadBuiltins() {
|
|
1313
|
+
await this.register(new NotificationPlugin());
|
|
1314
|
+
}
|
|
1315
|
+
/**
|
|
1316
|
+
* Notify all plugins of a quota update
|
|
1317
|
+
*/
|
|
1318
|
+
async notifyQuotaUpdate(snapshot) {
|
|
1319
|
+
for (const plugin of this.plugins) {
|
|
1320
|
+
if (plugin.onQuotaUpdate) {
|
|
1321
|
+
try {
|
|
1322
|
+
await plugin.onQuotaUpdate(snapshot, this.context);
|
|
1323
|
+
} catch (err) {
|
|
1324
|
+
debug("plugins", `Error in ${plugin.name}.onQuotaUpdate: ${err}`);
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
for (const group of snapshot.groups) {
|
|
1329
|
+
for (const bucket of group.buckets) {
|
|
1330
|
+
if (bucket.remainingFraction < 0.15) {
|
|
1331
|
+
await this.notifyQuotaLow(group.displayName, bucket.displayName, bucket.remainingFraction);
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
/**
|
|
1337
|
+
* Notify all plugins of low quota
|
|
1338
|
+
*/
|
|
1339
|
+
async notifyQuotaLow(group, bucket, remaining) {
|
|
1340
|
+
for (const plugin of this.plugins) {
|
|
1341
|
+
if (plugin.onQuotaLow) {
|
|
1342
|
+
try {
|
|
1343
|
+
await plugin.onQuotaLow(group, bucket, remaining, this.context);
|
|
1344
|
+
} catch (err) {
|
|
1345
|
+
debug("plugins", `Error in ${plugin.name}.onQuotaLow: ${err}`);
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
/**
|
|
1351
|
+
* Destroy all plugins
|
|
1352
|
+
*/
|
|
1353
|
+
async destroy() {
|
|
1354
|
+
for (const plugin of this.plugins) {
|
|
1355
|
+
if (plugin.onDestroy) {
|
|
1356
|
+
try {
|
|
1357
|
+
await plugin.onDestroy();
|
|
1358
|
+
} catch (err) {
|
|
1359
|
+
debug("plugins", `Error in ${plugin.name}.onDestroy: ${err}`);
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
this.plugins = [];
|
|
1364
|
+
}
|
|
1365
|
+
/**
|
|
1366
|
+
* Get registered plugin names
|
|
1367
|
+
*/
|
|
1368
|
+
getNames() {
|
|
1369
|
+
return this.plugins.map((p) => p.name);
|
|
1370
|
+
}
|
|
1371
|
+
};
|
|
1372
|
+
|
|
1233
1373
|
// src/sdk/quota.ts
|
|
1234
1374
|
var QuotaClient = class {
|
|
1235
1375
|
registry;
|
|
1236
1376
|
cache;
|
|
1377
|
+
plugins;
|
|
1237
1378
|
verbose;
|
|
1238
1379
|
constructor(options) {
|
|
1239
1380
|
this.verbose = options?.verbose || false;
|
|
1240
|
-
|
|
1381
|
+
if (options?.verbose !== void 0) {
|
|
1382
|
+
setVerbose(this.verbose);
|
|
1383
|
+
}
|
|
1241
1384
|
const config = loadConfig();
|
|
1242
1385
|
this.registry = new SourceRegistry();
|
|
1243
1386
|
this.cache = new QuotaCache(options?.cacheTtlMs || config.cacheTtlMs);
|
|
1387
|
+
this.plugins = new PluginRegistry();
|
|
1388
|
+
if (config.notifications) {
|
|
1389
|
+
this.plugins.loadBuiltins().catch(() => {
|
|
1390
|
+
});
|
|
1391
|
+
}
|
|
1244
1392
|
this.registerOAuthSources();
|
|
1245
1393
|
}
|
|
1246
1394
|
/**
|
|
@@ -1251,11 +1399,19 @@ var QuotaClient = class {
|
|
|
1251
1399
|
return this.fetchAllAccounts(options);
|
|
1252
1400
|
}
|
|
1253
1401
|
if (!options?.refresh) {
|
|
1402
|
+
const tokens = getAllAccountTokens();
|
|
1403
|
+
for (const token of tokens) {
|
|
1404
|
+
if (!token.email) continue;
|
|
1405
|
+
const cached = this.cache.get(token.email, options?.source || "auto");
|
|
1406
|
+
if (cached) return cached;
|
|
1407
|
+
}
|
|
1254
1408
|
}
|
|
1255
1409
|
const snapshot = await this.registry.fetchQuota(options?.source);
|
|
1256
|
-
if (snapshot.email) {
|
|
1410
|
+
if (snapshot.email && snapshot.email !== "unknown") {
|
|
1257
1411
|
this.cache.set(snapshot.email, snapshot.method, snapshot);
|
|
1258
1412
|
storeSnapshot(snapshot);
|
|
1413
|
+
this.plugins.notifyQuotaUpdate(snapshot).catch(() => {
|
|
1414
|
+
});
|
|
1259
1415
|
}
|
|
1260
1416
|
return snapshot;
|
|
1261
1417
|
}
|
|
@@ -1659,8 +1815,8 @@ function registerAccountsCommand(program2) {
|
|
|
1659
1815
|
accounts.command("add").description("Login a new Google account").option("--manual", "Manual login for headless/SSH (get URL instead of browser)").action(async (opts) => {
|
|
1660
1816
|
try {
|
|
1661
1817
|
if (opts.manual) {
|
|
1662
|
-
const { getManualLoginUrl
|
|
1663
|
-
const url =
|
|
1818
|
+
const { getManualLoginUrl, completeManualLogin } = await import("../auth-GX4ZIG5O.js");
|
|
1819
|
+
const url = getManualLoginUrl();
|
|
1664
1820
|
console.log(chalk6.bold("\nOpen this URL in your browser:\n"));
|
|
1665
1821
|
console.log(chalk6.cyan(url));
|
|
1666
1822
|
console.log(chalk6.dim("\nPaste the authorization code here:"));
|
|
@@ -1669,7 +1825,7 @@ function registerAccountsCommand(program2) {
|
|
|
1669
1825
|
name: "code",
|
|
1670
1826
|
message: "Authorization code:"
|
|
1671
1827
|
}]);
|
|
1672
|
-
const email = await
|
|
1828
|
+
const email = await completeManualLogin(code);
|
|
1673
1829
|
console.log(chalk6.green(`
|
|
1674
1830
|
\u2713 Logged in as ${email}`));
|
|
1675
1831
|
} else {
|
|
@@ -2086,7 +2242,15 @@ function registerWakeupCommand(program2) {
|
|
|
2086
2242
|
default: true
|
|
2087
2243
|
}
|
|
2088
2244
|
]);
|
|
2089
|
-
config.
|
|
2245
|
+
config.wakeupMode = answers.mode;
|
|
2246
|
+
if (answers.intervalHours) config.wakeupIntervalHours = answers.intervalHours;
|
|
2247
|
+
if (answers.dailyTimes) config.wakeupDailyTimes = answers.dailyTimes;
|
|
2248
|
+
if (!answers.allAccounts) {
|
|
2249
|
+
const { listAccounts: listAccounts2 } = await import("../accounts-FH6AKZT7.js");
|
|
2250
|
+
config.wakeupAccounts = listAccounts2().map((a) => a.email);
|
|
2251
|
+
} else {
|
|
2252
|
+
config.wakeupAccounts = [];
|
|
2253
|
+
}
|
|
2090
2254
|
saveConfig(config);
|
|
2091
2255
|
console.log(chalk11.green("\n \u2713 Configuration saved"));
|
|
2092
2256
|
console.log(chalk11.dim(` Run \`aq wakeup install\` to activate`));
|
|
@@ -2120,8 +2284,11 @@ function registerWakeupCommand(program2) {
|
|
|
2120
2284
|
wakeup.command("status").description("Check wakeup status").action(() => {
|
|
2121
2285
|
const config = loadConfig();
|
|
2122
2286
|
console.log(chalk11.bold("\n Wakeup Status\n"));
|
|
2287
|
+
console.log(` Mode: ${config.wakeupMode}`);
|
|
2123
2288
|
console.log(` Models: ${config.wakeupModels.join(", ")}`);
|
|
2124
2289
|
console.log(` Accounts: ${config.wakeupAccounts.length > 0 ? config.wakeupAccounts.join(", ") : "all"}`);
|
|
2290
|
+
if (config.wakeupMode === "interval") console.log(` Interval: every ${config.wakeupIntervalHours}h`);
|
|
2291
|
+
if (config.wakeupMode === "daily") console.log(` Daily at: ${config.wakeupDailyTimes}`);
|
|
2125
2292
|
if (isWindows()) {
|
|
2126
2293
|
const installed = checkWindowsTask();
|
|
2127
2294
|
console.log(` Scheduler: ${installed ? chalk11.green("Installed") : chalk11.dim("Not installed")}`);
|
|
@@ -2142,12 +2309,32 @@ function registerWakeupCommand(program2) {
|
|
|
2142
2309
|
}
|
|
2143
2310
|
});
|
|
2144
2311
|
}
|
|
2312
|
+
function escapeShellArg(arg) {
|
|
2313
|
+
return arg.replace(/'/g, "'\\''");
|
|
2314
|
+
}
|
|
2145
2315
|
function installCrontab() {
|
|
2146
2316
|
const aqPath = process.argv[1];
|
|
2317
|
+
const safeAqPath = escapeShellArg(aqPath);
|
|
2147
2318
|
const config = loadConfig();
|
|
2148
2319
|
uninstallCrontab();
|
|
2149
|
-
const intervalHours = 6;
|
|
2150
|
-
|
|
2320
|
+
const intervalHours = config.wakeupIntervalHours || 6;
|
|
2321
|
+
let cronLine;
|
|
2322
|
+
if (config.wakeupMode === "daily" && config.wakeupDailyTimes) {
|
|
2323
|
+
const hours = config.wakeupDailyTimes.split(",").map((h) => parseInt(h.trim(), 10)).filter((h) => !isNaN(h));
|
|
2324
|
+
for (const hour of hours) {
|
|
2325
|
+
cronLine = `0 ${hour} * * * "${safeAqPath}" quota --refresh --json > /dev/null 2>&1 ${CRON_MARKER}`;
|
|
2326
|
+
try {
|
|
2327
|
+
const existing = execSync("crontab -l 2>/dev/null", { encoding: "utf-8" }).trim();
|
|
2328
|
+
const newCron = existing ? `${existing}
|
|
2329
|
+
${cronLine}` : cronLine;
|
|
2330
|
+
execSync(`echo '${newCron}' | crontab -`, { encoding: "utf-8" });
|
|
2331
|
+
} catch {
|
|
2332
|
+
execSync(`echo '${cronLine}' | crontab -`, { encoding: "utf-8" });
|
|
2333
|
+
}
|
|
2334
|
+
}
|
|
2335
|
+
return;
|
|
2336
|
+
}
|
|
2337
|
+
cronLine = `0 */${intervalHours} * * * "${safeAqPath}" quota --refresh --json > /dev/null 2>&1 ${CRON_MARKER}`;
|
|
2151
2338
|
try {
|
|
2152
2339
|
const existing = execSync("crontab -l 2>/dev/null", { encoding: "utf-8" }).trim();
|
|
2153
2340
|
const newCron = existing ? `${existing}
|
|
@@ -2161,7 +2348,8 @@ function uninstallCrontab() {
|
|
|
2161
2348
|
try {
|
|
2162
2349
|
const existing = execSync("crontab -l 2>/dev/null", { encoding: "utf-8" });
|
|
2163
2350
|
const filtered = existing.split("\n").filter((l) => !l.includes(CRON_MARKER)).join("\n").trim();
|
|
2164
|
-
execSync(`
|
|
2351
|
+
execSync(`printf '%s
|
|
2352
|
+
' '${filtered.replace(/'/g, "'\\''")}' | crontab -`, { encoding: "utf-8" });
|
|
2165
2353
|
} catch {
|
|
2166
2354
|
}
|
|
2167
2355
|
}
|
|
@@ -2177,12 +2365,14 @@ function installWindowsTask() {
|
|
|
2177
2365
|
const aqPath = process.argv[1];
|
|
2178
2366
|
const xmlPath = join2(homedir(), "when-does-my-quota-refresh-wakeup.xml");
|
|
2179
2367
|
uninstallWindowsTask();
|
|
2368
|
+
const config = loadConfig();
|
|
2369
|
+
const intervalHours = config.wakeupIntervalHours || 6;
|
|
2180
2370
|
const xml = `<?xml version="1.0" encoding="UTF-16"?>
|
|
2181
2371
|
<Task version="1.4" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
|
|
2182
2372
|
<Triggers>
|
|
2183
2373
|
<TimeTrigger>
|
|
2184
2374
|
<Repetition>
|
|
2185
|
-
<Interval>
|
|
2375
|
+
<Interval>PT${intervalHours}H</Interval>
|
|
2186
2376
|
</Repetition>
|
|
2187
2377
|
<StartBoundary>2024-01-01T09:00:00</StartBoundary>
|
|
2188
2378
|
</TimeTrigger>
|