mirror of
https://github.com/emsesp/EMS-ESP32.git
synced 2025-12-07 16:29:51 +03:00
Optimize WebUI rendering when using Dialog Boxes #1116
This commit is contained in:
@@ -1,7 +1,10 @@
|
||||
import { Tab } from '@mui/material';
|
||||
import { Navigate, Route, Routes } from 'react-router-dom';
|
||||
import DashboardData from './DashboardData';
|
||||
|
||||
import DashboardDevices from './DashboardDevices';
|
||||
import DashboardSensors from './DashboardSensors';
|
||||
import DashboardStatus from './DashboardStatus';
|
||||
|
||||
import type { FC } from 'react';
|
||||
|
||||
import { RouterTabs, useRouterTab, useLayoutTitle } from 'components';
|
||||
@@ -17,13 +20,15 @@ const Dashboard: FC = () => {
|
||||
return (
|
||||
<>
|
||||
<RouterTabs value={routerTab}>
|
||||
<Tab value="data" label={LL.DEVICES_SENSORS()} />
|
||||
<Tab value="devices" label={LL.DEVICES()} />
|
||||
<Tab value="sensors" label={LL.SENSORS()} />
|
||||
<Tab value="status" label="Status" />
|
||||
</RouterTabs>
|
||||
<Routes>
|
||||
<Route path="data" element={<DashboardData />} />
|
||||
<Route path="devices" element={<DashboardDevices />} />
|
||||
<Route path="sensors" element={<DashboardSensors />} />
|
||||
<Route path="status" element={<DashboardStatus />} />
|
||||
<Route path="/*" element={<Navigate replace to="data" />} />
|
||||
<Route path="/*" element={<Navigate replace to="devices" />} />
|
||||
</Routes>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -45,8 +45,8 @@ import DeviceIcon from './DeviceIcon';
|
||||
|
||||
import * as EMSESP from './api';
|
||||
|
||||
import { DeviceValueUOM, DeviceValueUOM_s, AnalogType, AnalogTypeNames, DeviceEntityMask } from './types';
|
||||
import type { SensorData, Device, CoreData, DeviceData, DeviceValue, Sensor, Analog } from './types';
|
||||
import { DeviceValueUOM, DeviceValueUOM_s, DeviceEntityMask } from './types';
|
||||
import type { SensorData, Device, CoreData, DeviceData, DeviceValue, TemperatureSensor, AnalogSensor } from './types';
|
||||
import type { FC } from 'react';
|
||||
import { ButtonRow, ValidatedTextField, SectionContent, MessageBox } from 'components';
|
||||
import { AuthenticatedContext } from 'contexts/authentication';
|
||||
@@ -54,26 +54,22 @@ import { AuthenticatedContext } from 'contexts/authentication';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
import { numberValue, updateValue, extractErrorMessage } from 'utils';
|
||||
|
||||
const DashboardData: FC = () => {
|
||||
const DashboardDevices: FC = () => {
|
||||
const { me } = useContext(AuthenticatedContext);
|
||||
|
||||
const { LL } = useI18nContext();
|
||||
|
||||
const [coreData, setCoreData] = useState<CoreData>({
|
||||
connected: true,
|
||||
devices: [],
|
||||
s_n: '',
|
||||
active_sensors: 0,
|
||||
analog_enabled: false
|
||||
});
|
||||
|
||||
const [deviceData, setDeviceData] = useState<DeviceData>({ label: '', data: [] });
|
||||
const [sensorData, setSensorData] = useState<SensorData>({ sensors: [], analogs: [] });
|
||||
const [deviceValue, setDeviceValue] = useState<DeviceValue>();
|
||||
const [sensor, setSensor] = useState<Sensor>();
|
||||
const [analog, setAnalog] = useState<Analog>();
|
||||
const [deviceDialog, setDeviceDialog] = useState<number>(-1);
|
||||
const [onlyFav, setOnlyFav] = useState(false);
|
||||
const [coreData, setCoreData] = useState<CoreData>({
|
||||
connected: true,
|
||||
devices: []
|
||||
});
|
||||
const [selectedDevice, setSelectedDevice] = useState<number>();
|
||||
|
||||
const [sensorData, setSensorData] = useState<SensorData>({ ts: [], as: [] });
|
||||
const [analog, setAnalog] = useState<AnalogSensor>();
|
||||
const [sensor, setSensor] = useState<TemperatureSensor>();
|
||||
|
||||
const common_theme = useTheme({
|
||||
BaseRow: `
|
||||
@@ -168,25 +164,6 @@ const DashboardData: FC = () => {
|
||||
}
|
||||
]);
|
||||
|
||||
const temperature_theme = useTheme([data_theme]);
|
||||
|
||||
const analog_theme = useTheme([
|
||||
data_theme,
|
||||
{
|
||||
Table: `
|
||||
--data-table-library_grid-template-columns: 80px repeat(1, minmax(0, 1fr)) 120px 100px 40px;
|
||||
`,
|
||||
BaseCell: `
|
||||
&:nth-of-type(2) {
|
||||
text-align: left;
|
||||
},
|
||||
&:nth-of-type(4) {
|
||||
text-align: right;
|
||||
}
|
||||
`
|
||||
}
|
||||
]);
|
||||
|
||||
const getSortIcon = (state: any, sortKey: any) => {
|
||||
if (state.sortKey === sortKey && state.reverse) {
|
||||
return <KeyboardArrowDownOutlinedIcon />;
|
||||
@@ -197,41 +174,6 @@ const DashboardData: FC = () => {
|
||||
return <UnfoldMoreOutlinedIcon />;
|
||||
};
|
||||
|
||||
const analog_sort = useSort(
|
||||
{ nodes: sensorData.analogs },
|
||||
{},
|
||||
{
|
||||
sortIcon: {
|
||||
iconDefault: <UnfoldMoreOutlinedIcon />,
|
||||
iconUp: <KeyboardArrowUpOutlinedIcon />,
|
||||
iconDown: <KeyboardArrowDownOutlinedIcon />
|
||||
},
|
||||
sortToggleType: SortToggleType.AlternateWithReset,
|
||||
sortFns: {
|
||||
GPIO: (array) => array.sort((a, b) => a.g - b.g),
|
||||
NAME: (array) => array.sort((a, b) => a.n.localeCompare(b.n)),
|
||||
TYPE: (array) => array.sort((a, b) => a.t - b.t)
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const sensor_sort = useSort(
|
||||
{ nodes: sensorData.sensors },
|
||||
{},
|
||||
{
|
||||
sortIcon: {
|
||||
iconDefault: <UnfoldMoreOutlinedIcon />,
|
||||
iconUp: <KeyboardArrowUpOutlinedIcon />,
|
||||
iconDown: <KeyboardArrowDownOutlinedIcon />
|
||||
},
|
||||
sortToggleType: SortToggleType.AlternateWithReset,
|
||||
sortFns: {
|
||||
NAME: (array) => array.sort((a, b) => a.n.localeCompare(b.n)),
|
||||
TEMPERATURE: (array) => array.sort((a, b) => a.t - b.t)
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const dv_sort = useSort(
|
||||
{ nodes: deviceData.data },
|
||||
{},
|
||||
@@ -256,18 +198,17 @@ const DashboardData: FC = () => {
|
||||
}
|
||||
);
|
||||
|
||||
const fetchSensorData = async () => {
|
||||
try {
|
||||
setSensorData((await EMSESP.readSensorData()).data);
|
||||
} catch (error) {
|
||||
toast.error(extractErrorMessage(error, LL.PROBLEM_LOADING()));
|
||||
}
|
||||
};
|
||||
// const fetchSensorData = async () => {
|
||||
// try {
|
||||
// setSensorData((await EMSESP.readSensorData()).data);
|
||||
// } catch (error) {
|
||||
// toast.error(extractErrorMessage(error, LL.PROBLEM_LOADING()));
|
||||
// }
|
||||
// };
|
||||
|
||||
const fetchDeviceData = async (id: string) => {
|
||||
const unique_id = parseInt(id);
|
||||
const fetchDeviceData = async (id: number) => {
|
||||
try {
|
||||
setDeviceData((await EMSESP.readDeviceData({ id: unique_id })).data);
|
||||
setDeviceData((await EMSESP.readDeviceData({ id })).data);
|
||||
} catch (error) {
|
||||
toast.error(extractErrorMessage(error, LL.PROBLEM_LOADING()));
|
||||
}
|
||||
@@ -285,29 +226,43 @@ const DashboardData: FC = () => {
|
||||
void fetchCoreData();
|
||||
}, [fetchCoreData]);
|
||||
|
||||
const refreshDataIndex = (selectedDevice: string) => {
|
||||
if (selectedDevice === 'sensor') {
|
||||
void fetchSensorData();
|
||||
return;
|
||||
}
|
||||
// const refreshDataIndex = (selectedDevice: string) => {
|
||||
// // if (selectedDevice === 'sensor') {
|
||||
// // void fetchSensorData();
|
||||
// // return;
|
||||
// // }
|
||||
|
||||
setSensorData({ sensors: [], analogs: [] });
|
||||
// // setSensorData({ sensors: [], analogs: [] });
|
||||
// if (selectedDevice) {
|
||||
// void fetchDeviceData(selectedDevice);
|
||||
// } else {
|
||||
// void fetchCoreData();
|
||||
// }
|
||||
// };
|
||||
|
||||
const refreshData = () => {
|
||||
// const selectedDevice = device_select.state.id;
|
||||
// if (selectedDevice === 'sensor') {
|
||||
// // void fetchSensorData();
|
||||
// return;
|
||||
// }
|
||||
|
||||
// setSensorData({ sensors: [], analogs: [] });
|
||||
if (selectedDevice) {
|
||||
void fetchDeviceData(selectedDevice);
|
||||
} else {
|
||||
void fetchCoreData();
|
||||
}
|
||||
};
|
||||
|
||||
const refreshData = () => {
|
||||
refreshDataIndex(device_select.state.id);
|
||||
// refreshDataIndex(device_select.state.id);
|
||||
};
|
||||
|
||||
function onSelectChange(action: any, state: any) {
|
||||
setSelectedDevice(device_select.state.id);
|
||||
if (action.type === 'ADD_BY_ID_EXCLUSIVELY') {
|
||||
refreshData();
|
||||
} else {
|
||||
setSensorData({ sensors: [], analogs: [] });
|
||||
// setSensorData({ sensors: [], analogs: [] });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -437,7 +392,7 @@ const DashboardData: FC = () => {
|
||||
const sendDeviceValue = async () => {
|
||||
if (deviceValue) {
|
||||
try {
|
||||
const response = await EMSESP.writeValue({
|
||||
const response = await EMSESP.writeDeviceValue({
|
||||
id: Number(device_select.state.id),
|
||||
devicevalue: deviceValue
|
||||
});
|
||||
@@ -523,100 +478,6 @@ const DashboardData: FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const addAnalogSensor = () => {
|
||||
setAnalog({ id: '0', g: 0, n: '', u: 0, v: 0, o: 0, t: 0, f: 1 });
|
||||
};
|
||||
|
||||
const sendSensor = async () => {
|
||||
if (sensor) {
|
||||
try {
|
||||
const response = await EMSESP.writeSensor({
|
||||
id: sensor.id,
|
||||
name: sensor.n,
|
||||
offset: sensor.o
|
||||
});
|
||||
if (response.status === 204) {
|
||||
toast.error(LL.UPLOAD_OF(LL.SENSOR()) + ' ' + LL.FAILED());
|
||||
} else if (response.status === 403) {
|
||||
toast.error(LL.ACCESS_DENIED());
|
||||
} else {
|
||||
toast.success(LL.UPDATED_OF(LL.SENSOR()));
|
||||
}
|
||||
setSensor(undefined);
|
||||
} catch (error) {
|
||||
toast.error(extractErrorMessage(error, LL.PROBLEM_UPDATING()));
|
||||
} finally {
|
||||
setSensor(undefined);
|
||||
await fetchSensorData();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const renderSensorDialog = () => {
|
||||
if (sensor) {
|
||||
return (
|
||||
<Dialog open={sensor !== undefined} onClose={() => setSensor(undefined)}>
|
||||
<DialogTitle>
|
||||
{LL.EDIT()} {LL.TEMP_SENSOR()}
|
||||
</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
<Box color="warning.main" p={0} pl={0} pr={0} mt={0} mb={2}>
|
||||
<Typography variant="body2">
|
||||
{LL.ID_OF(LL.SENSOR())}: {sensor.id}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Grid container spacing={1}>
|
||||
<Grid item>
|
||||
<ValidatedTextField
|
||||
name="n"
|
||||
label={LL.ENTITY_NAME()}
|
||||
value={sensor.n}
|
||||
autoFocus
|
||||
sx={{ width: '30ch' }}
|
||||
onChange={updateValue(setSensor)}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<ValidatedTextField
|
||||
name="o"
|
||||
label={LL.OFFSET()}
|
||||
value={numberValue(sensor.o)}
|
||||
sx={{ width: '12ch' }}
|
||||
type="number"
|
||||
variant="outlined"
|
||||
onChange={updateValue(setSensor)}
|
||||
inputProps={{ min: '-5', max: '5', step: '0.1' }}
|
||||
InputProps={{
|
||||
startAdornment: <InputAdornment position="start">°C</InputAdornment>
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button
|
||||
startIcon={<CancelIcon />}
|
||||
variant="outlined"
|
||||
onClick={() => setSensor(undefined)}
|
||||
color="secondary"
|
||||
>
|
||||
{LL.CANCEL()}
|
||||
</Button>
|
||||
<Button
|
||||
startIcon={<WarningIcon color="warning" />}
|
||||
variant="contained"
|
||||
type="submit"
|
||||
onClick={() => sendSensor()}
|
||||
color="info"
|
||||
>
|
||||
{LL.UPDATE()}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const renderDeviceDialog = () => {
|
||||
if (coreData && coreData.devices.length > 0 && deviceDialog !== -1) {
|
||||
return (
|
||||
@@ -692,21 +553,6 @@ const DashboardData: FC = () => {
|
||||
</Cell>
|
||||
</Row>
|
||||
))}
|
||||
{(coreData.active_sensors > 0 || coreData.analog_enabled) && (
|
||||
<Row key="sensor" item={{ id: 'sensor' }}>
|
||||
<Cell>
|
||||
<DeviceIcon type_id={1} />
|
||||
</Cell>
|
||||
<Cell>{coreData.s_n}</Cell>
|
||||
<Cell>{LL.ATTACHED_SENSORS()}</Cell>
|
||||
<Cell>{coreData.active_sensors}</Cell>
|
||||
<Cell>
|
||||
<IconButton size="small" onClick={() => addAnalogSensor()}>
|
||||
<AddCircleOutlineOutlinedIcon sx={{ fontSize: 16, verticalAlign: 'middle' }} />
|
||||
</IconButton>
|
||||
</Cell>
|
||||
</Row>
|
||||
)}
|
||||
</Body>
|
||||
</>
|
||||
)}
|
||||
@@ -813,421 +659,13 @@ const DashboardData: FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const updateSensor = (s: Sensor) => {
|
||||
if (s && me.admin) {
|
||||
setSensor(s);
|
||||
}
|
||||
};
|
||||
|
||||
const updateAnalog = (a: Analog) => {
|
||||
if (me.admin) {
|
||||
setAnalog(a);
|
||||
}
|
||||
};
|
||||
|
||||
const renderDallasData = () => (
|
||||
<>
|
||||
<Typography sx={{ pt: 2, pb: 1 }} variant="h6" color="secondary">
|
||||
{LL.TEMP_SENSORS()}
|
||||
</Typography>
|
||||
<Table
|
||||
data={{ nodes: sensorData.sensors }}
|
||||
theme={temperature_theme}
|
||||
sort={sensor_sort}
|
||||
layout={{ custom: true }}
|
||||
>
|
||||
{(tableList: any) => (
|
||||
<>
|
||||
<Header>
|
||||
<HeaderRow>
|
||||
<HeaderCell resize>
|
||||
<Button
|
||||
fullWidth
|
||||
style={{ fontSize: '14px', justifyContent: 'flex-start' }}
|
||||
endIcon={getSortIcon(sensor_sort.state, 'NAME')}
|
||||
onClick={() => sensor_sort.fns.onToggleSort({ sortKey: 'NAME' })}
|
||||
>
|
||||
{LL.ENTITY_NAME()}
|
||||
</Button>
|
||||
</HeaderCell>
|
||||
<HeaderCell stiff>
|
||||
<Button
|
||||
fullWidth
|
||||
style={{ fontSize: '14px', justifyContent: 'flex-end' }}
|
||||
endIcon={getSortIcon(sensor_sort.state, 'TEMPERATURE')}
|
||||
onClick={() => sensor_sort.fns.onToggleSort({ sortKey: 'TEMPERATURE' })}
|
||||
>
|
||||
{LL.VALUE(0)}
|
||||
</Button>
|
||||
</HeaderCell>
|
||||
<HeaderCell stiff />
|
||||
</HeaderRow>
|
||||
</Header>
|
||||
<Body>
|
||||
{tableList.map((s: Sensor) => (
|
||||
<Row key={s.id} item={s} onClick={() => updateSensor(s)}>
|
||||
<Cell>{s.n}</Cell>
|
||||
<Cell>{formatValue(s.t, s.u)}</Cell>
|
||||
<Cell>
|
||||
{me.admin && (
|
||||
<IconButton onClick={() => updateSensor(s)}>
|
||||
<EditIcon color="primary" sx={{ fontSize: 16 }} />
|
||||
</IconButton>
|
||||
)}
|
||||
</Cell>
|
||||
</Row>
|
||||
))}
|
||||
</Body>
|
||||
</>
|
||||
)}
|
||||
</Table>
|
||||
</>
|
||||
);
|
||||
|
||||
const renderAnalogData = () => (
|
||||
<>
|
||||
<Typography sx={{ pt: 2, pb: 1 }} variant="h6" color="secondary">
|
||||
{LL.ANALOG_SENSORS()}
|
||||
</Typography>
|
||||
|
||||
<Table data={{ nodes: sensorData.analogs }} theme={analog_theme} sort={analog_sort} layout={{ custom: true }}>
|
||||
{(tableList: any) => (
|
||||
<>
|
||||
<Header>
|
||||
<HeaderRow>
|
||||
<HeaderCell stiff>
|
||||
<Button
|
||||
fullWidth
|
||||
style={{ fontSize: '14px', justifyContent: 'flex-start' }}
|
||||
endIcon={getSortIcon(analog_sort.state, 'GPIO')}
|
||||
onClick={() => analog_sort.fns.onToggleSort({ sortKey: 'GPIO' })}
|
||||
>
|
||||
GPIO
|
||||
</Button>
|
||||
</HeaderCell>
|
||||
<HeaderCell resize>
|
||||
<Button
|
||||
fullWidth
|
||||
style={{ fontSize: '14px', justifyContent: 'flex-start' }}
|
||||
endIcon={getSortIcon(analog_sort.state, 'NAME')}
|
||||
onClick={() => analog_sort.fns.onToggleSort({ sortKey: 'NAME' })}
|
||||
>
|
||||
{LL.ENTITY_NAME()}
|
||||
</Button>
|
||||
</HeaderCell>
|
||||
<HeaderCell stiff>
|
||||
<Button
|
||||
fullWidth
|
||||
style={{ fontSize: '14px', justifyContent: 'flex-start' }}
|
||||
endIcon={getSortIcon(analog_sort.state, 'TYPE')}
|
||||
onClick={() => analog_sort.fns.onToggleSort({ sortKey: 'TYPE' })}
|
||||
>
|
||||
{LL.TYPE(0)}
|
||||
</Button>
|
||||
</HeaderCell>
|
||||
<HeaderCell stiff>{LL.VALUE(0)}</HeaderCell>
|
||||
<HeaderCell stiff />
|
||||
</HeaderRow>
|
||||
</Header>
|
||||
<Body>
|
||||
{tableList.map((a: Analog) => (
|
||||
<Row key={a.id} item={a} onClick={() => updateAnalog(a)}>
|
||||
<Cell stiff>{a.g}</Cell>
|
||||
<Cell>{a.n}</Cell>
|
||||
<Cell stiff>{AnalogTypeNames[a.t]} </Cell>
|
||||
<Cell stiff>{a.t ? formatValue(a.v, a.u) : ''}</Cell>
|
||||
<Cell stiff>
|
||||
{me.admin && (
|
||||
<IconButton onClick={() => updateAnalog(a)}>
|
||||
<EditIcon color="primary" sx={{ fontSize: 16 }} />
|
||||
</IconButton>
|
||||
)}
|
||||
</Cell>
|
||||
</Row>
|
||||
))}
|
||||
</Body>
|
||||
</>
|
||||
)}
|
||||
</Table>
|
||||
</>
|
||||
);
|
||||
|
||||
const sendRemoveAnalog = async () => {
|
||||
if (analog) {
|
||||
try {
|
||||
const response = await EMSESP.writeAnalog({
|
||||
gpio: analog.g,
|
||||
name: analog.n,
|
||||
offset: analog.o,
|
||||
factor: analog.f,
|
||||
uom: analog.u,
|
||||
type: -1
|
||||
});
|
||||
|
||||
if (response.status === 204) {
|
||||
toast.error(LL.DELETION_OF(LL.ANALOG_SENSOR()) + ' ' + LL.FAILED());
|
||||
} else if (response.status === 403) {
|
||||
toast.error(LL.ACCESS_DENIED());
|
||||
} else {
|
||||
toast.success(LL.REMOVED_OF(LL.ANALOG_SENSOR()));
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error(extractErrorMessage(error, LL.PROBLEM_UPDATING()));
|
||||
} finally {
|
||||
setAnalog(undefined);
|
||||
await fetchSensorData();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const sendAnalog = async () => {
|
||||
if (analog) {
|
||||
try {
|
||||
const response = await EMSESP.writeAnalog({
|
||||
gpio: analog.g,
|
||||
name: analog.n,
|
||||
offset: analog.o,
|
||||
factor: analog.f,
|
||||
uom: analog.u,
|
||||
type: analog.t
|
||||
});
|
||||
|
||||
if (response.status === 204) {
|
||||
toast.error(LL.UPDATE_OF(LL.ANALOG_SENSOR()) + ' ' + LL.FAILED());
|
||||
} else if (response.status === 403) {
|
||||
toast.error(LL.ACCESS_DENIED());
|
||||
} else {
|
||||
toast.success(LL.UPDATED_OF(LL.ANALOG_SENSOR()));
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error(extractErrorMessage(error, LL.PROBLEM_UPDATING()));
|
||||
} finally {
|
||||
setAnalog(undefined);
|
||||
await fetchSensorData();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const renderAnalogDialog = () => {
|
||||
if (analog) {
|
||||
return (
|
||||
<Dialog open={analog !== undefined} onClose={() => setAnalog(undefined)}>
|
||||
<DialogTitle>
|
||||
{LL.EDIT()} {LL.ANALOG_SENSOR()}
|
||||
</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={12}>
|
||||
<ValidatedTextField
|
||||
name="n"
|
||||
label={LL.ENTITY_NAME()}
|
||||
value={analog.n}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
onChange={updateValue(setAnalog)}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={4}>
|
||||
<ValidatedTextField
|
||||
name="g"
|
||||
label="GPIO"
|
||||
value={numberValue(analog.g)}
|
||||
fullWidth
|
||||
type="number"
|
||||
variant="outlined"
|
||||
autoFocus
|
||||
onChange={updateValue(setAnalog)}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={8}>
|
||||
<ValidatedTextField
|
||||
name="t"
|
||||
label={LL.TYPE(0)}
|
||||
value={analog.t}
|
||||
fullWidth
|
||||
select
|
||||
onChange={updateValue(setAnalog)}
|
||||
>
|
||||
{AnalogTypeNames.map((val, i) => (
|
||||
<MenuItem key={i} value={i}>
|
||||
{val}
|
||||
</MenuItem>
|
||||
))}
|
||||
</ValidatedTextField>
|
||||
</Grid>
|
||||
{analog.t >= AnalogType.COUNTER && analog.t <= AnalogType.RATE && (
|
||||
<>
|
||||
<Grid item xs={4}>
|
||||
<ValidatedTextField
|
||||
name="u"
|
||||
label={LL.UNIT()}
|
||||
value={analog.u}
|
||||
fullWidth
|
||||
select
|
||||
onChange={updateValue(setAnalog)}
|
||||
>
|
||||
{DeviceValueUOM_s.map((val, i) => (
|
||||
<MenuItem key={i} value={i}>
|
||||
{val}
|
||||
</MenuItem>
|
||||
))}
|
||||
</ValidatedTextField>
|
||||
</Grid>
|
||||
{analog.t === AnalogType.ADC && (
|
||||
<Grid item xs={4}>
|
||||
<ValidatedTextField
|
||||
name="o"
|
||||
label={LL.OFFSET()}
|
||||
value={numberValue(analog.o)}
|
||||
fullWidth
|
||||
type="number"
|
||||
variant="outlined"
|
||||
onChange={updateValue(setAnalog)}
|
||||
inputProps={{ min: '0', max: '3300', step: '1' }}
|
||||
InputProps={{
|
||||
startAdornment: <InputAdornment position="start">mV</InputAdornment>
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
)}
|
||||
{analog.t === AnalogType.COUNTER && (
|
||||
<Grid item xs={4}>
|
||||
<ValidatedTextField
|
||||
name="o"
|
||||
label={LL.STARTVALUE()}
|
||||
value={numberValue(analog.o)}
|
||||
fullWidth
|
||||
type="number"
|
||||
variant="outlined"
|
||||
onChange={updateValue(setAnalog)}
|
||||
inputProps={{ step: '0.001' }}
|
||||
/>
|
||||
</Grid>
|
||||
)}
|
||||
<Grid item xs={4}>
|
||||
<ValidatedTextField
|
||||
name="f"
|
||||
label={LL.FACTOR()}
|
||||
value={numberValue(analog.f)}
|
||||
fullWidth
|
||||
type="number"
|
||||
variant="outlined"
|
||||
onChange={updateValue(setAnalog)}
|
||||
inputProps={{ step: '0.001' }}
|
||||
/>
|
||||
</Grid>
|
||||
</>
|
||||
)}
|
||||
{analog.t === AnalogType.DIGITAL_OUT && (analog.g === 25 || analog.g === 26) && (
|
||||
<>
|
||||
<Grid item xs={4}>
|
||||
<ValidatedTextField
|
||||
name="o"
|
||||
label={LL.VALUE(0)}
|
||||
value={numberValue(analog.o)}
|
||||
fullWidth
|
||||
type="number"
|
||||
variant="outlined"
|
||||
onChange={updateValue(setAnalog)}
|
||||
inputProps={{ min: '0', max: '255', step: '1' }}
|
||||
/>
|
||||
</Grid>
|
||||
</>
|
||||
)}
|
||||
{analog.t === AnalogType.DIGITAL_OUT && analog.g !== 25 && analog.g !== 26 && (
|
||||
<>
|
||||
<Grid item xs={4}>
|
||||
<ValidatedTextField
|
||||
name="o"
|
||||
label={LL.VALUE(0)}
|
||||
value={numberValue(analog.o)}
|
||||
fullWidth
|
||||
type="number"
|
||||
variant="outlined"
|
||||
onChange={updateValue(setAnalog)}
|
||||
inputProps={{ min: '0', max: '1', step: '1' }}
|
||||
/>
|
||||
</Grid>
|
||||
</>
|
||||
)}
|
||||
{analog.t >= AnalogType.PWM_0 && (
|
||||
<>
|
||||
<Grid item xs={4}>
|
||||
<ValidatedTextField
|
||||
name="f"
|
||||
label={LL.FREQ()}
|
||||
value={numberValue(analog.f)}
|
||||
fullWidth
|
||||
type="number"
|
||||
variant="outlined"
|
||||
onChange={updateValue(setAnalog)}
|
||||
inputProps={{ min: '1', max: '5000', step: '1' }}
|
||||
InputProps={{
|
||||
startAdornment: <InputAdornment position="start">Hz</InputAdornment>
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={4}>
|
||||
<ValidatedTextField
|
||||
name="o"
|
||||
label={LL.DUTY_CYCLE()}
|
||||
value={numberValue(analog.o)}
|
||||
fullWidth
|
||||
type="number"
|
||||
variant="outlined"
|
||||
onChange={updateValue(setAnalog)}
|
||||
inputProps={{ min: '0', max: '100', step: '0.1' }}
|
||||
InputProps={{
|
||||
startAdornment: <InputAdornment position="start">%</InputAdornment>
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
</>
|
||||
)}
|
||||
</Grid>
|
||||
<Box color="warning.main" mt={2}>
|
||||
<Typography variant="body2">{LL.WARN_GPIO()}</Typography>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Box flexGrow={1} sx={{ '& button': { mt: 0 } }}>
|
||||
<Button startIcon={<RemoveIcon />} variant="outlined" color="error" onClick={() => sendRemoveAnalog()}>
|
||||
{LL.REMOVE()}
|
||||
</Button>
|
||||
</Box>
|
||||
<Button
|
||||
startIcon={<CancelIcon />}
|
||||
variant="outlined"
|
||||
onClick={() => setAnalog(undefined)}
|
||||
color="secondary"
|
||||
>
|
||||
{LL.CANCEL()}
|
||||
</Button>
|
||||
<Button
|
||||
startIcon={<WarningIcon color="warning" />}
|
||||
variant="contained"
|
||||
type="submit"
|
||||
onClick={() => sendAnalog()}
|
||||
color="info"
|
||||
>
|
||||
{LL.UPDATE()}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<SectionContent title={LL.DEVICE_SENSOR_DATA()} titleGutter>
|
||||
<SectionContent title={LL.DEVICE_DATA()} titleGutter>
|
||||
{renderCoreData()}
|
||||
{renderDeviceData()}
|
||||
{renderDeviceDialog()}
|
||||
{sensorData.sensors.length !== 0 && renderDallasData()}
|
||||
{sensorData.analogs.length !== 0 && renderAnalogData()}
|
||||
{renderDeviceValueDialog()}
|
||||
{renderSensorDialog()}
|
||||
{renderAnalogDialog()}
|
||||
|
||||
<ButtonRow>
|
||||
<Button startIcon={<RefreshIcon />} variant="outlined" color="secondary" onClick={refreshData}>
|
||||
{LL.REFRESH()}
|
||||
@@ -1242,4 +680,4 @@ const DashboardData: FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default DashboardData;
|
||||
export default DashboardDevices;
|
||||
469
interface/src/project/DashboardSensors.tsx
Normal file
469
interface/src/project/DashboardSensors.tsx
Normal file
@@ -0,0 +1,469 @@
|
||||
import AddCircleOutlineOutlinedIcon from '@mui/icons-material/AddCircleOutlineOutlined';
|
||||
import KeyboardArrowDownOutlinedIcon from '@mui/icons-material/KeyboardArrowDownOutlined';
|
||||
import KeyboardArrowUpOutlinedIcon from '@mui/icons-material/KeyboardArrowUpOutlined';
|
||||
import RefreshIcon from '@mui/icons-material/Refresh';
|
||||
import UnfoldMoreOutlinedIcon from '@mui/icons-material/UnfoldMoreOutlined';
|
||||
import { Button, Typography, Box } from '@mui/material';
|
||||
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 { toast } from 'react-toastify';
|
||||
|
||||
import DashboardSensorsAnalogDialog from './DashboardSensorsAnalogDialog';
|
||||
import DashboardSensorsTemperatureDialog from './DashboardSensorsTemperatureDialog';
|
||||
import * as EMSESP from './api';
|
||||
|
||||
import { DeviceValueUOM, DeviceValueUOM_s, AnalogTypeNames } from './types';
|
||||
import { temperatureSensorItemValidation, analogSensorItemValidation } from './validators';
|
||||
import type { SensorData, TemperatureSensor, AnalogSensor } from './types';
|
||||
import type { FC } from 'react';
|
||||
import { ButtonRow, SectionContent } from 'components';
|
||||
|
||||
import { AuthenticatedContext } from 'contexts/authentication';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
import { extractErrorMessage } from 'utils';
|
||||
|
||||
const DashboardSensors: FC = () => {
|
||||
const { LL } = useI18nContext();
|
||||
const { me } = useContext(AuthenticatedContext);
|
||||
const [sensorData, setSensorData] = useState<SensorData>({ ts: [], as: [], analog_enabled: false });
|
||||
const [selectedTemperatureSensor, setSelectedTemperatureSensor] = useState<TemperatureSensor>();
|
||||
const [selectedAnalogSensor, setSelectedAnalogSensor] = useState<AnalogSensor>();
|
||||
const [temperatureDialogOpen, setTemperatureDialogOpen] = useState<boolean>(false);
|
||||
const [analogDialogOpen, setAnalogDialogOpen] = useState<boolean>(false);
|
||||
const [creating, setCreating] = useState<boolean>(false);
|
||||
|
||||
const isAdmin = me.admin;
|
||||
|
||||
const common_theme = useTheme({
|
||||
BaseRow: `
|
||||
font-size: 14px;
|
||||
.td {
|
||||
height: 32px;
|
||||
}
|
||||
`,
|
||||
HeaderRow: `
|
||||
text-transform: uppercase;
|
||||
background-color: black;
|
||||
color: #90CAF9;
|
||||
.th {
|
||||
border-bottom: 1px solid #565656;
|
||||
}
|
||||
.th {
|
||||
height: 36px;
|
||||
}
|
||||
`,
|
||||
Row: `
|
||||
background-color: #1e1e1e;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
.td {
|
||||
padding: 8px;
|
||||
border-top: 1px solid #565656;
|
||||
border-bottom: 1px solid #565656;
|
||||
}
|
||||
&.tr.tr-body.row-select.row-select-single-selected {
|
||||
background-color: #3d4752;
|
||||
font-weight: normal;
|
||||
}
|
||||
&:hover .td {
|
||||
border-top: 1px solid #177ac9;
|
||||
border-bottom: 1px solid #177ac9;
|
||||
}
|
||||
&:nth-of-type(odd) .td {
|
||||
background-color: #303030;
|
||||
}
|
||||
`,
|
||||
Cell: `
|
||||
&:last-of-type {
|
||||
text-align: right;
|
||||
},
|
||||
`
|
||||
});
|
||||
|
||||
const temperature_theme = useTheme([
|
||||
common_theme,
|
||||
{
|
||||
Table: `
|
||||
--data-table-library_grid-template-columns: minmax(0, 1fr) 35%;
|
||||
`
|
||||
}
|
||||
]);
|
||||
|
||||
const analog_theme = useTheme([
|
||||
common_theme,
|
||||
{
|
||||
Table: `
|
||||
--data-table-library_grid-template-columns: 80px repeat(1, minmax(0, 1fr)) 120px 100px;
|
||||
`
|
||||
}
|
||||
]);
|
||||
|
||||
const fetchSensorData = useCallback(async () => {
|
||||
try {
|
||||
setSensorData((await EMSESP.readSensorData()).data);
|
||||
} catch (error) {
|
||||
toast.error(extractErrorMessage(error, LL.PROBLEM_LOADING()));
|
||||
}
|
||||
}, [LL]);
|
||||
|
||||
useEffect(() => {
|
||||
void fetchSensorData();
|
||||
}, []);
|
||||
|
||||
const getSortIcon = (state: any, sortKey: any) => {
|
||||
if (state.sortKey === sortKey && state.reverse) {
|
||||
return <KeyboardArrowDownOutlinedIcon />;
|
||||
}
|
||||
if (state.sortKey === sortKey && !state.reverse) {
|
||||
return <KeyboardArrowUpOutlinedIcon />;
|
||||
}
|
||||
return <UnfoldMoreOutlinedIcon />;
|
||||
};
|
||||
|
||||
const analog_sort = useSort(
|
||||
{ nodes: sensorData.as },
|
||||
{},
|
||||
{
|
||||
sortIcon: {
|
||||
iconDefault: <UnfoldMoreOutlinedIcon />,
|
||||
iconUp: <KeyboardArrowUpOutlinedIcon />,
|
||||
iconDown: <KeyboardArrowDownOutlinedIcon />
|
||||
},
|
||||
sortToggleType: SortToggleType.AlternateWithReset,
|
||||
sortFns: {
|
||||
GPIO: (array) => array.sort((a, b) => a.g - b.g),
|
||||
NAME: (array) => array.sort((a, b) => a.n.localeCompare(b.n)),
|
||||
TYPE: (array) => array.sort((a, b) => a.t - b.t),
|
||||
VALUE: (array) => array.sort((a, b) => a.v - b.v)
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const temperature_sort = useSort(
|
||||
{ nodes: sensorData.ts },
|
||||
{},
|
||||
{
|
||||
sortIcon: {
|
||||
iconDefault: <UnfoldMoreOutlinedIcon />,
|
||||
iconUp: <KeyboardArrowUpOutlinedIcon />,
|
||||
iconDown: <KeyboardArrowDownOutlinedIcon />
|
||||
},
|
||||
sortToggleType: SortToggleType.AlternateWithReset,
|
||||
sortFns: {
|
||||
NAME: (array) => array.sort((a, b) => a.n.localeCompare(b.n)),
|
||||
VALUE: (array) => array.sort((a, b) => a.t - b.t)
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setInterval(() => fetchSensorData(), 60000);
|
||||
return () => {
|
||||
clearInterval(timer);
|
||||
};
|
||||
}, [fetchSensorData]);
|
||||
|
||||
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 updateTemperatureSensor = (ts: TemperatureSensor) => {
|
||||
if (isAdmin) {
|
||||
setSelectedTemperatureSensor(ts);
|
||||
setTemperatureDialogOpen(true);
|
||||
}
|
||||
};
|
||||
|
||||
const onTemperatureDialogClose = () => {
|
||||
setTemperatureDialogOpen(false);
|
||||
};
|
||||
|
||||
const onTemperatureDialogSave = async (ts: TemperatureSensor) => {
|
||||
try {
|
||||
const response = await EMSESP.writeTemperatureSensor({
|
||||
id: ts.id,
|
||||
name: ts.n,
|
||||
offset: ts.o
|
||||
});
|
||||
if (response.status === 204) {
|
||||
toast.error(LL.UPLOAD_OF(LL.SENSOR()) + ' ' + LL.FAILED());
|
||||
} else if (response.status === 403) {
|
||||
toast.error(LL.ACCESS_DENIED());
|
||||
} else {
|
||||
toast.success(LL.UPDATED_OF(LL.SENSOR()));
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error(extractErrorMessage(error, LL.PROBLEM_UPDATING()));
|
||||
} finally {
|
||||
setTemperatureDialogOpen(false);
|
||||
setSelectedTemperatureSensor(undefined);
|
||||
await fetchSensorData();
|
||||
}
|
||||
};
|
||||
|
||||
const updateAnalogSensor = (as: AnalogSensor) => {
|
||||
if (isAdmin) {
|
||||
setCreating(false);
|
||||
setSelectedAnalogSensor(as);
|
||||
setAnalogDialogOpen(true);
|
||||
}
|
||||
};
|
||||
|
||||
const onAnalogDialogClose = () => {
|
||||
setAnalogDialogOpen(false);
|
||||
};
|
||||
|
||||
const addAnalogSensor = () => {
|
||||
setCreating(true);
|
||||
setSelectedAnalogSensor({
|
||||
id: Math.floor(Math.random() * (Math.floor(200) - 100) + 100),
|
||||
n: '',
|
||||
g: 40,
|
||||
u: 0,
|
||||
v: 0,
|
||||
o: 0,
|
||||
t: 0,
|
||||
f: 1,
|
||||
d: false
|
||||
});
|
||||
setAnalogDialogOpen(true);
|
||||
};
|
||||
|
||||
const onAnalogDialogSave = async (as: AnalogSensor) => {
|
||||
try {
|
||||
const response = await EMSESP.writeAnalogSensor({
|
||||
id: as.id,
|
||||
gpio: as.g,
|
||||
name: as.n,
|
||||
offset: as.o,
|
||||
factor: as.f,
|
||||
uom: as.u,
|
||||
type: as.t,
|
||||
deleted: as.d
|
||||
});
|
||||
|
||||
if (response.status === 204) {
|
||||
toast.error(LL.UPDATE_OF(LL.ANALOG_SENSOR()) + ' ' + LL.FAILED());
|
||||
} else if (response.status === 403) {
|
||||
toast.error(LL.ACCESS_DENIED());
|
||||
} else {
|
||||
toast.success(LL.UPDATED_OF(LL.ANALOG_SENSOR()));
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error(extractErrorMessage(error, LL.PROBLEM_UPDATING()));
|
||||
} finally {
|
||||
setAnalogDialogOpen(false);
|
||||
setSelectedAnalogSensor(undefined);
|
||||
await fetchSensorData();
|
||||
}
|
||||
};
|
||||
|
||||
const RenderTemperatureSensors = () => (
|
||||
<Table data={{ nodes: sensorData.ts }} theme={temperature_theme} sort={temperature_sort} layout={{ custom: true }}>
|
||||
{(tableList: any) => (
|
||||
<>
|
||||
<Header>
|
||||
<HeaderRow>
|
||||
<HeaderCell resize>
|
||||
<Button
|
||||
fullWidth
|
||||
style={{ fontSize: '14px', justifyContent: 'flex-start' }}
|
||||
endIcon={getSortIcon(temperature_sort.state, 'NAME')}
|
||||
onClick={() => temperature_sort.fns.onToggleSort({ sortKey: 'NAME' })}
|
||||
>
|
||||
{LL.NAME(0)}
|
||||
</Button>
|
||||
</HeaderCell>
|
||||
<HeaderCell stiff>
|
||||
<Button
|
||||
fullWidth
|
||||
style={{ fontSize: '14px', justifyContent: 'flex-end' }}
|
||||
endIcon={getSortIcon(temperature_sort.state, 'VALUE')}
|
||||
onClick={() => temperature_sort.fns.onToggleSort({ sortKey: 'VALUE' })}
|
||||
>
|
||||
{LL.VALUE(0)}
|
||||
</Button>
|
||||
</HeaderCell>
|
||||
</HeaderRow>
|
||||
</Header>
|
||||
<Body>
|
||||
{tableList.map((ts: TemperatureSensor) => (
|
||||
<Row key={ts.id} item={ts} onClick={() => updateTemperatureSensor(ts)}>
|
||||
<Cell>{ts.n}</Cell>
|
||||
<Cell>{formatValue(ts.t, ts.u)}</Cell>
|
||||
</Row>
|
||||
))}
|
||||
</Body>
|
||||
</>
|
||||
)}
|
||||
</Table>
|
||||
);
|
||||
|
||||
const RenderAnalogSensors = () => (
|
||||
<Table data={{ nodes: sensorData.as }} theme={analog_theme} sort={analog_sort} layout={{ custom: true }}>
|
||||
{(tableList: any) => (
|
||||
<>
|
||||
<Header>
|
||||
<HeaderRow>
|
||||
<HeaderCell stiff>
|
||||
<Button
|
||||
fullWidth
|
||||
style={{ fontSize: '14px', justifyContent: 'flex-start' }}
|
||||
endIcon={getSortIcon(analog_sort.state, 'GPIO')}
|
||||
onClick={() => analog_sort.fns.onToggleSort({ sortKey: 'GPIO' })}
|
||||
>
|
||||
GPIO
|
||||
</Button>
|
||||
</HeaderCell>
|
||||
<HeaderCell resize>
|
||||
<Button
|
||||
fullWidth
|
||||
style={{ fontSize: '14px', justifyContent: 'flex-start' }}
|
||||
endIcon={getSortIcon(analog_sort.state, 'NAME')}
|
||||
onClick={() => analog_sort.fns.onToggleSort({ sortKey: 'NAME' })}
|
||||
>
|
||||
{LL.NAME(0)}
|
||||
</Button>
|
||||
</HeaderCell>
|
||||
<HeaderCell stiff>
|
||||
<Button
|
||||
fullWidth
|
||||
style={{ fontSize: '14px', justifyContent: 'flex-start' }}
|
||||
endIcon={getSortIcon(analog_sort.state, 'TYPE')}
|
||||
onClick={() => analog_sort.fns.onToggleSort({ sortKey: 'TYPE' })}
|
||||
>
|
||||
{LL.TYPE(0)}
|
||||
</Button>
|
||||
</HeaderCell>
|
||||
<HeaderCell stiff>
|
||||
<Button
|
||||
fullWidth
|
||||
style={{ fontSize: '14px', justifyContent: 'flex-end' }}
|
||||
endIcon={getSortIcon(analog_sort.state, 'VALUE')}
|
||||
onClick={() => analog_sort.fns.onToggleSort({ sortKey: 'VALUE' })}
|
||||
>
|
||||
{LL.VALUE(0)}
|
||||
</Button>
|
||||
</HeaderCell>
|
||||
</HeaderRow>
|
||||
</Header>
|
||||
<Body>
|
||||
{tableList.map((a: AnalogSensor) => (
|
||||
<Row key={a.id} item={a} onClick={() => updateAnalogSensor(a)}>
|
||||
<Cell stiff>{a.g}</Cell>
|
||||
<Cell>{a.n}</Cell>
|
||||
<Cell stiff>{AnalogTypeNames[a.t]} </Cell>
|
||||
<Cell stiff>{a.t ? formatValue(a.v, a.u) : ''}</Cell>
|
||||
</Row>
|
||||
))}
|
||||
</Body>
|
||||
</>
|
||||
)}
|
||||
</Table>
|
||||
);
|
||||
|
||||
return (
|
||||
<SectionContent title={LL.SENSOR_DATA()} titleGutter>
|
||||
<Typography sx={{ pt: 2, pb: 1 }} variant="h6" color="secondary">
|
||||
{LL.TEMP_SENSORS()}
|
||||
</Typography>
|
||||
<RenderTemperatureSensors />
|
||||
{selectedTemperatureSensor && (
|
||||
<DashboardSensorsTemperatureDialog
|
||||
open={temperatureDialogOpen}
|
||||
onClose={onTemperatureDialogClose}
|
||||
onSave={onTemperatureDialogSave}
|
||||
selectedItem={selectedTemperatureSensor}
|
||||
validator={temperatureSensorItemValidation()}
|
||||
/>
|
||||
)}
|
||||
|
||||
{sensorData?.analog_enabled === true && (
|
||||
<>
|
||||
<Typography sx={{ pt: 4, pb: 1 }} variant="h6" color="secondary">
|
||||
{LL.ANALOG_SENSORS()}
|
||||
</Typography>
|
||||
<RenderAnalogSensors />
|
||||
{selectedAnalogSensor && (
|
||||
<DashboardSensorsAnalogDialog
|
||||
open={analogDialogOpen}
|
||||
onClose={onAnalogDialogClose}
|
||||
onSave={onAnalogDialogSave}
|
||||
creating={creating}
|
||||
selectedItem={selectedAnalogSensor}
|
||||
validator={analogSensorItemValidation(sensorData.as, creating)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<ButtonRow>
|
||||
<Box display="flex" flexWrap="wrap">
|
||||
<Box flexGrow={1}>
|
||||
<Button startIcon={<RefreshIcon />} variant="outlined" color="secondary" onClick={fetchSensorData}>
|
||||
{LL.REFRESH()}
|
||||
</Button>
|
||||
</Box>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
startIcon={<AddCircleOutlineOutlinedIcon />}
|
||||
onClick={addAnalogSensor}
|
||||
>
|
||||
{LL.ADD(0) + ' ' + LL.ANALOG_SENSOR()}
|
||||
</Button>
|
||||
</Box>
|
||||
</ButtonRow>
|
||||
</SectionContent>
|
||||
);
|
||||
};
|
||||
|
||||
export default DashboardSensors;
|
||||
261
interface/src/project/DashboardSensorsAnalogDialog.tsx
Normal file
261
interface/src/project/DashboardSensorsAnalogDialog.tsx
Normal file
@@ -0,0 +1,261 @@
|
||||
import CancelIcon from '@mui/icons-material/Cancel';
|
||||
import RemoveIcon from '@mui/icons-material/RemoveCircleOutline';
|
||||
import WarningIcon from '@mui/icons-material/Warning';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Typography,
|
||||
Box,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
InputAdornment,
|
||||
Grid,
|
||||
MenuItem,
|
||||
TextField
|
||||
} from '@mui/material';
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
import { AnalogType, AnalogTypeNames, DeviceValueUOM_s } from './types';
|
||||
import type { AnalogSensor } 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 { numberValue, updateValue } from 'utils';
|
||||
|
||||
import { validate } from 'validators';
|
||||
|
||||
type DashboardSensorsAnalogDialogProps = {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onSave: (as: AnalogSensor) => void;
|
||||
creating: boolean;
|
||||
selectedItem: AnalogSensor;
|
||||
validator: Schema;
|
||||
};
|
||||
|
||||
const DashboardSensorsAnalogDialog = ({
|
||||
open,
|
||||
onClose,
|
||||
onSave,
|
||||
creating,
|
||||
selectedItem,
|
||||
validator
|
||||
}: DashboardSensorsAnalogDialogProps) => {
|
||||
const { LL } = useI18nContext();
|
||||
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
||||
const [editItem, setEditItem] = useState<AnalogSensor>(selectedItem);
|
||||
const updateFormValue = updateValue(setEditItem);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setFieldErrors(undefined);
|
||||
setEditItem(selectedItem);
|
||||
}
|
||||
}, [open, selectedItem]);
|
||||
|
||||
const close = () => {
|
||||
onClose();
|
||||
};
|
||||
|
||||
const save = async () => {
|
||||
try {
|
||||
setFieldErrors(undefined);
|
||||
await validate(validator, editItem);
|
||||
onSave(editItem);
|
||||
} catch (errors: any) {
|
||||
setFieldErrors(errors);
|
||||
}
|
||||
};
|
||||
|
||||
const remove = () => {
|
||||
editItem.d = true;
|
||||
onSave(editItem);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={close}>
|
||||
<DialogTitle>
|
||||
{creating ? LL.ADD(1) + ' ' + LL.NEW(1) : LL.EDIT()} {LL.ANALOG_SENSOR()}
|
||||
</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={12}>
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="n"
|
||||
label={LL.NAME(0)}
|
||||
value={editItem.n}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
onChange={updateFormValue}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={4}>
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="g"
|
||||
label="GPIO"
|
||||
value={numberValue(editItem.g)}
|
||||
fullWidth
|
||||
type="number"
|
||||
variant="outlined"
|
||||
autoFocus
|
||||
onChange={updateFormValue}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={8}>
|
||||
<TextField name="t" label={LL.TYPE(0)} value={editItem.t} fullWidth select onChange={updateFormValue}>
|
||||
{AnalogTypeNames.map((val, i) => (
|
||||
<MenuItem key={i} value={i}>
|
||||
{val}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
</Grid>
|
||||
{editItem.t >= AnalogType.COUNTER && editItem.t <= AnalogType.RATE && (
|
||||
<>
|
||||
<Grid item xs={4}>
|
||||
<TextField name="u" label={LL.UNIT()} value={editItem.u} fullWidth select onChange={updateFormValue}>
|
||||
{DeviceValueUOM_s.map((val, i) => (
|
||||
<MenuItem key={i} value={i}>
|
||||
{val}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
</Grid>
|
||||
{editItem.t === AnalogType.ADC && (
|
||||
<Grid item xs={4}>
|
||||
<TextField
|
||||
name="o"
|
||||
label={LL.OFFSET()}
|
||||
value={numberValue(editItem.o)}
|
||||
fullWidth
|
||||
type="number"
|
||||
variant="outlined"
|
||||
onChange={updateFormValue}
|
||||
inputProps={{ min: '0', max: '3300', step: '1' }}
|
||||
InputProps={{
|
||||
startAdornment: <InputAdornment position="start">mV</InputAdornment>
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
)}
|
||||
{editItem.t === AnalogType.COUNTER && (
|
||||
<Grid item xs={4}>
|
||||
<TextField
|
||||
name="o"
|
||||
label={LL.STARTVALUE()}
|
||||
value={numberValue(editItem.o)}
|
||||
fullWidth
|
||||
type="number"
|
||||
variant="outlined"
|
||||
onChange={updateFormValue}
|
||||
inputProps={{ step: '0.001' }}
|
||||
/>
|
||||
</Grid>
|
||||
)}
|
||||
<Grid item xs={4}>
|
||||
<TextField
|
||||
name="f"
|
||||
label={LL.FACTOR()}
|
||||
value={numberValue(editItem.f)}
|
||||
fullWidth
|
||||
type="number"
|
||||
variant="outlined"
|
||||
onChange={updateFormValue}
|
||||
inputProps={{ step: '0.001' }}
|
||||
/>
|
||||
</Grid>
|
||||
</>
|
||||
)}
|
||||
{editItem.t === AnalogType.DIGITAL_OUT && (editItem.g === 25 || editItem.g === 26) && (
|
||||
<Grid item xs={4}>
|
||||
<TextField
|
||||
name="o"
|
||||
label={LL.VALUE(0)}
|
||||
value={numberValue(editItem.o)}
|
||||
fullWidth
|
||||
type="number"
|
||||
variant="outlined"
|
||||
onChange={updateFormValue}
|
||||
inputProps={{ min: '0', max: '255', step: '1' }}
|
||||
/>
|
||||
</Grid>
|
||||
)}
|
||||
{editItem.t === AnalogType.DIGITAL_OUT && editItem.g !== 25 && editItem.g !== 26 && (
|
||||
<Grid item xs={4}>
|
||||
<TextField
|
||||
name="o"
|
||||
label={LL.VALUE(0)}
|
||||
value={numberValue(editItem.o)}
|
||||
fullWidth
|
||||
type="number"
|
||||
variant="outlined"
|
||||
onChange={updateFormValue}
|
||||
inputProps={{ min: '0', max: '1', step: '1' }}
|
||||
/>
|
||||
</Grid>
|
||||
)}
|
||||
{editItem.t >= AnalogType.PWM_0 && (
|
||||
<>
|
||||
<Grid item xs={4}>
|
||||
<TextField
|
||||
name="f"
|
||||
label={LL.FREQ()}
|
||||
value={numberValue(editItem.f)}
|
||||
fullWidth
|
||||
type="number"
|
||||
variant="outlined"
|
||||
onChange={updateFormValue}
|
||||
inputProps={{ min: '1', max: '5000', step: '1' }}
|
||||
InputProps={{
|
||||
startAdornment: <InputAdornment position="start">Hz</InputAdornment>
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={4}>
|
||||
<TextField
|
||||
name="o"
|
||||
label={LL.DUTY_CYCLE()}
|
||||
value={numberValue(editItem.o)}
|
||||
fullWidth
|
||||
type="number"
|
||||
variant="outlined"
|
||||
onChange={updateFormValue}
|
||||
inputProps={{ min: '0', max: '100', step: '0.1' }}
|
||||
InputProps={{
|
||||
startAdornment: <InputAdornment position="start">%</InputAdornment>
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
</>
|
||||
)}
|
||||
</Grid>
|
||||
<Box color="warning.main" mt={2}>
|
||||
<Typography variant="body2">{LL.WARN_GPIO()}</Typography>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
{!creating && (
|
||||
<Box flexGrow={1} sx={{ '& button': { mt: 0 } }}>
|
||||
<Button startIcon={<RemoveIcon />} variant="outlined" color="error" onClick={remove}>
|
||||
{LL.REMOVE()}
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
<Button startIcon={<CancelIcon />} variant="outlined" onClick={close} color="secondary">
|
||||
{LL.CANCEL()}
|
||||
</Button>
|
||||
<Button startIcon={<WarningIcon color="warning" />} variant="contained" onClick={save} color="info">
|
||||
{creating ? LL.ADD(0) : LL.UPDATE()}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default DashboardSensorsAnalogDialog;
|
||||
121
interface/src/project/DashboardSensorsTemperatureDialog.tsx
Normal file
121
interface/src/project/DashboardSensorsTemperatureDialog.tsx
Normal file
@@ -0,0 +1,121 @@
|
||||
import CancelIcon from '@mui/icons-material/Cancel';
|
||||
import WarningIcon from '@mui/icons-material/Warning';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Typography,
|
||||
Box,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
InputAdornment,
|
||||
Grid,
|
||||
TextField
|
||||
} from '@mui/material';
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
import type { TemperatureSensor } 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 { numberValue, updateValue } from 'utils';
|
||||
|
||||
import { validate } from 'validators';
|
||||
|
||||
type DashboardSensorsTemperatureDialogProps = {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onSave: (ts: TemperatureSensor) => void;
|
||||
selectedItem: TemperatureSensor;
|
||||
validator: Schema;
|
||||
};
|
||||
|
||||
const DashboardSensorsTemperatureDialog = ({
|
||||
open,
|
||||
onClose,
|
||||
onSave,
|
||||
selectedItem,
|
||||
validator
|
||||
}: DashboardSensorsTemperatureDialogProps) => {
|
||||
const { LL } = useI18nContext();
|
||||
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
||||
const [editItem, setEditItem] = useState<TemperatureSensor>(selectedItem);
|
||||
const updateFormValue = updateValue(setEditItem);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setFieldErrors(undefined);
|
||||
setEditItem(selectedItem);
|
||||
}
|
||||
}, [open, selectedItem]);
|
||||
|
||||
const close = () => {
|
||||
onClose();
|
||||
};
|
||||
|
||||
const save = async () => {
|
||||
try {
|
||||
setFieldErrors(undefined);
|
||||
await validate(validator, editItem);
|
||||
onSave(editItem);
|
||||
} catch (errors: any) {
|
||||
setFieldErrors(errors);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={close}>
|
||||
<DialogTitle>
|
||||
{LL.EDIT()} {LL.TEMP_SENSOR()}
|
||||
</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
<Box color="warning.main" p={0} pl={0} pr={0} mt={0} mb={2}>
|
||||
<Typography variant="body2">
|
||||
{LL.ID_OF(LL.SENSOR())}: {editItem.id}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Grid container spacing={1}>
|
||||
<Grid item>
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="n"
|
||||
label={LL.NAME(0)}
|
||||
value={editItem.n}
|
||||
autoFocus
|
||||
sx={{ width: '30ch' }}
|
||||
onChange={updateFormValue}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<TextField
|
||||
name="o"
|
||||
label={LL.OFFSET()}
|
||||
value={numberValue(editItem.o)}
|
||||
sx={{ width: '12ch' }}
|
||||
type="number"
|
||||
variant="outlined"
|
||||
onChange={updateFormValue}
|
||||
inputProps={{ min: '-5', max: '5', step: '0.1' }}
|
||||
InputProps={{
|
||||
startAdornment: <InputAdornment position="start">°C</InputAdornment>
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button startIcon={<CancelIcon />} variant="outlined" onClick={close} color="secondary">
|
||||
{LL.CANCEL()}
|
||||
</Button>
|
||||
<Button startIcon={<WarningIcon color="warning" />} variant="contained" onClick={save} color="info">
|
||||
{LL.UPDATE()}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default DashboardSensorsTemperatureDialog;
|
||||
@@ -15,7 +15,7 @@ interface DeviceIconProps {
|
||||
// matches emsdevice.h DeviceType
|
||||
const enum DeviceType {
|
||||
SYSTEM = 0,
|
||||
DALLASSENSOR,
|
||||
TEMPERATURESENSOR,
|
||||
ANALOGSENSOR,
|
||||
SCHEDULER,
|
||||
BOILER,
|
||||
@@ -37,7 +37,7 @@ const enum DeviceType {
|
||||
|
||||
const DeviceIcon: FC<DeviceIconProps> = ({ type_id }) => {
|
||||
switch (type_id) {
|
||||
case DeviceType.DALLASSENSOR:
|
||||
case DeviceType.TEMPERATURESENSOR:
|
||||
case DeviceType.ANALOGSENSOR:
|
||||
return <MdOutlineSensors />;
|
||||
case DeviceType.BOILER:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import CancelIcon from '@mui/icons-material/Cancel';
|
||||
import PowerSettingsNewIcon from '@mui/icons-material/PowerSettingsNew';
|
||||
import WarningIcon from '@mui/icons-material/Warning';
|
||||
import { Box, Button, Checkbox, MenuItem, Grid, Typography, Divider, InputAdornment } from '@mui/material';
|
||||
import { Box, Button, Checkbox, MenuItem, Grid, Typography, Divider, InputAdornment, TextField } from '@mui/material';
|
||||
import { useState } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
@@ -132,7 +132,7 @@ const SettingsApplication: FC = () => {
|
||||
<Box color="warning.main">
|
||||
<Typography variant="body2">{LL.BOARD_PROFILE_TEXT()}</Typography>
|
||||
</Box>
|
||||
<ValidatedTextField
|
||||
<TextField
|
||||
name="board_profile"
|
||||
label={LL.BOARD_PROFILE()}
|
||||
value={data.board_profile}
|
||||
@@ -148,7 +148,7 @@ const SettingsApplication: FC = () => {
|
||||
<MenuItem key={'CUSTOM'} value={'CUSTOM'}>
|
||||
{LL.CUSTOM()}…
|
||||
</MenuItem>
|
||||
</ValidatedTextField>
|
||||
</TextField>
|
||||
{data.board_profile === 'CUSTOM' && (
|
||||
<>
|
||||
<Grid
|
||||
@@ -230,7 +230,7 @@ const SettingsApplication: FC = () => {
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6} md={4}>
|
||||
<ValidatedTextField
|
||||
<TextField
|
||||
name="phy_type"
|
||||
label={LL.PHY_TYPE()}
|
||||
disabled={saving}
|
||||
@@ -244,7 +244,7 @@ const SettingsApplication: FC = () => {
|
||||
<MenuItem value={0}>{LL.DISABLED(1)}</MenuItem>
|
||||
<MenuItem value={1}>LAN8720</MenuItem>
|
||||
<MenuItem value={2}>TLK110</MenuItem>
|
||||
</ValidatedTextField>
|
||||
</TextField>
|
||||
</Grid>
|
||||
</Grid>
|
||||
{data.phy_type !== 0 && (
|
||||
@@ -257,7 +257,7 @@ const SettingsApplication: FC = () => {
|
||||
alignItems="flex-start"
|
||||
>
|
||||
<Grid item xs={12} sm={6} md={4}>
|
||||
<ValidatedTextField
|
||||
<TextField
|
||||
name="eth_power"
|
||||
label={LL.GPIO_OF('PHY Power') + ' (-1=' + LL.DISABLED(1) + ')'}
|
||||
fullWidth
|
||||
@@ -270,7 +270,7 @@ const SettingsApplication: FC = () => {
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6} md={4}>
|
||||
<ValidatedTextField
|
||||
<TextField
|
||||
name="eth_phy_addr"
|
||||
label={LL.ADDRESS_OF('PHY I²C')}
|
||||
fullWidth
|
||||
@@ -283,7 +283,7 @@ const SettingsApplication: FC = () => {
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6} md={4}>
|
||||
<ValidatedTextField
|
||||
<TextField
|
||||
name="eth_clock_mode"
|
||||
label="PHY Clk"
|
||||
disabled={saving}
|
||||
@@ -298,7 +298,7 @@ const SettingsApplication: FC = () => {
|
||||
<MenuItem value={1}>GPIO0_OUT</MenuItem>
|
||||
<MenuItem value={2}>GPIO16_OUT</MenuItem>
|
||||
<MenuItem value={3}>GPIO17_OUT</MenuItem>
|
||||
</ValidatedTextField>
|
||||
</TextField>
|
||||
</Grid>
|
||||
</Grid>
|
||||
)}
|
||||
@@ -309,7 +309,7 @@ const SettingsApplication: FC = () => {
|
||||
</Typography>
|
||||
<Grid container spacing={1} direction="row" justifyContent="flex-start" alignItems="flex-start">
|
||||
<Grid item xs={12} sm={6}>
|
||||
<ValidatedTextField
|
||||
<TextField
|
||||
name="tx_mode"
|
||||
label={LL.TX_MODE()}
|
||||
disabled={saving}
|
||||
@@ -324,10 +324,10 @@ const SettingsApplication: FC = () => {
|
||||
<MenuItem value={2}>EMS+</MenuItem>
|
||||
<MenuItem value={3}>HT3</MenuItem>
|
||||
<MenuItem value={4}>{LL.HARDWARE()}</MenuItem>
|
||||
</ValidatedTextField>
|
||||
</TextField>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<ValidatedTextField
|
||||
<TextField
|
||||
name="ems_bus_id"
|
||||
label={LL.ID_OF(LL.EMS_BUS(1))}
|
||||
disabled={saving}
|
||||
@@ -349,14 +349,14 @@ const SettingsApplication: FC = () => {
|
||||
<MenuItem value={0x4b}>Gateway 4 (0x4B)</MenuItem>
|
||||
<MenuItem value={0x4c}>Gateway 5 (0x4C)</MenuItem>
|
||||
<MenuItem value={0x4d}>Gateway 7 (0x4D)</MenuItem>
|
||||
</ValidatedTextField>
|
||||
</TextField>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Typography sx={{ pt: 2 }} variant="h6" color="primary">
|
||||
{LL.GENERAL_OPTIONS()}
|
||||
</Typography>
|
||||
<Grid item>
|
||||
<ValidatedTextField
|
||||
<TextField
|
||||
name="locale"
|
||||
label={LL.LANGUAGE_ENTITIES()}
|
||||
disabled={saving}
|
||||
@@ -376,7 +376,7 @@ const SettingsApplication: FC = () => {
|
||||
<MenuItem value="pl">Polski (PL)</MenuItem>
|
||||
<MenuItem value="sv">Svenska (SV)</MenuItem>
|
||||
<MenuItem value="tr">Türk (TR)</MenuItem>
|
||||
</ValidatedTextField>
|
||||
</TextField>
|
||||
</Grid>
|
||||
{data.led_gpio !== 0 && (
|
||||
<BlockFormControlLabel
|
||||
@@ -478,7 +478,7 @@ const SettingsApplication: FC = () => {
|
||||
</Typography>
|
||||
<Grid container spacing={1} direction="row" justifyContent="flex-start" alignItems="flex-start">
|
||||
<Grid item xs={12} sm={6} md={4}>
|
||||
<ValidatedTextField
|
||||
<TextField
|
||||
name="bool_dashboard"
|
||||
label={LL.BOOLEAN_FORMAT_DASHBOARD()}
|
||||
value={data.bool_dashboard}
|
||||
@@ -492,10 +492,10 @@ const SettingsApplication: FC = () => {
|
||||
<MenuItem value={2}>{LL.ONOFF_CAP()}</MenuItem>
|
||||
<MenuItem value={3}>true/false</MenuItem>
|
||||
<MenuItem value={5}>1/0</MenuItem>
|
||||
</ValidatedTextField>
|
||||
</TextField>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6} md={4}>
|
||||
<ValidatedTextField
|
||||
<TextField
|
||||
name="bool_format"
|
||||
label={LL.BOOLEAN_FORMAT_API()}
|
||||
value={data.bool_format}
|
||||
@@ -511,10 +511,10 @@ const SettingsApplication: FC = () => {
|
||||
<MenuItem value={4}>true/false</MenuItem>
|
||||
<MenuItem value={5}>"1"/"0"</MenuItem>
|
||||
<MenuItem value={6}>1/0</MenuItem>
|
||||
</ValidatedTextField>
|
||||
</TextField>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6} md={4}>
|
||||
<ValidatedTextField
|
||||
<TextField
|
||||
name="enum_format"
|
||||
label={LL.ENUM_FORMAT()}
|
||||
value={data.enum_format}
|
||||
@@ -526,7 +526,7 @@ const SettingsApplication: FC = () => {
|
||||
>
|
||||
<MenuItem value={1}>{LL.VALUE(1)}</MenuItem>
|
||||
<MenuItem value={2}>{LL.INDEX()}</MenuItem>
|
||||
</ValidatedTextField>
|
||||
</TextField>
|
||||
</Grid>
|
||||
</Grid>
|
||||
{data.dallas_gpio !== 0 && (
|
||||
@@ -590,7 +590,7 @@ const SettingsApplication: FC = () => {
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<ValidatedTextField
|
||||
<TextField
|
||||
name="syslog_level"
|
||||
label={LL.LOG_LEVEL()}
|
||||
value={data.syslog_level}
|
||||
@@ -607,7 +607,7 @@ const SettingsApplication: FC = () => {
|
||||
<MenuItem value={6}>INFO</MenuItem>
|
||||
<MenuItem value={7}>DEBUG</MenuItem>
|
||||
<MenuItem value={9}>ALL</MenuItem>
|
||||
</ValidatedTextField>
|
||||
</TextField>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<ValidatedTextField
|
||||
@@ -635,7 +635,6 @@ const SettingsApplication: FC = () => {
|
||||
</Button>
|
||||
</MessageBox>
|
||||
)}
|
||||
|
||||
{!restartNeeded && dirtyFlags && dirtyFlags.length !== 0 && (
|
||||
<ButtonRow>
|
||||
<Button
|
||||
|
||||
@@ -156,7 +156,6 @@ const SettingsCustomization: FC = () => {
|
||||
}
|
||||
}, [LL]);
|
||||
|
||||
// on mount
|
||||
useEffect(() => {
|
||||
void fetchDevices();
|
||||
}, [fetchDevices]);
|
||||
@@ -580,7 +579,7 @@ const SettingsCustomization: FC = () => {
|
||||
open={dialogOpen}
|
||||
onClose={onDialogClose}
|
||||
onSave={onDialogSave}
|
||||
selectedDeviceEntity={selectedDeviceEntity}
|
||||
selectedItem={selectedDeviceEntity}
|
||||
/>
|
||||
)}
|
||||
</SectionContent>
|
||||
|
||||
@@ -26,17 +26,12 @@ type SettingsCustomizationDialogProps = {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onSave: (di: DeviceEntity) => void;
|
||||
selectedDeviceEntity: DeviceEntity;
|
||||
selectedItem: DeviceEntity;
|
||||
};
|
||||
|
||||
const SettingsCustomizationDialog = ({
|
||||
open,
|
||||
onClose,
|
||||
onSave,
|
||||
selectedDeviceEntity
|
||||
}: SettingsCustomizationDialogProps) => {
|
||||
const SettingsCustomizationDialog = ({ open, onClose, onSave, selectedItem }: SettingsCustomizationDialogProps) => {
|
||||
const { LL } = useI18nContext();
|
||||
const [editItem, setEditItem] = useState<DeviceEntity>(selectedDeviceEntity);
|
||||
const [editItem, setEditItem] = useState<DeviceEntity>(selectedItem);
|
||||
const [error, setError] = useState<boolean>(false);
|
||||
|
||||
const updateFormValue = updateValue(setEditItem);
|
||||
@@ -47,9 +42,9 @@ const SettingsCustomizationDialog = ({
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setError(false);
|
||||
setEditItem(selectedDeviceEntity);
|
||||
setEditItem(selectedItem);
|
||||
}
|
||||
}, [open, selectedDeviceEntity]);
|
||||
}, [open, selectedItem]);
|
||||
|
||||
const close = () => {
|
||||
onClose();
|
||||
|
||||
@@ -261,7 +261,7 @@ const SettingsEntities: FC = () => {
|
||||
creating={creating}
|
||||
onClose={onDialogClose}
|
||||
onSave={onDialogSave}
|
||||
selectedEntityItem={selectedEntityItem}
|
||||
selectedItem={selectedEntityItem}
|
||||
validator={entityItemValidation()}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -12,7 +12,8 @@ import {
|
||||
DialogTitle,
|
||||
Grid,
|
||||
InputAdornment,
|
||||
MenuItem
|
||||
MenuItem,
|
||||
TextField
|
||||
} from '@mui/material';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
@@ -33,7 +34,7 @@ type SettingsEntitiesDialogProps = {
|
||||
creating: boolean;
|
||||
onClose: () => void;
|
||||
onSave: (ei: EntityItem) => void;
|
||||
selectedEntityItem: EntityItem;
|
||||
selectedItem: EntityItem;
|
||||
validator: Schema;
|
||||
};
|
||||
|
||||
@@ -42,28 +43,26 @@ const SettingsEntitiesDialog = ({
|
||||
creating,
|
||||
onClose,
|
||||
onSave,
|
||||
selectedEntityItem,
|
||||
selectedItem,
|
||||
validator
|
||||
}: SettingsEntitiesDialogProps) => {
|
||||
const { LL } = useI18nContext();
|
||||
const [editItem, setEditItem] = useState<EntityItem>(selectedEntityItem);
|
||||
const [editItem, setEditItem] = useState<EntityItem>(selectedItem);
|
||||
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
||||
|
||||
const updateFormValue = updateValue(setEditItem);
|
||||
|
||||
// on mount
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setFieldErrors(undefined);
|
||||
setEditItem(selectedEntityItem);
|
||||
setEditItem(selectedItem);
|
||||
// convert to hex strings straight away
|
||||
setEditItem({
|
||||
...selectedEntityItem,
|
||||
device_id: selectedEntityItem.device_id.toString(16).toUpperCase().slice(-2),
|
||||
type_id: selectedEntityItem.type_id.toString(16).toUpperCase().slice(-4)
|
||||
...selectedItem,
|
||||
device_id: selectedItem.device_id.toString(16).toUpperCase().slice(-2),
|
||||
type_id: selectedItem.type_id.toString(16).toUpperCase().slice(-4)
|
||||
});
|
||||
}
|
||||
}, [open, selectedEntityItem]);
|
||||
}, [open, selectedItem]);
|
||||
|
||||
const close = () => {
|
||||
onClose();
|
||||
@@ -157,7 +156,7 @@ const SettingsEntitiesDialog = ({
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={4}>
|
||||
<ValidatedTextField
|
||||
<TextField
|
||||
name="value_type"
|
||||
label="Value Type"
|
||||
value={editItem.value_type}
|
||||
@@ -174,13 +173,13 @@ const SettingsEntitiesDialog = ({
|
||||
<MenuItem value={4}>USHORT</MenuItem>
|
||||
<MenuItem value={5}>ULONG</MenuItem>
|
||||
<MenuItem value={6}>TIME</MenuItem>
|
||||
</ValidatedTextField>
|
||||
</TextField>
|
||||
</Grid>
|
||||
|
||||
{editItem.value_type !== 0 && (
|
||||
<>
|
||||
<Grid item xs={4}>
|
||||
<ValidatedTextField
|
||||
<TextField
|
||||
name="factor"
|
||||
label={LL.FACTOR()}
|
||||
value={editItem.factor}
|
||||
@@ -193,7 +192,7 @@ const SettingsEntitiesDialog = ({
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={4}>
|
||||
<ValidatedTextField
|
||||
<TextField
|
||||
name="uom"
|
||||
label={LL.UNIT()}
|
||||
value={editItem.uom}
|
||||
@@ -207,7 +206,7 @@ const SettingsEntitiesDialog = ({
|
||||
{val}
|
||||
</MenuItem>
|
||||
))}
|
||||
</ValidatedTextField>
|
||||
</TextField>
|
||||
</Grid>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -270,7 +270,7 @@ const SettingsScheduler: FC = () => {
|
||||
creating={creating}
|
||||
onClose={onDialogClose}
|
||||
onSave={onDialogSave}
|
||||
selectedSchedulerItem={selectedScheduleItem}
|
||||
selectedItem={selectedScheduleItem}
|
||||
validator={schedulerItemValidation()}
|
||||
dow={dow}
|
||||
/>
|
||||
|
||||
@@ -37,7 +37,7 @@ type SettingsSchedulerDialogProps = {
|
||||
creating: boolean;
|
||||
onClose: () => void;
|
||||
onSave: (ei: ScheduleItem) => void;
|
||||
selectedSchedulerItem: ScheduleItem;
|
||||
selectedItem: ScheduleItem;
|
||||
validator: Schema;
|
||||
dow: string[];
|
||||
};
|
||||
@@ -47,12 +47,12 @@ const SettingsSchedulerDialog = ({
|
||||
creating,
|
||||
onClose,
|
||||
onSave,
|
||||
selectedSchedulerItem,
|
||||
selectedItem,
|
||||
validator,
|
||||
dow
|
||||
}: SettingsSchedulerDialogProps) => {
|
||||
const { LL } = useI18nContext();
|
||||
const [editItem, setEditItem] = useState<ScheduleItem>(selectedSchedulerItem);
|
||||
const [editItem, setEditItem] = useState<ScheduleItem>(selectedItem);
|
||||
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
||||
|
||||
const updateFormValue = updateValue(setEditItem);
|
||||
@@ -60,9 +60,9 @@ const SettingsSchedulerDialog = ({
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setFieldErrors(undefined);
|
||||
setEditItem(selectedSchedulerItem);
|
||||
setEditItem(selectedItem);
|
||||
}
|
||||
}, [open, selectedSchedulerItem]);
|
||||
}, [open, selectedItem]);
|
||||
|
||||
const close = () => {
|
||||
onClose();
|
||||
|
||||
@@ -10,9 +10,9 @@ import type {
|
||||
DeviceEntity,
|
||||
UniqueID,
|
||||
CustomEntities,
|
||||
WriteValue,
|
||||
WriteSensor,
|
||||
WriteAnalog,
|
||||
WriteDeviceValue,
|
||||
WriteTemperatureSensor,
|
||||
WriteAnalogSensor,
|
||||
SensorData,
|
||||
Schedule,
|
||||
Entities
|
||||
@@ -68,16 +68,16 @@ export function writeCustomEntities(customEntities: CustomEntities): AxiosPromis
|
||||
return AXIOS.post('/customEntities', customEntities);
|
||||
}
|
||||
|
||||
export function writeValue(writevalue: WriteValue): AxiosPromise<void> {
|
||||
return AXIOS.post('/writeValue', writevalue);
|
||||
export function writeDeviceValue(dv: WriteDeviceValue): AxiosPromise<void> {
|
||||
return AXIOS.post('/writeDeviceValue', dv);
|
||||
}
|
||||
|
||||
export function writeSensor(writesensor: WriteSensor): AxiosPromise<void> {
|
||||
return AXIOS.post('/writeSensor', writesensor);
|
||||
export function writeTemperatureSensor(ts: WriteTemperatureSensor): AxiosPromise<void> {
|
||||
return AXIOS.post('/writeTemperatureSensor', ts);
|
||||
}
|
||||
|
||||
export function writeAnalog(writeanalog: WriteAnalog): AxiosPromise<void> {
|
||||
return AXIOS.post('/writeAnalog', writeanalog);
|
||||
export function writeAnalogSensor(as: WriteAnalogSensor): AxiosPromise<void> {
|
||||
return AXIOS.post('/writeAnalogSensor', as);
|
||||
}
|
||||
|
||||
export function resetCustomizations(): AxiosPromise<void> {
|
||||
|
||||
@@ -71,7 +71,7 @@ export interface Device {
|
||||
e: number; // number of entries
|
||||
}
|
||||
|
||||
export interface Sensor {
|
||||
export interface TemperatureSensor {
|
||||
id: string; // id string
|
||||
n: string; // name/alias
|
||||
t?: number; // temp, optional
|
||||
@@ -79,34 +79,33 @@ export interface Sensor {
|
||||
u: number; // uom
|
||||
}
|
||||
|
||||
export interface Analog {
|
||||
id: string; // id string
|
||||
export interface AnalogSensor {
|
||||
id: number;
|
||||
g: number; // GPIO
|
||||
n: string;
|
||||
v: number; // is optional
|
||||
v: number;
|
||||
u: number;
|
||||
o: number;
|
||||
f: number;
|
||||
t: number;
|
||||
d: boolean; // deleted flag
|
||||
}
|
||||
|
||||
export interface WriteSensor {
|
||||
export interface WriteTemperatureSensor {
|
||||
id: string;
|
||||
name: string;
|
||||
offset: number;
|
||||
}
|
||||
|
||||
export interface SensorData {
|
||||
sensors: Sensor[];
|
||||
analogs: Analog[];
|
||||
ts: TemperatureSensor[];
|
||||
as: AnalogSensor[];
|
||||
analog_enabled: boolean;
|
||||
}
|
||||
|
||||
export interface CoreData {
|
||||
connected: boolean;
|
||||
devices: Device[];
|
||||
s_n: string;
|
||||
active_sensors: number;
|
||||
analog_enabled: boolean;
|
||||
}
|
||||
|
||||
export interface DeviceShort {
|
||||
@@ -216,6 +215,7 @@ export const DeviceValueUOM_s = [
|
||||
];
|
||||
|
||||
export enum AnalogType {
|
||||
REMOVED = -1,
|
||||
NOTUSED = 0,
|
||||
DIGITAL_IN,
|
||||
COUNTER,
|
||||
@@ -281,18 +281,20 @@ export interface APIcall {
|
||||
id: any;
|
||||
}
|
||||
|
||||
export interface WriteValue {
|
||||
export interface WriteDeviceValue {
|
||||
id: number;
|
||||
devicevalue: DeviceValue;
|
||||
}
|
||||
|
||||
export interface WriteAnalog {
|
||||
export interface WriteAnalogSensor {
|
||||
id: number;
|
||||
gpio: number;
|
||||
name: string;
|
||||
factor: number;
|
||||
offset: number;
|
||||
uom: number;
|
||||
type: number;
|
||||
deleted: boolean;
|
||||
}
|
||||
|
||||
export enum DeviceEntityMask {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Schema from 'async-validator';
|
||||
import type { Settings } from './types';
|
||||
import type { AnalogSensor, Settings } from './types';
|
||||
import type { InternalRuleItem } from 'async-validator';
|
||||
import { IP_OR_HOSTNAME_VALIDATOR } from 'validators/shared';
|
||||
|
||||
@@ -136,3 +136,28 @@ export const entityItemValidation = () =>
|
||||
{ type: 'number', min: 0, max: 255, message: 'Must be between 0 and 255' }
|
||||
]
|
||||
});
|
||||
|
||||
export const temperatureSensorItemValidation = () =>
|
||||
new Schema({
|
||||
n: [{ required: true, message: 'Name is required' }]
|
||||
});
|
||||
|
||||
export const isGPIOUniqueValidator = (sensors: AnalogSensor[]) => ({
|
||||
validator(rule: InternalRuleItem, gpio: number, callback: (error?: string) => void) {
|
||||
if (sensors.find((as) => as.g === gpio)) {
|
||||
callback('GPIO already in use');
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export const analogSensorItemValidation = (sensors: AnalogSensor[], creating: boolean) =>
|
||||
new Schema({
|
||||
n: [{ required: true, message: 'Name is required' }],
|
||||
g: [
|
||||
{ required: true, message: 'GPIO is required' },
|
||||
GPIO_VALIDATOR,
|
||||
...(creating ? [isGPIOUniqueValidator(sensors)] : [])
|
||||
]
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user