Merge branch 'dev2' of https://github.com/emsesp/EMS-ESP32 into dev2

This commit is contained in:
MichaelDvP
2023-07-02 13:56:47 +02:00
94 changed files with 3916 additions and 3774 deletions

View File

@@ -30,6 +30,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 { useRequest } from 'alova';
import { useState, useContext, useEffect, useCallback, useLayoutEffect } from 'react';
import { IconContext } from 'react-icons';
@@ -42,27 +43,39 @@ import { formatValue } from './deviceValue';
import { DeviceValueUOM_s, DeviceEntityMask, DeviceType } from './types';
import { deviceValueItemValidation } from './validators';
import type { Device, CoreData, DeviceData, DeviceValue } from './types';
import type { Device, DeviceValue } from './types';
import type { FC } from 'react';
import { ButtonRow, SectionContent, MessageBox } from 'components';
import { AuthenticatedContext } from 'contexts/authentication';
import { useI18nContext } from 'i18n/i18n-react';
import { extractErrorMessage } from 'utils';
const DashboardDevices: FC = () => {
const [size, setSize] = useState([0, 0]);
const { me } = useContext(AuthenticatedContext);
const { LL } = useI18nContext();
const [deviceData, setDeviceData] = useState<DeviceData>({ data: [] });
const [selectedDeviceValue, setSelectedDeviceValue] = useState<DeviceValue>();
const [onlyFav, setOnlyFav] = useState(false);
const [deviceValueDialogOpen, setDeviceValueDialogOpen] = useState(false);
const [showDeviceInfo, setShowDeviceInfo] = useState<boolean>(false);
const [selectedDevice, setSelectedDevice] = useState<number>();
const [coreData, setCoreData] = useState<CoreData>({
connected: true,
devices: []
const { data: coreData, send: readCoreData } = useRequest(() => EMSESP.readCoreData(), {
initialData: {
connected: true,
devices: []
}
});
const { data: deviceData, send: readDeviceData } = useRequest((id) => EMSESP.readDeviceData(id), {
initialData: {
data: []
},
immediate: false
});
const { loading: submitting, send: writeDeviceValue } = useRequest((data) => EMSESP.writeDeviceValue(data), {
immediate: false
});
useLayoutEffect(() => {
@@ -148,7 +161,7 @@ const DashboardDevices: FC = () => {
common_theme,
{
Table: `
--data-table-library_grid-template-columns: minmax(0, 1fr) minmax(150px, auto) 40px;
--data-table-library_grid-template-columns: minmax(200px, auto) minmax(150px, auto) 40px;
height: auto;
max-height: 100%;
overflow-y: scroll;
@@ -212,19 +225,10 @@ const DashboardDevices: FC = () => {
}
);
const fetchDeviceData = async (id: number) => {
try {
setDeviceData((await EMSESP.readDeviceData({ id })).data);
} catch (error) {
toast.error(extractErrorMessage(error, LL.PROBLEM_LOADING()));
}
};
function onSelectChange(action: any, state: any) {
setDeviceData({ data: [] });
async function onSelectChange(action: any, state: any) {
setSelectedDevice(state.id);
if (action.type === 'ADD_BY_ID_EXCLUSIVELY') {
void fetchDeviceData(state.id);
await readDeviceData(state.id);
}
}
@@ -257,27 +261,14 @@ const DashboardDevices: FC = () => {
};
}, [escFunction]);
const fetchCoreData = useCallback(async () => {
try {
setSelectedDevice(undefined);
setCoreData((await EMSESP.readCoreData()).data);
} catch (error) {
toast.error(extractErrorMessage(error, LL.PROBLEM_LOADING()));
}
}, [LL]);
useEffect(() => {
void fetchCoreData();
}, [fetchCoreData]);
const refreshData = () => {
if (deviceValueDialogOpen) {
return;
}
if (selectedDevice) {
void fetchDeviceData(selectedDevice);
void readDeviceData(selectedDevice);
} else {
void fetchCoreData();
void readCoreData();
}
};
@@ -346,27 +337,20 @@ const DashboardDevices: FC = () => {
};
});
const deviceValueDialogSave = async (dv: DeviceValue) => {
const selectedDeviceID = Number(device_select.state.id);
try {
const response = await EMSESP.writeDeviceValue({
id: selectedDeviceID,
devicevalue: dv
});
if (response.status === 204) {
toast.error(LL.WRITE_CMD_FAILED());
} else if (response.status === 403) {
toast.error(LL.ACCESS_DENIED());
} else {
const deviceValueDialogSave = async (devicevalue: DeviceValue) => {
const id = Number(device_select.state.id);
await writeDeviceValue({ id, devicevalue })
.then(() => {
toast.success(LL.WRITE_CMD_SENT());
}
} catch (error) {
toast.error(extractErrorMessage(error, LL.PROBLEM_UPDATING()));
} finally {
setDeviceValueDialogOpen(false);
await fetchDeviceData(selectedDeviceID);
setSelectedDeviceValue(undefined);
}
})
.catch((error) => {
toast.error(error.message);
})
.finally(async () => {
setDeviceValueDialogOpen(false);
await readDeviceData(id);
setSelectedDeviceValue(undefined);
});
};
const renderDeviceDetails = () => {
@@ -500,20 +484,11 @@ const DashboardDevices: FC = () => {
}}
>
<Box sx={{ border: '1px solid #177ac9' }}>
<Grid container justifyContent="space-between">
<Box color="warning.main" ml={1}>
<Typography noWrap variant="h6">
{coreData.devices[deviceIndex].n}
</Typography>
</Box>
<Grid item zeroMinWidth justifyContent="flex-end">
<IconButton onClick={resetDeviceSelect}>
<CancelIcon color="info" sx={{ fontSize: 18, verticalAlign: 'middle' }} />
</IconButton>
</Grid>
</Grid>
<Typography noWrap variant="subtitle1" color="warning.main" sx={{ mx: 1 }}>
{coreData.devices[deviceIndex].n}
</Typography>
<Grid item xs>
<Grid container justifyContent="space-between">
<Typography sx={{ ml: 1 }} variant="subtitle2" color="primary">
{shown_data.length + ' ' + LL.ENTITIES(shown_data.length)}
<IconButton onClick={() => setShowDeviceInfo(true)}>
@@ -533,6 +508,11 @@ const DashboardDevices: FC = () => {
<RefreshIcon color="primary" sx={{ fontSize: 18, verticalAlign: 'middle' }} />
</IconButton>
</Typography>
<Grid item zeroMinWidth justifyContent="flex-end">
<IconButton onClick={resetDeviceSelect}>
<CancelIcon color="info" sx={{ fontSize: 18, verticalAlign: 'middle' }} />
</IconButton>
</Grid>
</Grid>
</Box>
@@ -612,6 +592,7 @@ const DashboardDevices: FC = () => {
!hasMask(selectedDeviceValue.id, DeviceEntityMask.DV_READONLY)
}
validator={deviceValueItemValidation(selectedDeviceValue)}
progress={submitting}
/>
)}
<ButtonRow>

View File

@@ -13,8 +13,10 @@ import {
FormHelperText,
Grid,
Box,
Typography
Typography,
CircularProgress
} from '@mui/material';
import { green } from '@mui/material/colors';
import { useState, useEffect } from 'react';
import { DeviceValueUOM, DeviceValueUOM_s } from './types';
@@ -35,6 +37,7 @@ type DashboardDevicesDialogProps = {
selectedItem: DeviceValue;
writeable: boolean;
validator: Schema;
progress: boolean;
};
const DashboarDevicesDialog = ({
@@ -43,7 +46,8 @@ const DashboarDevicesDialog = ({
onSave,
selectedItem,
writeable,
validator
validator,
progress
}: DashboardDevicesDialogProps) => {
const { LL } = useI18nContext();
const [editItem, setEditItem] = useState<DeviceValue>(selectedItem);
@@ -115,7 +119,8 @@ const DashboarDevicesDialog = ({
sx={{
'& .MuiDialog-paper': {
borderRadius: '12px'
}
},
backdropFilter: 'blur(1px)'
}}
>
<DialogTitle>
@@ -184,14 +189,32 @@ const DashboarDevicesDialog = ({
<DialogActions>
{writeable ? (
<>
<Box
sx={{
'& button, & a, & .MuiCard-root': {
mx: 0.6
},
position: 'relative'
}}
>
<Button startIcon={<CancelIcon />} variant="outlined" onClick={close} color="secondary">
{LL.CANCEL()}
</Button>
<Button startIcon={<WarningIcon color="warning" />} variant="contained" onClick={save} color="info">
{selectedItem.v === '' && selectedItem.c ? LL.EXECUTE() : LL.UPDATE()}
</Button>
</>
{progress && (
<CircularProgress
size={24}
sx={{
color: green[500],
position: 'absolute',
right: '20%',
marginTop: '6px'
}}
/>
)}
</Box>
) : (
<Button variant="outlined" onClick={close} color="secondary">
{LL.CLOSE()}

View File

@@ -7,7 +7,8 @@ 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 { useRequest } from 'alova';
import { useState, useContext, useEffect } from 'react';
import { toast } from 'react-toastify';
@@ -17,24 +18,38 @@ 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 { 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 { data: sensorData, send: fetchSensorData } = useRequest(() => EMSESP.readSensorData(), {
initialData: {
ts: [],
as: [],
analog_enabled: false
}
});
const { send: writeTemperatureSensor } = useRequest((data) => EMSESP.writeTemperatureSensor(data), {
immediate: false
});
const { send: writeAnalogSensor } = useRequest((data) => EMSESP.writeAnalogSensor(data), {
immediate: false
});
const isAdmin = me.admin;
const common_theme = useTheme({
@@ -101,20 +116,6 @@ const DashboardSensors: FC = () => {
}
]);
const fetchSensorData = useCallback(async () => {
if (!analogDialogOpen && !temperatureDialogOpen) {
try {
setSensorData((await EMSESP.readSensorData()).data);
} catch (error) {
toast.error(extractErrorMessage(error, LL.PROBLEM_LOADING()));
}
}
}, [LL, analogDialogOpen, temperatureDialogOpen]);
useEffect(() => {
void fetchSensorData();
}, [fetchSensorData]);
const getSortIcon = (state: any, sortKey: any) => {
if (state.sortKey === sortKey && state.reverse) {
return <KeyboardArrowDownOutlinedIcon />;
@@ -229,26 +230,18 @@ const DashboardSensors: FC = () => {
};
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.UPDATE_OF(LL.SENSOR(2)) + ' ' + LL.FAILED(1));
} else if (response.status === 403) {
toast.error(LL.ACCESS_DENIED());
} else {
await writeTemperatureSensor({ id: ts.id, name: ts.n, offset: ts.o })
.then(() => {
toast.success(LL.UPDATED_OF(LL.SENSOR(1)));
}
} catch (error) {
toast.error(extractErrorMessage(error, LL.PROBLEM_UPDATING()));
} finally {
setTemperatureDialogOpen(false);
setSelectedTemperatureSensor(undefined);
await fetchSensorData();
}
})
.catch(() => {
toast.error(LL.UPDATE_OF(LL.SENSOR(2)) + ' ' + LL.FAILED(1));
})
.finally(async () => {
setTemperatureDialogOpen(false);
setSelectedTemperatureSensor(undefined);
await fetchSensorData();
});
};
const updateAnalogSensor = (as: AnalogSensor) => {
@@ -280,32 +273,27 @@ const DashboardSensors: FC = () => {
};
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(5)) + ' ' + LL.FAILED(1));
} else if (response.status === 403) {
toast.error(LL.ACCESS_DENIED());
} else {
await 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
})
.then(() => {
toast.success(LL.UPDATED_OF(LL.ANALOG_SENSOR(2)));
}
} catch (error) {
toast.error(extractErrorMessage(error, LL.PROBLEM_UPDATING()));
} finally {
setAnalogDialogOpen(false);
setSelectedAnalogSensor(undefined);
await fetchSensorData();
}
})
.catch(() => {
toast.error(LL.UPDATE_OF(LL.ANALOG_SENSOR(5)) + ' ' + LL.FAILED(1));
})
.finally(async () => {
setAnalogDialogOpen(false);
setSelectedAnalogSensor(undefined);
await fetchSensorData();
});
};
const RenderTemperatureSensors = () => (
@@ -431,7 +419,7 @@ const DashboardSensors: FC = () => {
{sensorData?.analog_enabled === true && (
<>
<Typography sx={{ pt: 4, pb: 1 }} variant="h6" color="secondary">
{LL.ANALOG_SENSORS(0)}
{LL.ANALOG_SENSORS()}
</Typography>
<RenderAnalogSensors />
{selectedAnalogSensor && (

View File

@@ -19,6 +19,7 @@ import {
} from '@mui/material';
import { Body, Cell, Header, HeaderCell, HeaderRow, Row, Table } from '@table-library/react-table-library/table';
import { useTheme as tableTheme } from '@table-library/react-table-library/theme';
import { useRequest } from 'alova';
import { useContext, useEffect, useState } from 'react';
import { toast } from 'react-toastify';
@@ -32,7 +33,6 @@ import type { FC } from 'react';
import { ButtonRow, FormLoader, SectionContent } from 'components';
import { AuthenticatedContext } from 'contexts/authentication';
import { useI18nContext } from 'i18n/i18n-react';
import { extractErrorMessage, useRest } from 'utils';
export const isConnected = ({ status }: Status) => status !== busConnectionStatus.BUS_STATUS_OFFLINE;
@@ -64,7 +64,7 @@ const showQuality = (stat: Stat) => {
};
const DashboardStatus: FC = () => {
const { loadData, data, errorMessage } = useRest<Status>({ read: EMSESP.readStatus });
const { data: data, send: loadData, error } = useRequest(EMSESP.readStatus);
const { LL } = useI18nContext();
@@ -73,6 +73,10 @@ const DashboardStatus: FC = () => {
const { me } = useContext(AuthenticatedContext);
const { send: scanDevices } = useRequest(EMSESP.scanDevices, {
immediate: false
});
const stats_theme = tableTheme({
Table: `
--data-table-library_grid-template-columns: repeat(1, minmax(0, 1fr)) 90px 90px 80px;
@@ -158,14 +162,14 @@ const DashboardStatus: FC = () => {
};
const scan = async () => {
try {
await EMSESP.scanDevices();
toast.info(LL.SCANNING() + '...');
} catch (error) {
toast.error(extractErrorMessage(error, LL.PROBLEM_UPDATING()));
} finally {
setConfirmScan(false);
}
await scanDevices()
.then(() => {
toast.info(LL.SCANNING() + '...');
})
.catch((err) => {
toast.error(err.message);
});
setConfirmScan(false);
};
const renderScanDialog = () => (
@@ -185,7 +189,7 @@ const DashboardStatus: FC = () => {
const content = () => {
if (!data) {
return <FormLoader onRetry={loadData} errorMessage={errorMessage} />;
return <FormLoader onRetry={loadData} errorMessage={error?.message} />;
}
return (

View File

@@ -4,6 +4,7 @@ import DownloadIcon from '@mui/icons-material/GetApp';
import GitHubIcon from '@mui/icons-material/GitHub';
import MenuBookIcon from '@mui/icons-material/MenuBookTwoTone';
import { Typography, Button, Box, List, ListItem, ListItemText, Link, ListItemAvatar } from '@mui/material';
import { useRequest } from 'alova';
import { toast } from 'react-toastify';
import * as EMSESP from './api';
import type { FC } from 'react';
@@ -11,16 +12,19 @@ import type { FC } from 'react';
import { SectionContent } from 'components';
import { useI18nContext } from 'i18n/i18n-react';
import { extractErrorMessage } from 'utils';
const HelpInformation: FC = () => {
const { LL } = useI18nContext();
const saveFile = (json: any, endpoint: string) => {
const { send: API, onSuccess: onSuccessAPI } = useRequest((data) => EMSESP.API(data), {
immediate: false
});
onSuccessAPI((event) => {
const a = document.createElement('a');
const filename = 'emsesp_' + endpoint + '.txt';
const filename = 'emsesp_info.txt';
a.href = URL.createObjectURL(
new Blob([JSON.stringify(json, null, 2)], {
new Blob([JSON.stringify(event.data, null, 2)], {
type: 'text/plain'
})
);
@@ -29,23 +33,12 @@ const HelpInformation: FC = () => {
a.click();
document.body.removeChild(a);
toast.info(LL.DOWNLOAD_SUCCESSFUL());
};
});
const callAPI = async (endpoint: string) => {
try {
const response = await EMSESP.API({
device: 'system',
entity: endpoint,
id: 0
});
if (response.status !== 200) {
toast.error(LL.PROBLEM_LOADING());
} else {
saveFile(response.data, endpoint);
}
} catch (error) {
toast.error(extractErrorMessage(error, LL.PROBLEM_LOADING()));
}
const callAPI = async () => {
await API({ device: 'system', entity: 'info', id: 0 }).catch((error) => {
toast.error(error.message);
});
};
return (
@@ -96,7 +89,7 @@ const HelpInformation: FC = () => {
size="small"
variant="outlined"
color="primary"
onClick={() => callAPI('info')}
onClick={() => callAPI()}
>
{LL.SUPPORT_INFO()}
</Button>

View File

@@ -2,6 +2,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, TextField } from '@mui/material';
import { useRequest } from 'alova';
import { useState } from 'react';
import { toast } from 'react-toastify';
@@ -11,6 +12,7 @@ import { createSettingsValidator } from './validators';
import type { Settings } from './types';
import type { ValidateFieldsError } from 'async-validator';
import type { FC } from 'react';
import * as SystemApi from 'api/system';
import {
SectionContent,
FormLoader,
@@ -23,7 +25,7 @@ import {
import RestartMonitor from 'framework/system/RestartMonitor';
import { useI18nContext } from 'i18n/i18n-react';
import { numberValue, extractErrorMessage, updateValueDirty, useRest } from 'utils';
import { numberValue, updateValueDirty, useRest } from 'utils';
import { validate } from 'validators';
export function boardProfileSelectItems() {
@@ -39,7 +41,7 @@ const SettingsApplication: FC = () => {
loadData,
saveData,
saving,
setData,
updateDataValue,
data,
origData,
dirtyFlags,
@@ -51,39 +53,48 @@ const SettingsApplication: FC = () => {
read: EMSESP.readSettings,
update: EMSESP.writeSettings
});
const [restarting, setRestarting] = useState<boolean>();
const { LL } = useI18nContext();
const updateFormValue = updateValueDirty(origData, dirtyFlags, setDirtyFlags, setData);
const updateFormValue = updateValueDirty(origData, dirtyFlags, setDirtyFlags, updateDataValue);
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
const [processingBoard, setProcessingBoard] = useState<boolean>(false);
const updateBoardProfile = async (boardProfile: string) => {
setProcessingBoard(true);
try {
const response = await EMSESP.getBoardProfile({ board_profile: boardProfile });
if (data) {
setData({
...data,
board_profile: boardProfile,
led_gpio: response.data.led_gpio,
dallas_gpio: response.data.dallas_gpio,
rx_gpio: response.data.rx_gpio,
tx_gpio: response.data.tx_gpio,
pbutton_gpio: response.data.pbutton_gpio,
phy_type: response.data.phy_type,
eth_power: response.data.eth_power,
eth_phy_addr: response.data.eth_phy_addr,
eth_clock_mode: response.data.eth_clock_mode
});
}
} catch (error) {
toast.error(extractErrorMessage(error, LL.PROBLEM_UPDATING()));
} finally {
setProcessingBoard(false);
}
const {
loading: processingBoard,
send: readBoardProfile,
onSuccess: onSuccessBoardProfile
} = useRequest((boardProfile) => EMSESP.getBoardProfile(boardProfile), {
immediate: false
});
const { send: restartCommand } = useRequest(SystemApi.restart(), {
immediate: false
});
onSuccessBoardProfile((event) => {
const response = event.data as Settings;
updateDataValue({
...data,
board_profile: response.board_profile,
led_gpio: response.led_gpio,
dallas_gpio: response.dallas_gpio,
rx_gpio: response.rx_gpio,
tx_gpio: response.tx_gpio,
pbutton_gpio: response.pbutton_gpio,
phy_type: response.phy_type,
eth_power: response.eth_power,
eth_phy_addr: response.eth_phy_addr,
eth_clock_mode: response.eth_clock_mode
});
});
const updateBoardProfile = async (board_profile: string) => {
await readBoardProfile(board_profile).catch((error) => {
toast.error(error.message);
});
};
const content = () => {
@@ -95,9 +106,10 @@ const SettingsApplication: FC = () => {
try {
setFieldErrors(undefined);
await validate(createSettingsValidator(data), data);
await saveData();
} catch (errors: any) {
setFieldErrors(errors);
} finally {
await saveData();
}
};
@@ -105,7 +117,7 @@ const SettingsApplication: FC = () => {
const boardProfile = event.target.value;
updateFormValue(event);
if (boardProfile === 'CUSTOM') {
setData({
updateDataValue({
...data,
board_profile: boardProfile
});
@@ -116,12 +128,10 @@ const SettingsApplication: FC = () => {
const restart = async () => {
await validateAndSubmit();
try {
await EMSESP.restart();
setRestarting(true);
} catch (error) {
toast.error(extractErrorMessage(error, LL.PROBLEM_UPDATING()));
}
await restartCommand().catch((error) => {
toast.error(error.message);
});
setRestarting(true);
};
return (

View File

@@ -21,6 +21,7 @@ import {
} from '@mui/material';
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 { useRequest } from 'alova';
import { useState, useEffect, useCallback } from 'react';
import { unstable_useBlocker as useBlocker } from 'react-router-dom';
import { toast } from 'react-toastify';
@@ -32,13 +33,13 @@ import SettingsCustomizationDialog from './SettingsCustomizationDialog';
import * as EMSESP from './api';
import { DeviceEntityMask } from './types';
import type { DeviceShort, Devices, DeviceEntity } from './types';
import type { DeviceShort, DeviceEntity } from './types';
import type { FC } from 'react';
import { ButtonRow, FormLoader, SectionContent, MessageBox, BlockNavigation } from 'components';
import * as SystemApi from 'api/system';
import { ButtonRow, SectionContent, MessageBox, BlockNavigation } from 'components';
import RestartMonitor from 'framework/system/RestartMonitor';
import { useI18nContext } from 'i18n/i18n-react';
import { extractErrorMessage } from 'utils';
export const APIURL = window.location.origin + '/api/';
@@ -46,11 +47,10 @@ const SettingsCustomization: FC = () => {
const { LL } = useI18nContext();
const [numChanges, setNumChanges] = useState<number>(0);
const blocker = useBlocker(numChanges !== 0);
const [restarting, setRestarting] = useState<boolean>(false);
const [restartNeeded, setRestartNeeded] = useState<boolean>(false);
const [deviceEntities, setDeviceEntities] = useState<DeviceEntity[]>();
const [devices, setDevices] = useState<Devices>();
const [errorMessage, setErrorMessage] = useState<string>();
const [deviceEntities, setDeviceEntities] = useState<DeviceEntity[]>([]);
const [selectedDevice, setSelectedDevice] = useState<number>(-1);
const [confirmReset, setConfirmReset] = useState<boolean>(false);
const [selectedFilters, setSelectedFilters] = useState<number>(0);
@@ -58,6 +58,31 @@ const SettingsCustomization: FC = () => {
const [selectedDeviceEntity, setSelectedDeviceEntity] = useState<DeviceEntity>();
const [dialogOpen, setDialogOpen] = useState<boolean>(false);
const { send: resetCustomizations } = useRequest(EMSESP.resetCustomizations(), {
immediate: false
});
const { data: devices } = useRequest(EMSESP.readDevices);
const { send: writeCustomEntities } = useRequest((data) => EMSESP.writeCustomEntities(data), { immediate: false });
const { send: readDeviceEntities, onSuccess: onSuccess } = useRequest((data) => EMSESP.readDeviceEntities(data), {
initialData: [],
immediate: false
});
const setOriginalSettings = (data: DeviceEntity[]) => {
setDeviceEntities(data.map((de) => ({ ...de, o_m: de.m, o_cn: de.cn, o_mi: de.mi, o_ma: de.ma })));
};
onSuccess((event) => {
setOriginalSettings(event.data);
});
const { send: restartCommand } = useRequest(SystemApi.restart(), {
immediate: false
});
const entities_theme = useTheme({
Table: `
--data-table-library_grid-template-columns: 150px repeat(1, minmax(80px, 1fr)) 45px minmax(45px, auto) minmax(120px, auto);
@@ -131,7 +156,7 @@ const SettingsCustomization: FC = () => {
}
useEffect(() => {
if (deviceEntities) {
if (deviceEntities.length) {
setNumChanges(
deviceEntities
.filter((de) => hasEntityChanged(de))
@@ -148,29 +173,11 @@ const SettingsCustomization: FC = () => {
}
}, [deviceEntities]);
const fetchDevices = useCallback(async () => {
try {
setDevices((await EMSESP.readDevices()).data);
} catch (error) {
setErrorMessage(extractErrorMessage(error, LL.PROBLEM_LOADING()));
}
}, [LL]);
useEffect(() => {
void fetchDevices();
}, [fetchDevices]);
const setOriginalSettings = (data: DeviceEntity[]) => {
setDeviceEntities(data.map((de) => ({ ...de, o_m: de.m, o_cn: de.cn, o_mi: de.mi, o_ma: de.ma })));
};
const fetchDeviceEntities = async (unique_id: number) => {
try {
const new_deviceEntities = (await EMSESP.readDeviceEntities({ id: unique_id })).data;
setOriginalSettings(new_deviceEntities);
} catch (error) {
setErrorMessage(extractErrorMessage(error, LL.PROBLEM_LOADING()));
}
const restart = async () => {
await restartCommand().catch((error) => {
toast.error(error.message);
});
setRestarting(true);
};
function formatValue(value: any) {
@@ -226,7 +233,7 @@ const SettingsCustomization: FC = () => {
const maskDisabled = (set: boolean) => {
setDeviceEntities(
deviceEntities?.map(function (de) {
deviceEntities.map(function (de) {
if ((de.m & selectedFilters || !selectedFilters) && de.id.toLowerCase().includes(search.toLowerCase())) {
return {
...de,
@@ -246,31 +253,22 @@ const SettingsCustomization: FC = () => {
const selected_device = parseInt(event.target.value, 10);
setSelectedDevice(selected_device);
setNumChanges(0);
void fetchDeviceEntities(devices?.devices[selected_device].i);
void readDeviceEntities(devices?.devices[selected_device].i);
setRestartNeeded(false);
}
};
const resetCustomization = async () => {
try {
await EMSESP.resetCustomizations();
await resetCustomizations();
toast.info(LL.CUSTOMIZATIONS_RESTART());
} catch (error) {
toast.error(extractErrorMessage(error, LL.PROBLEM_UPDATING()));
toast.error(error.message);
} finally {
setConfirmReset(false);
}
};
const restart = async () => {
try {
await EMSESP.restart();
setRestarting(true);
} catch (error) {
toast.error(extractErrorMessage(error, LL.PROBLEM_UPDATING()));
}
};
const onDialogClose = () => {
setDialogOpen(false);
};
@@ -300,7 +298,7 @@ const SettingsCustomization: FC = () => {
const saveCustomization = async () => {
if (devices && deviceEntities && selectedDevice !== -1) {
const masked_entities = deviceEntities
.filter((de) => hasEntityChanged(de))
.filter((de: DeviceEntity) => hasEntityChanged(de))
.map(
(new_de) =>
new_de.m.toString(16).padStart(2, '0') +
@@ -318,72 +316,56 @@ const SettingsCustomization: FC = () => {
return;
}
try {
const response = await EMSESP.writeCustomEntities({
id: devices?.devices[selectedDevice].i,
entity_ids: masked_entities
});
if (response.status === 200) {
toast.success(LL.CUSTOMIZATIONS_SAVED());
} else if (response.status === 201) {
setRestartNeeded(true);
} else {
toast.error(LL.PROBLEM_UPDATING());
await writeCustomEntities({ id: devices?.devices[selectedDevice].i, entity_ids: masked_entities }).catch(
(error) => {
if (error.message === 'Reboot required') {
setRestartNeeded(true);
} else {
toast.error(error.message);
}
}
} catch (error) {
toast.error(extractErrorMessage(error, LL.PROBLEM_UPDATING()));
}
);
setOriginalSettings(deviceEntities);
}
};
const renderDeviceList = () => {
if (!devices) {
return <FormLoader errorMessage={errorMessage} />;
}
return (
<>
<Box mb={2} color="warning.main">
<Typography variant="body2">{LL.CUSTOMIZATIONS_HELP_1()}.</Typography>
<Typography variant="body2" mt={1}>
<OptionIcon type="favorite" isSet={true} />={LL.CUSTOMIZATIONS_HELP_2()}&nbsp;&nbsp;
<OptionIcon type="readonly" isSet={true} />={LL.CUSTOMIZATIONS_HELP_3()}&nbsp;&nbsp;
<OptionIcon type="api_mqtt_exclude" isSet={true} />={LL.CUSTOMIZATIONS_HELP_4()}&nbsp;&nbsp;
<OptionIcon type="web_exclude" isSet={true} />={LL.CUSTOMIZATIONS_HELP_5()}&nbsp;&nbsp;
<OptionIcon type="deleted" isSet={true} />={LL.CUSTOMIZATIONS_HELP_6()}
</Typography>
</Box>
<TextField
name="device"
label={LL.EMS_DEVICE()}
variant="outlined"
fullWidth
value={selectedDevice}
disabled={numChanges !== 0}
onChange={changeSelectedDevice}
margin="normal"
select
>
<MenuItem disabled key={0} value={-1}>
{LL.SELECT_DEVICE()}...
const renderDeviceList = () => (
<>
<Box mb={2} color="warning.main">
<Typography variant="body2">{LL.CUSTOMIZATIONS_HELP_1()}.</Typography>
<Typography variant="body2" mt={1}>
<OptionIcon type="favorite" isSet={true} />={LL.CUSTOMIZATIONS_HELP_2()}&nbsp;&nbsp;
<OptionIcon type="readonly" isSet={true} />={LL.CUSTOMIZATIONS_HELP_3()}&nbsp;&nbsp;
<OptionIcon type="api_mqtt_exclude" isSet={true} />={LL.CUSTOMIZATIONS_HELP_4()}&nbsp;&nbsp;
<OptionIcon type="web_exclude" isSet={true} />={LL.CUSTOMIZATIONS_HELP_5()}&nbsp;&nbsp;
<OptionIcon type="deleted" isSet={true} />={LL.CUSTOMIZATIONS_HELP_6()}
</Typography>
</Box>
<TextField
name="device"
label={LL.EMS_DEVICE()}
variant="outlined"
fullWidth
value={selectedDevice}
disabled={numChanges !== 0}
onChange={changeSelectedDevice}
margin="normal"
select
>
<MenuItem disabled key={0} value={-1}>
{LL.SELECT_DEVICE()}...
</MenuItem>
{devices.devices.map((device: DeviceShort, index) => (
<MenuItem key={index} value={index}>
{device.s}
</MenuItem>
{devices.devices.map((device: DeviceShort, index) => (
<MenuItem key={index} value={index}>
{device.s}
</MenuItem>
))}
</TextField>
</>
);
};
))}
</TextField>
</>
);
const renderDeviceData = () => {
if (!deviceEntities) {
return;
}
if (devices?.devices.length === 0 || deviceEntities[0].id === '') {
if (deviceEntities.length === 0) {
return;
}
@@ -521,7 +503,7 @@ const SettingsCustomization: FC = () => {
<Typography sx={{ pt: 2, pb: 2 }} variant="h6" color="primary">
{LL.DEVICE_ENTITIES()}
</Typography>
{renderDeviceList()}
{devices && renderDeviceList()}
{renderDeviceData()}
{restartNeeded && (
<MessageBox my={2} level="warning" message={LL.RESTART_TEXT()}>
@@ -539,7 +521,7 @@ const SettingsCustomization: FC = () => {
startIcon={<CancelIcon />}
variant="outlined"
color="secondary"
onClick={() => devices && fetchDeviceEntities(devices.devices[selectedDevice].i)}
onClick={() => devices && readDeviceEntities(devices.devices[selectedDevice].i)}
>
{LL.CANCEL()}
</Button>

View File

@@ -124,7 +124,7 @@ const SettingsCustomizationDialog = ({ open, onClose, onSave, selectedItem }: Se
{LL.CANCEL()}
</Button>
<Button startIcon={<DoneIcon />} variant="outlined" onClick={save} color="primary">
{LL.UPDATE(0)}
{LL.UPDATE()}
</Button>
</DialogActions>
</Dialog>

View File

@@ -4,7 +4,9 @@ import WarningIcon from '@mui/icons-material/Warning';
import { Button, Typography, Box } from '@mui/material';
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, useEffect, useCallback } from 'react';
// eslint-disable-next-line import/named
import { updateState, useRequest } from 'alova';
import { useState, useCallback } from 'react';
import { unstable_useBlocker as useBlocker } from 'react-router-dom';
import { toast } from 'react-toastify';
@@ -18,18 +20,26 @@ import type { FC } from 'react';
import { ButtonRow, FormLoader, SectionContent, BlockNavigation } from 'components';
import { useI18nContext } from 'i18n/i18n-react';
import { extractErrorMessage } from 'utils';
const SettingsEntities: FC = () => {
const { LL } = useI18nContext();
const [numChanges, setNumChanges] = useState<number>(0);
const blocker = useBlocker(numChanges !== 0);
const [entities, setEntities] = useState<EntityItem[]>();
const [selectedEntityItem, setSelectedEntityItem] = useState<EntityItem>();
const [errorMessage, setErrorMessage] = useState<string>();
const [creating, setCreating] = useState<boolean>(false);
const [dialogOpen, setDialogOpen] = useState<boolean>(false);
const {
data: entities,
send: fetchEntities,
error
} = useRequest(EMSESP.readEntities, {
initialData: [],
force: true
});
const { send: writeEntities } = useRequest((data) => EMSESP.writeEntities(data), { immediate: false });
function hasEntityChanged(ei: EntityItem) {
return (
ei.id !== ei.o_id ||
@@ -45,12 +55,6 @@ const SettingsEntities: FC = () => {
);
}
useEffect(() => {
if (entities) {
setNumChanges(entities ? entities.filter((ei) => hasEntityChanged(ei)).length : 0);
}
}, [entities]);
const entity_theme = useTheme({
Table: `
--data-table-library_grid-template-columns: repeat(1, minmax(60px, 1fr)) minmax(80px, auto) 80px 80px 80px;
@@ -105,62 +109,32 @@ const SettingsEntities: FC = () => {
`
});
const fetchEntities = useCallback(async () => {
try {
const response = await EMSESP.readEntities();
setEntities(
response.data.entities.map((ei) => ({
...ei,
o_id: ei.id,
o_device_id: ei.device_id,
o_type_id: ei.type_id,
o_offset: ei.offset,
o_factor: ei.factor,
o_uom: ei.uom,
o_value_type: ei.value_type,
o_name: ei.name,
o_writeable: ei.writeable,
o_deleted: ei.deleted
}))
);
} catch (error) {
setErrorMessage(extractErrorMessage(error, LL.PROBLEM_LOADING()));
}
}, [LL]);
useEffect(() => {
void fetchEntities();
}, [fetchEntities]);
const saveEntities = async () => {
if (entities) {
try {
const response = await EMSESP.writeEntities({
entities: entities
.filter((ei) => !ei.deleted)
.map((condensed_ei) => ({
id: condensed_ei.id,
name: condensed_ei.name,
device_id: condensed_ei.device_id,
type_id: condensed_ei.type_id,
offset: condensed_ei.offset,
factor: condensed_ei.factor,
uom: condensed_ei.uom,
writeable: condensed_ei.writeable,
value_type: condensed_ei.value_type
}))
});
if (response.status === 200) {
toast.success(LL.ENTITIES_UPDATED());
} else {
toast.error(LL.PROBLEM_UPDATING());
}
await writeEntities({
entities: entities
.filter((ei) => !ei.deleted)
.map((condensed_ei) => ({
id: condensed_ei.id,
name: condensed_ei.name,
device_id: condensed_ei.device_id,
type_id: condensed_ei.type_id,
offset: condensed_ei.offset,
factor: condensed_ei.factor,
uom: condensed_ei.uom,
writeable: condensed_ei.writeable,
value_type: condensed_ei.value_type
}))
})
.then(() => {
toast.success(LL.ENTITIES_UPDATED());
})
.catch((err) => {
toast.error(err.message);
})
.finally(async () => {
await fetchEntities();
} catch (error) {
toast.error(extractErrorMessage(error, LL.PROBLEM_UPDATING()));
}
}
setNumChanges(0);
});
};
const editEntityItem = useCallback((ei: EntityItem) => {
@@ -173,13 +147,22 @@ const SettingsEntities: FC = () => {
setDialogOpen(false);
};
const onDialogCancel = async () => {
await fetchEntities().then(() => {
setNumChanges(0);
});
};
const onDialogSave = (updatedItem: EntityItem) => {
setDialogOpen(false);
if (entities && creating) {
setEntities([...entities.filter((ei) => creating || ei.o_id !== updatedItem.o_id), updatedItem]);
} else {
setEntities(entities?.map((ei) => (ei.id === updatedItem.id ? { ...ei, ...updatedItem } : ei)));
}
updateState('entities', (data) => {
const new_data = creating
? [...data.filter((ei) => creating || ei.o_id !== updatedItem.o_id), updatedItem]
: data.map((ei) => (ei.id === updatedItem.id ? { ...ei, ...updatedItem } : ei));
setNumChanges(new_data.filter((ei) => hasEntityChanged(ei)).length);
return new_data;
});
};
const addEntityItem = () => {
@@ -208,14 +191,12 @@ const SettingsEntities: FC = () => {
}
function showHex(value: number, digit: number) {
return digit === 4
? '0x' + ('000' + value.toString(16).toUpperCase()).slice(-4)
: '0x' + ('0' + value.toString(16).toUpperCase()).slice(-2);
return '0x' + value.toString(16).toUpperCase().padStart(digit, '0');
}
const renderEntity = () => {
if (!entities) {
return <FormLoader errorMessage={errorMessage} />;
return <FormLoader onRetry={fetchEntities} errorMessage={error?.message} />;
}
return (
@@ -236,7 +217,7 @@ const SettingsEntities: FC = () => {
<Row key={ei.name} item={ei} onClick={() => editEntityItem(ei)}>
<Cell>{ei.name}</Cell>
<Cell>{showHex(ei.device_id as number, 2)}</Cell>
<Cell>{showHex(ei.type_id as number, 4)}</Cell>
<Cell>{showHex(ei.type_id as number, 3)}</Cell>
<Cell>{ei.offset}</Cell>
<Cell>{formatValue(ei.value, ei.uom)}</Cell>
</Row>
@@ -272,7 +253,7 @@ const SettingsEntities: FC = () => {
<Box flexGrow={1}>
{numChanges > 0 && (
<ButtonRow>
<Button startIcon={<CancelIcon />} variant="outlined" onClick={fetchEntities} color="secondary">
<Button startIcon={<CancelIcon />} variant="outlined" onClick={onDialogCancel} color="secondary">
{LL.CANCEL()}
</Button>
<Button

View File

@@ -58,8 +58,8 @@ const SettingsEntitiesDialog = ({
// convert to hex strings straight away
setEditItem({
...selectedItem,
device_id: selectedItem.device_id.toString(16).toUpperCase().slice(-2),
type_id: selectedItem.type_id.toString(16).toUpperCase().slice(-4)
device_id: selectedItem.device_id.toString(16).toUpperCase(),
type_id: selectedItem.type_id.toString(16).toUpperCase()
});
}
}, [open, selectedItem]);

View File

@@ -6,6 +6,8 @@ import WarningIcon from '@mui/icons-material/Warning';
import { Box, Typography, Divider, Stack, Button } from '@mui/material';
import { Table, Header, HeaderRow, HeaderCell, Body, Row, Cell } from '@table-library/react-table-library/table';
import { useTheme } from '@table-library/react-table-library/theme';
// eslint-disable-next-line import/named
import { updateState, useRequest } from 'alova';
import { useState, useEffect, useCallback } from 'react';
import { unstable_useBlocker as useBlocker } from 'react-router-dom';
import { toast } from 'react-toastify';
@@ -19,23 +21,31 @@ import type { FC } from 'react';
import { ButtonRow, FormLoader, SectionContent, BlockNavigation } from 'components';
import { useI18nContext } from 'i18n/i18n-react';
import { extractErrorMessage } from 'utils';
const SettingsScheduler: FC = () => {
const { LL, locale } = useI18nContext();
const [numChanges, setNumChanges] = useState<number>(0);
const blocker = useBlocker(numChanges !== 0);
const [schedule, setSchedule] = useState<ScheduleItem[]>([]);
const [selectedScheduleItem, setSelectedScheduleItem] = useState<ScheduleItem>();
const [dow, setDow] = useState<string[]>([]);
const [errorMessage, setErrorMessage] = useState<string>();
const [creating, setCreating] = useState<boolean>(false);
const [dialogOpen, setDialogOpen] = useState<boolean>(false);
const {
data: schedule,
send: fetchSchedule,
error
} = useRequest(EMSESP.readSchedule, {
initialData: [],
force: true
});
const { send: writeSchedule } = useRequest((data) => EMSESP.writeSchedule(data), { immediate: false });
function hasScheduleChanged(si: ScheduleItem) {
return (
si.id !== si.o_id ||
(si?.name || '') !== (si?.o_name || '') ||
(si.name || '') !== (si.o_name || '') ||
si.active !== si.o_active ||
si.deleted !== si.o_deleted ||
si.flags !== si.o_flags ||
@@ -46,10 +56,13 @@ const SettingsScheduler: FC = () => {
}
useEffect(() => {
if (schedule) {
setNumChanges(schedule ? schedule.filter((si) => hasScheduleChanged(si)).length : 0);
}
}, [schedule]);
const formatter = new Intl.DateTimeFormat(locale, { weekday: 'short', timeZone: 'UTC' });
const days = [1, 2, 3, 4, 5, 6, 7].map((day) => {
const dd = day < 10 ? `0${day}` : day;
return new Date(`2017-01-${dd}T00:00:00+00:00`);
});
setDow(days.map((date) => formatter.format(date)));
}, [locale]);
const schedule_theme = useTheme({
Table: `
@@ -96,63 +109,30 @@ const SettingsScheduler: FC = () => {
`
});
const fetchSchedule = useCallback(async () => {
try {
const response = await EMSESP.readSchedule();
setSchedule(
response.data.schedule.map((si) => ({
...si,
o_id: si.id,
o_active: si.active,
o_deleted: si.deleted,
o_flags: si.flags,
o_time: si.time,
o_cmd: si.cmd,
o_value: si.value,
o_name: si.name
}))
);
} catch (error) {
setErrorMessage(extractErrorMessage(error, LL.PROBLEM_LOADING()));
}
}, [LL]);
useEffect(() => {
const formatter = new Intl.DateTimeFormat(locale, { weekday: 'short', timeZone: 'UTC' });
const days = [1, 2, 3, 4, 5, 6, 7].map((day) => {
const dd = day < 10 ? `0${day}` : day;
return new Date(`2017-01-${dd}T00:00:00+00:00`);
});
setDow(days.map((date) => formatter.format(date)));
void fetchSchedule();
}, [locale, fetchSchedule]);
const saveSchedule = async () => {
if (schedule) {
try {
const response = await EMSESP.writeSchedule({
schedule: schedule
.filter((si) => !si.deleted)
.map((condensed_si) => ({
id: condensed_si.id,
active: condensed_si.active,
flags: condensed_si.flags,
time: condensed_si.time,
cmd: condensed_si.cmd,
value: condensed_si.value,
name: condensed_si.name
}))
});
if (response.status === 200) {
toast.success(LL.SCHEDULE_UPDATED());
} else {
toast.error(LL.PROBLEM_UPDATING());
}
await writeSchedule({
schedule: schedule
.filter((si) => !si.deleted)
.map((condensed_si) => ({
id: condensed_si.id,
active: condensed_si.active,
flags: condensed_si.flags,
time: condensed_si.time,
cmd: condensed_si.cmd,
value: condensed_si.value,
name: condensed_si.name
}))
})
.then(() => {
toast.success(LL.SCHEDULE_UPDATED());
})
.catch((err) => {
toast.error(err.message);
})
.finally(async () => {
await fetchSchedule();
} catch (error) {
toast.error(extractErrorMessage(error, LL.PROBLEM_UPDATING()));
}
}
setNumChanges(0);
});
};
const editScheduleItem = useCallback((si: ScheduleItem) => {
@@ -165,13 +145,22 @@ const SettingsScheduler: FC = () => {
setDialogOpen(false);
};
const onDialogCancel = async () => {
await fetchSchedule().then(() => {
setNumChanges(0);
});
};
const onDialogSave = (updatedItem: ScheduleItem) => {
setDialogOpen(false);
if (schedule && creating) {
setSchedule([...schedule.filter((si) => creating || si.o_id !== updatedItem.o_id), updatedItem]);
} else {
setSchedule(schedule?.map((si) => (si.id === updatedItem.id ? { ...si, ...updatedItem } : si)));
}
updateState('schedule', (data) => {
const new_data = creating
? [...data.filter((si) => creating || si.o_id !== updatedItem.o_id), updatedItem]
: data.map((si) => (si.id === updatedItem.id ? { ...si, ...updatedItem } : si));
setNumChanges(new_data.filter((si) => hasScheduleChanged(si)).length);
return new_data;
});
};
const addScheduleItem = () => {
@@ -191,7 +180,7 @@ const SettingsScheduler: FC = () => {
const renderSchedule = () => {
if (!schedule) {
return <FormLoader errorMessage={errorMessage} />;
return <FormLoader onRetry={fetchSchedule} errorMessage={error?.message} />;
}
const dayBox = (si: ScheduleItem, flag: number) => (
@@ -270,7 +259,7 @@ const SettingsScheduler: FC = () => {
onClose={onDialogClose}
onSave={onDialogSave}
selectedItem={selectedScheduleItem}
validator={schedulerItemValidation()}
validator={schedulerItemValidation(schedule, selectedScheduleItem)}
dow={dow}
/>
)}
@@ -279,7 +268,7 @@ const SettingsScheduler: FC = () => {
<Box flexGrow={1}>
{numChanges !== 0 && (
<ButtonRow>
<Button startIcon={<CancelIcon />} variant="outlined" onClick={fetchSchedule} color="secondary">
<Button startIcon={<CancelIcon />} variant="outlined" onClick={onDialogCancel} color="secondary">
{LL.CANCEL()}
</Button>
<Button

View File

@@ -215,7 +215,7 @@ const SettingsSchedulerDialog = ({
/>
<TextField
name="value"
label={LL.VALUE(0)}
label={LL.VALUE(1)}
multiline
margin="normal"
fullWidth

View File

@@ -1,121 +1,107 @@
import type {
BoardProfile,
BoardProfileName,
APIcall,
Settings,
Status,
CoreData,
Devices,
DeviceData,
DeviceEntity,
UniqueID,
CustomEntities,
WriteDeviceValue,
WriteTemperatureSensor,
WriteAnalogSensor,
SensorData,
Schedule,
Entities
Entities,
DeviceData,
ScheduleItem,
EntityItem
} from './types';
import type { AxiosPromise } from 'axios';
import { AXIOS, AXIOS_API, AXIOS_BIN } from 'api/endpoints';
import { alovaInstance } from 'api/endpoints';
export function restart(): AxiosPromise<void> {
return AXIOS.post('/restart');
}
// DashboardDevices
export const readCoreData = () => alovaInstance.Get<CoreData>(`/rest/coreData`);
export const readDeviceData = (id: number) =>
alovaInstance.Get<DeviceData>('/rest/deviceData', {
params: { id },
responseType: 'arraybuffer' // uses msgpack
});
export const writeDeviceValue = (data: any) => alovaInstance.Post('/rest/writeDeviceValue', data);
export function readSettings(): AxiosPromise<Settings> {
return AXIOS.get('/settings');
}
// SettingsApplication
export const readSettings = () => alovaInstance.Get<Settings>('/rest/settings');
export const writeSettings = (data: any) => alovaInstance.Post('/rest/settings', data);
export const getBoardProfile = (boardProfile: string) =>
alovaInstance.Get('/rest/boardProfile', {
params: { boardProfile }
});
export function writeSettings(settings: Settings): AxiosPromise<Settings> {
return AXIOS.post('/settings', settings);
}
// DashboardSensors
export const readSensorData = () => alovaInstance.Get<SensorData>('/rest/sensorData');
export const writeTemperatureSensor = (ts: WriteTemperatureSensor) =>
alovaInstance.Post('/rest/writeTemperatureSensor', ts);
export const writeAnalogSensor = (as: WriteAnalogSensor) => alovaInstance.Post('/rest/writeAnalogSensor', as);
export function getBoardProfile(boardProfile: BoardProfileName): AxiosPromise<BoardProfile> {
return AXIOS.post('/boardProfile', boardProfile);
}
// DashboardStatus
export const readStatus = () => alovaInstance.Get<Status>('/rest/status');
export const scanDevices = () => alovaInstance.Post('/rest/scanDevices');
export function readStatus(): AxiosPromise<Status> {
return AXIOS.get('/status');
}
// HelpInformation
export const API = (apiCall: APIcall) => alovaInstance.Post('/api', apiCall);
export function readCoreData(): AxiosPromise<CoreData> {
return AXIOS.get('/coreData');
}
// UploadFileForm
export const getSettings = () => alovaInstance.Get('/rest/getSettings');
export const getCustomizations = () => alovaInstance.Get('/rest/getCustomizations');
export const getEntities = () => alovaInstance.Get<Entities>('/rest/getEntities');
export const getSchedule = () => alovaInstance.Get('/rest/getSchedule');
export function readDevices(): AxiosPromise<Devices> {
return AXIOS.get('/devices');
}
// SettingsCustomization
export const readDeviceEntities = (id: number) =>
alovaInstance.Get<DeviceEntity[]>('/rest/deviceEntities', {
params: { id },
responseType: 'arraybuffer',
transformData(data: any) {
return data.map((de: DeviceEntity) => ({ ...de, o_m: de.m, o_cn: de.cn, o_mi: de.mi, o_ma: de.ma }));
}
});
export const readDevices = () => alovaInstance.Get<Devices>('/rest/devices');
export const resetCustomizations = () => alovaInstance.Post('/rest/resetCustomizations');
export const writeCustomEntities = (data: any) => alovaInstance.Post('/rest/customEntities', data);
export function scanDevices(): AxiosPromise<void> {
return AXIOS.post('/scanDevices');
}
// SettingsScheduler
export const readSchedule = () =>
alovaInstance.Get<ScheduleItem[]>('/rest/schedule', {
name: 'schedule',
transformData(data: any) {
return data.schedule.map((si: ScheduleItem) => ({
...si,
o_id: si.id,
o_active: si.active,
o_deleted: si.deleted,
o_flags: si.flags,
o_time: si.time,
o_cmd: si.cmd,
o_value: si.value,
o_name: si.name
}));
}
});
export const writeSchedule = (data: any) => alovaInstance.Post('/rest/schedule', data);
export function readDeviceData(unique_id: UniqueID): AxiosPromise<DeviceData> {
return AXIOS_BIN.post('/deviceData', unique_id);
}
export function readSensorData(): AxiosPromise<SensorData> {
return AXIOS.get('/sensorData');
}
export function readDeviceEntities(unique_id: UniqueID): AxiosPromise<DeviceEntity[]> {
return AXIOS_BIN.post('/deviceEntities', unique_id);
}
export function writeCustomEntities(customEntities: CustomEntities): AxiosPromise<void> {
return AXIOS.post('/customEntities', customEntities);
}
export function writeDeviceValue(dv: WriteDeviceValue): AxiosPromise<void> {
return AXIOS.post('/writeDeviceValue', dv);
}
export function writeTemperatureSensor(ts: WriteTemperatureSensor): AxiosPromise<void> {
return AXIOS.post('/writeTemperatureSensor', ts);
}
export function writeAnalogSensor(as: WriteAnalogSensor): AxiosPromise<void> {
return AXIOS.post('/writeAnalogSensor', as);
}
export function resetCustomizations(): AxiosPromise<void> {
return AXIOS.post('/resetCustomizations');
}
export function API(apiCall: APIcall): AxiosPromise<void> {
return AXIOS_API.post('/', apiCall);
}
export function getSettings(): AxiosPromise<void> {
return AXIOS.get('/getSettings');
}
export function getCustomizations(): AxiosPromise<void> {
return AXIOS.get('/getCustomizations');
}
export function getSchedule(): AxiosPromise<Schedule> {
return AXIOS.get('/getSchedule');
}
export function readSchedule(): AxiosPromise<Schedule> {
return AXIOS.get('/schedule');
}
export function writeSchedule(schedule: Schedule): AxiosPromise<void> {
return AXIOS.post('/schedule', schedule);
}
export function getEntities(): AxiosPromise<Entities> {
return AXIOS.get('/getEntities');
}
export function readEntities(): AxiosPromise<Entities> {
return AXIOS.get('/entities');
}
export function writeEntities(entities: Entities): AxiosPromise<void> {
return AXIOS.post('/entities', entities);
}
// SettingsEntities
export const readEntities = () =>
alovaInstance.Get<EntityItem[]>('/rest/entities', {
name: 'entities',
transformData(data: any) {
return data.entities.map((ei: EntityItem) => ({
...ei,
o_id: ei.id,
o_device_id: ei.device_id,
o_type_id: ei.type_id,
o_offset: ei.offset,
o_factor: ei.factor,
o_uom: ei.uom,
o_value_type: ei.value_type,
o_name: ei.name,
o_writeable: ei.writeable,
o_deleted: ei.deleted
}));
}
});
export const writeEntities = (data: any) => alovaInstance.Post('/rest/entities', data);

View File

@@ -131,7 +131,6 @@ export interface DeviceValue {
m?: number; // min, optional
x?: number; // max, optional
}
export interface DeviceData {
data: DeviceValue[];
}
@@ -151,15 +150,6 @@ export interface DeviceEntity {
o_ma?: number; // original max value
}
export interface CustomEntities {
id: number;
entity_ids: string[];
}
export interface UniqueID {
id: number;
}
export enum DeviceValueUOM {
NONE = 0,
DEGREES,
@@ -256,10 +246,6 @@ export const BOARD_PROFILES: BoardProfiles = {
S3MINI: 'Liligo S3'
};
export interface BoardProfileName {
board_profile: string;
}
export interface BoardProfile {
board_profile: string;
led_gpio: number;
@@ -278,12 +264,6 @@ export interface APIcall {
entity: string;
id: any;
}
export interface WriteDeviceValue {
id: number;
devicevalue: DeviceValue;
}
export interface WriteAnalogSensor {
id: number;
gpio: number;
@@ -312,7 +292,7 @@ export interface ScheduleItem {
time: string;
cmd: string;
value: string;
name?: string; // optional
name: string; // optional
o_id?: number;
o_active?: boolean;
o_deleted?: boolean;
@@ -323,10 +303,6 @@ export interface ScheduleItem {
o_name?: string;
}
export interface Schedule {
schedule: ScheduleItem[];
}
export enum ScheduleFlag {
SCHEDULE_SUN = 1,
SCHEDULE_MON = 2,

View File

@@ -1,5 +1,5 @@
import Schema from 'async-validator';
import type { AnalogSensor, DeviceValue, Settings } from './types';
import type { AnalogSensor, DeviceValue, ScheduleItem, Settings } from './types';
import type { InternalRuleItem } from 'async-validator';
import { IP_OR_HOSTNAME_VALIDATOR } from 'validators/shared';
@@ -86,14 +86,26 @@ export const createSettingsValidator = (settings: Settings) =>
})
});
export const schedulerItemValidation = () =>
export const uniqueNameValidator = (schedule: ScheduleItem[], o_name?: string) => ({
validator(rule: InternalRuleItem, name: string, callback: (error?: string) => void) {
if ((o_name === undefined || o_name !== name) && schedule.find((si) => si.name === name)) {
callback('Name already in use');
} else {
callback();
}
}
});
export const schedulerItemValidation = (schedule: ScheduleItem[], scheduleItem: ScheduleItem) =>
new Schema({
name: [
{
required: true,
type: 'string',
pattern: /^[a-zA-Z0-9_\\.]{0,15}$/,
message: "Must be <15 characters: alpha numeric, '_' or '.'"
}
},
...[uniqueNameValidator(schedule, scheduleItem.o_name)]
],
cmd: [
{ required: true, message: 'Command is required' },