feat: add relay composables

This commit is contained in:
2026-05-14 22:41:11 +02:00
parent 03e53aa389
commit 970a38153e
9 changed files with 136 additions and 10 deletions

View File

@@ -891,8 +891,8 @@ CLOSED: [2026-05-14 jeu. 20:09]
- *File*: =src/presentation/api/relay_api.rs= - *File*: =src/presentation/api/relay_api.rs=
- *Complexity*: Low | *Uncertainty*: Low - *Complexity*: Low | *Uncertainty*: Low
*** TODO Frontend Implementation [0/2] *** TODO Frontend Implementation [1/2]
- [ ] *T052* [P] [US1] [TDD] Create =RelayDto= TypeScript interface - [X] *T052* [P] [US1] [TDD] Create =RelayDto= TypeScript interface
- Generate from OpenAPI spec or manually define - Generate from OpenAPI spec or manually define
- *File*: =frontend/src/types/relay.ts= - *File*: =frontend/src/types/relay.ts=
- *Complexity*: Low | *Uncertainty*: Low - *Complexity*: Low | *Uncertainty*: Low

View File

@@ -0,0 +1,37 @@
import { ref } from 'vue';
import { apiClient } from '../api/client';
import { relayDtoToDomain } from '../types/mappers/relayDtoMapper';
import type { Relay, RelayDto } from '../types/relay';
export function useRelay() {
const isLoading = ref(false);
const error = ref<string | null>(null);
const response = ref<Relay | null>(null);
const toggleRelay = async (id: number) => {
isLoading.value = true;
try {
const { data } = await apiClient.POST('/api/relays/{id}/toggle', {
params: {
path: {
id,
},
},
});
error.value = null;
response.value = relayDtoToDomain(data as RelayDto);
} catch (err: any) {
console.error(`Failed to toggle relay ${id}:`, err);
error.value = err.message || `Failed to toggle relay ${id}`;
} finally {
isLoading.value = false;
}
};
return {
toggleRelay,
isLoading,
error,
response,
};
}

View File

@@ -0,0 +1,51 @@
import { onMounted, onUnmounted, ref } from 'vue';
import { apiClient } from '../api/client';
import { relayDtoToDomain } from '../types/mappers/relayDtoMapper';
import type { Relay } from '../types/relay';
import { isNil } from '../utils/isNil';
export function useRelayPolling(intervalMs: number = 2000) {
const relays = ref<Relay[]>([]);
const isLoading = ref(false);
const error = ref<string | null>(null);
let pollingInterval: number | null = null;
const fetchData = async () => {
isLoading.value = true;
try {
const { data } = await apiClient.GET('/api/relays');
relays.value = data?.map(relayDtoToDomain) ?? [];
error.value = null;
} catch (err: any) {
console.error('Polling error:', err);
error.value = err.message || 'Failed to fetch data';
} finally {
isLoading.value = false;
}
};
const startPolling = () => {
fetchData();
pollingInterval = window.setInterval(fetchData, intervalMs);
};
const stopPolling = () => {
if (isNil(pollingInterval)) {
return;
}
clearInterval(pollingInterval);
pollingInterval = null;
};
onMounted(startPolling);
onUnmounted(stopPolling);
return {
relays,
isLoading,
error,
refresh: fetchData,
};
}

View File

@@ -1,5 +1,6 @@
import { createApp } from "vue"; import { createApp } from 'vue';
import "./style.css";
import App from "./App.vue";
createApp(App).mount("#app"); import './style.css';
import App from './App.vue';
createApp(App).mount('#app');

View File

@@ -0,0 +1,13 @@
import { isNil } from '../../utils/isNil';
import { RelayState, Relay, type RelayDto } from '../relay';
const relayStateToDomain = (dto: string | null): RelayState => {
if (isNil(dto) || dto.trim() === '') {
return RelayState.Off;
}
return dto.trim().toLowerCase() === 'on' ? RelayState.On : RelayState.Off;
};
export const relayDtoToDomain = (dto: RelayDto): Relay => {
return new Relay(dto.id, relayStateToDomain(dto.state), dto.label);
};

20
src/types/relay.ts Normal file
View File

@@ -0,0 +1,20 @@
import type { components } from '../api/schema';
export type RelayDto = components['schemas']['RelayDto'];
export enum RelayState {
On = 'on',
Off = 'off',
}
export class Relay {
id: number;
state: RelayState;
label: string;
constructor(id: number, state: RelayState, label: string) {
this.id = id;
this.state = state;
this.label = label;
}
}

2
src/utils/isNil.ts Normal file
View File

@@ -0,0 +1,2 @@
export const isNil = (value: unknown | null | undefined): value is null | undefined =>
value === null || value === undefined;

View File

@@ -8,9 +8,10 @@
"strict": true, "strict": true,
"noUnusedLocals": true, "noUnusedLocals": true,
"noUnusedParameters": true, "noUnusedParameters": true,
"erasableSyntaxOnly": true, "erasableSyntaxOnly": false,
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true "noUncheckedSideEffectImports": true,
"noUncheckedIndexedAccess": true,
}, },
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"] "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
} }

View File

@@ -18,9 +18,10 @@
"strict": true, "strict": true,
"noUnusedLocals": true, "noUnusedLocals": true,
"noUnusedParameters": true, "noUnusedParameters": true,
"erasableSyntaxOnly": true, "erasableSyntaxOnly": false,
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true "noUncheckedSideEffectImports": true,
"noUncheckedIndexedAccess": true,
}, },
"include": ["vite.config.ts"] "include": ["vite.config.ts"]
} }