From 6bd744f12e87bdb3710dac5e9a47725e00540a28 Mon Sep 17 00:00:00 2001 From: Proddy Date: Tue, 18 Apr 2023 22:04:00 +0200 Subject: [PATCH] refactor dialog to prevent multiple parent renders --- .vscode/settings.json | 17 +- interface/.eslintrc.json | 26 +- interface/package.json | 15 +- .../framework/network/WiFiNetworkSelector.tsx | 2 +- interface/src/i18n/de/index.ts | 5 +- interface/src/i18n/en/index.ts | 3 +- interface/src/i18n/fr/index.ts | 3 +- interface/src/i18n/nl/index.ts | 3 +- interface/src/i18n/no/index.ts | 3 +- interface/src/i18n/pl/index.ts | 3 +- interface/src/i18n/sv/index.ts | 4 +- interface/src/i18n/tr/index.ts | 3 +- interface/src/project/SettingsEntities.tsx | 330 ++++-------------- .../src/project/SettingsEntitiesDialog.tsx | 228 ++++++++++++ interface/src/project/SettingsScheduler.tsx | 35 +- interface/src/project/types.ts | 6 +- interface/src/project/validators.ts | 20 +- interface/src/validators/shared.ts | 10 +- interface/tsconfig.json | 3 +- interface/yarn.lock | 214 +++++++----- mock-api/server.js | 7 +- src/web/WebEntityService.cpp | 173 ++++----- src/web/WebEntityService.h | 10 +- 23 files changed, 622 insertions(+), 501 deletions(-) create mode 100644 interface/src/project/SettingsEntitiesDialog.tsx diff --git a/.vscode/settings.json b/.vscode/settings.json index 4bba87e57..5afd93c63 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -7,5 +7,20 @@ "eslint.workingDirectories": ["interface"], "prettier.prettierPath": "interface/.yarn/sdks/prettier/index.js", "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" + } } diff --git a/interface/.eslintrc.json b/interface/.eslintrc.json index 86703c739..c207db972 100644 --- a/interface/.eslintrc.json +++ b/interface/.eslintrc.json @@ -24,7 +24,7 @@ "tsconfigRootDir": ".", "project": ["tsconfig.json"] }, - "plugins": ["react", "@typescript-eslint"], + "plugins": ["react", "@typescript-eslint", "autofix", "react-hooks"], "settings": { "import/resolver": { "typescript": { @@ -36,7 +36,6 @@ } }, "rules": { - "react-hooks/exhaustive-deps": "off", "object-shorthand": "error", "no-console": "warn", "@typescript-eslint/consistent-type-definitions": ["off", "type"], @@ -52,6 +51,29 @@ "@typescript-eslint/no-unused-expressions": "off", "@typescript-eslint/no-implied-eval": "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": [ "error", { diff --git a/interface/package.json b/interface/package.json index f182db090..7e746c3a3 100644 --- a/interface/package.json +++ b/interface/package.json @@ -23,12 +23,12 @@ "@emotion/styled": "^11.10.6", "@msgpack/msgpack": "^3.0.0-beta2", "@mui/icons-material": "^5.11.16", - "@mui/material": "^5.12.0", + "@mui/material": "^5.12.1", "@remix-run/router": "^1.5.0", "@table-library/react-table-library": "4.1.0", "@types/lodash-es": "^4.17.7", "@types/node": "^18.15.11", - "@types/react": "^18.0.34", + "@types/react": "^18.0.37", "@types/react-dom": "^18.0.11", "@types/react-router-dom": "^5.3.3", "@yarnpkg/pnpify": "^4.0.0-rc.42", @@ -51,14 +51,15 @@ "devDependencies": { "@types/mime-types": "^2", "@types/styled-components": "^5", - "@typescript-eslint/eslint-plugin": "^5.58.0", - "@typescript-eslint/parser": "^5.58.0", + "@typescript-eslint/eslint-plugin": "^5.59.0", + "@typescript-eslint/parser": "^5.59.0", "@vitejs/plugin-react-swc": "^3.3.0", "eslint": "^8.38.0", "eslint-config-airbnb": "^19.0.4", "eslint-config-airbnb-typescript": "^17.0.0", "eslint-config-prettier": "^8.8.0", "eslint-import-resolver-typescript": "^3.5.5", + "eslint-plugin-autofix": "^1.1.0", "eslint-plugin-import": "^2.27.5", "eslint-plugin-jsx-a11y": "^6.7.1", "eslint-plugin-prettier": "^4.2.1", @@ -68,11 +69,11 @@ "npm-run-all": "^4.1.5", "prettier": "^2.8.7", "rollup-plugin-visualizer": "^5.9.0", - "terser": "^5.16.9", - "vite": "^4.2.1", + "terser": "^5.17.0", + "vite": "^4.2.2", "vite-plugin-minify": "^1.5.2", "vite-plugin-svgr": "^2.4.0", - "vite-tsconfig-paths": "^4.1.0" + "vite-tsconfig-paths": "^4.2.0" }, "packageManager": "yarn@3.4.1" } diff --git a/interface/src/framework/network/WiFiNetworkSelector.tsx b/interface/src/framework/network/WiFiNetworkSelector.tsx index 5233b7826..9be0d46e1 100644 --- a/interface/src/framework/network/WiFiNetworkSelector.tsx +++ b/interface/src/framework/network/WiFiNetworkSelector.tsx @@ -47,7 +47,7 @@ const WiFiNetworkSelector: FC = ({ networkList }) => { const renderNetwork = (network: WiFiNetwork) => { return ( - wifiConnectionContext.selectNetwork(network)}> + wifiConnectionContext.selectNetwork(network)}> {isNetworkOpen(network) ? : } diff --git a/interface/src/i18n/de/index.ts b/interface/src/i18n/de/index.ts index db5fbcbc4..ac9dcf90e 100644 --- a/interface/src/i18n/de/index.ts +++ b/interface/src/i18n/de/index.ts @@ -311,7 +311,7 @@ const de: Translation = { LEAVE: 'Verlassen', 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_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', TIME: 'Zeit', TIMER: 'Timer', @@ -320,7 +320,8 @@ const de: Translation = { SCHEDULE_TIMER_2: 'jede Minute', SCHEDULE_TIMER_3: 'jede Stunde', 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; diff --git a/interface/src/i18n/en/index.ts b/interface/src/i18n/en/index.ts index 71139effb..ba5a5d8d7 100644 --- a/interface/src/i18n/en/index.ts +++ b/interface/src/i18n/en/index.ts @@ -320,7 +320,8 @@ const en: Translation = { SCHEDULE_TIMER_2: 'every minute', SCHEDULE_TIMER_3: 'every hour', 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; diff --git a/interface/src/i18n/fr/index.ts b/interface/src/i18n/fr/index.ts index 1d9734569..bac7958d0 100644 --- a/interface/src/i18n/fr/index.ts +++ b/interface/src/i18n/fr/index.ts @@ -320,7 +320,8 @@ const fr: Translation = { SCHEDULE_TIMER_2: 'every minute', // TODO translate SCHEDULE_TIMER_3: 'every hour', // 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; diff --git a/interface/src/i18n/nl/index.ts b/interface/src/i18n/nl/index.ts index c51914b62..a468ba134 100644 --- a/interface/src/i18n/nl/index.ts +++ b/interface/src/i18n/nl/index.ts @@ -320,7 +320,8 @@ const nl: Translation = { SCHEDULE_TIMER_2: 'every minute', // TODO translate SCHEDULE_TIMER_3: 'every hour', // 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; diff --git a/interface/src/i18n/no/index.ts b/interface/src/i18n/no/index.ts index e8983b8af..34081696b 100644 --- a/interface/src/i18n/no/index.ts +++ b/interface/src/i18n/no/index.ts @@ -320,7 +320,8 @@ const no: Translation = { SCHEDULE_TIMER_2: 'hvert minutt', SCHEDULE_TIMER_3: 'hver time', 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; diff --git a/interface/src/i18n/pl/index.ts b/interface/src/i18n/pl/index.ts index b91ecd04b..4a85f60e2 100644 --- a/interface/src/i18n/pl/index.ts +++ b/interface/src/i18n/pl/index.ts @@ -320,7 +320,8 @@ const pl: BaseTranslation = { SCHEDULE_TIMER_2: 'co minutę', SCHEDULE_TIMER_3: 'co godzinę', 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; diff --git a/interface/src/i18n/sv/index.ts b/interface/src/i18n/sv/index.ts index 54a94f257..3d6873122 100644 --- a/interface/src/i18n/sv/index.ts +++ b/interface/src/i18n/sv/index.ts @@ -320,7 +320,9 @@ const sv: Translation = { SCHEDULE_TIMER_2: 'every minute', // TODO translate SCHEDULE_TIMER_3: 'every hour', // 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; diff --git a/interface/src/i18n/tr/index.ts b/interface/src/i18n/tr/index.ts index 11858ba32..61867b860 100644 --- a/interface/src/i18n/tr/index.ts +++ b/interface/src/i18n/tr/index.ts @@ -320,7 +320,8 @@ const tr: Translation = { SCHEDULE_TIMER_2: 'every minute', // TODO translate SCHEDULE_TIMER_3: 'every hour', // 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; diff --git a/interface/src/project/SettingsEntities.tsx b/interface/src/project/SettingsEntities.tsx index 756472743..08163a86e 100644 --- a/interface/src/project/SettingsEntities.tsx +++ b/interface/src/project/SettingsEntities.tsx @@ -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 { - Button, - Typography, - Box, - Grid, - Dialog, - DialogActions, - DialogContent, - DialogTitle, - MenuItem, - InputAdornment -} from '@mui/material'; +import { Button, Typography, Box } from '@mui/material'; 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 { toast } from 'react-toastify'; -import RemoveIcon from '@mui/icons-material/RemoveCircleOutline'; import WarningIcon from '@mui/icons-material/Warning'; import CancelIcon from '@mui/icons-material/Cancel'; -import DoneIcon from '@mui/icons-material/Done'; 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 { extractErrorMessage, updateValue } from 'utils'; +import SettingsEntitiesDialog from './SettingsEntitiesDialog'; -import { validate } from 'validators'; -import { entityItemValidation } from './validators'; -import { ValidateFieldsError } from 'async-validator'; +import type { EntityItem } from './types'; +import { DeviceValueUOM_s } from './types'; +import { extractErrorMessage } from 'utils'; import { useI18nContext } from 'i18n/i18n-react'; import * as EMSESP from './api'; - -function makeid() { - return Math.floor(Math.random() * (Math.floor(200) - 100) + 100); -} +import { entityItemValidation } from './validators'; const SettingsEntities: FC = () => { const { LL } = useI18nContext(); - const [numChanges, setNumChanges] = useState(0); const blocker = useBlocker(numChanges !== 0); - - const emptyEntity = { - 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([emptyEntity]); - const [entityItem, setEntityItem] = useState(); + const [entities, setEntities] = useState([]); + const [selectedEntityItem, setSelectedEntityItem] = useState(); const [errorMessage, setErrorMessage] = useState(); const [creating, setCreating] = useState(false); - const [fieldErrors, setFieldErrors] = useState(); + const [dialogOpen, setDialogOpen] = useState(false); function hasEntityChanged(ei: EntityItem) { return ( @@ -76,21 +45,15 @@ const SettingsEntities: FC = () => { ei.offset !== ei.o_offset || ei.uom !== ei.o_uom || 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 ); } - const getNumChanges = () => { - if (!entities) { - return 0; - } - return entities.filter((ei) => hasEntityChanged(ei)).length; - }; - useEffect(() => { - setNumChanges(getNumChanges()); - }); + setNumChanges(entities ? entities.filter((ei) => hasEntityChanged(ei)).length : 0); + }, [entities]); const entity_theme = useTheme({ Table: ` @@ -156,8 +119,9 @@ const SettingsEntities: FC = () => { o_offset: ei.offset, o_factor: ei.factor, o_uom: ei.uom, - o_val_type: ei.val_type, + o_value_type: ei.value_type, o_name: ei.name, + o_writeable: ei.writeable, o_deleted: ei.deleted })) ); @@ -182,19 +146,19 @@ const SettingsEntities: FC = () => { const response = await EMSESP.writeEntities({ entities: entities .filter((ei) => !ei.deleted) - .map((condensed_ei) => { - return { - 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, - val_type: condensed_ei.val_type - }; - }) + .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.SUCCESS()); } else { @@ -207,42 +171,48 @@ const SettingsEntities: FC = () => { } }; - const editEntityItem = (ei: EntityItem) => { + const editEntityItem = useCallback((ei: EntityItem) => { 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 = () => { setCreating(true); - setEntityItem({ - id: makeid(), + setSelectedEntityItem({ + id: Math.floor(Math.random() * (Math.floor(200) - 100) + 100), device_id: 11, type_id: 0, offset: 0, factor: 1, - val_type: 2, + value_type: 2, uom: 0, name: '', + writeable: false, deleted: false }); - }; - - const updateEntityItem = () => { - console.log('here'); - if (entityItem) { - setEntities([...entities.filter((ei) => creating || ei.o_id !== entityItem.o_id), entityItem]); - } - setEntityItem(undefined); + setDialogOpen(true); }; function formatValue(value: any, uom: number) { if (value === undefined) { return ''; } - if (uom === 0) { - return new Intl.NumberFormat().format(value); - } - return new Intl.NumberFormat().format(value) + ' ' + DeviceValueUOM_s[uom]; + return new Intl.NumberFormat().format(value) + (uom === 0 ? '' : ' ' + DeviceValueUOM_s[uom]); } 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 ( - closeDialog()}> - - {creating ? LL.ADD(1) + ' ' + LL.NEW() : LL.EDIT()} {LL.ENTITY()} - - - - - - - - - - - - 0x - }} - /> - - - 0x - }} - /> - - - - - - - BOOL - INT - UINT - SHORT - USHORT - ULONG - TIME - - - {entityItem.val_type !== 0 && ( - <> - - - - - - {DeviceValueUOM_s.map((val, i) => ( - - {val} - - ))} - - - - )} - - - - {!creating && ( - - - - )} - - - - - ); - } - }; - return ( {blocker ? : null} @@ -473,10 +264,21 @@ const SettingsEntities: FC = () => { {LL.ENTITIES_HELP_1()} {renderEntity()} - {renderEditEntity()} + + {selectedEntityItem && ( + + )} + - {numChanges !== 0 && ( + {numChanges > 0 && ( + + )} + + + + + ); +}; + +export default SettingsEntitiesDialog; diff --git a/interface/src/project/SettingsScheduler.tsx b/interface/src/project/SettingsScheduler.tsx index f607c9dfb..cf1f84277 100644 --- a/interface/src/project/SettingsScheduler.tsx +++ b/interface/src/project/SettingsScheduler.tsx @@ -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 { @@ -43,9 +44,10 @@ import { extractErrorMessage, updateValue } from 'utils'; import { validate } 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'; @@ -105,6 +107,7 @@ const SettingsScheduler: FC = () => { ); } + // TODO fix const getNumChanges = () => { if (!schedule) { return 0; @@ -184,12 +187,12 @@ const SettingsScheduler: FC = () => { } catch (error) { setErrorMessage(extractErrorMessage(error, LL.PROBLEM_LOADING())); } - setDow(getDayNames()); }, [LL]); useEffect(() => { - fetchSchedule(); - }, [fetchSchedule]); + void fetchSchedule(); + setDow(getDayNames()); + }, [getDayNames, fetchSchedule]); const getFlagNumber = (newFlag: string[]) => { let new_flag = 0; @@ -234,17 +237,15 @@ const SettingsScheduler: FC = () => { const response = await EMSESP.writeSchedule({ schedule: schedule .filter((si) => !si.deleted) - .map((condensed_si) => { - return { - 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 - }; - }) + .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_SAVED()); diff --git a/interface/src/project/types.ts b/interface/src/project/types.ts index afa87ecf8..2fa83fcab 100644 --- a/interface/src/project/types.ts +++ b/interface/src/project/types.ts @@ -346,8 +346,9 @@ export interface EntityItem { offset: number; factor: number; uom: number; - val_type: number; + value_type: number; value?: number; + writeable: boolean; deleted?: boolean; // optional o_id?: number; o_name?: string; @@ -356,8 +357,9 @@ export interface EntityItem { o_offset?: number; o_factor?: number; o_uom?: number; - o_val_type?: number; + o_value_type?: number; o_deleted?: boolean; + o_writeable?: boolean; } export interface Entities { diff --git a/interface/src/project/validators.ts b/interface/src/project/validators.ts index 6417b06f4..3d7e9b795 100644 --- a/interface/src/project/validators.ts +++ b/interface/src/project/validators.ts @@ -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 { Settings, ScheduleItem, EntityItem } from './types'; +import type { Settings, ScheduleItem, EntityItem } from './types'; export const GPIO_VALIDATOR = { 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) => ({ - 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) => +export const entityItemValidation = (entities: EntityItem[], creating: boolean) => new Schema({ name: [ { required: true, message: 'Name is required' }, @@ -129,8 +120,7 @@ export const entityItemValidation = (entities: EntityItem[], entityItem: EntityI type: 'string', pattern: /^[a-zA-Z0-9_\\.]{1,15}$/, 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' }] // type_id: [ diff --git a/interface/src/validators/shared.ts b/interface/src/validators/shared.ts index 9f0c42e5c..a028b5893 100644 --- a/interface/src/validators/shared.ts +++ b/interface/src/validators/shared.ts @@ -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 = ( validator: Schema, source: Partial, options?: ValidateOption -): Promise => { - return new Promise((resolve, reject) => { - validator.validate(source, options ? options : {}, (errors, fieldErrors) => { +): Promise => + new Promise((resolve, reject) => { + void validator.validate(source, options ? options : {}, (errors, fieldErrors) => { if (errors) { reject(fieldErrors); } else { @@ -14,7 +15,6 @@ export const validate = ( } }); }); -}; // updated to support both IPv4 and IPv6 const IP_ADDRESS_REGEXP = diff --git a/interface/tsconfig.json b/interface/tsconfig.json index d50dac669..6a77480de 100644 --- a/interface/tsconfig.json +++ b/interface/tsconfig.json @@ -13,7 +13,8 @@ "noFallthroughCasesInSwitch": true, "composite": true, "module": "ESNext", - "moduleResolution": "Node", + // "moduleResolution": "Node", + "moduleResolution": "bundler", "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, diff --git a/interface/yarn.lock b/interface/yarn.lock index be5a1cc4a..7054d0cc2 100644 --- a/interface/yarn.lock +++ b/interface/yarn.lock @@ -703,9 +703,9 @@ __metadata: languageName: node linkType: hard -"@mui/base@npm:5.0.0-alpha.125": - version: 5.0.0-alpha.125 - resolution: "@mui/base@npm:5.0.0-alpha.125" +"@mui/base@npm:5.0.0-alpha.126": + version: 5.0.0-alpha.126 + resolution: "@mui/base@npm:5.0.0-alpha.126" dependencies: "@babel/runtime": ^7.21.0 "@emotion/is-prop-valid": ^1.2.0 @@ -722,14 +722,14 @@ __metadata: peerDependenciesMeta: "@types/react": optional: true - checksum: 0a87b5141500885c364382375816d23b48799e38c57993b0bbb2dbfce8052a8bdba588b2cd6cee75dc2fc43a873ce3d27a223ef1395c42a0b2e28d59e559b2bf + checksum: afb6b99cba541cdb40bb098a78d105721cd6c6db3156fe9923bbb0aea184f6949ab5db79ab95177b4edb011acb35ac34556124b1203fd91ea16f52dc5477da7b languageName: node linkType: hard -"@mui/core-downloads-tracker@npm:^5.12.0": - version: 5.12.0 - resolution: "@mui/core-downloads-tracker@npm:5.12.0" - checksum: b0bc0c67be036fc6b965827ffb2ad2134c317237439dcbbe0b90a1807e92f93894e8d5e6650df4833b5a9b88b28d77cb2cd4435d23bbb9ab751c023684012e5f +"@mui/core-downloads-tracker@npm:^5.12.1": + version: 5.12.1 + resolution: "@mui/core-downloads-tracker@npm:5.12.1" + checksum: 0fc90f840e888e0a671ce43dfaa50018c7f2b82379b3adf3128387c3f29d06cdc235493939698339eb2aab49cf9a167473ed95f6a64eb424ee2d95849dd4aa76 languageName: node linkType: hard @@ -749,14 +749,14 @@ __metadata: languageName: node linkType: hard -"@mui/material@npm:^5.12.0": - version: 5.12.0 - resolution: "@mui/material@npm:5.12.0" +"@mui/material@npm:^5.12.1": + version: 5.12.1 + resolution: "@mui/material@npm:5.12.1" dependencies: "@babel/runtime": ^7.21.0 - "@mui/base": 5.0.0-alpha.125 - "@mui/core-downloads-tracker": ^5.12.0 - "@mui/system": ^5.12.0 + "@mui/base": 5.0.0-alpha.126 + "@mui/core-downloads-tracker": ^5.12.1 + "@mui/system": ^5.12.1 "@mui/types": ^7.2.4 "@mui/utils": ^5.12.0 "@types/react-transition-group": ^4.4.5 @@ -778,7 +778,7 @@ __metadata: optional: true "@types/react": optional: true - checksum: d8f2e875393dd254d70aafea08e1289d4cc4d085af581cd8fd4cc2882d5e265b8c926322ac64c1e0d18c5f441969abef2611c87346d685ad18fcfbc27e2d8ddf + checksum: a4b4becea4da7af787d58b09afe6a3e1c7b1aa1b27f93186f848efd0bcb250e5b39ca51cdbcedc0216db32049665a69a13a2f8480d357f2310ca6efcf8b10a76 languageName: node linkType: hard @@ -820,9 +820,9 @@ __metadata: languageName: node linkType: hard -"@mui/system@npm:^5.12.0": - version: 5.12.0 - resolution: "@mui/system@npm:5.12.0" +"@mui/system@npm:^5.12.1": + version: 5.12.1 + resolution: "@mui/system@npm:5.12.1" dependencies: "@babel/runtime": ^7.21.0 "@mui/private-theming": ^5.12.0 @@ -844,7 +844,7 @@ __metadata: optional: true "@types/react": optional: true - checksum: d53e70f35b8cc19c687ba72fc79a1f4cc20e0dd0335433fc255bc70291f89460b88bdddb7a9f17d11ffa6de3710117cc666c28ea0ac234fd2da13e82ee3c3c34 + checksum: a9dc1e3503f8c036663d0bc38b9bed71f67833890148a29efba5501f0da1f62d0c5372648c840779bdc74ea6e997783441934fa1df90f00ffef158cb6e6d5ce7 languageName: node linkType: hard @@ -1421,14 +1421,14 @@ __metadata: languageName: node linkType: hard -"@types/react@npm:^18.0.34": - version: 18.0.34 - resolution: "@types/react@npm:18.0.34" +"@types/react@npm:^18.0.37": + version: 18.0.37 + resolution: "@types/react@npm:18.0.37" dependencies: "@types/prop-types": "*" "@types/scheduler": "*" csstype: ^3.0.2 - checksum: 97e6ea3b5eea0b270c2c36f5cc44699fe21e30ceda4ffc6936ad40ff755bd8b16637f41d8b0bd20d50eb3261b0981e8f4822b2bd5805208532292499f6de340c + checksum: 1919fb9fb48d574fafeb196aced7ea1ee345525597afa6ad01049c7ce090a732bc500c4a392deb0a5fb9165d5ae5fe720b795e3023bb5a9b2b2a0fae059b4407 languageName: node linkType: hard @@ -1473,14 +1473,14 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/eslint-plugin@npm:^5.58.0": - version: 5.58.0 - resolution: "@typescript-eslint/eslint-plugin@npm:5.58.0" +"@typescript-eslint/eslint-plugin@npm:^5.59.0": + version: 5.59.0 + resolution: "@typescript-eslint/eslint-plugin@npm:5.59.0" dependencies: "@eslint-community/regexpp": ^4.4.0 - "@typescript-eslint/scope-manager": 5.58.0 - "@typescript-eslint/type-utils": 5.58.0 - "@typescript-eslint/utils": 5.58.0 + "@typescript-eslint/scope-manager": 5.59.0 + "@typescript-eslint/type-utils": 5.59.0 + "@typescript-eslint/utils": 5.59.0 debug: ^4.3.4 grapheme-splitter: ^1.0.4 ignore: ^5.2.0 @@ -1493,43 +1493,43 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: cda31ae6ff09c742f921304ea1a2a0d0823f7e20d7969ba6320ab14df2b3269b93a61eded96a59f01cfd24f28efb91e461e42bb09f493ed013936a899697a868 + checksum: f3b557fc875f688073835b6d41af4184c08c00b0f4887e62bb110e2935d50a158b026547cde89708bf6463913322e757a07d2de26fc505a3c15a81120d64ccef languageName: node linkType: hard -"@typescript-eslint/parser@npm:^5.58.0": - version: 5.58.0 - resolution: "@typescript-eslint/parser@npm:5.58.0" +"@typescript-eslint/parser@npm:^5.59.0": + version: 5.59.0 + resolution: "@typescript-eslint/parser@npm:5.59.0" dependencies: - "@typescript-eslint/scope-manager": 5.58.0 - "@typescript-eslint/types": 5.58.0 - "@typescript-eslint/typescript-estree": 5.58.0 + "@typescript-eslint/scope-manager": 5.59.0 + "@typescript-eslint/types": 5.59.0 + "@typescript-eslint/typescript-estree": 5.59.0 debug: ^4.3.4 peerDependencies: eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 peerDependenciesMeta: typescript: optional: true - checksum: fb7a4ce59eb803d29705e0134b6731823d9d7b56dd76a4de4ff07eb09d56cc851ed9988ecacdc2d0cbd929115a02ce564b0bb1b97d8732e05707dbe4332460ae + checksum: 5e0f8dfe4eb762bf1d2e8186559d39df653005a8f976101cd7b3739f3e0253d1003b4268976e52c7e42b63bf6ea042b1ed9412f85d06ed8fb0407b56dba19db2 languageName: node linkType: hard -"@typescript-eslint/scope-manager@npm:5.58.0": - version: 5.58.0 - resolution: "@typescript-eslint/scope-manager@npm:5.58.0" +"@typescript-eslint/scope-manager@npm:5.59.0": + version: 5.59.0 + resolution: "@typescript-eslint/scope-manager@npm:5.59.0" dependencies: - "@typescript-eslint/types": 5.58.0 - "@typescript-eslint/visitor-keys": 5.58.0 - checksum: 66c82609ac6c9cf00e163126619e7c487adc938f02e4567a2c26319916a175b9aee792aa80bd319a20848c834c6e599cd302c9f5b68c64b95d02f024f511ac66 + "@typescript-eslint/types": 5.59.0 + "@typescript-eslint/visitor-keys": 5.59.0 + checksum: b53c9581daf3d6ac2ec5bd660d62c56ea77f71d77261a23bf21bc23a8140b5b7738304ace576b6af6e1d4ffc5170b7b6be7375da488e9e2997984011c509ead8 languageName: node linkType: hard -"@typescript-eslint/type-utils@npm:5.58.0": - version: 5.58.0 - resolution: "@typescript-eslint/type-utils@npm:5.58.0" +"@typescript-eslint/type-utils@npm:5.59.0": + version: 5.59.0 + resolution: "@typescript-eslint/type-utils@npm:5.59.0" dependencies: - "@typescript-eslint/typescript-estree": 5.58.0 - "@typescript-eslint/utils": 5.58.0 + "@typescript-eslint/typescript-estree": 5.59.0 + "@typescript-eslint/utils": 5.59.0 debug: ^4.3.4 tsutils: ^3.21.0 peerDependencies: @@ -1537,23 +1537,23 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: 3ca4443f43b8263745afda3ff05517074da77d1dad25867845d386b29b012548b720d12334aca8bf15323a76557099e4ce3025a5a0fa84e070f6a4c1dc36d44e + checksum: 8675af740e89ab15e10ef1938530dc14595f45ebaa8b57375e931a4e8b42d71cff8ddfacf71f20a333532a0dbe593eff6d59eb5d43b73c9dd19ad7439a30f443 languageName: node linkType: hard -"@typescript-eslint/types@npm:5.58.0": - version: 5.58.0 - resolution: "@typescript-eslint/types@npm:5.58.0" - checksum: 3e5973909a5c585f5aebf919eec8ac213e9b5089c7357ea832ffa2bd39df70dce0b806d4bcc39a15e309830dfbf7bdf22d9808ab3c466729b8536e9d7e83eccc +"@typescript-eslint/types@npm:5.59.0": + version: 5.59.0 + resolution: "@typescript-eslint/types@npm:5.59.0" + checksum: f756843a49b418a23674842d356aaef14e7373e7df80729e64cf23b3fc7c9d9ab4f0a764b41555236af7821cd1d3c0efcc9a3c97b778f0b67b6dbbd9c5e852cc languageName: node linkType: hard -"@typescript-eslint/typescript-estree@npm:5.58.0": - version: 5.58.0 - resolution: "@typescript-eslint/typescript-estree@npm:5.58.0" +"@typescript-eslint/typescript-estree@npm:5.59.0": + version: 5.59.0 + resolution: "@typescript-eslint/typescript-estree@npm:5.59.0" dependencies: - "@typescript-eslint/types": 5.58.0 - "@typescript-eslint/visitor-keys": 5.58.0 + "@typescript-eslint/types": 5.59.0 + "@typescript-eslint/visitor-keys": 5.59.0 debug: ^4.3.4 globby: ^11.1.0 is-glob: ^4.0.3 @@ -1562,35 +1562,35 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: 51c2a92217a1ccc01acf3c5c371b8c4b48b066cb6341441c35b74b6f3e13d21f81e0aed041215f3f4cf73320f5cd6cc3061801c51a3049958ee9a171a6efa196 + checksum: 2e677677927721d0db286f2f2e0263d5b8ae06072f217fc2fd17c96c347f8cec2201dccaf393c41e6f4b2a7c3e2b7ca6ab8a27283e76c6ec5576f53d1d26a0b6 languageName: node linkType: hard -"@typescript-eslint/utils@npm:5.58.0": - version: 5.58.0 - resolution: "@typescript-eslint/utils@npm:5.58.0" +"@typescript-eslint/utils@npm:5.59.0": + version: 5.59.0 + resolution: "@typescript-eslint/utils@npm:5.59.0" dependencies: "@eslint-community/eslint-utils": ^4.2.0 "@types/json-schema": ^7.0.9 "@types/semver": ^7.3.12 - "@typescript-eslint/scope-manager": 5.58.0 - "@typescript-eslint/types": 5.58.0 - "@typescript-eslint/typescript-estree": 5.58.0 + "@typescript-eslint/scope-manager": 5.59.0 + "@typescript-eslint/types": 5.59.0 + "@typescript-eslint/typescript-estree": 5.59.0 eslint-scope: ^5.1.1 semver: ^7.3.7 peerDependencies: eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 - checksum: 71ea338d9b67b59792e9d9a82b723acbee815534044294b169e3727f5394445d95a6200c919f0c28020bc5954df0f7110e9d0a4586e77ebebcd1662c06b30157 + checksum: 653ea4032b51c8b3bdc386971cb437f59fc20a8df5ca8d11ef6c917e6376df26c73cfd18cbeee8c8818ba4350a8cbd87d0ea9ec5242e35c5d26059a11476bc13 languageName: node linkType: hard -"@typescript-eslint/visitor-keys@npm:5.58.0": - version: 5.58.0 - resolution: "@typescript-eslint/visitor-keys@npm:5.58.0" +"@typescript-eslint/visitor-keys@npm:5.59.0": + version: 5.59.0 + resolution: "@typescript-eslint/visitor-keys@npm:5.59.0" dependencies: - "@typescript-eslint/types": 5.58.0 + "@typescript-eslint/types": 5.59.0 eslint-visitor-keys: ^3.3.0 - checksum: e41b0cf8bf766c491fe96e26b4cd20e6af4dbe85ff773a32887b7557ffd199117d8cdc86ceef5ce224d06c5e14d54a8edb679e58185f5a9c6b450615eaac6f30 + checksum: 184a23424a6bf7ea48f700a71461d3a89270e8af32db6f1fcc5367834818c9e8bc4b57853a15b6a9d44297c064d04d08583815fd8f2019135c0bc197d60a6c0c languageName: node linkType: hard @@ -1732,18 +1732,18 @@ __metadata: "@emotion/styled": ^11.10.6 "@msgpack/msgpack": ^3.0.0-beta2 "@mui/icons-material": ^5.11.16 - "@mui/material": ^5.12.0 + "@mui/material": ^5.12.1 "@remix-run/router": ^1.5.0 "@table-library/react-table-library": 4.1.0 "@types/lodash-es": ^4.17.7 "@types/mime-types": ^2 "@types/node": ^18.15.11 - "@types/react": ^18.0.34 + "@types/react": ^18.0.37 "@types/react-dom": ^18.0.11 "@types/react-router-dom": ^5.3.3 "@types/styled-components": ^5 - "@typescript-eslint/eslint-plugin": ^5.58.0 - "@typescript-eslint/parser": ^5.58.0 + "@typescript-eslint/eslint-plugin": ^5.59.0 + "@typescript-eslint/parser": ^5.59.0 "@vitejs/plugin-react-swc": ^3.3.0 "@yarnpkg/pnpify": ^4.0.0-rc.42 async-validator: ^4.2.5 @@ -1753,6 +1753,7 @@ __metadata: eslint-config-airbnb-typescript: ^17.0.0 eslint-config-prettier: ^8.8.0 eslint-import-resolver-typescript: ^3.5.5 + eslint-plugin-autofix: ^1.1.0 eslint-plugin-import: ^2.27.5 eslint-plugin-jsx-a11y: ^6.7.1 eslint-plugin-prettier: ^4.2.1 @@ -1773,13 +1774,13 @@ __metadata: react-toastify: ^9.1.2 rollup-plugin-visualizer: ^5.9.0 sockette: ^2.0.6 - terser: ^5.16.9 + terser: ^5.17.0 typesafe-i18n: ^5.24.3 typescript: ^5.0.4 - vite: ^4.2.1 + vite: ^4.2.2 vite-plugin-minify: ^1.5.2 vite-plugin-svgr: ^2.4.0 - vite-tsconfig-paths: ^4.1.0 + vite-tsconfig-paths: ^4.2.0 languageName: unknown linkType: soft @@ -3021,6 +3022,21 @@ __metadata: languageName: node 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": version: 2.27.5 resolution: "eslint-plugin-import@npm:2.27.5" @@ -3121,6 +3137,13 @@ __metadata: languageName: node 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": version: 5.1.1 resolution: "eslint-scope@npm:5.1.1" @@ -3198,7 +3221,7 @@ __metadata: languageName: node linkType: hard -"espree@npm:^9.5.1": +"espree@npm:^9.0.0, espree@npm:^9.5.1": version: 9.5.1 resolution: "espree@npm:9.5.1" dependencies: @@ -4401,7 +4424,7 @@ __metadata: languageName: node linkType: hard -"lodash@npm:^4.17.15": +"lodash@npm:^4.17.15, lodash@npm:^4.17.20": version: 4.17.21 resolution: "lodash@npm:4.17.21" checksum: d8cbea072bb08655bb4c989da418994b073a608dffa608b09ac04b43a791b12aeae7cd7ad919aa4c925f33b48490b5cfe6c1f71d827956071dae2e7bb3a6b74c @@ -5883,6 +5906,13 @@ __metadata: languageName: node 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": version: 4.2.3 resolution: "string-width@npm:4.2.3" @@ -6070,9 +6100,9 @@ __metadata: languageName: node linkType: hard -"terser@npm:^5.16.9": - version: 5.16.9 - resolution: "terser@npm:5.16.9" +"terser@npm:^5.17.0": + version: 5.17.0 + resolution: "terser@npm:5.17.0" dependencies: "@jridgewell/source-map": ^0.3.2 acorn: ^8.5.0 @@ -6080,7 +6110,7 @@ __metadata: source-map-support: ~0.5.20 bin: terser: bin/terser - checksum: eb883b606aa698e314957aa2cf6e70c1dc632d0d2dcda13e7a2cc73569a05034721826c0d6f9b31c6bb08bbc4fc633b6591871814dada71da9d34af9e284dc4f + checksum: fb5c81a837fc4083c1471b5cd599505666ef9f007381fb934c39f8fc5e0c034914e32d2de247aeb7f8a9723c1dc411baf5153368cfe8ed1927139ff030516eda languageName: node linkType: hard @@ -6365,9 +6395,9 @@ __metadata: languageName: node linkType: hard -"vite-tsconfig-paths@npm:^4.1.0": - version: 4.1.0 - resolution: "vite-tsconfig-paths@npm:4.1.0" +"vite-tsconfig-paths@npm:^4.2.0": + version: 4.2.0 + resolution: "vite-tsconfig-paths@npm:4.2.0" dependencies: debug: ^4.1.1 globrex: ^0.1.2 @@ -6377,13 +6407,13 @@ __metadata: peerDependenciesMeta: vite: optional: true - checksum: 9846dfdd7118067539728f88a3b5f6a109d01392fa83bef6ad0505def2b1ed24579a4955df7db4b3ab60e9a816867a48e8b508f34030ef0d20b773293c91298d + checksum: 04bd792bb4f6b4fb57ec8368cff076abffba8d6923af032affb14be43b6e2dfd8b25085947a3204d702a8c8e9d79d3c361373cf98566df682420728857906289 languageName: node linkType: hard -"vite@npm:^4.2.1": - version: 4.2.1 - resolution: "vite@npm:4.2.1" +"vite@npm:^4.2.2": + version: 4.2.2 + resolution: "vite@npm:4.2.2" dependencies: esbuild: ^0.17.5 fsevents: ~2.3.2 @@ -6415,7 +6445,7 @@ __metadata: optional: true bin: vite: bin/vite.js - checksum: a64e3a1563b9584d1fd1ca45e06ee3c9fa4956320e6d4e9d83bf09fc8e64bb9d3ef62f664b6f740141eee16643690e8a41ffdb3030f4f2e170c57894df1f9a5d + checksum: 60e7298c817f0626bcbfdfc8877431421eabab85131c64d69e58f5ea20a66c14c4e0901ed63286b362627a667560d257dd135615b63f844da6052f6248dd7be6 languageName: node linkType: hard diff --git a/mock-api/server.js b/mock-api/server.js index 23c8047cb..dea914377 100644 --- a/mock-api/server.js +++ b/mock-api/server.js @@ -14,7 +14,6 @@ rest_server.use(express.json()); // endpoints const API_ENDPOINT_ROOT = '/api/'; const REST_ENDPOINT_ROOT = '/rest/'; -const EVENTSOURCE_ENDPOINT_ROOT = '/es/'; // LOG const LOG_SETTINGS_ENDPOINT = REST_ENDPOINT_ROOT + 'logSettings'; @@ -610,7 +609,8 @@ let emsesp_entities = { offset: 0, factor: 0, uom: 2, - val_type: 2 + value: 1, + value_type: 2 }, { id: 1, @@ -620,7 +620,8 @@ let emsesp_entities = { offset: 2, factor: 2, uom: 4, - val_type: 5 + value: 2, + value_type: 5 } ] }; diff --git a/src/web/WebEntityService.cpp b/src/web/WebEntityService.cpp index c5ff61ce4..2572f7ce7 100644 --- a/src/web/WebEntityService.cpp +++ b/src/web/WebEntityService.cpp @@ -33,24 +33,27 @@ void WebEntityService::begin() { EMSESP::logger().info("Starting custom entity service"); } -// this creates the scheduler file, saving it to the FS -// and also calls when the Scheduler web page is refreshed +// this creates the entity file, saving it to the FS +// and also calls when the Entity web page is refreshed 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) { - JsonObject ei = entity.createNestedObject(); - ei["device_id"] = Helpers::hextoa(entityItem.device_id, false); - ei["type_id"] = Helpers::hextoa(entityItem.type_id, false); - ei["offset"] = entityItem.offset; - ei["factor"] = entityItem.factor; - ei["name"] = entityItem.name; - ei["uom"] = entityItem.uom; - ei["val_type"] = entityItem.valuetype; + JsonObject ei = entity.createNestedObject(); + ei["id"] = counter++; // id is only used to render the table and must be unique + ei["device_id"] = Helpers::hextoa(entityItem.device_id, false); + ei["type_id"] = Helpers::hextoa(entityItem.type_id, false); + ei["offset"] = entityItem.offset; + ei["factor"] = entityItem.factor; + ei["name"] = entityItem.name; + ei["uom"] = entityItem.uom; + ei["value_type"] = entityItem.value_type; + ei["writeable"] = entityItem.writeable; 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 StateUpdateResult WebEntity::update(JsonObject & root, WebEntity & webEntity) { for (EntityItem & entityItem : webEntity.entityItems) { @@ -60,36 +63,42 @@ StateUpdateResult WebEntity::update(JsonObject & root, WebEntity & webEntity) { if (root["entity"].is()) { for (const JsonObject ei : root["entity"].as()) { - auto entityItem = EntityItem(); - entityItem.device_id = Helpers::hextoint(ei["device_id"]); - entityItem.type_id = Helpers::hextoint(ei["type_id"]); - entityItem.offset = ei["offset"]; - entityItem.factor = ei["factor"]; - entityItem.name = ei["name"].as(); - entityItem.uom = ei["uom"]; - entityItem.valuetype = ei["val_type"]; + auto entityItem = EntityItem(); + entityItem.device_id = Helpers::hextoint(ei["device_id"]); // TODO don't need + entityItem.type_id = Helpers::hextoint(ei["type_id"]); + entityItem.offset = ei["offset"]; + entityItem.factor = ei["factor"]; + entityItem.name = ei["name"].as(); + entityItem.uom = ei["uom"]; + entityItem.value_type = ei["value_type"]; + entityItem.writeable = ei["writeable"]; - if (entityItem.valuetype == DeviceValueType::BOOL) { - entityItem.val = EMS_VALUE_DEFAULT_BOOL; - } else if (entityItem.valuetype == DeviceValueType::INT) { - entityItem.val = EMS_VALUE_DEFAULT_INT; - } else if (entityItem.valuetype == DeviceValueType::UINT) { - entityItem.val = EMS_VALUE_DEFAULT_UINT; - } else if (entityItem.valuetype == DeviceValueType::SHORT) { - entityItem.val = EMS_VALUE_DEFAULT_SHORT; - } else if (entityItem.valuetype == DeviceValueType::USHORT) { - entityItem.val = EMS_VALUE_DEFAULT_USHORT; - } else { // if (entityItem.valuetype == DeviceValueType::ULONG || entityItem.valuetype == DeviceValueType::TIME) { - entityItem.val = EMS_VALUE_DEFAULT_ULONG; + if (entityItem.value_type == DeviceValueType::BOOL) { + entityItem.value = EMS_VALUE_DEFAULT_BOOL; + } else if (entityItem.value_type == DeviceValueType::INT) { + entityItem.value = EMS_VALUE_DEFAULT_INT; + } else if (entityItem.value_type == DeviceValueType::UINT) { + entityItem.value = EMS_VALUE_DEFAULT_UINT; + } else if (entityItem.value_type == DeviceValueType::SHORT) { + entityItem.value = EMS_VALUE_DEFAULT_SHORT; + } else if (entityItem.value_type == DeviceValueType::USHORT) { + entityItem.value = EMS_VALUE_DEFAULT_USHORT; + } else { // if (entityItem.value_type == DeviceValueType::ULONG || entityItem.valuetype == DeviceValueType::TIME) { + entityItem.value = EMS_VALUE_DEFAULT_ULONG; } webEntity.entityItems.push_back(entityItem); // add to list - Command::add( - EMSdevice::DeviceType::CUSTOM, - webEntity.entityItems.back().name.c_str(), - [webEntity](const char * value, const int8_t id) { return EMSESP::webEntityService.command_setvalue(value, webEntity.entityItems.back().name); }, - FL_(entity_cmd), - CommandFlag::ADMIN_ONLY); + + if (entityItem.writeable) { + Command::add( + EMSdevice::DeviceType::CUSTOM, + webEntity.entityItems.back().name.c_str(), + [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; @@ -100,7 +109,7 @@ bool WebEntityService::command_setvalue(const char * value, const std::string na EMSESP::webEntityService.read([&](WebEntity & webEntity) { entityItems = &webEntity.entityItems; }); for (EntityItem & entityItem : *entityItems) { if (entityItem.name == name) { - if (entityItem.valuetype == DeviceValueType::BOOL) { + if (entityItem.value_type == DeviceValueType::BOOL) { bool v; if (!Helpers::value2bool(value, v)) { return false; @@ -112,9 +121,9 @@ bool WebEntityService::command_setvalue(const char * value, const std::string na return false; } 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); - } 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)}; EMSESP::send_write_request(entityItem.type_id, entityItem.device_id, entityItem.offset, v1, 2, 0); } 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); } } + publish_single(entityItem); if (EMSESP::mqtt_.get_publish_onchange(0)) { 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) { char payload[12]; std::string name = useVal ? "value" : entity.name; - switch (entity.valuetype) { + switch (entity.value_type) { 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) { - 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) { - output[name] = (uint8_t)entity.val ? 1 : 0; + output[name] = (uint8_t)entity.value ? 1 : 0; } else { - output[name] = Helpers::render_boolean(payload, (uint8_t)entity.val); + output[name] = Helpers::render_boolean(payload, (uint8_t)entity.value); } } break; case DeviceValueType::INT: - if ((int8_t)entity.val != EMS_VALUE_INT_NOTSET) { - output[name] = serialized(Helpers::render_value(payload, entity.factor * (int8_t)entity.val, 2)); + if ((int8_t)entity.value != EMS_VALUE_INT_NOTSET) { + output[name] = serialized(Helpers::render_value(payload, entity.factor * (int8_t)entity.value, 2)); } break; case DeviceValueType::UINT: - if ((uint8_t)entity.val != EMS_VALUE_UINT_NOTSET) { - output[name] = serialized(Helpers::render_value(payload, entity.factor * (uint8_t)entity.val, 2)); + if ((uint8_t)entity.value != EMS_VALUE_UINT_NOTSET) { + output[name] = serialized(Helpers::render_value(payload, entity.factor * (uint8_t)entity.value, 2)); } break; case DeviceValueType::SHORT: - if ((int16_t)entity.val != EMS_VALUE_SHORT_NOTSET) { - output[name] = serialized(Helpers::render_value(payload, entity.factor * (int16_t)entity.val, 2)); + if ((int16_t)entity.value != EMS_VALUE_SHORT_NOTSET) { + output[name] = serialized(Helpers::render_value(payload, entity.factor * (int16_t)entity.value, 2)); } break; case DeviceValueType::USHORT: - if ((uint16_t)entity.val != EMS_VALUE_USHORT_NOTSET) { - output[name] = serialized(Helpers::render_value(payload, entity.factor * (uint16_t)entity.val, 2)); + if ((uint16_t)entity.value != EMS_VALUE_USHORT_NOTSET) { + output[name] = serialized(Helpers::render_value(payload, entity.factor * (uint16_t)entity.value, 2)); } break; case DeviceValueType::ULONG: case DeviceValueType::TIME: - if (entity.val != EMS_VALUE_ULONG_NOTSET) { - output[name] = serialized(Helpers::render_value(payload, entity.factor * entity.val, 2)); + if (entity.value != EMS_VALUE_ULONG_NOTSET) { + output[name] = serialized(Helpers::render_value(payload, entity.factor * entity.value, 2)); } break; default: @@ -215,7 +225,7 @@ bool WebEntityService::get_value_info(JsonObject & output, const char * cmd) { output["name"] = entity.name; output["uom"] = EMSdevice::uom_to_string(entity.uom); output["readable"] = true; - output["writeable"] = true; + output["writeable"] = entity.writeable; output["visible"] = true; render_value(output, entity, true); if (attribute_s) { @@ -250,8 +260,8 @@ void WebEntityService::publish_single(const EntityItem & entity) { } else { snprintf(topic, sizeof(topic), "%s/%s", "custom_data", entity.name.c_str()); } - StaticJsonDocument<256> doc; - JsonObject output = doc.to(); + StaticJsonDocument doc; + JsonObject output = doc.to(); render_value(output, entity, true); Mqtt::queue_publish(topic, output["value"].as()); } @@ -326,8 +336,10 @@ uint8_t WebEntityService::count_entities() { if (entityItems->size() == 0) { return 0; } + DynamicJsonDocument doc(EMSESP_JSON_SIZE_XLARGE); JsonObject output = doc.to(); + for (const EntityItem & entity : *entityItems) { render_value(output, entity); } @@ -337,46 +349,51 @@ uint8_t WebEntityService::count_entities() { // send to dashboard, msgpack don't like serialized, use number void WebEntityService::generate_value_web(JsonObject & output) { EMSESP::webEntityService.read([&](WebEntity & webEntity) { entityItems = &webEntity.entityItems; }); + output["label"] = (std::string) "Custom Entities"; JsonArray data = output.createNestedArray("data"); + for (const EntityItem & entity : *entityItems) { JsonObject obj = data.createNestedObject(); // create the object, we know there is a value obj["id"] = "00" + entity.name; obj["u"] = entity.uom; - obj["c"] = entity.name; - switch (entity.valuetype) { + if (entity.writeable) { + obj["c"] = entity.name; + } + + switch (entity.value_type) { case DeviceValueType::BOOL: { 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"); l.add(Helpers::render_boolean(s, false, true)); l.add(Helpers::render_boolean(s, true, true)); break; } case DeviceValueType::INT: - if ((int8_t)entity.val != EMS_VALUE_INT_NOTSET) { - obj["v"] = Helpers::transformNumFloat(entity.factor * (int8_t)entity.val, 0); + if ((int8_t)entity.value != EMS_VALUE_INT_NOTSET) { + obj["v"] = Helpers::transformNumFloat(entity.factor * (int8_t)entity.value, 0); } break; case DeviceValueType::UINT: - if ((uint8_t)entity.val != EMS_VALUE_UINT_NOTSET) { - obj["v"] = Helpers::transformNumFloat(entity.factor * (uint8_t)entity.val, 0); + if ((uint8_t)entity.value != EMS_VALUE_UINT_NOTSET) { + obj["v"] = Helpers::transformNumFloat(entity.factor * (uint8_t)entity.value, 0); } break; case DeviceValueType::SHORT: - if ((int16_t)entity.val != EMS_VALUE_SHORT_NOTSET) { - obj["v"] = Helpers::transformNumFloat(entity.factor * (int16_t)entity.val, 0); + if ((int16_t)entity.value != EMS_VALUE_SHORT_NOTSET) { + obj["v"] = Helpers::transformNumFloat(entity.factor * (int16_t)entity.value, 0); } break; case DeviceValueType::USHORT: - if ((uint16_t)entity.val != EMS_VALUE_USHORT_NOTSET) { - obj["v"] = Helpers::transformNumFloat(entity.factor * (uint16_t)entity.val, 0); + if ((uint16_t)entity.value != EMS_VALUE_USHORT_NOTSET) { + obj["v"] = Helpers::transformNumFloat(entity.factor * (uint16_t)entity.value, 0); } break; case DeviceValueType::ULONG: case DeviceValueType::TIME: - if (entity.val != EMS_VALUE_ULONG_NOTSET) { - obj["v"] = Helpers::transformNumFloat(entity.factor * entity.val, 0); + if (entity.value != EMS_VALUE_ULONG_NOTSET) { + obj["v"] = Helpers::transformNumFloat(entity.factor * entity.value, 0); } break; default: @@ -402,13 +419,13 @@ bool WebEntityService::get_value(std::shared_ptr telegram) { const uint8_t len[] = {1, 1, 1, 2, 2, 3, 3}; for (auto & entity : *entityItems) { 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])) { - uint32_t val = 0; - for (uint8_t i = 0; i < len[entity.valuetype]; i++) { - val = (val << 8) + telegram->message_data[i + entity.offset - telegram->offset]; + && (telegram->offset + telegram->message_length) >= (entity.offset + len[entity.value_type])) { + uint32_t value = 0; + for (uint8_t i = 0; i < len[entity.value_type]; i++) { + value = (value << 8) + telegram->message_data[i + entity.offset - telegram->offset]; } - if (val != entity.val) { - entity.val = val; + if (value != entity.value) { + entity.value = value; if (Mqtt::publish_single()) { publish_single(entity); } else if (EMSESP::mqtt_.get_publish_onchange(0)) { diff --git a/src/web/WebEntityService.h b/src/web/WebEntityService.h index fc58370cb..41de3ed9c 100644 --- a/src/web/WebEntityService.h +++ b/src/web/WebEntityService.h @@ -1,7 +1,7 @@ /* * EMS-ESP - https://github.com/emsesp/EMS-ESP * Copyright 2020-2023 Paul Derbyshire - * + * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or @@ -27,14 +27,16 @@ namespace emsesp { class EntityItem { public: + uint8_t id; uint8_t device_id; uint16_t type_id; uint8_t offset; - int8_t valuetype; + int8_t value_type; uint8_t uom; std::string name; double factor; - uint32_t val; + bool writeable; + uint32_t value; }; class WebEntity { @@ -65,7 +67,7 @@ class WebEntityService : public StatefulService { HttpEndpoint _httpEndpoint; FSPersistence _fsPersistence; - std::list * entityItems; // pointer to the list of schedule events + std::list * entityItems; // pointer to the list of entity items bool ha_registered_ = false; };