@meshmakers/octo-meshboard 3.5.520 → 3.5.530

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.
@@ -4350,6 +4350,29 @@ class QueryExecutorService {
4350
4350
  };
4351
4351
  }));
4352
4352
  }
4353
+ /**
4354
+ * Column metadata of a persisted stream-data query for the widget config pickers.
4355
+ * Runs the query with `first: 1` and its own window first; a query that cannot run
4356
+ * without a window — an `Integral` column over a raw archive requires from/to
4357
+ * (AB#7921) — fails or returns no columns, so it is run once more with `window`
4358
+ * (the board's time window or a recent default). The error of the retry propagates.
4359
+ */
4360
+ async fetchStreamDataColumns(queryRtId, window) {
4361
+ try {
4362
+ const result = await firstValueFrom(this.executeStreamData(queryRtId, { first: 1 }));
4363
+ if (result.columns.length > 0) {
4364
+ return result.columns;
4365
+ }
4366
+ }
4367
+ catch {
4368
+ // Fall through to the windowed retry below.
4369
+ }
4370
+ const windowed = await firstValueFrom(this.executeStreamData(queryRtId, {
4371
+ first: 1,
4372
+ streamDataArgs: { from: window.from, to: window.to }
4373
+ }));
4374
+ return windowed.columns;
4375
+ }
4353
4376
  /**
4354
4377
  * Resolution-aware series routing (AB#4290): asks the backend which archive/rollup to
4355
4378
  * query for a base archive family, time window and target point count — so a widget can
@@ -6422,14 +6445,20 @@ const SUPPORTED_ROW_TYPES$1 = [
6422
6445
  function sanitizeFieldName(fieldName) {
6423
6446
  return fieldName.replace(/\./g, '_');
6424
6447
  }
6448
+ /**
6449
+ * Trailing aggregation function suffix the engine appends to a wire-form column:
6450
+ * `_count`, `_sum`, `_avg`, `_min`, `_max`, and `_integral` for the stream-data
6451
+ * Integral (energy over the query window, AB#7921).
6452
+ */
6453
+ const AGGREGATION_FUNCTION_SUFFIX = /_(?:count|sum|avg|min|max|integral)$/i;
6425
6454
  /**
6426
6455
  * Strips a trailing aggregation function suffix from a path so the base path can
6427
6456
  * be compared independently of which aggregation produced it. The engine emits
6428
- * cell paths like `meterreading_count` / `amountvalue_sum` for RT aggregation,
6429
- * grouped aggregation, and stream-data variants.
6457
+ * cell paths like `meterreading_count` / `amountvalue_sum` / `importpower_integral`
6458
+ * for RT aggregation, grouped aggregation, and stream-data variants.
6430
6459
  */
6431
6460
  function stripAggregationFunctionSuffix(path) {
6432
- return path.replace(/_(?:count|sum|avg|min|max)$/i, '');
6461
+ return path.replace(AGGREGATION_FUNCTION_SUFFIX, '');
6433
6462
  }
6434
6463
  /**
6435
6464
  * Canonical key form: drop dot and underscore separators and lowercase the rest.
@@ -6443,11 +6472,11 @@ function toCanonicalAttributeKey(s) {
6443
6472
  }
6444
6473
  /**
6445
6474
  * Detects whether a path ends with the engine's aggregation function suffix
6446
- * (`_count`, `_sum`, `_avg`, `_min`, `_max`). Used to decide whether the loose
6447
- * back-compat fallback in {@link matchesAttributePath} should fire.
6475
+ * (`_count`, `_sum`, `_avg`, `_min`, `_max`, `_integral`). Used to decide whether the
6476
+ * loose back-compat fallback in {@link matchesAttributePath} should fire.
6448
6477
  */
6449
6478
  function hasAggregationFunctionSuffix(path) {
6450
- return /_(?:count|sum|avg|min|max)$/i.test(path);
6479
+ return AGGREGATION_FUNCTION_SUFFIX.test(path);
6451
6480
  }
6452
6481
  /**
6453
6482
  * Returns true when a cell's `attributePath` refers to the same source attribute
@@ -6701,10 +6730,10 @@ function processPieChartData(rows, categoryField, valueField) {
6701
6730
  /**
6702
6731
  * Applies an optional scale factor to a query value (KPI `valueMultiplier`).
6703
6732
  * Only numeric values are scaled — strings (e.g. enum labels or error
6704
- * placeholders) pass through unchanged, as does everything when no multiplier
6705
- * is configured. Use case: cumulated energy from uniformly sampled power
6706
- * (Σ(kW samples) × sampleIntervalHours → kWh); the factor is window-independent
6707
- * because SUM is linear in the sample count.
6733
+ * placeholders) and `null` (no data) pass through unchanged, as does everything
6734
+ * when no multiplier is configured. Use case: the unit factor of an energy KPI
6735
+ * over a stream-data Integral column (value × hours): 1 for kW → kWh, 0.001 for
6736
+ * W → kWh (AB#7921).
6708
6737
  */
6709
6738
  function applyValueMultiplier(value, multiplier) {
6710
6739
  if (typeof multiplier !== 'number' || typeof value !== 'number') {
@@ -7184,8 +7213,8 @@ class KpiWidgetComponent {
7184
7213
  value = this.extractGroupedAggregationValue(result);
7185
7214
  break;
7186
7215
  }
7187
- // Optional scale factor (e.g. Σ(kW samples) × sampleInterval → kWh).
7188
- // Applied to numeric values only; strings pass through unchanged.
7216
+ // Optional unit factor (e.g. 1 for an Integral over kW → kWh, 0.001 for W → kWh).
7217
+ // Applied to numeric values only; strings and null (no data) pass through unchanged.
7189
7218
  value = applyValueMultiplier(value, this.config.valueMultiplier);
7190
7219
  // Create a synthetic entity with the value
7191
7220
  const kpiEntity = {
@@ -7254,10 +7283,12 @@ class KpiWidgetComponent {
7254
7283
  /**
7255
7284
  * Extracts a cell value, preserving strings and converting numbers.
7256
7285
  * Returns the value as-is if it's a string, or parses it as a number.
7286
+ * A null cell is "no data" (e.g. an Integral over an empty window, AB#7921) and
7287
+ * stays null, so the KPI shows its empty state instead of a misleading 0.
7257
7288
  */
7258
7289
  extractCellValue(value) {
7259
7290
  if (value === null || value === undefined)
7260
- return 0;
7291
+ return null;
7261
7292
  if (typeof value === 'number')
7262
7293
  return value;
7263
7294
  if (typeof value === 'string') {
@@ -8082,14 +8113,24 @@ class KpiConfigDialogComponent {
8082
8113
  aggregationType: c.aggregationType ?? null
8083
8114
  }));
8084
8115
  }
8085
- // Stream-data: execute with a tiny page just to surface columns.
8086
- const sdResult = await firstValueFrom(this.queryExecutor.executeStreamData(rtId, { first: 1 }));
8087
- return sdResult.columns.map(c => ({
8116
+ // Stream-data: execute with a tiny page just to surface columns. An Integral
8117
+ // column needs from/to (AB#7921), so the executor retries with the board's
8118
+ // window — or the last 24 hours when the board has no time filter.
8119
+ const columns = await this.queryExecutor.fetchStreamDataColumns(rtId, this.columnProbeWindow());
8120
+ return columns.map(c => ({
8088
8121
  attributePath: c.attributePath,
8089
8122
  attributeValueType: c.attributeValueType ?? '',
8090
8123
  aggregationType: c.aggregationType ?? null
8091
8124
  }));
8092
8125
  }
8126
+ columnProbeWindow() {
8127
+ const boardWindow = this.meshBoardStateService.resolveStreamDataTimeArgs();
8128
+ if (boardWindow) {
8129
+ return boardWindow;
8130
+ }
8131
+ const to = new Date();
8132
+ return { from: new Date(to.getTime() - 24 * 60 * 60 * 1000), to };
8133
+ }
8093
8134
  async onCategoryFieldChange(categoryField) {
8094
8135
  this.form.queryCategoryField = categoryField;
8095
8136
  this.form.queryCategoryValue = '';