From 53720f98760c063ff09e8c6efd1084d653def9d2 Mon Sep 17 00:00:00 2001 From: Daniel Bayerlein Date: Sun, 22 Jan 2023 10:34:10 +0100 Subject: [PATCH] feat(ui): show only available hardware based options (#119) Closes: #109 Co-authored-by: TheRealKasumi --- ui/public/locales/de/translation.json | 8 +- ui/public/locales/en/translation.json | 8 +- ui/src/components/Notification.tsx | 19 ++- ui/src/components/Slider.tsx | 3 +- ui/src/mocks/handlers.ts | 62 +++++++- ui/src/pages/CustomAnimations.tsx | 15 +- ui/src/pages/Settings.tsx | 201 ++++++++++++++++---------- ui/src/pages/Update.tsx | 2 +- ui/src/pages/Zone.tsx | 36 +++-- ui/src/pages/api/index.ts | 1 + ui/src/pages/api/systemInfo.ts | 58 ++++++++ 11 files changed, 310 insertions(+), 103 deletions(-) create mode 100644 ui/src/pages/api/systemInfo.ts diff --git a/ui/public/locales/de/translation.json b/ui/public/locales/de/translation.json index 3e329e2..e58d38f 100644 --- a/ui/public/locales/de/translation.json +++ b/ui/public/locales/de/translation.json @@ -24,7 +24,12 @@ "title": "Log-Dateien" }, "notification": { - "headline": "Etwas ist schief gelaufen" + "error": { + "headline": "Etwas ist schief gelaufen" + }, + "warning": { + "headline": "Achtung" + } }, "settings": { "accessPointPassword": "Passwort", @@ -43,6 +48,7 @@ "Manual75": "75%", "Manual100": "100%" }, + "fanModeAutomaticWithoutTemperatureSensors": "Keine Temperatursensoren sind angeschlossen. Die Lüftergeschwindigkeit wird automatisch auf 100% gesetzt.", "language": "Sprache", "lightSensorDuration": "Sensor Laufzeit", "lightSensorMaxAmbientBrightness": "Maximale Helligkeit", diff --git a/ui/public/locales/en/translation.json b/ui/public/locales/en/translation.json index 93a2bf5..3eaf7bd 100644 --- a/ui/public/locales/en/translation.json +++ b/ui/public/locales/en/translation.json @@ -24,7 +24,12 @@ "title": "Log Files" }, "notification": { - "headline": "Something went wrong" + "error": { + "headline": "Something went wrong" + }, + "warning": { + "headline": "Warning" + } }, "settings": { "accessPointPassword": "Password", @@ -43,6 +48,7 @@ "Manual75": "75%", "Manual100": "100%" }, + "fanModeAutomaticWithoutTemperatureSensors": "No temperature sensors are connected. The fan speed is automatically set to 100%.", "language": "Language", "lightSensorDuration": "Sensor Duration", "lightSensorMaxAmbientBrightness": "Maximum Brightness", diff --git a/ui/src/components/Notification.tsx b/ui/src/components/Notification.tsx index 8d7ee0b..a99b2a0 100644 --- a/ui/src/components/Notification.tsx +++ b/ui/src/components/Notification.tsx @@ -4,19 +4,26 @@ import { twMerge } from 'tailwind-merge'; type NotificationProps = { message: string; + state: 'error' | 'warning'; } & React.HTMLAttributes; export const Notification = ({ message, className, + state, ...props }: NotificationProps) => { const { t } = useTranslation(); + const isError = state === 'error'; + const isWarning = state === 'warning'; + return (
- +
-

{t('notification.headline')}

+

{t(`notification.${state}.headline`)}

{message}

diff --git a/ui/src/components/Slider.tsx b/ui/src/components/Slider.tsx index 52fd42c..a91a014 100644 --- a/ui/src/components/Slider.tsx +++ b/ui/src/components/Slider.tsx @@ -26,6 +26,7 @@ export const Slider = ({ step={1} className={twMerge( 'relative flex h-5 w-64 touch-none items-center', + 'radix-disabled:opacity-50', className, )} {...props} @@ -40,7 +41,7 @@ export const Slider = ({ diff --git a/ui/src/mocks/handlers.ts b/ui/src/mocks/handlers.ts index 43a51cb..f336987 100644 --- a/ui/src/mocks/handlers.ts +++ b/ui/src/mocks/handlers.ts @@ -1,5 +1,5 @@ import { rest } from 'msw'; -import type { Fseq, Led, System, Ui, Wifi } from '../pages/api'; +import type { Fseq, Led, System, SystemInfo, Ui, Wifi } from '../pages/api'; import { throttledRes } from './throttledRes'; const mock: { @@ -15,7 +15,11 @@ const mock: { ui: { uiConfig: Ui; }; + info: { + system: SystemInfo; + }; motion: { + // TODO: Update type motionSensorCalibration: { accXRaw: number; accYRaw: number; @@ -29,7 +33,7 @@ const mock: { gyroXDeg: number; gyroYDeg: number; gyroZDeg: number; - }; // TODO: Update type + }; }; fseq: { fileList: Fseq[]; @@ -93,6 +97,40 @@ const mock: { theme: 'dark', }, }, + info: { + system: { + socInfo: { + chipModel: 'ESP32', + chipRevision: 1, + cpuCores: 2, + cpuClock: 240000000, + freeHeap: 265332, + flashSize: 4194304, + flashSpeed: 40000000, + sketchSize: 1161168, + freeSketchSpace: 1966080, + }, + tlSystemInfo: { + rps: 60, + fps: 60, + ledCount: 720, + }, + hardwareInfo: { + regulatorCount: 2, + regulatorVoltage: 5, + regulatorCurrentLimit: 6, + regulatorCurrentDraw: 2.5, + regulatorPowerLimit: 30, + regulatorPowerDraw: 15, + regulatorTemperature: 50, + fanSpeed: 0, + mpu6050: 1, + ds18b20: 2, + bh1750: 1, + audioUnit: 1, + }, + }, + }, motion: { motionSensorCalibration: { accXRaw: 0, @@ -132,6 +170,18 @@ export const handlers = [ res(ctx.status(200), ctx.json({ status: 200, message: 'ok' })), ), + // ------------------ + // System Information + // ------------------ + + rest.get('/api/info/system', (_req, res, ctx) => { + console.debug(`Get system information: ${mock.info.system}`); + return res( + ctx.status(200), + ctx.json({ status: 200, message: 'ok', ...mock.info.system }), + ); + }), + // -------------------- // System Configuration // -------------------- @@ -186,9 +236,9 @@ export const handlers = [ return res(ctx.status(200), ctx.json({ status: 200, message: 'ok' })); }), - // ------------------ + // ---------------- // UI Configuration - // ------------------ + // ---------------- rest.get('/api/config/ui', (_req, res, ctx) => { console.debug(`Get UI configuration: ${mock.ui}`); @@ -204,9 +254,9 @@ export const handlers = [ return res(ctx.status(200), ctx.json({ status: 200, message: 'ok' })); }), - // ----- + // --------------------------------------- // Motion Sensor Calibration Configuration - // ----- + // --------------------------------------- rest.get('/api/config/motion', (_req, res, ctx) => { console.debug(`Get motion configuration: ${mock.motion}`); diff --git a/ui/src/pages/CustomAnimations.tsx b/ui/src/pages/CustomAnimations.tsx index aedd087..f6ab9e8 100644 --- a/ui/src/pages/CustomAnimations.tsx +++ b/ui/src/pages/CustomAnimations.tsx @@ -153,13 +153,22 @@ const Form = (): JSX.Element => { {isUploadSuccess && ( )} + {isDeleteSuccess && ( )} - {isUploadError && } - {isDeleteError && } - {isUpdateError && } + {isUploadError && ( + + )} + + {isDeleteError && ( + + )} + + {isUpdateError && ( + + )} {isUploadLoading && } diff --git a/ui/src/pages/Settings.tsx b/ui/src/pages/Settings.tsx index 4448160..f964ddd 100644 --- a/ui/src/pages/Settings.tsx +++ b/ui/src/pages/Settings.tsx @@ -20,6 +20,7 @@ import i18n from '../i18n'; import { changeTheme, toPercentage } from '../libs'; import { useSystem, + useSystemInfo, useUi, useUpdateSystem, useUpdateUi, @@ -112,6 +113,7 @@ const DEFAULT_VALUES: FormData = { const Form = (): JSX.Element => { const { t } = useTranslation(); const { data: system } = useSystem(); + const { data: systemInfo, refetch: refetchSystemInfo } = useSystemInfo(); const { data: wifi } = useWifi(); const { data: ui } = useUi(); const { @@ -184,13 +186,20 @@ const Form = (): JSX.Element => { }, ); - await mutateAsyncSystem({ - ...systemCopy, - ...rest, - fanMode: Number(fanMode), - lightSensorMode: Number(lightSensorMode), - logLevel: Number(logLevel), - }); + await mutateAsyncSystem( + { + ...systemCopy, + ...rest, + fanMode: Number(fanMode), + lightSensorMode: Number(lightSensorMode), + logLevel: Number(logLevel), + }, + { + onSuccess: async () => { + await refetchSystemInfo(); + }, + }, + ); // Update WiFi configuration only if there was a change. if (formState.dirtyFields.wifi) { @@ -206,6 +215,31 @@ const Form = (): JSX.Element => { return await onSubmit(); }; + const getAvailablePowerModes = () => { + const hasMPU6050 = (systemInfo?.hardwareInfo.mpu6050 ?? 0) > 0; + const hasBH1750 = (systemInfo?.hardwareInfo.bh1750 ?? 0) > 0; + + return Object.entries(LightSensorMode) + .filter(([key]) => isNaN(Number(key))) + .filter( + ([, value]) => + value !== LightSensorMode.AutomaticOnOffMPU6050 || + (value === LightSensorMode.AutomaticOnOffMPU6050 && hasMPU6050), + ) + .filter( + ([, value]) => + value !== LightSensorMode.AutomaticBrightnessBH1750 || + (value === LightSensorMode.AutomaticBrightnessBH1750 && hasBH1750), + ) + .filter( + ([, value]) => + value !== LightSensorMode.AutomaticOnOffBH1750 || + (value === LightSensorMode.AutomaticOnOffBH1750 && hasBH1750), + ); + }; + + const hasTemperatureSensors = (systemInfo?.hardwareInfo.ds18b20 ?? 0) > 0; + return ( <> {isSystemSuccess && @@ -214,9 +248,23 @@ const Form = (): JSX.Element => { )} - {isSystemError && } - {isWifiError && } - {isUiError && } + {isSystemError && ( + + )} + + {isWifiError && ( + + )} + + {isUiError && } + + {Number(watch('system.fanMode')) === FanMode.Automatic && + !hasTemperatureSensors && ( + + )}
@@ -229,13 +277,11 @@ const Form = (): JSX.Element => {
control={control} name="system.lightSensorMode"> - {Object.entries(LightSensorMode) - .filter(([key]) => isNaN(Number(key))) - .map(([key, value]) => ( - - {t(`settings.lightSensorModes.${key}`)} - - ))} + {getAvailablePowerModes().map(([key, value]) => ( + + {t(`settings.lightSensorModes.${key}`)} + + ))}
@@ -443,66 +489,67 @@ const Form = (): JSX.Element => {
- {FanMode.Automatic === Number(watch('system.fanMode')) && ( - <> - - - - - - )} + {FanMode.Automatic === Number(watch('system.fanMode')) && + hasTemperatureSensors && ( + <> + + + + + + )}
diff --git a/ui/src/pages/Update.tsx b/ui/src/pages/Update.tsx index 1fcf554..7e57541 100644 --- a/ui/src/pages/Update.tsx +++ b/ui/src/pages/Update.tsx @@ -29,7 +29,7 @@ export const Update = (): JSX.Element => { <> {isSuccess && } - {isError && } + {isError && } {isLoading && } diff --git a/ui/src/pages/Zone.tsx b/ui/src/pages/Zone.tsx index 2e016d8..0ca7e30 100644 --- a/ui/src/pages/Zone.tsx +++ b/ui/src/pages/Zone.tsx @@ -24,7 +24,7 @@ import { Toast, } from '../components'; import { toPercentage } from '../libs'; -import { useLed, useUpdateLed } from '../pages/api'; +import { useLed, useSystemInfo, useUpdateLed } from '../pages/api'; type FormData = { animationSettings: [ @@ -88,6 +88,7 @@ type FormProps = { zoneId: number }; const Form = ({ zoneId }: FormProps): JSX.Element => { const { t } = useTranslation(); + const { data: systemInfo } = useSystemInfo(); const { data } = useLed(); const { mutateAsync, isSuccess, isError, error } = useUpdateLed(); const zone = data?.[zoneId]; @@ -229,11 +230,29 @@ const Form = ({ zoneId }: FormProps): JSX.Element => { } }; + const getAvailableAnimationTypes = () => { + const hasMPU6050 = (systemInfo?.hardwareInfo.mpu6050 ?? 0) > 0; + + return Object.entries(AnimationType) + .filter(([key]) => isNaN(Number(key))) + .filter(([, value]) => value !== AnimationType.FSEQ) + .filter( + ([, value]) => + value !== AnimationType.RainbowMotion || + (value === AnimationType.RainbowMotion && hasMPU6050), + ) + .filter( + ([, value]) => + value !== AnimationType.GradientMotion || + (value === AnimationType.GradientMotion && hasMPU6050), + ); + }; + return ( <> {isSuccess && } - {isError && } + {isError && }
@@ -251,14 +270,11 @@ const Form = ({ zoneId }: FormProps): JSX.Element => { name="type" onValueChange={onTypeChange} > - {Object.entries(AnimationType) - .filter(([key]) => isNaN(Number(key))) - .filter(([, value]) => value !== AnimationType.FSEQ) - .map(([key, value]) => ( - - {t(`zone.animationTypes.${key}`)} - - ))} + {getAvailableAnimationTypes().map(([key, value]) => ( + + {t(`zone.animationTypes.${key}`)} + + ))} diff --git a/ui/src/pages/api/index.ts b/ui/src/pages/api/index.ts index 24432eb..256bfc4 100644 --- a/ui/src/pages/api/index.ts +++ b/ui/src/pages/api/index.ts @@ -2,6 +2,7 @@ export * from './fseq'; export * from './led'; export * from './log'; export * from './system'; +export * from './systemInfo'; export * from './ui'; export * from './upload'; export * from './wifi'; diff --git a/ui/src/pages/api/systemInfo.ts b/ui/src/pages/api/systemInfo.ts new file mode 100644 index 0000000..eaf7d13 --- /dev/null +++ b/ui/src/pages/api/systemInfo.ts @@ -0,0 +1,58 @@ +import { useQuery, UseQueryOptions } from '@tanstack/react-query'; +import ky from 'ky'; + +const API_URL = '/api/info/system'; + +export type SystemInfo = { + socInfo: { + chipModel: string; + chipRevision: number; + cpuCores: number; + cpuClock: number; + freeHeap: number; + flashSize: number; + flashSpeed: number; + sketchSize: number; + freeSketchSpace: number; + }; + tlSystemInfo: { + rps: number; + fps: number; + ledCount: number; + }; + hardwareInfo: { + regulatorCount: number; + regulatorVoltage: number; + regulatorCurrentLimit: number; + regulatorCurrentDraw: number; + regulatorPowerLimit: number; + regulatorPowerDraw: number; + regulatorTemperature: number; + fanSpeed: number; + mpu6050: number; + ds18b20: number; + bh1750: number; + audioUnit: number; + }; +}; + +type Response = { + status: number; + message: string; +}; + +type DataResponse = SystemInfo & Response; + +export const useSystemInfo = ( + options?: UseQueryOptions, +) => + useQuery({ + queryKey: [API_URL], + queryFn: async () => await ky.get(API_URL).json(), + select: (data) => ({ + hardwareInfo: data.hardwareInfo, + socInfo: data.socInfo, + tlSystemInfo: data.tlSystemInfo, + }), + ...options, + });