M8-T18: add source and multi-commodity meter UI

This commit is contained in:
2026-08-23 21:22:06 +02:00
parent 39c11ae606
commit 963e43e3e4
15 changed files with 319 additions and 18 deletions
+47 -1
View File
@@ -237,6 +237,13 @@ export type SummaryResponse = components['schemas']['SummaryResponse']
export type DsmrLatestResponse = components['schemas']['DsmrLatestResponse']
export type TibberTestResponse = components['schemas']['TibberTestResponse']
export type TibberTestPriceSchema = components['schemas']['TibberTestPriceSchema']
export type SourceProfileResponse = components['schemas']['SourceProfileResponse']
export type MeterSourceResponse = components['schemas']['MeterSourceResponse']
export type MeterSourceCreate = components['schemas']['MeterSourceCreate']
export type MeterSourcePatch = components['schemas']['MeterSourcePatch']
export type MeterSourceChannelResponse = components['schemas']['MeterSourceChannelResponse']
export type BindingResponse = components['schemas']['BindingResponse']
export type BindingCreate = components['schemas']['BindingCreate']
// ---------------------------------------------------------------------------
// Query: list all energy contracts
@@ -470,8 +477,14 @@ export function useDeclareMeter() {
return useMutation({
mutationFn: (body: MeterDeclareRequest) =>
apiClient.POST('/api/energy/meters', { body }),
onSuccess: () => {
onSuccess: (_data, variables) => {
void qc.invalidateQueries({ queryKey: ['energy-meters'] })
if (variables.source_channel_uuid) {
void qc.invalidateQueries({ queryKey: ['energy-source-channels'] })
void qc.invalidateQueries({ queryKey: ['energy-meter-bindings'] })
void qc.invalidateQueries({ queryKey: ['energy-sources'] })
void qc.invalidateQueries({ queryKey: ['expose-catalog'] })
}
// Invalidate cost-related queries: a new meter may trigger recompute server-side.
void qc.invalidateQueries({ queryKey: ['energy-costs'] })
void qc.invalidateQueries({ queryKey: ['energy-costs-summary'] })
@@ -500,6 +513,39 @@ export function useUpdateMeter() {
})
}
// Source → channel → binding hooks. These deliberately use the generated
// OpenAPI types; UI suggestions remain just suggestions until a user binds one.
export function useSourceProfiles() {
return useQuery({ queryKey: ['energy-source-profiles'], queryFn: async () => {
const res = await apiClient.GET('/api/energy/source-profiles'); return res.data
}, staleTime: 5 * 60 * 1000 })
}
export function useSources() {
return useQuery({ queryKey: ['energy-sources'], queryFn: async () => {
const res = await apiClient.GET('/api/energy/sources'); return res.data
} })
}
export function useSource(uuid: string | null) {
return useQuery({ queryKey: ['energy-source', uuid], enabled: !!uuid, queryFn: async () => {
const res = await apiClient.GET('/api/energy/sources/{source_uuid}', { params: { path: { source_uuid: uuid! } } }); return res.data
} })
}
function invalidateSourceQueries(qc: ReturnType<typeof useQueryClient>) {
void qc.invalidateQueries({ queryKey: ['energy-sources'] }); void qc.invalidateQueries({ queryKey: ['energy-source'] });
void qc.invalidateQueries({ queryKey: ['energy-source-channels'] }); void qc.invalidateQueries({ queryKey: ['energy-meters'] });
void qc.invalidateQueries({ queryKey: ['energy-channel-readings'] }); void qc.invalidateQueries({ queryKey: ['energy-meter-bindings'] })
void qc.invalidateQueries({ queryKey: ['expose-catalog'] })
}
export function useCreateSource() { const qc = useQueryClient(); return useMutation({ mutationFn: (body: MeterSourceCreate) => apiClient.POST('/api/energy/sources', { body }), onSuccess: () => invalidateSourceQueries(qc) }) }
export function useUpdateSource() { const qc = useQueryClient(); return useMutation({ mutationFn: ({ uuid, body }: { uuid: string; body: MeterSourcePatch }) => apiClient.PATCH('/api/energy/sources/{source_uuid}', { params: { path: { source_uuid: uuid } }, body }), onSuccess: () => invalidateSourceQueries(qc) }) }
export function useDeleteSource() { const qc = useQueryClient(); return useMutation({ mutationFn: (uuid: string) => apiClient.DELETE('/api/energy/sources/{source_uuid}', { params: { path: { source_uuid: uuid } } }), onSuccess: () => invalidateSourceQueries(qc) }) }
export function useDiscoverSource() { const qc = useQueryClient(); return useMutation({ mutationFn: (uuid: string) => apiClient.POST('/api/energy/sources/{source_uuid}/discover', { params: { path: { source_uuid: uuid } } }), onSuccess: () => invalidateSourceQueries(qc) }) }
export function useSourceChannels(uuid: string | null) { return useQuery({ queryKey: ['energy-source-channels', uuid], enabled: !!uuid, queryFn: async () => { const res = await apiClient.GET('/api/energy/sources/{source_uuid}/channels', { params: { path: { source_uuid: uuid! } } }); return res.data } }) }
export function useChannelReadings(sourceUuid: string | null, channelUuid: string | null) { return useQuery({ queryKey: ['energy-channel-readings', sourceUuid, channelUuid], enabled: !!sourceUuid && !!channelUuid, queryFn: async () => { const res = await apiClient.GET('/api/energy/sources/{source_uuid}/channels/{channel_uuid}/readings', { params: { path: { source_uuid: sourceUuid!, channel_uuid: channelUuid! }, query: { limit: 60 } } }); return res.data } }) }
export function useMeterBindings(id: number | null) { return useQuery({ queryKey: ['energy-meter-bindings', id], enabled: id != null, queryFn: async () => { const res = await apiClient.GET('/api/energy/meters/{meter_id}/bindings', { params: { path: { meter_id: id! } } }); return res.data } }) }
export function useCreateBinding() { const qc = useQueryClient(); return useMutation({ mutationFn: ({ id, body }: { id: number; body: BindingCreate }) => apiClient.POST('/api/energy/meters/{meter_id}/bindings', { params: { path: { meter_id: id } }, body }), onSuccess: () => invalidateSourceQueries(qc) }) }
export function useCloseBinding() { const qc = useQueryClient(); return useMutation({ mutationFn: ({ uuid, ended_at }: { uuid: string; ended_at: string }) => apiClient.PATCH('/api/energy/bindings/{binding_uuid}', { params: { path: { binding_uuid: uuid } }, body: { ended_at } }), onSuccess: () => invalidateSourceQueries(qc) }) }
// ---------------------------------------------------------------------------
// Query: time-range readings for a device (window + limit — never full-table)
// ---------------------------------------------------------------------------