mirror of
https://github.com/emsesp/EMS-ESP32.git
synced 2025-12-07 08:19:52 +03:00
fic dialog deviceentities
This commit is contained in:
@@ -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) => (
|
||||
<Row key={dv.id} item={dv} onClick={() => sendCommand(dv)}>
|
||||
<Cell>{renderNameCell(dv)}</Cell>
|
||||
<Cell>{formatValue(dv.v, dv.u)}</Cell>
|
||||
<Cell>{formatValue(LL, dv.v, dv.u)}</Cell>
|
||||
<Cell stiff>
|
||||
{dv.c && me.admin && !hasMask(dv.id, DeviceEntityMask.DV_READONLY) && (
|
||||
<IconButton size="small" onClick={() => sendCommand(dv)}>
|
||||
{isCmdOnly(dv) ? (
|
||||
{dv.v === '' && dv.c ? (
|
||||
<PlayArrowIcon color="primary" sx={{ fontSize: 16 }} />
|
||||
) : (
|
||||
<EditIcon color="primary" sx={{ fontSize: 16 }} />
|
||||
@@ -542,7 +495,6 @@ const DashboardDevices: FC = () => {
|
||||
{renderCoreData()}
|
||||
{renderDeviceData()}
|
||||
{renderDeviceDetails()}
|
||||
{console.log('redndering device data')}
|
||||
|
||||
{selectedDeviceValue && (
|
||||
<DashboarDevicesDialog
|
||||
@@ -550,6 +502,7 @@ const DashboardDevices: FC = () => {
|
||||
onClose={deviceValueDialogClose}
|
||||
onSave={deviceValueDialogSave}
|
||||
selectedItem={selectedDeviceValue}
|
||||
validator={deviceValueItemValidation(isNumberUOM(selectedDeviceValue.u))}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -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<DeviceValue>(selectedItem);
|
||||
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
||||
|
||||
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 (
|
||||
<Dialog open={open} onClose={close}>
|
||||
<DialogTitle>
|
||||
<DialogTitle>{isCmdOnly(editItem) ? LL.RUN_COMMAND() : LL.CHANGE_VALUE()}</DialogTitle>
|
||||
</DialogTitle>
|
||||
<DialogTitle>{selectedItem.v === '' && selectedItem.c ? LL.RUN_COMMAND() : LL.CHANGE_VALUE()}</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
<Box color="warning.main" p={0} pl={0} pr={0} mt={0} mb={2}>
|
||||
<Typography variant="body2">{editItem.id.slice(2)}</Typography>
|
||||
</Box>
|
||||
<Grid container spacing={1}>
|
||||
<Grid item>
|
||||
{editItem.l && (
|
||||
<TextField
|
||||
name="v"
|
||||
label={editItem.id.slice(2)}
|
||||
label={LL.VALUE(1)}
|
||||
value={editItem.v}
|
||||
autoFocus
|
||||
sx={{ width: '30ch' }}
|
||||
@@ -88,16 +110,15 @@ const DashboarDevicesDialog = ({ open, onClose, onSave, selectedItem }: Dashboar
|
||||
</TextField>
|
||||
)}
|
||||
{!editItem.l && (
|
||||
<TextField
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="v"
|
||||
label={editItem.id.slice(2)}
|
||||
value={typeof editItem.v === 'number' ? Math.round(editItem.v * 10) / 10 : editItem.v}
|
||||
label={LL.VALUE(1)}
|
||||
value={editItem.v}
|
||||
autoFocus
|
||||
multiline={editItem.u ? false : true}
|
||||
sx={{ width: '30ch' }}
|
||||
type={editItem.u ? 'number' : 'text'}
|
||||
multiline={editItem.u ? false : true}
|
||||
onChange={updateFormValue}
|
||||
inputProps={editItem.u ? { min: editItem.m, max: editItem.x, step: editItem.s } : {}}
|
||||
InputProps={{
|
||||
startAdornment: <InputAdornment position="start">{setUom(editItem.u)}</InputAdornment>
|
||||
}}
|
||||
|
||||
85
interface/src/project/deviceValue.ts
Normal file
85
interface/src/project/deviceValue.ts
Normal file
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user