refactor dialog to prevent multiple parent renders

This commit is contained in:
Proddy
2023-04-18 22:04:00 +02:00
parent 8eb7793cd0
commit 6bd744f12e
23 changed files with 622 additions and 501 deletions

17
.vscode/settings.json vendored
View File

@@ -7,5 +7,20 @@
"eslint.workingDirectories": ["interface"], "eslint.workingDirectories": ["interface"],
"prettier.prettierPath": "interface/.yarn/sdks/prettier/index.js", "prettier.prettierPath": "interface/.yarn/sdks/prettier/index.js",
"typescript.tsdk": "interface/.yarn/sdks/typescript/lib", "typescript.tsdk": "interface/.yarn/sdks/typescript/lib",
"typescript.enablePromptUseWorkspaceTsdk": true "typescript.enablePromptUseWorkspaceTsdk": true,
"files.associations": {
"*.tsx": "typescriptreact",
"*.tcc": "cpp",
"optional": "cpp",
"istream": "cpp",
"ostream": "cpp",
"ratio": "cpp",
"system_error": "cpp",
"array": "cpp",
"functional": "cpp",
"regex": "cpp",
"tuple": "cpp",
"type_traits": "cpp",
"utility": "cpp"
}
} }

View File

@@ -24,7 +24,7 @@
"tsconfigRootDir": ".", "tsconfigRootDir": ".",
"project": ["tsconfig.json"] "project": ["tsconfig.json"]
}, },
"plugins": ["react", "@typescript-eslint"], "plugins": ["react", "@typescript-eslint", "autofix", "react-hooks"],
"settings": { "settings": {
"import/resolver": { "import/resolver": {
"typescript": { "typescript": {
@@ -36,7 +36,6 @@
} }
}, },
"rules": { "rules": {
"react-hooks/exhaustive-deps": "off",
"object-shorthand": "error", "object-shorthand": "error",
"no-console": "warn", "no-console": "warn",
"@typescript-eslint/consistent-type-definitions": ["off", "type"], "@typescript-eslint/consistent-type-definitions": ["off", "type"],
@@ -52,6 +51,29 @@
"@typescript-eslint/no-unused-expressions": "off", "@typescript-eslint/no-unused-expressions": "off",
"@typescript-eslint/no-implied-eval": "off", "@typescript-eslint/no-implied-eval": "off",
"@typescript-eslint/no-misused-promises": "off", "@typescript-eslint/no-misused-promises": "off",
"arrow-body-style": ["error", "as-needed"],
"react-hooks/exhaustive-deps": "error",
"@typescript-eslint/consistent-type-imports": [
"error",
{
"prefer": "type-imports"
}
],
// "autofix/no-unused-vars": [
// "error",
// {
// "argsIgnorePattern": "^_",
// "ignoreRestSiblings": true,
// "destructuredArrayIgnorePattern": "^_"
// }
// ],
"react/self-closing-comp": [
"error",
{
"component": true,
"html": true
}
],
"@typescript-eslint/ban-types": [ "@typescript-eslint/ban-types": [
"error", "error",
{ {

View File

@@ -23,12 +23,12 @@
"@emotion/styled": "^11.10.6", "@emotion/styled": "^11.10.6",
"@msgpack/msgpack": "^3.0.0-beta2", "@msgpack/msgpack": "^3.0.0-beta2",
"@mui/icons-material": "^5.11.16", "@mui/icons-material": "^5.11.16",
"@mui/material": "^5.12.0", "@mui/material": "^5.12.1",
"@remix-run/router": "^1.5.0", "@remix-run/router": "^1.5.0",
"@table-library/react-table-library": "4.1.0", "@table-library/react-table-library": "4.1.0",
"@types/lodash-es": "^4.17.7", "@types/lodash-es": "^4.17.7",
"@types/node": "^18.15.11", "@types/node": "^18.15.11",
"@types/react": "^18.0.34", "@types/react": "^18.0.37",
"@types/react-dom": "^18.0.11", "@types/react-dom": "^18.0.11",
"@types/react-router-dom": "^5.3.3", "@types/react-router-dom": "^5.3.3",
"@yarnpkg/pnpify": "^4.0.0-rc.42", "@yarnpkg/pnpify": "^4.0.0-rc.42",
@@ -51,14 +51,15 @@
"devDependencies": { "devDependencies": {
"@types/mime-types": "^2", "@types/mime-types": "^2",
"@types/styled-components": "^5", "@types/styled-components": "^5",
"@typescript-eslint/eslint-plugin": "^5.58.0", "@typescript-eslint/eslint-plugin": "^5.59.0",
"@typescript-eslint/parser": "^5.58.0", "@typescript-eslint/parser": "^5.59.0",
"@vitejs/plugin-react-swc": "^3.3.0", "@vitejs/plugin-react-swc": "^3.3.0",
"eslint": "^8.38.0", "eslint": "^8.38.0",
"eslint-config-airbnb": "^19.0.4", "eslint-config-airbnb": "^19.0.4",
"eslint-config-airbnb-typescript": "^17.0.0", "eslint-config-airbnb-typescript": "^17.0.0",
"eslint-config-prettier": "^8.8.0", "eslint-config-prettier": "^8.8.0",
"eslint-import-resolver-typescript": "^3.5.5", "eslint-import-resolver-typescript": "^3.5.5",
"eslint-plugin-autofix": "^1.1.0",
"eslint-plugin-import": "^2.27.5", "eslint-plugin-import": "^2.27.5",
"eslint-plugin-jsx-a11y": "^6.7.1", "eslint-plugin-jsx-a11y": "^6.7.1",
"eslint-plugin-prettier": "^4.2.1", "eslint-plugin-prettier": "^4.2.1",
@@ -68,11 +69,11 @@
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"prettier": "^2.8.7", "prettier": "^2.8.7",
"rollup-plugin-visualizer": "^5.9.0", "rollup-plugin-visualizer": "^5.9.0",
"terser": "^5.16.9", "terser": "^5.17.0",
"vite": "^4.2.1", "vite": "^4.2.2",
"vite-plugin-minify": "^1.5.2", "vite-plugin-minify": "^1.5.2",
"vite-plugin-svgr": "^2.4.0", "vite-plugin-svgr": "^2.4.0",
"vite-tsconfig-paths": "^4.1.0" "vite-tsconfig-paths": "^4.2.0"
}, },
"packageManager": "yarn@3.4.1" "packageManager": "yarn@3.4.1"
} }

View File

@@ -47,7 +47,7 @@ const WiFiNetworkSelector: FC<WiFiNetworkSelectorProps> = ({ networkList }) => {
const renderNetwork = (network: WiFiNetwork) => { const renderNetwork = (network: WiFiNetwork) => {
return ( return (
<ListItem key={network.bssid} button onClick={() => wifiConnectionContext.selectNetwork(network)}> <ListItem key={network.bssid} onClick={() => wifiConnectionContext.selectNetwork(network)}>
<ListItemAvatar> <ListItemAvatar>
<Avatar>{isNetworkOpen(network) ? <LockOpenIcon /> : <LockIcon />}</Avatar> <Avatar>{isNetworkOpen(network) ? <LockOpenIcon /> : <LockIcon />}</Avatar>
</ListItemAvatar> </ListItemAvatar>

View File

@@ -311,7 +311,7 @@ const de: Translation = {
LEAVE: 'Verlassen', LEAVE: 'Verlassen',
SCHEDULER: 'Planer', SCHEDULER: 'Planer',
SCHEDULER_HELP_1: 'Fügen Sie eigene, geplante Befehle zur Automatisierung hinzu. Vergeben Sie einen Entitätsnamen um die Aktivierung über API/Mqtt zu steuern', SCHEDULER_HELP_1: 'Fügen Sie eigene, geplante Befehle zur Automatisierung hinzu. Vergeben Sie einen Entitätsnamen um die Aktivierung über API/Mqtt zu steuern',
SCHEDULER_HELP_2: 'Use 00:00 to trigger on boot', // TODO translate SCHEDULER_HELP_2: 'Use 00:00 to trigger once on start-up', // TODO translate
SCHEDULE: 'Zeitplan', SCHEDULE: 'Zeitplan',
TIME: 'Zeit', TIME: 'Zeit',
TIMER: 'Timer', TIMER: 'Timer',
@@ -320,7 +320,8 @@ const de: Translation = {
SCHEDULE_TIMER_2: 'jede Minute', SCHEDULE_TIMER_2: 'jede Minute',
SCHEDULE_TIMER_3: 'jede Stunde', SCHEDULE_TIMER_3: 'jede Stunde',
CUSTOM_ENTITIES: 'Individuelle Entitäten', CUSTOM_ENTITIES: 'Individuelle Entitäten',
ENTITIES_HELP_1: 'Abfrage von Werten auf dem EMS-Bus' ENTITIES_HELP_1: 'Abfrage von Werten auf dem EMS-Bus',
WRITEABLE: 'Schreibbar'
}; };
export default de; export default de;

View File

@@ -320,7 +320,8 @@ const en: Translation = {
SCHEDULE_TIMER_2: 'every minute', SCHEDULE_TIMER_2: 'every minute',
SCHEDULE_TIMER_3: 'every hour', SCHEDULE_TIMER_3: 'every hour',
CUSTOM_ENTITIES: 'Custom Entities', CUSTOM_ENTITIES: 'Custom Entities',
ENTITIES_HELP_1: 'Fetch custom entities from the EMS bus' ENTITIES_HELP_1: 'Fetch custom entities from the EMS bus',
WRITEABLE: 'Writeable'
}; };
export default en; export default en;

View File

@@ -320,7 +320,8 @@ const fr: Translation = {
SCHEDULE_TIMER_2: 'every minute', // TODO translate SCHEDULE_TIMER_2: 'every minute', // TODO translate
SCHEDULE_TIMER_3: 'every hour', // TODO translate SCHEDULE_TIMER_3: 'every hour', // TODO translate
CUSTOM_ENTITIES: 'Custom Entities', // TODO translate CUSTOM_ENTITIES: 'Custom Entities', // TODO translate
ENTITIES_HELP_1: 'Fetch custom entities from the EMS bus' // TODO translate ENTITIES_HELP_1: 'Fetch custom entities from the EMS bus', // TODO translate
WRITEABLE: 'Writeable' // TODO translate
}; };
export default fr; export default fr;

View File

@@ -320,7 +320,8 @@ const nl: Translation = {
SCHEDULE_TIMER_2: 'every minute', // TODO translate SCHEDULE_TIMER_2: 'every minute', // TODO translate
SCHEDULE_TIMER_3: 'every hour', // TODO translate SCHEDULE_TIMER_3: 'every hour', // TODO translate
CUSTOM_ENTITIES: 'Custom Entities', // TODO translate CUSTOM_ENTITIES: 'Custom Entities', // TODO translate
ENTITIES_HELP_1: 'Fetch custom entities from the EMS bus' // TODO translate ENTITIES_HELP_1: 'Fetch custom entities from the EMS bus', // TODO translate
WRITEABLE: 'Writeable' // TODO translate
}; };
export default nl; export default nl;

View File

@@ -320,7 +320,8 @@ const no: Translation = {
SCHEDULE_TIMER_2: 'hvert minutt', SCHEDULE_TIMER_2: 'hvert minutt',
SCHEDULE_TIMER_3: 'hver time', SCHEDULE_TIMER_3: 'hver time',
CUSTOM_ENTITIES: 'Custom Entities', // TODO translate CUSTOM_ENTITIES: 'Custom Entities', // TODO translate
ENTITIES_HELP_1: 'Fetch custom entities from the EMS bus' // TODO translate ENTITIES_HELP_1: 'Fetch custom entities from the EMS bus', // TODO translate
WRITEABLE: 'Writeable' // TODO translate
}; };
export default no; export default no;

View File

@@ -320,7 +320,8 @@ const pl: BaseTranslation = {
SCHEDULE_TIMER_2: 'co minutę', SCHEDULE_TIMER_2: 'co minutę',
SCHEDULE_TIMER_3: 'co godzinę', SCHEDULE_TIMER_3: 'co godzinę',
CUSTOM_ENTITIES: 'Custom Entities', // TODO translate CUSTOM_ENTITIES: 'Custom Entities', // TODO translate
ENTITIES_HELP_1: 'Fetch custom entities from the EMS bus' // TODO translate ENTITIES_HELP_1: 'Fetch custom entities from the EMS bus',
WRITEABLE: 'Writeable' // TODO translate
}; };
export default pl; export default pl;

View File

@@ -320,7 +320,9 @@ const sv: Translation = {
SCHEDULE_TIMER_2: 'every minute', // TODO translate SCHEDULE_TIMER_2: 'every minute', // TODO translate
SCHEDULE_TIMER_3: 'every hour', // TODO translate SCHEDULE_TIMER_3: 'every hour', // TODO translate
CUSTOM_ENTITIES: 'Custom Entities', // TODO translate CUSTOM_ENTITIES: 'Custom Entities', // TODO translate
ENTITIES_HELP_1: 'Fetch custom entities from the EMS bus' // TODO translate ENTITIES_HELP_1: 'Fetch custom entities from the EMS bus', // TODO translate
WRITEABLE: 'Writeable' // TODO translate
}; };
export default sv; export default sv;

View File

@@ -320,7 +320,8 @@ const tr: Translation = {
SCHEDULE_TIMER_2: 'every minute', // TODO translate SCHEDULE_TIMER_2: 'every minute', // TODO translate
SCHEDULE_TIMER_3: 'every hour', // TODO translate SCHEDULE_TIMER_3: 'every hour', // TODO translate
CUSTOM_ENTITIES: 'Custom Entities', // TODO translate CUSTOM_ENTITIES: 'Custom Entities', // TODO translate
ENTITIES_HELP_1: 'Fetch custom entities from the EMS bus' // TODO translate ENTITIES_HELP_1: 'Fetch custom entities from the EMS bus', // TODO translate
WRITEABLE: 'Writeable' // TODO translate
}; };
export default tr; export default tr;

View File

@@ -1,71 +1,40 @@
import { FC, useState, useEffect, useCallback } from 'react'; import type { FC } from 'react';
import { useState, useEffect, useCallback } from 'react';
import { unstable_useBlocker as useBlocker } from 'react-router-dom'; import { unstable_useBlocker as useBlocker } from 'react-router-dom';
import { import { Button, Typography, Box } from '@mui/material';
Button,
Typography,
Box,
Grid,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
MenuItem,
InputAdornment
} from '@mui/material';
import { useTheme } from '@table-library/react-table-library/theme'; import { useTheme } from '@table-library/react-table-library/theme';
import { Table, Header, HeaderRow, HeaderCell, Body, Row, Cell } from '@table-library/react-table-library/table'; import { Table, Header, HeaderRow, HeaderCell, Body, Row, Cell } from '@table-library/react-table-library/table';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import RemoveIcon from '@mui/icons-material/RemoveCircleOutline';
import WarningIcon from '@mui/icons-material/Warning'; import WarningIcon from '@mui/icons-material/Warning';
import CancelIcon from '@mui/icons-material/Cancel'; import CancelIcon from '@mui/icons-material/Cancel';
import DoneIcon from '@mui/icons-material/Done';
import AddIcon from '@mui/icons-material/Add'; import AddIcon from '@mui/icons-material/Add';
import { ValidatedTextField, ButtonRow, FormLoader, SectionContent, BlockNavigation } from 'components'; import { ButtonRow, FormLoader, SectionContent, BlockNavigation } from 'components';
import { DeviceValueUOM_s, EntityItem } from './types'; import SettingsEntitiesDialog from './SettingsEntitiesDialog';
import { extractErrorMessage, updateValue } from 'utils';
import { validate } from 'validators'; import type { EntityItem } from './types';
import { entityItemValidation } from './validators'; import { DeviceValueUOM_s } from './types';
import { ValidateFieldsError } from 'async-validator'; import { extractErrorMessage } from 'utils';
import { useI18nContext } from 'i18n/i18n-react'; import { useI18nContext } from 'i18n/i18n-react';
import * as EMSESP from './api'; import * as EMSESP from './api';
import { entityItemValidation } from './validators';
function makeid() {
return Math.floor(Math.random() * (Math.floor(200) - 100) + 100);
}
const SettingsEntities: FC = () => { const SettingsEntities: FC = () => {
const { LL } = useI18nContext(); const { LL } = useI18nContext();
const [numChanges, setNumChanges] = useState<number>(0); const [numChanges, setNumChanges] = useState<number>(0);
const blocker = useBlocker(numChanges !== 0); const blocker = useBlocker(numChanges !== 0);
const [entities, setEntities] = useState<EntityItem[]>([]);
const emptyEntity = { const [selectedEntityItem, setSelectedEntityItem] = useState<EntityItem>();
id: 0,
name: '',
device_id: 8,
type_id: 0,
offset: 0,
factor: 1,
uom: 0,
val_type: 2,
deleted: false,
o_name: ''
};
const [entities, setEntities] = useState<EntityItem[]>([emptyEntity]);
const [entityItem, setEntityItem] = useState<EntityItem>();
const [errorMessage, setErrorMessage] = useState<string>(); const [errorMessage, setErrorMessage] = useState<string>();
const [creating, setCreating] = useState<boolean>(false); const [creating, setCreating] = useState<boolean>(false);
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>(); const [dialogOpen, setDialogOpen] = useState<boolean>(false);
function hasEntityChanged(ei: EntityItem) { function hasEntityChanged(ei: EntityItem) {
return ( return (
@@ -76,21 +45,15 @@ const SettingsEntities: FC = () => {
ei.offset !== ei.o_offset || ei.offset !== ei.o_offset ||
ei.uom !== ei.o_uom || ei.uom !== ei.o_uom ||
ei.factor !== ei.o_factor || ei.factor !== ei.o_factor ||
ei.val_type !== ei.o_val_type || ei.value_type !== ei.o_value_type ||
ei.writeable !== ei.o_writeable ||
ei.deleted !== ei.o_deleted ei.deleted !== ei.o_deleted
); );
} }
const getNumChanges = () => {
if (!entities) {
return 0;
}
return entities.filter((ei) => hasEntityChanged(ei)).length;
};
useEffect(() => { useEffect(() => {
setNumChanges(getNumChanges()); setNumChanges(entities ? entities.filter((ei) => hasEntityChanged(ei)).length : 0);
}); }, [entities]);
const entity_theme = useTheme({ const entity_theme = useTheme({
Table: ` Table: `
@@ -156,8 +119,9 @@ const SettingsEntities: FC = () => {
o_offset: ei.offset, o_offset: ei.offset,
o_factor: ei.factor, o_factor: ei.factor,
o_uom: ei.uom, o_uom: ei.uom,
o_val_type: ei.val_type, o_value_type: ei.value_type,
o_name: ei.name, o_name: ei.name,
o_writeable: ei.writeable,
o_deleted: ei.deleted o_deleted: ei.deleted
})) }))
); );
@@ -182,19 +146,19 @@ const SettingsEntities: FC = () => {
const response = await EMSESP.writeEntities({ const response = await EMSESP.writeEntities({
entities: entities entities: entities
.filter((ei) => !ei.deleted) .filter((ei) => !ei.deleted)
.map((condensed_ei) => { .map((condensed_ei) => ({
return { id: condensed_ei.id,
id: condensed_ei.id, name: condensed_ei.name,
name: condensed_ei.name, device_id: condensed_ei.device_id,
device_id: condensed_ei.device_id, type_id: condensed_ei.type_id,
type_id: condensed_ei.type_id, offset: condensed_ei.offset,
offset: condensed_ei.offset, factor: condensed_ei.factor,
factor: condensed_ei.factor, uom: condensed_ei.uom,
uom: condensed_ei.uom, writeable: condensed_ei.writeable,
val_type: condensed_ei.val_type value_type: condensed_ei.value_type
}; }))
})
}); });
if (response.status === 200) { if (response.status === 200) {
toast.success(LL.SUCCESS()); toast.success(LL.SUCCESS());
} else { } else {
@@ -207,42 +171,48 @@ const SettingsEntities: FC = () => {
} }
}; };
const editEntityItem = (ei: EntityItem) => { const editEntityItem = useCallback((ei: EntityItem) => {
setCreating(false); setCreating(false);
setEntityItem(ei); setSelectedEntityItem(ei);
setDialogOpen(true);
}, []);
const onDialogClose = () => {
setDialogOpen(false);
}; };
const onDialogSave = (updatedItem: EntityItem) => {
if (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)));
}
setDialogOpen(false);
};
// TODO need callback here too?
const addEntityItem = () => { const addEntityItem = () => {
setCreating(true); setCreating(true);
setEntityItem({ setSelectedEntityItem({
id: makeid(), id: Math.floor(Math.random() * (Math.floor(200) - 100) + 100),
device_id: 11, device_id: 11,
type_id: 0, type_id: 0,
offset: 0, offset: 0,
factor: 1, factor: 1,
val_type: 2, value_type: 2,
uom: 0, uom: 0,
name: '', name: '',
writeable: false,
deleted: false deleted: false
}); });
}; setDialogOpen(true);
const updateEntityItem = () => {
console.log('here');
if (entityItem) {
setEntities([...entities.filter((ei) => creating || ei.o_id !== entityItem.o_id), entityItem]);
}
setEntityItem(undefined);
}; };
function formatValue(value: any, uom: number) { function formatValue(value: any, uom: number) {
if (value === undefined) { if (value === undefined) {
return ''; return '';
} }
if (uom === 0) { return new Intl.NumberFormat().format(value) + (uom === 0 ? '' : ' ' + DeviceValueUOM_s[uom]);
return new Intl.NumberFormat().format(value);
}
return new Intl.NumberFormat().format(value) + ' ' + DeviceValueUOM_s[uom];
} }
function showHex(value: number, digit: number) { function showHex(value: number, digit: number) {
@@ -287,185 +257,6 @@ const SettingsEntities: FC = () => {
); );
}; };
const removeEntityItem = (ei: EntityItem) => {
ei.deleted = true;
setEntityItem(ei);
updateEntityItem();
};
const validateEntityItem = async () => {
if (entityItem) {
console.log(1);
try {
setFieldErrors(undefined);
console.log(2);
await validate(entityItemValidation(entities, entityItem), entityItem);
console.log(3);
updateEntityItem();
} catch (errors: any) {
console.log(4);
console.log(errors);
setFieldErrors(errors);
}
}
};
const closeDialog = () => {
setEntityItem(undefined);
setFieldErrors(undefined);
};
const renderEditEntity = () => {
if (entityItem) {
return (
<Dialog open={!!entityItem} onClose={() => closeDialog()}>
<DialogTitle>
{creating ? LL.ADD(1) + ' ' + LL.NEW() : LL.EDIT()}&nbsp;{LL.ENTITY()}
</DialogTitle>
<DialogContent dividers>
<Box display="flex" flexWrap="wrap" mb={1}>
<Box flexGrow={1}></Box>
<Box flexWrap="nowrap" whiteSpace="nowrap"></Box>
</Box>
<Grid container spacing={2}>
<Grid item xs={12}>
<ValidatedTextField
fieldErrors={fieldErrors}
name="name"
label={LL.NAME(0)}
value={entityItem.name}
margin="normal"
fullWidth
// onChange={updateValue(setEntityItem)}
/>
</Grid>
<Grid item xs={4}>
<ValidatedTextField
fieldErrors={fieldErrors}
name="device_id"
label="Device ID"
margin="normal"
fullWidth
value={entityItem.device_id}
onChange={updateValue(setEntityItem)}
InputProps={{
startAdornment: <InputAdornment position="start">0x</InputAdornment>
}}
/>
</Grid>
<Grid item xs={4}>
<ValidatedTextField
fieldErrors={fieldErrors}
name="type_id"
label="Type ID"
margin="normal"
fullWidth
value={entityItem.type_id}
onChange={updateValue(setEntityItem)}
InputProps={{
startAdornment: <InputAdornment position="start">0x</InputAdornment>
}}
/>
</Grid>
<Grid item xs={4}>
<ValidatedTextField
fieldErrors={fieldErrors}
name="offset"
label="Offset"
margin="normal"
fullWidth
type="number"
value={entityItem.offset}
onChange={updateValue(setEntityItem)}
/>
</Grid>
<Grid item xs={4}>
<ValidatedTextField
name="val_type"
label="Value Type"
value={entityItem.val_type}
variant="outlined"
onChange={updateValue(setEntityItem)}
margin="normal"
fullWidth
select
>
<MenuItem value={0}>BOOL</MenuItem>
<MenuItem value={1}>INT</MenuItem>
<MenuItem value={2}>UINT</MenuItem>
<MenuItem value={3}>SHORT</MenuItem>
<MenuItem value={4}>USHORT</MenuItem>
<MenuItem value={5}>ULONG</MenuItem>
<MenuItem value={6}>TIME</MenuItem>
</ValidatedTextField>
</Grid>
{entityItem.val_type !== 0 && (
<>
<Grid item xs={4}>
<ValidatedTextField
name="factor"
label={LL.FACTOR()}
value={entityItem.factor}
variant="outlined"
onChange={updateValue(setEntityItem)}
fullWidth
margin="normal"
type="number"
inputProps={{ step: '0.001' }}
/>
</Grid>
<Grid item xs={4}>
<ValidatedTextField
name="uom"
label={LL.UNIT()}
value={entityItem.uom}
margin="normal"
fullWidth
onChange={updateValue(setEntityItem)}
select
>
{DeviceValueUOM_s.map((val, i) => (
<MenuItem key={i} value={i}>
{val}
</MenuItem>
))}
</ValidatedTextField>
</Grid>
</>
)}
</Grid>
</DialogContent>
<DialogActions>
{!creating && (
<Box flexGrow={1}>
<Button
startIcon={<RemoveIcon />}
variant="outlined"
color="error"
onClick={() => removeEntityItem(entityItem)}
>
{LL.REMOVE()}
</Button>
</Box>
)}
<Button startIcon={<CancelIcon />} variant="outlined" onClick={() => closeDialog()} color="secondary">
{LL.CANCEL()}
</Button>
<Button
startIcon={creating ? <AddIcon /> : <DoneIcon />}
variant="outlined"
type="submit"
onClick={() => validateEntityItem()}
color="primary"
>
{creating ? LL.ADD(0) : LL.UPDATE()}
</Button>
</DialogActions>
</Dialog>
);
}
};
return ( return (
<SectionContent title={LL.CUSTOM_ENTITIES()} titleGutter> <SectionContent title={LL.CUSTOM_ENTITIES()} titleGutter>
{blocker ? <BlockNavigation blocker={blocker} /> : null} {blocker ? <BlockNavigation blocker={blocker} /> : null}
@@ -473,10 +264,21 @@ const SettingsEntities: FC = () => {
<Typography variant="body2">{LL.ENTITIES_HELP_1()}</Typography> <Typography variant="body2">{LL.ENTITIES_HELP_1()}</Typography>
</Box> </Box>
{renderEntity()} {renderEntity()}
{renderEditEntity()}
{selectedEntityItem && (
<SettingsEntitiesDialog
open={dialogOpen}
creating={creating}
onClose={onDialogClose}
onSave={onDialogSave}
selectedEntityItem={selectedEntityItem}
validator={entityItemValidation(entities, creating)}
/>
)}
<Box display="flex" flexWrap="wrap"> <Box display="flex" flexWrap="wrap">
<Box flexGrow={1}> <Box flexGrow={1}>
{numChanges !== 0 && ( {numChanges > 0 && (
<ButtonRow> <ButtonRow>
<Button startIcon={<CancelIcon />} variant="outlined" onClick={() => fetchEntities()} color="secondary"> <Button startIcon={<CancelIcon />} variant="outlined" onClick={() => fetchEntities()} color="secondary">
{LL.CANCEL()} {LL.CANCEL()}

View File

@@ -0,0 +1,228 @@
import { useState, useEffect } from 'react';
import {
Grid,
Button,
Box,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
Checkbox,
InputAdornment,
MenuItem
} from '@mui/material';
import { ValidatedTextField, BlockFormControlLabel } from 'components';
import DoneIcon from '@mui/icons-material/Done';
import RemoveIcon from '@mui/icons-material/RemoveCircleOutline';
import CancelIcon from '@mui/icons-material/Cancel';
import AddIcon from '@mui/icons-material/Add';
import { useI18nContext } from 'i18n/i18n-react';
import { validate } from 'validators';
import type { ValidateFieldsError } from 'async-validator';
import type Schema from 'async-validator';
import { updateValue } from 'utils';
import type { EntityItem } from './types';
import { DeviceValueUOM_s } from './types';
type SettingsEntitiesDialogProps = {
open: boolean;
creating: boolean;
onClose: () => void;
onSave: (ei: EntityItem) => void;
selectedEntityItem: EntityItem;
validator: Schema;
};
const SettingsEntitiesDialog = ({
open,
creating,
onClose,
onSave,
selectedEntityItem,
validator
}: SettingsEntitiesDialogProps) => {
const { LL } = useI18nContext();
const [editItem, setEditItem] = useState<EntityItem>(selectedEntityItem);
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
const updateFormValue = updateValue(setEditItem);
useEffect(() => {
if (open) {
setFieldErrors(undefined);
setEditItem(selectedEntityItem);
}
}, [open, selectedEntityItem]);
const close = () => {
onClose();
};
const save = async () => {
try {
setFieldErrors(undefined);
await validate(validator, editItem);
onSave(editItem);
} catch (errors: any) {
setFieldErrors(errors);
}
};
const remove = () => {
editItem.deleted = true;
onSave(editItem);
};
return (
<Dialog open={open} onClose={close}>
<DialogTitle>
{creating ? LL.ADD(1) + ' ' + LL.NEW() : LL.EDIT()}&nbsp;{LL.ENTITY()}
</DialogTitle>
<DialogContent dividers>
<Box display="flex" flexWrap="wrap" mb={1}>
<Box flexWrap="nowrap" whiteSpace="nowrap" />
</Box>
<Grid container spacing={2}>
<Grid item xs={8}>
<ValidatedTextField
fieldErrors={fieldErrors}
name="name"
label={LL.NAME(0)}
value={editItem.name}
margin="normal"
fullWidth
onChange={updateFormValue}
/>
</Grid>
<Grid item xs={4} mt={3}>
<BlockFormControlLabel
control={<Checkbox checked={selectedEntityItem.writeable} onChange={updateFormValue} name="write" />}
label={LL.WRITEABLE()}
/>
</Grid>
<Grid item xs={4}>
<ValidatedTextField
fieldErrors={fieldErrors}
name="device_id"
label="Device ID"
margin="normal"
fullWidth
value={editItem.device_id}
onChange={updateFormValue}
InputProps={{
startAdornment: <InputAdornment position="start">0x</InputAdornment>
}}
/>
</Grid>
<Grid item xs={4}>
<ValidatedTextField
fieldErrors={fieldErrors}
name="type_id"
label="Type ID"
margin="normal"
fullWidth
value={editItem.type_id}
onChange={updateFormValue}
InputProps={{
startAdornment: <InputAdornment position="start">0x</InputAdornment>
}}
/>
</Grid>
<Grid item xs={4}>
<ValidatedTextField
fieldErrors={fieldErrors}
name="offset"
label="Offset"
margin="normal"
fullWidth
type="number"
value={editItem.offset}
onChange={updateFormValue}
/>
</Grid>
<Grid item xs={4}>
<ValidatedTextField
name="val_type"
label="Value Type"
value={editItem.value_type}
variant="outlined"
onChange={updateFormValue}
margin="normal"
fullWidth
select
>
<MenuItem value={0}>BOOL</MenuItem>
<MenuItem value={1}>INT</MenuItem>
<MenuItem value={2}>UINT</MenuItem>
<MenuItem value={3}>SHORT</MenuItem>
<MenuItem value={4}>USHORT</MenuItem>
<MenuItem value={5}>ULONG</MenuItem>
<MenuItem value={6}>TIME</MenuItem>
</ValidatedTextField>
</Grid>
{selectedEntityItem.value_type !== 0 && (
<>
<Grid item xs={4}>
<ValidatedTextField
name="factor"
label={LL.FACTOR()}
value={editItem.factor}
variant="outlined"
onChange={updateFormValue}
fullWidth
margin="normal"
type="number"
inputProps={{ step: '0.001' }}
/>
</Grid>
<Grid item xs={4}>
<ValidatedTextField
name="uom"
label={LL.UNIT()}
value={editItem.uom}
margin="normal"
fullWidth
onChange={updateFormValue}
select
>
{DeviceValueUOM_s.map((val, i) => (
<MenuItem key={i} value={i}>
{val}
</MenuItem>
))}
</ValidatedTextField>
</Grid>
</>
)}
</Grid>
</DialogContent>
<DialogActions>
{!creating && (
<Box flexGrow={1}>
<Button startIcon={<RemoveIcon />} variant="outlined" color="warning" onClick={remove}>
{LL.REMOVE()}
</Button>
</Box>
)}
<Button startIcon={<CancelIcon />} variant="outlined" onClick={close} color="secondary">
{LL.CANCEL()}
</Button>
<Button startIcon={creating ? <AddIcon /> : <DoneIcon />} variant="outlined" onClick={save} color="primary">
{creating ? LL.ADD(0) : LL.UPDATE()}
</Button>
</DialogActions>
</Dialog>
);
};
export default SettingsEntitiesDialog;

View File

@@ -1,4 +1,5 @@
import { FC, useState, useEffect, useCallback } from 'react'; import type { FC } from 'react';
import { useState, useEffect, useCallback } from 'react';
import { unstable_useBlocker as useBlocker } from 'react-router-dom'; import { unstable_useBlocker as useBlocker } from 'react-router-dom';
import { import {
@@ -43,9 +44,10 @@ import { extractErrorMessage, updateValue } from 'utils';
import { validate } from 'validators'; import { validate } from 'validators';
import { schedulerItemValidation } from './validators'; import { schedulerItemValidation } from './validators';
import { ValidateFieldsError } from 'async-validator'; import type { ValidateFieldsError } from 'async-validator';
import { ScheduleItem, ScheduleFlag } from './types'; import type { ScheduleItem } from './types';
import { ScheduleFlag } from './types';
import { useI18nContext } from 'i18n/i18n-react'; import { useI18nContext } from 'i18n/i18n-react';
@@ -105,6 +107,7 @@ const SettingsScheduler: FC = () => {
); );
} }
// TODO fix
const getNumChanges = () => { const getNumChanges = () => {
if (!schedule) { if (!schedule) {
return 0; return 0;
@@ -184,12 +187,12 @@ const SettingsScheduler: FC = () => {
} catch (error) { } catch (error) {
setErrorMessage(extractErrorMessage(error, LL.PROBLEM_LOADING())); setErrorMessage(extractErrorMessage(error, LL.PROBLEM_LOADING()));
} }
setDow(getDayNames());
}, [LL]); }, [LL]);
useEffect(() => { useEffect(() => {
fetchSchedule(); void fetchSchedule();
}, [fetchSchedule]); setDow(getDayNames());
}, [getDayNames, fetchSchedule]);
const getFlagNumber = (newFlag: string[]) => { const getFlagNumber = (newFlag: string[]) => {
let new_flag = 0; let new_flag = 0;
@@ -234,17 +237,15 @@ const SettingsScheduler: FC = () => {
const response = await EMSESP.writeSchedule({ const response = await EMSESP.writeSchedule({
schedule: schedule schedule: schedule
.filter((si) => !si.deleted) .filter((si) => !si.deleted)
.map((condensed_si) => { .map((condensed_si) => ({
return { id: condensed_si.id,
id: condensed_si.id, active: condensed_si.active,
active: condensed_si.active, flags: condensed_si.flags,
flags: condensed_si.flags, time: condensed_si.time,
time: condensed_si.time, cmd: condensed_si.cmd,
cmd: condensed_si.cmd, value: condensed_si.value,
value: condensed_si.value, name: condensed_si.name
name: condensed_si.name }))
};
})
}); });
if (response.status === 200) { if (response.status === 200) {
toast.success(LL.SCHEDULE_SAVED()); toast.success(LL.SCHEDULE_SAVED());

View File

@@ -346,8 +346,9 @@ export interface EntityItem {
offset: number; offset: number;
factor: number; factor: number;
uom: number; uom: number;
val_type: number; value_type: number;
value?: number; value?: number;
writeable: boolean;
deleted?: boolean; // optional deleted?: boolean; // optional
o_id?: number; o_id?: number;
o_name?: string; o_name?: string;
@@ -356,8 +357,9 @@ export interface EntityItem {
o_offset?: number; o_offset?: number;
o_factor?: number; o_factor?: number;
o_uom?: number; o_uom?: number;
o_val_type?: number; o_value_type?: number;
o_deleted?: boolean; o_deleted?: boolean;
o_writeable?: boolean;
} }
export interface Entities { export interface Entities {

View File

@@ -1,6 +1,7 @@
import Schema, { InternalRuleItem } from 'async-validator'; import type { InternalRuleItem } from 'async-validator';
import Schema from 'async-validator';
import { IP_OR_HOSTNAME_VALIDATOR } from 'validators/shared'; import { IP_OR_HOSTNAME_VALIDATOR } from 'validators/shared';
import { Settings, ScheduleItem, EntityItem } from './types'; import type { Settings, ScheduleItem, EntityItem } from './types';
export const GPIO_VALIDATOR = { export const GPIO_VALIDATOR = {
validator(rule: InternalRuleItem, value: number, callback: (error?: string) => void) { validator(rule: InternalRuleItem, value: number, callback: (error?: string) => void) {
@@ -111,17 +112,7 @@ export const schedulerItemValidation = (schedule: ScheduleItem[], scheduleItem:
] ]
}); });
export const uniqueEntityNameValidator = (entities: EntityItem[], o_name?: string) => ({ export const entityItemValidation = (entities: EntityItem[], creating: boolean) =>
validator(rule: InternalRuleItem, name: string, callback: (error?: string) => void) {
if (name && o_name && o_name !== name && entities.find((ei) => ei.name === name)) {
callback('Name already in use');
} else {
callback();
}
}
});
export const entityItemValidation = (entities: EntityItem[], entityItem: EntityItem) =>
new Schema({ new Schema({
name: [ name: [
{ required: true, message: 'Name is required' }, { required: true, message: 'Name is required' },
@@ -129,8 +120,7 @@ export const entityItemValidation = (entities: EntityItem[], entityItem: EntityI
type: 'string', type: 'string',
pattern: /^[a-zA-Z0-9_\\.]{1,15}$/, pattern: /^[a-zA-Z0-9_\\.]{1,15}$/,
message: "Must be <15 characters: alpha numeric, '_' or '.'" message: "Must be <15 characters: alpha numeric, '_' or '.'"
}, }
...[uniqueEntityNameValidator(entities, entityItem.o_name)]
], ],
device_id: [{ type: 'hex', required: true, message: 'ID must be a hex value' }] device_id: [{ type: 'hex', required: true, message: 'ID must be a hex value' }]
// type_id: [ // type_id: [

View File

@@ -1,12 +1,13 @@
import Schema, { InternalRuleItem, ValidateOption } from 'async-validator'; import type { InternalRuleItem, ValidateOption } from 'async-validator';
import type Schema from 'async-validator';
export const validate = <T extends object>( export const validate = <T extends object>(
validator: Schema, validator: Schema,
source: Partial<T>, source: Partial<T>,
options?: ValidateOption options?: ValidateOption
): Promise<T> => { ): Promise<T> =>
return new Promise((resolve, reject) => { new Promise((resolve, reject) => {
validator.validate(source, options ? options : {}, (errors, fieldErrors) => { void validator.validate(source, options ? options : {}, (errors, fieldErrors) => {
if (errors) { if (errors) {
reject(fieldErrors); reject(fieldErrors);
} else { } else {
@@ -14,7 +15,6 @@ export const validate = <T extends object>(
} }
}); });
}); });
};
// updated to support both IPv4 and IPv6 // updated to support both IPv4 and IPv6
const IP_ADDRESS_REGEXP = const IP_ADDRESS_REGEXP =

View File

@@ -13,7 +13,8 @@
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"composite": true, "composite": true,
"module": "ESNext", "module": "ESNext",
"moduleResolution": "Node", // "moduleResolution": "Node",
"moduleResolution": "bundler",
"resolveJsonModule": true, "resolveJsonModule": true,
"isolatedModules": true, "isolatedModules": true,
"noEmit": true, "noEmit": true,

View File

@@ -703,9 +703,9 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@mui/base@npm:5.0.0-alpha.125": "@mui/base@npm:5.0.0-alpha.126":
version: 5.0.0-alpha.125 version: 5.0.0-alpha.126
resolution: "@mui/base@npm:5.0.0-alpha.125" resolution: "@mui/base@npm:5.0.0-alpha.126"
dependencies: dependencies:
"@babel/runtime": ^7.21.0 "@babel/runtime": ^7.21.0
"@emotion/is-prop-valid": ^1.2.0 "@emotion/is-prop-valid": ^1.2.0
@@ -722,14 +722,14 @@ __metadata:
peerDependenciesMeta: peerDependenciesMeta:
"@types/react": "@types/react":
optional: true optional: true
checksum: 0a87b5141500885c364382375816d23b48799e38c57993b0bbb2dbfce8052a8bdba588b2cd6cee75dc2fc43a873ce3d27a223ef1395c42a0b2e28d59e559b2bf checksum: afb6b99cba541cdb40bb098a78d105721cd6c6db3156fe9923bbb0aea184f6949ab5db79ab95177b4edb011acb35ac34556124b1203fd91ea16f52dc5477da7b
languageName: node languageName: node
linkType: hard linkType: hard
"@mui/core-downloads-tracker@npm:^5.12.0": "@mui/core-downloads-tracker@npm:^5.12.1":
version: 5.12.0 version: 5.12.1
resolution: "@mui/core-downloads-tracker@npm:5.12.0" resolution: "@mui/core-downloads-tracker@npm:5.12.1"
checksum: b0bc0c67be036fc6b965827ffb2ad2134c317237439dcbbe0b90a1807e92f93894e8d5e6650df4833b5a9b88b28d77cb2cd4435d23bbb9ab751c023684012e5f checksum: 0fc90f840e888e0a671ce43dfaa50018c7f2b82379b3adf3128387c3f29d06cdc235493939698339eb2aab49cf9a167473ed95f6a64eb424ee2d95849dd4aa76
languageName: node languageName: node
linkType: hard linkType: hard
@@ -749,14 +749,14 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@mui/material@npm:^5.12.0": "@mui/material@npm:^5.12.1":
version: 5.12.0 version: 5.12.1
resolution: "@mui/material@npm:5.12.0" resolution: "@mui/material@npm:5.12.1"
dependencies: dependencies:
"@babel/runtime": ^7.21.0 "@babel/runtime": ^7.21.0
"@mui/base": 5.0.0-alpha.125 "@mui/base": 5.0.0-alpha.126
"@mui/core-downloads-tracker": ^5.12.0 "@mui/core-downloads-tracker": ^5.12.1
"@mui/system": ^5.12.0 "@mui/system": ^5.12.1
"@mui/types": ^7.2.4 "@mui/types": ^7.2.4
"@mui/utils": ^5.12.0 "@mui/utils": ^5.12.0
"@types/react-transition-group": ^4.4.5 "@types/react-transition-group": ^4.4.5
@@ -778,7 +778,7 @@ __metadata:
optional: true optional: true
"@types/react": "@types/react":
optional: true optional: true
checksum: d8f2e875393dd254d70aafea08e1289d4cc4d085af581cd8fd4cc2882d5e265b8c926322ac64c1e0d18c5f441969abef2611c87346d685ad18fcfbc27e2d8ddf checksum: a4b4becea4da7af787d58b09afe6a3e1c7b1aa1b27f93186f848efd0bcb250e5b39ca51cdbcedc0216db32049665a69a13a2f8480d357f2310ca6efcf8b10a76
languageName: node languageName: node
linkType: hard linkType: hard
@@ -820,9 +820,9 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@mui/system@npm:^5.12.0": "@mui/system@npm:^5.12.1":
version: 5.12.0 version: 5.12.1
resolution: "@mui/system@npm:5.12.0" resolution: "@mui/system@npm:5.12.1"
dependencies: dependencies:
"@babel/runtime": ^7.21.0 "@babel/runtime": ^7.21.0
"@mui/private-theming": ^5.12.0 "@mui/private-theming": ^5.12.0
@@ -844,7 +844,7 @@ __metadata:
optional: true optional: true
"@types/react": "@types/react":
optional: true optional: true
checksum: d53e70f35b8cc19c687ba72fc79a1f4cc20e0dd0335433fc255bc70291f89460b88bdddb7a9f17d11ffa6de3710117cc666c28ea0ac234fd2da13e82ee3c3c34 checksum: a9dc1e3503f8c036663d0bc38b9bed71f67833890148a29efba5501f0da1f62d0c5372648c840779bdc74ea6e997783441934fa1df90f00ffef158cb6e6d5ce7
languageName: node languageName: node
linkType: hard linkType: hard
@@ -1421,14 +1421,14 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@types/react@npm:^18.0.34": "@types/react@npm:^18.0.37":
version: 18.0.34 version: 18.0.37
resolution: "@types/react@npm:18.0.34" resolution: "@types/react@npm:18.0.37"
dependencies: dependencies:
"@types/prop-types": "*" "@types/prop-types": "*"
"@types/scheduler": "*" "@types/scheduler": "*"
csstype: ^3.0.2 csstype: ^3.0.2
checksum: 97e6ea3b5eea0b270c2c36f5cc44699fe21e30ceda4ffc6936ad40ff755bd8b16637f41d8b0bd20d50eb3261b0981e8f4822b2bd5805208532292499f6de340c checksum: 1919fb9fb48d574fafeb196aced7ea1ee345525597afa6ad01049c7ce090a732bc500c4a392deb0a5fb9165d5ae5fe720b795e3023bb5a9b2b2a0fae059b4407
languageName: node languageName: node
linkType: hard linkType: hard
@@ -1473,14 +1473,14 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@typescript-eslint/eslint-plugin@npm:^5.58.0": "@typescript-eslint/eslint-plugin@npm:^5.59.0":
version: 5.58.0 version: 5.59.0
resolution: "@typescript-eslint/eslint-plugin@npm:5.58.0" resolution: "@typescript-eslint/eslint-plugin@npm:5.59.0"
dependencies: dependencies:
"@eslint-community/regexpp": ^4.4.0 "@eslint-community/regexpp": ^4.4.0
"@typescript-eslint/scope-manager": 5.58.0 "@typescript-eslint/scope-manager": 5.59.0
"@typescript-eslint/type-utils": 5.58.0 "@typescript-eslint/type-utils": 5.59.0
"@typescript-eslint/utils": 5.58.0 "@typescript-eslint/utils": 5.59.0
debug: ^4.3.4 debug: ^4.3.4
grapheme-splitter: ^1.0.4 grapheme-splitter: ^1.0.4
ignore: ^5.2.0 ignore: ^5.2.0
@@ -1493,43 +1493,43 @@ __metadata:
peerDependenciesMeta: peerDependenciesMeta:
typescript: typescript:
optional: true optional: true
checksum: cda31ae6ff09c742f921304ea1a2a0d0823f7e20d7969ba6320ab14df2b3269b93a61eded96a59f01cfd24f28efb91e461e42bb09f493ed013936a899697a868 checksum: f3b557fc875f688073835b6d41af4184c08c00b0f4887e62bb110e2935d50a158b026547cde89708bf6463913322e757a07d2de26fc505a3c15a81120d64ccef
languageName: node languageName: node
linkType: hard linkType: hard
"@typescript-eslint/parser@npm:^5.58.0": "@typescript-eslint/parser@npm:^5.59.0":
version: 5.58.0 version: 5.59.0
resolution: "@typescript-eslint/parser@npm:5.58.0" resolution: "@typescript-eslint/parser@npm:5.59.0"
dependencies: dependencies:
"@typescript-eslint/scope-manager": 5.58.0 "@typescript-eslint/scope-manager": 5.59.0
"@typescript-eslint/types": 5.58.0 "@typescript-eslint/types": 5.59.0
"@typescript-eslint/typescript-estree": 5.58.0 "@typescript-eslint/typescript-estree": 5.59.0
debug: ^4.3.4 debug: ^4.3.4
peerDependencies: peerDependencies:
eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 eslint: ^6.0.0 || ^7.0.0 || ^8.0.0
peerDependenciesMeta: peerDependenciesMeta:
typescript: typescript:
optional: true optional: true
checksum: fb7a4ce59eb803d29705e0134b6731823d9d7b56dd76a4de4ff07eb09d56cc851ed9988ecacdc2d0cbd929115a02ce564b0bb1b97d8732e05707dbe4332460ae checksum: 5e0f8dfe4eb762bf1d2e8186559d39df653005a8f976101cd7b3739f3e0253d1003b4268976e52c7e42b63bf6ea042b1ed9412f85d06ed8fb0407b56dba19db2
languageName: node languageName: node
linkType: hard linkType: hard
"@typescript-eslint/scope-manager@npm:5.58.0": "@typescript-eslint/scope-manager@npm:5.59.0":
version: 5.58.0 version: 5.59.0
resolution: "@typescript-eslint/scope-manager@npm:5.58.0" resolution: "@typescript-eslint/scope-manager@npm:5.59.0"
dependencies: dependencies:
"@typescript-eslint/types": 5.58.0 "@typescript-eslint/types": 5.59.0
"@typescript-eslint/visitor-keys": 5.58.0 "@typescript-eslint/visitor-keys": 5.59.0
checksum: 66c82609ac6c9cf00e163126619e7c487adc938f02e4567a2c26319916a175b9aee792aa80bd319a20848c834c6e599cd302c9f5b68c64b95d02f024f511ac66 checksum: b53c9581daf3d6ac2ec5bd660d62c56ea77f71d77261a23bf21bc23a8140b5b7738304ace576b6af6e1d4ffc5170b7b6be7375da488e9e2997984011c509ead8
languageName: node languageName: node
linkType: hard linkType: hard
"@typescript-eslint/type-utils@npm:5.58.0": "@typescript-eslint/type-utils@npm:5.59.0":
version: 5.58.0 version: 5.59.0
resolution: "@typescript-eslint/type-utils@npm:5.58.0" resolution: "@typescript-eslint/type-utils@npm:5.59.0"
dependencies: dependencies:
"@typescript-eslint/typescript-estree": 5.58.0 "@typescript-eslint/typescript-estree": 5.59.0
"@typescript-eslint/utils": 5.58.0 "@typescript-eslint/utils": 5.59.0
debug: ^4.3.4 debug: ^4.3.4
tsutils: ^3.21.0 tsutils: ^3.21.0
peerDependencies: peerDependencies:
@@ -1537,23 +1537,23 @@ __metadata:
peerDependenciesMeta: peerDependenciesMeta:
typescript: typescript:
optional: true optional: true
checksum: 3ca4443f43b8263745afda3ff05517074da77d1dad25867845d386b29b012548b720d12334aca8bf15323a76557099e4ce3025a5a0fa84e070f6a4c1dc36d44e checksum: 8675af740e89ab15e10ef1938530dc14595f45ebaa8b57375e931a4e8b42d71cff8ddfacf71f20a333532a0dbe593eff6d59eb5d43b73c9dd19ad7439a30f443
languageName: node languageName: node
linkType: hard linkType: hard
"@typescript-eslint/types@npm:5.58.0": "@typescript-eslint/types@npm:5.59.0":
version: 5.58.0 version: 5.59.0
resolution: "@typescript-eslint/types@npm:5.58.0" resolution: "@typescript-eslint/types@npm:5.59.0"
checksum: 3e5973909a5c585f5aebf919eec8ac213e9b5089c7357ea832ffa2bd39df70dce0b806d4bcc39a15e309830dfbf7bdf22d9808ab3c466729b8536e9d7e83eccc checksum: f756843a49b418a23674842d356aaef14e7373e7df80729e64cf23b3fc7c9d9ab4f0a764b41555236af7821cd1d3c0efcc9a3c97b778f0b67b6dbbd9c5e852cc
languageName: node languageName: node
linkType: hard linkType: hard
"@typescript-eslint/typescript-estree@npm:5.58.0": "@typescript-eslint/typescript-estree@npm:5.59.0":
version: 5.58.0 version: 5.59.0
resolution: "@typescript-eslint/typescript-estree@npm:5.58.0" resolution: "@typescript-eslint/typescript-estree@npm:5.59.0"
dependencies: dependencies:
"@typescript-eslint/types": 5.58.0 "@typescript-eslint/types": 5.59.0
"@typescript-eslint/visitor-keys": 5.58.0 "@typescript-eslint/visitor-keys": 5.59.0
debug: ^4.3.4 debug: ^4.3.4
globby: ^11.1.0 globby: ^11.1.0
is-glob: ^4.0.3 is-glob: ^4.0.3
@@ -1562,35 +1562,35 @@ __metadata:
peerDependenciesMeta: peerDependenciesMeta:
typescript: typescript:
optional: true optional: true
checksum: 51c2a92217a1ccc01acf3c5c371b8c4b48b066cb6341441c35b74b6f3e13d21f81e0aed041215f3f4cf73320f5cd6cc3061801c51a3049958ee9a171a6efa196 checksum: 2e677677927721d0db286f2f2e0263d5b8ae06072f217fc2fd17c96c347f8cec2201dccaf393c41e6f4b2a7c3e2b7ca6ab8a27283e76c6ec5576f53d1d26a0b6
languageName: node languageName: node
linkType: hard linkType: hard
"@typescript-eslint/utils@npm:5.58.0": "@typescript-eslint/utils@npm:5.59.0":
version: 5.58.0 version: 5.59.0
resolution: "@typescript-eslint/utils@npm:5.58.0" resolution: "@typescript-eslint/utils@npm:5.59.0"
dependencies: dependencies:
"@eslint-community/eslint-utils": ^4.2.0 "@eslint-community/eslint-utils": ^4.2.0
"@types/json-schema": ^7.0.9 "@types/json-schema": ^7.0.9
"@types/semver": ^7.3.12 "@types/semver": ^7.3.12
"@typescript-eslint/scope-manager": 5.58.0 "@typescript-eslint/scope-manager": 5.59.0
"@typescript-eslint/types": 5.58.0 "@typescript-eslint/types": 5.59.0
"@typescript-eslint/typescript-estree": 5.58.0 "@typescript-eslint/typescript-estree": 5.59.0
eslint-scope: ^5.1.1 eslint-scope: ^5.1.1
semver: ^7.3.7 semver: ^7.3.7
peerDependencies: peerDependencies:
eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 eslint: ^6.0.0 || ^7.0.0 || ^8.0.0
checksum: 71ea338d9b67b59792e9d9a82b723acbee815534044294b169e3727f5394445d95a6200c919f0c28020bc5954df0f7110e9d0a4586e77ebebcd1662c06b30157 checksum: 653ea4032b51c8b3bdc386971cb437f59fc20a8df5ca8d11ef6c917e6376df26c73cfd18cbeee8c8818ba4350a8cbd87d0ea9ec5242e35c5d26059a11476bc13
languageName: node languageName: node
linkType: hard linkType: hard
"@typescript-eslint/visitor-keys@npm:5.58.0": "@typescript-eslint/visitor-keys@npm:5.59.0":
version: 5.58.0 version: 5.59.0
resolution: "@typescript-eslint/visitor-keys@npm:5.58.0" resolution: "@typescript-eslint/visitor-keys@npm:5.59.0"
dependencies: dependencies:
"@typescript-eslint/types": 5.58.0 "@typescript-eslint/types": 5.59.0
eslint-visitor-keys: ^3.3.0 eslint-visitor-keys: ^3.3.0
checksum: e41b0cf8bf766c491fe96e26b4cd20e6af4dbe85ff773a32887b7557ffd199117d8cdc86ceef5ce224d06c5e14d54a8edb679e58185f5a9c6b450615eaac6f30 checksum: 184a23424a6bf7ea48f700a71461d3a89270e8af32db6f1fcc5367834818c9e8bc4b57853a15b6a9d44297c064d04d08583815fd8f2019135c0bc197d60a6c0c
languageName: node languageName: node
linkType: hard linkType: hard
@@ -1732,18 +1732,18 @@ __metadata:
"@emotion/styled": ^11.10.6 "@emotion/styled": ^11.10.6
"@msgpack/msgpack": ^3.0.0-beta2 "@msgpack/msgpack": ^3.0.0-beta2
"@mui/icons-material": ^5.11.16 "@mui/icons-material": ^5.11.16
"@mui/material": ^5.12.0 "@mui/material": ^5.12.1
"@remix-run/router": ^1.5.0 "@remix-run/router": ^1.5.0
"@table-library/react-table-library": 4.1.0 "@table-library/react-table-library": 4.1.0
"@types/lodash-es": ^4.17.7 "@types/lodash-es": ^4.17.7
"@types/mime-types": ^2 "@types/mime-types": ^2
"@types/node": ^18.15.11 "@types/node": ^18.15.11
"@types/react": ^18.0.34 "@types/react": ^18.0.37
"@types/react-dom": ^18.0.11 "@types/react-dom": ^18.0.11
"@types/react-router-dom": ^5.3.3 "@types/react-router-dom": ^5.3.3
"@types/styled-components": ^5 "@types/styled-components": ^5
"@typescript-eslint/eslint-plugin": ^5.58.0 "@typescript-eslint/eslint-plugin": ^5.59.0
"@typescript-eslint/parser": ^5.58.0 "@typescript-eslint/parser": ^5.59.0
"@vitejs/plugin-react-swc": ^3.3.0 "@vitejs/plugin-react-swc": ^3.3.0
"@yarnpkg/pnpify": ^4.0.0-rc.42 "@yarnpkg/pnpify": ^4.0.0-rc.42
async-validator: ^4.2.5 async-validator: ^4.2.5
@@ -1753,6 +1753,7 @@ __metadata:
eslint-config-airbnb-typescript: ^17.0.0 eslint-config-airbnb-typescript: ^17.0.0
eslint-config-prettier: ^8.8.0 eslint-config-prettier: ^8.8.0
eslint-import-resolver-typescript: ^3.5.5 eslint-import-resolver-typescript: ^3.5.5
eslint-plugin-autofix: ^1.1.0
eslint-plugin-import: ^2.27.5 eslint-plugin-import: ^2.27.5
eslint-plugin-jsx-a11y: ^6.7.1 eslint-plugin-jsx-a11y: ^6.7.1
eslint-plugin-prettier: ^4.2.1 eslint-plugin-prettier: ^4.2.1
@@ -1773,13 +1774,13 @@ __metadata:
react-toastify: ^9.1.2 react-toastify: ^9.1.2
rollup-plugin-visualizer: ^5.9.0 rollup-plugin-visualizer: ^5.9.0
sockette: ^2.0.6 sockette: ^2.0.6
terser: ^5.16.9 terser: ^5.17.0
typesafe-i18n: ^5.24.3 typesafe-i18n: ^5.24.3
typescript: ^5.0.4 typescript: ^5.0.4
vite: ^4.2.1 vite: ^4.2.2
vite-plugin-minify: ^1.5.2 vite-plugin-minify: ^1.5.2
vite-plugin-svgr: ^2.4.0 vite-plugin-svgr: ^2.4.0
vite-tsconfig-paths: ^4.1.0 vite-tsconfig-paths: ^4.2.0
languageName: unknown languageName: unknown
linkType: soft linkType: soft
@@ -3021,6 +3022,21 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"eslint-plugin-autofix@npm:^1.1.0":
version: 1.1.0
resolution: "eslint-plugin-autofix@npm:1.1.0"
dependencies:
eslint-rule-composer: ^0.3.0
espree: ^9.0.0
esutils: ^2.0.2
lodash: ^4.17.20
string-similarity: ^4.0.3
peerDependencies:
eslint: ">= 5.12.1"
checksum: f46b2a9a1e2d99fd2dfdbdd2303c108d5fd773d00abe3c92f7a44a604235d2df28faf74ede9b6802925461815784a57a4909d6e4e2a2c71031cadf8903711c43
languageName: node
linkType: hard
"eslint-plugin-import@npm:^2.27.5": "eslint-plugin-import@npm:^2.27.5":
version: 2.27.5 version: 2.27.5
resolution: "eslint-plugin-import@npm:2.27.5" resolution: "eslint-plugin-import@npm:2.27.5"
@@ -3121,6 +3137,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"eslint-rule-composer@npm:^0.3.0":
version: 0.3.0
resolution: "eslint-rule-composer@npm:0.3.0"
checksum: 1f0c40d209e1503a955101a0dbba37e7fc67c8aaa47a5b9ae0b0fcbae7022c86e52b3df2b1b9ffd658e16cd80f31fff92e7222460a44d8251e61d49e0af79a07
languageName: node
linkType: hard
"eslint-scope@npm:^5.1.1": "eslint-scope@npm:^5.1.1":
version: 5.1.1 version: 5.1.1
resolution: "eslint-scope@npm:5.1.1" resolution: "eslint-scope@npm:5.1.1"
@@ -3198,7 +3221,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"espree@npm:^9.5.1": "espree@npm:^9.0.0, espree@npm:^9.5.1":
version: 9.5.1 version: 9.5.1
resolution: "espree@npm:9.5.1" resolution: "espree@npm:9.5.1"
dependencies: dependencies:
@@ -4401,7 +4424,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"lodash@npm:^4.17.15": "lodash@npm:^4.17.15, lodash@npm:^4.17.20":
version: 4.17.21 version: 4.17.21
resolution: "lodash@npm:4.17.21" resolution: "lodash@npm:4.17.21"
checksum: d8cbea072bb08655bb4c989da418994b073a608dffa608b09ac04b43a791b12aeae7cd7ad919aa4c925f33b48490b5cfe6c1f71d827956071dae2e7bb3a6b74c checksum: d8cbea072bb08655bb4c989da418994b073a608dffa608b09ac04b43a791b12aeae7cd7ad919aa4c925f33b48490b5cfe6c1f71d827956071dae2e7bb3a6b74c
@@ -5883,6 +5906,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"string-similarity@npm:^4.0.3":
version: 4.0.4
resolution: "string-similarity@npm:4.0.4"
checksum: fce331b818efafa701f692ddc2e170bd3ceaf6e7ca56a445b36b139981effe0884d8edc794a65005e54304da55ba054edfcff16a339bd301c9b94983fbc62047
languageName: node
linkType: hard
"string-width@npm:^1.0.2 || 2 || 3 || 4, string-width@npm:^4.1.0, string-width@npm:^4.2.0, string-width@npm:^4.2.3": "string-width@npm:^1.0.2 || 2 || 3 || 4, string-width@npm:^4.1.0, string-width@npm:^4.2.0, string-width@npm:^4.2.3":
version: 4.2.3 version: 4.2.3
resolution: "string-width@npm:4.2.3" resolution: "string-width@npm:4.2.3"
@@ -6070,9 +6100,9 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"terser@npm:^5.16.9": "terser@npm:^5.17.0":
version: 5.16.9 version: 5.17.0
resolution: "terser@npm:5.16.9" resolution: "terser@npm:5.17.0"
dependencies: dependencies:
"@jridgewell/source-map": ^0.3.2 "@jridgewell/source-map": ^0.3.2
acorn: ^8.5.0 acorn: ^8.5.0
@@ -6080,7 +6110,7 @@ __metadata:
source-map-support: ~0.5.20 source-map-support: ~0.5.20
bin: bin:
terser: bin/terser terser: bin/terser
checksum: eb883b606aa698e314957aa2cf6e70c1dc632d0d2dcda13e7a2cc73569a05034721826c0d6f9b31c6bb08bbc4fc633b6591871814dada71da9d34af9e284dc4f checksum: fb5c81a837fc4083c1471b5cd599505666ef9f007381fb934c39f8fc5e0c034914e32d2de247aeb7f8a9723c1dc411baf5153368cfe8ed1927139ff030516eda
languageName: node languageName: node
linkType: hard linkType: hard
@@ -6365,9 +6395,9 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"vite-tsconfig-paths@npm:^4.1.0": "vite-tsconfig-paths@npm:^4.2.0":
version: 4.1.0 version: 4.2.0
resolution: "vite-tsconfig-paths@npm:4.1.0" resolution: "vite-tsconfig-paths@npm:4.2.0"
dependencies: dependencies:
debug: ^4.1.1 debug: ^4.1.1
globrex: ^0.1.2 globrex: ^0.1.2
@@ -6377,13 +6407,13 @@ __metadata:
peerDependenciesMeta: peerDependenciesMeta:
vite: vite:
optional: true optional: true
checksum: 9846dfdd7118067539728f88a3b5f6a109d01392fa83bef6ad0505def2b1ed24579a4955df7db4b3ab60e9a816867a48e8b508f34030ef0d20b773293c91298d checksum: 04bd792bb4f6b4fb57ec8368cff076abffba8d6923af032affb14be43b6e2dfd8b25085947a3204d702a8c8e9d79d3c361373cf98566df682420728857906289
languageName: node languageName: node
linkType: hard linkType: hard
"vite@npm:^4.2.1": "vite@npm:^4.2.2":
version: 4.2.1 version: 4.2.2
resolution: "vite@npm:4.2.1" resolution: "vite@npm:4.2.2"
dependencies: dependencies:
esbuild: ^0.17.5 esbuild: ^0.17.5
fsevents: ~2.3.2 fsevents: ~2.3.2
@@ -6415,7 +6445,7 @@ __metadata:
optional: true optional: true
bin: bin:
vite: bin/vite.js vite: bin/vite.js
checksum: a64e3a1563b9584d1fd1ca45e06ee3c9fa4956320e6d4e9d83bf09fc8e64bb9d3ef62f664b6f740141eee16643690e8a41ffdb3030f4f2e170c57894df1f9a5d checksum: 60e7298c817f0626bcbfdfc8877431421eabab85131c64d69e58f5ea20a66c14c4e0901ed63286b362627a667560d257dd135615b63f844da6052f6248dd7be6
languageName: node languageName: node
linkType: hard linkType: hard

View File

@@ -14,7 +14,6 @@ rest_server.use(express.json());
// endpoints // endpoints
const API_ENDPOINT_ROOT = '/api/'; const API_ENDPOINT_ROOT = '/api/';
const REST_ENDPOINT_ROOT = '/rest/'; const REST_ENDPOINT_ROOT = '/rest/';
const EVENTSOURCE_ENDPOINT_ROOT = '/es/';
// LOG // LOG
const LOG_SETTINGS_ENDPOINT = REST_ENDPOINT_ROOT + 'logSettings'; const LOG_SETTINGS_ENDPOINT = REST_ENDPOINT_ROOT + 'logSettings';
@@ -610,7 +609,8 @@ let emsesp_entities = {
offset: 0, offset: 0,
factor: 0, factor: 0,
uom: 2, uom: 2,
val_type: 2 value: 1,
value_type: 2
}, },
{ {
id: 1, id: 1,
@@ -620,7 +620,8 @@ let emsesp_entities = {
offset: 2, offset: 2,
factor: 2, factor: 2,
uom: 4, uom: 4,
val_type: 5 value: 2,
value_type: 5
} }
] ]
}; };

View File

@@ -33,24 +33,27 @@ void WebEntityService::begin() {
EMSESP::logger().info("Starting custom entity service"); EMSESP::logger().info("Starting custom entity service");
} }
// this creates the scheduler file, saving it to the FS // this creates the entity file, saving it to the FS
// and also calls when the Scheduler web page is refreshed // and also calls when the Entity web page is refreshed
void WebEntity::read(WebEntity & webEntity, JsonObject & root) { void WebEntity::read(WebEntity & webEntity, JsonObject & root) {
JsonArray entity = root.createNestedArray("entity"); JsonArray entity = root.createNestedArray("entity");
uint8_t counter = 0;
for (const EntityItem & entityItem : webEntity.entityItems) { for (const EntityItem & entityItem : webEntity.entityItems) {
JsonObject ei = entity.createNestedObject(); JsonObject ei = entity.createNestedObject();
ei["device_id"] = Helpers::hextoa(entityItem.device_id, false); ei["id"] = counter++; // id is only used to render the table and must be unique
ei["type_id"] = Helpers::hextoa(entityItem.type_id, false); ei["device_id"] = Helpers::hextoa(entityItem.device_id, false);
ei["offset"] = entityItem.offset; ei["type_id"] = Helpers::hextoa(entityItem.type_id, false);
ei["factor"] = entityItem.factor; ei["offset"] = entityItem.offset;
ei["name"] = entityItem.name; ei["factor"] = entityItem.factor;
ei["uom"] = entityItem.uom; ei["name"] = entityItem.name;
ei["val_type"] = entityItem.valuetype; ei["uom"] = entityItem.uom;
ei["value_type"] = entityItem.value_type;
ei["writeable"] = entityItem.writeable;
EMSESP::webEntityService.render_value(ei, entityItem, true); EMSESP::webEntityService.render_value(ei, entityItem, true);
} }
} }
// call on initialization and also when the Schedule web page is updated // call on initialization and also when the Entity web page is updated
// this loads the data into the internal class // this loads the data into the internal class
StateUpdateResult WebEntity::update(JsonObject & root, WebEntity & webEntity) { StateUpdateResult WebEntity::update(JsonObject & root, WebEntity & webEntity) {
for (EntityItem & entityItem : webEntity.entityItems) { for (EntityItem & entityItem : webEntity.entityItems) {
@@ -60,36 +63,42 @@ StateUpdateResult WebEntity::update(JsonObject & root, WebEntity & webEntity) {
if (root["entity"].is<JsonArray>()) { if (root["entity"].is<JsonArray>()) {
for (const JsonObject ei : root["entity"].as<JsonArray>()) { for (const JsonObject ei : root["entity"].as<JsonArray>()) {
auto entityItem = EntityItem(); auto entityItem = EntityItem();
entityItem.device_id = Helpers::hextoint(ei["device_id"]); entityItem.device_id = Helpers::hextoint(ei["device_id"]); // TODO don't need
entityItem.type_id = Helpers::hextoint(ei["type_id"]); entityItem.type_id = Helpers::hextoint(ei["type_id"]);
entityItem.offset = ei["offset"]; entityItem.offset = ei["offset"];
entityItem.factor = ei["factor"]; entityItem.factor = ei["factor"];
entityItem.name = ei["name"].as<std::string>(); entityItem.name = ei["name"].as<std::string>();
entityItem.uom = ei["uom"]; entityItem.uom = ei["uom"];
entityItem.valuetype = ei["val_type"]; entityItem.value_type = ei["value_type"];
entityItem.writeable = ei["writeable"];
if (entityItem.valuetype == DeviceValueType::BOOL) { if (entityItem.value_type == DeviceValueType::BOOL) {
entityItem.val = EMS_VALUE_DEFAULT_BOOL; entityItem.value = EMS_VALUE_DEFAULT_BOOL;
} else if (entityItem.valuetype == DeviceValueType::INT) { } else if (entityItem.value_type == DeviceValueType::INT) {
entityItem.val = EMS_VALUE_DEFAULT_INT; entityItem.value = EMS_VALUE_DEFAULT_INT;
} else if (entityItem.valuetype == DeviceValueType::UINT) { } else if (entityItem.value_type == DeviceValueType::UINT) {
entityItem.val = EMS_VALUE_DEFAULT_UINT; entityItem.value = EMS_VALUE_DEFAULT_UINT;
} else if (entityItem.valuetype == DeviceValueType::SHORT) { } else if (entityItem.value_type == DeviceValueType::SHORT) {
entityItem.val = EMS_VALUE_DEFAULT_SHORT; entityItem.value = EMS_VALUE_DEFAULT_SHORT;
} else if (entityItem.valuetype == DeviceValueType::USHORT) { } else if (entityItem.value_type == DeviceValueType::USHORT) {
entityItem.val = EMS_VALUE_DEFAULT_USHORT; entityItem.value = EMS_VALUE_DEFAULT_USHORT;
} else { // if (entityItem.valuetype == DeviceValueType::ULONG || entityItem.valuetype == DeviceValueType::TIME) { } else { // if (entityItem.value_type == DeviceValueType::ULONG || entityItem.valuetype == DeviceValueType::TIME) {
entityItem.val = EMS_VALUE_DEFAULT_ULONG; entityItem.value = EMS_VALUE_DEFAULT_ULONG;
} }
webEntity.entityItems.push_back(entityItem); // add to list webEntity.entityItems.push_back(entityItem); // add to list
Command::add(
EMSdevice::DeviceType::CUSTOM, if (entityItem.writeable) {
webEntity.entityItems.back().name.c_str(), Command::add(
[webEntity](const char * value, const int8_t id) { return EMSESP::webEntityService.command_setvalue(value, webEntity.entityItems.back().name); }, EMSdevice::DeviceType::CUSTOM,
FL_(entity_cmd), webEntity.entityItems.back().name.c_str(),
CommandFlag::ADMIN_ONLY); [webEntity](const char * value, const int8_t id) {
return EMSESP::webEntityService.command_setvalue(value, webEntity.entityItems.back().name);
},
FL_(entity_cmd),
CommandFlag::ADMIN_ONLY);
}
} }
} }
return StateUpdateResult::CHANGED; return StateUpdateResult::CHANGED;
@@ -100,7 +109,7 @@ bool WebEntityService::command_setvalue(const char * value, const std::string na
EMSESP::webEntityService.read([&](WebEntity & webEntity) { entityItems = &webEntity.entityItems; }); EMSESP::webEntityService.read([&](WebEntity & webEntity) { entityItems = &webEntity.entityItems; });
for (EntityItem & entityItem : *entityItems) { for (EntityItem & entityItem : *entityItems) {
if (entityItem.name == name) { if (entityItem.name == name) {
if (entityItem.valuetype == DeviceValueType::BOOL) { if (entityItem.value_type == DeviceValueType::BOOL) {
bool v; bool v;
if (!Helpers::value2bool(value, v)) { if (!Helpers::value2bool(value, v)) {
return false; return false;
@@ -112,9 +121,9 @@ bool WebEntityService::command_setvalue(const char * value, const std::string na
return false; return false;
} }
int v = f / entityItem.factor; int v = f / entityItem.factor;
if (entityItem.valuetype == DeviceValueType::UINT || entityItem.valuetype == DeviceValueType::INT) { if (entityItem.value_type == DeviceValueType::UINT || entityItem.value_type == DeviceValueType::INT) {
EMSESP::send_write_request(entityItem.type_id, entityItem.device_id, entityItem.offset, v, 0); EMSESP::send_write_request(entityItem.type_id, entityItem.device_id, entityItem.offset, v, 0);
} else if (entityItem.valuetype == DeviceValueType::USHORT || entityItem.valuetype == DeviceValueType::SHORT) { } else if (entityItem.value_type == DeviceValueType::USHORT || entityItem.value_type == DeviceValueType::SHORT) {
uint8_t v1[2] = {(uint8_t)(v >> 8), (uint8_t)(v & 0xFF)}; uint8_t v1[2] = {(uint8_t)(v >> 8), (uint8_t)(v & 0xFF)};
EMSESP::send_write_request(entityItem.type_id, entityItem.device_id, entityItem.offset, v1, 2, 0); EMSESP::send_write_request(entityItem.type_id, entityItem.device_id, entityItem.offset, v1, 2, 0);
} else { } else {
@@ -122,6 +131,7 @@ bool WebEntityService::command_setvalue(const char * value, const std::string na
EMSESP::send_write_request(entityItem.type_id, entityItem.device_id, entityItem.offset, v1, 3, 0); EMSESP::send_write_request(entityItem.type_id, entityItem.device_id, entityItem.offset, v1, 3, 0);
} }
} }
publish_single(entityItem); publish_single(entityItem);
if (EMSESP::mqtt_.get_publish_onchange(0)) { if (EMSESP::mqtt_.get_publish_onchange(0)) {
publish(); publish();
@@ -136,42 +146,42 @@ bool WebEntityService::command_setvalue(const char * value, const std::string na
void WebEntityService::render_value(JsonObject & output, EntityItem entity, const bool useVal) { void WebEntityService::render_value(JsonObject & output, EntityItem entity, const bool useVal) {
char payload[12]; char payload[12];
std::string name = useVal ? "value" : entity.name; std::string name = useVal ? "value" : entity.name;
switch (entity.valuetype) { switch (entity.value_type) {
case DeviceValueType::BOOL: case DeviceValueType::BOOL:
if ((uint8_t)entity.val != EMS_VALUE_BOOL_NOTSET) { if ((uint8_t)entity.value != EMS_VALUE_BOOL_NOTSET) {
if (EMSESP::system_.bool_format() == BOOL_FORMAT_TRUEFALSE) { if (EMSESP::system_.bool_format() == BOOL_FORMAT_TRUEFALSE) {
output[name] = (uint8_t)entity.val ? true : false; output[name] = (uint8_t)entity.value ? true : false;
} else if (EMSESP::system_.bool_format() == BOOL_FORMAT_10) { } else if (EMSESP::system_.bool_format() == BOOL_FORMAT_10) {
output[name] = (uint8_t)entity.val ? 1 : 0; output[name] = (uint8_t)entity.value ? 1 : 0;
} else { } else {
output[name] = Helpers::render_boolean(payload, (uint8_t)entity.val); output[name] = Helpers::render_boolean(payload, (uint8_t)entity.value);
} }
} }
break; break;
case DeviceValueType::INT: case DeviceValueType::INT:
if ((int8_t)entity.val != EMS_VALUE_INT_NOTSET) { if ((int8_t)entity.value != EMS_VALUE_INT_NOTSET) {
output[name] = serialized(Helpers::render_value(payload, entity.factor * (int8_t)entity.val, 2)); output[name] = serialized(Helpers::render_value(payload, entity.factor * (int8_t)entity.value, 2));
} }
break; break;
case DeviceValueType::UINT: case DeviceValueType::UINT:
if ((uint8_t)entity.val != EMS_VALUE_UINT_NOTSET) { if ((uint8_t)entity.value != EMS_VALUE_UINT_NOTSET) {
output[name] = serialized(Helpers::render_value(payload, entity.factor * (uint8_t)entity.val, 2)); output[name] = serialized(Helpers::render_value(payload, entity.factor * (uint8_t)entity.value, 2));
} }
break; break;
case DeviceValueType::SHORT: case DeviceValueType::SHORT:
if ((int16_t)entity.val != EMS_VALUE_SHORT_NOTSET) { if ((int16_t)entity.value != EMS_VALUE_SHORT_NOTSET) {
output[name] = serialized(Helpers::render_value(payload, entity.factor * (int16_t)entity.val, 2)); output[name] = serialized(Helpers::render_value(payload, entity.factor * (int16_t)entity.value, 2));
} }
break; break;
case DeviceValueType::USHORT: case DeviceValueType::USHORT:
if ((uint16_t)entity.val != EMS_VALUE_USHORT_NOTSET) { if ((uint16_t)entity.value != EMS_VALUE_USHORT_NOTSET) {
output[name] = serialized(Helpers::render_value(payload, entity.factor * (uint16_t)entity.val, 2)); output[name] = serialized(Helpers::render_value(payload, entity.factor * (uint16_t)entity.value, 2));
} }
break; break;
case DeviceValueType::ULONG: case DeviceValueType::ULONG:
case DeviceValueType::TIME: case DeviceValueType::TIME:
if (entity.val != EMS_VALUE_ULONG_NOTSET) { if (entity.value != EMS_VALUE_ULONG_NOTSET) {
output[name] = serialized(Helpers::render_value(payload, entity.factor * entity.val, 2)); output[name] = serialized(Helpers::render_value(payload, entity.factor * entity.value, 2));
} }
break; break;
default: default:
@@ -215,7 +225,7 @@ bool WebEntityService::get_value_info(JsonObject & output, const char * cmd) {
output["name"] = entity.name; output["name"] = entity.name;
output["uom"] = EMSdevice::uom_to_string(entity.uom); output["uom"] = EMSdevice::uom_to_string(entity.uom);
output["readable"] = true; output["readable"] = true;
output["writeable"] = true; output["writeable"] = entity.writeable;
output["visible"] = true; output["visible"] = true;
render_value(output, entity, true); render_value(output, entity, true);
if (attribute_s) { if (attribute_s) {
@@ -250,8 +260,8 @@ void WebEntityService::publish_single(const EntityItem & entity) {
} else { } else {
snprintf(topic, sizeof(topic), "%s/%s", "custom_data", entity.name.c_str()); snprintf(topic, sizeof(topic), "%s/%s", "custom_data", entity.name.c_str());
} }
StaticJsonDocument<256> doc; StaticJsonDocument<EMSESP_JSON_SIZE_SMALL> doc;
JsonObject output = doc.to<JsonObject>(); JsonObject output = doc.to<JsonObject>();
render_value(output, entity, true); render_value(output, entity, true);
Mqtt::queue_publish(topic, output["value"].as<std::string>()); Mqtt::queue_publish(topic, output["value"].as<std::string>());
} }
@@ -326,8 +336,10 @@ uint8_t WebEntityService::count_entities() {
if (entityItems->size() == 0) { if (entityItems->size() == 0) {
return 0; return 0;
} }
DynamicJsonDocument doc(EMSESP_JSON_SIZE_XLARGE); DynamicJsonDocument doc(EMSESP_JSON_SIZE_XLARGE);
JsonObject output = doc.to<JsonObject>(); JsonObject output = doc.to<JsonObject>();
for (const EntityItem & entity : *entityItems) { for (const EntityItem & entity : *entityItems) {
render_value(output, entity); render_value(output, entity);
} }
@@ -337,46 +349,51 @@ uint8_t WebEntityService::count_entities() {
// send to dashboard, msgpack don't like serialized, use number // send to dashboard, msgpack don't like serialized, use number
void WebEntityService::generate_value_web(JsonObject & output) { void WebEntityService::generate_value_web(JsonObject & output) {
EMSESP::webEntityService.read([&](WebEntity & webEntity) { entityItems = &webEntity.entityItems; }); EMSESP::webEntityService.read([&](WebEntity & webEntity) { entityItems = &webEntity.entityItems; });
output["label"] = (std::string) "Custom Entities"; output["label"] = (std::string) "Custom Entities";
JsonArray data = output.createNestedArray("data"); JsonArray data = output.createNestedArray("data");
for (const EntityItem & entity : *entityItems) { for (const EntityItem & entity : *entityItems) {
JsonObject obj = data.createNestedObject(); // create the object, we know there is a value JsonObject obj = data.createNestedObject(); // create the object, we know there is a value
obj["id"] = "00" + entity.name; obj["id"] = "00" + entity.name;
obj["u"] = entity.uom; obj["u"] = entity.uom;
obj["c"] = entity.name; if (entity.writeable) {
switch (entity.valuetype) { obj["c"] = entity.name;
}
switch (entity.value_type) {
case DeviceValueType::BOOL: { case DeviceValueType::BOOL: {
char s[12]; char s[12];
obj["v"] = Helpers::render_boolean(s, (uint8_t)entity.val); obj["v"] = Helpers::render_boolean(s, (uint8_t)entity.value);
JsonArray l = obj.createNestedArray("l"); JsonArray l = obj.createNestedArray("l");
l.add(Helpers::render_boolean(s, false, true)); l.add(Helpers::render_boolean(s, false, true));
l.add(Helpers::render_boolean(s, true, true)); l.add(Helpers::render_boolean(s, true, true));
break; break;
} }
case DeviceValueType::INT: case DeviceValueType::INT:
if ((int8_t)entity.val != EMS_VALUE_INT_NOTSET) { if ((int8_t)entity.value != EMS_VALUE_INT_NOTSET) {
obj["v"] = Helpers::transformNumFloat(entity.factor * (int8_t)entity.val, 0); obj["v"] = Helpers::transformNumFloat(entity.factor * (int8_t)entity.value, 0);
} }
break; break;
case DeviceValueType::UINT: case DeviceValueType::UINT:
if ((uint8_t)entity.val != EMS_VALUE_UINT_NOTSET) { if ((uint8_t)entity.value != EMS_VALUE_UINT_NOTSET) {
obj["v"] = Helpers::transformNumFloat(entity.factor * (uint8_t)entity.val, 0); obj["v"] = Helpers::transformNumFloat(entity.factor * (uint8_t)entity.value, 0);
} }
break; break;
case DeviceValueType::SHORT: case DeviceValueType::SHORT:
if ((int16_t)entity.val != EMS_VALUE_SHORT_NOTSET) { if ((int16_t)entity.value != EMS_VALUE_SHORT_NOTSET) {
obj["v"] = Helpers::transformNumFloat(entity.factor * (int16_t)entity.val, 0); obj["v"] = Helpers::transformNumFloat(entity.factor * (int16_t)entity.value, 0);
} }
break; break;
case DeviceValueType::USHORT: case DeviceValueType::USHORT:
if ((uint16_t)entity.val != EMS_VALUE_USHORT_NOTSET) { if ((uint16_t)entity.value != EMS_VALUE_USHORT_NOTSET) {
obj["v"] = Helpers::transformNumFloat(entity.factor * (uint16_t)entity.val, 0); obj["v"] = Helpers::transformNumFloat(entity.factor * (uint16_t)entity.value, 0);
} }
break; break;
case DeviceValueType::ULONG: case DeviceValueType::ULONG:
case DeviceValueType::TIME: case DeviceValueType::TIME:
if (entity.val != EMS_VALUE_ULONG_NOTSET) { if (entity.value != EMS_VALUE_ULONG_NOTSET) {
obj["v"] = Helpers::transformNumFloat(entity.factor * entity.val, 0); obj["v"] = Helpers::transformNumFloat(entity.factor * entity.value, 0);
} }
break; break;
default: default:
@@ -402,13 +419,13 @@ bool WebEntityService::get_value(std::shared_ptr<const Telegram> telegram) {
const uint8_t len[] = {1, 1, 1, 2, 2, 3, 3}; const uint8_t len[] = {1, 1, 1, 2, 2, 3, 3};
for (auto & entity : *entityItems) { for (auto & entity : *entityItems) {
if (telegram->type_id == entity.type_id && telegram->src == entity.device_id && telegram->offset <= entity.offset if (telegram->type_id == entity.type_id && telegram->src == entity.device_id && telegram->offset <= entity.offset
&& (telegram->offset + telegram->message_length) >= (entity.offset + len[entity.valuetype])) { && (telegram->offset + telegram->message_length) >= (entity.offset + len[entity.value_type])) {
uint32_t val = 0; uint32_t value = 0;
for (uint8_t i = 0; i < len[entity.valuetype]; i++) { for (uint8_t i = 0; i < len[entity.value_type]; i++) {
val = (val << 8) + telegram->message_data[i + entity.offset - telegram->offset]; value = (value << 8) + telegram->message_data[i + entity.offset - telegram->offset];
} }
if (val != entity.val) { if (value != entity.value) {
entity.val = val; entity.value = value;
if (Mqtt::publish_single()) { if (Mqtt::publish_single()) {
publish_single(entity); publish_single(entity);
} else if (EMSESP::mqtt_.get_publish_onchange(0)) { } else if (EMSESP::mqtt_.get_publish_onchange(0)) {

View File

@@ -27,14 +27,16 @@ namespace emsesp {
class EntityItem { class EntityItem {
public: public:
uint8_t id;
uint8_t device_id; uint8_t device_id;
uint16_t type_id; uint16_t type_id;
uint8_t offset; uint8_t offset;
int8_t valuetype; int8_t value_type;
uint8_t uom; uint8_t uom;
std::string name; std::string name;
double factor; double factor;
uint32_t val; bool writeable;
uint32_t value;
}; };
class WebEntity { class WebEntity {
@@ -65,7 +67,7 @@ class WebEntityService : public StatefulService<WebEntity> {
HttpEndpoint<WebEntity> _httpEndpoint; HttpEndpoint<WebEntity> _httpEndpoint;
FSPersistence<WebEntity> _fsPersistence; FSPersistence<WebEntity> _fsPersistence;
std::list<EntityItem> * entityItems; // pointer to the list of schedule events std::list<EntityItem> * entityItems; // pointer to the list of entity items
bool ha_registered_ = false; bool ha_registered_ = false;
}; };