From d06dc3e2cff552beaf15fd9fbfcb436fe4839a43 Mon Sep 17 00:00:00 2001 From: proddy Date: Sat, 29 Apr 2023 14:34:56 +0200 Subject: [PATCH] fic dialog deviceentities --- interface/src/project/DashboardDevices.tsx | 89 +++++-------------- .../src/project/DashboardDevicesDialog.tsx | 53 +++++++---- interface/src/project/deviceValue.ts | 85 ++++++++++++++++++ interface/src/project/validators.ts | 15 ++++ mock-api/server.js | 48 +++++----- 5 files changed, 183 insertions(+), 107 deletions(-) create mode 100644 interface/src/project/deviceValue.ts diff --git a/interface/src/project/DashboardDevices.tsx b/interface/src/project/DashboardDevices.tsx index 33088ebbd..1760f22b1 100644 --- a/interface/src/project/DashboardDevices.tsx +++ b/interface/src/project/DashboardDevices.tsx @@ -27,7 +27,7 @@ import { useRowSelect } from '@table-library/react-table-library/select'; import { useSort, SortToggleType } from '@table-library/react-table-library/sort'; import { Table, Header, HeaderRow, HeaderCell, Body, Row, Cell } from '@table-library/react-table-library/table'; import { useTheme } from '@table-library/react-table-library/theme'; -import { useState, useContext, useCallback, useEffect } from 'react'; +import { useState, useContext, useEffect } from 'react'; import { IconContext } from 'react-icons'; import { toast } from 'react-toastify'; @@ -35,8 +35,10 @@ import DashboarDevicesDialog from './DashboardDevicesDialog'; import DeviceIcon from './DeviceIcon'; import * as EMSESP from './api'; +import { formatValue, isNumberUOM } from './deviceValue'; -import { DeviceValueUOM, DeviceValueUOM_s, DeviceEntityMask } from './types'; +import { DeviceValueUOM_s, DeviceEntityMask } from './types'; +import { deviceValueItemValidation } from './validators'; import type { Device, CoreData, DeviceData, DeviceValue } from './types'; import type { FC } from 'react'; import { ButtonRow, SectionContent, MessageBox } from 'components'; @@ -174,19 +176,12 @@ const DashboardDevices: FC = () => { }, sortToggleType: SortToggleType.AlternateWithReset, sortFns: { - NAME: (array) => array.sort((a, b) => a.id.slice(2).localeCompare(b.id.slice(2))), + NAME: (array) => array.sort((a, b) => a.id.toString().slice(2).localeCompare(b.id.toString().slice(2))), VALUE: (array) => array.sort((a, b) => a.v.toString().localeCompare(b.v.toString())) } } ); - const device_select = useRowSelect( - { nodes: coreData.devices }, - { - onChange: onSelectChange - } - ); - const fetchDeviceData = async (id: number) => { try { setDeviceData((await EMSESP.readDeviceData({ id })).data); @@ -195,13 +190,13 @@ const DashboardDevices: FC = () => { } }; - const fetchCoreData = useCallback(async () => { + const fetchCoreData = async () => { try { setCoreData((await EMSESP.readCoreData()).data); } catch (error) { toast.error(extractErrorMessage(error, LL.PROBLEM_LOADING())); } - }, [LL]); + }; useEffect(() => { void fetchCoreData(); @@ -216,10 +211,19 @@ const DashboardDevices: FC = () => { }; function onSelectChange(action: any, state: any) { - setSelectedDevice(device_select.state.id); - refreshData(); + setSelectedDevice(state.id); + if (action.type === 'ADD_BY_ID_EXCLUSIVELY') { + void fetchDeviceData(state.id); + } } + const device_select = useRowSelect( + { nodes: coreData.devices }, + { + onChange: onSelectChange + } + ); + const escapeCsvCell = (cell: any) => { if (cell == null) { return ''; @@ -278,57 +282,6 @@ const DashboardDevices: FC = () => { }; }, []); - const isCmdOnly = (dv: DeviceValue) => dv.v === '' && dv.c; - - const formatDurationMin = (duration_min: number) => { - const days = Math.trunc((duration_min * 60000) / 86400000); - const hours = Math.trunc((duration_min * 60000) / 3600000) % 24; - const minutes = Math.trunc((duration_min * 60000) / 60000) % 60; - - let formatted = ''; - if (days) { - formatted += LL.NUM_DAYS({ num: days }) + ' '; - } - if (hours) { - formatted += LL.NUM_HOURS({ num: hours }) + ' '; - } - if (minutes) { - formatted += LL.NUM_MINUTES({ num: minutes }); - } - return formatted; - }; - - function formatValue(value: any, uom: number) { - if (value === undefined) { - return ''; - } - switch (uom) { - case DeviceValueUOM.HOURS: - return value ? formatDurationMin(value * 60) : LL.NUM_HOURS({ num: 0 }); - case DeviceValueUOM.MINUTES: - return value ? formatDurationMin(value) : LL.NUM_MINUTES({ num: 0 }); - case DeviceValueUOM.SECONDS: - return LL.NUM_SECONDS({ num: value }); - case DeviceValueUOM.NONE: - if (typeof value === 'number') { - return new Intl.NumberFormat().format(value); - } - return value; - case DeviceValueUOM.DEGREES: - case DeviceValueUOM.DEGREES_R: - case DeviceValueUOM.FAHRENHEIT: - return ( - new Intl.NumberFormat(undefined, { - minimumFractionDigits: 1 - }).format(value) + - ' ' + - DeviceValueUOM_s[uom] - ); - default: - return new Intl.NumberFormat().format(value) + ' ' + DeviceValueUOM_s[uom]; - } - } - const deviceValueDialogSave = async (dv: DeviceValue) => { try { const response = await EMSESP.writeDeviceValue({ @@ -515,11 +468,11 @@ const DashboardDevices: FC = () => { {tableList.map((dv: DeviceValue) => ( sendCommand(dv)}> {renderNameCell(dv)} - {formatValue(dv.v, dv.u)} + {formatValue(LL, dv.v, dv.u)} {dv.c && me.admin && !hasMask(dv.id, DeviceEntityMask.DV_READONLY) && ( sendCommand(dv)}> - {isCmdOnly(dv) ? ( + {dv.v === '' && dv.c ? ( ) : ( @@ -542,7 +495,6 @@ const DashboardDevices: FC = () => { {renderCoreData()} {renderDeviceData()} {renderDeviceDetails()} - {console.log('redndering device data')} {selectedDeviceValue && ( { onClose={deviceValueDialogClose} onSave={deviceValueDialogSave} selectedItem={selectedDeviceValue} + validator={deviceValueItemValidation(isNumberUOM(selectedDeviceValue.u))} /> )} diff --git a/interface/src/project/DashboardDevicesDialog.tsx b/interface/src/project/DashboardDevicesDialog.tsx index c9670c1da..9631d0335 100644 --- a/interface/src/project/DashboardDevicesDialog.tsx +++ b/interface/src/project/DashboardDevicesDialog.tsx @@ -11,31 +11,48 @@ import { MenuItem, TextField, FormHelperText, - Grid + Grid, + Box, + Typography } from '@mui/material'; import { useState, useEffect } from 'react'; +import { formatValueNoUOM } from './deviceValue'; import { DeviceValueUOM, DeviceValueUOM_s } from './types'; import type { DeviceValue } from './types'; +import type Schema from 'async-validator'; +import type { ValidateFieldsError } from 'async-validator'; +import { ValidatedTextField } from 'components'; import { useI18nContext } from 'i18n/i18n-react'; import { updateValue } from 'utils'; +import { validate } from 'validators'; + type DashboardDevicesDialogProps = { open: boolean; onClose: () => void; onSave: (as: DeviceValue) => void; selectedItem: DeviceValue; + validator: Schema; }; -const DashboarDevicesDialog = ({ open, onClose, onSave, selectedItem }: DashboardDevicesDialogProps) => { +const DashboarDevicesDialog = ({ open, onClose, onSave, selectedItem, validator }: DashboardDevicesDialogProps) => { const { LL } = useI18nContext(); const [editItem, setEditItem] = useState(selectedItem); + const [fieldErrors, setFieldErrors] = useState(); + const updateFormValue = updateValue(setEditItem); useEffect(() => { if (open) { + setFieldErrors(undefined); setEditItem(selectedItem); + // format value and convert to string + setEditItem({ + ...selectedItem, + v: formatValueNoUOM(selectedItem.v, selectedItem.u) + }); } }, [open, selectedItem]); @@ -43,12 +60,16 @@ const DashboarDevicesDialog = ({ open, onClose, onSave, selectedItem }: Dashboar onClose(); }; - const save = () => { - onSave(editItem); + const save = async () => { + try { + setFieldErrors(undefined); + await validate(validator, editItem); + onSave(editItem); + } catch (errors: any) { + setFieldErrors(errors); + } }; - const isCmdOnly = (dv: DeviceValue) => dv.v === '' && dv.c; - const setUom = (uom: number) => { switch (uom) { case DeviceValueUOM.HOURS: @@ -64,16 +85,17 @@ const DashboarDevicesDialog = ({ open, onClose, onSave, selectedItem }: Dashboar return ( - - {isCmdOnly(editItem) ? LL.RUN_COMMAND() : LL.CHANGE_VALUE()} - + {selectedItem.v === '' && selectedItem.c ? LL.RUN_COMMAND() : LL.CHANGE_VALUE()} + + {editItem.id.slice(2)} + {editItem.l && ( )} {!editItem.l && ( - {setUom(editItem.u)} }} diff --git a/interface/src/project/deviceValue.ts b/interface/src/project/deviceValue.ts new file mode 100644 index 000000000..0c1088349 --- /dev/null +++ b/interface/src/project/deviceValue.ts @@ -0,0 +1,85 @@ +import { DeviceValueUOM, DeviceValueUOM_s } from './types'; +import type { TranslationFunctions } from 'i18n/i18n-types'; + +const formatDurationMin = (LL: TranslationFunctions, duration_min: number) => { + const days = Math.trunc((duration_min * 60000) / 86400000); + const hours = Math.trunc((duration_min * 60000) / 3600000) % 24; + const minutes = Math.trunc((duration_min * 60000) / 60000) % 60; + + let formatted = ''; + if (days) { + formatted += LL.NUM_DAYS({ num: days }); + } + + if (hours) { + if (formatted) formatted += ' '; + formatted += LL.NUM_HOURS({ num: hours }); + } + + if (minutes) { + if (formatted) formatted += ' '; + formatted += LL.NUM_MINUTES({ num: minutes }); + } + + return formatted; +}; + +export function formatValue(LL: TranslationFunctions, value: any, uom: number) { + if (value === undefined) { + return ''; + } + switch (uom) { + case DeviceValueUOM.HOURS: + return value ? formatDurationMin(LL, value * 60) : LL.NUM_HOURS({ num: 0 }); + case DeviceValueUOM.MINUTES: + return value ? formatDurationMin(LL, value) : LL.NUM_MINUTES({ num: 0 }); + case DeviceValueUOM.SECONDS: + return LL.NUM_SECONDS({ num: value }); + case DeviceValueUOM.NONE: + if (typeof value === 'number') { + return new Intl.NumberFormat().format(value); + } + return value; + case DeviceValueUOM.DEGREES: + case DeviceValueUOM.DEGREES_R: + case DeviceValueUOM.FAHRENHEIT: + return ( + new Intl.NumberFormat(undefined, { + minimumFractionDigits: 1 + }).format(value) + + ' ' + + DeviceValueUOM_s[uom] + ); + default: + return new Intl.NumberFormat().format(value) + ' ' + DeviceValueUOM_s[uom]; + } +} + +export const formatValueNoUOM = (value: any, uom: number) => { + if (value === undefined) { + return ''; + } + + switch (uom) { + case DeviceValueUOM.NONE: + if (typeof value === 'number') { + return new Intl.NumberFormat().format(value); + } + return value; + case DeviceValueUOM.DEGREES: + case DeviceValueUOM.DEGREES_R: + case DeviceValueUOM.FAHRENHEIT: + return new Intl.NumberFormat(undefined, { + minimumFractionDigits: 1 + }).format(Number(value)); + default: + return value; + } +}; + +export function isNumberUOM(uom: number) { + if (uom === DeviceValueUOM.NONE) { + return false; + } + return true; +} diff --git a/interface/src/project/validators.ts b/interface/src/project/validators.ts index 33f958da5..d9b65bf8a 100644 --- a/interface/src/project/validators.ts +++ b/interface/src/project/validators.ts @@ -161,3 +161,18 @@ export const analogSensorItemValidation = (sensors: AnalogSensor[], creating: bo ...(creating ? [isGPIOUniqueValidator(sensors)] : []) ] }); + +export const deviceValueItemValidation = (isNumber: boolean) => + new Schema({ + v: [ + { required: true, message: 'Value is required' }, + { + validator(rule: InternalRuleItem, value: string, callback: (error?: string) => void) { + if (isNumber && isNaN(+value)) { + callback('Not a valid number'); + } + callback(); + } + } + ] + }); diff --git a/mock-api/server.js b/mock-api/server.js index d91e23327..7016c4757 100644 --- a/mock-api/server.js +++ b/mock-api/server.js @@ -488,7 +488,8 @@ const emsesp_devicedata_1 = { v: 'auto', u: 0, id: '00hc1 mode', - c: 'hc1/mode' + c: 'hc1/mode', + l: ['off', 'on', 'auto'] } ] }; @@ -497,8 +498,8 @@ const emsesp_devicedata_2 = { label: 'Boiler: Nefit GBx72/Trendline/Cerapur/Greenstar Si/27i', data: [ { v: '', u: 0, id: '08reset', c: 'reset', l: ['-', 'maintenance', 'error'] }, - { v: 'false', u: 0, id: '08heating active' }, - { v: 'false', u: 0, id: '04tapwater active' }, + { v: 'off', u: 0, id: '08heating active' }, + { v: 'off', u: 0, id: '04tapwater active' }, { v: 5, u: 1, id: '04selected flow temperature', c: 'selflowtemp' }, { v: 0, u: 3, id: '0Eburner selected max power', c: 'selburnpow' }, { v: 0, u: 3, id: '00heating pump modulation' }, @@ -506,14 +507,14 @@ const emsesp_devicedata_2 = { { v: 52.7, u: 1, id: '00return temperature' }, { v: 1.3, u: 10, id: '00system pressure' }, { v: 54.9, u: 1, id: '00actual boiler temperature' }, - { v: 'false', u: 0, id: '00gas' }, - { v: 'false', u: 0, id: '00gas stage 2' }, + { v: 'off', u: 0, id: '00gas' }, + { v: 'off', u: 0, id: '00gas stage 2' }, { v: 0, u: 9, id: '00flame current' }, - { v: 'false', u: 0, id: '00heating pump' }, - { v: 'false', u: 0, id: '00fan' }, - { v: 'false', u: 0, id: '00ignition' }, - { v: 'false', u: 0, id: '00oil preheating' }, - { v: 'true', u: 0, id: '00heating activated', c: 'heatingactivated', l: ['off', 'on'] }, + { v: 'off', u: 0, id: '00heating pump' }, + { v: 'off', u: 0, id: '00fan' }, + { v: 'off', u: 0, id: '00ignition' }, + { v: 'off', u: 0, id: '00oil preheating' }, + { v: 'on', u: 0, id: '00heating activated', c: 'heatingactivated', l: ['off', 'on'] }, { v: 80, u: 1, id: '00heating temperature', c: 'heatingtemp' }, { v: 70, u: 3, id: '00burner pump max power', c: 'pumpmodmax' }, { v: 30, u: 3, id: '00burner pump min power', c: 'pumpmodmin' }, @@ -537,14 +538,14 @@ const emsesp_devicedata_2 = { { v: 'manual', u: 0, id: '00maintenance scheduled', c: 'maintenance', l: ['off', 'time', 'date', 'manual'] }, { v: 6000, u: 7, id: '00time to next maintenance', c: 'maintenancetime' }, { v: '01.01.2012', u: 0, id: '00next maintenance date', c: 'maintenancedate', o: 'Format: < dd.mm.yyyy >' }, - { v: 'true', u: 0, id: '00dhw turn on/off', c: 'wwtapactivated', l: ['off', 'on'] }, + { v: 'on', u: 0, id: '00dhw turn on/off', c: 'wwtapactivated', l: ['off', 'on'] }, { v: 62, u: 1, id: '00dhw set temperature' }, { v: 60, u: 1, id: '00dhw selected temperature', c: 'wwseltemp' }, { v: 'flow', u: 0, id: '00dhw type' }, { v: 'hot', u: 0, id: '00dhw comfort', c: 'wwcomfort', l: ['hot', 'eco', 'intelligent'] }, { v: 40, u: 2, id: '00dhw flow temperature offset', c: 'wwflowtempoffset' }, { v: 100, u: 3, id: '00dhw max power', c: 'wwmaxpower' }, - { v: 'false', u: 0, id: '00dhw circulation pump available', c: 'wwcircpump', l: ['off', 'on'] }, + { v: 'off', u: 0, id: '00dhw circulation pump available', c: 'wwcircpump', l: ['off', 'on'] }, { v: '3-way valve', u: 0, id: '00dhw charging type' }, { v: -5, u: 2, id: '00dhw hysteresis on temperature', c: 'wwhyston' }, { v: 0, u: 2, id: '00dhw hysteresis off temperature', c: 'wwhystoff' }, @@ -556,18 +557,18 @@ const emsesp_devicedata_2 = { c: 'wwcircmode', l: ['off', '1x3min', '2x3min', '3x3min', '4x3min', '5x3min', '6x3min', 'continuous'] }, - { v: 'false', u: 0, id: '00dhw circulation active', c: 'wwcirc', l: ['off', 'on'] }, + { v: 'off', u: 0, id: '00dhw circulation active', c: 'wwcirc', l: ['off', 'on'] }, { v: 47.3, u: 1, id: '00dhw current intern temperature' }, { v: 0, u: 4, id: '00dhw current tap water flow' }, { v: 47.3, u: 1, id: '00dhw storage intern temperature' }, - { v: 'true', u: 0, id: '00dhw activated', c: 'wwactivated', l: ['off', 'on'] }, - { v: 'false', u: 0, id: '00dhw one time charging', c: 'wwonetime', l: ['off', 'on'] }, - { v: 'false', u: 0, id: '00dhw disinfecting', c: 'wwdisinfecting', l: ['off', 'on'] }, - { v: 'false', u: 0, id: '00dhw charging' }, - { v: 'false', u: 0, id: '00dhw recharging' }, - { v: 'true', u: 0, id: '00dhw temperature ok' }, - { v: 'false', u: 0, id: '00dhw active' }, - { v: 'true', u: 0, id: '00dhw 3way valve active' }, + { v: 'on', u: 0, id: '00dhw activated', c: 'wwactivated', l: ['off', 'on'] }, + { v: 'off', u: 0, id: '00dhw one time charging', c: 'wwonetime', l: ['off', 'on'] }, + { v: 'off', u: 0, id: '00dhw disinfecting', c: 'wwdisinfecting', l: ['off', 'on'] }, + { v: 'off', u: 0, id: '00dhw charging' }, + { v: 'off', u: 0, id: '00dhw recharging' }, + { v: 'on', u: 0, id: '00dhw temperature ok' }, + { v: 'off', u: 0, id: '00dhw active' }, + { v: 'on', u: 0, id: '00dhw 3way valve active' }, { v: 0, u: 3, id: '00dhw set pump power' }, { v: 288768, u: 0, id: '00dhw starts' }, { v: 102151, u: 8, id: '00dhw active time' } @@ -593,7 +594,8 @@ const emsesp_devicedata_4 = { v: 'off', u: 0, id: '02hc2 mode', - c: 'hc2/mode' + c: 'hc2/mode', + l: ['off', 'on', 'auto'] } ] }; @@ -963,7 +965,7 @@ rest_server.get(EMSESP_CORE_DATA_ENDPOINT, (req, res) => { }); rest_server.get(EMSESP_SENSOR_DATA_ENDPOINT, (req, res) => { console.log('send back sensor data...'); - console.log(emsesp_sensordata); + // console.log(emsesp_sensordata); res.json(emsesp_sensordata); }); rest_server.get(EMSESP_DEVICES_ENDPOINT, (req, res) => {