1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/vue-query';
import { computed } from 'vue';
import type { ComputedRef } from 'vue';
import api from '@/store/api';
import { useRedfishRoot } from '@/api/composables/useRedfishRoot';
import { useRedfishCollection } from '@/api/composables/useRedfishCollection';
import { shouldRetry } from '@/api/composables/useAllSubResources';
import type { Chassis, EnvironmentMetrics } from '@/api/types/redfish';
export const powerControlQueryKey = ['redfish', 'environmentMetrics'] as const;
export interface UsePowerControlReturn {
powerConsumptionValue: ComputedRef<number | null>;
powerCapMin: ComputedRef<number | null>;
powerCapMax: ComputedRef<number | null>;
/** EnvironmentMetrics from Redfish; use data.PowerLimitWatts?.SetPoint etc. */
environmentMetrics: ComputedRef<EnvironmentMetrics | null>;
submitPowerControl: (
powerCapValue: number | null,
isPowerCapEnabled: boolean,
) => Promise<void>;
metricsQuery: ReturnType<typeof useQuery<EnvironmentMetrics | null, unknown>>;
mutation: ReturnType<
typeof useMutation<
void,
unknown,
{
powerCapValue: number | null;
isPowerCapEnabled: boolean;
},
unknown
>
>;
chassisQuery: ReturnType<typeof useRedfishCollection<Chassis>>;
environmentMetricsUri: ComputedRef<string | null>;
}
/**
* Composable for power control data fetching and mutations.
* Focuses on query/mutation logic only - form state management is left to consuming components.
* This maintains unidirectional data flow: query → component state → edit → mutation.
*
* Note: This implementation uses the first Chassis with EnvironmentMetrics.
* In multi-chassis systems, only the first chassis with power metrics will be controlled.
* This is intentional for the current use case but could be extended to support
* chassis selection if needed in the future.
*/
export function usePowerControl(): UsePowerControlReturn {
const queryClient = useQueryClient();
// Ensure ServiceRoot is cached; useRedfishCollection uses it internally
useRedfishRoot();
const chassisQuery = useRedfishCollection<Chassis>(
'/redfish/v1/Chassis',
{ expand: true, expandLevels: 2 },
);
const chassisMembers = chassisQuery.data;
const environmentMetricsUri = computed(() => {
const members = chassisMembers.value;
if (!members?.length) return null;
const firstWithMetrics = members.find(
(c: Chassis) => c.EnvironmentMetrics?.['@odata.id'],
);
return firstWithMetrics?.EnvironmentMetrics?.['@odata.id'] ?? null;
});
const metricsQuery = useQuery({
queryKey: computed(() => [
...powerControlQueryKey,
environmentMetricsUri.value,
]),
queryFn: async ({ signal }) => {
const uri = environmentMetricsUri.value;
if (!uri) return null;
const { data } = await api.get<EnvironmentMetrics>(uri, { signal });
return data;
},
enabled: computed(() => !!environmentMetricsUri.value),
staleTime: 30000, // 30 seconds - data is considered fresh for this duration
refetchInterval: 30000, // Auto-refresh every 30 seconds for live power consumption updates
refetchIntervalInBackground: false, // Only poll when tab is visible
gcTime: 300000,
refetchOnMount: true,
refetchOnWindowFocus: true,
refetchOnReconnect: true,
placeholderData: (prev) => prev,
retry: shouldRetry,
retryDelay: (attemptIndex: number) =>
Math.min(1000 * 2 ** attemptIndex, 30000),
});
const mutation = useMutation<
void,
unknown,
{
powerCapValue: number | null;
isPowerCapEnabled: boolean;
},
unknown
>({
mutationFn: async ({
powerCapValue,
isPowerCapEnabled,
}: {
powerCapValue: number | null;
isPowerCapEnabled: boolean;
}) => {
const data = metricsQuery.data.value;
const metricsUri = data?.['@odata.id'];
if (!metricsUri) {
throw new Error('Power control not loaded or not available');
}
// UI allows toggling between enabled/disabled.
// When enabling: preserve the original mode if it was Manual/Override,
// otherwise default to Automatic.
// When disabling: always use Disabled.
const originalMode = data?.PowerLimitWatts?.ControlMode ?? 'Disabled';
const controlMode = isPowerCapEnabled
? originalMode === 'Disabled'
? 'Automatic'
: originalMode
: 'Disabled';
// Build the patch payload - omit SetPoint when disabling to avoid sending invalid 0 value
const patchPayload: {
PowerLimitWatts: {
ControlMode: 'Automatic' | 'Disabled' | 'Manual' | 'Override';
SetPoint?: number;
};
} = {
PowerLimitWatts: {
ControlMode: controlMode,
},
};
// Only include SetPoint when enabling and value is provided
if (isPowerCapEnabled && powerCapValue !== null) {
patchPayload.PowerLimitWatts.SetPoint = powerCapValue;
}
await api.patch(metricsUri, patchPayload);
},
onSuccess: () => {
// Invalidate queries to refetch fresh data from server
// This ensures we get the actual server state after mutation
queryClient.invalidateQueries({ queryKey: powerControlQueryKey });
},
});
const environmentMetrics = computed<EnvironmentMetrics | null>(
() => metricsQuery.data.value ?? null,
);
const powerConsumptionValue = computed<number | null>(
() => environmentMetrics.value?.PowerWatts?.Reading ?? null,
);
const powerCapMin = computed<number | null>(
() => environmentMetrics.value?.PowerLimitWatts?.AllowableMin ?? null,
);
const powerCapMax = computed<number | null>(
() => environmentMetrics.value?.PowerLimitWatts?.AllowableMax ?? null,
);
async function submitPowerControl(
powerCapValue: number | null,
isPowerCapEnabled: boolean,
): Promise<void> {
await mutation.mutateAsync({
powerCapValue,
isPowerCapEnabled,
});
}
return {
powerConsumptionValue,
powerCapMin,
powerCapMax,
environmentMetrics,
submitPowerControl,
metricsQuery,
mutation,
chassisQuery,
environmentMetricsUri,
};
}
|