@procaaso/alphinity-ui-components 1.1.3 → 1.3.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/index.js CHANGED
@@ -477,11 +477,12 @@ var ControlPanel = ({
477
477
  secondSetpointValue,
478
478
  onSecondSetpointChange,
479
479
  modeConfigs = {},
480
- defaultUnit = "Units",
480
+ defaultUnit,
481
481
  defaultLabel = "Setpoint",
482
- defaultMin = 0,
483
- defaultMax = 100,
484
- defaultPrecision = 2,
482
+ defaultMin,
483
+ defaultMax,
484
+ defaultPrecision,
485
+ scaling,
485
486
  status = "normal",
486
487
  collapsed = false,
487
488
  onCollapseChange,
@@ -503,11 +504,11 @@ var ControlPanel = ({
503
504
  onCollapseChange?.(!newExpanded);
504
505
  };
505
506
  const currentModeConfig = modeConfigs[selectedMode] || {};
506
- const currentUnit = currentModeConfig.unit || defaultUnit;
507
+ const currentUnit = currentModeConfig.unit || defaultUnit || scaling?.units || "Units";
507
508
  const currentLabel = currentModeConfig.label || defaultLabel;
508
- const currentMin = currentModeConfig.min ?? defaultMin;
509
- const currentMax = currentModeConfig.max ?? defaultMax;
510
- const currentPrecision = currentModeConfig.precision ?? defaultPrecision;
509
+ const currentMin = currentModeConfig.min ?? defaultMin ?? scaling?.euLow ?? 0;
510
+ const currentMax = currentModeConfig.max ?? defaultMax ?? scaling?.euHigh ?? 100;
511
+ const currentPrecision = currentModeConfig.precision ?? defaultPrecision ?? scaling?.decimals ?? 2;
511
512
  const hasSecondSetpoint = currentModeConfig.hasSecondSetpoint || false;
512
513
  const baseClass = "ui-control-panel";
513
514
  const statusClass = status ? `ui-control-panel--${status}` : "";
@@ -936,6 +937,8 @@ function DeviceControlPanel({
936
937
  onParameterChange,
937
938
  onStart,
938
939
  onStop,
940
+ startPending = false,
941
+ stopPending = false,
939
942
  onCustomAction,
940
943
  onOpenConfig,
941
944
  showConfigButton = true,
@@ -1682,11 +1685,10 @@ function DeviceControlPanel({
1682
1685
  "button",
1683
1686
  {
1684
1687
  onClick: onStart,
1685
- disabled: binding.state.isRunning,
1686
1688
  style: {
1687
1689
  flex: 1,
1688
1690
  height: touchSize,
1689
- backgroundColor: binding.state.isRunning ? "#a3a3a3" : "#10b981",
1691
+ backgroundColor: "#10b981",
1690
1692
  color: "#ffffff",
1691
1693
  border: "none",
1692
1694
  borderRadius: "6px",
@@ -1694,21 +1696,20 @@ function DeviceControlPanel({
1694
1696
  fontWeight: 700,
1695
1697
  fontFamily: "Arial, sans-serif",
1696
1698
  letterSpacing: "0.5px",
1697
- cursor: binding.state.isRunning ? "not-allowed" : "pointer",
1698
- opacity: binding.state.isRunning ? 0.5 : 1
1699
+ cursor: "pointer",
1700
+ opacity: 1
1699
1701
  },
1700
- children: "START"
1702
+ children: startPending ? "STARTING\u2026" : "START"
1701
1703
  }
1702
1704
  ),
1703
1705
  activeCapabilities?.canStop && /* @__PURE__ */ jsx8(
1704
1706
  "button",
1705
1707
  {
1706
1708
  onClick: onStop,
1707
- disabled: !binding.state.isRunning,
1708
1709
  style: {
1709
1710
  flex: 1,
1710
1711
  height: touchSize,
1711
- backgroundColor: !binding.state.isRunning ? "#a3a3a3" : "#ef4444",
1712
+ backgroundColor: "#ef4444",
1712
1713
  color: "#ffffff",
1713
1714
  border: "none",
1714
1715
  borderRadius: "6px",
@@ -1716,10 +1717,10 @@ function DeviceControlPanel({
1716
1717
  fontWeight: 700,
1717
1718
  fontFamily: "Arial, sans-serif",
1718
1719
  letterSpacing: "0.5px",
1719
- cursor: !binding.state.isRunning ? "not-allowed" : "pointer",
1720
- opacity: !binding.state.isRunning ? 0.5 : 1
1720
+ cursor: "pointer",
1721
+ opacity: 1
1721
1722
  },
1722
- children: "STOP"
1723
+ children: stopPending ? "STOPPING\u2026" : "STOP"
1723
1724
  }
1724
1725
  )
1725
1726
  ] }),
@@ -4354,12 +4355,13 @@ function DataOverlay({
4354
4355
  scale = 1,
4355
4356
  statusColorMap
4356
4357
  }) {
4357
- const { value, display, history } = telemetry;
4358
+ const { value, display, history, scaling } = telemetry;
4358
4359
  const showValue = display?.showValue ?? true;
4359
4360
  const showStatus = display?.showStatus ?? true;
4360
4361
  const showSparkline = display?.showSparkline ?? false;
4361
4362
  const label = display?.label;
4362
- const precision = display?.precision;
4363
+ const precision = scaling?.decimals ?? display?.precision;
4364
+ const unit = scaling?.units ?? value.unit;
4363
4365
  const sparklineColor = display?.sparklineColor;
4364
4366
  const textColor = display?.textColor;
4365
4367
  const backgroundColor = display?.backgroundColor;
@@ -4383,7 +4385,7 @@ function DataOverlay({
4383
4385
  const colors = backgroundColor && backgroundColor !== "status" ? { bg: backgroundColor, text: textColor || "#ffffff", border: backgroundColor } : statusColors;
4384
4386
  const finalTextColor = textColor || colors.text;
4385
4387
  const formattedValue = formatValue(value.value);
4386
- const displayText = value.unit ? `${formattedValue} ${value.unit}` : formattedValue;
4388
+ const displayText = unit ? `${formattedValue} ${unit}` : formattedValue;
4387
4389
  const sparklinePath = (() => {
4388
4390
  if (!showSparkline || !history || history.length < 2) return "";
4389
4391
  const width = 70 * scale;
@@ -4391,8 +4393,9 @@ function DataOverlay({
4391
4393
  const padding = 5 * scale;
4392
4394
  const effectiveWidth = width - padding * 2;
4393
4395
  const effectiveHeight = height - padding * 2;
4394
- const min = Math.min(...history);
4395
- const max = Math.max(...history);
4396
+ const hasEuRange = scaling?.euLow !== void 0 && scaling?.euHigh !== void 0 && scaling.euHigh !== scaling.euLow;
4397
+ const min = hasEuRange ? scaling.euLow : Math.min(...history);
4398
+ const max = hasEuRange ? scaling.euHigh : Math.max(...history);
4396
4399
  const range = max - min || 1;
4397
4400
  const points = history.map((val, i) => {
4398
4401
  const xPos = i / (history.length - 1) * effectiveWidth + padding;
@@ -7553,8 +7556,12 @@ function createControlBinding(nodeId, deviceConfig, actions) {
7553
7556
  }
7554
7557
 
7555
7558
  // src/hooks/useDeviceControls.ts
7556
- import { useState as useState15, useCallback as useCallback8 } from "react";
7557
- function useDeviceControls(initialBindings) {
7559
+ import { useState as useState15, useCallback as useCallback8, useRef as useRef6 } from "react";
7560
+ function commandKey(nodeId, kind, suffix) {
7561
+ return suffix ? `${nodeId}:${kind}:${suffix}` : `${nodeId}:${kind}`;
7562
+ }
7563
+ function useDeviceControls(initialBindings, options) {
7564
+ const { commandTimeoutMs } = options ?? {};
7558
7565
  const [controls, setControls] = useState15(() => {
7559
7566
  const map = /* @__PURE__ */ new Map();
7560
7567
  if (initialBindings) {
@@ -7564,6 +7571,11 @@ function useDeviceControls(initialBindings) {
7564
7571
  }
7565
7572
  return map;
7566
7573
  });
7574
+ const pendingRef = useRef6(/* @__PURE__ */ new Map());
7575
+ const [pendingCommands, setPendingCommands] = useState15(() => /* @__PURE__ */ new Set());
7576
+ const syncPending = useCallback8(() => {
7577
+ setPendingCommands(new Set(pendingRef.current.keys()));
7578
+ }, []);
7567
7579
  const updateControl = useCallback8((nodeId, updates) => {
7568
7580
  setControls((prev) => {
7569
7581
  const newMap = new Map(prev);
@@ -7653,121 +7665,112 @@ function useDeviceControls(initialBindings) {
7653
7665
  },
7654
7666
  [controls]
7655
7667
  );
7656
- const sendModeChange = useCallback8(
7657
- async (nodeId, mode) => {
7658
- const binding = controls.get(nodeId);
7659
- if (!binding) return;
7660
- updateDeviceState(nodeId, { currentMode: mode });
7661
- try {
7662
- await binding.actions.onModeChange?.(mode);
7663
- updateDeviceState(nodeId, {
7664
- lastCommandTime: Date.now(),
7665
- lastCommandResult: { success: true, message: `Mode changed to ${mode}` }
7666
- });
7667
- } catch (error) {
7668
- updateDeviceState(nodeId, {
7669
- currentMode: binding.state.currentMode,
7670
- lastCommandResult: {
7671
- success: false,
7672
- message: error instanceof Error ? error.message : "Mode change failed"
7673
- }
7674
- });
7668
+ const isCommandPending = useCallback8(
7669
+ (nodeId, kind) => {
7670
+ if (kind) {
7671
+ const prefix2 = `${nodeId}:${kind}`;
7672
+ for (const key of pendingCommands) {
7673
+ if (key === prefix2 || key.startsWith(`${prefix2}:`)) return true;
7674
+ }
7675
+ return false;
7676
+ }
7677
+ const prefix = `${nodeId}:`;
7678
+ for (const key of pendingCommands) {
7679
+ if (key.startsWith(prefix)) return true;
7675
7680
  }
7681
+ return false;
7676
7682
  },
7677
- [controls, updateDeviceState]
7683
+ [pendingCommands]
7678
7684
  );
7679
- const sendParameterChange = useCallback8(
7680
- async (nodeId, parameterId, value) => {
7681
- const binding = controls.get(nodeId);
7682
- if (!binding) return;
7683
- updateParameter(nodeId, parameterId, value);
7684
- try {
7685
- await binding.actions.onParameterChange?.(parameterId, value);
7686
- updateDeviceState(nodeId, {
7687
- lastCommandTime: Date.now(),
7688
- lastCommandResult: { success: true, message: `Parameter ${parameterId} updated` }
7689
- });
7690
- } catch (error) {
7691
- updateDeviceState(nodeId, {
7692
- lastCommandResult: {
7693
- success: false,
7694
- message: error instanceof Error ? error.message : "Parameter change failed"
7695
- }
7696
- });
7697
- }
7685
+ const markPending = useCallback8(
7686
+ (key, delta) => {
7687
+ const next = (pendingRef.current.get(key) ?? 0) + delta;
7688
+ if (next > 0) pendingRef.current.set(key, next);
7689
+ else pendingRef.current.delete(key);
7690
+ syncPending();
7698
7691
  },
7699
- [controls, updateParameter, updateDeviceState]
7692
+ [syncPending]
7700
7693
  );
7701
- const sendStartCommand = useCallback8(
7702
- async (nodeId) => {
7694
+ const runCommand = useCallback8(
7695
+ async (nodeId, key, invoke, messages) => {
7703
7696
  const binding = controls.get(nodeId);
7704
7697
  if (!binding) return;
7705
- updateDeviceState(nodeId, { isRunning: true, status: "starting" });
7698
+ markPending(key, 1);
7699
+ let timer;
7706
7700
  try {
7707
- await binding.actions.onStart?.();
7701
+ const action = Promise.resolve(invoke(binding));
7702
+ if (commandTimeoutMs && commandTimeoutMs > 0) {
7703
+ await Promise.race([
7704
+ action,
7705
+ new Promise((_, reject) => {
7706
+ timer = setTimeout(
7707
+ () => reject(new Error(`${messages.success.replace(/ sent$/, "")} timed out (unconfirmed)`)),
7708
+ commandTimeoutMs
7709
+ );
7710
+ })
7711
+ ]);
7712
+ } else {
7713
+ await action;
7714
+ }
7708
7715
  updateDeviceState(nodeId, {
7709
- status: "normal",
7710
7716
  lastCommandTime: Date.now(),
7711
- lastCommandResult: { success: true, message: "Device started" }
7717
+ lastCommandResult: { success: true, message: messages.success }
7712
7718
  });
7713
7719
  } catch (error) {
7714
7720
  updateDeviceState(nodeId, {
7715
- isRunning: false,
7716
- status: "fault",
7721
+ lastCommandTime: Date.now(),
7717
7722
  lastCommandResult: {
7718
7723
  success: false,
7719
- message: error instanceof Error ? error.message : "Start failed"
7724
+ message: error instanceof Error ? error.message : messages.failure
7720
7725
  }
7721
7726
  });
7727
+ } finally {
7728
+ if (timer) clearTimeout(timer);
7729
+ markPending(key, -1);
7722
7730
  }
7723
7731
  },
7724
- [controls, updateDeviceState]
7732
+ [controls, updateDeviceState, markPending, commandTimeoutMs]
7733
+ );
7734
+ const sendModeChange = useCallback8(
7735
+ (nodeId, mode) => runCommand(
7736
+ nodeId,
7737
+ commandKey(nodeId, "mode"),
7738
+ (binding) => binding.actions.onModeChange?.(mode),
7739
+ { success: `Mode ${mode} command sent`, failure: "Mode change failed" }
7740
+ ),
7741
+ [runCommand]
7742
+ );
7743
+ const sendParameterChange = useCallback8(
7744
+ (nodeId, parameterId, value) => runCommand(
7745
+ nodeId,
7746
+ commandKey(nodeId, "parameter", parameterId),
7747
+ (binding) => binding.actions.onParameterChange?.(parameterId, value),
7748
+ { success: `Parameter ${parameterId} command sent`, failure: "Parameter change failed" }
7749
+ ),
7750
+ [runCommand]
7751
+ );
7752
+ const sendStartCommand = useCallback8(
7753
+ (nodeId) => runCommand(nodeId, commandKey(nodeId, "start"), (binding) => binding.actions.onStart?.(), {
7754
+ success: "Start command sent",
7755
+ failure: "Start command failed"
7756
+ }),
7757
+ [runCommand]
7725
7758
  );
7726
7759
  const sendStopCommand = useCallback8(
7727
- async (nodeId) => {
7728
- const binding = controls.get(nodeId);
7729
- if (!binding) return;
7730
- updateDeviceState(nodeId, { isRunning: false, status: "stopping" });
7731
- try {
7732
- await binding.actions.onStop?.();
7733
- updateDeviceState(nodeId, {
7734
- status: "off",
7735
- lastCommandTime: Date.now(),
7736
- lastCommandResult: { success: true, message: "Device stopped" }
7737
- });
7738
- } catch (error) {
7739
- updateDeviceState(nodeId, {
7740
- isRunning: true,
7741
- status: "fault",
7742
- lastCommandResult: {
7743
- success: false,
7744
- message: error instanceof Error ? error.message : "Stop failed"
7745
- }
7746
- });
7747
- }
7748
- },
7749
- [controls, updateDeviceState]
7760
+ (nodeId) => runCommand(nodeId, commandKey(nodeId, "stop"), (binding) => binding.actions.onStop?.(), {
7761
+ success: "Stop command sent",
7762
+ failure: "Stop command failed"
7763
+ }),
7764
+ [runCommand]
7750
7765
  );
7751
7766
  const sendCustomAction = useCallback8(
7752
- async (nodeId, actionId) => {
7753
- const binding = controls.get(nodeId);
7754
- if (!binding) return;
7755
- try {
7756
- await binding.actions.onCustomAction?.(actionId);
7757
- updateDeviceState(nodeId, {
7758
- lastCommandTime: Date.now(),
7759
- lastCommandResult: { success: true, message: `Action ${actionId} executed` }
7760
- });
7761
- } catch (error) {
7762
- updateDeviceState(nodeId, {
7763
- lastCommandResult: {
7764
- success: false,
7765
- message: error instanceof Error ? error.message : "Action failed"
7766
- }
7767
- });
7768
- }
7769
- },
7770
- [controls, updateDeviceState]
7767
+ (nodeId, actionId) => runCommand(
7768
+ nodeId,
7769
+ commandKey(nodeId, "action", actionId),
7770
+ (binding) => binding.actions.onCustomAction?.(actionId),
7771
+ { success: `Action ${actionId} command sent`, failure: "Action failed" }
7772
+ ),
7773
+ [runCommand]
7771
7774
  );
7772
7775
  return {
7773
7776
  controls,
@@ -7778,6 +7781,10 @@ function useDeviceControls(initialBindings) {
7778
7781
  setControlBinding,
7779
7782
  removeControlBinding,
7780
7783
  getControlBinding,
7784
+ // In-flight command visibility (interaction state, not device state).
7785
+ // `isCommandPending` is the stable query surface; the raw key set is
7786
+ // intentionally not exported (its key format is an implementation detail).
7787
+ isCommandPending,
7781
7788
  // Command helpers
7782
7789
  sendModeChange,
7783
7790
  sendParameterChange,
@@ -8066,7 +8073,7 @@ function useDeviceConfigs(initialConfigs) {
8066
8073
  }
8067
8074
 
8068
8075
  // src/components/DeviceConfigPanel/DeviceConfigPanel.tsx
8069
- import { useState as useState17, useEffect as useEffect12, useMemo, useRef as useRef6 } from "react";
8076
+ import { useState as useState17, useEffect as useEffect12, useMemo, useRef as useRef7 } from "react";
8070
8077
  import { Fragment as Fragment9, jsx as jsx22, jsxs as jsxs18 } from "react/jsx-runtime";
8071
8078
  function DeviceConfigPanel({
8072
8079
  binding,
@@ -8108,7 +8115,7 @@ function DeviceConfigPanel({
8108
8115
  });
8109
8116
  const [showToast, setShowToast] = useState17(true);
8110
8117
  const [bindingId, setBindingId] = useState17(binding?.nodeId || null);
8111
- const lastResultTimestampRef = useRef6(0);
8118
+ const lastResultTimestampRef = useRef7(0);
8112
8119
  if (binding && binding.nodeId !== bindingId) {
8113
8120
  const values = {};
8114
8121
  binding.parameters.forEach((param) => {