diff --git a/.vscode/settings.json b/.vscode/settings.json index 4bba87e57..970fb6b8c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,9 +3,30 @@ "**/.yarn": true, "**/.pnp.*": true }, + "editor.codeActionsOnSave": { + "source.fixAll": true + // "source.organizeImports": true + }, "eslint.nodePath": "interface/.yarn/sdks", "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", + "string": "cpp", + "string_view": "cpp" + } } diff --git a/CHANGELOG.md b/CHANGELOG.md index 51ad02fa5..57a0e916e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -212,7 +212,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added pool data to telegrams 0x494 & 0x495 [#102](https://github.com/emsesp/EMS-ESP32/issues/102) - Add RC300 second summermode telegram [#108](https://github.com/emsesp/EMS-ESP32/issues/108) - Add support for the RC25 thermostat [#106](https://github.com/emsesp/EMS-ESP32/issues/106) -- Add new command 'entities' for a device, e.g. http://ems-esp/api/boiler/entities to show the shortname, description and HA Entity name (if HA enabled) [#116](https://github.com/emsesp/EMS-ESP32/issues/116) +- Add new command 'entities' for a device, e.g. to show the shortname, description and HA Entity name (if HA enabled) [#116](https://github.com/emsesp/EMS-ESP32/issues/116) - Support for Junkers program and remote (fb10/fb110) temperature - Home Assistant `state_class` attribute for Wh, kWh, W and KW [#129](https://github.com/emsesp/EMS-ESP32/issues/129) - Add current room influence for RC300 [#136](https://github.com/emsesp/EMS-ESP32/issues/136) @@ -434,4 +434,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - some names of mqtt-tags like in v2.2.1 - new ESP32 partition side to allow for smoother OTA and fallback - Network Gateway IP is optional (#682)emsesp/EMS-ESP -- moved to a new GitHub repo https://github.com/emsesp/EMS-ESP32 +- moved to a new GitHub repo diff --git a/CHANGELOG_LATEST.md b/CHANGELOG_LATEST.md index a45fe6e6e..6bf09662c 100644 --- a/CHANGELOG_LATEST.md +++ b/CHANGELOG_LATEST.md @@ -4,6 +4,11 @@ ## **IMPORTANT! BREAKING CHANGES** +There are breaking changes in 3.6.0. Please read carefully before applying the update. + +- The sensors have been renamed. `dallassensor` is now `temperaturesensor` in MQTT and `ts` in the Customizations file. Also `analogs` is now `analogsensor` in MQTT and `as` in the Customizations file. If you have customizations, make backup first using the Download option and rename the JSON arrays to `as` and `ts` respectively. Also removed any MQTT topics that start with `dallassensor` using something like MQTTExplorer. +- The format of the Custom Entities has changed, so you will need to manually re-create them. + ## Added - Workaround for better Domoticz MQTT intergration? [#904](https://github.com/emsesp/EMS-ESP32/issues/904) @@ -29,3 +34,5 @@ - Improved HA Discovery so previous configs no longer need to be removed when starting [#1077](https://github.com/emsesp/EMS-ESP32/pull/1077) (thanks @pswid!) - Enlarge UART-Stack to 2,5k - Retry timeout for Mqtt-QOS1/2 10seconds +- Optimize WebUI rendering when using Dialog Boxes [#1116](https://github.com/emsesp/EMS-ESP32/issues/1116) +- Optimize Web libraries to reduce bundle size (3.6.x) [#1112](https://github.com/emsesp/EMS-ESP32/issues/1112) diff --git a/interface/.eslintignore b/interface/.eslintignore index 55ba94de1..c04eade2b 100644 --- a/interface/.eslintignore +++ b/interface/.eslintignore @@ -1,9 +1,12 @@ node_modules/ build/ dist/ +.yarn/ + .prettierrc .eslintrc* -.yarn/ env.d.ts progmem-generator.js -vite.config.ts \ No newline at end of file +unpack.ts +vite.config.ts +package.json \ No newline at end of file diff --git a/interface/.eslintrc.json b/interface/.eslintrc.json index 135736d0f..4fc97f6f7 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"], @@ -51,6 +50,44 @@ "@typescript-eslint/restrict-plus-operands": "off", "@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": "warn", + "@typescript-eslint/consistent-type-imports": [ + "error", + { + "prefer": "type-imports" + } + ], + "import/order": [ + "warn", + { + "groups": ["builtin", "external", "parent", "sibling", "index", "object", "type"], + "pathGroups": [ + { + "pattern": "@/**/**", + "group": "parent", + "position": "before" + } + ], + "alphabetize": { "order": "asc" } + } + ], + // "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 de07d3a09..fc1eec4ed 100644 --- a/interface/package.json +++ b/interface/package.json @@ -16,49 +16,44 @@ "standalone": "npm-run-all -p dev typesafe-i18n mock-api", "typesafe-i18n": "typesafe-i18n", "format": "prettier --write '**/*.{ts,tsx,js,css,json,md}'", - "lint": "eslint . --cache --max-warnings=0" + "lint": "eslint . --cache --fix" }, "dependencies": { - "@emotion/react": "^11.10.6", - "@emotion/styled": "^11.10.6", - "@msgpack/msgpack": "^3.0.0-beta2", + "@emotion/react": "^11.11.0", + "@emotion/styled": "^11.11.0", "@mui/icons-material": "^5.11.16", - "@mui/material": "^5.11.16", - "@remix-run/router": "^1.5.0", - "@table-library/react-table-library": "4.1.0", + "@mui/material": "^5.13.2", + "@table-library/react-table-library": "4.1.4", "@types/lodash-es": "^4.17.7", - "@types/node": "^18.15.11", - "@types/react": "^18.0.33", - "@types/react-dom": "^18.0.11", + "@types/node": "^20.2.3", + "@types/react": "^18.2.7", + "@types/react-dom": "^18.2.4", "@types/react-router-dom": "^5.3.3", - "@yarnpkg/pnpify": "^4.0.0-rc.42", "async-validator": "^4.2.5", - "axios": "^1.3.5", + "axios": "^1.4.0", "history": "^5.3.0", "jwt-decode": "^3.1.2", "lodash-es": "^4.17.21", - "mime-types": "^2.1.35", "react": "latest", "react-dom": "latest", "react-dropzone": "^14.2.3", "react-icons": "^4.8.0", - "react-router-dom": "^6.10.0", - "react-toastify": "^9.1.2", + "react-router-dom": "^6.11.2", + "react-toastify": "^9.1.3", "sockette": "^2.0.6", "typesafe-i18n": "^5.24.3", - "typescript": "^5.0.3" + "typescript": "^5.0.4" }, "devDependencies": { - "@types/mime-types": "^2", - "@types/styled-components": "^5", - "@typescript-eslint/eslint-plugin": "^5.57.1", - "@typescript-eslint/parser": "^5.57.1", - "@vitejs/plugin-react-swc": "^3.2.0", - "eslint": "^8.37.0", + "@typescript-eslint/eslint-plugin": "^5.59.7", + "@typescript-eslint/parser": "^5.59.7", + "@vitejs/plugin-react-swc": "^3.3.1", + "eslint": "^8.41.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", @@ -66,13 +61,12 @@ "eslint-plugin-react-hooks": "^4.6.0", "nodemon": "^2.0.22", "npm-run-all": "^4.1.5", - "prettier": "^2.8.7", + "prettier": "^2.8.8", "rollup-plugin-visualizer": "^5.9.0", - "terser": "^5.16.8", - "vite": "^4.2.1", - "vite-plugin-minify": "^1.5.2", - "vite-plugin-svgr": "^2.4.0", - "vite-tsconfig-paths": "^4.0.8" + "terser": "^5.17.6", + "vite": "^4.3.8", + "vite-plugin-svgr": "^3.2.0", + "vite-tsconfig-paths": "^4.2.0" }, "packageManager": "yarn@3.4.1" } diff --git a/interface/progmem-generator.js b/interface/progmem-generator.js index f9d9b3ad7..2a96ee410 100644 --- a/interface/progmem-generator.js +++ b/interface/progmem-generator.js @@ -1,5 +1,5 @@ -const { resolve, relative, sep } = require('path'); const { readdirSync, existsSync, unlinkSync, readFileSync, createWriteStream } = require('fs'); +const { resolve, relative, sep } = require('path'); var zlib = require('zlib'); var mime = require('mime-types'); @@ -18,9 +18,9 @@ function getFilesSync(dir, files = []) { return files; } -function coherseToBuffer(input) { - return Buffer.isBuffer(input) ? input : Buffer.from(input); -} +// function coherseToBuffer(input) { +// return Buffer.isBuffer(input) ? input : Buffer.from(input); +// } function cleanAndOpen(path) { if (existsSync(path)) { @@ -87,9 +87,8 @@ export default function ProgmemGenerator({ outputPath = './WWWData.h', bytesPerL // }); }; - const generateWWWClass = () => { - // eslint-disable-next-line max-len - return `typedef std::function RouteRegistrationHandler; + const generateWWWClass = () => + `typedef std::function RouteRegistrationHandler; class WWWData { ${indent}public: @@ -100,8 +99,6 @@ ${fileInfo ${indent.repeat(2)}} }; `; - }; - const writeWWWClass = () => { writeStream.write(generateWWWClass()); }; diff --git a/interface/public/css/roboto.css b/interface/public/css/roboto.css index 43f4bb622..dd5649d0c 100644 --- a/interface/public/css/roboto.css +++ b/interface/public/css/roboto.css @@ -1,15 +1,14 @@ -/* -* Just supporting latin due to size constrains on the esp chip -* -* The framework only makes use of 400 (regular) weight fonts. -* -* If using medium (500), light or strong typography variants you will need to add additional fonts. +/* +* Uses font-size 400 (normal) only and Latin (plus extra unicode chars) to keep flash memory to a minimun +* View fonts on https://fonts.google.com/ +* Download woff2 using e.g. https://fonts.googleapis.com/css2?family=Lato or https://fonts.googleapis.com/css2?family=Roboto */ @font-face { font-family: 'Roboto'; font-style: normal; font-weight: 400; - src: local('Roboto'), local('Roboto-Regular'), url(../fonts/re.woff2) format('woff2'); + /* src: url(https://fonts.gstatic.com/s/roboto/v30/KFOmCnqEu92Fr1Mu4mxK.woff2) format('woff2'); */ + src: local('Roboto'), local('Roboto-Regular'), url(../fonts/re.off2) format('woff2'); unicode-range: U+0000-00FF, U+0104-0107, U+0118-0119, U+011E-011F, U+0130-0131, U+0141-0144, U+0152-0153, U+015A-015B, U+015E-015F, U+0179-017C, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; diff --git a/interface/public/fonts/re.woff2 b/interface/public/fonts/re.woff2 index 716979a34..020729ef8 100644 Binary files a/interface/public/fonts/re.woff2 and b/interface/public/fonts/re.woff2 differ diff --git a/interface/src/App.tsx b/interface/src/App.tsx index 4fe3a46a5..77f79e41d 100644 --- a/interface/src/App.tsx +++ b/interface/src/App.tsx @@ -1,12 +1,13 @@ -import { FC, useEffect, useState } from 'react'; - +import { useEffect, useState } from 'react'; import { ToastContainer, Slide } from 'react-toastify'; + import 'react-toastify/dist/ReactToastify.min.css'; -import CustomTheme from 'CustomTheme'; -import AppRouting from 'AppRouting'; - import { localStorageDetector } from 'typesafe-i18n/detectors'; +import type { FC } from 'react'; +import AppRouting from 'AppRouting'; +import CustomTheme from 'CustomTheme'; + import TypesafeI18n from 'i18n/i18n-react'; import { detectLocale } from 'i18n/i18n-util'; import { loadLocaleAsync } from 'i18n/i18n-util.async'; @@ -17,7 +18,7 @@ const App: FC = () => { const [wasLoaded, setWasLoaded] = useState(false); useEffect(() => { - loadLocaleAsync(detectedLocale).then(() => setWasLoaded(true)); + void loadLocaleAsync(detectedLocale).then(() => setWasLoaded(true)); }, []); if (!wasLoaded) return null; diff --git a/interface/src/AppRouting.tsx b/interface/src/AppRouting.tsx index b71c9eba2..7e6507621 100644 --- a/interface/src/AppRouting.tsx +++ b/interface/src/AppRouting.tsx @@ -1,16 +1,16 @@ -import { FC, useContext, useEffect } from 'react'; +import { useContext, useEffect } from 'react'; import { Route, Routes, Navigate, useLocation } from 'react-router-dom'; import { toast } from 'react-toastify'; +import type { FC } from 'react'; -import { useI18nContext } from 'i18n/i18n-react'; - -import { Authentication, AuthenticationContext } from 'contexts/authentication'; +import AuthenticatedRouting from 'AuthenticatedRouting'; +import SignIn from 'SignIn'; import { RequireAuthenticated, RequireUnauthenticated } from 'components'; -import SignIn from 'SignIn'; -import AuthenticatedRouting from 'AuthenticatedRouting'; +import { Authentication, AuthenticationContext } from 'contexts/authentication'; +import { useI18nContext } from 'i18n/i18n-react'; interface SecurityRedirectProps { message: string; diff --git a/interface/src/AuthenticatedRouting.tsx b/interface/src/AuthenticatedRouting.tsx index cf4e7c2e0..eaf6041f2 100644 --- a/interface/src/AuthenticatedRouting.tsx +++ b/interface/src/AuthenticatedRouting.tsx @@ -1,21 +1,21 @@ -import { FC, useCallback, useEffect } from 'react'; +import { useCallback, useEffect } from 'react'; import { Navigate, Routes, Route, useNavigate, useLocation } from 'react-router-dom'; -import { AxiosError } from 'axios'; +import Dashboard from './project/Dashboard'; +import Help from './project/Help'; +import Settings from './project/Settings'; +import type { AxiosError } from 'axios'; +import type { FC } from 'react'; import * as AuthenticationApi from 'api/authentication'; import { AXIOS } from 'api/endpoints'; import { Layout, RequireAdmin } from 'components'; -import Dashboard from './project/Dashboard'; -import Settings from './project/Settings'; -import Help from './project/Help'; - -import NetworkConnection from 'framework/network/NetworkConnection'; import AccessPoint from 'framework/ap/AccessPoint'; -import NetworkTime from 'framework/ntp/NetworkTime'; import Mqtt from 'framework/mqtt/Mqtt'; -import System from 'framework/system/System'; +import NetworkConnection from 'framework/network/NetworkConnection'; +import NetworkTime from 'framework/ntp/NetworkTime'; import Security from 'framework/security/Security'; +import System from 'framework/system/System'; const AuthenticatedRouting: FC = () => { const location = useLocation(); diff --git a/interface/src/CustomTheme.tsx b/interface/src/CustomTheme.tsx index 91e1b5c5d..a482e0b28 100644 --- a/interface/src/CustomTheme.tsx +++ b/interface/src/CustomTheme.tsx @@ -1,10 +1,9 @@ -import { FC } from 'react'; - import { CssBaseline } from '@mui/material'; -import { createTheme, responsiveFontSizes, ThemeProvider } from '@mui/material/styles'; import { blueGrey, blue } from '@mui/material/colors'; +import { createTheme, responsiveFontSizes, ThemeProvider } from '@mui/material/styles'; +import type { FC } from 'react'; -import { RequiredChildrenProps } from 'utils'; +import type { RequiredChildrenProps } from 'utils'; const theme = responsiveFontSizes( createTheme({ diff --git a/interface/src/SignIn.tsx b/interface/src/SignIn.tsx index 85881c2f8..0cca183c9 100644 --- a/interface/src/SignIn.tsx +++ b/interface/src/SignIn.tsx @@ -1,31 +1,30 @@ -import { FC, useContext, useState } from 'react'; -import { ValidateFieldsError } from 'async-validator'; -import { toast } from 'react-toastify'; - -import { Box, Fab, Paper, Typography, Button } from '@mui/material'; import ForwardIcon from '@mui/icons-material/Forward'; +import { Box, Fab, Paper, Typography, Button } from '@mui/material'; +import { useContext, useState } from 'react'; +import { toast } from 'react-toastify'; +import type { ValidateFieldsError } from 'async-validator'; +import type { Locales } from 'i18n/i18n-types'; +import type { FC } from 'react'; +import type { SignInRequest } from 'types'; import * as AuthenticationApi from 'api/authentication'; import { PROJECT_NAME } from 'api/env'; + +import { ValidatedTextField } from 'components'; import { AuthenticationContext } from 'contexts/authentication'; -import { extractErrorMessage, onEnterCallback, updateValue } from 'utils'; -import { SignInRequest } from 'types'; -import { ValidatedTextField } from 'components'; -import { SIGN_IN_REQUEST_VALIDATOR, validate } from 'validators'; - -import { I18nContext } from 'i18n/i18n-react'; -import type { Locales } from 'i18n/i18n-types'; -import { loadLocaleAsync } from 'i18n/i18n-util.async'; - -import { ReactComponent as NLflag } from 'i18n/NL.svg'; import { ReactComponent as DEflag } from 'i18n/DE.svg'; -import { ReactComponent as GBflag } from 'i18n/GB.svg'; -import { ReactComponent as SVflag } from 'i18n/SV.svg'; -import { ReactComponent as PLflag } from 'i18n/PL.svg'; -import { ReactComponent as NOflag } from 'i18n/NO.svg'; import { ReactComponent as FRflag } from 'i18n/FR.svg'; +import { ReactComponent as GBflag } from 'i18n/GB.svg'; +import { ReactComponent as NLflag } from 'i18n/NL.svg'; +import { ReactComponent as NOflag } from 'i18n/NO.svg'; +import { ReactComponent as PLflag } from 'i18n/PL.svg'; +import { ReactComponent as SVflag } from 'i18n/SV.svg'; import { ReactComponent as TRflag } from 'i18n/TR.svg'; +import { I18nContext } from 'i18n/i18n-react'; +import { loadLocaleAsync } from 'i18n/i18n-util.async'; +import { extractErrorMessage, onEnterCallback, updateValue } from 'utils'; +import { SIGN_IN_REQUEST_VALIDATOR, validate } from 'validators'; const SignIn: FC = () => { const authenticationContext = useContext(AuthenticationContext); @@ -64,7 +63,7 @@ const SignIn: FC = () => { }); try { await validate(SIGN_IN_REQUEST_VALIDATOR, signInRequest); - signIn(); + await signIn(); } catch (errors: any) { setFieldErrors(errors); setProcessing(false); diff --git a/interface/src/api/ap.ts b/interface/src/api/ap.ts index 81d1f81f5..734a40f64 100644 --- a/interface/src/api/ap.ts +++ b/interface/src/api/ap.ts @@ -1,7 +1,7 @@ -import { AxiosPromise } from 'axios'; - -import { APSettings, APStatus } from 'types'; import { AXIOS } from './endpoints'; +import type { AxiosPromise } from 'axios'; + +import type { APSettings, APStatus } from 'types'; export function readAPStatus(): AxiosPromise { return AXIOS.get('/apStatus'); diff --git a/interface/src/api/authentication.ts b/interface/src/api/authentication.ts index 32d0c4f90..9b8b01030 100644 --- a/interface/src/api/authentication.ts +++ b/interface/src/api/authentication.ts @@ -1,11 +1,10 @@ -import { AxiosPromise } from 'axios'; -import * as H from 'history'; import jwtDecode from 'jwt-decode'; -import { Path } from 'react-router-dom'; - -import { Me, SignInRequest, SignInResponse } from 'types'; - import { ACCESS_TOKEN, AXIOS } from './endpoints'; +import type { AxiosPromise } from 'axios'; +import type * as H from 'history'; +import type { Path } from 'react-router-dom'; + +import type { Me, SignInRequest, SignInResponse } from 'types'; export const SIGN_IN_PATHNAME = 'loginPathname'; export const SIGN_IN_SEARCH = 'loginSearch'; @@ -45,7 +44,7 @@ export function fetchLoginRedirect(): Partial { } export const clearAccessToken = () => localStorage.removeItem(ACCESS_TOKEN); -export const decodeMeJWT = (accessToken: string): Me => jwtDecode(accessToken) as Me; +export const decodeMeJWT = (accessToken: string): Me => jwtDecode(accessToken); export function addAccessTokenParameter(url: string) { const accessToken = getStorage().getItem(ACCESS_TOKEN); diff --git a/interface/src/api/endpoints.ts b/interface/src/api/endpoints.ts index 18ad2a612..7c003693b 100644 --- a/interface/src/api/endpoints.ts +++ b/interface/src/api/endpoints.ts @@ -1,6 +1,7 @@ -import axios, { AxiosPromise, CancelToken, AxiosProgressEvent } from 'axios'; +import axios from 'axios'; +import { unpack } from './unpack'; -import { decode } from '@msgpack/msgpack'; +import type { AxiosPromise, CancelToken, AxiosProgressEvent } from 'axios'; export const WS_BASE_URL = '/ws/'; export const API_BASE_URL = '/rest/'; @@ -72,11 +73,8 @@ export const AXIOS_BIN = axios.create({ return JSON.stringify(data); } ], - transformResponse: [ - (data) => { - return decode(data); - } - ] + // transformResponse: [(data) => decode(data)] + transformResponse: [(data) => unpack(data)] // new using msgpackr }); export interface FileUploadConfig { diff --git a/interface/src/api/features.ts b/interface/src/api/features.ts index c4e1605f9..6ada2405c 100644 --- a/interface/src/api/features.ts +++ b/interface/src/api/features.ts @@ -1,8 +1,7 @@ -import { AxiosPromise } from 'axios'; - -import { Features } from 'types'; - import { AXIOS } from './endpoints'; +import type { AxiosPromise } from 'axios'; + +import type { Features } from 'types'; export function readFeatures(): AxiosPromise { return AXIOS.get('/features'); diff --git a/interface/src/api/mqtt.ts b/interface/src/api/mqtt.ts index e3603050e..d599a3121 100644 --- a/interface/src/api/mqtt.ts +++ b/interface/src/api/mqtt.ts @@ -1,7 +1,6 @@ -import { AxiosPromise } from 'axios'; -import { MqttSettings, MqttStatus } from 'types'; - import { AXIOS } from './endpoints'; +import type { AxiosPromise } from 'axios'; +import type { MqttSettings, MqttStatus } from 'types'; export function readMqttStatus(): AxiosPromise { return AXIOS.get('/mqttStatus'); diff --git a/interface/src/api/network.ts b/interface/src/api/network.ts index 4993964ab..a9535336c 100644 --- a/interface/src/api/network.ts +++ b/interface/src/api/network.ts @@ -1,8 +1,7 @@ -import { AxiosPromise } from 'axios'; - -import { WiFiNetworkList, NetworkSettings, NetworkStatus } from 'types'; - import { AXIOS } from './endpoints'; +import type { AxiosPromise } from 'axios'; + +import type { WiFiNetworkList, NetworkSettings, NetworkStatus } from 'types'; export function readNetworkStatus(): AxiosPromise { return AXIOS.get('/networkStatus'); diff --git a/interface/src/api/ntp.ts b/interface/src/api/ntp.ts index cdd81862d..dcd3c7ca9 100644 --- a/interface/src/api/ntp.ts +++ b/interface/src/api/ntp.ts @@ -1,7 +1,6 @@ -import { AxiosPromise } from 'axios'; -import { NTPSettings, NTPStatus, Time } from 'types'; - import { AXIOS } from './endpoints'; +import type { AxiosPromise } from 'axios'; +import type { NTPSettings, NTPStatus, Time } from 'types'; export function readNTPStatus(): AxiosPromise { return AXIOS.get('/ntpStatus'); diff --git a/interface/src/api/security.ts b/interface/src/api/security.ts index dab188aaf..f46194432 100644 --- a/interface/src/api/security.ts +++ b/interface/src/api/security.ts @@ -1,8 +1,7 @@ -import { AxiosPromise } from 'axios'; - -import { SecuritySettings, Token } from 'types'; - import { AXIOS } from './endpoints'; +import type { AxiosPromise } from 'axios'; + +import type { SecuritySettings, Token } from 'types'; export function readSecuritySettings(): AxiosPromise { return AXIOS.get('/securitySettings'); diff --git a/interface/src/api/system.ts b/interface/src/api/system.ts index 5c5972c7b..dc9b34db6 100644 --- a/interface/src/api/system.ts +++ b/interface/src/api/system.ts @@ -1,8 +1,8 @@ -import { AxiosPromise } from 'axios'; +import { AXIOS, AXIOS_BIN, startUploadFile } from './endpoints'; +import type { FileUploadConfig } from './endpoints'; +import type { AxiosPromise } from 'axios'; -import { OTASettings, SystemStatus, LogSettings, LogEntries } from 'types'; - -import { AXIOS, AXIOS_BIN, FileUploadConfig, startUploadFile } from './endpoints'; +import type { OTASettings, SystemStatus, LogSettings, LogEntries } from 'types'; export function readSystemStatus(timeout?: number): AxiosPromise { return AXIOS.get('/systemStatus', { timeout }); diff --git a/interface/src/api/unpack.ts b/interface/src/api/unpack.ts new file mode 100644 index 000000000..d28619786 --- /dev/null +++ b/interface/src/api/unpack.ts @@ -0,0 +1,1134 @@ +let decoder; +try { + decoder = new TextDecoder(); +} catch (error) {} +let src; +let srcEnd; +let position = 0; +const EMPTY_ARRAY = []; +let strings = EMPTY_ARRAY; +let stringPosition = 0; +let currentUnpackr = {}; +let currentStructures; +let srcString; +let srcStringStart = 0; +let srcStringEnd = 0; +let bundledStrings; +let referenceMap; +const currentExtensions = []; +let dataView; +const defaultOptions = { + useRecords: false, + mapsAsObjects: true +}; +export class C1Type {} +export const C1 = new C1Type(); +C1.name = 'MessagePack 0xC1'; +let sequentialMode = false; +let inlineObjectReadThreshold = 2; +let readStruct, onLoadedStructures, onSaveState; +// no-eval build +try { + new Function(''); +} catch (error) { + // if eval variants are not supported, do not create inline object readers ever + inlineObjectReadThreshold = Infinity; +} + +export class Unpackr { + constructor(options) { + if (options) { + if (options.useRecords === false && options.mapsAsObjects === undefined) options.mapsAsObjects = true; + if (options.sequential && options.trusted !== false) { + options.trusted = true; + if (!options.structures && options.useRecords != false) { + options.structures = []; + if (!options.maxSharedStructures) options.maxSharedStructures = 0; + } + } + if (options.structures) options.structures.sharedLength = options.structures.length; + else if (options.getStructures) { + (options.structures = []).uninitialized = true; // this is what we use to denote an uninitialized structures + options.structures.sharedLength = 0; + } + if (options.int64AsNumber) { + options.int64AsType = 'number'; + } + } + Object.assign(this, options); + } + + unpack(source, options?: any) { + if (src) { + // re-entrant execution, save the state and restore it after we do this unpack + return saveState(() => { + clearSource(); + return this ? this.unpack(source, options) : Unpackr.prototype.unpack.call(defaultOptions, source, options); + }); + } + if (!source.buffer && source.constructor === ArrayBuffer) + source = typeof Buffer !== 'undefined' ? Buffer.from(source) : new Uint8Array(source); + if (typeof options === 'object') { + srcEnd = options.end || source.length; + position = options.start || 0; + } else { + position = 0; + srcEnd = options > -1 ? options : source.length; + } + stringPosition = 0; + srcStringEnd = 0; + srcString = null; + strings = EMPTY_ARRAY; + bundledStrings = null; + src = source; + // this provides cached access to the data view for a buffer if it is getting reused, which is a recommend + // technique for getting data from a database where it can be copied into an existing buffer instead of creating + // new ones + try { + dataView = + source.dataView || (source.dataView = new DataView(source.buffer, source.byteOffset, source.byteLength)); + } catch (error) { + // if it doesn't have a buffer, maybe it is the wrong type of object + src = null; + if (source instanceof Uint8Array) throw error; + throw new Error( + 'Source must be a Uint8Array or Buffer but was a ' + + (source && typeof source == 'object' ? source.constructor.name : typeof source) + ); + } + if (this instanceof Unpackr) { + currentUnpackr = this; + if (this.structures) { + currentStructures = this.structures; + return checkedRead(options); + } else if (!currentStructures || currentStructures.length > 0) { + currentStructures = []; + } + } else { + currentUnpackr = defaultOptions; + if (!currentStructures || currentStructures.length > 0) currentStructures = []; + } + return checkedRead(options); + } + + unpackMultiple(source, forEach) { + let values, + lastPosition = 0; + try { + sequentialMode = true; + const size = source.length; + const value = this ? this.unpack(source, size) : defaultUnpackr.unpack(source, size); + if (forEach) { + if (forEach(value) === false) return; + while (position < size) { + lastPosition = position; + if (forEach(checkedRead()) === false) { + return; + } + } + } else { + values = [value]; + while (position < size) { + lastPosition = position; + values.push(checkedRead()); + } + return values; + } + } catch (error) { + error.lastPosition = lastPosition; + error.values = values; + throw error; + } finally { + sequentialMode = false; + clearSource(); + } + } + + _mergeStructures(loadedStructures, existingStructures) { + if (onLoadedStructures) loadedStructures = onLoadedStructures.call(this, loadedStructures); + loadedStructures = loadedStructures || []; + if (Object.isFrozen(loadedStructures)) loadedStructures = loadedStructures.map((structure) => structure.slice(0)); + for (let i = 0, l = loadedStructures.length; i < l; i++) { + const structure = loadedStructures[i]; + if (structure) { + structure.isShared = true; + if (i >= 32) structure.highByte = (i - 32) >> 5; + } + } + loadedStructures.sharedLength = loadedStructures.length; + for (const id in existingStructures || []) { + if (id >= 0) { + const structure = loadedStructures[id]; + const existing = existingStructures[id]; + if (existing) { + if (structure) + (loadedStructures.restoreStructures || (loadedStructures.restoreStructures = []))[id] = structure; + loadedStructures[id] = existing; + } + } + } + return (this.structures = loadedStructures); + } + + decode(source, end) { + return this.unpack(source, end); + } +} +export function getPosition() { + return position; +} +export function checkedRead(options: any) { + try { + if (!currentUnpackr.trusted && !sequentialMode) { + const sharedLength = currentStructures.sharedLength || 0; + if (sharedLength < currentStructures.length) currentStructures.length = sharedLength; + } + let result; + if (currentUnpackr.randomAccessStructure && src[position] < 0x40 && src[position] >= 0x20 && readStruct) { + result = readStruct(src, position, srcEnd, currentUnpackr); + src = null; // dispose of this so that recursive unpack calls don't save state + if (!(options && options.lazy) && result) result = result.toJSON(); + position = srcEnd; + } else result = read(); + if (bundledStrings) { + // bundled strings to skip past + position = bundledStrings.postBundlePosition; + bundledStrings = null; + } + + if (position == srcEnd) { + // finished reading this source, cleanup references + if (currentStructures && currentStructures.restoreStructures) restoreStructures(); + currentStructures = null; + src = null; + if (referenceMap) referenceMap = null; + } else if (position > srcEnd) { + // over read + throw new Error('Unexpected end of MessagePack data'); + } else if (!sequentialMode) { + let jsonView; + try { + jsonView = JSON.stringify(result, (_, value) => (typeof value === 'bigint' ? `${value}n` : value)).slice( + 0, + 100 + ); + } catch (error) { + jsonView = '(JSON view not available ' + error + ')'; + } + throw new Error('Data read, but end of buffer not reached ' + jsonView); + } + // else more to read, but we are reading sequentially, so don't clear source yet + return result; + } catch (error) { + if (currentStructures && currentStructures.restoreStructures) restoreStructures(); + clearSource(); + if (error instanceof RangeError || error.message.startsWith('Unexpected end of buffer') || position > srcEnd) { + error.incomplete = true; + } + throw error; + } +} + +function restoreStructures() { + for (const id in currentStructures.restoreStructures) { + currentStructures[id] = currentStructures.restoreStructures[id]; + } + currentStructures.restoreStructures = null; +} + +export function read() { + let token = src[position++]; + if (token < 0xa0) { + if (token < 0x80) { + if (token < 0x40) return token; + else { + const structure = + currentStructures[token & 0x3f] || (currentUnpackr.getStructures && loadStructures()[token & 0x3f]); + if (structure) { + if (!structure.read) { + structure.read = createStructureReader(structure, token & 0x3f); + } + return structure.read(); + } else return token; + } + } else if (token < 0x90) { + // map + token -= 0x80; + if (currentUnpackr.mapsAsObjects) { + const object = {}; + for (let i = 0; i < token; i++) { + let key = readKey(); + if (key === '__proto__') key = '__proto_'; + object[key] = read(); + } + return object; + } else { + const map = new Map(); + for (let i = 0; i < token; i++) { + map.set(read(), read()); + } + return map; + } + } else { + token -= 0x90; + const array = new Array(token); + for (let i = 0; i < token; i++) { + array[i] = read(); + } + if (currentUnpackr.freezeData) return Object.freeze(array); + return array; + } + } else if (token < 0xc0) { + // fixstr + const length = token - 0xa0; + if (srcStringEnd >= position) { + return srcString.slice(position - srcStringStart, (position += length) - srcStringStart); + } + if (srcStringEnd == 0 && srcEnd < 140) { + // for small blocks, avoiding the overhead of the extract call is helpful + const string = length < 16 ? shortStringInJS(length) : longStringInJS(length); + if (string != null) return string; + } + return readFixedString(length); + } else { + let value; + switch (token) { + case 0xc0: + return null; + case 0xc1: + if (bundledStrings) { + value = read(); // followed by the length of the string in characters (not bytes!) + if (value > 0) return bundledStrings[1].slice(bundledStrings.position1, (bundledStrings.position1 += value)); + else return bundledStrings[0].slice(bundledStrings.position0, (bundledStrings.position0 -= value)); + } + return C1; // "never-used", return special object to denote that + case 0xc2: + return false; + case 0xc3: + return true; + case 0xc4: + // bin 8 + value = src[position++]; + if (value === undefined) throw new Error('Unexpected end of buffer'); + return readBin(value); + case 0xc5: + // bin 16 + value = dataView.getUint16(position); + position += 2; + return readBin(value); + case 0xc6: + // bin 32 + value = dataView.getUint32(position); + position += 4; + return readBin(value); + case 0xc7: + // ext 8 + return readExt(src[position++]); + case 0xc8: + // ext 16 + value = dataView.getUint16(position); + position += 2; + return readExt(value); + case 0xc9: + // ext 32 + value = dataView.getUint32(position); + position += 4; + return readExt(value); + case 0xca: + value = dataView.getFloat32(position); + if (currentUnpackr.useFloat32 > 2) { + // this does rounding of numbers that were encoded in 32-bit float to nearest significant decimal digit that could be preserved + const multiplier = mult10[((src[position] & 0x7f) << 1) | (src[position + 1] >> 7)]; + position += 4; + return ((multiplier * value + (value > 0 ? 0.5 : -0.5)) >> 0) / multiplier; + } + position += 4; + return value; + case 0xcb: + value = dataView.getFloat64(position); + position += 8; + return value; + // uint handlers + case 0xcc: + return src[position++]; + case 0xcd: + value = dataView.getUint16(position); + position += 2; + return value; + case 0xce: + value = dataView.getUint32(position); + position += 4; + return value; + case 0xcf: + if (currentUnpackr.int64AsType === 'number') { + value = dataView.getUint32(position) * 0x100000000; + value += dataView.getUint32(position + 4); + } else if (currentUnpackr.int64AsType === 'string') { + value = dataView.getBigUint64(position).toString(); + } else if (currentUnpackr.int64AsType === 'auto') { + value = dataView.getBigUint64(position); + if (value <= BigInt(2) << BigInt(52)) value = Number(value); + } else value = dataView.getBigUint64(position); + position += 8; + return value; + + // int handlers + case 0xd0: + return dataView.getInt8(position++); + case 0xd1: + value = dataView.getInt16(position); + position += 2; + return value; + case 0xd2: + value = dataView.getInt32(position); + position += 4; + return value; + case 0xd3: + if (currentUnpackr.int64AsType === 'number') { + value = dataView.getInt32(position) * 0x100000000; + value += dataView.getUint32(position + 4); + } else if (currentUnpackr.int64AsType === 'string') { + value = dataView.getBigInt64(position).toString(); + } else if (currentUnpackr.int64AsType === 'auto') { + value = dataView.getBigInt64(position); + if (value >= BigInt(-2) << BigInt(52) && value <= BigInt(2) << BigInt(52)) value = Number(value); + } else value = dataView.getBigInt64(position); + position += 8; + return value; + + case 0xd4: + // fixext 1 + value = src[position++]; + if (value == 0x72) { + return recordDefinition(src[position++] & 0x3f); + } else { + const extension = currentExtensions[value]; + if (extension) { + if (extension.read) { + position++; // skip filler byte + return extension.read(read()); + } else if (extension.noBuffer) { + position++; // skip filler byte + return extension(); + } else return extension(src.subarray(position, ++position)); + } else throw new Error('Unknown extension ' + value); + } + case 0xd5: + // fixext 2 + value = src[position]; + if (value == 0x72) { + position++; + return recordDefinition(src[position++] & 0x3f, src[position++]); + } else return readExt(2); + case 0xd6: + // fixext 4 + return readExt(4); + case 0xd7: + // fixext 8 + return readExt(8); + case 0xd8: + // fixext 16 + return readExt(16); + case 0xd9: + // str 8 + value = src[position++]; + if (srcStringEnd >= position) { + return srcString.slice(position - srcStringStart, (position += value) - srcStringStart); + } + return readString8(value); + case 0xda: + // str 16 + value = dataView.getUint16(position); + position += 2; + if (srcStringEnd >= position) { + return srcString.slice(position - srcStringStart, (position += value) - srcStringStart); + } + return readString16(value); + case 0xdb: + // str 32 + value = dataView.getUint32(position); + position += 4; + if (srcStringEnd >= position) { + return srcString.slice(position - srcStringStart, (position += value) - srcStringStart); + } + return readString32(value); + case 0xdc: + // array 16 + value = dataView.getUint16(position); + position += 2; + return readArray(value); + case 0xdd: + // array 32 + value = dataView.getUint32(position); + position += 4; + return readArray(value); + case 0xde: + // map 16 + value = dataView.getUint16(position); + position += 2; + return readMap(value); + case 0xdf: + // map 32 + value = dataView.getUint32(position); + position += 4; + return readMap(value); + default: // negative int + if (token >= 0xe0) return token - 0x100; + if (token === undefined) { + const error = new Error('Unexpected end of MessagePack data'); + error.incomplete = true; + throw error; + } + throw new Error('Unknown MessagePack token ' + token); + } + } +} +const validName = /^[a-zA-Z_$][a-zA-Z\d_$]*$/; +function createStructureReader(structure, firstId) { + function readObject() { + // This initial function is quick to instantiate, but runs slower. After several iterations pay the cost to build the faster function + if (readObject.count++ > inlineObjectReadThreshold) { + const readObject = (structure.read = new Function( + 'r', + 'return function(){return ' + + (currentUnpackr.freezeData ? 'Object.freeze' : '') + + '({' + + structure + .map((key) => + key === '__proto__' + ? '__proto_:r()' + : validName.test(key) + ? key + ':r()' + : '[' + JSON.stringify(key) + ']:r()' + ) + .join(',') + + '})}' + )(read)); + if (structure.highByte === 0) structure.read = createSecondByteReader(firstId, structure.read); + return readObject(); // second byte is already read, if there is one so immediately read object + } + const object = {}; + for (let i = 0, l = structure.length; i < l; i++) { + let key = structure[i]; + if (key === '__proto__') key = '__proto_'; + object[key] = read(); + } + if (currentUnpackr.freezeData) return Object.freeze(object); + return object; + } + readObject.count = 0; + if (structure.highByte === 0) { + return createSecondByteReader(firstId, readObject); + } + return readObject; +} + +const createSecondByteReader = (firstId, read0) => + function () { + const highByte = src[position++]; + if (highByte === 0) return read0(); + const id = firstId < 32 ? -(firstId + (highByte << 5)) : firstId + (highByte << 5); + const structure = currentStructures[id] || loadStructures()[id]; + if (!structure) { + throw new Error('Record id is not defined for ' + id); + } + if (!structure.read) structure.read = createStructureReader(structure, firstId); + return structure.read(); + }; + +export function loadStructures() { + const loadedStructures = saveState(() => { + // save the state in case getStructures modifies our buffer + src = null; + return currentUnpackr.getStructures(); + }); + return (currentStructures = currentUnpackr._mergeStructures(loadedStructures, currentStructures)); +} + +var readFixedString = readStringJS; +var readString8 = readStringJS; +var readString16 = readStringJS; +var readString32 = readStringJS; +export let isNativeAccelerationEnabled = false; + +export function setExtractor(extractStrings) { + isNativeAccelerationEnabled = true; + readFixedString = readString(1); + readString8 = readString(2); + readString16 = readString(3); + readString32 = readString(5); + function readString(headerLength) { + return function readString(length) { + let string = strings[stringPosition++]; + if (string == null) { + if (bundledStrings) return readStringJS(length); + const byteOffset = src.byteOffset; + const extraction = extractStrings(position - headerLength + byteOffset, srcEnd + byteOffset, src.buffer); + if (typeof extraction == 'string') { + string = extraction; + strings = EMPTY_ARRAY; + } else { + strings = extraction; + stringPosition = 1; + srcStringEnd = 1; // even if a utf-8 string was decoded, must indicate we are in the midst of extracted strings and can't skip strings + string = strings[0]; + if (string === undefined) throw new Error('Unexpected end of buffer'); + } + } + const srcStringLength = string.length; + if (srcStringLength <= length) { + position += length; + return string; + } + srcString = string; + srcStringStart = position; + srcStringEnd = position + srcStringLength; + position += length; + return string.slice(0, length); // we know we just want the beginning + }; + } +} +function readStringJS(length) { + let result; + if (length < 16) { + if ((result = shortStringInJS(length))) return result; + } + if (length > 64 && decoder) return decoder.decode(src.subarray(position, (position += length))); + const end = position + length; + const units = []; + result = ''; + while (position < end) { + const byte1 = src[position++]; + if ((byte1 & 0x80) === 0) { + // 1 byte + units.push(byte1); + } else if ((byte1 & 0xe0) === 0xc0) { + // 2 bytes + const byte2 = src[position++] & 0x3f; + units.push(((byte1 & 0x1f) << 6) | byte2); + } else if ((byte1 & 0xf0) === 0xe0) { + // 3 bytes + const byte2 = src[position++] & 0x3f; + const byte3 = src[position++] & 0x3f; + units.push(((byte1 & 0x1f) << 12) | (byte2 << 6) | byte3); + } else if ((byte1 & 0xf8) === 0xf0) { + // 4 bytes + const byte2 = src[position++] & 0x3f; + const byte3 = src[position++] & 0x3f; + const byte4 = src[position++] & 0x3f; + let unit = ((byte1 & 0x07) << 0x12) | (byte2 << 0x0c) | (byte3 << 0x06) | byte4; + if (unit > 0xffff) { + unit -= 0x10000; + units.push(((unit >>> 10) & 0x3ff) | 0xd800); + unit = 0xdc00 | (unit & 0x3ff); + } + units.push(unit); + } else { + units.push(byte1); + } + + if (units.length >= 0x1000) { + result += fromCharCode.apply(String, units); + units.length = 0; + } + } + + if (units.length > 0) { + result += fromCharCode.apply(String, units); + } + + return result; +} +export function readString(source, start, length) { + const existingSrc = src; + src = source; + position = start; + try { + return readStringJS(length); + } finally { + src = existingSrc; + } +} + +function readArray(length) { + const array = new Array(length); + for (let i = 0; i < length; i++) { + array[i] = read(); + } + if (currentUnpackr.freezeData) return Object.freeze(array); + return array; +} + +function readMap(length) { + if (currentUnpackr.mapsAsObjects) { + const object = {}; + for (let i = 0; i < length; i++) { + let key = readKey(); + if (key === '__proto__') key = '__proto_'; + object[key] = read(); + } + return object; + } else { + const map = new Map(); + for (let i = 0; i < length; i++) { + map.set(read(), read()); + } + return map; + } +} + +var fromCharCode = String.fromCharCode; +function longStringInJS(length) { + const start = position; + const bytes = new Array(length); + for (let i = 0; i < length; i++) { + const byte = src[position++]; + if ((byte & 0x80) > 0) { + position = start; + return; + } + bytes[i] = byte; + } + return fromCharCode.apply(String, bytes); +} +function shortStringInJS(length) { + if (length < 4) { + if (length < 2) { + if (length === 0) return ''; + else { + const a = src[position++]; + if ((a & 0x80) > 1) { + position -= 1; + return; + } + return fromCharCode(a); + } + } else { + const a = src[position++]; + const b = src[position++]; + if ((a & 0x80) > 0 || (b & 0x80) > 0) { + position -= 2; + return; + } + if (length < 3) return fromCharCode(a, b); + const c = src[position++]; + if ((c & 0x80) > 0) { + position -= 3; + return; + } + return fromCharCode(a, b, c); + } + } else { + const a = src[position++]; + const b = src[position++]; + const c = src[position++]; + const d = src[position++]; + if ((a & 0x80) > 0 || (b & 0x80) > 0 || (c & 0x80) > 0 || (d & 0x80) > 0) { + position -= 4; + return; + } + if (length < 6) { + if (length === 4) return fromCharCode(a, b, c, d); + else { + const e = src[position++]; + if ((e & 0x80) > 0) { + position -= 5; + return; + } + return fromCharCode(a, b, c, d, e); + } + } else if (length < 8) { + const e = src[position++]; + const f = src[position++]; + if ((e & 0x80) > 0 || (f & 0x80) > 0) { + position -= 6; + return; + } + if (length < 7) return fromCharCode(a, b, c, d, e, f); + const g = src[position++]; + if ((g & 0x80) > 0) { + position -= 7; + return; + } + return fromCharCode(a, b, c, d, e, f, g); + } else { + const e = src[position++]; + const f = src[position++]; + const g = src[position++]; + const h = src[position++]; + if ((e & 0x80) > 0 || (f & 0x80) > 0 || (g & 0x80) > 0 || (h & 0x80) > 0) { + position -= 8; + return; + } + if (length < 10) { + if (length === 8) return fromCharCode(a, b, c, d, e, f, g, h); + else { + const i = src[position++]; + if ((i & 0x80) > 0) { + position -= 9; + return; + } + return fromCharCode(a, b, c, d, e, f, g, h, i); + } + } else if (length < 12) { + const i = src[position++]; + const j = src[position++]; + if ((i & 0x80) > 0 || (j & 0x80) > 0) { + position -= 10; + return; + } + if (length < 11) return fromCharCode(a, b, c, d, e, f, g, h, i, j); + const k = src[position++]; + if ((k & 0x80) > 0) { + position -= 11; + return; + } + return fromCharCode(a, b, c, d, e, f, g, h, i, j, k); + } else { + const i = src[position++]; + const j = src[position++]; + const k = src[position++]; + const l = src[position++]; + if ((i & 0x80) > 0 || (j & 0x80) > 0 || (k & 0x80) > 0 || (l & 0x80) > 0) { + position -= 12; + return; + } + if (length < 14) { + if (length === 12) return fromCharCode(a, b, c, d, e, f, g, h, i, j, k, l); + else { + const m = src[position++]; + if ((m & 0x80) > 0) { + position -= 13; + return; + } + return fromCharCode(a, b, c, d, e, f, g, h, i, j, k, l, m); + } + } else { + const m = src[position++]; + const n = src[position++]; + if ((m & 0x80) > 0 || (n & 0x80) > 0) { + position -= 14; + return; + } + if (length < 15) return fromCharCode(a, b, c, d, e, f, g, h, i, j, k, l, m, n); + const o = src[position++]; + if ((o & 0x80) > 0) { + position -= 15; + return; + } + return fromCharCode(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o); + } + } + } + } +} + +function readOnlyJSString() { + const token = src[position++]; + let length; + if (token < 0xc0) { + // fixstr + length = token - 0xa0; + } else { + switch (token) { + case 0xd9: + // str 8 + length = src[position++]; + break; + case 0xda: + // str 16 + length = dataView.getUint16(position); + position += 2; + break; + case 0xdb: + // str 32 + length = dataView.getUint32(position); + position += 4; + break; + default: + throw new Error('Expected string'); + } + } + return readStringJS(length); +} + +function readBin(length) { + return currentUnpackr.copyBuffers + ? // specifically use the copying slice (not the node one) + Uint8Array.prototype.slice.call(src, position, (position += length)) + : src.subarray(position, (position += length)); +} +function readExt(length) { + const type = src[position++]; + if (currentExtensions[type]) { + let end; + return currentExtensions[type](src.subarray(position, (end = position += length)), (readPosition) => { + position = readPosition; + try { + return read(); + } finally { + position = end; + } + }); + } else throw new Error('Unknown extension type ' + type); +} + +const keyCache = new Array(4096); +function readKey() { + let length = src[position++]; + if (length >= 0xa0 && length < 0xc0) { + // fixstr, potentially use key cache + length = length - 0xa0; + if (srcStringEnd >= position) + // if it has been extracted, must use it (and faster anyway) + return srcString.slice(position - srcStringStart, (position += length) - srcStringStart); + else if (!(srcStringEnd == 0 && srcEnd < 180)) return readFixedString(length); + } else { + // not cacheable, go back and do a standard read + position--; + return read().toString(); + } + const key = ((length << 5) ^ (length > 1 ? dataView.getUint16(position) : length > 0 ? src[position] : 0)) & 0xfff; + let entry = keyCache[key]; + let checkPosition = position; + let end = position + length - 3; + let chunk; + let i = 0; + if (entry && entry.bytes == length) { + while (checkPosition < end) { + chunk = dataView.getUint32(checkPosition); + if (chunk != entry[i++]) { + checkPosition = 0x70000000; + break; + } + checkPosition += 4; + } + end += 3; + while (checkPosition < end) { + chunk = src[checkPosition++]; + if (chunk != entry[i++]) { + checkPosition = 0x70000000; + break; + } + } + if (checkPosition === end) { + position = checkPosition; + return entry.string; + } + end -= 3; + checkPosition = position; + } + entry = []; + keyCache[key] = entry; + entry.bytes = length; + while (checkPosition < end) { + chunk = dataView.getUint32(checkPosition); + entry.push(chunk); + checkPosition += 4; + } + end += 3; + while (checkPosition < end) { + chunk = src[checkPosition++]; + entry.push(chunk); + } + // for small blocks, avoiding the overhead of the extract call is helpful + const string = length < 16 ? shortStringInJS(length) : longStringInJS(length); + if (string != null) return (entry.string = string); + return (entry.string = readFixedString(length)); +} + +// the registration of the record definition extension (as "r") +const recordDefinition = (id, highByte) => { + const structure = read().map((property) => property.toString()); // ensure that all keys are strings and that the array is mutable + const firstByte = id; + if (highByte !== undefined) { + id = id < 32 ? -((highByte << 5) + id) : (highByte << 5) + id; + structure.highByte = highByte; + } + const existingStructure = currentStructures[id]; + if (existingStructure && existingStructure.isShared) { + (currentStructures.restoreStructures || (currentStructures.restoreStructures = []))[id] = existingStructure; + } + currentStructures[id] = structure; + structure.read = createStructureReader(structure, firstByte); + return structure.read(); +}; +currentExtensions[0] = () => {}; // notepack defines extension 0 to mean undefined, so use that as the default here +currentExtensions[0].noBuffer = true; + +const glbl = typeof globalThis === 'object' ? globalThis : window; +currentExtensions[0x65] = () => { + const data = read(); + return (glbl[data[0]] || Error)(data[1]); +}; + +currentExtensions[0x69] = (data) => { + // id extension (for structured clones) + const id = dataView.getUint32(position - 4); + if (!referenceMap) referenceMap = new Map(); + const token = src[position]; + let target; + // TODO: handle Maps, Sets, and other types that can cycle; this is complicated, because you potentially need to read + // ahead past references to record structure definitions + if ((token >= 0x90 && token < 0xa0) || token == 0xdc || token == 0xdd) target = []; + else target = {}; + + const refEntry = { target }; // a placeholder object + referenceMap.set(id, refEntry); + const targetProperties = read(); // read the next value as the target object to id + if (refEntry.used) + // there is a cycle, so we have to assign properties to original target + return Object.assign(target, targetProperties); + refEntry.target = targetProperties; // the placeholder wasn't used, replace with the deserialized one + return targetProperties; // no cycle, can just use the returned read object +}; + +currentExtensions[0x70] = (data) => { + // pointer extension (for structured clones) + const id = dataView.getUint32(position - 4); + const refEntry = referenceMap.get(id); + refEntry.used = true; + return refEntry.target; +}; + +currentExtensions[0x73] = () => new Set(read()); + +export const typedArrays = [ + 'Int8', + 'Uint8', + 'Uint8Clamped', + 'Int16', + 'Uint16', + 'Int32', + 'Uint32', + 'Float32', + 'Float64', + 'BigInt64', + 'BigUint64' +].map((type) => type + 'Array'); + +currentExtensions[0x74] = (data) => { + const typeCode = data[0]; + const typedArrayName = typedArrays[typeCode]; + if (!typedArrayName) throw new Error('Could not find typed array for code ' + typeCode); + // we have to always slice/copy here to get a new ArrayBuffer that is word/byte aligned + return new glbl[typedArrayName](Uint8Array.prototype.slice.call(data, 1).buffer); +}; +currentExtensions[0x78] = () => { + const data = read(); + return new RegExp(data[0], data[1]); +}; +const TEMP_BUNDLE = []; +currentExtensions[0x62] = (data) => { + const dataSize = (data[0] << 24) + (data[1] << 16) + (data[2] << 8) + data[3]; + const dataPosition = position; + position += dataSize - data.length; + bundledStrings = TEMP_BUNDLE; + bundledStrings = [readOnlyJSString(), readOnlyJSString()]; + bundledStrings.position0 = 0; + bundledStrings.position1 = 0; + bundledStrings.postBundlePosition = position; + position = dataPosition; + return read(); +}; + +currentExtensions[0xff] = (data) => { + // 32-bit date extension + if (data.length == 4) return new Date((data[0] * 0x1000000 + (data[1] << 16) + (data[2] << 8) + data[3]) * 1000); + else if (data.length == 8) + return new Date( + ((data[0] << 22) + (data[1] << 14) + (data[2] << 6) + (data[3] >> 2)) / 1000000 + + ((data[3] & 0x3) * 0x100000000 + data[4] * 0x1000000 + (data[5] << 16) + (data[6] << 8) + data[7]) * 1000 + ); + else if (data.length == 12) + // TODO: Implement support for negative + return new Date( + ((data[0] << 24) + (data[1] << 16) + (data[2] << 8) + data[3]) / 1000000 + + ((data[4] & 0x80 ? -0x1000000000000 : 0) + + data[6] * 0x10000000000 + + data[7] * 0x100000000 + + data[8] * 0x1000000 + + (data[9] << 16) + + (data[10] << 8) + + data[11]) * + 1000 + ); + else return new Date('invalid'); +}; // notepack defines extension 0 to mean undefined, so use that as the default here +// registration of bulk record definition? +// currentExtensions[0x52] = () => + +function saveState(callback) { + if (onSaveState) onSaveState(); + const savedSrcEnd = srcEnd; + const savedPosition = position; + const savedStringPosition = stringPosition; + const savedSrcStringStart = srcStringStart; + const savedSrcStringEnd = srcStringEnd; + const savedSrcString = srcString; + const savedStrings = strings; + const savedReferenceMap = referenceMap; + const savedBundledStrings = bundledStrings; + + // TODO: We may need to revisit this if we do more external calls to user code (since it could be slow) + const savedSrc = new Uint8Array(src.slice(0, srcEnd)); // we copy the data in case it changes while external data is processed + const savedStructures = currentStructures; + const savedStructuresContents = currentStructures.slice(0, currentStructures.length); + const savedPackr = currentUnpackr; + const savedSequentialMode = sequentialMode; + const value = callback(); + srcEnd = savedSrcEnd; + position = savedPosition; + stringPosition = savedStringPosition; + srcStringStart = savedSrcStringStart; + srcStringEnd = savedSrcStringEnd; + srcString = savedSrcString; + strings = savedStrings; + referenceMap = savedReferenceMap; + bundledStrings = savedBundledStrings; + src = savedSrc; + sequentialMode = savedSequentialMode; + currentStructures = savedStructures; + currentStructures.splice(0, currentStructures.length, ...savedStructuresContents); + currentUnpackr = savedPackr; + dataView = new DataView(src.buffer, src.byteOffset, src.byteLength); + return value; +} +export function clearSource() { + src = null; + referenceMap = null; + currentStructures = null; +} + +export function addExtension(extension) { + if (extension.unpack) currentExtensions[extension.type] = extension.unpack; + else currentExtensions[extension.type] = extension; +} + +export const mult10 = new Array(147); // this is a table matching binary exponents to the multiplier to determine significant digit rounding +for (let i = 0; i < 256; i++) { + mult10[i] = +('1e' + Math.floor(45.15 - i * 0.30103)); +} +export const Decoder = Unpackr; +var defaultUnpackr = new Unpackr({ useRecords: false }); +export const unpack = defaultUnpackr.unpack; +export const unpackMultiple = defaultUnpackr.unpackMultiple; +export const decode = defaultUnpackr.unpack; +export const FLOAT32_OPTIONS = { + NEVER: 0, + ALWAYS: 1, + DECIMAL_ROUND: 3, + DECIMAL_FIT: 4 +}; +const f32Array = new Float32Array(1); +const u8Array = new Uint8Array(f32Array.buffer, 0, 4); +export function roundFloat32(float32Number) { + f32Array[0] = float32Number; + const multiplier = mult10[((u8Array[3] & 0x7f) << 1) | (u8Array[2] >> 7)]; + return ((multiplier * float32Number + (float32Number > 0 ? 0.5 : -0.5)) >> 0) / multiplier; +} +export function setReadStruct(updatedReadStruct, loadedStructs, saveState) { + readStruct = updatedReadStruct; + onLoadedStructures = loadedStructs; + onSaveState = saveState; +} diff --git a/interface/src/components/ButtonRow.tsx b/interface/src/components/ButtonRow.tsx index 40eb0182e..adf01472c 100644 --- a/interface/src/components/ButtonRow.tsx +++ b/interface/src/components/ButtonRow.tsx @@ -1,26 +1,25 @@ -import { FC } from 'react'; -import { Box, BoxProps } from '@mui/material'; +import { Box } from '@mui/material'; +import type { BoxProps } from '@mui/material'; +import type { FC } from 'react'; -const ButtonRow: FC = ({ children, ...rest }) => { - return ( - = ({ children, ...rest }) => ( + - {children} - - ); -}; + } + }} + {...rest} + > + {children} + +); export default ButtonRow; diff --git a/interface/src/components/MessageBox.tsx b/interface/src/components/MessageBox.tsx index a530d8f26..e7358e73c 100644 --- a/interface/src/components/MessageBox.tsx +++ b/interface/src/components/MessageBox.tsx @@ -1,11 +1,10 @@ -import { FC } from 'react'; - -import { Box, BoxProps, SvgIconProps, Theme, Typography, useTheme } from '@mui/material'; - import CheckCircleOutlineOutlinedIcon from '@mui/icons-material/CheckCircleOutlineOutlined'; +import ErrorIcon from '@mui/icons-material/Error'; import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined'; import ReportProblemOutlinedIcon from '@mui/icons-material/ReportProblemOutlined'; -import ErrorIcon from '@mui/icons-material/Error'; +import { Box, Typography, useTheme } from '@mui/material'; +import type { BoxProps, SvgIconProps, Theme } from '@mui/material'; +import type { FC } from 'react'; type MessageBoxLevel = 'warning' | 'success' | 'info' | 'error'; diff --git a/interface/src/components/SectionContent.tsx b/interface/src/components/SectionContent.tsx index dfcb37e9b..95428d982 100644 --- a/interface/src/components/SectionContent.tsx +++ b/interface/src/components/SectionContent.tsx @@ -1,8 +1,7 @@ -import { FC } from 'react'; - import { Paper, Divider } from '@mui/material'; +import type { FC } from 'react'; -import { RequiredChildrenProps } from 'utils'; +import type { RequiredChildrenProps } from 'utils'; interface SectionContentProps extends RequiredChildrenProps { title: string; diff --git a/interface/src/components/inputs/BlockFormControlLabel.tsx b/interface/src/components/inputs/BlockFormControlLabel.tsx index e3e8b7cd3..8da7932a5 100644 --- a/interface/src/components/inputs/BlockFormControlLabel.tsx +++ b/interface/src/components/inputs/BlockFormControlLabel.tsx @@ -1,5 +1,6 @@ -import { FC } from 'react'; -import { FormControlLabel, FormControlLabelProps } from '@mui/material'; +import { FormControlLabel } from '@mui/material'; +import type { FormControlLabelProps } from '@mui/material'; +import type { FC } from 'react'; const BlockFormControlLabel: FC = (props) => (
diff --git a/interface/src/components/inputs/ValidatedPasswordField.tsx b/interface/src/components/inputs/ValidatedPasswordField.tsx index d4a215199..43cc0f426 100644 --- a/interface/src/components/inputs/ValidatedPasswordField.tsx +++ b/interface/src/components/inputs/ValidatedPasswordField.tsx @@ -1,10 +1,11 @@ -import { FC, useState } from 'react'; - -import { IconButton, InputAdornment } from '@mui/material'; import VisibilityIcon from '@mui/icons-material/Visibility'; import VisibilityOffIcon from '@mui/icons-material/VisibilityOff'; +import { IconButton, InputAdornment } from '@mui/material'; +import { useState } from 'react'; -import ValidatedTextField, { ValidatedTextFieldProps } from './ValidatedTextField'; +import ValidatedTextField from './ValidatedTextField'; +import type { ValidatedTextFieldProps } from './ValidatedTextField'; +import type { FC } from 'react'; type ValidatedPasswordFieldProps = Omit; diff --git a/interface/src/components/inputs/ValidatedTextField.tsx b/interface/src/components/inputs/ValidatedTextField.tsx index 8d07f8127..11f8d9268 100644 --- a/interface/src/components/inputs/ValidatedTextField.tsx +++ b/interface/src/components/inputs/ValidatedTextField.tsx @@ -1,7 +1,7 @@ -import { FC } from 'react'; -import { ValidateFieldsError } from 'async-validator'; - -import { FormHelperText, TextField, TextFieldProps } from '@mui/material'; +import { FormHelperText, TextField } from '@mui/material'; +import type { TextFieldProps } from '@mui/material'; +import type { ValidateFieldsError } from 'async-validator'; +import type { FC } from 'react'; interface ValidatedFieldProps { fieldErrors?: ValidateFieldsError; diff --git a/interface/src/components/layout/Layout.tsx b/interface/src/components/layout/Layout.tsx index e937b12d3..c9dbf0cdd 100644 --- a/interface/src/components/layout/Layout.tsx +++ b/interface/src/components/layout/Layout.tsx @@ -1,16 +1,15 @@ -import { FC, useState, useEffect } from 'react'; -import { useLocation } from 'react-router-dom'; - import { Box, Toolbar } from '@mui/material'; - -import { PROJECT_NAME } from 'api/env'; -import { RequiredChildrenProps } from 'utils'; - -import LayoutDrawer from './LayoutDrawer'; +import { useState, useEffect } from 'react'; +import { useLocation } from 'react-router-dom'; import LayoutAppBar from './LayoutAppBar'; +import LayoutDrawer from './LayoutDrawer'; import { LayoutContext } from './context'; +import type { FC } from 'react'; -export const DRAWER_WIDTH = 240; +import type { RequiredChildrenProps } from 'utils'; +import { PROJECT_NAME } from 'api/env'; + +export const DRAWER_WIDTH = 210; const Layout: FC = ({ children }) => { const [mobileOpen, setMobileOpen] = useState(false); diff --git a/interface/src/components/layout/LayoutAppBar.tsx b/interface/src/components/layout/LayoutAppBar.tsx index 225283806..500d7519e 100644 --- a/interface/src/components/layout/LayoutAppBar.tsx +++ b/interface/src/components/layout/LayoutAppBar.tsx @@ -1,46 +1,42 @@ -import { FC } from 'react'; - -import { AppBar, Box, IconButton, Toolbar, Typography } from '@mui/material'; import MenuIcon from '@mui/icons-material/Menu'; - +import { AppBar, Box, IconButton, Toolbar, Typography } from '@mui/material'; import LayoutAuthMenu from './LayoutAuthMenu'; +import type { FC } from 'react'; -export const DRAWER_WIDTH = 240; +export const DRAWER_WIDTH = 210; interface LayoutAppBarProps { title: string; onToggleDrawer: () => void; } -const LayoutAppBar: FC = ({ title, onToggleDrawer }) => { - return ( - - - - - - - {title} - - - - - - ); -}; +const LayoutAppBar: FC = ({ title, onToggleDrawer }) => ( + + + + + + + {title} + + + + + +); export default LayoutAppBar; diff --git a/interface/src/components/layout/LayoutAuthMenu.tsx b/interface/src/components/layout/LayoutAuthMenu.tsx index 65204307d..c8d96344d 100644 --- a/interface/src/components/layout/LayoutAuthMenu.tsx +++ b/interface/src/components/layout/LayoutAuthMenu.tsx @@ -1,5 +1,5 @@ -import { FC, useState, useContext, ChangeEventHandler } from 'react'; - +import AccountCircleIcon from '@mui/icons-material/AccountCircle'; +import PersonIcon from '@mui/icons-material/Person'; import { Box, Button, @@ -9,28 +9,26 @@ import { Typography, Avatar, styled, - TypographyProps, MenuItem, TextField } from '@mui/material'; +import { useState, useContext } from 'react'; +import type { TypographyProps } from '@mui/material'; -import PersonIcon from '@mui/icons-material/Person'; -import AccountCircleIcon from '@mui/icons-material/AccountCircle'; - +import type { Locales } from 'i18n/i18n-types'; +import type { FC, ChangeEventHandler } from 'react'; import { AuthenticatedContext } from 'contexts/authentication'; -import { I18nContext } from 'i18n/i18n-react'; -import type { Locales } from 'i18n/i18n-types'; -import { loadLocaleAsync } from 'i18n/i18n-util.async'; - -import { ReactComponent as NLflag } from 'i18n/NL.svg'; import { ReactComponent as DEflag } from 'i18n/DE.svg'; -import { ReactComponent as GBflag } from 'i18n/GB.svg'; -import { ReactComponent as SVflag } from 'i18n/SV.svg'; -import { ReactComponent as PLflag } from 'i18n/PL.svg'; -import { ReactComponent as NOflag } from 'i18n/NO.svg'; import { ReactComponent as FRflag } from 'i18n/FR.svg'; +import { ReactComponent as GBflag } from 'i18n/GB.svg'; +import { ReactComponent as NLflag } from 'i18n/NL.svg'; +import { ReactComponent as NOflag } from 'i18n/NO.svg'; +import { ReactComponent as PLflag } from 'i18n/PL.svg'; +import { ReactComponent as SVflag } from 'i18n/SV.svg'; import { ReactComponent as TRflag } from 'i18n/TR.svg'; +import { I18nContext } from 'i18n/i18n-react'; +import { loadLocaleAsync } from 'i18n/i18n-util.async'; const ItemTypography = styled(Typography)({ maxWidth: '250px', diff --git a/interface/src/components/layout/LayoutDrawer.tsx b/interface/src/components/layout/LayoutDrawer.tsx index c4d4868ee..b63955c54 100644 --- a/interface/src/components/layout/LayoutDrawer.tsx +++ b/interface/src/components/layout/LayoutDrawer.tsx @@ -1,19 +1,17 @@ -import { FC } from 'react'; - import { Box, Divider, Drawer, Toolbar, Typography, styled } from '@mui/material'; +import { DRAWER_WIDTH } from './Layout'; +import LayoutMenu from './LayoutMenu'; +import type { FC } from 'react'; import { PROJECT_NAME } from 'api/env'; -import LayoutMenu from './LayoutMenu'; -import { DRAWER_WIDTH } from './Layout'; - const LayoutDrawerLogo = styled('img')(({ theme }) => ({ [theme.breakpoints.down('sm')]: { height: 24, marginRight: theme.spacing(2) }, [theme.breakpoints.up('sm')]: { - height: 36, + height: 38, marginRight: theme.spacing(2) } })); @@ -29,9 +27,7 @@ const LayoutDrawer: FC = ({ mobileOpen, onClose }) => { - - {PROJECT_NAME} - + {PROJECT_NAME} diff --git a/interface/src/components/layout/LayoutMenu.tsx b/interface/src/components/layout/LayoutMenu.tsx index 0f0004591..f7d32788f 100644 --- a/interface/src/components/layout/LayoutMenu.tsx +++ b/interface/src/components/layout/LayoutMenu.tsx @@ -1,17 +1,16 @@ -import { FC, useContext } from 'react'; - -import { Divider, List } from '@mui/material'; - -import SettingsInputAntennaIcon from '@mui/icons-material/SettingsInputAntenna'; import AccessTimeIcon from '@mui/icons-material/AccessTime'; -import DeviceHubIcon from '@mui/icons-material/DeviceHub'; -import SettingsIcon from '@mui/icons-material/Settings'; -import LockIcon from '@mui/icons-material/Lock'; -import SettingsEthernetIcon from '@mui/icons-material/SettingsEthernet'; -import TuneIcon from '@mui/icons-material/Tune'; import DashboardIcon from '@mui/icons-material/Dashboard'; +import DeviceHubIcon from '@mui/icons-material/DeviceHub'; import InfoIcon from '@mui/icons-material/Info'; +import LockIcon from '@mui/icons-material/Lock'; +import SettingsIcon from '@mui/icons-material/Settings'; +import SettingsEthernetIcon from '@mui/icons-material/SettingsEthernet'; +import SettingsInputAntennaIcon from '@mui/icons-material/SettingsInputAntenna'; +import TuneIcon from '@mui/icons-material/Tune'; +import { Divider, List } from '@mui/material'; +import { useContext } from 'react'; +import type { FC } from 'react'; import LayoutMenuItem from 'components/layout/LayoutMenuItem'; diff --git a/interface/src/components/layout/LayoutMenuItem.tsx b/interface/src/components/layout/LayoutMenuItem.tsx index 04da15106..91914169d 100644 --- a/interface/src/components/layout/LayoutMenuItem.tsx +++ b/interface/src/components/layout/LayoutMenuItem.tsx @@ -1,12 +1,11 @@ -import { FC } from 'react'; +import { ListItem, ListItemButton, ListItemIcon, ListItemText } from '@mui/material'; +import { grey } from '@mui/material/colors'; import { Link, useLocation } from 'react-router-dom'; - -import { ListItem, ListItemButton, ListItemIcon, ListItemText, SvgIconProps } from '@mui/material'; +import type { SvgIconProps } from '@mui/material'; +import type { FC } from 'react'; import { routeMatches } from 'utils'; -import { grey } from '@mui/material/colors'; - interface LayoutMenuItemProps { icon: React.ComponentType; label: string; diff --git a/interface/src/components/loading/ApplicationError.tsx b/interface/src/components/loading/ApplicationError.tsx index 9ab428d58..4a1be5c31 100644 --- a/interface/src/components/loading/ApplicationError.tsx +++ b/interface/src/components/loading/ApplicationError.tsx @@ -1,7 +1,6 @@ -import { FC } from 'react'; - -import { Box, Paper, Typography } from '@mui/material'; import WarningIcon from '@mui/icons-material/Warning'; +import { Box, Paper, Typography } from '@mui/material'; +import type { FC } from 'react'; interface ApplicationErrorProps { message?: string; diff --git a/interface/src/components/loading/FormLoader.tsx b/interface/src/components/loading/FormLoader.tsx index b2d23a7a1..5a555f4d0 100644 --- a/interface/src/components/loading/FormLoader.tsx +++ b/interface/src/components/loading/FormLoader.tsx @@ -1,7 +1,6 @@ -import { FC } from 'react'; - -import { Box, Button, CircularProgress, Typography } from '@mui/material'; import RefreshIcon from '@mui/icons-material/Refresh'; +import { Box, Button, CircularProgress, Typography } from '@mui/material'; +import type { FC } from 'react'; import { MessageBox } from 'components'; diff --git a/interface/src/components/loading/LoadingSpinner.tsx b/interface/src/components/loading/LoadingSpinner.tsx index 243e6a8cb..06d6664ba 100644 --- a/interface/src/components/loading/LoadingSpinner.tsx +++ b/interface/src/components/loading/LoadingSpinner.tsx @@ -1,6 +1,6 @@ -import { FC } from 'react'; - -import { CircularProgress, Box, Typography, Theme } from '@mui/material'; +import { CircularProgress, Box, Typography } from '@mui/material'; +import type { Theme } from '@mui/material'; +import type { FC } from 'react'; import { useI18nContext } from 'i18n/i18n-react'; diff --git a/interface/src/components/routing/BlockNavigation.tsx b/interface/src/components/routing/BlockNavigation.tsx index 0f6bb4ef0..c8986c14a 100644 --- a/interface/src/components/routing/BlockNavigation.tsx +++ b/interface/src/components/routing/BlockNavigation.tsx @@ -1,6 +1,7 @@ -import { FC } from 'react'; -import type { Blocker } from '@remix-run/router'; import { Button, Dialog, DialogActions, DialogContent, DialogTitle } from '@mui/material'; +import type { FC } from 'react'; + +import type { unstable_Blocker as Blocker } from 'react-router-dom'; import { useI18nContext } from 'i18n/i18n-react'; diff --git a/interface/src/components/routing/RequireAdmin.tsx b/interface/src/components/routing/RequireAdmin.tsx index 1f6be2bf4..a78aeaf01 100644 --- a/interface/src/components/routing/RequireAdmin.tsx +++ b/interface/src/components/routing/RequireAdmin.tsx @@ -1,8 +1,9 @@ -import { FC, useContext } from 'react'; +import { useContext } from 'react'; import { Navigate } from 'react-router-dom'; +import type { FC } from 'react'; +import type { RequiredChildrenProps } from 'utils'; import { AuthenticatedContext } from 'contexts/authentication'; -import { RequiredChildrenProps } from 'utils'; const RequireAdmin: FC = ({ children }) => { const authenticatedContext = useContext(AuthenticatedContext); diff --git a/interface/src/components/routing/RequireAuthenticated.tsx b/interface/src/components/routing/RequireAuthenticated.tsx index 6eea9ed7c..d9f157f56 100644 --- a/interface/src/components/routing/RequireAuthenticated.tsx +++ b/interface/src/components/routing/RequireAuthenticated.tsx @@ -1,14 +1,12 @@ -import { FC, useContext, useEffect } from 'react'; +import { useContext, useEffect } from 'react'; import { Navigate, useLocation } from 'react-router-dom'; -import { - AuthenticatedContext, - AuthenticatedContextValue, - AuthenticationContext -} from 'contexts/authentication/context'; -import { storeLoginRedirect } from 'api/authentication'; +import type { AuthenticatedContextValue } from 'contexts/authentication/context'; +import type { FC } from 'react'; -import { RequiredChildrenProps } from 'utils'; +import type { RequiredChildrenProps } from 'utils'; +import { storeLoginRedirect } from 'api/authentication'; +import { AuthenticatedContext, AuthenticationContext } from 'contexts/authentication/context'; const RequireAuthenticated: FC = ({ children }) => { const authenticationContext = useContext(AuthenticationContext); diff --git a/interface/src/components/routing/RequireUnauthenticated.tsx b/interface/src/components/routing/RequireUnauthenticated.tsx index 33ba900c8..65a334926 100644 --- a/interface/src/components/routing/RequireUnauthenticated.tsx +++ b/interface/src/components/routing/RequireUnauthenticated.tsx @@ -1,9 +1,10 @@ -import { FC, useContext } from 'react'; +import { useContext } from 'react'; import { Navigate } from 'react-router-dom'; +import type { FC } from 'react'; +import type { RequiredChildrenProps } from 'utils'; import * as AuthenticationApi from 'api/authentication'; import { AuthenticationContext } from 'contexts/authentication'; -import { RequiredChildrenProps } from 'utils'; const RequireUnauthenticated: FC = ({ children }) => { const authenticationContext = useContext(AuthenticationContext); diff --git a/interface/src/components/routing/RouterTabs.tsx b/interface/src/components/routing/RouterTabs.tsx index c16b77ba6..485a0d995 100644 --- a/interface/src/components/routing/RouterTabs.tsx +++ b/interface/src/components/routing/RouterTabs.tsx @@ -1,9 +1,8 @@ -import { FC } from 'react'; -import { useNavigate } from 'react-router-dom'; - import { Tabs, useMediaQuery, useTheme } from '@mui/material'; +import { useNavigate } from 'react-router-dom'; +import type { FC } from 'react'; -import { RequiredChildrenProps } from 'utils'; +import type { RequiredChildrenProps } from 'utils'; interface RouterTabsProps extends RequiredChildrenProps { value: string | false; diff --git a/interface/src/components/upload/SingleUpload.tsx b/interface/src/components/upload/SingleUpload.tsx index c7c4ce185..57c17bb8a 100644 --- a/interface/src/components/upload/SingleUpload.tsx +++ b/interface/src/components/upload/SingleUpload.tsx @@ -1,12 +1,12 @@ -import { FC, Fragment } from 'react'; -import { useDropzone, DropzoneState } from 'react-dropzone'; - -import { AxiosProgressEvent } from 'axios'; - -import { Box, Button, LinearProgress, Theme, Typography, useTheme } from '@mui/material'; - -import CloudUploadIcon from '@mui/icons-material/CloudUpload'; import CancelIcon from '@mui/icons-material/Cancel'; +import CloudUploadIcon from '@mui/icons-material/CloudUpload'; +import { Box, Button, LinearProgress, Typography, useTheme } from '@mui/material'; +import { Fragment } from 'react'; +import { useDropzone } from 'react-dropzone'; +import type { Theme } from '@mui/material'; +import type { AxiosProgressEvent } from 'axios'; +import type { FC } from 'react'; +import type { DropzoneState } from 'react-dropzone'; import { useI18nContext } from 'i18n/i18n-react'; diff --git a/interface/src/components/upload/useFileUpload.ts b/interface/src/components/upload/useFileUpload.ts index afa41aaa7..47d286a77 100644 --- a/interface/src/components/upload/useFileUpload.ts +++ b/interface/src/components/upload/useFileUpload.ts @@ -1,11 +1,12 @@ +import axios from 'axios'; import { useCallback, useEffect, useState } from 'react'; -import axios, { AxiosPromise, CancelTokenSource, AxiosProgressEvent } from 'axios'; import { toast } from 'react-toastify'; -import { extractErrorMessage } from 'utils'; -import { FileUploadConfig } from 'api/endpoints'; +import type { FileUploadConfig } from 'api/endpoints'; +import type { AxiosPromise, CancelTokenSource, AxiosProgressEvent } from 'axios'; import { useI18nContext } from 'i18n/i18n-react'; +import { extractErrorMessage } from 'utils'; interface MediaUploadOptions { upload: (file: File, config?: FileUploadConfig) => AxiosPromise; @@ -31,11 +32,12 @@ const useFileUpload = ({ upload }: MediaUploadOptions) => { resetUploadingStates(); }, [uploadCancelToken]); - useEffect(() => { - return () => { + useEffect( + () => () => { uploadCancelToken?.cancel(); - }; - }, [uploadCancelToken]); + }, + [uploadCancelToken] + ); const uploadFile = async (images: File[]) => { try { diff --git a/interface/src/contexts/authentication/Authentication.tsx b/interface/src/contexts/authentication/Authentication.tsx index d997356e4..1ff343b19 100644 --- a/interface/src/contexts/authentication/Authentication.tsx +++ b/interface/src/contexts/authentication/Authentication.tsx @@ -1,15 +1,15 @@ -import { FC, useCallback, useEffect, useState } from 'react'; -import { toast } from 'react-toastify'; +import { useCallback, useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom'; +import { toast } from 'react-toastify'; +import { AuthenticationContext } from './context'; +import type { FC } from 'react'; -import { useI18nContext } from 'i18n/i18n-react'; - +import type { Me } from 'types'; +import type { RequiredChildrenProps } from 'utils'; import * as AuthenticationApi from 'api/authentication'; import { ACCESS_TOKEN } from 'api/endpoints'; -import { RequiredChildrenProps } from 'utils'; import { LoadingSpinner } from 'components'; -import { Me } from 'types'; -import { AuthenticationContext } from './context'; +import { useI18nContext } from 'i18n/i18n-react'; const Authentication: FC = ({ children }) => { const { LL } = useI18nContext(); @@ -57,7 +57,7 @@ const Authentication: FC = ({ children }) => { }, []); useEffect(() => { - refresh(); + void refresh(); }, [refresh]); if (initialized) { diff --git a/interface/src/contexts/authentication/context.ts b/interface/src/contexts/authentication/context.ts index 858fba24d..d81ef1a99 100644 --- a/interface/src/contexts/authentication/context.ts +++ b/interface/src/contexts/authentication/context.ts @@ -1,5 +1,5 @@ import { createContext } from 'react'; -import { Me } from 'types'; +import type { Me } from 'types'; export interface AuthenticationContextValue { refresh: () => Promise; diff --git a/interface/src/contexts/features/FeaturesLoader.tsx b/interface/src/contexts/features/FeaturesLoader.tsx index 0235de844..8169d7b82 100644 --- a/interface/src/contexts/features/FeaturesLoader.tsx +++ b/interface/src/contexts/features/FeaturesLoader.tsx @@ -1,12 +1,13 @@ -import { FC, useCallback, useEffect, useState } from 'react'; - -import * as FeaturesApi from 'api/features'; - -import { extractErrorMessage, RequiredChildrenProps } from 'utils'; -import { Features } from 'types'; -import { ApplicationError, LoadingSpinner } from 'components'; +import { useCallback, useEffect, useState } from 'react'; import { FeaturesContext } from '.'; +import type { FC } from 'react'; + +import type { Features } from 'types'; +import type { RequiredChildrenProps } from 'utils'; +import * as FeaturesApi from 'api/features'; +import { ApplicationError, LoadingSpinner } from 'components'; +import { extractErrorMessage } from 'utils'; const FeaturesLoader: FC = (props) => { const [errorMessage, setErrorMessage] = useState(); @@ -22,7 +23,7 @@ const FeaturesLoader: FC = (props) => { }, []); useEffect(() => { - loadFeatures(); + void loadFeatures(); }, [loadFeatures]); if (features) { diff --git a/interface/src/contexts/features/context.ts b/interface/src/contexts/features/context.ts index 5c64ec113..78a8f1ec2 100644 --- a/interface/src/contexts/features/context.ts +++ b/interface/src/contexts/features/context.ts @@ -1,6 +1,6 @@ import { createContext } from 'react'; -import { Features } from 'types'; +import type { Features } from 'types'; export interface FeaturesContextValue { features: Features; diff --git a/interface/src/framework/ap/APSettingsForm.tsx b/interface/src/framework/ap/APSettingsForm.tsx index 349240d1d..f43c704d9 100644 --- a/interface/src/framework/ap/APSettingsForm.tsx +++ b/interface/src/framework/ap/APSettingsForm.tsx @@ -1,12 +1,13 @@ -import { FC, useState } from 'react'; -import { ValidateFieldsError } from 'async-validator'; -import { range } from 'lodash-es'; - -import { Button, Checkbox, MenuItem } from '@mui/material'; -import WarningIcon from '@mui/icons-material/Warning'; import CancelIcon from '@mui/icons-material/Cancel'; +import WarningIcon from '@mui/icons-material/Warning'; +import { Button, Checkbox, MenuItem } from '@mui/material'; +import { range } from 'lodash-es'; +import { useState } from 'react'; +import type { ValidateFieldsError } from 'async-validator'; +import type { FC } from 'react'; -import { createAPSettingsValidator, validate } from 'validators'; +import type { APSettings } from 'types'; +import * as APApi from 'api/ap'; import { BlockFormControlLabel, ButtonRow, @@ -17,15 +18,14 @@ import { BlockNavigation } from 'components'; -import { APProvisionMode, APSettings } from 'types'; -import { numberValue, updateValueDirty, useRest } from 'utils'; -import * as APApi from 'api/ap'; - import { useI18nContext } from 'i18n/i18n-react'; +import { APProvisionMode } from 'types'; +import { numberValue, updateValueDirty, useRest } from 'utils'; -export const isAPEnabled = ({ provision_mode }: APSettings) => { - return provision_mode === APProvisionMode.AP_MODE_ALWAYS || provision_mode === APProvisionMode.AP_MODE_DISCONNECTED; -}; +import { createAPSettingsValidator, validate } from 'validators'; + +export const isAPEnabled = ({ provision_mode }: APSettings) => + provision_mode === APProvisionMode.AP_MODE_ALWAYS || provision_mode === APProvisionMode.AP_MODE_DISCONNECTED; const APSettingsForm: FC = () => { const { loadData, saving, data, setData, origData, dirtyFlags, setDirtyFlags, blocker, saveData, errorMessage } = @@ -49,7 +49,7 @@ const APSettingsForm: FC = () => { try { setFieldErrors(undefined); await validate(createAPSettingsValidator(data), data); - saveData(); + await saveData(); } catch (errors: any) { setFieldErrors(errors); } @@ -174,7 +174,7 @@ const APSettingsForm: FC = () => { variant="outlined" color="primary" type="submit" - onClick={() => loadData()} + onClick={loadData} > {LL.CANCEL()} diff --git a/interface/src/framework/ap/APStatusForm.tsx b/interface/src/framework/ap/APStatusForm.tsx index d8a056ebb..a4cfb6172 100644 --- a/interface/src/framework/ap/APStatusForm.tsx +++ b/interface/src/framework/ap/APStatusForm.tsx @@ -1,17 +1,18 @@ -import { FC } from 'react'; - -import { Avatar, Button, Divider, List, ListItem, ListItemAvatar, ListItemText, Theme, useTheme } from '@mui/material'; -import SettingsInputAntennaIcon from '@mui/icons-material/SettingsInputAntenna'; -import DeviceHubIcon from '@mui/icons-material/DeviceHub'; import ComputerIcon from '@mui/icons-material/Computer'; +import DeviceHubIcon from '@mui/icons-material/DeviceHub'; import RefreshIcon from '@mui/icons-material/Refresh'; +import SettingsInputAntennaIcon from '@mui/icons-material/SettingsInputAntenna'; +import { Avatar, Button, Divider, List, ListItem, ListItemAvatar, ListItemText, useTheme } from '@mui/material'; +import type { Theme } from '@mui/material'; +import type { FC } from 'react'; +import type { APStatus } from 'types'; import * as APApi from 'api/ap'; -import { APNetworkStatus, APStatus } from 'types'; import { ButtonRow, FormLoader, SectionContent } from 'components'; -import { useRest } from 'utils'; import { useI18nContext } from 'i18n/i18n-react'; +import { APNetworkStatus } from 'types'; +import { useRest } from 'utils'; export const apStatusHighlight = ({ status }: APStatus, theme: Theme) => { switch (status) { diff --git a/interface/src/framework/ap/AccessPoint.tsx b/interface/src/framework/ap/AccessPoint.tsx index 26dc3812d..87fecefb3 100644 --- a/interface/src/framework/ap/AccessPoint.tsx +++ b/interface/src/framework/ap/AccessPoint.tsx @@ -1,12 +1,12 @@ -import { FC, useContext } from 'react'; +import { Tab } from '@mui/material'; +import { useContext } from 'react'; import { Navigate, Routes, Route } from 'react-router-dom'; -import { Tab } from '@mui/material'; - -import { AuthenticatedContext } from 'contexts/authentication'; -import APStatusForm from './APStatusForm'; import APSettingsForm from './APSettingsForm'; +import APStatusForm from './APStatusForm'; +import type { FC } from 'react'; import { RequireAdmin, RouterTabs, useLayoutTitle, useRouterTab } from 'components'; +import { AuthenticatedContext } from 'contexts/authentication'; import { useI18nContext } from 'i18n/i18n-react'; @@ -36,7 +36,6 @@ const AccessPoint: FC = () => { } /> - {/* } /> */} } /> diff --git a/interface/src/framework/mqtt/Mqtt.tsx b/interface/src/framework/mqtt/Mqtt.tsx index 9af749753..7520b0eec 100644 --- a/interface/src/framework/mqtt/Mqtt.tsx +++ b/interface/src/framework/mqtt/Mqtt.tsx @@ -1,14 +1,13 @@ -import { FC, useContext } from 'react'; -import { Navigate, Route, Routes } from 'react-router-dom'; - import { Tab } from '@mui/material'; +import { useContext } from 'react'; +import { Navigate, Route, Routes } from 'react-router-dom'; +import MqttSettingsForm from './MqttSettingsForm'; +import MqttStatusForm from './MqttStatusForm'; +import type { FC } from 'react'; import { RequireAdmin, RouterTabs, useLayoutTitle, useRouterTab } from 'components'; import { AuthenticatedContext } from 'contexts/authentication'; -import MqttStatusForm from './MqttStatusForm'; -import MqttSettingsForm from './MqttSettingsForm'; - import { useI18nContext } from 'i18n/i18n-react'; const Mqtt: FC = () => { diff --git a/interface/src/framework/mqtt/MqttSettingsForm.tsx b/interface/src/framework/mqtt/MqttSettingsForm.tsx index ad559c8a1..045e3ea79 100644 --- a/interface/src/framework/mqtt/MqttSettingsForm.tsx +++ b/interface/src/framework/mqtt/MqttSettingsForm.tsx @@ -1,12 +1,12 @@ -import { FC, useState } from 'react'; -import { ValidateFieldsError } from 'async-validator'; - -import { Button, Checkbox, MenuItem, Grid, Typography, InputAdornment } from '@mui/material'; - -import WarningIcon from '@mui/icons-material/Warning'; import CancelIcon from '@mui/icons-material/Cancel'; +import WarningIcon from '@mui/icons-material/Warning'; +import { Button, Checkbox, MenuItem, Grid, Typography, InputAdornment, TextField } from '@mui/material'; +import { useState } from 'react'; +import type { ValidateFieldsError } from 'async-validator'; +import type { FC } from 'react'; -import { createMqttSettingsValidator, validate } from 'validators'; +import type { MqttSettings } from 'types'; +import * as MqttApi from 'api/mqtt'; import { BlockFormControlLabel, ButtonRow, @@ -16,11 +16,10 @@ import { ValidatedTextField, BlockNavigation } from 'components'; -import { MqttSettings } from 'types'; -import { numberValue, updateValueDirty, useRest } from 'utils'; -import * as MqttApi from 'api/mqtt'; - import { useI18nContext } from 'i18n/i18n-react'; +import { numberValue, updateValueDirty, useRest } from 'utils'; + +import { createMqttSettingsValidator, validate } from 'validators'; const MqttSettingsForm: FC = () => { const { loadData, saving, data, setData, origData, dirtyFlags, setDirtyFlags, blocker, saveData, errorMessage } = @@ -44,7 +43,7 @@ const MqttSettingsForm: FC = () => { try { setFieldErrors(undefined); await validate(createMqttSettingsValidator(data), data); - saveData(); + await saveData(); } catch (errors: any) { setFieldErrors(errors); } @@ -95,7 +94,7 @@ const MqttSettingsForm: FC = () => { /> - { /> - { /> - { 0 1 2 - + { {LL.FORMATTING()} - { > {LL.MQTT_NEST_1()} {LL.MQTT_NEST_2()} - + } label={LL.MQTT_RESPONSE()} @@ -234,7 +233,7 @@ const MqttSettingsForm: FC = () => { alignItems="flex-start" > - { > Home Assistant Domoticz - + - { /> - { {LL.MQTT_ENTITY_FORMAT_0()} {LL.MQTT_ENTITY_FORMAT_1()} {LL.MQTT_ENTITY_FORMAT_2()} - + )} @@ -300,8 +299,7 @@ const MqttSettingsForm: FC = () => { /> - { /> - { /> - { /> - { /> - { /> - {LL.SECONDS()} @@ -405,7 +398,7 @@ const MqttSettingsForm: FC = () => { variant="outlined" color="primary" type="submit" - onClick={() => loadData()} + onClick={loadData} > {LL.CANCEL()} diff --git a/interface/src/framework/mqtt/MqttStatusForm.tsx b/interface/src/framework/mqtt/MqttStatusForm.tsx index e258ae538..b8a20f915 100644 --- a/interface/src/framework/mqtt/MqttStatusForm.tsx +++ b/interface/src/framework/mqtt/MqttStatusForm.tsx @@ -1,18 +1,18 @@ -import { FC } from 'react'; -import { Avatar, Button, Divider, List, ListItem, ListItemAvatar, ListItemText, Theme, useTheme } from '@mui/material'; - +import AutoAwesomeMotionIcon from '@mui/icons-material/AutoAwesomeMotion'; import DeviceHubIcon from '@mui/icons-material/DeviceHub'; import RefreshIcon from '@mui/icons-material/Refresh'; import ReportIcon from '@mui/icons-material/Report'; import SpeakerNotesOffIcon from '@mui/icons-material/SpeakerNotesOff'; -import AutoAwesomeMotionIcon from '@mui/icons-material/AutoAwesomeMotion'; +import { Avatar, Button, Divider, List, ListItem, ListItemAvatar, ListItemText, useTheme } from '@mui/material'; +import type { Theme } from '@mui/material'; +import type { FC } from 'react'; -import { ButtonRow, FormLoader, SectionContent } from 'components'; -import { MqttStatus, MqttDisconnectReason } from 'types'; +import type { MqttStatus } from 'types'; import * as MqttApi from 'api/mqtt'; -import { useRest } from 'utils'; - +import { ButtonRow, FormLoader, SectionContent } from 'components'; import { useI18nContext } from 'i18n/i18n-react'; +import { MqttDisconnectReason } from 'types'; +import { useRest } from 'utils'; export const mqttStatusHighlight = ({ enabled, connected }: MqttStatus, theme: Theme) => { if (!enabled) { @@ -83,50 +83,48 @@ const MqttStatusForm: FC = () => { return ; } - const renderConnectionStatus = () => { - return ( - <> - {!data.connected && ( - <> - - - - - - - - - - - )} - - - # - - - - - - - - - - - - - - - - - - - - - - - - ); - }; + const renderConnectionStatus = () => ( + <> + {!data.connected && ( + <> + + + + + + + + + + + )} + + + # + + + + + + + + + + + + + + + + + + + + + + + + ); return ( <> diff --git a/interface/src/framework/network/NetworkConnection.tsx b/interface/src/framework/network/NetworkConnection.tsx index a7930ef03..18d1842ef 100644 --- a/interface/src/framework/network/NetworkConnection.tsx +++ b/interface/src/framework/network/NetworkConnection.tsx @@ -1,16 +1,15 @@ -import { FC, useCallback, useContext, useState } from 'react'; -import { Navigate, Routes, Route, useNavigate } from 'react-router-dom'; - import { Tab } from '@mui/material'; - -import { RequireAdmin, RouterTabs, useLayoutTitle, useRouterTab } from 'components'; -import { WiFiNetwork } from 'types'; -import { AuthenticatedContext } from 'contexts/authentication'; -import { WiFiConnectionContext } from './WiFiConnectionContext'; -import NetworkStatusForm from './NetworkStatusForm'; -import WiFiNetworkScanner from './WiFiNetworkScanner'; +import { useCallback, useContext, useState } from 'react'; +import { Navigate, Routes, Route, useNavigate } from 'react-router-dom'; import NetworkSettingsForm from './NetworkSettingsForm'; +import NetworkStatusForm from './NetworkStatusForm'; +import { WiFiConnectionContext } from './WiFiConnectionContext'; +import WiFiNetworkScanner from './WiFiNetworkScanner'; +import type { FC } from 'react'; +import type { WiFiNetwork } from 'types'; +import { RequireAdmin, RouterTabs, useLayoutTitle, useRouterTab } from 'components'; +import { AuthenticatedContext } from 'contexts/authentication'; import { useI18nContext } from 'i18n/i18n-react'; const NetworkConnection: FC = () => { diff --git a/interface/src/framework/network/NetworkSettingsForm.tsx b/interface/src/framework/network/NetworkSettingsForm.tsx index ad980f34a..4e4c40912 100644 --- a/interface/src/framework/network/NetworkSettingsForm.tsx +++ b/interface/src/framework/network/NetworkSettingsForm.tsx @@ -1,6 +1,9 @@ -import { FC, useContext, useEffect, useState } from 'react'; -import { toast } from 'react-toastify'; - +import CancelIcon from '@mui/icons-material/Cancel'; +import DeleteIcon from '@mui/icons-material/Delete'; +import LockIcon from '@mui/icons-material/Lock'; +import LockOpenIcon from '@mui/icons-material/LockOpen'; +import PowerSettingsNewIcon from '@mui/icons-material/PowerSettingsNew'; +import WarningIcon from '@mui/icons-material/Warning'; import { Avatar, Button, @@ -12,16 +15,19 @@ import { ListItemSecondaryAction, ListItemText, Typography, - InputAdornment + InputAdornment, + TextField } from '@mui/material'; +import { useContext, useEffect, useState } from 'react'; +import { toast } from 'react-toastify'; +import RestartMonitor from '../system/RestartMonitor'; +import { WiFiConnectionContext } from './WiFiConnectionContext'; +import { isNetworkOpen, networkSecurityMode } from './WiFiNetworkSelector'; +import type { ValidateFieldsError } from 'async-validator'; +import type { FC } from 'react'; -import LockOpenIcon from '@mui/icons-material/LockOpen'; -import DeleteIcon from '@mui/icons-material/Delete'; -import WarningIcon from '@mui/icons-material/Warning'; -import LockIcon from '@mui/icons-material/Lock'; -import PowerSettingsNewIcon from '@mui/icons-material/PowerSettingsNew'; -import CancelIcon from '@mui/icons-material/Cancel'; - +import type { NetworkSettings } from 'types'; +import * as NetworkApi from 'api/network'; import { BlockFormControlLabel, ButtonRow, @@ -32,20 +38,13 @@ import { MessageBox, BlockNavigation } from 'components'; -import { NetworkSettings } from 'types'; -import * as NetworkApi from 'api/network'; -import { numberValue, updateValueDirty, useRest } from 'utils'; +import { useI18nContext } from 'i18n/i18n-react'; import * as EMSESP from 'project/api'; +import { numberValue, updateValueDirty, useRest } from 'utils'; -import { WiFiConnectionContext } from './WiFiConnectionContext'; -import { isNetworkOpen, networkSecurityMode } from './WiFiNetworkSelector'; -import { ValidateFieldsError } from 'async-validator'; import { validate } from 'validators'; import { createNetworkSettingsValidator } from 'validators/network'; -import { useI18nContext } from 'i18n/i18n-react'; -import RestartMonitor from '../system/RestartMonitor'; - const WiFiSettingsForm: FC = () => { const { LL } = useI18nContext(); @@ -106,7 +105,7 @@ const WiFiSettingsForm: FC = () => { try { setFieldErrors(undefined); await validate(createNetworkSettingsValidator(data), data); - saveData(); + await saveData(); } catch (errors: any) { setFieldErrors(errors); } @@ -167,7 +166,6 @@ const WiFiSettingsForm: FC = () => { margin="normal" /> )} - { type="number" margin="normal" /> - } label={LL.NETWORK_DISABLE_SLEEP()} /> - } label={LL.NETWORK_LOW_BAND()} /> - {LL.GENERAL_OPTIONS()} - { onChange={updateFormValue} margin="normal" /> - } label={LL.NETWORK_USE_DNS()} /> - } label={LL.NETWORK_ENABLE_CORS()} /> {data.enableCORS && ( - { margin="normal" /> )} - } label={LL.NETWORK_ENABLE_IPV6()} /> - } label={LL.NETWORK_FIXED_IP()} @@ -309,7 +298,7 @@ const WiFiSettingsForm: FC = () => { variant="outlined" color="primary" type="submit" - onClick={() => loadData()} + onClick={loadData} > {LL.CANCEL()} diff --git a/interface/src/framework/network/NetworkStatusForm.tsx b/interface/src/framework/network/NetworkStatusForm.tsx index 8bc70ed72..325a0c5b0 100644 --- a/interface/src/framework/network/NetworkStatusForm.tsx +++ b/interface/src/framework/network/NetworkStatusForm.tsx @@ -1,20 +1,21 @@ -import { FC } from 'react'; -import { Avatar, Button, Divider, List, ListItem, ListItemAvatar, ListItemText, Theme, useTheme } from '@mui/material'; - -import SettingsInputComponentIcon from '@mui/icons-material/SettingsInputComponent'; -import SettingsInputAntennaIcon from '@mui/icons-material/SettingsInputAntenna'; import DeviceHubIcon from '@mui/icons-material/DeviceHub'; -import WifiIcon from '@mui/icons-material/Wifi'; import DnsIcon from '@mui/icons-material/Dns'; import RefreshIcon from '@mui/icons-material/Refresh'; import RouterIcon from '@mui/icons-material/Router'; +import SettingsInputAntennaIcon from '@mui/icons-material/SettingsInputAntenna'; +import SettingsInputComponentIcon from '@mui/icons-material/SettingsInputComponent'; +import WifiIcon from '@mui/icons-material/Wifi'; +import { Avatar, Button, Divider, List, ListItem, ListItemAvatar, ListItemText, useTheme } from '@mui/material'; +import type { Theme } from '@mui/material'; +import type { FC } from 'react'; -import { ButtonRow, FormLoader, SectionContent } from 'components'; -import { NetworkConnectionStatus, NetworkStatus } from 'types'; +import type { NetworkStatus } from 'types'; import * as NetworkApi from 'api/network'; -import { useRest } from 'utils'; +import { ButtonRow, FormLoader, SectionContent } from 'components'; import { useI18nContext } from 'i18n/i18n-react'; +import { NetworkConnectionStatus } from 'types'; +import { useRest } from 'utils'; const isConnected = ({ status }: NetworkStatus) => status === NetworkConnectionStatus.WIFI_STATUS_CONNECTED || diff --git a/interface/src/framework/network/WiFiConnectionContext.tsx b/interface/src/framework/network/WiFiConnectionContext.tsx index f22002455..4965416c8 100644 --- a/interface/src/framework/network/WiFiConnectionContext.tsx +++ b/interface/src/framework/network/WiFiConnectionContext.tsx @@ -1,5 +1,5 @@ import { createContext } from 'react'; -import { WiFiNetwork } from 'types'; +import type { WiFiNetwork } from 'types'; export interface WiFiConnectionContextValue { selectedNetwork?: WiFiNetwork; diff --git a/interface/src/framework/network/WiFiNetworkScanner.tsx b/interface/src/framework/network/WiFiNetworkScanner.tsx index 61bb39aed..97205d02c 100644 --- a/interface/src/framework/network/WiFiNetworkScanner.tsx +++ b/interface/src/framework/network/WiFiNetworkScanner.tsx @@ -1,14 +1,13 @@ -import { useEffect, FC, useState, useCallback, useRef } from 'react'; +import PermScanWifiIcon from '@mui/icons-material/PermScanWifi'; +import { Button } from '@mui/material'; +import { useEffect, useState, useCallback, useRef } from 'react'; import { toast } from 'react-toastify'; -import { Button } from '@mui/material'; -import PermScanWifiIcon from '@mui/icons-material/PermScanWifi'; - -import * as NetworkApi from 'api/network'; -import { WiFiNetwork, WiFiNetworkList } from 'types'; -import { ButtonRow, FormLoader, SectionContent } from 'components'; - import WiFiNetworkSelector from './WiFiNetworkSelector'; +import type { FC } from 'react'; +import type { WiFiNetwork, WiFiNetworkList } from 'types'; +import * as NetworkApi from 'api/network'; +import { ButtonRow, FormLoader, SectionContent } from 'components'; import { useI18nContext } from 'i18n/i18n-react'; @@ -76,7 +75,7 @@ const WiFiNetworkScanner: FC = () => { }, [finishedWithError, pollNetworkList, LL]); useEffect(() => { - startNetworkScan(); + void startNetworkScan(); }, [startNetworkScan]); const renderNetworkScanner = () => { diff --git a/interface/src/framework/network/WiFiNetworkSelector.tsx b/interface/src/framework/network/WiFiNetworkSelector.tsx index 5233b7826..b344ca8e1 100644 --- a/interface/src/framework/network/WiFiNetworkSelector.tsx +++ b/interface/src/framework/network/WiFiNetworkSelector.tsx @@ -1,18 +1,16 @@ -import { FC, useContext } from 'react'; - -import { Avatar, Badge, List, ListItem, ListItemAvatar, ListItemIcon, ListItemText } from '@mui/material'; - -import LockOpenIcon from '@mui/icons-material/LockOpen'; import LockIcon from '@mui/icons-material/Lock'; +import LockOpenIcon from '@mui/icons-material/LockOpen'; import WifiIcon from '@mui/icons-material/Wifi'; - -import { MessageBox } from 'components'; - -import { WiFiEncryptionType, WiFiNetwork, WiFiNetworkList } from 'types'; +import { Avatar, Badge, List, ListItem, ListItemAvatar, ListItemIcon, ListItemText } from '@mui/material'; +import { useContext } from 'react'; import { WiFiConnectionContext } from './WiFiConnectionContext'; +import type { FC } from 'react'; +import type { WiFiNetwork, WiFiNetworkList } from 'types'; +import { MessageBox } from 'components'; import { useI18nContext } from 'i18n/i18n-react'; +import { WiFiEncryptionType } from 'types'; interface WiFiNetworkSelectorProps { networkList: WiFiNetworkList; @@ -45,24 +43,22 @@ const WiFiNetworkSelector: FC = ({ networkList }) => { const wifiConnectionContext = useContext(WiFiConnectionContext); - const renderNetwork = (network: WiFiNetwork) => { - return ( - wifiConnectionContext.selectNetwork(network)}> - - {isNetworkOpen(network) ? : } - - - - - - - - - ); - }; + const renderNetwork = (network: WiFiNetwork) => ( + wifiConnectionContext.selectNetwork(network)}> + + {isNetworkOpen(network) ? : } + + + + + + + + + ); if (networkList.networks.length === 0) { return ; diff --git a/interface/src/framework/ntp/NTPSettingsForm.tsx b/interface/src/framework/ntp/NTPSettingsForm.tsx index 12df18103..56c3b3e0e 100644 --- a/interface/src/framework/ntp/NTPSettingsForm.tsx +++ b/interface/src/framework/ntp/NTPSettingsForm.tsx @@ -1,11 +1,13 @@ -import { FC, useState } from 'react'; -import { ValidateFieldsError } from 'async-validator'; - -import { Button, Checkbox, MenuItem } from '@mui/material'; -import WarningIcon from '@mui/icons-material/Warning'; import CancelIcon from '@mui/icons-material/Cancel'; +import WarningIcon from '@mui/icons-material/Warning'; +import { Button, Checkbox, MenuItem } from '@mui/material'; +import { useState } from 'react'; +import { selectedTimeZone, timeZoneSelectItems, TIME_ZONES } from './TZ'; +import type { ValidateFieldsError } from 'async-validator'; +import type { FC } from 'react'; -import { validate } from 'validators'; +import type { NTPSettings } from 'types'; +import * as NTPApi from 'api/ntp'; import { BlockFormControlLabel, ButtonRow, @@ -14,13 +16,10 @@ import { ValidatedTextField, BlockNavigation } from 'components'; -import { NTPSettings } from 'types'; -import { updateValueDirty, useRest } from 'utils'; -import * as NTPApi from 'api/ntp'; -import { selectedTimeZone, timeZoneSelectItems, TIME_ZONES } from './TZ'; -import { NTP_SETTINGS_VALIDATOR } from 'validators/ntp'; - import { useI18nContext } from 'i18n/i18n-react'; +import { updateValueDirty, useRest } from 'utils'; +import { validate } from 'validators'; +import { NTP_SETTINGS_VALIDATOR } from 'validators/ntp'; const NTPSettingsForm: FC = () => { const { loadData, saving, data, setData, origData, dirtyFlags, setDirtyFlags, blocker, saveData, errorMessage } = @@ -44,7 +43,7 @@ const NTPSettingsForm: FC = () => { try { setFieldErrors(undefined); await validate(NTP_SETTINGS_VALIDATOR, data); - saveData(); + await saveData(); } catch (errors: any) { setFieldErrors(errors); } @@ -96,7 +95,7 @@ const NTPSettingsForm: FC = () => { variant="outlined" color="primary" type="submit" - onClick={() => loadData()} + onClick={loadData} > {LL.CANCEL()} diff --git a/interface/src/framework/ntp/NTPStatusForm.tsx b/interface/src/framework/ntp/NTPStatusForm.tsx index 9f2c42b0a..51fb5a640 100644 --- a/interface/src/framework/ntp/NTPStatusForm.tsx +++ b/interface/src/framework/ntp/NTPStatusForm.tsx @@ -1,6 +1,9 @@ -import { FC, useContext, useState } from 'react'; -import { toast } from 'react-toastify'; - +import AccessTimeIcon from '@mui/icons-material/AccessTime'; +import CancelIcon from '@mui/icons-material/Cancel'; +import DnsIcon from '@mui/icons-material/Dns'; +import RefreshIcon from '@mui/icons-material/Refresh'; +import SwapVerticalCircleIcon from '@mui/icons-material/SwapVerticalCircle'; +import UpdateIcon from '@mui/icons-material/Update'; import { Avatar, Box, @@ -15,24 +18,22 @@ import { ListItemAvatar, ListItemText, TextField, - Theme, useTheme, Typography } from '@mui/material'; -import RefreshIcon from '@mui/icons-material/Refresh'; -import AccessTimeIcon from '@mui/icons-material/AccessTime'; -import SwapVerticalCircleIcon from '@mui/icons-material/SwapVerticalCircle'; -import UpdateIcon from '@mui/icons-material/Update'; -import DnsIcon from '@mui/icons-material/Dns'; -import CancelIcon from '@mui/icons-material/Cancel'; +import { useContext, useState } from 'react'; +import { toast } from 'react-toastify'; +import type { Theme } from '@mui/material'; +import type { FC } from 'react'; +import type { NTPStatus } from 'types'; import * as NTPApi from 'api/ntp'; -import { NTPStatus, NTPSyncStatus } from 'types'; import { ButtonRow, FormLoader, SectionContent } from 'components'; -import { extractErrorMessage, formatDateTime, formatLocalDateTime, useRest } from 'utils'; import { AuthenticatedContext } from 'contexts/authentication'; import { useI18nContext } from 'i18n/i18n-react'; +import { NTPSyncStatus } from 'types'; +import { extractErrorMessage, formatDateTime, formatLocalDateTime, useRest } from 'utils'; export const isNtpActive = ({ status }: NTPStatus) => status === NTPSyncStatus.NTP_ACTIVE; export const isNtpEnabled = ({ status }: NTPStatus) => status !== NTPSyncStatus.NTP_DISABLED; @@ -89,7 +90,7 @@ const NTPStatusForm: FC = () => { }); toast.success(LL.TIME_SET()); setSettingTime(false); - loadData(); + await loadData(); } catch (error) { toast.error(extractErrorMessage(error, LL.PROBLEM_UPDATING())); } finally { diff --git a/interface/src/framework/ntp/NetworkTime.tsx b/interface/src/framework/ntp/NetworkTime.tsx index 22248a1d8..d41d1fb8c 100644 --- a/interface/src/framework/ntp/NetworkTime.tsx +++ b/interface/src/framework/ntp/NetworkTime.tsx @@ -1,14 +1,13 @@ -import { FC, useContext } from 'react'; -import { Navigate, Route, Routes } from 'react-router-dom'; - import { Tab } from '@mui/material'; +import { useContext } from 'react'; +import { Navigate, Route, Routes } from 'react-router-dom'; +import NTPSettingsForm from './NTPSettingsForm'; +import NTPStatusForm from './NTPStatusForm'; +import type { FC } from 'react'; import { RequireAdmin, RouterTabs, useLayoutTitle, useRouterTab } from 'components'; import { AuthenticatedContext } from 'contexts/authentication'; -import NTPStatusForm from './NTPStatusForm'; -import NTPSettingsForm from './NTPSettingsForm'; - import { useI18nContext } from 'i18n/i18n-react'; const NetworkTime: FC = () => { diff --git a/interface/src/framework/security/GenerateToken.tsx b/interface/src/framework/security/GenerateToken.tsx index 2b0c0e9fd..d3937b109 100644 --- a/interface/src/framework/security/GenerateToken.tsx +++ b/interface/src/framework/security/GenerateToken.tsx @@ -1,4 +1,4 @@ -import { FC, useCallback, useState, useEffect } from 'react'; +import CloseIcon from '@mui/icons-material/Close'; import { Dialog, DialogTitle, @@ -10,16 +10,16 @@ import { TextField, Button } from '@mui/material'; +import { useCallback, useState, useEffect } from 'react'; -import CloseIcon from '@mui/icons-material/Close'; - -import { extractErrorMessage } from 'utils'; import { toast } from 'react-toastify'; -import { MessageBox } from 'components'; +import type { FC } from 'react'; +import type { Token } from 'types'; import * as SecurityApi from 'api/security'; -import { Token } from 'types'; +import { MessageBox } from 'components'; import { useI18nContext } from 'i18n/i18n-react'; +import { extractErrorMessage } from 'utils'; interface GenerateTokenProps { username?: string; @@ -42,7 +42,7 @@ const GenerateToken: FC = ({ username, onClose }) => { useEffect(() => { if (open) { - getToken(); + void getToken(); } }, [open, getToken]); diff --git a/interface/src/framework/security/ManageUsersForm.tsx b/interface/src/framework/security/ManageUsersForm.tsx index 43d7b45a5..e4aa4862a 100644 --- a/interface/src/framework/security/ManageUsersForm.tsx +++ b/interface/src/framework/security/ManageUsersForm.tsx @@ -1,28 +1,26 @@ -import { FC, useContext, useState } from 'react'; - -import { Button, IconButton, Box } from '@mui/material'; -import SaveIcon from '@mui/icons-material/Save'; -import DeleteIcon from '@mui/icons-material/Delete'; -import PersonAddIcon from '@mui/icons-material/PersonAdd'; -import EditIcon from '@mui/icons-material/Edit'; import CheckIcon from '@mui/icons-material/Check'; import CloseIcon from '@mui/icons-material/Close'; +import DeleteIcon from '@mui/icons-material/Delete'; +import EditIcon from '@mui/icons-material/Edit'; +import PersonAddIcon from '@mui/icons-material/PersonAdd'; +import SaveIcon from '@mui/icons-material/Save'; import VpnKeyIcon from '@mui/icons-material/VpnKey'; +import { Button, IconButton, 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 * as SecurityApi from 'api/security'; -import { SecuritySettings, User } from 'types'; -import { ButtonRow, FormLoader, MessageBox, SectionContent } from 'components'; -import { createUserValidator } from 'validators'; -import { useRest } from 'utils'; -import { AuthenticatedContext } from 'contexts/authentication'; - -import { useI18nContext } from 'i18n/i18n-react'; +import { useTheme } from '@table-library/react-table-library/theme'; +import { useContext, useState } from 'react'; import GenerateToken from './GenerateToken'; import UserForm from './UserForm'; +import type { FC } from 'react'; +import type { SecuritySettings, User } from 'types'; +import * as SecurityApi from 'api/security'; +import { ButtonRow, FormLoader, MessageBox, SectionContent } from 'components'; +import { AuthenticatedContext } from 'contexts/authentication'; +import { useI18nContext } from 'i18n/i18n-react'; +import { useRest } from 'utils'; +import { createUserValidator } from 'validators'; const ManageUsersForm: FC = () => { const { loadData, saving, data, setData, saveData, errorMessage } = useRest({ @@ -60,7 +58,6 @@ const ManageUsersForm: FC = () => { border-top: 1px solid #565656; border-bottom: 1px solid #565656; } - &:nth-of-type(odd) .td { background-color: #303030; } @@ -126,7 +123,7 @@ const ManageUsersForm: FC = () => { const onSubmit = async () => { await saveData(); - authenticatedContext.refresh(); + await authenticatedContext.refresh(); }; const user_table = data.users.map((u) => ({ ...u, id: u.username })); diff --git a/interface/src/framework/security/Security.tsx b/interface/src/framework/security/Security.tsx index 4bb68580f..d55a171e1 100644 --- a/interface/src/framework/security/Security.tsx +++ b/interface/src/framework/security/Security.tsx @@ -1,13 +1,11 @@ -import { FC } from 'react'; -import { Navigate, Routes, Route } from 'react-router-dom'; - import { Tab } from '@mui/material'; +import { Navigate, Routes, Route } from 'react-router-dom'; +import ManageUsersForm from './ManageUsersForm'; +import SecuritySettingsForm from './SecuritySettingsForm'; +import type { FC } from 'react'; import { RouterTabs, useRouterTab, useLayoutTitle } from 'components'; -import SecuritySettingsForm from './SecuritySettingsForm'; -import ManageUsersForm from './ManageUsersForm'; - import { useI18nContext } from 'i18n/i18n-react'; const Security: FC = () => { diff --git a/interface/src/framework/security/SecuritySettingsForm.tsx b/interface/src/framework/security/SecuritySettingsForm.tsx index f02a5b6f4..14930d399 100644 --- a/interface/src/framework/security/SecuritySettingsForm.tsx +++ b/interface/src/framework/security/SecuritySettingsForm.tsx @@ -1,18 +1,18 @@ -import { FC, useContext, useState } from 'react'; -import { ValidateFieldsError } from 'async-validator'; - -import { Button } from '@mui/material'; -import WarningIcon from '@mui/icons-material/Warning'; import CancelIcon from '@mui/icons-material/Cancel'; +import WarningIcon from '@mui/icons-material/Warning'; +import { Button } from '@mui/material'; +import { useContext, useState } from 'react'; +import type { ValidateFieldsError } from 'async-validator'; +import type { FC } from 'react'; +import type { SecuritySettings } from 'types'; import * as SecurityApi from 'api/security'; -import { SecuritySettings } from 'types'; import { ButtonRow, FormLoader, MessageBox, SectionContent, ValidatedPasswordField, BlockNavigation } from 'components'; -import { SECURITY_SETTINGS_VALIDATOR, validate } from 'validators'; -import { updateValueDirty, useRest } from 'utils'; import { AuthenticatedContext } from 'contexts/authentication'; import { useI18nContext } from 'i18n/i18n-react'; +import { updateValueDirty, useRest } from 'utils'; +import { SECURITY_SETTINGS_VALIDATOR, validate } from 'validators'; const SecuritySettingsForm: FC = () => { const { LL } = useI18nContext(); @@ -65,7 +65,7 @@ const SecuritySettingsForm: FC = () => { variant="outlined" color="primary" type="submit" - onClick={() => loadData()} + onClick={loadData} > {LL.CANCEL()} diff --git a/interface/src/framework/security/UserForm.tsx b/interface/src/framework/security/UserForm.tsx index f17c9dfbd..f11343096 100644 --- a/interface/src/framework/security/UserForm.tsx +++ b/interface/src/framework/security/UserForm.tsx @@ -1,19 +1,19 @@ -import { FC, useState, useEffect } from 'react'; -import Schema, { ValidateFieldsError } from 'async-validator'; - import CancelIcon from '@mui/icons-material/Cancel'; import PersonAddIcon from '@mui/icons-material/PersonAdd'; import SaveIcon from '@mui/icons-material/Save'; import { Button, Checkbox, Dialog, DialogActions, DialogContent, DialogTitle } from '@mui/material'; +import { useState, useEffect } from 'react'; +import type Schema from 'async-validator'; +import type { ValidateFieldsError } from 'async-validator'; +import type { FC } from 'react'; +import type { User } from 'types'; import { BlockFormControlLabel, ValidatedPasswordField, ValidatedTextField } from 'components'; -import { User } from 'types'; +import { useI18nContext } from 'i18n/i18n-react'; import { updateValue } from 'utils'; import { validate } from 'validators'; -import { useI18nContext } from 'i18n/i18n-react'; - interface UserFormProps { creating: boolean; validator: Schema; diff --git a/interface/src/framework/system/GeneralFileUpload.tsx b/interface/src/framework/system/GeneralFileUpload.tsx index b3a291ca0..11ade4405 100644 --- a/interface/src/framework/system/GeneralFileUpload.tsx +++ b/interface/src/framework/system/GeneralFileUpload.tsx @@ -1,21 +1,15 @@ -import { FC } from 'react'; -import { AxiosPromise } from 'axios'; - +import DownloadIcon from '@mui/icons-material/GetApp'; import { Typography, Button, Box } from '@mui/material'; - -import { FileUploadConfig } from 'api/endpoints'; +import { toast } from 'react-toastify'; +import type { FileUploadConfig } from 'api/endpoints'; +import type { AxiosPromise } from 'axios'; +import type { FC } from 'react'; import { SingleUpload, useFileUpload } from 'components'; -import DownloadIcon from '@mui/icons-material/GetApp'; - -import { toast } from 'react-toastify'; - -import { extractErrorMessage } from 'utils'; - -import * as EMSESP from 'project/api'; - import { useI18nContext } from 'i18n/i18n-react'; +import * as EMSESP from 'project/api'; +import { extractErrorMessage } from 'utils'; interface UploadFileProps { uploadGeneralFile: (file: File, config?: FileUploadConfig) => AxiosPromise; @@ -122,7 +116,7 @@ const GeneralFileUpload: FC = ({ uploadGeneralFile }) => { {LL.DOWNLOAD_SETTINGS_TEXT()} - @@ -130,12 +124,7 @@ const GeneralFileUpload: FC = ({ uploadGeneralFile }) => { {LL.DOWNLOAD_CUSTOMIZATION_TEXT()}{' '} - {LL.DOWNLOAD_SCHEDULE_TEXT()}{' '} - diff --git a/interface/src/framework/system/OTASettingsForm.tsx b/interface/src/framework/system/OTASettingsForm.tsx index 3bf5c6817..33543f0c9 100644 --- a/interface/src/framework/system/OTASettingsForm.tsx +++ b/interface/src/framework/system/OTASettingsForm.tsx @@ -1,9 +1,11 @@ -import { FC, useState } from 'react'; - -import { Button, Checkbox } from '@mui/material'; -import WarningIcon from '@mui/icons-material/Warning'; import CancelIcon from '@mui/icons-material/Cancel'; +import WarningIcon from '@mui/icons-material/Warning'; +import { Button, Checkbox } from '@mui/material'; +import { useState } from 'react'; +import type { ValidateFieldsError } from 'async-validator'; +import type { FC } from 'react'; +import type { OTASettings } from 'types'; import * as SystemApi from 'api/system'; import { BlockFormControlLabel, @@ -15,15 +17,12 @@ import { BlockNavigation } from 'components'; -import { OTASettings } from 'types'; +import { useI18nContext } from 'i18n/i18n-react'; import { numberValue, updateValueDirty, useRest } from 'utils'; -import { ValidateFieldsError } from 'async-validator'; import { validate } from 'validators'; import { OTA_SETTINGS_VALIDATOR } from 'validators/system'; -import { useI18nContext } from 'i18n/i18n-react'; - const OTASettingsForm: FC = () => { const { loadData, saving, data, setData, origData, dirtyFlags, setDirtyFlags, blocker, saveData, errorMessage } = useRest({ @@ -46,7 +45,7 @@ const OTASettingsForm: FC = () => { try { setFieldErrors(undefined); await validate(OTA_SETTINGS_VALIDATOR, data); - saveData(); + await saveData(); } catch (errors: any) { setFieldErrors(errors); } @@ -87,7 +86,7 @@ const OTASettingsForm: FC = () => { variant="outlined" color="primary" type="submit" - onClick={() => loadData()} + onClick={loadData} > {LL.CANCEL()} diff --git a/interface/src/framework/system/RestartMonitor.tsx b/interface/src/framework/system/RestartMonitor.tsx index 028028174..9ba331568 100644 --- a/interface/src/framework/system/RestartMonitor.tsx +++ b/interface/src/framework/system/RestartMonitor.tsx @@ -1,4 +1,5 @@ -import { FC, useRef, useState, useEffect } from 'react'; +import { useRef, useState, useEffect } from 'react'; +import type { FC } from 'react'; import * as SystemApi from 'api/system'; import { FormLoader } from 'components'; @@ -30,7 +31,7 @@ const RestartMonitor: FC = () => { }); useEffect(() => { - poll.current(); + void poll.current(); }, []); useEffect(() => () => timeoutId && clearTimeout(timeoutId), [timeoutId]); diff --git a/interface/src/framework/system/System.tsx b/interface/src/framework/system/System.tsx index 8cc5cc80d..2caf4b7c6 100644 --- a/interface/src/framework/system/System.tsx +++ b/interface/src/framework/system/System.tsx @@ -1,15 +1,14 @@ -import { FC, useContext } from 'react'; -import { Navigate, Routes, Route } from 'react-router-dom'; - import { Tab } from '@mui/material'; +import { useContext } from 'react'; +import { Navigate, Routes, Route } from 'react-router-dom'; +import OTASettingsForm from './OTASettingsForm'; +import SystemLog from './SystemLog'; +import SystemStatusForm from './SystemStatusForm'; +import UploadFileForm from './UploadFileForm'; +import type { FC } from 'react'; import { useRouterTab, RouterTabs, useLayoutTitle, RequireAdmin } from 'components'; import { AuthenticatedContext } from 'contexts/authentication'; -import UploadFileForm from './UploadFileForm'; -import SystemStatusForm from './SystemStatusForm'; -import OTASettingsForm from './OTASettingsForm'; - -import SystemLog from './SystemLog'; import { useI18nContext } from 'i18n/i18n-react'; diff --git a/interface/src/framework/system/SystemLog.tsx b/interface/src/framework/system/SystemLog.tsx index b79b02248..b7e3e69fd 100644 --- a/interface/src/framework/system/SystemLog.tsx +++ b/interface/src/framework/system/SystemLog.tsx @@ -1,39 +1,24 @@ -import { FC, useState, useEffect, useCallback, useLayoutEffect } from 'react'; - -import { Box, styled, Button, Checkbox, MenuItem, Grid, Slider, FormLabel } from '@mui/material'; - -import * as SystemApi from 'api/system'; -import { addAccessTokenParameter } from 'api/authentication'; - -import { SectionContent, FormLoader, BlockFormControlLabel, ValidatedTextField } from 'components'; - -import { LogSettings, LogEntry, LogEntries, LogLevel } from 'types'; -import { updateValue, useRest, extractErrorMessage } from 'utils'; - import DownloadIcon from '@mui/icons-material/GetApp'; - +import WarningIcon from '@mui/icons-material/Warning'; +import { Box, styled, Button, Checkbox, MenuItem, Grid, TextField } from '@mui/material'; +import { useState, useEffect, useCallback } from 'react'; import { toast } from 'react-toastify'; +import type { FC } from 'react'; +import type { LogSettings, LogEntry, LogEntries } from 'types'; +import { addAccessTokenParameter } from 'api/authentication'; import { EVENT_SOURCE_ROOT } from 'api/endpoints'; +import * as SystemApi from 'api/system'; + +import { SectionContent, FormLoader, BlockFormControlLabel, BlockNavigation } from 'components'; import { useI18nContext } from 'i18n/i18n-react'; +import { LogLevel } from 'types'; +import { useRest, updateValueDirty, extractErrorMessage } from 'utils'; export const LOG_EVENTSOURCE_URL = EVENT_SOURCE_ROOT + 'log'; -const useWindowSize = () => { - const [size, setSize] = useState([0, 0]); - useLayoutEffect(() => { - function updateSize() { - setSize([window.innerWidth, window.innerHeight]); - } - window.addEventListener('resize', updateSize); - updateSize(); - return () => window.removeEventListener('resize', updateSize); - }, []); - return size; -}; - -const LogEntryLine = styled('div')(({ theme }) => ({ +const LogEntryLine = styled('div')(() => ({ color: '#bbbbbb', fontFamily: 'monospace', fontSize: '14px', @@ -64,11 +49,9 @@ const levelLabel = (level: LogLevel) => { }; const SystemLog: FC = () => { - useWindowSize(); - const { LL } = useI18nContext(); - const { loadData, data, setData } = useRest({ + const { loadData, data, setData, origData, dirtyFlags, blocker, setDirtyFlags, setOrigData } = useRest({ read: SystemApi.readLogSettings }); @@ -91,47 +74,7 @@ const SystemLog: FC = () => { return data?.compact ? label : label.padEnd(7, '\xa0'); }; - const updateFormValue = updateValue(setData); - - const reloadPage = () => { - window.location.reload(); - }; - - const sendSettings = async (new_max_messages: number, new_level: number) => { - if (data) { - try { - const response = await SystemApi.updateLogSettings({ - level: new_level, - max_messages: new_max_messages, - compact: data.compact - }); - if (response.status !== 200) { - toast.error(LL.PROBLEM_UPDATING()); - } - } catch (error) { - toast.error(extractErrorMessage(error, LL.PROBLEM_UPDATING())); - } - } - }; - - const changeLevel = (event: React.ChangeEvent) => { - if (data) { - setData({ - ...data, - level: parseInt(event.target.value) - }); - sendSettings(data.max_messages, parseInt(event.target.value)); - } - }; - - const changeMaxMessages = (event: Event, value: number | number[]) => { - if (data) { - setData({ - ...data, - max_messages: value as number - }); - } - }; + const updateFormValue = updateValueDirty(origData, dirtyFlags, setDirtyFlags, setData); const onDownload = () => { let result = ''; @@ -166,7 +109,7 @@ const SystemLog: FC = () => { }, [LL]); useEffect(() => { - fetchLog(); + void fetchLog(); }, [fetchLog]); useEffect(() => { @@ -174,14 +117,35 @@ const SystemLog: FC = () => { es.onmessage = onMessage; es.onerror = () => { es.close(); - reloadPage(); + window.location.reload(); }; return () => { es.close(); }; - // eslint-disable-next-line - }, []); + }); + + const saveSettings = async () => { + if (data) { + try { + const response = await SystemApi.updateLogSettings({ + level: data.level, + max_messages: data.max_messages, + compact: data.compact + }); + + if (response.status !== 200) { + toast.error(LL.PROBLEM_UPDATING()); + } else { + setOrigData(response.data); + setDirtyFlags([]); + toast.success(LL.UPDATED_OF(LL.SETTINGS_OF(''))); + } + } catch (error) { + toast.error(extractErrorMessage(error, LL.PROBLEM_UPDATING())); + } + } + }; const content = () => { if (!data) { @@ -191,14 +155,14 @@ const SystemLog: FC = () => { return ( <> - - + @@ -209,26 +173,24 @@ const SystemLog: FC = () => { INFO DEBUG ALL - + - - {LL.BUFFER_SIZE()} - + sendSettings(data.max_messages, data.level)} - /> + label={LL.BUFFER_SIZE()} + value={data.max_messages} + fullWidth + variant="outlined" + onChange={updateFormValue} + margin="normal" + select + > + 25 + 50 + 75 + 100 + { label={LL.COMPACT()} /> - + - + {dirtyFlags && dirtyFlags.length !== 0 && ( + + )} + { return ( + {blocker ? : null} {content()} ); diff --git a/interface/src/framework/system/SystemStatusForm.tsx b/interface/src/framework/system/SystemStatusForm.tsx index f0d078da9..792e5df79 100644 --- a/interface/src/framework/system/SystemStatusForm.tsx +++ b/interface/src/framework/system/SystemStatusForm.tsx @@ -1,5 +1,16 @@ -import { FC, useContext, useState, useEffect } from 'react'; -import { toast } from 'react-toastify'; +import AppsIcon from '@mui/icons-material/Apps'; +import BuildIcon from '@mui/icons-material/Build'; +import CancelIcon from '@mui/icons-material/Cancel'; +import DevicesIcon from '@mui/icons-material/Devices'; +import FolderIcon from '@mui/icons-material/Folder'; +import MemoryIcon from '@mui/icons-material/Memory'; +import PowerSettingsNewIcon from '@mui/icons-material/PowerSettingsNew'; +import RefreshIcon from '@mui/icons-material/Refresh'; +import SdCardAlertIcon from '@mui/icons-material/SdCardAlert'; +import SdStorageIcon from '@mui/icons-material/SdStorage'; +import SettingsBackupRestoreIcon from '@mui/icons-material/SettingsBackupRestore'; +import ShowChartIcon from '@mui/icons-material/ShowChart'; +import TimerIcon from '@mui/icons-material/Timer'; import { Avatar, Box, @@ -17,31 +28,18 @@ import { Typography } from '@mui/material'; -import DevicesIcon from '@mui/icons-material/Devices'; -import ShowChartIcon from '@mui/icons-material/ShowChart'; -import MemoryIcon from '@mui/icons-material/Memory'; -import AppsIcon from '@mui/icons-material/Apps'; -import SdStorageIcon from '@mui/icons-material/SdStorage'; -import SdCardAlertIcon from '@mui/icons-material/SdCardAlert'; -import FolderIcon from '@mui/icons-material/Folder'; -import RefreshIcon from '@mui/icons-material/Refresh'; -import PowerSettingsNewIcon from '@mui/icons-material/PowerSettingsNew'; -import SettingsBackupRestoreIcon from '@mui/icons-material/SettingsBackupRestore'; -import BuildIcon from '@mui/icons-material/Build'; -import TimerIcon from '@mui/icons-material/Timer'; -import CancelIcon from '@mui/icons-material/Cancel'; - -import { ButtonRow, FormLoader, SectionContent, MessageBox } from 'components'; -import { SystemStatus, Version } from 'types'; -import * as SystemApi from 'api/system'; -import { extractErrorMessage, useRest } from 'utils'; - -import { AuthenticatedContext } from 'contexts/authentication'; - import axios from 'axios'; +import { useContext, useState, useEffect } from 'react'; +import { toast } from 'react-toastify'; import RestartMonitor from './RestartMonitor'; +import type { FC } from 'react'; +import type { SystemStatus, Version } from 'types'; +import * as SystemApi from 'api/system'; +import { ButtonRow, FormLoader, SectionContent, MessageBox } from 'components'; +import { AuthenticatedContext } from 'contexts/authentication'; import { useI18nContext } from 'i18n/i18n-react'; +import { extractErrorMessage, useRest } from 'utils'; export const VERSIONCHECK_ENDPOINT = 'https://api.github.com/repos/emsesp/EMS-ESP32/releases/latest'; export const VERSIONCHECK_DEV_ENDPOINT = 'https://api.github.com/repos/emsesp/EMS-ESP32/releases/tags/latest'; @@ -66,14 +64,14 @@ const SystemStatusForm: FC = () => { const [latestDevVersion, setLatestDevVersion] = useState(); useEffect(() => { - axios.get(VERSIONCHECK_ENDPOINT).then((response) => { + void axios.get(VERSIONCHECK_ENDPOINT).then((response) => { setLatestVersion({ version: response.data.name, url: response.data.assets[1].browser_download_url, changelog: response.data.assets[0].browser_download_url }); }); - axios.get(VERSIONCHECK_DEV_ENDPOINT).then((response) => { + void axios.get(VERSIONCHECK_DEV_ENDPOINT).then((response) => { setLatestDevVersion({ version: response.data.name.split(/\s+/).splice(-1), url: response.data.assets[1].browser_download_url, @@ -148,61 +146,59 @@ const SystemStatusForm: FC = () => { ); - const renderVersionDialog = () => { - return ( - setShowingVersion(false)}> - {LL.VERSION_CHECK(1)} - - - {latestVersion && ( - - {LL.THE_LATEST()} {LL.OFFICIAL()} {LL.VERSION_IS()} {latestVersion.version} -  ( - - {LL.RELEASE_NOTES()} - - ) ( - - {LL.DOWNLOAD(1)} - - ) - - )} - - {latestDevVersion && ( - - {LL.THE_LATEST()} {LL.DEVELOPMENT()} {LL.VERSION_IS()}  - {latestDevVersion.version} -  ( - - {LL.RELEASE_NOTES()} - - ) ( - - {LL.DOWNLOAD(1)} - - ) - - )} - - - - {LL.USE()}  - - {LL.UPLOAD()} - -  {LL.SYSTEM_APPLY_FIRMWARE()} - + const renderVersionDialog = () => ( + setShowingVersion(false)}> + {LL.VERSION_CHECK(1)} + + + {latestVersion && ( + + {LL.THE_LATEST()} {LL.OFFICIAL()} {LL.RELEASE_IS()} {latestVersion.version} +  ( + + {LL.RELEASE_NOTES()} + + ) ( + + {LL.DOWNLOAD(1)} + + ) - - - - - - ); - }; + )} + + {latestDevVersion && ( + + {LL.THE_LATEST()} {LL.DEVELOPMENT()} {LL.RELEASE_IS()}  + {latestDevVersion.version} +  ( + + {LL.RELEASE_NOTES()} + + ) ( + + {LL.DOWNLOAD(1)} + + ) + + )} + + + + {LL.USE()}  + + {LL.UPLOAD()} + +  {LL.SYSTEM_APPLY_FIRMWARE()} + + + + + + + + ); const factoryReset = async () => { setProcessing(true); diff --git a/interface/src/framework/system/UploadFileForm.tsx b/interface/src/framework/system/UploadFileForm.tsx index 5a71f6121..568738124 100644 --- a/interface/src/framework/system/UploadFileForm.tsx +++ b/interface/src/framework/system/UploadFileForm.tsx @@ -1,11 +1,11 @@ -import { FC, useRef, useState } from 'react'; +import { useRef, useState } from 'react'; +import GeneralFileUpload from './GeneralFileUpload'; +import RestartMonitor from './RestartMonitor'; +import type { FileUploadConfig } from 'api/endpoints'; +import type { FC } from 'react'; import * as SystemApi from 'api/system'; import { SectionContent } from 'components'; -import { FileUploadConfig } from 'api/endpoints'; - -import GeneralFileUpload from './GeneralFileUpload'; -import RestartMonitor from './RestartMonitor'; import { useI18nContext } from 'i18n/i18n-react'; diff --git a/interface/src/i18n/de/index.ts b/interface/src/i18n/de/index.ts index 0471f8b40..e87be85aa 100644 --- a/interface/src/i18n/de/index.ts +++ b/interface/src/i18n/de/index.ts @@ -37,15 +37,14 @@ const de: Translation = { BRAND: 'Marke', ENTITY_NAME: 'Entitätsname', VALUE: '{{Wert|wert}}', - SHOW_FAV: 'nur Favoriten anzeigen', - DEVICE_SENSOR_DATA: 'Geräte- und Sensordaten', - DEVICES_SENSORS: 'Geräte & Sensoren', - ATTACHED_SENSORS: 'Angeschlossene EMS-ESP Sensoren', + DEVICE_DATA: 'Gerätedaten', + SENSOR_DATA: 'Sensordaten', + DEVICES: 'Geräte', + SENSORS: 'Sensoren', RUN_COMMAND: 'Befehl ausführen', CHANGE_VALUE: 'Wert ändern', CANCEL: 'Abbrechen', RESET: 'Zurücksetzen', - SEND: 'Senden', APPLY_CHANGES: 'Apply Changes ({0})', // TODO translate UPDATE: 'Update', // TODO translate REMOVE: 'Entfernen', @@ -174,7 +173,7 @@ const de: Translation = { SUPPORT_INFO: 'Support Info', UPLOAD_OF: '{0} Hochladen', UPLOAD: 'Hochladen', - DOWNLOAD: 'Herunterladen', + DOWNLOAD: '{{H|h|h}}erunterladen', ABORTED: 'abgebrochen', FAILED: 'gescheitert', SUCCESSFUL: 'erfolgreich', @@ -182,7 +181,7 @@ const de: Translation = { LOG_OF: '{0} Log', STATUS_OF: '{0} Status', UPLOAD_DOWNLOAD: 'Hoch-/Herunterladen', - SYSTEM_VERSION_RUNNING: 'Sie verwenden die Version', + VERSION_ON: 'You are currently on', // TODO translate SYSTEM_APPLY_FIRMWARE: 'um die neue Firmware anzuwenden', CLOSE: 'Schließen', USE: 'Verwenden Sie', @@ -193,7 +192,7 @@ const de: Translation = { THE_LATEST: 'Die neueste', OFFICIAL: 'offizielle', DEVELOPMENT: 'Entwicklungs', - VERSION_IS: 'Version ist', + RELEASE_IS: 'release ist', // TODO translate RELEASE_NOTES: 'Versionshinweise', EMS_ESP_VER: 'EMS-ESP Version', PLATFORM: 'Platform (Platform / SDK)', @@ -311,17 +310,20 @@ 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', - SCHEDULE_SAVED: 'Plan gespeichert', + SCHEDULE_UPDATED: 'Plan gespeichert', SCHEDULE_TIMER_1: 'beim Start', 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', - WRITEABLE: 'Schreibbar' + ENTITIES_UPDATED: 'Entities Updated', // TODO translate + WRITEABLE: 'Schreibbar', + SHOWING: 'Showing', // TODO translate + SEARCH: 'Search' // TODO translate }; export default de; diff --git a/interface/src/i18n/en/index.ts b/interface/src/i18n/en/index.ts index d7d348213..9982ada54 100644 --- a/interface/src/i18n/en/index.ts +++ b/interface/src/i18n/en/index.ts @@ -17,8 +17,8 @@ const en: Translation = { HELP_OF: '{0} Help', LOGGED_IN: 'Logged in as {name}', PLEASE_SIGNIN: 'Please sign in to continue', - UPLOAD_SUCCESSFUL: 'Upload finished', - DOWNLOAD_SUCCESSFUL: 'Download finished', + UPLOAD_SUCCESSFUL: 'Upload successful', + DOWNLOAD_SUCCESSFUL: 'Download successful', INVALID_LOGIN: 'Invalid login details', NETWORK: 'Network', SECURITY: 'Security', @@ -37,15 +37,14 @@ const en: Translation = { BRAND: 'Brand', ENTITY_NAME: 'Entity Name', VALUE: '{{Value|value}}', - SHOW_FAV: 'only show favorites', - DEVICE_SENSOR_DATA: 'Device and Sensor Data', - DEVICES_SENSORS: 'Devices & Sensors', - ATTACHED_SENSORS: 'Attached EMS-ESP Sensors', + DEVICE_DATA: 'Device Data', + SENSOR_DATA: 'Sensor Data', + DEVICES: 'Devices', + SENSORS: 'Sensors', RUN_COMMAND: 'Call Command', CHANGE_VALUE: 'Change Value', CANCEL: 'Cancel', RESET: 'Reset', - SEND: 'Send', APPLY_CHANGES: 'Apply Changes ({0})', UPDATE: 'Update', REMOVE: 'Remove', @@ -69,12 +68,12 @@ const en: Translation = { SENSOR: 'Sensor', TEMP_SENSOR: 'Temperature Sensor', TEMP_SENSORS: 'Temperature Sensors', - WRITE_CMD_SENT: 'Write command has been sent', + WRITE_CMD_SENT: 'Write command sent', WRITE_CMD_FAILED: 'Write command failed', EMS_BUS_WARNING: 'EMS bus disconnected. If this warning still persists after a few seconds please check settings and board profile', EMS_BUS_SCANNING: 'Scanning for EMS devices...', CONNECTED: 'Connected', - TX_ISSUES: 'Tx issues - try a different Tx Mode', + TX_ISSUES: 'Tx issues - check Tx Mode', DISCONNECTED: 'Disconnected', EMS_SCAN: 'Are you sure you want to initiate a full device scan of the EMS bus?', EMS_BUS_STATUS: 'EMS Bus Status', @@ -174,7 +173,7 @@ const en: Translation = { SUPPORT_INFO: 'Support Info', UPLOAD_OF: '{0} Upload', UPLOAD: 'Upload', - DOWNLOAD: 'Download', + DOWNLOAD: '{{D|d|d}}ownload', ABORTED: 'aborted', FAILED: 'failed', SUCCESSFUL: 'successful', @@ -182,7 +181,7 @@ const en: Translation = { LOG_OF: '{0} Log', STATUS_OF: '{0} Status', UPLOAD_DOWNLOAD: 'Upload/Download', - SYSTEM_VERSION_RUNNING: 'You are currently running version', + VERSION_ON: 'You are currently on', SYSTEM_APPLY_FIRMWARE: 'to apply the new firmware', CLOSE: 'Close', USE: 'Use', @@ -193,7 +192,7 @@ const en: Translation = { THE_LATEST: 'The latest', OFFICIAL: 'official', DEVELOPMENT: 'development', - VERSION_IS: 'version is', + RELEASE_IS: 'release is', RELEASE_NOTES: 'release notes', EMS_ESP_VER: 'EMS-ESP Version', PLATFORM: 'Device (Platform / SDK)', @@ -315,13 +314,17 @@ const en: Translation = { SCHEDULE: 'Schedule', TIME: 'Time', TIMER: 'Timer', - SCHEDULE_SAVED: 'Schedule updated', + SCHEDULE_UPDATED: 'Schedule updated', SCHEDULE_TIMER_1: 'on startup', SCHEDULE_TIMER_2: 'every minute', SCHEDULE_TIMER_3: 'every hour', - CUSTOM_ENTITIES: 'Custom entities', - ENTITIES_HELP_1: 'Fetch custom entities from the EMS-bus', - WRITEABLE: 'Writeable' + CUSTOM_ENTITIES: 'Custom Entities', + ENTITIES_HELP_1: 'Fetch custom entities from the EMS bus', + ENTITIES_UPDATED: 'Entities Updated', + WRITEABLE: 'Writeable', + SHOWING: 'Showing', + SEARCH: 'Search' + }; export default en; diff --git a/interface/src/i18n/formatters.ts b/interface/src/i18n/formatters.ts index b9d999653..6818d3da7 100644 --- a/interface/src/i18n/formatters.ts +++ b/interface/src/i18n/formatters.ts @@ -1,7 +1,7 @@ -import type { FormattersInitializer } from 'typesafe-i18n'; import type { Locales, Formatters } from './i18n-types'; +import type { FormattersInitializer } from 'typesafe-i18n'; -export const initFormatters: FormattersInitializer = (locale: Locales) => { +export const initFormatters: FormattersInitializer = () => { const formatters: Formatters = { // add your formatter functions here }; diff --git a/interface/src/i18n/fr/index.ts b/interface/src/i18n/fr/index.ts index 4931886eb..26b7a1cd1 100644 --- a/interface/src/i18n/fr/index.ts +++ b/interface/src/i18n/fr/index.ts @@ -37,15 +37,14 @@ const fr: Translation = { BRAND: 'Marque', ENTITY_NAME: 'Nom de l\'entité', VALUE: 'Valeur', - SHOW_FAV: 'ne montrer que les favoris', - DEVICE_SENSOR_DATA: 'Données des appareils et capteurs', - DEVICES_SENSORS: 'Appareils et capteurs', - ATTACHED_SENSORS: 'Capteurs EMS-ESP connectés', + DEVICE_DATA: 'Données des appareils', + SENSOR_DATA: 'Données des capteurs', + DEVICES: 'Appareils', + SENSORS: 'Capteurs', RUN_COMMAND: 'Lancer une commande', CHANGE_VALUE: 'Changer la valeur', CANCEL: 'Annuler', RESET: 'Réinitialiser', - SEND: 'Envoyer', APPLY_CHANGES: 'Apply Changes ({0})', // TODO translate UPDATE: 'Update', // TODO translate REMOVE: 'Enlever', @@ -174,7 +173,7 @@ const fr: Translation = { SUPPORT_INFO: 'Information de support', UPLOAD_OF: 'Upload de {0}', UPLOAD: 'Upload', - DOWNLOAD: 'Download', + DOWNLOAD: '{{D|d|d}}ownload', ABORTED: 'annulé', FAILED: 'échoué', SUCCESSFUL: 'réussi', @@ -182,7 +181,7 @@ const fr: Translation = { LOG_OF: '{0} Log', STATUS_OF: 'Statut {0}', UPLOAD_DOWNLOAD: 'Upload/Download', - SYSTEM_VERSION_RUNNING: 'Vous utilisez actuellement la version', + VERSION_ON: 'You are currently on', // TODO translate SYSTEM_APPLY_FIRMWARE: 'pour appliquer le nouveau firmware', CLOSE: 'Fermer', USE: 'Utiliser', @@ -193,7 +192,7 @@ const fr: Translation = { THE_LATEST: 'La dernière', OFFICIAL: 'officielle', DEVELOPMENT: 'développement', - VERSION_IS: 'version est', + RELEASE_IS: 'release est', // TODO translate RELEASE_NOTES: 'notes de version', EMS_ESP_VER: 'Version EMS-ESP', PLATFORM: 'Appareil (Plateforme / SDK)', @@ -315,13 +314,16 @@ const fr: Translation = { SCHEDULE: 'Schedule', // TODO translate TIME: 'Time', // TODO translate TIMER: 'Timer', // TODO translate - SCHEDULE_SAVED: 'Schedule updated', // TODO translate + SCHEDULE_UPDATED: 'Schedule updated', // TODO translate SCHEDULE_TIMER_1: 'on startup', // TODO translate 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 - WRITEABLE: 'Writeable' // TODO translate + CUSTOM_ENTITIES: 'Custom Entities', // TODO translate + ENTITIES_HELP_1: 'Fetch custom entities from the EMS bus', // TODO translate + ENTITIES_UPDATED: 'Entities Updated', // TODO translate + WRITEABLE: 'Writeable', // TODO translate + SHOWING: 'Showing', // TODO translate + SEARCH: 'Search' // TODO translate }; export default fr; diff --git a/interface/src/i18n/nl/index.ts b/interface/src/i18n/nl/index.ts index 42e063261..f99466172 100644 --- a/interface/src/i18n/nl/index.ts +++ b/interface/src/i18n/nl/index.ts @@ -37,15 +37,14 @@ const nl: Translation = { BRAND: 'Merk', ENTITY_NAME: 'Entiteit', VALUE: '{{Waarde|waarde}}', - SHOW_FAV: 'alleen favorieten weergeven', - DEVICE_SENSOR_DATA: 'Apparaat en Sensor data', - DEVICES_SENSORS: 'Apparaten & Sensoren', - ATTACHED_SENSORS: 'Aangesloten EMS-ESP sensoren', + SENSOR_DATA: 'Sensor data', + DEVICE_DATA: 'Apparaat data', + DEVICES: 'Apparaten', + SENSORS: 'Sensoren', RUN_COMMAND: 'Call commando', CHANGE_VALUE: 'Wijzig waarde', CANCEL: 'Annuleren', RESET: 'Reset', - SEND: 'Verzenden', APPLY_CHANGES: 'Apply Changes ({0})', // TODO translate UPDATE: 'Update', // TODO translate REMOVE: 'Verwijderen', @@ -136,7 +135,7 @@ const nl: Translation = { BOOLEAN_FORMAT_API: 'Boolean formaat API/MQTT', ENUM_FORMAT: 'Enum formaat API/MQTT', INDEX: 'Index', - ENABLE_PARASITE: 'Activeer Dallas parasitaire modus', + ENABLE_PARASITE: 'Activeer parasitaire modus', LOGGING: 'Logging', LOG_HEX: 'Log EMS telegrammen in hexadecimaal', ENABLE_SYSLOG: 'Activeer Syslog', @@ -174,7 +173,7 @@ const nl: Translation = { SUPPORT_INFO: 'Support Info', UPLOAD_OF: '{0} Upload', UPLOAD: 'Upload', - DOWNLOAD: 'Download', + DOWNLOAD: '{{D|d|d}}ownload', ABORTED: 'afgebroken', FAILED: 'mislukt', SUCCESSFUL: 'successvol', @@ -182,7 +181,7 @@ const nl: Translation = { LOG_OF: '{0} Log', STATUS_OF: '{0} Status', UPLOAD_DOWNLOAD: 'Upload/Download', - SYSTEM_VERSION_RUNNING: 'op dit moment draai je versie', + VERSION_ON: 'You are currently on', // TODO translate SYSTEM_APPLY_FIRMWARE: 'om de nieuwe firmware te activeren', CLOSE: 'Sluiten', USE: 'Gebruik', @@ -193,7 +192,7 @@ const nl: Translation = { THE_LATEST: 'De laatste', OFFICIAL: 'official', DEVELOPMENT: 'development', - VERSION_IS: 'versie is', + RELEASE_IS: 'release is', RELEASE_NOTES: 'release notes', EMS_ESP_VER: 'EMS-ESP Version', PLATFORM: 'Apparaat (Platform / SDK)', @@ -315,13 +314,17 @@ const nl: Translation = { SCHEDULE: 'Schedule', // TODO translate TIME: 'Time', // TODO translate TIMER: 'Timer', // TODO translate - SCHEDULE_SAVED: 'Schedule updated', // TODO translate + SCHEDULE_UPDATED: 'Schedule updated', // TODO translate SCHEDULE_TIMER_1: 'on startup', // TODO translate SCHEDULE_TIMER_2: 'every minute', // TODO translate SCHEDULE_TIMER_3: 'every hour', // TODO translate - CUSTOM_ENTITIES: 'Custom entities', - ENTITIES_HELP_1: 'Fetch custom entities from the EMS-bus', - WRITEABLE: 'Writeable' // TODO translate + CUSTOM_ENTITIES: 'Custom Entities', // TODO translate + ENTITIES_HELP_1: 'Fetch custom entities from the EMS bus', // TODO translate + ENTITIES_UPDATED: 'Entities Updated', // TODO translate + WRITEABLE: 'Writeable', // TODO translate + SHOWING: 'Showing', // TODO translate + SEARCH: 'Zoek' + }; export default nl; diff --git a/interface/src/i18n/no/index.ts b/interface/src/i18n/no/index.ts index cb540a082..6385064d7 100644 --- a/interface/src/i18n/no/index.ts +++ b/interface/src/i18n/no/index.ts @@ -37,15 +37,14 @@ const no: Translation = { BRAND: 'Fabrikat', ENTITY_NAME: 'Objektsnavn', VALUE: '{{Verdi|verdi}}', - SHOW_FAV: ' Vis kun favoritter', - DEVICE_SENSOR_DATA: 'Enheter og Sensordata', - DEVICES_SENSORS: 'Enheter og Sensorer', - ATTACHED_SENSORS: 'Tilkoblede EMS-ESP Sensorer', + DEVICE_DATA: 'Enheterdata', + SENSOR_DATA: 'Sensordata', + DEVICES: 'Enheter', + SENSORS: 'Sensorer', RUN_COMMAND: 'Kjør kommando', CHANGE_VALUE: 'Endre Verdi', CANCEL: 'Avbryt', RESET: 'Nullstill', - SEND: 'Send', APPLY_CHANGES: 'Utfør endringer({0})', UPDATE: 'Oppdater', REMOVE: 'Fjern', @@ -174,7 +173,7 @@ const no: Translation = { SUPPORT_INFO: 'Supportinfo', UPLOAD_OF: '{0} Opplasning', UPLOAD: 'Opplasning', - DOWNLOAD: 'Nedlasting', + DOWNLOAD: '{{N|n|n}}edlasting', ABORTED: 'avbrutt', FAILED: 'feilet', SUCCESSFUL: 'vellykket', @@ -182,7 +181,7 @@ const no: Translation = { LOG_OF: '{0} Logg', STATUS_OF: '{0} Status', UPLOAD_DOWNLOAD: 'Opp/Nedlasting', - SYSTEM_VERSION_RUNNING: 'Du benytter versjon', + VERSION_ON: 'You are currently on', // TODO translate SYSTEM_APPLY_FIRMWARE: 'for å aktivere ny firmware', CLOSE: 'Steng', USE: 'Bruk', @@ -193,7 +192,7 @@ const no: Translation = { THE_LATEST: 'Den nyeste', OFFICIAL: 'official', DEVELOPMENT: 'development', - VERSION_IS: 'versjonen er', + RELEASE_IS: 'release er', RELEASE_NOTES: 'release notes', EMS_ESP_VER: 'EMS-ESP Version', PLATFORM: 'Enhet (Platform / SDK)', @@ -315,13 +314,17 @@ const no: Translation = { SCHEDULE: 'Planlegg', TIME: 'Tid', TIMER: 'Timer', - SCHEDULE_SAVED: 'Planlegger er oppdatert', + SCHEDULE_UPDATED: 'Planlegger er oppdatert', SCHEDULE_TIMER_1: 'ved oppstart', SCHEDULE_TIMER_2: 'hvert minutt', SCHEDULE_TIMER_3: 'hver time', - CUSTOM_ENTITIES: 'Custom entities', - ENTITIES_HELP_1: 'Fetch custom entities from the EMS-bus', - WRITEABLE: 'Writeable' // TODO translate + CUSTOM_ENTITIES: 'Custom Entities', // TODO translate + ENTITIES_HELP_1: 'Fetch custom entities from the EMS bus', // TODO translate + ENTITIES_UPDATED: 'Entities Updated', // TODO translate + WRITEABLE: 'Writeable', // TODO translate + SHOWING: 'Showing', // TODO translate + SEARCH: 'Search' // TODO translate + }; export default no; diff --git a/interface/src/i18n/pl/index.ts b/interface/src/i18n/pl/index.ts index 0cde3ab06..73b9c8689 100644 --- a/interface/src/i18n/pl/index.ts +++ b/interface/src/i18n/pl/index.ts @@ -24,7 +24,7 @@ const pl: BaseTranslation = { SECURITY: '{{B|b|}}ezpieczeństw{{o|a|}}', ONOFF_CAP: 'wł./wył.', ONOFF: 'włączono/wyłączono', - TYPE: 'Typ', + TYPE: '{{T|t|}}yp{{|u|}}', DESCRIPTION: 'Opis', ENTITIES: 'Encje', REFRESH: 'Odśwież', @@ -37,15 +37,14 @@ const pl: BaseTranslation = { VERSION: 'Wersja', ENTITY_NAME: 'Nazwa encji', VALUE: '{{W|w|}}artość', - SHOW_FAV: 'Pokaż tylko "ulubione"', - DEVICE_SENSOR_DATA: 'Dane z urządzeń i czujników', - DEVICES_SENSORS: 'Urządzenia i czujniki', - ATTACHED_SENSORS: 'Urządzenia podłączone do EMS-ESP (czujniki temperatury/analogowe/cyfrowe, wyjścia cyfrowe)', + DEVICE_DATA: 'Dane z urządzeń', + SENSOR_DATA: 'Dane z czujników', + DEVICES: 'Urządzenia', + SENSORS: 'Czujniki', RUN_COMMAND: 'Wykonaj komendę', CHANGE_VALUE: 'Zmień wartość', CANCEL: 'Anuluj', RESET: 'Reset{{uj|owanie|}}', - SEND: 'Wyślij', APPLY_CHANGES: 'Zapisz zmiany ({0})', UPDATE: 'Uaktualnij', REMOVE: 'Usuń', @@ -182,7 +181,7 @@ const pl: BaseTranslation = { LOG_OF: 'Log {0}', STATUS_OF: 'Status {0}', UPLOAD_DOWNLOAD: 'Przesyłanie plików', - SYSTEM_VERSION_RUNNING: 'Obecnie zainstalowana wersja to:', + VERSION_ON: 'You are currently on', // TODO translate SYSTEM_APPLY_FIRMWARE: '', CLOSE: 'Zamknij', USE: 'Aby zaktualizować firmware skorzystaj z funkcji', @@ -193,7 +192,7 @@ const pl: BaseTranslation = { THE_LATEST: 'Najnowsza', OFFICIAL: 'oficjalna', DEVELOPMENT: 'testowa', - VERSION_IS: 'wersja to', + RELEASE_IS: 'release to', // TODO translate RELEASE_NOTES: 'lista zmian', EMS_ESP_VER: 'Wersja EMS-ESP', PLATFORM: 'Urządzenie (platforma / SDK)', @@ -240,7 +239,7 @@ const pl: BaseTranslation = { MQTT_RESPONSE: 'Rezultat wykonania komendy publikuj w temacie "response"', MQTT_PUBLISH_TEXT_1: 'Tematy z pojedynczą wartością publikuj po jej zmianie', MQTT_PUBLISH_TEXT_2: 'Publikuj w tematach "command" (ioBroker)', - MQTT_PUBLISH_TEXT_3: 'Włącz opcję "MQTT discovery', + MQTT_PUBLISH_TEXT_3: 'Włącz opcję "MQTT discovery"', MQTT_PUBLISH_TEXT_4: 'Prefiks dla "MQTT discovery"', MQTT_PUBLISH_TEXT_5: 'Typ "MQTT discovery"', MQTT_PUBLISH_INTERVALS: 'Interwały publikowania', @@ -287,7 +286,7 @@ const pl: BaseTranslation = { NETWORK_BLANK_SSID: 'pozostaw puste aby wyłączyć WiFi', TX_POWER: 'Moc nadawania', HOSTNAME: 'Nazwa w sieci', - NETWORK_DISABLE_SLEEP: 'Wyłącz tryb usypiania WiFi', + NETWORK_DISABLE_SLEEP: 'Wyłącz tryb uśpienia WiFi', NETWORK_LOW_BAND: 'Używaj mniejszej szerokości pasma WiFi (20MHz)', NETWORK_USE_DNS: 'Włącz wsparcie dla mDNS', NETWORK_ENABLE_CORS: 'Włącz wsparcie dla CORS', @@ -300,7 +299,7 @@ const pl: BaseTranslation = { ADDRESS_OF: 'Adres {0}', ADMIN: 'Użytkownik "administrator".', GUEST: 'Użytkownik "gość".', - NEW: 'nowego', + NEW: 'nowe{{go|j|}}', NEW_NAME_OF: 'Nowa nazwa {0}', ENTITY: 'encji', MIN: 'Min.', @@ -315,13 +314,17 @@ const pl: BaseTranslation = { SCHEDULE: '{{H|h|}}armonogram{{|u|}}', TIME: '{{Zegar|Godzina|}}', TIMER: '{{m|M|}}inutnik', - SCHEDULE_SAVED: 'Harmonogram został uaktualniony.', + SCHEDULE_UPDATED: 'Harmonogram został uaktualniony.', SCHEDULE_TIMER_1: 'przy starcie', SCHEDULE_TIMER_2: 'co minutę', SCHEDULE_TIMER_3: 'co godzinę', - CUSTOM_ENTITIES: 'Custom entities', - ENTITIES_HELP_1: 'Fetch custom entities from the EMS-bus', - WRITEABLE: 'Writeable' // TODO translate + CUSTOM_ENTITIES: '{{N|n|}}iestandardowe{{|j|}} encj{{e|i|}}', + ENTITIES_HELP_1: 'Zdefiniuj niestandardowe encje pobierane z magistrali EMS.', + ENTITIES_UPDATED: 'Niestandardowe encje zostały uaktualnione.', + WRITEABLE: 'Writeable', // TODO translate + SHOWING: 'Showing', // TODO translate + SEARCH: 'Search' // TODO translate + }; export default pl; diff --git a/interface/src/i18n/sv/index.ts b/interface/src/i18n/sv/index.ts index ff8231513..0fa2a02e6 100644 --- a/interface/src/i18n/sv/index.ts +++ b/interface/src/i18n/sv/index.ts @@ -37,15 +37,14 @@ const sv: Translation = { BRAND: 'Fabrikat', ENTITY_NAME: 'Entitetsnamn', VALUE: '{{Värde|värde}}', - SHOW_FAV: 'Visa enbart favoriter', - DEVICE_SENSOR_DATA: 'Enhets och Sensor-data', - DEVICES_SENSORS: 'Enheter & Sensorer', - ATTACHED_SENSORS: 'Anslutna EMS-ESP Sensorer', + DEVICE_DATA: 'Enhets data', + SENSOR_DATA: 'Sensor data', + DEVICES: 'Enheter', + SENSORS: 'Sensorer', RUN_COMMAND: 'Kör Kommando', CHANGE_VALUE: 'Ändra Värde', CANCEL: 'Avbryt', RESET: 'Nollställ', - SEND: 'Skicka', APPLY_CHANGES: 'Apply Changes ({0})', // TODO translate UPDATE: 'Update', // TODO translate REMOVE: 'Ta bort', @@ -174,7 +173,7 @@ const sv: Translation = { SUPPORT_INFO: 'Supportinfo', UPLOAD_OF: '{0} Uppladdning', UPLOAD: 'Uppladdning', - DOWNLOAD: 'Nedladdning', + DOWNLOAD: '{{N|n|n}}edladdning', ABORTED: 'Avbruten', FAILED: 'Misslyckades', SUCCESSFUL: 'Lyckades', @@ -182,7 +181,7 @@ const sv: Translation = { LOG_OF: '{0} Logg', STATUS_OF: '{0} Status', UPLOAD_DOWNLOAD: 'Upp/Nedladdning', - SYSTEM_VERSION_RUNNING: 'Du använder version', + VERSION_ON: 'You are currently on', // TODO translate SYSTEM_APPLY_FIRMWARE: 'för att aktivera ny firmware', CLOSE: 'Stäng', USE: 'Använd', @@ -193,7 +192,7 @@ const sv: Translation = { THE_LATEST: 'Den senaste', OFFICIAL: 'officiell', DEVELOPMENT: 'utveckling', - VERSION_IS: 'version är', + RELEASE_IS: 'release är', // TODO translate RELEASE_NOTES: 'release-logg', EMS_ESP_VER: 'EMS-ESP Version', PLATFORM: 'Enhet (Plattform / SDK)', @@ -315,13 +314,17 @@ const sv: Translation = { SCHEDULE: 'Schedule', // TODO translate TIME: 'Time', // TODO translate TIMER: 'Timer', // TODO translate - SCHEDULE_SAVED: 'Schedule updated', // TODO translate + SCHEDULE_UPDATED: 'Schedule updated', // TODO translate SCHEDULE_TIMER_1: 'on startup', // TODO translate SCHEDULE_TIMER_2: 'every minute', // TODO translate SCHEDULE_TIMER_3: 'every hour', // TODO translate - CUSTOM_ENTITIES: 'Custom entities', - ENTITIES_HELP_1: 'Fetch custom entities from the EMS-bus', - WRITEABLE: 'Writeable' // TODO translate + CUSTOM_ENTITIES: 'Custom Entities', // TODO translate + ENTITIES_HELP_1: 'Fetch custom entities from the EMS bus', // TODO translate + ENTITIES_UPDATED: 'Entities Updated', // TODO translate + WRITEABLE: 'Writeable', // TODO translate + SHOWING: 'Showing', // TODO translate + SEARCH: 'Search' // TODO translate + }; export default sv; diff --git a/interface/src/i18n/tr/index.ts b/interface/src/i18n/tr/index.ts index 21cbdaa7d..46598d4fd 100644 --- a/interface/src/i18n/tr/index.ts +++ b/interface/src/i18n/tr/index.ts @@ -37,15 +37,14 @@ const tr: Translation = { BRAND: 'Marka', ENTITY_NAME: 'Valık Adı', VALUE: '{{Değer|değer}}', - SHOW_FAV: 'sadece favorileri göster', - DEVICE_SENSOR_DATA: 'Cihaz ve Sensör Bilgisi', - DEVICES_SENSORS: 'Cihazlar & Sensörler', - ATTACHED_SENSORS: 'Eklenmiş EMS-ESP Sensörler', + DEVICE_DATA: 'Cihaz Bilgisi', + SENSOR_DATA: 'Sensör Bilgisi', + DEVICES: 'Cihazlar', + SENSORS: 'Sensörler', RUN_COMMAND: 'Çalıştırma Komutu', CHANGE_VALUE: 'Değeri Değiştir', CANCEL: 'İptal', RESET: 'Reset', - SEND: 'Gönder', APPLY_CHANGES: 'Apply Changes ({0})', UPDATE: 'Update', REMOVE: 'Kaldır', @@ -174,7 +173,7 @@ const tr: Translation = { SUPPORT_INFO: 'Destek Bilgisi', UPLOAD_OF: '{0} Yüklemesi', UPLOAD: 'Yükleme', - DOWNLOAD: 'İndirme', + DOWNLOAD: '{{İ|i|i}}İndirme', ABORTED: 'iptal edildi', FAILED: 'başarısız', SUCCESSFUL: 'başarılı', @@ -182,7 +181,7 @@ const tr: Translation = { LOG_OF: '{0} Kaydı', STATUS_OF: '{0} Durumu', UPLOAD_DOWNLOAD: 'Yükleme/İndirme', - SYSTEM_VERSION_RUNNING: 'Şu anda çalıştırdığınız sürüm', + VERSION_ON: 'You are currently on', // TODO translate SYSTEM_APPLY_FIRMWARE: 'yeni bellenimi uygulamak için', CLOSE: 'Kapat', USE: 'KUllan', @@ -193,7 +192,7 @@ const tr: Translation = { THE_LATEST: 'En son', OFFICIAL: 'resmi', DEVELOPMENT: 'geliştirme', - VERSION_IS: 'sürüm: ', + RELEASE_IS: 'release is', // TODO translate RELEASE_NOTES: 'yayınlanma notları', EMS_ESP_VER: 'EMS-ESP Sürümü', PLATFORM: 'Cihaz (Platform / SDK)', @@ -315,13 +314,17 @@ const tr: Translation = { SCHEDULE: 'Schedule', // TODO translate TIME: 'Time', // TODO translate TIMER: 'Timer', // TODO translate - SCHEDULE_SAVED: 'Schedule updated', // TODO translate + SCHEDULE_UPDATED: 'Schedule updated', // TODO translate SCHEDULE_TIMER_1: 'on startup', // TODO translate SCHEDULE_TIMER_2: 'every minute', // TODO translate SCHEDULE_TIMER_3: 'every hour', // TODO translate - CUSTOM_ENTITIES: 'Custom entities', - ENTITIES_HELP_1: 'Fetch custom entities from the EMS-bus', - WRITEABLE: 'Writeable' // TODO translate + CUSTOM_ENTITIES: 'Custom Entities', // TODO translate + ENTITIES_HELP_1: 'Fetch custom entities from the EMS bus', // TODO translate + ENTITIES_UPDATED: 'Entities Updated', // TODO translate + WRITEABLE: 'Writeable', // TODO translate + SHOWING: 'Showing', // TODO translate + SEARCH: 'Search' // TODO translate + }; export default tr; diff --git a/interface/src/project/Dashboard.tsx b/interface/src/project/Dashboard.tsx index c2c1abaa5..bfbaa8923 100644 --- a/interface/src/project/Dashboard.tsx +++ b/interface/src/project/Dashboard.tsx @@ -1,15 +1,16 @@ -import { FC } from 'react'; +import { Tab } from '@mui/material'; import { Navigate, Route, Routes } from 'react-router-dom'; -import { Tab } from '@mui/material'; +import DashboardDevices from './DashboardDevices'; +import DashboardSensors from './DashboardSensors'; +import DashboardStatus from './DashboardStatus'; + +import type { FC } from 'react'; import { RouterTabs, useRouterTab, useLayoutTitle } from 'components'; import { useI18nContext } from 'i18n/i18n-react'; -import DashboardStatus from './DashboardStatus'; -import DashboardData from './DashboardData'; - const Dashboard: FC = () => { const { routerTab } = useRouterTab(); @@ -19,13 +20,15 @@ const Dashboard: FC = () => { return ( <> - + + - } /> + } /> + } /> } /> - } /> + } /> ); diff --git a/interface/src/project/DashboardData.tsx b/interface/src/project/DashboardData.tsx deleted file mode 100644 index 15ffcc6cf..000000000 --- a/interface/src/project/DashboardData.tsx +++ /dev/null @@ -1,1263 +0,0 @@ -import { FC, useState, useContext, useCallback, useEffect } from 'react'; - -import { - Button, - Typography, - Box, - Dialog, - DialogTitle, - DialogContent, - DialogActions, - MenuItem, - InputAdornment, - FormHelperText, - IconButton, - List, - ListItem, - ListItemText, - Grid, - FormControlLabel, - Checkbox -} from '@mui/material'; - -import { toast } from 'react-toastify'; - -import { useTheme } from '@table-library/react-table-library/theme'; -import { useSort, SortToggleType } from '@table-library/react-table-library/sort'; -import { Table, Header, HeaderRow, HeaderCell, Body, Row, Cell } from '@table-library/react-table-library/table'; -import { useRowSelect } from '@table-library/react-table-library/select'; - -import DownloadIcon from '@mui/icons-material/GetApp'; -import RefreshIcon from '@mui/icons-material/Refresh'; -import EditIcon from '@mui/icons-material/Edit'; -import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined'; -import AddCircleOutlineOutlinedIcon from '@mui/icons-material/AddCircleOutlineOutlined'; -import CancelIcon from '@mui/icons-material/Cancel'; -import SendIcon from '@mui/icons-material/TrendingFlat'; -import WarningIcon from '@mui/icons-material/Warning'; -import RemoveIcon from '@mui/icons-material/RemoveCircleOutline'; -import PlayArrowIcon from '@mui/icons-material/PlayArrow'; -import EditOffOutlinedIcon from '@mui/icons-material/EditOffOutlined'; -import CommentsDisabledOutlinedIcon from '@mui/icons-material/CommentsDisabledOutlined'; -import KeyboardArrowUpOutlinedIcon from '@mui/icons-material/KeyboardArrowUpOutlined'; -import KeyboardArrowDownOutlinedIcon from '@mui/icons-material/KeyboardArrowDownOutlined'; -import UnfoldMoreOutlinedIcon from '@mui/icons-material/UnfoldMoreOutlined'; -import StarIcon from '@mui/icons-material/Star'; - -import DeviceIcon from './DeviceIcon'; - -import { IconContext } from 'react-icons'; - -import { AuthenticatedContext } from 'contexts/authentication'; - -import { ButtonRow, ValidatedTextField, SectionContent, MessageBox } from 'components'; - -import * as EMSESP from './api'; - -import { numberValue, updateValue, extractErrorMessage } from 'utils'; - -import { - SensorData, - Device, - CoreData, - DeviceData, - DeviceValue, - DeviceValueUOM, - DeviceValueUOM_s, - AnalogType, - AnalogTypeNames, - Sensor, - Analog, - DeviceEntityMask -} from './types'; - -import { useI18nContext } from 'i18n/i18n-react'; - -const DashboardData: FC = () => { - const { me } = useContext(AuthenticatedContext); - - const { LL } = useI18nContext(); - - const [coreData, setCoreData] = useState({ - connected: true, - devices: [], - s_n: '', - active_sensors: 0, - analog_enabled: false - }); - - const [deviceData, setDeviceData] = useState({ label: '', data: [] }); - const [sensorData, setSensorData] = useState({ sensors: [], analogs: [] }); - const [deviceValue, setDeviceValue] = useState(); - const [sensor, setSensor] = useState(); - const [analog, setAnalog] = useState(); - const [deviceDialog, setDeviceDialog] = useState(-1); - const [onlyFav, setOnlyFav] = useState(false); - - const common_theme = useTheme({ - BaseRow: ` - font-size: 14px; - `, - HeaderRow: ` - text-transform: uppercase; - background-color: black; - color: #90CAF9; - .th { - border-bottom: 1px solid #565656; - } - `, - Row: ` - background-color: #1e1e1e; - position: relative; - cursor: pointer; - .td { - padding: 8px; - border-top: 1px solid #565656; - border-bottom: 1px solid #565656; - } - &.tr.tr-body.row-select.row-select-single-selected { - background-color: #3d4752; - font-weight: normal; - } - &:hover .td { - border-top: 1px solid #177ac9; - border-bottom: 1px solid #177ac9; - } - `, - Cell: ` - &:last-of-type { - text-align: right; - }, - ` - }); - - const device_theme = useTheme([ - common_theme, - { - Table: ` - --data-table-library_grid-template-columns: 40px 160px repeat(1, minmax(0, 1fr)) 100px 40px; - `, - BaseRow: ` - .td { - height: 42px; - } - `, - BaseCell: ` - &:nth-of-type(2) { - text-align: left; - }, - &:nth-of-type(4) { - text-align: center; - } - `, - HeaderRow: ` - .th { - padding: 8px; - height: 36px; - ` - } - ]); - - const data_theme = useTheme([ - common_theme, - { - Table: ` - --data-table-library_grid-template-columns: minmax(0, 1fr) 35% 40px; - `, - BaseRow: ` - .td { - height: 32px; - } - `, - BaseCell: ` - &:nth-of-type(2) { - text-align: right; - }, - `, - HeaderRow: ` - .th { - height: 36px; - } - `, - Row: ` - &:nth-of-type(odd) .td { - background-color: #303030; - } - ` - } - ]); - - const temperature_theme = useTheme([data_theme]); - - const analog_theme = useTheme([ - data_theme, - { - Table: ` - --data-table-library_grid-template-columns: 80px repeat(1, minmax(0, 1fr)) 120px 100px 40px; - `, - BaseCell: ` - &:nth-of-type(2) { - text-align: left; - }, - &:nth-of-type(4) { - text-align: right; - } - ` - } - ]); - - const getSortIcon = (state: any, sortKey: any) => { - if (state.sortKey === sortKey && state.reverse) { - return ; - } - if (state.sortKey === sortKey && !state.reverse) { - return ; - } - return ; - }; - - const analog_sort = useSort( - { nodes: sensorData.analogs }, - {}, - { - sortIcon: { - iconDefault: , - iconUp: , - iconDown: - }, - sortToggleType: SortToggleType.AlternateWithReset, - sortFns: { - GPIO: (array) => array.sort((a, b) => a.g - b.g), - NAME: (array) => array.sort((a, b) => a.n.localeCompare(b.n)), - TYPE: (array) => array.sort((a, b) => a.t - b.t) - } - } - ); - - const sensor_sort = useSort( - { nodes: sensorData.sensors }, - {}, - { - sortIcon: { - iconDefault: , - iconUp: , - iconDown: - }, - sortToggleType: SortToggleType.AlternateWithReset, - sortFns: { - NAME: (array) => array.sort((a, b) => a.n.localeCompare(b.n)), - TEMPERATURE: (array) => array.sort((a, b) => a.t - b.t) - } - } - ); - - const dv_sort = useSort( - { nodes: deviceData.data }, - {}, - { - sortIcon: { - iconDefault: , - iconUp: , - iconDown: - }, - sortToggleType: SortToggleType.AlternateWithReset, - sortFns: { - NAME: (array) => array.sort((a, b) => a.id.slice(2).localeCompare(b.id.slice(2))), - VALUE: (array) => array.sort((a, b) => a.v.toString().localeCompare(b.v.toString())) - } - } - ); - - const device_select = useRowSelect( - { nodes: coreData.devices }, - { - onChange: onSelectChange - } - ); - - const fetchSensorData = async () => { - try { - setSensorData((await EMSESP.readSensorData()).data); - } catch (error) { - toast.error(extractErrorMessage(error, LL.PROBLEM_LOADING())); - } - }; - - const fetchDeviceData = async (id: string) => { - const unique_id = parseInt(id); - try { - setDeviceData((await EMSESP.readDeviceData({ id: unique_id })).data); - } catch (error) { - toast.error(extractErrorMessage(error, LL.PROBLEM_LOADING())); - } - }; - - const fetchCoreData = useCallback(async () => { - try { - setCoreData((await EMSESP.readCoreData()).data); - } catch (error) { - toast.error(extractErrorMessage(error, LL.PROBLEM_LOADING())); - } - }, [LL]); - - useEffect(() => { - fetchCoreData(); - }, [fetchCoreData]); - - const refreshDataIndex = (selectedDevice: string) => { - if (selectedDevice === 'sensor') { - fetchSensorData(); - return; - } - - setSensorData({ sensors: [], analogs: [] }); - if (selectedDevice) { - fetchDeviceData(selectedDevice); - } else { - fetchCoreData(); - } - }; - - const refreshData = () => { - refreshDataIndex(device_select.state.id); - }; - - function onSelectChange(action: any, state: any) { - if (action.type === 'ADD_BY_ID_EXCLUSIVELY') { - refreshData(); - } else { - setSensorData({ sensors: [], analogs: [] }); - } - } - - const escapeCsvCell = (cell: any) => { - if (cell == null) { - return ''; - } - const sc = cell.toString().trim(); - if (sc === '' || sc === '""') { - return sc; - } - if (sc.includes('"') || sc.includes(';') || sc.includes('\n') || sc.includes('\r')) { - return '"' + sc.replace(/"/g, '""') + '"'; - } - return sc; - }; - - const makeCsvData = (columns: any, data: any) => { - return data.reduce((csvString: any, rowItem: any) => { - return csvString + columns.map(({ accessor }: any) => escapeCsvCell(accessor(rowItem))).join(';') + '\r\n'; - }, columns.map(({ name }: any) => escapeCsvCell(name)).join(';') + '\r\n'); - }; - - const downloadAsCsv = (columns: any, data: any, filename: string) => { - const csvData = makeCsvData(columns, data); - const csvFile = new Blob([csvData], { type: 'text/csv;charset:utf-8' }); - const downloadLink = document.createElement('a'); - - downloadLink.download = filename; - downloadLink.href = window.URL.createObjectURL(csvFile); - document.body.appendChild(downloadLink); - downloadLink.click(); - document.body.removeChild(downloadLink); - }; - - const hasMask = (id: string, mask: number) => (parseInt(id.slice(0, 2), 16) & mask) === mask; - - const handleDownloadCsv = () => { - const columns = [ - { accessor: (dv: any) => dv.id.slice(2), name: LL.ENTITY_NAME() }, - { - accessor: (dv: any) => (typeof dv.v === 'number' ? new Intl.NumberFormat().format(dv.v) : dv.v), - name: LL.VALUE(0) - }, - { accessor: (dv: any) => DeviceValueUOM_s[dv.u], name: 'UoM' } - ]; - downloadAsCsv( - columns, - onlyFav ? deviceData.data.filter((dv) => hasMask(dv.id, DeviceEntityMask.DV_FAVORITE)) : deviceData.data, - 'device_entities' - ); - }; - - useEffect(() => { - const timer = setInterval(() => refreshData(), 60000); - return () => { - clearInterval(timer); - }; - // eslint-disable-next-line - }, [analog, sensor, deviceValue, sensorData]); - - const isCmdOnly = (dv: DeviceValue) => dv.v === '' && dv.c; - - const formatDurationMin = (duration_min: number) => { - const days = Math.trunc((duration_min * 60000) / 86400000); - const hours = Math.trunc((duration_min * 60000) / 3600000) % 24; - const minutes = Math.trunc((duration_min * 60000) / 60000) % 60; - - let formatted = ''; - if (days) { - formatted += LL.NUM_DAYS({ num: days }) + ' '; - } - if (hours) { - formatted += LL.NUM_HOURS({ num: hours }) + ' '; - } - if (minutes) { - formatted += LL.NUM_MINUTES({ num: minutes }); - } - return formatted; - }; - - function formatValue(value: any, uom: number) { - if (value === undefined) { - return ''; - } - switch (uom) { - case DeviceValueUOM.HOURS: - return value ? formatDurationMin(value * 60) : LL.NUM_HOURS({ num: 0 }); - case DeviceValueUOM.MINUTES: - return value ? formatDurationMin(value) : LL.NUM_MINUTES({ num: 0 }); - case DeviceValueUOM.SECONDS: - return LL.NUM_SECONDS({ num: value }); - case DeviceValueUOM.NONE: - if (typeof value === 'number') { - return new Intl.NumberFormat().format(value); - } - return value; - case DeviceValueUOM.DEGREES: - case DeviceValueUOM.DEGREES_R: - case DeviceValueUOM.FAHRENHEIT: - return ( - new Intl.NumberFormat(undefined, { - minimumFractionDigits: 1 - }).format(value) + - ' ' + - DeviceValueUOM_s[uom] - ); - default: - return new Intl.NumberFormat().format(value) + ' ' + DeviceValueUOM_s[uom]; - } - } - - const setUom = (uom: number) => { - switch (uom) { - case DeviceValueUOM.HOURS: - return LL.HOURS(); - case DeviceValueUOM.MINUTES: - return LL.MINUTES(); - case DeviceValueUOM.SECONDS: - return LL.SECONDS(); - default: - return DeviceValueUOM_s[uom]; - } - }; - - const sendDeviceValue = async () => { - if (deviceValue) { - try { - const response = await EMSESP.writeValue({ - id: Number(device_select.state.id), - devicevalue: deviceValue - }); - if (response.status === 204) { - toast.error(LL.WRITE_CMD_FAILED()); - } else if (response.status === 403) { - toast.error(LL.ACCESS_DENIED()); - } else { - toast.success(LL.WRITE_CMD_SENT()); - } - setDeviceValue(undefined); - } catch (error) { - toast.error(extractErrorMessage(error, LL.PROBLEM_UPDATING())); - } finally { - refreshData(); - setDeviceValue(undefined); - } - } - }; - - const renderDeviceValueDialog = () => { - if (deviceValue) { - return ( - setDeviceValue(undefined)}> - {isCmdOnly(deviceValue) ? LL.RUN_COMMAND() : LL.CHANGE_VALUE()} - - {deviceValue.l && ( - - {deviceValue.l.map((val) => ( - - {val} - - ))} - - )} - {!deviceValue.l && ( - {setUom(deviceValue.u)} - }} - /> - )} - {deviceValue.h && {deviceValue.h}} - - - - - - - ); - } - }; - - const addAnalogSensor = () => { - setAnalog({ id: '0', g: 0, n: '', u: 0, v: 0, o: 0, t: 0, f: 1 }); - }; - - const sendSensor = async () => { - if (sensor) { - try { - const response = await EMSESP.writeSensor({ - id: sensor.id, - name: sensor.n, - offset: sensor.o - }); - if (response.status === 204) { - toast.error(LL.UPLOAD_OF(LL.SENSOR()) + ' ' + LL.FAILED()); - } else if (response.status === 403) { - toast.error(LL.ACCESS_DENIED()); - } else { - toast.success(LL.UPDATED_OF(LL.SENSOR())); - } - setSensor(undefined); - } catch (error) { - toast.error(extractErrorMessage(error, LL.PROBLEM_UPDATING())); - } finally { - setSensor(undefined); - fetchSensorData(); - } - } - }; - - const renderSensorDialog = () => { - if (sensor) { - return ( - setSensor(undefined)}> - - {LL.EDIT()} {LL.TEMP_SENSOR()} - - - - - {LL.ID_OF(LL.SENSOR())}: {sensor.id} - - - - - - - - °C - }} - /> - - - - - - - - - ); - } - }; - - const renderDeviceDialog = () => { - if (coreData && coreData.devices.length > 0 && deviceDialog !== -1) { - return ( - setDeviceDialog(-1)}> - {LL.DEVICE_DETAILS()} - - - - - - - - - - - - - - - - - - - - - - - - - - - ); - } - }; - - const renderCoreData = () => ( - - {!coreData.connected && } - {coreData.connected && coreData.devices.length === 0 && ( - - )} - - - {(tableList: any) => ( - <> -
- - - {LL.TYPE()} - {LL.DESCRIPTION()} - {LL.ENTITIES()} - - -
- - {tableList.map((device: Device, index: number) => ( - - - - - {device.tn} - {device.n} - {device.e} - - setDeviceDialog(index)}> - - - - - ))} - {(coreData.active_sensors > 0 || coreData.analog_enabled) && ( - - - - - {coreData.s_n} - {LL.ATTACHED_SENSORS()} - {coreData.active_sensors} - - addAnalogSensor()}> - - - - - )} - - - )} -
-
- ); - - const renderDeviceData = () => { - if (!device_select.state.id || device_select.state.id === 'sensor') { - return; - } - - const sendCommand = (dv: DeviceValue) => { - if (dv.c && me.admin && !hasMask(dv.id, DeviceEntityMask.DV_READONLY)) { - setDeviceValue(dv); - } - }; - - const renderNameCell = (dv: DeviceValue) => ( - <> - {dv.id.slice(2)}  - {hasMask(dv.id, DeviceEntityMask.DV_FAVORITE) && } - {hasMask(dv.id, DeviceEntityMask.DV_READONLY) && } - {hasMask(dv.id, DeviceEntityMask.DV_API_MQTT_EXCLUDE) && ( - - )} - - ); - - return ( - <> - - {deviceData.label} - - setOnlyFav(!onlyFav)} />} - label={ - - {LL.SHOW_FAV()}  - - - } - /> - hasMask(dv.id, DeviceEntityMask.DV_FAVORITE)) - : deviceData.data - }} - theme={data_theme} - sort={dv_sort} - layout={{ custom: true }} - > - {(tableList: any) => ( - <> -
- - - - - - - - - -
- - {tableList.map((dv: DeviceValue) => ( - sendCommand(dv)}> - {renderNameCell(dv)} - {formatValue(dv.v, dv.u)} - - {dv.c && me.admin && !hasMask(dv.id, DeviceEntityMask.DV_READONLY) && ( - sendCommand(dv)}> - {isCmdOnly(dv) ? ( - - ) : ( - - )} - - )} - - - ))} - - - )} -
- - ); - }; - - const updateSensor = (s: Sensor) => { - if (s && me.admin) { - setSensor(s); - } - }; - - const updateAnalog = (a: Analog) => { - if (me.admin) { - setAnalog(a); - } - }; - - const renderDallasData = () => ( - <> - - {LL.TEMP_SENSORS()} - - - {(tableList: any) => ( - <> -
- - - - - - - - - -
- - {tableList.map((s: Sensor) => ( - updateSensor(s)}> - {s.n} - {formatValue(s.t, s.u)} - - {me.admin && ( - updateSensor(s)}> - - - )} - - - ))} - - - )} -
- - ); - - const renderAnalogData = () => ( - <> - - {LL.ANALOG_SENSORS()} - - - - {(tableList: any) => ( - <> -
- - - - - - - - - - - {LL.VALUE(0)} - - -
- - {tableList.map((a: Analog) => ( - updateAnalog(a)}> - {a.g} - {a.n} - {AnalogTypeNames[a.t]} - {a.t ? formatValue(a.v, a.u) : ''} - - {me.admin && ( - updateAnalog(a)}> - - - )} - - - ))} - - - )} -
- - ); - - const sendRemoveAnalog = async () => { - if (analog) { - try { - const response = await EMSESP.writeAnalog({ - gpio: analog.g, - name: analog.n, - offset: analog.o, - factor: analog.f, - uom: analog.u, - type: -1 - }); - - if (response.status === 204) { - toast.error(LL.DELETION_OF(LL.ANALOG_SENSOR()) + ' ' + LL.FAILED()); - } else if (response.status === 403) { - toast.error(LL.ACCESS_DENIED()); - } else { - toast.success(LL.REMOVED_OF(LL.ANALOG_SENSOR())); - } - } catch (error) { - toast.error(extractErrorMessage(error, LL.PROBLEM_UPDATING())); - } finally { - setAnalog(undefined); - fetchSensorData(); - } - } - }; - - const sendAnalog = async () => { - if (analog) { - try { - const response = await EMSESP.writeAnalog({ - gpio: analog.g, - name: analog.n, - offset: analog.o, - factor: analog.f, - uom: analog.u, - type: analog.t - }); - - if (response.status === 204) { - toast.error(LL.UPDATE_OF(LL.ANALOG_SENSOR()) + ' ' + LL.FAILED()); - } else if (response.status === 403) { - toast.error(LL.ACCESS_DENIED()); - } else { - toast.success(LL.UPDATED_OF(LL.ANALOG_SENSOR())); - } - } catch (error) { - toast.error(extractErrorMessage(error, LL.PROBLEM_UPDATING())); - } finally { - setAnalog(undefined); - fetchSensorData(); - } - } - }; - - const renderAnalogDialog = () => { - if (analog) { - return ( - setAnalog(undefined)}> - - {LL.EDIT()} {LL.ANALOG_SENSOR()} - - - - - - - - - - - - {AnalogTypeNames.map((val, i) => ( - - {val} - - ))} - - - {analog.t >= AnalogType.COUNTER && analog.t <= AnalogType.RATE && ( - <> - - - {DeviceValueUOM_s.map((val, i) => ( - - {val} - - ))} - - - {analog.t === AnalogType.ADC && ( - - mV - }} - /> - - )} - {analog.t === AnalogType.COUNTER && ( - - - - )} - - - - - )} - {analog.t === AnalogType.DIGITAL_OUT && (analog.g === 25 || analog.g === 26) && ( - <> - - - - - )} - {analog.t === AnalogType.DIGITAL_OUT && analog.g !== 25 && analog.g !== 26 && ( - <> - - - - - )} - {analog.t >= AnalogType.PWM_0 && ( - <> - - Hz - }} - /> - - - % - }} - /> - - - )} - - - {LL.WARN_GPIO()} - - - - - - - - - - - ); - } - }; - - return ( - - {renderCoreData()} - {renderDeviceData()} - {renderDeviceDialog()} - {sensorData.sensors.length !== 0 && renderDallasData()} - {sensorData.analogs.length !== 0 && renderAnalogData()} - {renderDeviceValueDialog()} - {renderSensorDialog()} - {renderAnalogDialog()} - - - {device_select.state.id && device_select.state.id !== 'sensor' && ( - - )} - - - ); -}; - -export default DashboardData; diff --git a/interface/src/project/DashboardDevices.tsx b/interface/src/project/DashboardDevices.tsx new file mode 100644 index 000000000..c559dc02d --- /dev/null +++ b/interface/src/project/DashboardDevices.tsx @@ -0,0 +1,619 @@ +import CancelIcon from '@mui/icons-material/Cancel'; +import CommentsDisabledOutlinedIcon from '@mui/icons-material/CommentsDisabledOutlined'; +import EditIcon from '@mui/icons-material/Edit'; +import EditOffOutlinedIcon from '@mui/icons-material/EditOffOutlined'; +import DownloadIcon from '@mui/icons-material/GetApp'; +import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined'; +import KeyboardArrowDownOutlinedIcon from '@mui/icons-material/KeyboardArrowDownOutlined'; +import KeyboardArrowUpOutlinedIcon from '@mui/icons-material/KeyboardArrowUpOutlined'; +import PlayArrowIcon from '@mui/icons-material/PlayArrow'; +import RefreshIcon from '@mui/icons-material/Refresh'; +import StarIcon from '@mui/icons-material/Star'; +import StarBorderOutlinedIcon from '@mui/icons-material/StarBorderOutlined'; +import UnfoldMoreOutlinedIcon from '@mui/icons-material/UnfoldMoreOutlined'; + +import { + Button, + Dialog, + DialogTitle, + DialogContent, + DialogActions, + IconButton, + List, + ListItem, + ListItemText, + Box, + Grid, + Typography +} from '@mui/material'; +import { useRowSelect } from '@table-library/react-table-library/select'; +import { useSort, SortToggleType } from '@table-library/react-table-library/sort'; +import { Table, Header, HeaderRow, HeaderCell, Body, Row, Cell } from '@table-library/react-table-library/table'; +import { useTheme } from '@table-library/react-table-library/theme'; +import { useState, useContext, useEffect, useCallback, useLayoutEffect } from 'react'; + +import { IconContext } from 'react-icons'; +import { toast } from 'react-toastify'; +import DashboarDevicesDialog from './DashboardDevicesDialog'; +import DeviceIcon from './DeviceIcon'; + +import * as EMSESP from './api'; +import { formatValue } from './deviceValue'; + +import { DeviceValueUOM_s, DeviceEntityMask, DeviceType } from './types'; +import { deviceValueItemValidation } from './validators'; +import type { Device, CoreData, DeviceData, DeviceValue } from './types'; +import type { FC } from 'react'; +import { ButtonRow, SectionContent, MessageBox } from 'components'; +import { AuthenticatedContext } from 'contexts/authentication'; + +import { useI18nContext } from 'i18n/i18n-react'; +import { extractErrorMessage } from 'utils'; + +const DashboardDevices: FC = () => { + const [size, setSize] = useState([0, 0]); + const { me } = useContext(AuthenticatedContext); + const { LL } = useI18nContext(); + const [deviceData, setDeviceData] = useState({ data: [] }); + const [selectedDeviceValue, setSelectedDeviceValue] = useState(); + const [onlyFav, setOnlyFav] = useState(false); + const [deviceValueDialogOpen, setDeviceValueDialogOpen] = useState(false); + const [showDeviceInfo, setShowDeviceInfo] = useState(false); + const [selectedDevice, setSelectedDevice] = useState(); + const [coreData, setCoreData] = useState({ + connected: true, + devices: [] + }); + + useLayoutEffect(() => { + function updateSize() { + setSize([window.innerWidth, window.innerHeight]); + } + window.addEventListener('resize', updateSize); + updateSize(); + return () => window.removeEventListener('resize', updateSize); + }, []); + + const leftOffset = () => { + const left = document.getElementById('devices-window')?.getBoundingClientRect().left; + const right = document.getElementById('devices-window')?.getBoundingClientRect().right; + + if (!left || !right) { + return 0; + } + + return left + (right - left < 400 ? 0 : 200); + }; + + const common_theme = useTheme({ + BaseRow: ` + font-size: 14px; + `, + HeaderRow: ` + text-transform: uppercase; + background-color: black; + color: #90CAF9; + .th { + border-bottom: 1px solid #565656; + } + `, + Row: ` + background-color: #1E1E1E; + position: relative; + cursor: pointer; + .td { + padding: 8px; + border-top: 1px solid #565656; + border-bottom: 1px solid #565656; + } + &.tr.tr-body.row-select.row-select-single-selected { + background-color: #3d4752; + font-weight: normal; + } + &:hover .td { + border-top: 1px solid #177ac9; + border-bottom: 1px solid #177ac9; + } + ` + }); + + const device_theme = useTheme([ + common_theme, + { + Table: ` + --data-table-library_grid-template-columns: 40px repeat(1, minmax(0, 1fr)) 130px; + `, + BaseRow: ` + .td { + height: 42px; + } + `, + BaseCell: ` + &:nth-of-type(2) { + text-align: left; + }, + &:nth-of-type(4) { + text-align: center; + } + `, + HeaderRow: ` + .th { + padding: 8px; + height: 36px; + ` + } + ]); + + const data_theme = useTheme([ + common_theme, + { + Table: ` + --data-table-library_grid-template-columns: minmax(0, 1fr) 150px 40px; + height: auto; + max-height: 100%; + overflow-y: scroll; + ::-webkit-scrollbar { + display:none; + } + `, + BaseCell: ` + &:nth-of-type(1) { + border-left: 1px solid #177ac9; + }, + &:nth-of-type(2) { + text-align: right; + }, + &:nth-of-type(3) { + border-right: 1px solid #177ac9; + } + `, + HeaderRow: ` + .th { + border-top: 1px solid #565656; + } + `, + Row: ` + &:nth-of-type(odd) .td { + background-color: #303030; + } + ` + } + ]); + + const getSortIcon = (state: any, sortKey: any) => { + if (state.sortKey === sortKey && state.reverse) { + return ; + } + if (state.sortKey === sortKey && !state.reverse) { + return ; + } + return ; + }; + + const dv_sort = useSort( + { nodes: deviceData.data }, + {}, + { + sortIcon: { + iconDefault: , + iconUp: , + iconDown: + }, + sortToggleType: SortToggleType.AlternateWithReset, + sortFns: { + NAME: (array) => array.sort((a, b) => a.id.toString().slice(2).localeCompare(b.id.toString().slice(2))), + VALUE: (array) => array.sort((a, b) => a.v.toString().localeCompare(b.v.toString())) + } + } + ); + + const fetchDeviceData = async (id: number) => { + try { + setDeviceData((await EMSESP.readDeviceData({ id })).data); + } catch (error) { + toast.error(extractErrorMessage(error, LL.PROBLEM_LOADING())); + } + }; + + function onSelectChange(action: any, state: any) { + setDeviceData({ data: [] }); + setSelectedDevice(state.id); + if (action.type === 'ADD_BY_ID_EXCLUSIVELY') { + void fetchDeviceData(state.id); + } + } + + const device_select = useRowSelect( + { nodes: coreData.devices }, + { + onChange: onSelectChange + } + ); + + const resetDeviceSelect = () => { + device_select.fns.onRemoveAll(); + }; + + const escFunction = useCallback( + (event: any) => { + if (event.keyCode === 27) { + if (device_select) { + device_select.fns.onRemoveAll(); + } + } + }, + [device_select] + ); + + useEffect(() => { + document.addEventListener('keydown', escFunction); + return () => { + document.removeEventListener('keydown', escFunction); + }; + }, [escFunction]); + + const fetchCoreData = useCallback(async () => { + try { + setSelectedDevice(undefined); + setCoreData((await EMSESP.readCoreData()).data); + } catch (error) { + toast.error(extractErrorMessage(error, LL.PROBLEM_LOADING())); + } + }, [LL]); + + useEffect(() => { + void fetchCoreData(); + }, [fetchCoreData]); + + const refreshData = () => { + if (deviceValueDialogOpen) { + return; + } + if (selectedDevice) { + void fetchDeviceData(selectedDevice); + } else { + void fetchCoreData(); + } + }; + + const escapeCsvCell = (cell: any) => { + if (cell == null) { + return ''; + } + const sc = cell.toString().trim(); + if (sc === '' || sc === '""') { + return sc; + } + if (sc.includes('"') || sc.includes(';') || sc.includes('\n') || sc.includes('\r')) { + return '"' + sc.replace(/"/g, '""') + '"'; + } + return sc; + }; + + const makeCsvData = (columns: any, data: any) => + data.reduce( + (csvString: any, rowItem: any) => + csvString + columns.map(({ accessor }: any) => escapeCsvCell(accessor(rowItem))).join(';') + '\r\n', + columns.map(({ name }: any) => escapeCsvCell(name)).join(';') + '\r\n' + ); + + const downloadAsCsv = (columns: any, data: any, filename: string) => { + const csvData = makeCsvData(columns, data); + const csvFile = new Blob([csvData], { type: 'text/csv;charset:utf-8' }); + const downloadLink = document.createElement('a'); + + downloadLink.download = filename; + downloadLink.href = window.URL.createObjectURL(csvFile); + document.body.appendChild(downloadLink); + downloadLink.click(); + document.body.removeChild(downloadLink); + }; + + const hasMask = (id: string, mask: number) => (parseInt(id.slice(0, 2), 16) & mask) === mask; + + const handleDownloadCsv = () => { + const columns = [ + { accessor: (dv: any) => dv.id.slice(2), name: LL.ENTITY_NAME() }, + { + accessor: (dv: any) => (typeof dv.v === 'number' ? new Intl.NumberFormat().format(dv.v) : dv.v), + name: LL.VALUE(0) + }, + { accessor: (dv: any) => DeviceValueUOM_s[dv.u], name: 'UoM' } + ]; + + const deviceIndex = coreData.devices.findIndex((d) => d.id === device_select.state.id); + if (deviceIndex === -1) { + return; + } + const filename = coreData.devices[deviceIndex].tn + '_' + coreData.devices[deviceIndex].n; + + downloadAsCsv( + columns, + onlyFav ? deviceData.data.filter((dv) => hasMask(dv.id, DeviceEntityMask.DV_FAVORITE)) : deviceData.data, + filename + ); + }; + + useEffect(() => { + const timer = setInterval(() => refreshData(), 60000); + return () => { + clearInterval(timer); + }; + }); + + const deviceValueDialogSave = async (dv: DeviceValue) => { + const selectedDeviceID = Number(device_select.state.id); + try { + const response = await EMSESP.writeDeviceValue({ + id: selectedDeviceID, + devicevalue: dv + }); + if (response.status === 204) { + toast.error(LL.WRITE_CMD_FAILED()); + } else if (response.status === 403) { + toast.error(LL.ACCESS_DENIED()); + } else { + toast.success(LL.WRITE_CMD_SENT()); + } + } catch (error) { + toast.error(extractErrorMessage(error, LL.PROBLEM_UPDATING())); + } finally { + setDeviceValueDialogOpen(false); + await fetchDeviceData(selectedDeviceID); + setSelectedDeviceValue(undefined); + } + }; + + const renderDeviceDetails = () => { + if (showDeviceInfo) { + const deviceIndex = coreData.devices.findIndex((d) => d.id === device_select.state.id); + if (deviceIndex === -1) { + return; + } + + return ( + setShowDeviceInfo(false)}> + {LL.DEVICE_DETAILS()} + + + + + + + + + {coreData.devices[deviceIndex].t !== DeviceType.CUSTOM && ( + <> + + + + + + + + + + + + + + )} + + + + + + + ); + } + }; + + const renderCoreData = () => ( + + {!coreData.connected && } + {coreData.connected && coreData.devices.length === 0 && ( + + )} + + + {(tableList: any) => ( + <> +
+ + + {LL.DESCRIPTION()} + {LL.TYPE(0)} + +
+ + {tableList.map((device: Device) => ( + + + + + {device.n} + {device.tn} + + ))} + + + )} +
+
+ ); + + const deviceValueDialogClose = () => { + setDeviceValueDialogOpen(false); + }; + + const renderDeviceData = () => { + if (!selectedDevice) { + return; + } + + const showDeviceValue = (dv: DeviceValue) => { + setSelectedDeviceValue(dv); + setDeviceValueDialogOpen(true); + }; + + const renderNameCell = (dv: DeviceValue) => ( + <> + {dv.id.slice(2)}  + {hasMask(dv.id, DeviceEntityMask.DV_FAVORITE) && } + {hasMask(dv.id, DeviceEntityMask.DV_READONLY) && } + {hasMask(dv.id, DeviceEntityMask.DV_API_MQTT_EXCLUDE) && ( + + )} + + ); + + const shown_data = onlyFav + ? deviceData.data.filter((dv) => hasMask(dv.id, DeviceEntityMask.DV_FAVORITE)) + : deviceData.data; + + const deviceIndex = coreData.devices.findIndex((d) => d.id === device_select.state.id); + if (deviceIndex === -1) { + return; + } + + return ( + leftOffset(), + right: 16, + bottom: 0, + top: 128, + maxHeight: () => size[1] - 210, + zIndex: 'modal' + }} + > + + + + + {coreData.devices[deviceIndex].n} +  ({shown_data.length}) + + + + + + + + + + + setShowDeviceInfo(true)}> + + + + + + setOnlyFav(!onlyFav)}> + {onlyFav ? ( + + ) : ( + + )} + + + + + + + + + {(tableList: any) => ( + <> +
+ + + + + + + + + +
+ + {tableList.map((dv: DeviceValue) => ( + showDeviceValue(dv)}> + {renderNameCell(dv)} + {formatValue(LL, dv.v, dv.u)} + + {dv.c && me.admin && !hasMask(dv.id, DeviceEntityMask.DV_READONLY) && ( + showDeviceValue(dv)}> + {dv.v === '' && dv.c ? ( + + ) : ( + + )} + + )} + + + ))} + + + )} +
+
+ ); + }; + + return ( + + {renderCoreData()} + {renderDeviceData()} + {renderDeviceDetails()} + {selectedDeviceValue && ( + + )} + + + + + ); +}; + +export default DashboardDevices; diff --git a/interface/src/project/DashboardDevicesDialog.tsx b/interface/src/project/DashboardDevicesDialog.tsx new file mode 100644 index 000000000..c786c7c57 --- /dev/null +++ b/interface/src/project/DashboardDevicesDialog.tsx @@ -0,0 +1,205 @@ +import CancelIcon from '@mui/icons-material/Cancel'; +import WarningIcon from '@mui/icons-material/Warning'; + +import { + Button, + Dialog, + DialogTitle, + DialogContent, + DialogActions, + InputAdornment, + MenuItem, + TextField, + FormHelperText, + Grid, + Box, + Typography +} from '@mui/material'; +import { useState, useEffect } from 'react'; + +import { DeviceValueUOM, DeviceValueUOM_s } from './types'; +import type { DeviceValue } from './types'; +import type Schema from 'async-validator'; + +import type { ValidateFieldsError } from 'async-validator'; +import { ValidatedTextField } from 'components'; +import { useI18nContext } from 'i18n/i18n-react'; +import { updateValue } from 'utils'; + +import { validate } from 'validators'; + +type DashboardDevicesDialogProps = { + open: boolean; + onClose: () => void; + onSave: (as: DeviceValue) => void; + selectedItem: DeviceValue; + writeable: boolean; + validator: Schema; +}; + +const DashboarDevicesDialog = ({ + open, + onClose, + onSave, + selectedItem, + writeable, + validator +}: DashboardDevicesDialogProps) => { + const { LL } = useI18nContext(); + const [editItem, setEditItem] = useState(selectedItem); + const [fieldErrors, setFieldErrors] = useState(); + + const updateFormValue = updateValue(setEditItem); + + useEffect(() => { + if (open) { + setFieldErrors(undefined); + setEditItem(selectedItem); + } + }, [open, selectedItem]); + + const close = () => { + onClose(); + }; + + const save = async () => { + try { + setFieldErrors(undefined); + await validate(validator, editItem); + onSave(editItem); + } catch (errors: any) { + setFieldErrors(errors); + } + }; + + const setUom = (uom: number) => { + switch (uom) { + case DeviceValueUOM.HOURS: + return LL.HOURS(); + case DeviceValueUOM.MINUTES: + return LL.MINUTES(); + case DeviceValueUOM.SECONDS: + return LL.SECONDS(); + default: + return DeviceValueUOM_s[uom]; + } + }; + + const showHelperText = (dv: DeviceValue) => { + if (dv.h) { + return dv.h; + } + if (dv.l) { + return '[ ' + dv.l.join(' | ') + ' ]'; + } + + let helperText = '<'; + if (dv.u !== DeviceValueUOM.NONE) { + helperText += 'n'; + if (dv.m && dv.x) { + helperText += ' between ' + dv.m + ' and ' + dv.x; + } + if (dv.s) { + helperText += ' , step ' + dv.s; + } + } else { + helperText += 'text'; + } + return helperText + '>'; + }; + + return ( + + + {selectedItem.v === '' && selectedItem.c ? LL.RUN_COMMAND() : writeable ? LL.CHANGE_VALUE() : LL.VALUE(1)} + + + + {editItem.id.slice(2)} + + + + {editItem.l ? ( + + {editItem.l.map((val) => ( + + {val} + + ))} + + ) : editItem.u !== DeviceValueUOM.NONE ? ( + {setUom(editItem.u)} + }} + /> + ) : ( + + )} + + {writeable && ( + + format: {showHelperText(editItem)} + + )} + + + + + {writeable ? ( + <> + + + + ) : ( + + )} + + + ); +}; + +export default DashboarDevicesDialog; diff --git a/interface/src/project/DashboardSensors.tsx b/interface/src/project/DashboardSensors.tsx new file mode 100644 index 000000000..b43cf91b6 --- /dev/null +++ b/interface/src/project/DashboardSensors.tsx @@ -0,0 +1,471 @@ +import AddCircleOutlineOutlinedIcon from '@mui/icons-material/AddCircleOutlineOutlined'; +import KeyboardArrowDownOutlinedIcon from '@mui/icons-material/KeyboardArrowDownOutlined'; +import KeyboardArrowUpOutlinedIcon from '@mui/icons-material/KeyboardArrowUpOutlined'; +import RefreshIcon from '@mui/icons-material/Refresh'; +import UnfoldMoreOutlinedIcon from '@mui/icons-material/UnfoldMoreOutlined'; +import { Button, Typography, Box } from '@mui/material'; +import { useSort, SortToggleType } from '@table-library/react-table-library/sort'; +import { Table, Header, HeaderRow, HeaderCell, Body, Row, Cell } from '@table-library/react-table-library/table'; +import { useTheme } from '@table-library/react-table-library/theme'; +import { useState, useContext, useCallback, useEffect } from 'react'; + +import { toast } from 'react-toastify'; + +import DashboardSensorsAnalogDialog from './DashboardSensorsAnalogDialog'; +import DashboardSensorsTemperatureDialog from './DashboardSensorsTemperatureDialog'; +import * as EMSESP from './api'; + +import { DeviceValueUOM, DeviceValueUOM_s, AnalogTypeNames } from './types'; +import { temperatureSensorItemValidation, analogSensorItemValidation } from './validators'; +import type { SensorData, TemperatureSensor, AnalogSensor } from './types'; +import type { FC } from 'react'; +import { ButtonRow, SectionContent } from 'components'; + +import { AuthenticatedContext } from 'contexts/authentication'; +import { useI18nContext } from 'i18n/i18n-react'; +import { extractErrorMessage } from 'utils'; + +const DashboardSensors: FC = () => { + const { LL } = useI18nContext(); + const { me } = useContext(AuthenticatedContext); + const [sensorData, setSensorData] = useState({ ts: [], as: [], analog_enabled: false }); + const [selectedTemperatureSensor, setSelectedTemperatureSensor] = useState(); + const [selectedAnalogSensor, setSelectedAnalogSensor] = useState(); + const [temperatureDialogOpen, setTemperatureDialogOpen] = useState(false); + const [analogDialogOpen, setAnalogDialogOpen] = useState(false); + const [creating, setCreating] = useState(false); + + const isAdmin = me.admin; + + const common_theme = useTheme({ + BaseRow: ` + font-size: 14px; + .td { + height: 32px; + } + `, + HeaderRow: ` + text-transform: uppercase; + background-color: black; + color: #90CAF9; + .th { + border-bottom: 1px solid #565656; + } + .th { + height: 36px; + } + `, + Row: ` + background-color: #1e1e1e; + position: relative; + cursor: pointer; + .td { + padding: 8px; + border-top: 1px solid #565656; + border-bottom: 1px solid #565656; + } + &.tr.tr-body.row-select.row-select-single-selected { + background-color: #3d4752; + font-weight: normal; + } + &:hover .td { + border-top: 1px solid #177ac9; + border-bottom: 1px solid #177ac9; + } + &:nth-of-type(odd) .td { + background-color: #303030; + } + `, + Cell: ` + &:last-of-type { + text-align: right; + }, + ` + }); + + const temperature_theme = useTheme([ + common_theme, + { + Table: ` + --data-table-library_grid-template-columns: minmax(0, 1fr) 35%; + ` + } + ]); + + const analog_theme = useTheme([ + common_theme, + { + Table: ` + --data-table-library_grid-template-columns: 80px repeat(1, minmax(0, 1fr)) 120px 100px; + ` + } + ]); + + const fetchSensorData = useCallback(async () => { + if (!analogDialogOpen && !temperatureDialogOpen) { + try { + setSensorData((await EMSESP.readSensorData()).data); + } catch (error) { + toast.error(extractErrorMessage(error, LL.PROBLEM_LOADING())); + } + } + }, [LL, analogDialogOpen, temperatureDialogOpen]); + + useEffect(() => { + void fetchSensorData(); + }, [fetchSensorData]); + + const getSortIcon = (state: any, sortKey: any) => { + if (state.sortKey === sortKey && state.reverse) { + return ; + } + if (state.sortKey === sortKey && !state.reverse) { + return ; + } + return ; + }; + + const analog_sort = useSort( + { nodes: sensorData.as }, + {}, + { + sortIcon: { + iconDefault: , + iconUp: , + iconDown: + }, + sortToggleType: SortToggleType.AlternateWithReset, + sortFns: { + GPIO: (array) => array.sort((a, b) => a.g - b.g), + NAME: (array) => array.sort((a, b) => a.n.localeCompare(b.n)), + TYPE: (array) => array.sort((a, b) => a.t - b.t), + VALUE: (array) => array.sort((a, b) => a.v - b.v) + } + } + ); + + const temperature_sort = useSort( + { nodes: sensorData.ts }, + {}, + { + sortIcon: { + iconDefault: , + iconUp: , + iconDown: + }, + sortToggleType: SortToggleType.AlternateWithReset, + sortFns: { + NAME: (array) => array.sort((a, b) => a.n.localeCompare(b.n)), + VALUE: (array) => array.sort((a, b) => a.t - b.t) + } + } + ); + + useEffect(() => { + const timer = setInterval(() => fetchSensorData(), 30000); + return () => { + clearInterval(timer); + }; + }); + + const formatDurationMin = (duration_min: number) => { + const days = Math.trunc((duration_min * 60000) / 86400000); + const hours = Math.trunc((duration_min * 60000) / 3600000) % 24; + const minutes = Math.trunc((duration_min * 60000) / 60000) % 60; + + let formatted = ''; + if (days) { + formatted += LL.NUM_DAYS({ num: days }) + ' '; + } + if (hours) { + formatted += LL.NUM_HOURS({ num: hours }) + ' '; + } + if (minutes) { + formatted += LL.NUM_MINUTES({ num: minutes }); + } + return formatted; + }; + + function formatValue(value: any, uom: number) { + if (value === undefined) { + return ''; + } + switch (uom) { + case DeviceValueUOM.HOURS: + return value ? formatDurationMin(value * 60) : LL.NUM_HOURS({ num: 0 }); + case DeviceValueUOM.MINUTES: + return value ? formatDurationMin(value) : LL.NUM_MINUTES({ num: 0 }); + case DeviceValueUOM.SECONDS: + return LL.NUM_SECONDS({ num: value }); + case DeviceValueUOM.NONE: + if (typeof value === 'number') { + return new Intl.NumberFormat().format(value); + } + return value; + case DeviceValueUOM.DEGREES: + case DeviceValueUOM.DEGREES_R: + case DeviceValueUOM.FAHRENHEIT: + return ( + new Intl.NumberFormat(undefined, { + minimumFractionDigits: 1 + }).format(value) + + ' ' + + DeviceValueUOM_s[uom] + ); + default: + return new Intl.NumberFormat().format(value) + ' ' + DeviceValueUOM_s[uom]; + } + } + + const updateTemperatureSensor = (ts: TemperatureSensor) => { + if (isAdmin) { + setSelectedTemperatureSensor(ts); + setTemperatureDialogOpen(true); + } + }; + + const onTemperatureDialogClose = () => { + setTemperatureDialogOpen(false); + }; + + const onTemperatureDialogSave = async (ts: TemperatureSensor) => { + try { + const response = await EMSESP.writeTemperatureSensor({ + id: ts.id, + name: ts.n, + offset: ts.o + }); + if (response.status === 204) { + toast.error(LL.UPLOAD_OF(LL.SENSOR()) + ' ' + LL.FAILED()); + } else if (response.status === 403) { + toast.error(LL.ACCESS_DENIED()); + } else { + toast.success(LL.UPDATED_OF(LL.SENSOR())); + } + } catch (error) { + toast.error(extractErrorMessage(error, LL.PROBLEM_UPDATING())); + } finally { + setTemperatureDialogOpen(false); + setSelectedTemperatureSensor(undefined); + await fetchSensorData(); + } + }; + + const updateAnalogSensor = (as: AnalogSensor) => { + if (isAdmin) { + setCreating(false); + setSelectedAnalogSensor(as); + setAnalogDialogOpen(true); + } + }; + + const onAnalogDialogClose = () => { + setAnalogDialogOpen(false); + }; + + const addAnalogSensor = () => { + setCreating(true); + setSelectedAnalogSensor({ + id: Math.floor(Math.random() * (Math.floor(200) - 100) + 100), + n: '', + g: 40, + u: 0, + v: 0, + o: 0, + t: 0, + f: 1, + d: false + }); + setAnalogDialogOpen(true); + }; + + const onAnalogDialogSave = async (as: AnalogSensor) => { + try { + const response = await EMSESP.writeAnalogSensor({ + id: as.id, + gpio: as.g, + name: as.n, + offset: as.o, + factor: as.f, + uom: as.u, + type: as.t, + deleted: as.d + }); + + if (response.status === 204) { + toast.error(LL.UPDATE_OF(LL.ANALOG_SENSOR()) + ' ' + LL.FAILED()); + } else if (response.status === 403) { + toast.error(LL.ACCESS_DENIED()); + } else { + toast.success(LL.UPDATED_OF(LL.ANALOG_SENSOR())); + } + } catch (error) { + toast.error(extractErrorMessage(error, LL.PROBLEM_UPDATING())); + } finally { + setAnalogDialogOpen(false); + setSelectedAnalogSensor(undefined); + await fetchSensorData(); + } + }; + + const RenderTemperatureSensors = () => ( + + {(tableList: any) => ( + <> +
+ + + + + + + + +
+ + {tableList.map((ts: TemperatureSensor) => ( + updateTemperatureSensor(ts)}> + {ts.n} + {formatValue(ts.t, ts.u)} + + ))} + + + )} +
+ ); + + const RenderAnalogSensors = () => ( + + {(tableList: any) => ( + <> +
+ + + + + + + + + + + + + + +
+ + {tableList.map((a: AnalogSensor) => ( + updateAnalogSensor(a)}> + {a.g} + {a.n} + {AnalogTypeNames[a.t]} + {a.t ? formatValue(a.v, a.u) : ''} + + ))} + + + )} +
+ ); + + return ( + + + {LL.TEMP_SENSORS()} + + + {selectedTemperatureSensor && ( + + )} + + {sensorData?.analog_enabled === true && ( + <> + + {LL.ANALOG_SENSORS()} + + + {selectedAnalogSensor && ( + + )} + + )} + + + + + + + + + + + ); +}; + +export default DashboardSensors; diff --git a/interface/src/project/DashboardSensorsAnalogDialog.tsx b/interface/src/project/DashboardSensorsAnalogDialog.tsx new file mode 100644 index 000000000..35d4a0b44 --- /dev/null +++ b/interface/src/project/DashboardSensorsAnalogDialog.tsx @@ -0,0 +1,264 @@ +import CancelIcon from '@mui/icons-material/Cancel'; +import RemoveIcon from '@mui/icons-material/RemoveCircleOutline'; +import WarningIcon from '@mui/icons-material/Warning'; + +import { + Button, + Typography, + Box, + Dialog, + DialogTitle, + DialogContent, + DialogActions, + InputAdornment, + Grid, + MenuItem, + TextField +} from '@mui/material'; +import { useState, useEffect } from 'react'; + +import { AnalogType, AnalogTypeNames, DeviceValueUOM_s } from './types'; +import type { AnalogSensor } from './types'; +import type Schema from 'async-validator'; +import type { ValidateFieldsError } from 'async-validator'; +import { ValidatedTextField } from 'components'; + +import { useI18nContext } from 'i18n/i18n-react'; +import { numberValue, updateValue } from 'utils'; + +import { validate } from 'validators'; + +type DashboardSensorsAnalogDialogProps = { + open: boolean; + onClose: () => void; + onSave: (as: AnalogSensor) => void; + creating: boolean; + selectedItem: AnalogSensor; + validator: Schema; +}; + +const DashboardSensorsAnalogDialog = ({ + open, + onClose, + onSave, + creating, + selectedItem, + validator +}: DashboardSensorsAnalogDialogProps) => { + const { LL } = useI18nContext(); + const [fieldErrors, setFieldErrors] = useState(); + const [editItem, setEditItem] = useState(selectedItem); + const updateFormValue = updateValue(setEditItem); + + useEffect(() => { + if (open) { + setFieldErrors(undefined); + setEditItem(selectedItem); + } + }, [open, selectedItem]); + + const close = () => { + onClose(); + }; + + const save = async () => { + try { + setFieldErrors(undefined); + await validate(validator, editItem); + onSave(editItem); + } catch (errors: any) { + setFieldErrors(errors); + } + }; + + const remove = () => { + editItem.d = true; + onSave(editItem); + }; + + return ( + + + {creating ? LL.ADD(1) + ' ' + LL.NEW(1) : LL.EDIT()} {LL.ANALOG_SENSOR()} + + + + + + + {creating && ( + + + {LL.WARN_GPIO()} + + + )} + + + + + + {AnalogTypeNames.map((val, i) => ( + + {val} + + ))} + + + {editItem.t >= AnalogType.COUNTER && editItem.t <= AnalogType.RATE && ( + <> + + + {DeviceValueUOM_s.map((val, i) => ( + + {val} + + ))} + + + {editItem.t === AnalogType.ADC && ( + + mV + }} + /> + + )} + {editItem.t === AnalogType.COUNTER && ( + + + + )} + + + + + )} + {editItem.t === AnalogType.DIGITAL_OUT && (editItem.g === 25 || editItem.g === 26) && ( + + + + )} + {editItem.t === AnalogType.DIGITAL_OUT && editItem.g !== 25 && editItem.g !== 26 && ( + + + + )} + {editItem.t >= AnalogType.PWM_0 && ( + <> + + Hz + }} + /> + + + % + }} + /> + + + )} + + + + {!creating && ( + + + + )} + + + + + ); +}; + +export default DashboardSensorsAnalogDialog; diff --git a/interface/src/project/DashboardSensorsTemperatureDialog.tsx b/interface/src/project/DashboardSensorsTemperatureDialog.tsx new file mode 100644 index 000000000..3089dc363 --- /dev/null +++ b/interface/src/project/DashboardSensorsTemperatureDialog.tsx @@ -0,0 +1,121 @@ +import CancelIcon from '@mui/icons-material/Cancel'; +import WarningIcon from '@mui/icons-material/Warning'; + +import { + Button, + Typography, + Box, + Dialog, + DialogTitle, + DialogContent, + DialogActions, + InputAdornment, + Grid, + TextField +} from '@mui/material'; +import { useState, useEffect } from 'react'; + +import type { TemperatureSensor } from './types'; +import type Schema from 'async-validator'; +import type { ValidateFieldsError } from 'async-validator'; +import { ValidatedTextField } from 'components'; + +import { useI18nContext } from 'i18n/i18n-react'; +import { numberValue, updateValue } from 'utils'; + +import { validate } from 'validators'; + +type DashboardSensorsTemperatureDialogProps = { + open: boolean; + onClose: () => void; + onSave: (ts: TemperatureSensor) => void; + selectedItem: TemperatureSensor; + validator: Schema; +}; + +const DashboardSensorsTemperatureDialog = ({ + open, + onClose, + onSave, + selectedItem, + validator +}: DashboardSensorsTemperatureDialogProps) => { + const { LL } = useI18nContext(); + const [fieldErrors, setFieldErrors] = useState(); + const [editItem, setEditItem] = useState(selectedItem); + const updateFormValue = updateValue(setEditItem); + + useEffect(() => { + if (open) { + setFieldErrors(undefined); + setEditItem(selectedItem); + } + }, [open, selectedItem]); + + const close = () => { + onClose(); + }; + + const save = async () => { + try { + setFieldErrors(undefined); + await validate(validator, editItem); + onSave(editItem); + } catch (errors: any) { + setFieldErrors(errors); + } + }; + + return ( + + + {LL.EDIT()} {LL.TEMP_SENSOR()} + + + + + {LL.ID_OF(LL.SENSOR())}: {editItem.id} + + + + + + + + °C + }} + /> + + + + + + + + + ); +}; + +export default DashboardSensorsTemperatureDialog; diff --git a/interface/src/project/DashboardStatus.tsx b/interface/src/project/DashboardStatus.tsx index 842bd0371..887a142d8 100644 --- a/interface/src/project/DashboardStatus.tsx +++ b/interface/src/project/DashboardStatus.tsx @@ -1,42 +1,38 @@ -import { FC, useState, useContext, useEffect } from 'react'; -import { toast } from 'react-toastify'; +import CancelIcon from '@mui/icons-material/Cancel'; +import DeviceHubIcon from '@mui/icons-material/DeviceHub'; +import DirectionsBusIcon from '@mui/icons-material/DirectionsBus'; +import PermScanWifiIcon from '@mui/icons-material/PermScanWifi'; +import RefreshIcon from '@mui/icons-material/Refresh'; import { Avatar, - Button, - List, - ListItem, - ListItemAvatar, - ListItemText, Box, + Button, Dialog, DialogActions, DialogContent, DialogTitle, - Theme, + List, + ListItem, + ListItemAvatar, + ListItemText, useTheme } from '@mui/material'; - +import { Body, Cell, Header, HeaderCell, HeaderRow, Row, Table } from '@table-library/react-table-library/table'; import { useTheme as tableTheme } from '@table-library/react-table-library/theme'; -import { Table, Header, HeaderRow, HeaderCell, Body, Row, Cell } from '@table-library/react-table-library/table'; - -import DeviceHubIcon from '@mui/icons-material/DeviceHub'; -import RefreshIcon from '@mui/icons-material/Refresh'; -import PermScanWifiIcon from '@mui/icons-material/PermScanWifi'; -import CancelIcon from '@mui/icons-material/Cancel'; -import DirectionsBusIcon from '@mui/icons-material/DirectionsBus'; - -import { AuthenticatedContext } from 'contexts/authentication'; - -import { ButtonRow, FormLoader, SectionContent } from 'components'; - -import { Status, busConnectionStatus, Stat } from './types'; - -import { extractErrorMessage, useRest } from 'utils'; +import { useContext, useEffect, useState } from 'react'; +import { toast } from 'react-toastify'; import * as EMSESP from './api'; +import { busConnectionStatus } from './types'; +import type { Stat, Status } from './types'; +import type { Theme } from '@mui/material'; import type { Translation } from 'i18n/i18n-types'; +import type { FC } from 'react'; +import { ButtonRow, FormLoader, SectionContent } from 'components'; +import { AuthenticatedContext } from 'contexts/authentication'; import { useI18nContext } from 'i18n/i18n-react'; +import { extractErrorMessage, useRest } from 'utils'; export const isConnected = ({ status }: Status) => status !== busConnectionStatus.BUS_STATUS_OFFLINE; @@ -77,24 +73,6 @@ const DashboardStatus: FC = () => { const { me } = useContext(AuthenticatedContext); - const showName = (id: any) => { - const name: keyof Translation['STATUS_NAMES'] = id; - return LL.STATUS_NAMES[name](); - }; - - const busStatus = ({ status }: Status) => { - switch (status) { - case busConnectionStatus.BUS_STATUS_CONNECTED: - return LL.CONNECTED(0); - case busConnectionStatus.BUS_STATUS_TX_ERRORS: - return LL.TX_ISSUES(); - case busConnectionStatus.BUS_STATUS_OFFLINE: - return LL.DISCONNECTED(); - default: - return 'Unknown'; - } - }; - const stats_theme = tableTheme({ Table: ` --data-table-library_grid-template-columns: repeat(1, minmax(0, 1fr)) 90px 90px 80px; @@ -138,18 +116,11 @@ const DashboardStatus: FC = () => { return () => { clearInterval(timer); }; - // eslint-disable-next-line - }, []); + }); - const scan = async () => { - try { - await EMSESP.scanDevices(); - toast.info(LL.SCANNING() + '...'); - } catch (error) { - toast.error(extractErrorMessage(error, LL.PROBLEM_UPDATING())); - } finally { - setConfirmScan(false); - } + const showName = (id: any) => { + const name: keyof Translation['STATUS_NAMES'] = id; + return LL.STATUS_NAMES[name](); }; const formatDurationSec = (duration_sec: number) => { @@ -172,6 +143,31 @@ const DashboardStatus: FC = () => { return formatted; }; + const busStatus = () => { + if (data) { + switch (data.status) { + case busConnectionStatus.BUS_STATUS_CONNECTED: + return LL.CONNECTED(0) + ' (' + formatDurationSec(data.uptime) + ')'; + case busConnectionStatus.BUS_STATUS_TX_ERRORS: + return LL.TX_ISSUES(); + case busConnectionStatus.BUS_STATUS_OFFLINE: + return LL.DISCONNECTED(); + } + } + return 'Unknown'; + }; + + const scan = async () => { + try { + await EMSESP.scanDevices(); + toast.info(LL.SCANNING() + '...'); + } catch (error) { + toast.error(extractErrorMessage(error, LL.PROBLEM_UPDATING())); + } finally { + setConfirmScan(false); + } + }; + const renderScanDialog = () => ( setConfirmScan(false)}> {LL.SCAN_DEVICES()} @@ -201,10 +197,7 @@ const DashboardStatus: FC = () => { - + diff --git a/interface/src/project/DeviceIcon.tsx b/interface/src/project/DeviceIcon.tsx index d199db075..e471928f5 100644 --- a/interface/src/project/DeviceIcon.tsx +++ b/interface/src/project/DeviceIcon.tsx @@ -1,43 +1,21 @@ -import { FC } from 'react'; - +import { AiOutlineControl, AiOutlineGateway, AiOutlineAlert, AiOutlineChrome } from 'react-icons/ai'; import { CgSmartHomeBoiler } from 'react-icons/cg'; + import { FaSolarPanel } from 'react-icons/fa'; -import { MdThermostatAuto, MdOutlineSensors, MdOutlineExtension } from 'react-icons/md'; import { GiHeatHaze } from 'react-icons/gi'; +import { MdThermostatAuto, MdOutlineSensors, MdOutlineExtension } from 'react-icons/md'; import { TiFlowSwitch } from 'react-icons/ti'; import { VscVmConnect } from 'react-icons/vsc'; -import { AiOutlineControl, AiOutlineGateway, AiOutlineAlert, AiOutlineChrome } from 'react-icons/ai'; +import { DeviceType } from './types'; +import type { FC } from 'react'; interface DeviceIconProps { type_id: number; } -// matches emsdevice.h DeviceType -const enum DeviceType { - SYSTEM = 0, - DALLASSENSOR, - ANALOGSENSOR, - SCHEDULER, - BOILER, - THERMOSTAT, - MIXER, - SOLAR, - HEATPUMP, - GATEWAY, - SWITCH, - CONTROLLER, - CONNECT, - ALERT, - PUMP, - GENERIC, - HEATSOURCE, - CUSTOM, - UNKNOWN -} - const DeviceIcon: FC = ({ type_id }) => { switch (type_id) { - case DeviceType.DALLASSENSOR: + case DeviceType.TEMPERATURESENSOR: case DeviceType.ANALOGSENSOR: return ; case DeviceType.BOILER: diff --git a/interface/src/project/EntityMaskToggle.tsx b/interface/src/project/EntityMaskToggle.tsx new file mode 100644 index 000000000..f02af3cea --- /dev/null +++ b/interface/src/project/EntityMaskToggle.tsx @@ -0,0 +1,81 @@ +import { ToggleButton, ToggleButtonGroup } from '@mui/material'; +import OptionIcon from './OptionIcon'; +import { DeviceEntityMask } from './types'; +import type { DeviceEntity } from './types'; + +type EntityMaskToggleProps = { + onUpdate: (de: DeviceEntity) => void; + de: DeviceEntity; +}; + +const EntityMaskToggle = ({ onUpdate, de }: EntityMaskToggleProps) => { + const getMaskNumber = (newMask: string[]) => { + let new_mask = 0; + for (const entry of newMask) { + new_mask |= Number(entry); + } + return new_mask; + }; + + const getMaskString = (m: number) => { + const new_masks: string[] = []; + if ((m & 1) === 1) { + new_masks.push('1'); + } + if ((m & 2) === 2) { + new_masks.push('2'); + } + if ((m & 4) === 4) { + new_masks.push('4'); + } + if ((m & 8) === 8) { + new_masks.push('8'); + } + if ((m & 128) === 128) { + new_masks.push('128'); + } + return new_masks; + }; + + return ( + { + de.m = getMaskNumber(mask); + if (de.n === '' && de.m & DeviceEntityMask.DV_READONLY) { + de.m = de.m | DeviceEntityMask.DV_WEB_EXCLUDE; + } + if (de.m & DeviceEntityMask.DV_WEB_EXCLUDE) { + de.m = de.m & ~DeviceEntityMask.DV_FAVORITE; + } + onUpdate(de); + }} + > + + + + = 3}> + + + + + + + + + + + + + ); +}; + +export default EntityMaskToggle; diff --git a/interface/src/project/Help.tsx b/interface/src/project/Help.tsx index 3d7238239..d5b355823 100644 --- a/interface/src/project/Help.tsx +++ b/interface/src/project/Help.tsx @@ -1,14 +1,12 @@ -import { FC } from 'react'; -import { Navigate, Route, Routes } from 'react-router-dom'; - import { Tab } from '@mui/material'; +import { Navigate, Route, Routes } from 'react-router-dom'; +import HelpInformation from './HelpInformation'; +import type { FC } from 'react'; import { RouterTabs, useRouterTab, useLayoutTitle } from 'components'; import { useI18nContext } from 'i18n/i18n-react'; -import HelpInformation from './HelpInformation'; - const Help: FC = () => { const { LL } = useI18nContext(); const { routerTab } = useRouterTab(); diff --git a/interface/src/project/HelpInformation.tsx b/interface/src/project/HelpInformation.tsx index 4b0c6288b..e849ac19e 100644 --- a/interface/src/project/HelpInformation.tsx +++ b/interface/src/project/HelpInformation.tsx @@ -1,22 +1,17 @@ -import { FC } from 'react'; - +import CommentIcon from '@mui/icons-material/CommentTwoTone'; +import EastIcon from '@mui/icons-material/East'; +import DownloadIcon from '@mui/icons-material/GetApp'; +import GitHubIcon from '@mui/icons-material/GitHub'; +import MenuBookIcon from '@mui/icons-material/MenuBookTwoTone'; import { Typography, Button, Box, List, ListItem, ListItemText, Link, ListItemAvatar } from '@mui/material'; +import { toast } from 'react-toastify'; +import * as EMSESP from './api'; +import type { FC } from 'react'; import { SectionContent } from 'components'; -import { toast } from 'react-toastify'; - -import CommentIcon from '@mui/icons-material/CommentTwoTone'; -import MenuBookIcon from '@mui/icons-material/MenuBookTwoTone'; -import GitHubIcon from '@mui/icons-material/GitHub'; -import DownloadIcon from '@mui/icons-material/GetApp'; -import EastIcon from '@mui/icons-material/East'; - -import { extractErrorMessage } from 'utils'; - import { useI18nContext } from 'i18n/i18n-react'; - -import * as EMSESP from './api'; +import { extractErrorMessage } from 'utils'; const HelpInformation: FC = () => { const { LL } = useI18nContext(); diff --git a/interface/src/project/OptionIcon.tsx b/interface/src/project/OptionIcon.tsx index 220f331c4..f05f4c3c9 100644 --- a/interface/src/project/OptionIcon.tsx +++ b/interface/src/project/OptionIcon.tsx @@ -1,20 +1,18 @@ -import { FC } from 'react'; -import { SvgIconProps } from '@mui/material'; - -import EditOutlinedIcon from '@mui/icons-material/EditOutlined'; +import CommentsDisabledOutlinedIcon from '@mui/icons-material/CommentsDisabledOutlined'; +import DeleteForeverIcon from '@mui/icons-material/DeleteForever'; +import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline'; import EditOffOutlinedIcon from '@mui/icons-material/EditOffOutlined'; +import EditOutlinedIcon from '@mui/icons-material/EditOutlined'; +import InsertCommentOutlinedIcon from '@mui/icons-material/InsertCommentOutlined'; import StarIcon from '@mui/icons-material/Star'; import StarOutlineIcon from '@mui/icons-material/StarOutline'; import VisibilityOffOutlinedIcon from '@mui/icons-material/VisibilityOffOutlined'; import VisibilityOutlinedIcon from '@mui/icons-material/VisibilityOutlined'; -import CommentsDisabledOutlinedIcon from '@mui/icons-material/CommentsDisabledOutlined'; -import InsertCommentOutlinedIcon from '@mui/icons-material/InsertCommentOutlined'; - -import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline'; -import DeleteForeverIcon from '@mui/icons-material/DeleteForever'; +import type { SvgIconProps } from '@mui/material'; +import type { FC } from 'react'; type OptionType = 'deleted' | 'readonly' | 'web_exclude' | 'api_mqtt_exclude' | 'favorite'; @@ -34,9 +32,9 @@ interface OptionIconProps { const OptionIcon: FC = ({ type, isSet }) => { const Icon = OPTION_ICONS[type][isSet ? 0 : 1]; return isSet ? ( - + ) : ( - + ); }; diff --git a/interface/src/project/Settings.tsx b/interface/src/project/Settings.tsx index 1b5a62c89..29edb1c13 100644 --- a/interface/src/project/Settings.tsx +++ b/interface/src/project/Settings.tsx @@ -1,16 +1,13 @@ -import { FC } from 'react'; -import { Navigate, Route, Routes } from 'react-router-dom'; - import { Tab } from '@mui/material'; - -import { RouterTabs, useRouterTab, useLayoutTitle } from 'components'; - -import { useI18nContext } from 'i18n/i18n-react'; +import { Navigate, Route, Routes } from 'react-router-dom'; import SettingsApplication from './SettingsApplication'; import SettingsCustomization from './SettingsCustomization'; -import SettingsScheduler from './SettingsScheduler'; import SettingsEntities from './SettingsEntities'; +import SettingsScheduler from './SettingsScheduler'; +import type { FC } from 'react'; +import { RouterTabs, useRouterTab, useLayoutTitle } from 'components'; +import { useI18nContext } from 'i18n/i18n-react'; const Settings: FC = () => { const { LL } = useI18nContext(); @@ -24,7 +21,7 @@ const Settings: FC = () => { - + } /> diff --git a/interface/src/project/SettingsApplication.tsx b/interface/src/project/SettingsApplication.tsx index 78481bebd..facc4a54b 100644 --- a/interface/src/project/SettingsApplication.tsx +++ b/interface/src/project/SettingsApplication.tsx @@ -1,17 +1,16 @@ -import { FC, useState } from 'react'; -import { ValidateFieldsError } from 'async-validator'; - -import { toast } from 'react-toastify'; - -import { Box, Button, Checkbox, MenuItem, Grid, Typography, Divider, InputAdornment } from '@mui/material'; - -import WarningIcon from '@mui/icons-material/Warning'; import CancelIcon from '@mui/icons-material/Cancel'; import PowerSettingsNewIcon from '@mui/icons-material/PowerSettingsNew'; +import WarningIcon from '@mui/icons-material/Warning'; +import { Box, Button, Checkbox, MenuItem, Grid, Typography, Divider, InputAdornment, TextField } from '@mui/material'; +import { useState } from 'react'; +import { toast } from 'react-toastify'; -import { validate } from 'validators'; +import * as EMSESP from './api'; +import { BOARD_PROFILES } from './types'; import { createSettingsValidator } from './validators'; - +import type { Settings } from './types'; +import type { ValidateFieldsError } from 'async-validator'; +import type { FC } from 'react'; import { SectionContent, FormLoader, @@ -21,13 +20,11 @@ import { MessageBox, BlockNavigation } from 'components'; -import { numberValue, extractErrorMessage, updateValueDirty, useRest } from 'utils'; -import * as EMSESP from './api'; -import { Settings, BOARD_PROFILES } from './types'; - -import { useI18nContext } from 'i18n/i18n-react'; import RestartMonitor from 'framework/system/RestartMonitor'; +import { useI18nContext } from 'i18n/i18n-react'; +import { numberValue, extractErrorMessage, updateValueDirty, useRest } from 'utils'; +import { validate } from 'validators'; export function boardProfileSelectItems() { return Object.keys(BOARD_PROFILES).map((code) => ( @@ -98,7 +95,7 @@ const SettingsApplication: FC = () => { try { setFieldErrors(undefined); await validate(createSettingsValidator(data), data); - saveData(); + await saveData(); } catch (errors: any) { setFieldErrors(errors); } @@ -113,12 +110,12 @@ const SettingsApplication: FC = () => { board_profile: boardProfile }); } else { - updateBoardProfile(boardProfile); + void updateBoardProfile(boardProfile); } }; const restart = async () => { - validateAndSubmit(); + await validateAndSubmit(); try { await EMSESP.restart(); setRestarting(true); @@ -135,7 +132,7 @@ const SettingsApplication: FC = () => { {LL.BOARD_PROFILE_TEXT()} - { {LL.CUSTOM()}… - + {data.board_profile === 'CUSTOM' && ( <> { /> - { {LL.DISABLED(1)} LAN8720 TLK110 - +
{data.phy_type !== 0 && ( @@ -260,7 +257,7 @@ const SettingsApplication: FC = () => { alignItems="flex-start" > - { /> - { /> - { GPIO0_OUT GPIO16_OUT GPIO17_OUT - + )} @@ -312,7 +309,7 @@ const SettingsApplication: FC = () => { - { EMS+ HT3 {LL.HARDWARE()} - + - { Gateway 4 (0x4B) Gateway 5 (0x4C) Gateway 7 (0x4D) - + {LL.GENERAL_OPTIONS()} - { Polski (PL) Svenska (SV) Türk (TR) - + {data.led_gpio !== 0 && ( { - { {LL.ONOFF_CAP()} true/false 1/0 - + - { true/false "1"/"0" 1/0 - + - { > {LL.VALUE(1)} {LL.INDEX()} - + {data.dallas_gpio !== 0 && ( @@ -593,7 +590,7 @@ const SettingsApplication: FC = () => { /> - { INFO DEBUG ALL - + { )} - {!restartNeeded && dirtyFlags && dirtyFlags.length !== 0 && ( diff --git a/interface/src/project/SettingsCustomization.tsx b/interface/src/project/SettingsCustomization.tsx index 18e9f1cd0..ec1076b24 100644 --- a/interface/src/project/SettingsCustomization.tsx +++ b/interface/src/project/SettingsCustomization.tsx @@ -1,7 +1,8 @@ -import { FC, useState, useEffect, useCallback } from 'react'; - -import { unstable_useBlocker as useBlocker } from 'react-router-dom'; - +import CancelIcon from '@mui/icons-material/Cancel'; +import PowerSettingsNewIcon from '@mui/icons-material/PowerSettingsNew'; +import SearchIcon from '@mui/icons-material/Search'; +import SettingsBackupRestoreIcon from '@mui/icons-material/SettingsBackupRestore'; +import WarningIcon from '@mui/icons-material/Warning'; import { Button, Typography, @@ -13,92 +14,49 @@ import { DialogTitle, ToggleButton, ToggleButtonGroup, - Tooltip, Grid, TextField, - Link + Link, + InputAdornment } 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 { useTheme } from '@table-library/react-table-library/theme'; +import { useState, useEffect, useCallback } from 'react'; +import { unstable_useBlocker as useBlocker } from 'react-router-dom'; import { toast } from 'react-toastify'; -import WarningIcon from '@mui/icons-material/Warning'; -import CancelIcon from '@mui/icons-material/Cancel'; -import DoneIcon from '@mui/icons-material/Done'; - -import SettingsBackupRestoreIcon from '@mui/icons-material/SettingsBackupRestore'; -import SearchIcon from '@mui/icons-material/Search'; -import FilterListIcon from '@mui/icons-material/FilterList'; -import PowerSettingsNewIcon from '@mui/icons-material/PowerSettingsNew'; - +import EntityMaskToggle from './EntityMaskToggle'; import OptionIcon from './OptionIcon'; - -import { ButtonRow, FormLoader, ValidatedTextField, SectionContent, MessageBox, BlockNavigation } from 'components'; +import SettingsCustomizationDialog from './SettingsCustomizationDialog'; import * as EMSESP from './api'; -import { extractErrorMessage, updateValue } from 'utils'; - -import { DeviceShort, Devices, DeviceEntity, DeviceEntityMask } from './types'; - -import { useI18nContext } from 'i18n/i18n-react'; +import { DeviceEntityMask } from './types'; +import type { DeviceShort, Devices, DeviceEntity } from './types'; +import type { FC } from 'react'; +import { ButtonRow, FormLoader, SectionContent, MessageBox, BlockNavigation } from 'components'; import RestartMonitor from 'framework/system/RestartMonitor'; +import { useI18nContext } from 'i18n/i18n-react'; +import { extractErrorMessage } from 'utils'; export const APIURL = window.location.origin + '/api/'; const SettingsCustomization: FC = () => { const { LL } = useI18nContext(); - - const emptyDeviceEntity = { id: '', v: 0, n: '', cn: '', m: 0, w: false }; - const [numChanges, setNumChanges] = useState(0); const blocker = useBlocker(numChanges !== 0); - const [restarting, setRestarting] = useState(false); const [restartNeeded, setRestartNeeded] = useState(false); - const [deviceEntities, setDeviceEntities] = useState([emptyDeviceEntity]); + const [deviceEntities, setDeviceEntities] = useState(); const [devices, setDevices] = useState(); const [errorMessage, setErrorMessage] = useState(); const [selectedDevice, setSelectedDevice] = useState(-1); const [confirmReset, setConfirmReset] = useState(false); const [selectedFilters, setSelectedFilters] = useState(0); const [search, setSearch] = useState(''); - const [deviceEntity, setDeviceEntity] = useState(); - // eslint-disable-next-line - const [masks, setMasks] = useState(() => ['']); - - function hasEntityChanged(de: DeviceEntity) { - return (de?.cn || '') !== (de?.o_cn || '') || de.m !== de.o_m || de.ma !== de.o_ma || de.mi !== de.o_mi; - } - - const getChanges = () => { - if (!deviceEntities || selectedDevice === -1) { - return []; - } - - return deviceEntities - .filter((de) => hasEntityChanged(de)) - .map( - (new_de) => - new_de.m.toString(16).padStart(2, '0') + - new_de.id + - (new_de.cn || new_de.mi || new_de.ma ? '|' : '') + - (new_de.cn ? new_de.cn : '') + - (new_de.mi ? '>' + new_de.mi : '') + - (new_de.ma ? '<' + new_de.ma : '') - ); - }; - - const countChanges = () => { - setNumChanges(getChanges().length); - }; - - useEffect(() => { - countChanges(); - }); + const [selectedDeviceEntity, setSelectedDeviceEntity] = useState(); + const [dialogOpen, setDialogOpen] = useState(false); const entities_theme = useTheme({ Table: ` @@ -125,12 +83,10 @@ const SettingsCustomization: FC = () => { text-transform: uppercase; background-color: black; color: #90CAF9; - .th { border-bottom: 1px solid #565656; height: 36px; } - &:nth-of-type(1) .th { text-align: center; } @@ -139,21 +95,17 @@ const SettingsCustomization: FC = () => { background-color: #1e1e1e; position: relative; cursor: pointer; - .td { border-top: 1px solid #565656; border-bottom: 1px solid #565656; } - &.tr.tr-body.row-select.row-select-single-selected { background-color: #3d4752; } - &:hover .td { border-top: 1px solid #177ac9; border-bottom: 1px solid #177ac9; } - &:nth-of-type(odd) .td { background-color: #303030; } @@ -174,6 +126,28 @@ const SettingsCustomization: FC = () => { ` }); + function hasEntityChanged(de: DeviceEntity) { + return (de?.cn || '') !== (de?.o_cn || '') || de.m !== de.o_m || de.ma !== de.o_ma || de.mi !== de.o_mi; + } + + useEffect(() => { + if (deviceEntities) { + setNumChanges( + deviceEntities + .filter((de) => hasEntityChanged(de)) + .map( + (new_de) => + new_de.m.toString(16).padStart(2, '0') + + new_de.id + + (new_de.cn || new_de.mi || new_de.ma ? '|' : '') + + (new_de.cn ? new_de.cn : '') + + (new_de.mi ? '>' + new_de.mi : '') + + (new_de.ma ? '<' + new_de.ma : '') + ).length + ); + } + }, [deviceEntities]); + const fetchDevices = useCallback(async () => { try { setDevices((await EMSESP.readDevices()).data); @@ -182,6 +156,10 @@ const SettingsCustomization: FC = () => { } }, [LL]); + useEffect(() => { + void fetchDevices(); + }, [fetchDevices]); + const setOriginalSettings = (data: DeviceEntity[]) => { setDeviceEntities(data.map((de) => ({ ...de, o_m: de.m, o_cn: de.cn, o_mi: de.mi, o_ma: de.ma }))); }; @@ -195,10 +173,6 @@ const SettingsCustomization: FC = () => { } }; - useEffect(() => { - fetchDevices(); - }, [fetchDevices]); - function formatValue(value: any) { if (typeof value === 'number') { return new Intl.NumberFormat().format(value); @@ -252,7 +226,7 @@ const SettingsCustomization: FC = () => { const maskDisabled = (set: boolean) => { setDeviceEntities( - deviceEntities.map(function (de) { + deviceEntities?.map(function (de) { if ((de.m & selectedFilters || !selectedFilters) && de.id.toLowerCase().includes(search.toLowerCase())) { return { ...de, @@ -272,7 +246,7 @@ const SettingsCustomization: FC = () => { const selected_device = parseInt(event.target.value, 10); setSelectedDevice(selected_device); setNumChanges(0); - fetchDeviceEntities(devices?.devices[selected_device].i); + void fetchDeviceEntities(devices?.devices[selected_device].i); setRestartNeeded(false); } }; @@ -297,9 +271,45 @@ const SettingsCustomization: FC = () => { } }; + const onDialogClose = () => { + setDialogOpen(false); + }; + + const updateDeviceEntity = (updatedItem: DeviceEntity) => { + setDeviceEntities(deviceEntities?.map((de) => (de.id === updatedItem.id ? { ...de, ...updatedItem } : de))); + }; + + const onDialogSave = (updatedItem: DeviceEntity) => { + setDialogOpen(false); + updateDeviceEntity(updatedItem); + }; + + const editDeviceEntity = useCallback((de: DeviceEntity) => { + if (de.n === undefined || (de.n && de.n[0] === '!')) { + return; + } + + if (de.cn === undefined) { + de.cn = ''; + } + + setSelectedDeviceEntity(de); + setDialogOpen(true); + }, []); + const saveCustomization = async () => { if (devices && deviceEntities && selectedDevice !== -1) { - const masked_entities = getChanges(); + const masked_entities = deviceEntities + .filter((de) => hasEntityChanged(de)) + .map( + (new_de) => + new_de.m.toString(16).padStart(2, '0') + + new_de.id + + (new_de.cn || new_de.mi || new_de.ma ? '|' : '') + + (new_de.cn ? new_de.cn : '') + + (new_de.mi ? '>' + new_de.mi : '') + + (new_de.ma ? '<' + new_de.ma : '') + ); // check size in bytes to match buffer in CPP, which is 2048 const bytes = new TextEncoder().encode(JSON.stringify(masked_entities)).length; @@ -335,8 +345,8 @@ const SettingsCustomization: FC = () => { return ( <> - {LL.CUSTOMIZATIONS_HELP_1()} - + {LL.CUSTOMIZATIONS_HELP_1()}. + ={LL.CUSTOMIZATIONS_HELP_2()}   ={LL.CUSTOMIZATIONS_HELP_3()}   ={LL.CUSTOMIZATIONS_HELP_4()}   @@ -344,7 +354,7 @@ const SettingsCustomization: FC = () => { ={LL.CUSTOMIZATIONS_HELP_6()} - { {device.s} ))} - + ); }; - const editEntity = (de: DeviceEntity) => { - if (de.n === undefined || (de.n && de.n[0] === '!')) { + const renderDeviceData = () => { + if (!deviceEntities) { return; } - if (de.cn === undefined) { - de.cn = ''; - } - setDeviceEntity(de); - }; - - const updateEntity = () => { - if (deviceEntity) { - setDeviceEntities((prevState) => { - const newState = prevState.map((obj) => { - if (obj.id === deviceEntity.id) { - return { ...obj, cn: deviceEntity.cn, mi: deviceEntity.mi, ma: deviceEntity.ma }; - } - return obj; - }); - return newState; - }); - } - - setDeviceEntity(undefined); - }; - - const renderDeviceData = () => { if (devices?.devices.length === 0 || deviceEntities[0].id === '') { return; } @@ -407,33 +394,23 @@ const SettingsCustomization: FC = () => { return ( <> - - - #: - - - - - {shown_data.length}/{deviceEntities.length} - - - - : - { setSearch(event.target.value); }} + InputProps={{ + startAdornment: ( + + + + ) + }} /> - - - : - - { - + + + {LL.SHOWING()} {shown_data.length}/{deviceEntities.length} + + {(tableList: any) => ( @@ -502,63 +483,14 @@ const SettingsCustomization: FC = () => { {tableList.map((de: DeviceEntity) => ( - editEntity(de)}> + editDeviceEntity(de)}> - {!deviceEntity && ( - { - de.m = getMaskNumber(mask); - if (de.n === '' && de.m & DeviceEntityMask.DV_READONLY) { - de.m = de.m | DeviceEntityMask.DV_WEB_EXCLUDE; - } - if (de.m & DeviceEntityMask.DV_WEB_EXCLUDE) { - de.m = de.m & ~DeviceEntityMask.DV_FAVORITE; - } - setMasks(['']); // forces a refresh - }} - > - - - - = 3}> - - - - - - - - - - - - - )} + - {!deviceEntity && formatName(de)} - {!deviceEntity && !(de.m & DeviceEntityMask.DV_READONLY) && formatValue(de.mi)} - {!deviceEntity && !(de.m & DeviceEntityMask.DV_READONLY) && formatValue(de.ma)} - {!deviceEntity && formatValue(de.v)} + {formatName(de)} + {!(de.m & DeviceEntityMask.DV_READONLY) && formatValue(de.mi)} + {!(de.m & DeviceEntityMask.DV_READONLY) && formatValue(de.ma)} + {formatValue(de.v)} ))} @@ -584,139 +516,72 @@ const SettingsCustomization: FC = () => { ); - const renderContent = () => { - return ( - <> - - {LL.DEVICE_ENTITIES()} - - {renderDeviceList()} - {renderDeviceData()} - {restartNeeded && ( - - - - )} - {!restartNeeded && ( - - - {numChanges !== 0 && ( - - - - - )} - - - - + const renderContent = () => ( + <> + + {LL.DEVICE_ENTITIES()} + + {renderDeviceList()} + {renderDeviceData()} + {restartNeeded && ( + + + + )} + {!restartNeeded && ( + + + {numChanges !== 0 && ( + + + + + )} - )} - {renderResetDialog()} - - ); - }; - - const renderEditDialog = () => { - if (deviceEntity) { - return ( - setDeviceEntity(undefined)}> - {LL.EDIT() + ' ' + LL.ENTITY() + ' "' + deviceEntity.id + '"'} - - - - {LL.DEFAULT(1) + ' ' + LL.NAME(1)}: {deviceEntity.n} - - - - - - - {typeof deviceEntity.v === 'number' && - deviceEntity.w && - !(deviceEntity.m & DeviceEntityMask.DV_READONLY) && ( - <> - - - - - - - - )} - - - + - - - - ); - } - }; + + + )} + {renderResetDialog()} + + ); return ( {blocker ? : null} {restarting ? : renderContent()} - {renderEditDialog()} + {selectedDeviceEntity && ( + + )} ); }; diff --git a/interface/src/project/SettingsCustomizationDialog.tsx b/interface/src/project/SettingsCustomizationDialog.tsx new file mode 100644 index 000000000..8d48f57fc --- /dev/null +++ b/interface/src/project/SettingsCustomizationDialog.tsx @@ -0,0 +1,134 @@ +import CancelIcon from '@mui/icons-material/Cancel'; +import DoneIcon from '@mui/icons-material/Done'; + +import { + Box, + Button, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + Grid, + TextField, + Typography +} from '@mui/material'; +import { useEffect, useState } from 'react'; + +import EntityMaskToggle from './EntityMaskToggle'; +import { DeviceEntityMask } from './types'; +import type { DeviceEntity } from './types'; + +import { useI18nContext } from 'i18n/i18n-react'; + +import { updateValue } from 'utils'; + +type SettingsCustomizationDialogProps = { + open: boolean; + onClose: () => void; + onSave: (di: DeviceEntity) => void; + selectedItem: DeviceEntity; +}; + +const SettingsCustomizationDialog = ({ open, onClose, onSave, selectedItem }: SettingsCustomizationDialogProps) => { + const { LL } = useI18nContext(); + const [editItem, setEditItem] = useState(selectedItem); + const [error, setError] = useState(false); + + const updateFormValue = updateValue(setEditItem); + + const isWriteableNumber = + typeof editItem.v === 'number' && editItem.w && !(editItem.m & DeviceEntityMask.DV_READONLY); + + useEffect(() => { + if (open) { + setError(false); + setEditItem(selectedItem); + } + }, [open, selectedItem]); + + const close = () => { + onClose(); + }; + + const save = () => { + if (isWriteableNumber && editItem.mi && editItem.ma && editItem.mi > editItem?.ma) { + setError(true); + } else { + onSave(editItem); + } + }; + + const updateDeviceEntity = (updatedItem: DeviceEntity) => { + setEditItem({ ...editItem, m: updatedItem.m }); + }; + + return ( + + {LL.EDIT() + ' ' + LL.ENTITY()} + + + {editItem.id} + + + + {LL.DEFAULT(1) + ' ' + LL.ENTITY_NAME()}: {editItem.n} + + + + + + + + + + {isWriteableNumber && ( + <> + + + + + + + + )} + + {error && ( + + Error: Check min and max values + + )} + + + + + + + ); +}; + +export default SettingsCustomizationDialog; diff --git a/interface/src/project/SettingsEntities.tsx b/interface/src/project/SettingsEntities.tsx index 4dcdead7b..9d38e570a 100644 --- a/interface/src/project/SettingsEntities.tsx +++ b/interface/src/project/SettingsEntities.tsx @@ -1,79 +1,55 @@ -import { FC, useState, useEffect, useCallback } from 'react'; -import { unstable_useBlocker as useBlocker } from 'react-router-dom'; - -import { - Button, - Typography, - Box, - Grid, - Dialog, - DialogActions, - DialogContent, - DialogTitle, - MenuItem, - Checkbox, - InputAdornment -} from '@mui/material'; - -import { useTheme } from '@table-library/react-table-library/theme'; +import AddIcon from '@mui/icons-material/Add'; +import CancelIcon from '@mui/icons-material/Cancel'; +import WarningIcon from '@mui/icons-material/Warning'; +import { Button, Typography, Box } from '@mui/material'; import { Table, Header, HeaderRow, HeaderCell, Body, Row, Cell } from '@table-library/react-table-library/table'; +import { useTheme } from '@table-library/react-table-library/theme'; +import { useState, useEffect, useCallback } from 'react'; +import { unstable_useBlocker as useBlocker } from 'react-router-dom'; 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, - BlockFormControlLabel -} from 'components'; - -import { DeviceValueUOM_s, EntityItem } from './types'; -import { extractErrorMessage, updateValue } from 'utils'; - -import { validate } from 'validators'; +import SettingsEntitiesDialog from './SettingsEntitiesDialog'; +import * as EMSESP from './api'; +import { DeviceValueUOM_s } from './types'; import { entityItemValidation } from './validators'; +import type { EntityItem } from './types'; +import type { FC } from 'react'; +import { ButtonRow, FormLoader, SectionContent, BlockNavigation } from 'components'; import { useI18nContext } from 'i18n/i18n-react'; - -import { ValidateFieldsError } from 'async-validator'; - -import * as EMSESP from './api'; +import { extractErrorMessage } from 'utils'; const SettingsEntities: FC = () => { - const { LL, locale } = useI18nContext(); - + const { LL } = useI18nContext(); const [numChanges, setNumChanges] = useState(0); const blocker = useBlocker(numChanges !== 0); - - const emptyEntity = { - device_id: '', - type_id: '', - offset: 0, - factor: 1, - uom: 0, - val_type: 2, - name: '', - write: false, - deleted: false - }; - const [entity, setEntity] = useState([emptyEntity]); - const [entityItem, setEntityItem] = useState(); + const [entities, setEntities] = useState(); + const [selectedEntityItem, setSelectedEntityItem] = useState(); const [errorMessage, setErrorMessage] = useState(); const [creating, setCreating] = useState(false); + const [dialogOpen, setDialogOpen] = useState(false); - const [fieldErrors, setFieldErrors] = useState(); + function hasEntityChanged(ei: EntityItem) { + return ( + ei.id !== ei.o_id || + (ei?.name || '') !== (ei?.o_name || '') || + ei.device_id !== ei.o_device_id || + ei.type_id !== ei.o_type_id || + ei.offset !== ei.o_offset || + ei.uom !== ei.o_uom || + ei.factor !== ei.o_factor || + ei.value_type !== ei.o_value_type || + ei.writeable !== ei.o_writeable || + ei.deleted !== ei.o_deleted + ); + } useEffect(() => { - setNumChanges(getNumChanges()); - }); + if (entities) { + setNumChanges(entities ? entities.filter((ei) => hasEntityChanged(ei)).length : 0); + } + }, [entities]); const entity_theme = useTheme({ Table: ` @@ -86,6 +62,9 @@ const SettingsEntities: FC = () => { } `, BaseCell: ` + &:nth-of-type(1) { + padding: 8px; + } &:nth-of-type(2) { text-align: center; } @@ -96,7 +75,7 @@ const SettingsEntities: FC = () => { text-align: center; } &:nth-of-type(5) { - text-align: right; + text-align: center; } `, HeaderRow: ` @@ -129,147 +108,135 @@ const SettingsEntities: FC = () => { const fetchEntities = useCallback(async () => { try { const response = await EMSESP.readEntities(); - setOriginalEntity(response.data.entity); + setEntities( + response.data.entities.map((ei) => ({ + ...ei, + o_id: ei.id, + o_device_id: ei.device_id, + o_type_id: ei.type_id, + o_offset: ei.offset, + o_factor: ei.factor, + o_uom: ei.uom, + o_value_type: ei.value_type, + o_name: ei.name, + o_writeable: ei.writeable, + o_deleted: ei.deleted + })) + ); } catch (error) { setErrorMessage(extractErrorMessage(error, LL.PROBLEM_LOADING())); } }, [LL]); useEffect(() => { - fetchEntities(); + void fetchEntities(); }, [fetchEntities]); - const setOriginalEntity = (data: EntityItem[]) => { - setEntity( - data.map((ei) => ({ - ...ei, - o_device_id: ei.device_id, - o_type_id: ei.type_id, - o_offset: ei.offset, - o_factor: ei.factor, - o_uom: ei.uom, - o_val_type: ei.val_type, - o_name: ei.name, - o_write: ei.write, - o_deleted: ei.deleted - })) - ); - }; - - function hasEntityChanged(ei: EntityItem) { - return ( - ei.device_id !== ei.o_device_id || - ei.type_id !== ei.o_type_id || - ei.name !== ei.o_name || - ei.offset !== ei.o_offset || - ei.uom !== ei.o_uom || - ei.factor !== ei.o_factor || - ei.val_type !== ei.o_val_type || - ei.write !== ei.o_write || - ei.deleted !== ei.o_deleted - ); - } - - const getNumChanges = () => { - if (!entity) { - return 0; - } - return entity.filter((ei) => hasEntityChanged(ei)).length; - }; - - const saveEntity = async () => { - if (entity) { + const saveEntities = async () => { + if (entities) { try { const response = await EMSESP.writeEntities({ - entity: entity + entities: entities .filter((ei) => !ei.deleted) - .map((condensed_ei) => { - return { - device_id: condensed_ei.device_id, - type_id: condensed_ei.type_id, - offset: condensed_ei.offset, - factor: condensed_ei.factor, - val_type: condensed_ei.val_type, - uom: condensed_ei.uom, - name: condensed_ei.name, - write: condensed_ei.write - }; - }) + .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()); + toast.success(LL.ENTITIES_UPDATED()); } else { toast.error(LL.PROBLEM_UPDATING()); } + await fetchEntities(); } catch (error) { toast.error(extractErrorMessage(error, LL.PROBLEM_UPDATING())); } - setOriginalEntity(entity); } }; - 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) => { + setDialogOpen(false); + if (entities && creating) { + setEntities([...entities.filter((ei) => creating || ei.o_id !== updatedItem.o_id), updatedItem]); + } else { + setEntities(entities?.map((ei) => (ei.id === updatedItem.id ? { ...ei, ...updatedItem } : ei))); + } }; const addEntityItem = () => { setCreating(true); - setEntityItem(emptyEntity); - }; - - const updateEntityItem = () => { - if (entityItem) { - setEntity([...entity.filter((ei) => creating || ei.o_name !== entityItem.o_name), entityItem]); - } - setEntityItem(undefined); + setSelectedEntityItem({ + id: Math.floor(Math.random() * (Math.floor(200) - 100) + 100), + name: '', + device_id: '', + type_id: '', + offset: 0, + factor: 1, + uom: 0, + value_type: 0, + writeable: false, + deleted: false + }); + 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 value === undefined || uom === undefined + ? '' + : typeof value === 'number' + ? new Intl.NumberFormat().format(value) + (uom === 0 ? '' : ' ' + DeviceValueUOM_s[uom]) + : value; } - function showHex(value: string, digit: number) { - if (digit === 4) { - return '0x' + ('000' + value).slice(-4); - } - return '0x' + ('0' + value).slice(-2); + function showHex(value: number, digit: number) { + return digit === 4 + ? '0x' + ('000' + value.toString(16).toUpperCase()).slice(-4) + : '0x' + ('0' + value.toString(16).toUpperCase()).slice(-2); } const renderEntity = () => { - if (!entity) { + if (!entities) { return ; } return ( -
!ei.deleted).sort((a, b) => a.name.localeCompare(b.time)) }} - theme={entity_theme} - layout={{ custom: true }} - > +
!ei.deleted) }} theme={entity_theme} layout={{ custom: true }}> {(tableList: any) => ( <>
{LL.NAME(0)} - Device ID - Type ID + {LL.ID_OF(LL.DEVICE())} + {LL.ID_OF(LL.TYPE(1))} Offset - {LL.VALUE()} + {LL.VALUE(0)}
{tableList.map((ei: EntityItem) => ( editEntityItem(ei)}> {ei.name} - {showHex(ei.device_id, 2)} - {showHex(ei.type_id, 4)} + {showHex(ei.device_id as number, 2)} + {showHex(ei.type_id as number, 4)} {ei.offset} {formatValue(ei.value, ei.uom)} @@ -281,205 +248,38 @@ const SettingsEntities: FC = () => { ); }; - const removeEntityItem = (ei: EntityItem) => { - ei.deleted = true; - setEntityItem(ei); - updateEntityItem(); - }; - - const validateEntityItem = async () => { - if (entityItem) { - try { - setFieldErrors(undefined); - await validate(entityItemValidation(entity, entityItem), entityItem); - updateEntityItem(); - } catch (errors: any) { - setFieldErrors(errors); - } - } - }; - - const closeDialog = () => { - setEntityItem(undefined); - setFieldErrors(undefined); - }; - - const renderEditEntity = () => { - if (entityItem) { - return ( - closeDialog()}> - - {creating ? LL.ADD(1) + ' ' + LL.NEW() : LL.EDIT()} {LL.CUSTOM_ENTITIES()} - - - - - - - - - - - - - } - label={LL.WRITEABLE()} - /> - - - 0x - }} - /> - - - 0x - }} - /> - - - - - - - BOOL - INT - UINT - SHORT - USHORT - ULONG - TIME - - - {entityItem.val_type !== 0 && ( - <> - - - - - - {DeviceValueUOM_s.map((val, i) => ( - - {val} - - ))} - - - - )} - - - - {!creating && ( - - - - )} - - - - - ); - } - }; - return ( - + {blocker ? : null} {LL.ENTITIES_HELP_1()} + {renderEntity()} - {renderEditEntity()} + + {selectedEntityItem && ( + + )} + - {numChanges !== 0 && ( + {numChanges > 0 && ( - @@ -488,7 +288,7 @@ const SettingsEntities: FC = () => { - diff --git a/interface/src/project/SettingsEntitiesDialog.tsx b/interface/src/project/SettingsEntitiesDialog.tsx new file mode 100644 index 000000000..1ae09bbd0 --- /dev/null +++ b/interface/src/project/SettingsEntitiesDialog.tsx @@ -0,0 +1,235 @@ +import AddIcon from '@mui/icons-material/Add'; +import CancelIcon from '@mui/icons-material/Cancel'; +import DoneIcon from '@mui/icons-material/Done'; +import RemoveIcon from '@mui/icons-material/RemoveCircleOutline'; +import { + Box, + Button, + Checkbox, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + Grid, + InputAdornment, + MenuItem, + TextField +} from '@mui/material'; +import { useEffect, useState } from 'react'; + +import { DeviceValueUOM_s } from './types'; +import type { EntityItem } from './types'; +import type Schema from 'async-validator'; +import type { ValidateFieldsError } from 'async-validator'; + +import { BlockFormControlLabel, ValidatedTextField } from 'components'; + +import { useI18nContext } from 'i18n/i18n-react'; + +import { updateValue } from 'utils'; +import { validate } from 'validators'; + +type SettingsEntitiesDialogProps = { + open: boolean; + creating: boolean; + onClose: () => void; + onSave: (ei: EntityItem) => void; + selectedItem: EntityItem; + validator: Schema; +}; + +const SettingsEntitiesDialog = ({ + open, + creating, + onClose, + onSave, + selectedItem, + validator +}: SettingsEntitiesDialogProps) => { + const { LL } = useI18nContext(); + const [editItem, setEditItem] = useState(selectedItem); + const [fieldErrors, setFieldErrors] = useState(); + const updateFormValue = updateValue(setEditItem); + + useEffect(() => { + if (open) { + setFieldErrors(undefined); + setEditItem(selectedItem); + // convert to hex strings straight away + setEditItem({ + ...selectedItem, + device_id: selectedItem.device_id.toString(16).toUpperCase().slice(-2), + type_id: selectedItem.type_id.toString(16).toUpperCase().slice(-4) + }); + } + }, [open, selectedItem]); + + const close = () => { + onClose(); + }; + + const save = async () => { + try { + setFieldErrors(undefined); + await validate(validator, editItem); + if (typeof editItem.device_id === 'string') { + editItem.device_id = parseInt(editItem.device_id, 16); + } + if (typeof editItem.type_id === 'string') { + editItem.type_id = parseInt(editItem.type_id, 16); + } + onSave(editItem); + } catch (errors: any) { + setFieldErrors(errors); + } + }; + + const remove = () => { + editItem.deleted = true; + onSave(editItem); + }; + + return ( + + + {creating ? LL.ADD(1) + ' ' + LL.NEW(1) : LL.EDIT()} {LL.ENTITY()} + + + + + + + + + + + } + label={LL.WRITEABLE()} + /> + + + 0x }} + /> + + + 0x }} + /> + + + + + + + BOOL + INT + UINT + SHORT + USHORT + ULONG + TIME + + + + {editItem.value_type !== 0 && ( + <> + + + + + + {DeviceValueUOM_s.map((val, i) => ( + + {val} + + ))} + + + + )} + + + + + {!creating && ( + + + + )} + + + + + ); +}; + +export default SettingsEntitiesDialog; diff --git a/interface/src/project/SettingsScheduler.tsx b/interface/src/project/SettingsScheduler.tsx index 344a67a63..59fdee05b 100644 --- a/interface/src/project/SettingsScheduler.tsx +++ b/interface/src/project/SettingsScheduler.tsx @@ -1,96 +1,36 @@ -import { FC, useState, useEffect, useCallback } from 'react'; -import { unstable_useBlocker as useBlocker } from 'react-router-dom'; - -import { - Button, - Typography, - Box, - Stack, - Dialog, - DialogActions, - DialogContent, - DialogTitle, - ToggleButton, - ToggleButtonGroup, - Checkbox, - Grid, - TextField, - Divider -} 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 CancelIcon from '@mui/icons-material/Cancel'; import CheckCircleIcon from '@mui/icons-material/CheckCircle'; +import WarningIcon from '@mui/icons-material/Warning'; -import { - ValidatedTextField, - ButtonRow, - FormLoader, - BlockFormControlLabel, - SectionContent, - BlockNavigation -} from 'components'; - -import { extractErrorMessage, updateValue } from 'utils'; - -import { validate } from 'validators'; +import { Box, Typography, Divider, Stack, Button } from '@mui/material'; +import { Table, Header, HeaderRow, HeaderCell, Body, Row, Cell } from '@table-library/react-table-library/table'; +import { useTheme } from '@table-library/react-table-library/theme'; +import { useState, useEffect, useCallback } from 'react'; +import { unstable_useBlocker as useBlocker } from 'react-router-dom'; +import { toast } from 'react-toastify'; +import SettingsSchedulerDialog from './SettingsSchedulerDialog'; +import * as EMSESP from './api'; +import { ScheduleFlag } from './types'; import { schedulerItemValidation } from './validators'; -import { ValidateFieldsError } from 'async-validator'; +import type { ScheduleItem } from './types'; +import type { FC } from 'react'; -import { ScheduleItem, ScheduleFlag } from './types'; +import { ButtonRow, FormLoader, SectionContent, BlockNavigation } from 'components'; import { useI18nContext } from 'i18n/i18n-react'; - -import * as EMSESP from './api'; - -function makeid() { - return Math.floor(Math.random() * (Math.floor(200) - 100) + 100); -} +import { extractErrorMessage } from 'utils'; const SettingsScheduler: FC = () => { const { LL, locale } = useI18nContext(); - const [numChanges, setNumChanges] = useState(0); const blocker = useBlocker(numChanges !== 0); - - const emptySchedule = { - id: 0, - active: false, - deleted: false, - flags: 0, - time: '12:00', - cmd: '', - value: '', - name: '', - o_name: '' - }; - const [schedule, setSchedule] = useState([emptySchedule]); - const [scheduleItem, setScheduleItem] = useState(); + const [schedule, setSchedule] = useState([]); + const [selectedScheduleItem, setSelectedScheduleItem] = useState(); const [dow, setDow] = useState([]); const [errorMessage, setErrorMessage] = useState(); const [creating, setCreating] = useState(false); - - const [fieldErrors, setFieldErrors] = useState(); - - // eslint-disable-next-line - const [flags, setFlags] = useState(() => ['']); - - function getDayNames() { - const formatter = new Intl.DateTimeFormat(locale, { weekday: 'short', timeZone: 'UTC' }); - const days = [1, 2, 3, 4, 5, 6, 7].map((day) => { - const dd = day < 10 ? `0${day}` : day; - return new Date(`2017-01-${dd}T00:00:00+00:00`); - }); - return days.map((date) => formatter.format(date)); - } + const [dialogOpen, setDialogOpen] = useState(false); function hasScheduleChanged(si: ScheduleItem) { return ( @@ -105,16 +45,11 @@ const SettingsScheduler: FC = () => { ); } - const getNumChanges = () => { - if (!schedule) { - return 0; - } - return schedule.filter((si) => hasScheduleChanged(si)).length; - }; - useEffect(() => { - setNumChanges(getNumChanges()); - }); + if (schedule) { + setNumChanges(schedule ? schedule.filter((si) => hasScheduleChanged(si)).length : 0); + } + }, [schedule]); const schedule_theme = useTheme({ Table: ` @@ -161,72 +96,36 @@ const SettingsScheduler: FC = () => { ` }); - const setOriginalSchedule = (data: ScheduleItem[]) => { - setSchedule( - data.map((si) => ({ - ...si, - o_id: si.id, - o_active: si.active, - o_deleted: si.deleted, - o_flags: si.flags, - o_time: si.time, - o_cmd: si.cmd, - o_value: si.value, - o_name: si.name - })) - ); - }; - const fetchSchedule = useCallback(async () => { try { const response = await EMSESP.readSchedule(); - setOriginalSchedule(response.data.schedule); + setSchedule( + response.data.schedule.map((si) => ({ + ...si, + o_id: si.id, + o_active: si.active, + o_deleted: si.deleted, + o_flags: si.flags, + o_time: si.time, + o_cmd: si.cmd, + o_value: si.value, + o_name: si.name + })) + ); } catch (error) { setErrorMessage(extractErrorMessage(error, LL.PROBLEM_LOADING())); } - setDow(getDayNames()); }, [LL]); useEffect(() => { - fetchSchedule(); - }, [fetchSchedule]); - - const getFlagNumber = (newFlag: string[]) => { - let new_flag = 0; - for (const entry of newFlag) { - new_flag |= Number(entry); - } - return new_flag; - }; - - const getFlagString = (f: number) => { - const new_flags: string[] = []; - if ((f & 1) === 1) { - new_flags.push('1'); - } - if ((f & 2) === 2) { - new_flags.push('2'); - } - if ((f & 4) === 4) { - new_flags.push('4'); - } - if ((f & 8) === 8) { - new_flags.push('8'); - } - if ((f & 16) === 16) { - new_flags.push('16'); - } - if ((f & 32) === 32) { - new_flags.push('32'); - } - if ((f & 64) === 64) { - new_flags.push('64'); - } - if ((f & 128) === 128) { - new_flags.push('128'); - } - return new_flags; - }; + const formatter = new Intl.DateTimeFormat(locale, { weekday: 'short', timeZone: 'UTC' }); + const days = [1, 2, 3, 4, 5, 6, 7].map((day) => { + const dd = day < 10 ? `0${day}` : day; + return new Date(`2017-01-${dd}T00:00:00+00:00`); + }); + setDow(days.map((date) => formatter.format(date))); + void fetchSchedule(); + }, [locale, fetchSchedule]); const saveSchedule = async () => { if (schedule) { @@ -234,87 +133,51 @@ 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()); + toast.success(LL.SCHEDULE_UPDATED()); } else { toast.error(LL.PROBLEM_UPDATING()); } + await fetchSchedule(); } catch (error) { toast.error(extractErrorMessage(error, LL.PROBLEM_UPDATING())); } - setOriginalSchedule(schedule); } }; - function getFlagName(flag: number) { - if ((flag & ScheduleFlag.SCHEDULE_MON) === ScheduleFlag.SCHEDULE_MON) { - return dow[1]; - } - if ((flag & ScheduleFlag.SCHEDULE_TUE) === ScheduleFlag.SCHEDULE_TUE) { - return dow[2]; - } - if ((flag & ScheduleFlag.SCHEDULE_WED) === ScheduleFlag.SCHEDULE_WED) { - return dow[3]; - } - if ((flag & ScheduleFlag.SCHEDULE_THU) === ScheduleFlag.SCHEDULE_THU) { - return dow[4]; - } - if ((flag & ScheduleFlag.SCHEDULE_FRI) === ScheduleFlag.SCHEDULE_FRI) { - return dow[5]; - } - if ((flag & ScheduleFlag.SCHEDULE_SAT) === ScheduleFlag.SCHEDULE_SAT) { - return dow[6]; - } - if ((flag & ScheduleFlag.SCHEDULE_SUN) === ScheduleFlag.SCHEDULE_SUN) { - return dow[0]; - } - if ((flag & ScheduleFlag.SCHEDULE_TIMER) === ScheduleFlag.SCHEDULE_TIMER) { - return LL.TIMER(0); - } - return ''; - } - - const dayBox = (si: ScheduleItem, flag: number) => ( - <> - - - {getFlagName(flag)} - - - - - ); - - const showFlag = (si: ScheduleItem, flag: number) => ( - - {getFlagName(flag)} - - ); - - const editScheduleItem = (si: ScheduleItem) => { - if (si.name === undefined) { - si.name = ''; - } + const editScheduleItem = useCallback((si: ScheduleItem) => { setCreating(false); - setScheduleItem(si); + setSelectedScheduleItem(si); + setDialogOpen(true); + }, []); + + const onDialogClose = () => { + setDialogOpen(false); + }; + + const onDialogSave = (updatedItem: ScheduleItem) => { + setDialogOpen(false); + if (schedule && creating) { + setSchedule([...schedule.filter((si) => creating || si.o_id !== updatedItem.o_id), updatedItem]); + } else { + setSchedule(schedule?.map((si) => (si.id === updatedItem.id ? { ...si, ...updatedItem } : si))); + } }; const addScheduleItem = () => { setCreating(true); - setScheduleItem({ - id: makeid(), + setSelectedScheduleItem({ + id: Math.floor(Math.random() * (Math.floor(200) - 100) + 100), active: false, deleted: false, flags: 0, @@ -323,13 +186,7 @@ const SettingsScheduler: FC = () => { value: '', name: '' }); - }; - - const updateScheduleItem = () => { - if (scheduleItem) { - setSchedule([...schedule.filter((si) => creating || si.o_id !== scheduleItem.o_id), scheduleItem]); - } - setScheduleItem(undefined); + setDialogOpen(true); }; const renderSchedule = () => { @@ -337,6 +194,17 @@ const SettingsScheduler: FC = () => { return ; } + const dayBox = (si: ScheduleItem, flag: number) => ( + <> + + + {flag === ScheduleFlag.SCHEDULE_TIMER ? LL.TIMER(0) : dow[Math.log(flag) / Math.log(2)]} + + + + + ); + return (
!si.deleted).sort((a, b) => a.time.localeCompare(b.time)) }} @@ -387,173 +255,6 @@ const SettingsScheduler: FC = () => { ); }; - const removeScheduleItem = (si: ScheduleItem) => { - si.deleted = true; - setScheduleItem(si); - updateScheduleItem(); - }; - - const validateScheduleItem = async () => { - if (scheduleItem) { - try { - setFieldErrors(undefined); - await validate(schedulerItemValidation(schedule, scheduleItem), scheduleItem); - updateScheduleItem(); - } catch (errors: any) { - setFieldErrors(errors); - } - } - }; - - const closeDialog = () => { - setScheduleItem(undefined); - setFieldErrors(undefined); - }; - - const renderEditSchedule = () => { - if (scheduleItem) { - const isTimer = scheduleItem.flags === ScheduleFlag.SCHEDULE_TIMER; - return ( - closeDialog()}> - - {creating ? LL.ADD(1) + ' ' + LL.NEW() : LL.EDIT()} {LL.SCHEDULE(1)} - - - - - { - scheduleItem.flags = getFlagNumber(flag) & 127; - setFlags(['']); // forces refresh - }} - > - {showFlag(scheduleItem, ScheduleFlag.SCHEDULE_MON)} - {showFlag(scheduleItem, ScheduleFlag.SCHEDULE_TUE)} - {showFlag(scheduleItem, ScheduleFlag.SCHEDULE_WED)} - {showFlag(scheduleItem, ScheduleFlag.SCHEDULE_THU)} - {showFlag(scheduleItem, ScheduleFlag.SCHEDULE_FRI)} - {showFlag(scheduleItem, ScheduleFlag.SCHEDULE_SAT)} - {showFlag(scheduleItem, ScheduleFlag.SCHEDULE_SUN)} - - - - {isTimer ? ( - - ) : ( - - )} - - - - - } - label={LL.ACTIVE()} - /> - {scheduleItem.active && ( - - - - )} - - - - - {isTimer && ( - - {LL.SCHEDULER_HELP_2()} - - )} - - - - - - - {!creating && ( - - - - )} - - - - - ); - } - }; - return ( {blocker ? : null} @@ -561,19 +262,31 @@ const SettingsScheduler: FC = () => { {LL.SCHEDULER_HELP_1()} {renderSchedule()} - {renderEditSchedule()} + + {selectedScheduleItem && ( + + )} + {numChanges !== 0 && ( - @@ -582,7 +295,7 @@ const SettingsScheduler: FC = () => { - diff --git a/interface/src/project/SettingsSchedulerDialog.tsx b/interface/src/project/SettingsSchedulerDialog.tsx new file mode 100644 index 000000000..514b36610 --- /dev/null +++ b/interface/src/project/SettingsSchedulerDialog.tsx @@ -0,0 +1,254 @@ +import AddIcon from '@mui/icons-material/Add'; +import CancelIcon from '@mui/icons-material/Cancel'; +import CheckCircleIcon from '@mui/icons-material/CheckCircle'; +import DoneIcon from '@mui/icons-material/Done'; +import RemoveIcon from '@mui/icons-material/RemoveCircleOutline'; + +import { + Box, + Button, + Checkbox, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + Grid, + TextField, + ToggleButton, + ToggleButtonGroup, + Typography +} from '@mui/material'; +import { useEffect, useState } from 'react'; + +import { ScheduleFlag } from './types'; +import type { ScheduleItem } from './types'; +import type Schema from 'async-validator'; +import type { ValidateFieldsError } from 'async-validator'; + +import { BlockFormControlLabel, ValidatedTextField } from 'components'; + +import { useI18nContext } from 'i18n/i18n-react'; + +import { updateValue } from 'utils'; +import { validate } from 'validators'; + +type SettingsSchedulerDialogProps = { + open: boolean; + creating: boolean; + onClose: () => void; + onSave: (ei: ScheduleItem) => void; + selectedItem: ScheduleItem; + validator: Schema; + dow: string[]; +}; + +const SettingsSchedulerDialog = ({ + open, + creating, + onClose, + onSave, + selectedItem, + validator, + dow +}: SettingsSchedulerDialogProps) => { + const { LL } = useI18nContext(); + const [editItem, setEditItem] = useState(selectedItem); + const [fieldErrors, setFieldErrors] = useState(); + + const updateFormValue = updateValue(setEditItem); + + useEffect(() => { + if (open) { + setFieldErrors(undefined); + setEditItem(selectedItem); + } + }, [open, selectedItem]); + + const close = () => { + onClose(); + }; + + const save = async () => { + try { + setFieldErrors(undefined); + await validate(validator, editItem); + onSave(editItem); + } catch (errors: any) { + setFieldErrors(errors); + } + }; + + const remove = () => { + editItem.deleted = true; + onSave(editItem); + }; + + const getFlagNumber = (newFlag: string[]) => { + let new_flag = 0; + for (const entry of newFlag) { + new_flag |= Number(entry); + } + return new_flag; + }; + + const getFlagString = (f: number) => { + const new_flags: string[] = []; + if ((f & 1) === 1) { + new_flags.push('1'); + } + if ((f & 2) === 2) { + new_flags.push('2'); + } + if ((f & 4) === 4) { + new_flags.push('4'); + } + if ((f & 8) === 8) { + new_flags.push('8'); + } + if ((f & 16) === 16) { + new_flags.push('16'); + } + if ((f & 32) === 32) { + new_flags.push('32'); + } + if ((f & 64) === 64) { + new_flags.push('64'); + } + if ((f & 128) === 128) { + new_flags.push('128'); + } + return new_flags; + }; + + const showFlag = (si: ScheduleItem, flag: number) => ( + + {flag === ScheduleFlag.SCHEDULE_TIMER ? LL.TIMER(0) : dow[Math.log(flag) / Math.log(2)]} + + ); + + const isTimer = editItem.flags === ScheduleFlag.SCHEDULE_TIMER; + + return ( + + + {creating ? LL.ADD(1) + ' ' + LL.NEW(1) : LL.EDIT()} {LL.SCHEDULE(0)} + + + + + { + setEditItem({ ...editItem, flags: getFlagNumber(flag) & 127 }); + }} + > + {showFlag(editItem, ScheduleFlag.SCHEDULE_MON)} + {showFlag(editItem, ScheduleFlag.SCHEDULE_TUE)} + {showFlag(editItem, ScheduleFlag.SCHEDULE_WED)} + {showFlag(editItem, ScheduleFlag.SCHEDULE_THU)} + {showFlag(editItem, ScheduleFlag.SCHEDULE_FRI)} + {showFlag(editItem, ScheduleFlag.SCHEDULE_SAT)} + {showFlag(editItem, ScheduleFlag.SCHEDULE_SUN)} + + + + {isTimer ? ( + + ) : ( + + )} + + + + } + label={LL.ACTIVE()} + /> + {editItem.active && ( + + + + )} + + + + {isTimer && ( + + {LL.SCHEDULER_HELP_2()} + + )} + + + + + + + {!creating && ( + + + + )} + + + + + ); +}; + +export default SettingsSchedulerDialog; diff --git a/interface/src/project/api.ts b/interface/src/project/api.ts index ab394168a..2dbc25f5a 100644 --- a/interface/src/project/api.ts +++ b/interface/src/project/api.ts @@ -1,7 +1,4 @@ -import { AxiosPromise } from 'axios'; -import { AXIOS, AXIOS_API, AXIOS_BIN } from 'api/endpoints'; - -import { +import type { BoardProfile, BoardProfileName, APIcall, @@ -13,13 +10,15 @@ import { DeviceEntity, UniqueID, CustomEntities, - WriteValue, - WriteSensor, - WriteAnalog, + WriteDeviceValue, + WriteTemperatureSensor, + WriteAnalogSensor, SensorData, Schedule, Entities } from './types'; +import type { AxiosPromise } from 'axios'; +import { AXIOS, AXIOS_API, AXIOS_BIN } from 'api/endpoints'; export function restart(): AxiosPromise { return AXIOS.post('/restart'); @@ -69,16 +68,16 @@ export function writeCustomEntities(customEntities: CustomEntities): AxiosPromis return AXIOS.post('/customEntities', customEntities); } -export function writeValue(writevalue: WriteValue): AxiosPromise { - return AXIOS.post('/writeValue', writevalue); +export function writeDeviceValue(dv: WriteDeviceValue): AxiosPromise { + return AXIOS.post('/writeDeviceValue', dv); } -export function writeSensor(writesensor: WriteSensor): AxiosPromise { - return AXIOS.post('/writeSensor', writesensor); +export function writeTemperatureSensor(ts: WriteTemperatureSensor): AxiosPromise { + return AXIOS.post('/writeTemperatureSensor', ts); } -export function writeAnalog(writeanalog: WriteAnalog): AxiosPromise { - return AXIOS.post('/writeAnalog', writeanalog); +export function writeAnalogSensor(as: WriteAnalogSensor): AxiosPromise { + return AXIOS.post('/writeAnalogSensor', as); } export function resetCustomizations(): AxiosPromise { @@ -97,18 +96,6 @@ export function getCustomizations(): AxiosPromise { return AXIOS.get('/getCustomizations'); } -export function getEntities(): AxiosPromise { - return AXIOS.get('/getEntities'); -} - -export function readEntities(): AxiosPromise { - return AXIOS.get('/entity'); -} - -export function writeEntities(entities: Entities): AxiosPromise { - return AXIOS.post('/entity', entities); -} - export function getSchedule(): AxiosPromise { return AXIOS.get('/getSchedule'); } @@ -120,3 +107,15 @@ export function readSchedule(): AxiosPromise { export function writeSchedule(schedule: Schedule): AxiosPromise { return AXIOS.post('/schedule', schedule); } + +export function getEntities(): AxiosPromise { + return AXIOS.get('/getEntities'); +} + +export function readEntities(): AxiosPromise { + return AXIOS.get('/entities'); +} + +export function writeEntities(entities: Entities): AxiosPromise { + return AXIOS.post('/entities', entities); +} diff --git a/interface/src/project/deviceValue.ts b/interface/src/project/deviceValue.ts new file mode 100644 index 000000000..ff7900340 --- /dev/null +++ b/interface/src/project/deviceValue.ts @@ -0,0 +1,56 @@ +import { DeviceValueUOM, DeviceValueUOM_s } from './types'; +import type { TranslationFunctions } from 'i18n/i18n-types'; + +const formatDurationMin = (LL: TranslationFunctions, duration_min: number) => { + const days = Math.trunc((duration_min * 60000) / 86400000); + const hours = Math.trunc((duration_min * 60000) / 3600000) % 24; + const minutes = Math.trunc((duration_min * 60000) / 60000) % 60; + + let formatted = ''; + if (days) { + formatted += LL.NUM_DAYS({ num: days }); + } + + if (hours) { + if (formatted) formatted += ' '; + formatted += LL.NUM_HOURS({ num: hours }); + } + + if (minutes) { + if (formatted) formatted += ' '; + formatted += LL.NUM_MINUTES({ num: minutes }); + } + + return formatted; +}; + +export function formatValue(LL: TranslationFunctions, value: any, uom: number) { + if (value === undefined) { + return ''; + } + switch (uom) { + case DeviceValueUOM.HOURS: + return value ? formatDurationMin(LL, value * 60) : LL.NUM_HOURS({ num: 0 }); + case DeviceValueUOM.MINUTES: + return value ? formatDurationMin(LL, value) : LL.NUM_MINUTES({ num: 0 }); + case DeviceValueUOM.SECONDS: + return LL.NUM_SECONDS({ num: value }); + case DeviceValueUOM.NONE: + if (typeof value === 'number') { + return new Intl.NumberFormat().format(value); + } + return value; + case DeviceValueUOM.DEGREES: + case DeviceValueUOM.DEGREES_R: + case DeviceValueUOM.FAHRENHEIT: + return ( + new Intl.NumberFormat(undefined, { + minimumFractionDigits: 1 + }).format(value) + + ' ' + + DeviceValueUOM_s[uom] + ); + default: + return new Intl.NumberFormat().format(value) + ' ' + DeviceValueUOM_s[uom]; + } +} diff --git a/interface/src/project/types.ts b/interface/src/project/types.ts index a33bbedf8..96d2f76b7 100644 --- a/interface/src/project/types.ts +++ b/interface/src/project/types.ts @@ -60,7 +60,7 @@ export interface Status { } export interface Device { - id: string; // id index + id: number; // id index tn: string; // device type translated name t: number; // device type id b: string; // brand @@ -68,10 +68,9 @@ export interface Device { d: number; // deviceid p: number; // productid v: string; // version - e: number; // number of entries } -export interface Sensor { +export interface TemperatureSensor { id: string; // id string n: string; // name/alias t?: number; // temp, optional @@ -79,34 +78,33 @@ export interface Sensor { u: number; // uom } -export interface Analog { - id: string; // id string +export interface AnalogSensor { + id: number; g: number; // GPIO n: string; - v: number; // is optional + v: number; u: number; o: number; f: number; t: number; + d: boolean; // deleted flag } -export interface WriteSensor { +export interface WriteTemperatureSensor { id: string; name: string; offset: number; } export interface SensorData { - sensors: Sensor[]; - analogs: Analog[]; + ts: TemperatureSensor[]; + as: AnalogSensor[]; + analog_enabled: boolean; } export interface CoreData { connected: boolean; devices: Device[]; - s_n: string; - active_sensors: number; - analog_enabled: boolean; } export interface DeviceShort { @@ -124,18 +122,17 @@ export interface Devices { export interface DeviceValue { id: string; // index, contains mask+name - v: any; // value, in any format + v: any; // value, Number or String u: number; // uom c?: string; // command, optional l?: string[]; // list, optional h?: string; // help text, optional - s?: string; // steps for up/down, optional - m?: string; // min, optional - x?: string; // max, optional + s?: number; // steps for up/down, optional + m?: number; // min, optional + x?: number; // max, optional } export interface DeviceData { - label: string; data: DeviceValue[]; } @@ -146,12 +143,12 @@ export interface DeviceEntity { cn?: string; // custom fullname, optional m: number; // mask w: boolean; // writeable + mi?: number; // min value + ma?: number; // max value o_m?: number; // original mask before edits o_cn?: string; // original cn before edits - mi?: string; // min value - ma?: string; // max value - o_mi?: string; // original min value - o_ma?: string; // original max value + o_mi?: number; // original min value + o_ma?: number; // original max value } export interface CustomEntities { @@ -216,6 +213,7 @@ export const DeviceValueUOM_s = [ ]; export enum AnalogType { + REMOVED = -1, NOTUSED = 0, DIGITAL_IN, COUNTER, @@ -281,18 +279,20 @@ export interface APIcall { id: any; } -export interface WriteValue { +export interface WriteDeviceValue { id: number; devicevalue: DeviceValue; } -export interface WriteAnalog { +export interface WriteAnalogSensor { + id: number; gpio: number; name: string; factor: number; offset: number; uom: number; type: number; + deleted: boolean; } export enum DeviceEntityMask { @@ -339,27 +339,52 @@ export enum ScheduleFlag { } export interface EntityItem { + id: number; // unique number name: string; - device_id: string; - type_id: string; + device_id: number | string; + type_id: number | string; offset: number; factor: number; uom: number; - val_type: number; - value?: number; + value_type: number; + value?: any; + writeable: boolean; + deleted?: boolean; + o_id?: number; o_name?: string; - o_device_id?: string; - o_type_id?: string; + o_device_id?: number | string; + o_type_id?: number | string; o_offset?: number; o_factor?: number; o_uom?: number; - o_val_type?: number; - deleted?: boolean; // optional + o_value_type?: number; o_deleted?: boolean; - write: boolean; - o_write?: boolean; + o_writeable?: boolean; } export interface Entities { - entity: EntityItem[]; + entities: EntityItem[]; +} + +// matches emsdevice.h DeviceType +export const enum DeviceType { + SYSTEM = 0, + TEMPERATURESENSOR, + ANALOGSENSOR, + SCHEDULER, + BOILER, + THERMOSTAT, + MIXER, + SOLAR, + HEATPUMP, + GATEWAY, + SWITCH, + CONTROLLER, + CONNECT, + ALERT, + PUMP, + GENERIC, + HEATSOURCE, + CUSTOM, + UNKNOWN } diff --git a/interface/src/project/validators.ts b/interface/src/project/validators.ts index 257078793..32b4f2505 100644 --- a/interface/src/project/validators.ts +++ b/interface/src/project/validators.ts @@ -1,6 +1,7 @@ -import Schema, { InternalRuleItem } from 'async-validator'; +import Schema from 'async-validator'; +import type { AnalogSensor, DeviceValue, Settings } from './types'; +import type { InternalRuleItem } from 'async-validator'; import { IP_OR_HOSTNAME_VALIDATOR } from 'validators/shared'; -import { Settings, ScheduleItem, EntityItem } from './types'; export const GPIO_VALIDATOR = { validator(rule: InternalRuleItem, value: number, callback: (error?: string) => void) { @@ -85,15 +86,14 @@ export const createSettingsValidator = (settings: Settings) => }) }); -export const schedulerItemValidation = (schedule: ScheduleItem[], scheduleItem: ScheduleItem) => +export const schedulerItemValidation = () => new Schema({ name: [ { type: 'string', pattern: /^[a-zA-Z0-9_\\.]{0,15}$/, message: "Must be <15 characters: alpha numeric, '_' or '.'" - }, - ...[uniqueNameValidator(schedule, scheduleItem.o_name)] + } ], cmd: [ { required: true, message: 'Command is required' }, @@ -101,17 +101,7 @@ export const schedulerItemValidation = (schedule: ScheduleItem[], scheduleItem: ] }); -export const uniqueNameValidator = (schedule: ScheduleItem[], o_name?: string) => ({ - validator(rule: InternalRuleItem, name: string, callback: (error?: string) => void) { - if (name && o_name && o_name !== name && schedule.find((si) => si.name === name)) { - callback('Name already in use'); - } else { - callback(); - } - } -}); - -export const entityItemValidation = (entity: EntityItem[], entityItem: EntityItem) => +export const entityItemValidation = () => new Schema({ name: [ { required: true, message: 'Name is required' }, @@ -119,16 +109,27 @@ export const entityItemValidation = (entity: EntityItem[], entityItem: EntityIte type: 'string', pattern: /^[a-zA-Z0-9_\\.]{1,15}$/, message: "Must be <15 characters: alpha numeric, '_' or '.'" - }, - ...[uniqueEntityNameValidator(entity, entityItem.o_name)] + } ], device_id: [ - { required: true, message: 'Device_id is required' }, - { type: 'string', pattern: /^[A-F0-9]{1,2}$/, message: 'Must be a hex number' } + { + validator(rule: InternalRuleItem, value: string, callback: (error?: string) => void) { + if (isNaN(parseInt(value, 16))) { + callback('Is required and must be in hex format'); + } + callback(); + } + } ], type_id: [ - { required: true, message: 'Type_id is required' }, - { type: 'string', pattern: /^[A-F0-9]{1,4}$/, message: 'Must be a hex number' } + { + validator(rule: InternalRuleItem, value: string, callback: (error?: string) => void) { + if (isNaN(parseInt(value, 16))) { + callback('Is required and must be in hex format'); + } + callback(); + } + } ], offset: [ { required: true, message: 'Offset is required' }, @@ -136,12 +137,42 @@ export const entityItemValidation = (entity: EntityItem[], entityItem: EntityIte ] }); -export const uniqueEntityNameValidator = (entity: EntityItem[], o_name?: string) => ({ - validator(rule: InternalRuleItem, name: string, callback: (error?: string) => void) { - if (name && o_name && o_name !== name && entity.find((ei) => ei.name === name)) { - callback('Name already in use'); +export const temperatureSensorItemValidation = () => + new Schema({ + n: [{ required: true, message: 'Name is required' }] + }); + +export const isGPIOUniqueValidator = (sensors: AnalogSensor[]) => ({ + validator(rule: InternalRuleItem, gpio: number, callback: (error?: string) => void) { + if (sensors.find((as) => as.g === gpio)) { + callback('GPIO already in use'); } else { callback(); } } }); + +export const analogSensorItemValidation = (sensors: AnalogSensor[], creating: boolean) => + new Schema({ + n: [{ required: true, message: 'Name is required' }], + g: [ + { required: true, message: 'GPIO is required' }, + GPIO_VALIDATOR, + ...(creating ? [isGPIOUniqueValidator(sensors)] : []) + ] + }); + +export const deviceValueItemValidation = (dv: DeviceValue) => + new Schema({ + v: [ + { required: true, message: 'Value is required' }, + { + validator(rule: InternalRuleItem, value: any, callback: (error?: string) => void) { + if (typeof value === 'number' && dv.m && dv.x && (value < dv.m || value > dv.x)) { + callback('Value out of range'); + } + callback(); + } + } + ] + }); diff --git a/interface/src/utils/time.ts b/interface/src/utils/time.ts index 5efc58719..3fefb063c 100644 --- a/interface/src/utils/time.ts +++ b/interface/src/utils/time.ts @@ -8,10 +8,7 @@ const LOCALE_FORMAT = new Intl.DateTimeFormat([...window.navigator.languages], { hour12: false }); -export const formatDateTime = (dateTime: string) => { - return LOCALE_FORMAT.format(new Date(dateTime.substring(0, 19))); -}; +export const formatDateTime = (dateTime: string) => LOCALE_FORMAT.format(new Date(dateTime.substring(0, 19))); -export const formatLocalDateTime = (date: Date) => { - return new Date(date.getTime() - date.getTimezoneOffset() * 60000).toISOString().slice(0, -1).substring(0, 19); -}; +export const formatLocalDateTime = (date: Date) => + new Date(date.getTime() - date.getTimezoneOffset() * 60000).toISOString().slice(0, -1).substring(0, 19); diff --git a/interface/src/utils/useRest.ts b/interface/src/utils/useRest.ts index f57d20ec0..7dbc5cd56 100644 --- a/interface/src/utils/useRest.ts +++ b/interface/src/utils/useRest.ts @@ -1,14 +1,12 @@ import { useCallback, useEffect, useState } from 'react'; +import { unstable_useBlocker as useBlocker } from 'react-router-dom'; import { toast } from 'react-toastify'; -import { AxiosPromise } from 'axios'; - import { extractErrorMessage } from '.'; +import type { AxiosPromise } from 'axios'; import { useI18nContext } from 'i18n/i18n-react'; -import { unstable_useBlocker as useBlocker } from 'react-router-dom'; - export interface RestRequestOptions { read: () => AxiosPromise; update?: (value: D) => AxiosPromise; @@ -74,7 +72,7 @@ export const useRest = ({ read, update }: RestRequestOptions) => { const saveData = () => data && save(data); useEffect(() => { - loadData(); + void loadData(); }, [loadData]); return { @@ -86,6 +84,7 @@ export const useRest = ({ read, update }: RestRequestOptions) => { origData, dirtyFlags, setDirtyFlags, + setOrigData, blocker, errorMessage, restartNeeded diff --git a/interface/src/utils/useWs.ts b/interface/src/utils/useWs.ts index 7839b53de..3471c691e 100644 --- a/interface/src/utils/useWs.ts +++ b/interface/src/utils/useWs.ts @@ -1,6 +1,6 @@ +import { debounce } from 'lodash-es'; import { useCallback, useEffect, useRef, useState } from 'react'; import Sockette from 'sockette'; -import { debounce } from 'lodash-es'; import { addAccessTokenParameter } from 'api/authentication'; @@ -83,6 +83,7 @@ export const useWs = (wsUrl: string, wsThrottle = 100) => { } }); ws.current = instance; + // eslint-disable-next-line @typescript-eslint/unbound-method return instance.close; }, [wsUrl, onMessage]); diff --git a/interface/src/validators/ap.ts b/interface/src/validators/ap.ts index e9e72ecd0..cd5812e8b 100644 --- a/interface/src/validators/ap.ts +++ b/interface/src/validators/ap.ts @@ -1,7 +1,7 @@ import Schema from 'async-validator'; -import { isAPEnabled } from 'framework/ap/APSettingsForm'; -import { APSettings } from 'types'; import { IP_ADDRESS_VALIDATOR } from './shared'; +import type { APSettings } from 'types'; +import { isAPEnabled } from 'framework/ap/APSettingsForm'; export const createAPSettingsValidator = (apSettings: APSettings) => new Schema({ diff --git a/interface/src/validators/mqtt.ts b/interface/src/validators/mqtt.ts index ec95a1b65..d8f841baa 100644 --- a/interface/src/validators/mqtt.ts +++ b/interface/src/validators/mqtt.ts @@ -1,6 +1,6 @@ import Schema from 'async-validator'; -import { MqttSettings } from 'types'; import { IP_OR_HOSTNAME_VALIDATOR } from './shared'; +import type { MqttSettings } from 'types'; export const createMqttSettingsValidator = (mqttSettings: MqttSettings) => new Schema({ diff --git a/interface/src/validators/network.ts b/interface/src/validators/network.ts index ba40ac051..761ce0de7 100644 --- a/interface/src/validators/network.ts +++ b/interface/src/validators/network.ts @@ -1,6 +1,6 @@ import Schema from 'async-validator'; -import { NetworkSettings } from 'types'; import { HOSTNAME_VALIDATOR, IP_ADDRESS_VALIDATOR } from './shared'; +import type { NetworkSettings } from 'types'; export const createNetworkSettingsValidator = (networkSettings: NetworkSettings) => new Schema({ diff --git a/interface/src/validators/security.ts b/interface/src/validators/security.ts index feb9498e2..d1843ac90 100644 --- a/interface/src/validators/security.ts +++ b/interface/src/validators/security.ts @@ -1,5 +1,6 @@ -import Schema, { InternalRuleItem } from 'async-validator'; -import { User } from 'types'; +import Schema from 'async-validator'; +import type { InternalRuleItem } from 'async-validator'; +import type { User } from 'types'; export const SECURITY_SETTINGS_VALIDATOR = new Schema({ jwt_secret: [ 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..45592aca2 100644 --- a/interface/tsconfig.json +++ b/interface/tsconfig.json @@ -13,7 +13,7 @@ "noFallthroughCasesInSwitch": true, "composite": true, "module": "ESNext", - "moduleResolution": "Node", + "moduleResolution": "bundler", "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, diff --git a/interface/vite.config.ts b/interface/vite.config.ts index 83f3cfe81..e2509f7eb 100644 --- a/interface/vite.config.ts +++ b/interface/vite.config.ts @@ -2,7 +2,6 @@ import { defineConfig, type PluginOption } from 'vite'; import react from '@vitejs/plugin-react-swc'; import viteTsconfigPaths from 'vite-tsconfig-paths'; import svgrPlugin from 'vite-plugin-svgr'; -// import { ViteMinifyPlugin } from 'vite-plugin-minify'; import { visualizer } from 'rollup-plugin-visualizer'; import ProgmemGenerator from './progmem-generator'; @@ -19,19 +18,6 @@ export default defineConfig(({ command, mode }) => { react(), viteTsconfigPaths(), svgrPlugin(), - // ViteMinifyPlugin({ - // removeAttributeQuotes: true, - // minifyCSS: true, - // minifyJS: true, - // decodeEntities: true, - // removeComments: true, - // removeEmptyAttributes: true, - // removeOptionalTags: true, - // removeEmptyElements: true, - // removeScriptTypeAttributes: true, - // removeTagWhitespace: true, - // minifyURLs: true - // }), ProgmemGenerator({ outputPath: '../lib/framework/WWWData.h', bytesPerLine: 20 }) ], diff --git a/interface/yarn.lock b/interface/yarn.lock index 813634ee0..ee0e7c410 100644 --- a/interface/yarn.lock +++ b/interface/yarn.lock @@ -15,15 +15,6 @@ __metadata: languageName: node linkType: hard -"@arcanis/slice-ansi@npm:^1.1.1": - version: 1.1.1 - resolution: "@arcanis/slice-ansi@npm:1.1.1" - dependencies: - grapheme-splitter: ^1.0.4 - checksum: 2f222b121b8aaf67e8495e27d60ebfc34e2472033445c3380e93fb06aba9bfef6ab3096aca190a181b3dd505ed4c07f4dc7243fc9cb5369008b649cd1e39e8d8 - languageName: node - linkType: hard - "@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.18.6, @babel/code-frame@npm:^7.21.4": version: 7.21.4 resolution: "@babel/code-frame@npm:7.21.4" @@ -33,67 +24,67 @@ __metadata: languageName: node linkType: hard -"@babel/compat-data@npm:^7.21.4": - version: 7.21.4 - resolution: "@babel/compat-data@npm:7.21.4" - checksum: 8752c19f78f6b91188b8c4867ae357fe79206ed3ea2fbc9357ac66639b1bd4aa1ba44cedba238369070704605caf9a4a742bf1cfa2b9414845a8995e0c9ac40a +"@babel/compat-data@npm:^7.21.5": + version: 7.21.7 + resolution: "@babel/compat-data@npm:7.21.7" + checksum: cd6bc85364a569cc74bcf0bfdc27161a1cb423c60c624e06f44b53c9e6fe7708bd0af3e389d376aec8ed9b2795907c43d01e4163dbc2a3a3142a2de55464a51d languageName: node linkType: hard -"@babel/core@npm:^7.19.6": - version: 7.21.4 - resolution: "@babel/core@npm:7.21.4" +"@babel/core@npm:^7.21.3": + version: 7.21.8 + resolution: "@babel/core@npm:7.21.8" dependencies: "@ampproject/remapping": ^2.2.0 "@babel/code-frame": ^7.21.4 - "@babel/generator": ^7.21.4 - "@babel/helper-compilation-targets": ^7.21.4 - "@babel/helper-module-transforms": ^7.21.2 - "@babel/helpers": ^7.21.0 - "@babel/parser": ^7.21.4 + "@babel/generator": ^7.21.5 + "@babel/helper-compilation-targets": ^7.21.5 + "@babel/helper-module-transforms": ^7.21.5 + "@babel/helpers": ^7.21.5 + "@babel/parser": ^7.21.8 "@babel/template": ^7.20.7 - "@babel/traverse": ^7.21.4 - "@babel/types": ^7.21.4 + "@babel/traverse": ^7.21.5 + "@babel/types": ^7.21.5 convert-source-map: ^1.7.0 debug: ^4.1.0 gensync: ^1.0.0-beta.2 json5: ^2.2.2 semver: ^6.3.0 - checksum: 0987cf87f277eb19c410ef3a03f9377efec40005a5dd2a67ddd0a5f6f429c9d88fefba25206ccf3378c93814b4c9c06a236bf8fcd3ed6ef1c8089fefaa76af24 + checksum: bf6bb92bd78fb8b6628bb0612ac0915407b996b179e1404108f92ed32972978340b4457b08f2abf86390a58fb51815cab419edb2dbbc8846efc39eaa61b8cde3 languageName: node linkType: hard -"@babel/generator@npm:^7.21.4": - version: 7.21.4 - resolution: "@babel/generator@npm:7.21.4" +"@babel/generator@npm:^7.21.5": + version: 7.21.5 + resolution: "@babel/generator@npm:7.21.5" dependencies: - "@babel/types": ^7.21.4 + "@babel/types": ^7.21.5 "@jridgewell/gen-mapping": ^0.3.2 "@jridgewell/trace-mapping": ^0.3.17 jsesc: ^2.5.1 - checksum: 0eb142a5ca8a978981c11de9e0ab033659f7110bc21cd14eaeb80977835b895c3a97e5a1807a2f6e79003682141057f00b4bd5f69fe998b4cf99bf989c361277 + checksum: e98b51440cbbcee68e66c66684b5334f5929dba512067a6c3c1aecc77131b308bf61eca74a5ae1fb73028089d22a188ca2219c364596117f27695102afc18e95 languageName: node linkType: hard -"@babel/helper-compilation-targets@npm:^7.21.4": - version: 7.21.4 - resolution: "@babel/helper-compilation-targets@npm:7.21.4" +"@babel/helper-compilation-targets@npm:^7.21.5": + version: 7.21.5 + resolution: "@babel/helper-compilation-targets@npm:7.21.5" dependencies: - "@babel/compat-data": ^7.21.4 + "@babel/compat-data": ^7.21.5 "@babel/helper-validator-option": ^7.21.0 browserslist: ^4.21.3 lru-cache: ^5.1.1 semver: ^6.3.0 peerDependencies: "@babel/core": ^7.0.0 - checksum: ad553d5a473beeedaf7be4e450d3d6f36920f34005bc45bc62d94a16ae553dcb7d9fc5b2bc721ffa203e542bc8a1fb241e1c97fba1fae5f7ef5ba87a7730a1b9 + checksum: 36752452eb70d6a6f52f68846344a739089374a97619e5a4857e31e7d067bdad8270efd9dd0dd5dfc483dd2d98bf0c1c6f08e3315fe949e7bfffef67eaf669ad languageName: node linkType: hard -"@babel/helper-environment-visitor@npm:^7.18.9": - version: 7.18.9 - resolution: "@babel/helper-environment-visitor@npm:7.18.9" - checksum: a69dd50ea91d8143b899a40ca7a387fa84dbaa02e606d8692188c7c59bd4007bcd632c189f7b7dab72cb7a016e159557a6fccf7093ab9b584d87cf2ea8cf36b7 +"@babel/helper-environment-visitor@npm:^7.21.5": + version: 7.21.5 + resolution: "@babel/helper-environment-visitor@npm:7.21.5" + checksum: d3f965d9691e3e2e11036d23ba9993a42d18f9be3d4589d3bb3d09d02e9d4d204026965633e36fb43b35fde905c2dfe753fb59b72ae0c3841f5a627fb1738d8a languageName: node linkType: hard @@ -116,7 +107,7 @@ __metadata: languageName: node linkType: hard -"@babel/helper-module-imports@npm:^7.16.7, @babel/helper-module-imports@npm:^7.18.6": +"@babel/helper-module-imports@npm:^7.16.7, @babel/helper-module-imports@npm:^7.21.4": version: 7.21.4 resolution: "@babel/helper-module-imports@npm:7.21.4" dependencies: @@ -125,28 +116,28 @@ __metadata: languageName: node linkType: hard -"@babel/helper-module-transforms@npm:^7.21.2": - version: 7.21.2 - resolution: "@babel/helper-module-transforms@npm:7.21.2" +"@babel/helper-module-transforms@npm:^7.21.5": + version: 7.21.5 + resolution: "@babel/helper-module-transforms@npm:7.21.5" dependencies: - "@babel/helper-environment-visitor": ^7.18.9 - "@babel/helper-module-imports": ^7.18.6 - "@babel/helper-simple-access": ^7.20.2 + "@babel/helper-environment-visitor": ^7.21.5 + "@babel/helper-module-imports": ^7.21.4 + "@babel/helper-simple-access": ^7.21.5 "@babel/helper-split-export-declaration": ^7.18.6 "@babel/helper-validator-identifier": ^7.19.1 "@babel/template": ^7.20.7 - "@babel/traverse": ^7.21.2 - "@babel/types": ^7.21.2 - checksum: 35d4508826bae2db69ab6966db1810b5e7b9157e471525ad1f2119e16742bd293da02587bddb2843368dcd411ddd5ae0f212d6381bcf32e1b338a84b5b27ae30 + "@babel/traverse": ^7.21.5 + "@babel/types": ^7.21.5 + checksum: a3b6ceaa995bf35e7a072066c3c9ba9ee6983cf36605f0c6a0ffcaab94d6dc13eba21b00434a023bf99d66c080fec335cf464619b97f7af39e1a5269cf0d7169 languageName: node linkType: hard -"@babel/helper-simple-access@npm:^7.20.2": - version: 7.20.2 - resolution: "@babel/helper-simple-access@npm:7.20.2" +"@babel/helper-simple-access@npm:^7.21.5": + version: 7.21.5 + resolution: "@babel/helper-simple-access@npm:7.21.5" dependencies: - "@babel/types": ^7.20.2 - checksum: 79cea28155536c74b37839748caea534bc413fac8c512e6101e9eecfe83f670db77bc782bdb41114caecbb1e2a73007ff6015d6a5ce58cae5363b8c5bd2dcee9 + "@babel/types": ^7.21.5 + checksum: 682cd80b47c2424c31afe70bcc8ad3e401c612f6923c432e4b8245c5b6bc5ccddf3e405ea41ba890ccab79c0b5b95da3db125944ac0decc8d31d48469e593a0e languageName: node linkType: hard @@ -166,6 +157,13 @@ __metadata: languageName: node linkType: hard +"@babel/helper-string-parser@npm:^7.21.5": + version: 7.21.5 + resolution: "@babel/helper-string-parser@npm:7.21.5" + checksum: 4d0834c4a67c283e9277f5e565551fede00b7d68007e368c95c776e13d05002e8f9861716e11613880889d6f3463329d2af687ceea5fc5263f8b3d25a53d31da + languageName: node + linkType: hard + "@babel/helper-validator-identifier@npm:^7.18.6, @babel/helper-validator-identifier@npm:^7.19.1": version: 7.19.1 resolution: "@babel/helper-validator-identifier@npm:7.19.1" @@ -180,14 +178,14 @@ __metadata: languageName: node linkType: hard -"@babel/helpers@npm:^7.21.0": - version: 7.21.0 - resolution: "@babel/helpers@npm:7.21.0" +"@babel/helpers@npm:^7.21.5": + version: 7.21.5 + resolution: "@babel/helpers@npm:7.21.5" dependencies: "@babel/template": ^7.20.7 - "@babel/traverse": ^7.21.0 - "@babel/types": ^7.21.0 - checksum: a7415373f1c9b84fe32839d5219c3d695e84b910f49a20786caf3b5a37f5079d26af6a5b36b4f2e3eb450b2413c309785483a8d59246d1326c44184c51c24255 + "@babel/traverse": ^7.21.5 + "@babel/types": ^7.21.5 + checksum: 5e58854afa1d0896185dcb12a1b6feacefb7d913d52bafa84792274651af2d3172923bdc26d1320fd6b04a2e208dc0d6730951043f17d10c08ca87231e5b84ec languageName: node linkType: hard @@ -202,7 +200,7 @@ __metadata: languageName: node linkType: hard -"@babel/parser@npm:^7.20.7, @babel/parser@npm:^7.21.4": +"@babel/parser@npm:^7.20.7": version: 7.21.4 resolution: "@babel/parser@npm:7.21.4" bin: @@ -211,6 +209,15 @@ __metadata: languageName: node linkType: hard +"@babel/parser@npm:^7.21.5, @babel/parser@npm:^7.21.8": + version: 7.21.8 + resolution: "@babel/parser@npm:7.21.8" + bin: + parser: ./bin/babel-parser.js + checksum: 58789e972e5acce3abbd9dd4c8d4be7e15e071818d2038d195bc56664722f238abb8842d91da5c8894ab0b8f8c0841eabc675f681925c2fba12675bf3ec5c5fc + languageName: node + linkType: hard + "@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.20.7, @babel/runtime@npm:^7.21.0, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.7.6, @babel/runtime@npm:^7.8.7": version: 7.21.0 resolution: "@babel/runtime@npm:7.21.0" @@ -231,25 +238,25 @@ __metadata: languageName: node linkType: hard -"@babel/traverse@npm:^7.21.0, @babel/traverse@npm:^7.21.2, @babel/traverse@npm:^7.21.4": - version: 7.21.4 - resolution: "@babel/traverse@npm:7.21.4" +"@babel/traverse@npm:^7.21.5": + version: 7.21.5 + resolution: "@babel/traverse@npm:7.21.5" dependencies: "@babel/code-frame": ^7.21.4 - "@babel/generator": ^7.21.4 - "@babel/helper-environment-visitor": ^7.18.9 + "@babel/generator": ^7.21.5 + "@babel/helper-environment-visitor": ^7.21.5 "@babel/helper-function-name": ^7.21.0 "@babel/helper-hoist-variables": ^7.18.6 "@babel/helper-split-export-declaration": ^7.18.6 - "@babel/parser": ^7.21.4 - "@babel/types": ^7.21.4 + "@babel/parser": ^7.21.5 + "@babel/types": ^7.21.5 debug: ^4.1.0 globals: ^11.1.0 - checksum: 3b2e7e80ef088881ad1f30a032f71ba63d734c270cd240dc229f26bfdeabcd661cf40d2c083f250812b08bb04985f77fb038b7b1ee629b3378ee867dff163878 + checksum: 1b126b71b98aaff01ec1f0f0389d08beb6eda3d0b71878af4c6cf386686933a076d969240f270c6a01910d8036a1fb9013d53bd5c136b9b24025204a4dc48d03 languageName: node linkType: hard -"@babel/types@npm:^7.18.6, @babel/types@npm:^7.20.0, @babel/types@npm:^7.20.2, @babel/types@npm:^7.20.7, @babel/types@npm:^7.21.0, @babel/types@npm:^7.21.2, @babel/types@npm:^7.21.4, @babel/types@npm:^7.8.3": +"@babel/types@npm:^7.18.6, @babel/types@npm:^7.20.7, @babel/types@npm:^7.21.0, @babel/types@npm:^7.21.4, @babel/types@npm:^7.8.3": version: 7.21.4 resolution: "@babel/types@npm:7.21.4" dependencies: @@ -260,149 +267,160 @@ __metadata: languageName: node linkType: hard -"@emotion/babel-plugin@npm:^11.10.6": - version: 11.10.6 - resolution: "@emotion/babel-plugin@npm:11.10.6" +"@babel/types@npm:^7.21.3, @babel/types@npm:^7.21.5": + version: 7.21.5 + resolution: "@babel/types@npm:7.21.5" + dependencies: + "@babel/helper-string-parser": ^7.21.5 + "@babel/helper-validator-identifier": ^7.19.1 + to-fast-properties: ^2.0.0 + checksum: 23c943aa2c0d11b798e9298b55b1993da8b386504aac2f781a49b4bbf2cf2ad5e1003409241578574e421c999ff7a3aab2cf30ad3581d33eb9053d82b9e20408 + languageName: node + linkType: hard + +"@emotion/babel-plugin@npm:^11.11.0": + version: 11.11.0 + resolution: "@emotion/babel-plugin@npm:11.11.0" dependencies: "@babel/helper-module-imports": ^7.16.7 "@babel/runtime": ^7.18.3 - "@emotion/hash": ^0.9.0 - "@emotion/memoize": ^0.8.0 - "@emotion/serialize": ^1.1.1 + "@emotion/hash": ^0.9.1 + "@emotion/memoize": ^0.8.1 + "@emotion/serialize": ^1.1.2 babel-plugin-macros: ^3.1.0 convert-source-map: ^1.5.0 escape-string-regexp: ^4.0.0 find-root: ^1.1.0 source-map: ^0.5.7 - stylis: 4.1.3 - checksum: 734ab5d59f8a64ec2cb140f71483f74910ada115bf78ce5b645a9e3e379554ffd79045edb74efb8c1d8f856ef4d302bf8ac59b969b1cc28dedcd5000072e63ce + stylis: 4.2.0 + checksum: 89cbb6ec0e52c8ee9c2a4b9889ccd4fc3a75d28091d835bfac6d7c4565d3338621e23af0a85f3bcd133e1cae795c692e1dadada015784d4b0554aa5bb111df43 languageName: node linkType: hard -"@emotion/cache@npm:^11.10.5": - version: 11.10.7 - resolution: "@emotion/cache@npm:11.10.7" +"@emotion/cache@npm:^11.11.0": + version: 11.11.0 + resolution: "@emotion/cache@npm:11.11.0" dependencies: - "@emotion/memoize": ^0.8.0 - "@emotion/sheet": ^1.2.1 - "@emotion/utils": ^1.2.0 - "@emotion/weak-memoize": ^0.3.0 - stylis: 4.1.3 - checksum: 0175b8be5117342e76e100fca92932a34c3641160c733a34534153eab7f1c1b2cecafee6d9a7a0646acf7be3c52b0654dc34900439316ae473b59a9eb1a1c8f3 + "@emotion/memoize": ^0.8.1 + "@emotion/sheet": ^1.2.2 + "@emotion/utils": ^1.2.1 + "@emotion/weak-memoize": ^0.3.1 + stylis: 4.2.0 + checksum: a23ab5ab2fd08e904698106d58ad3536fed51cc1aa0ef228e95bb640eaf11f560dbd91a395477b0d84e1e3c20150263764b4558517cf6576a89d2d6cc5253688 languageName: node linkType: hard -"@emotion/hash@npm:^0.9.0": - version: 0.9.0 - resolution: "@emotion/hash@npm:0.9.0" - checksum: 0910d3e9ec46cc780f691c96fb6f6f67b4f080b50ecf4f441bc4b33b5906e28099f530a368fe0b31c6bad38a857ac44df3c36f8978be603789d71330ac01af12 +"@emotion/hash@npm:^0.9.1": + version: 0.9.1 + resolution: "@emotion/hash@npm:0.9.1" + checksum: cdafe5da63fc1137f3db6e232fdcde9188b2b47ee66c56c29137199642a4086f42382d866911cfb4833cae2cc00271ab45cad3946b024f67b527bb7fac7f4c9d languageName: node linkType: hard -"@emotion/is-prop-valid@npm:^1.2.0": - version: 1.2.0 - resolution: "@emotion/is-prop-valid@npm:1.2.0" +"@emotion/is-prop-valid@npm:^1.2.1": + version: 1.2.1 + resolution: "@emotion/is-prop-valid@npm:1.2.1" dependencies: - "@emotion/memoize": ^0.8.0 - checksum: 098bfde166ddbc3ad635157dff8dd8b90ceb5ee2804b3dbc9ffbffcac33955390d6c6e94dc36b1fde8c90f49dc1e1359dfdcd967906b006a3966382dbe8cc90b + "@emotion/memoize": ^0.8.1 + checksum: 7c2aabdf0ca9986ca25abc9dae711348308cf18d418d64ffa4c8ffd2114806c47f2e06ba8ee769f38ec67d65bd59ec73f34d94023e81baa1c43510ac86ccd5e6 languageName: node linkType: hard -"@emotion/memoize@npm:^0.8.0": - version: 0.8.0 - resolution: "@emotion/memoize@npm:0.8.0" - checksum: 246087ec09b32b295af67a094253831f398aabd953d03d14f186acb8607ed2a755e944f5e20b5ccebb461f15c2e5ccbf8fe977bcf3be951cf10961c504e1e65b +"@emotion/memoize@npm:^0.8.1": + version: 0.8.1 + resolution: "@emotion/memoize@npm:0.8.1" + checksum: dffed372fc3b9fa2ba411e76af22b6bb686fb0cb07694fdfaa6dd2baeb0d5e4968c1a7caa472bfcf06a5997d5e7c7d16b90e993f9a6ffae79a2c3dbdc76dfe78 languageName: node linkType: hard -"@emotion/react@npm:^11.10.6": - version: 11.10.6 - resolution: "@emotion/react@npm:11.10.6" +"@emotion/react@npm:^11.11.0": + version: 11.11.0 + resolution: "@emotion/react@npm:11.11.0" dependencies: "@babel/runtime": ^7.18.3 - "@emotion/babel-plugin": ^11.10.6 - "@emotion/cache": ^11.10.5 - "@emotion/serialize": ^1.1.1 - "@emotion/use-insertion-effect-with-fallbacks": ^1.0.0 - "@emotion/utils": ^1.2.0 - "@emotion/weak-memoize": ^0.3.0 + "@emotion/babel-plugin": ^11.11.0 + "@emotion/cache": ^11.11.0 + "@emotion/serialize": ^1.1.2 + "@emotion/use-insertion-effect-with-fallbacks": ^1.0.1 + "@emotion/utils": ^1.2.1 + "@emotion/weak-memoize": ^0.3.1 hoist-non-react-statics: ^3.3.1 peerDependencies: react: ">=16.8.0" peerDependenciesMeta: "@types/react": optional: true - checksum: 4c5ce8ef279a8ce0d371414720d9b2b195ce6b30abf82d99a856ef3b7dc8643f8a32b9b01fded35d94ab9159e4982f7eb0ffa3c4b1aabb102180383e56232bcf + checksum: c287fdef680c6cc95c021d2ccd48891052cd97edfe371ef0c0a9aa78f1cb764587c80a50e9f22eb943f522258dc4d7b80c4778c45331720e330e338db32f8a95 languageName: node linkType: hard -"@emotion/serialize@npm:^1.1.1": - version: 1.1.1 - resolution: "@emotion/serialize@npm:1.1.1" +"@emotion/serialize@npm:^1.1.2": + version: 1.1.2 + resolution: "@emotion/serialize@npm:1.1.2" dependencies: - "@emotion/hash": ^0.9.0 - "@emotion/memoize": ^0.8.0 - "@emotion/unitless": ^0.8.0 - "@emotion/utils": ^1.2.0 + "@emotion/hash": ^0.9.1 + "@emotion/memoize": ^0.8.1 + "@emotion/unitless": ^0.8.1 + "@emotion/utils": ^1.2.1 csstype: ^3.0.2 - checksum: ea353abbf530ede8b74fe4df30eb626f245f710ce0bfcb9d34e72630a1dede688fddf02b1143f33a1a4ef5b66b70715a3c1cd6a12ec43f5b585ed60d4f3e8712 + checksum: d243e0e5abce8d2183d25a32ec89bf650ee741ebadb29e6405abde05d4e2ed446ba5b3f725a29833ad709d0d08f0a5c8d0532fdcd43f4b23d931d8b6d4f218c1 languageName: node linkType: hard -"@emotion/sheet@npm:^1.2.1": - version: 1.2.1 - resolution: "@emotion/sheet@npm:1.2.1" - checksum: 88268c00005d310df3ebb249b839ad0b234943da5a0cc614b232b9bd4ae600292dca9b0f61c45cde3a592c77459e880d77a2aa73af20ec3c0d579afccc3f71af +"@emotion/sheet@npm:^1.2.2": + version: 1.2.2 + resolution: "@emotion/sheet@npm:1.2.2" + checksum: 69827a1bfa43d7b188f1d8cea42163143a36312543fdade5257c459a2b3efd7ce386aac84ba152bc2517a4f7e54384c04800b26adb382bb284ac7e4ad40e584b languageName: node linkType: hard -"@emotion/styled@npm:^11.10.6": - version: 11.10.6 - resolution: "@emotion/styled@npm:11.10.6" +"@emotion/styled@npm:^11.11.0": + version: 11.11.0 + resolution: "@emotion/styled@npm:11.11.0" dependencies: "@babel/runtime": ^7.18.3 - "@emotion/babel-plugin": ^11.10.6 - "@emotion/is-prop-valid": ^1.2.0 - "@emotion/serialize": ^1.1.1 - "@emotion/use-insertion-effect-with-fallbacks": ^1.0.0 - "@emotion/utils": ^1.2.0 + "@emotion/babel-plugin": ^11.11.0 + "@emotion/is-prop-valid": ^1.2.1 + "@emotion/serialize": ^1.1.2 + "@emotion/use-insertion-effect-with-fallbacks": ^1.0.1 + "@emotion/utils": ^1.2.1 peerDependencies: "@emotion/react": ^11.0.0-rc.0 react: ">=16.8.0" peerDependenciesMeta: "@types/react": optional: true - checksum: 69e968bb359758ec454c547d6f1d5b1a50a8c149c13274119f63d81fbb76db46b2df53c236ecbc9cb15696f9834647156d715e5e6ccc84ff7b1852617e367386 + checksum: a168bd7a8a6f254e54a321be4c7b7dd4bf65815e6570ba7c5a435b7d5aeebd76434e04886db7799a955817c8d5bf0103a3dcc3c785fba2bb53922320dda59a10 languageName: node linkType: hard -"@emotion/unitless@npm:^0.8.0": - version: 0.8.0 - resolution: "@emotion/unitless@npm:0.8.0" - checksum: 1f2cfb7c0ccb83c20b1c6d8d92a74a93da4b2a440f9a0d49ded08647faf299065a2ffde17e1335920fa10397b85f8635bbfe14f3cd29222a59ea81d978478072 +"@emotion/unitless@npm:^0.8.1": + version: 0.8.1 + resolution: "@emotion/unitless@npm:0.8.1" + checksum: a1ed508628288f40bfe6dd17d431ed899c067a899fa293a13afe3aed1d70fac0412b8a215fafab0b42829360db687fecd763e5f01a64ddc4a4b58ec3112ff548 languageName: node linkType: hard -"@emotion/use-insertion-effect-with-fallbacks@npm:^1.0.0": - version: 1.0.0 - resolution: "@emotion/use-insertion-effect-with-fallbacks@npm:1.0.0" +"@emotion/use-insertion-effect-with-fallbacks@npm:^1.0.1": + version: 1.0.1 + resolution: "@emotion/use-insertion-effect-with-fallbacks@npm:1.0.1" peerDependencies: react: ">=16.8.0" - checksum: 0c5fbd36a4f416a5abaf428ba3dca6e79018c4c74016ecb4e3991a11cf8b5dbd306d7770fee09692971335e33f97e3b555deda595e5ae7831841505078bd07d7 + checksum: a15b2167940e3a908160687b73fc4fcd81e59ab45136b6967f02c7c419d9a149acd22a416b325c389642d4f1c3d33cf4196cad6b618128b55b7c74f6807a240b languageName: node linkType: hard -"@emotion/utils@npm:^1.2.0": - version: 1.2.0 - resolution: "@emotion/utils@npm:1.2.0" - checksum: 7051cec83bb49688549667484058d3a19a30001fa3692c23f7a2e727c05121f952854e1196feb9ece4fa36914705ebf474edba833a2178bdc133c654b5e3ca7d +"@emotion/utils@npm:^1.2.1": + version: 1.2.1 + resolution: "@emotion/utils@npm:1.2.1" + checksum: db43ca803361740c14dfb1cca1464d10d27f4c8b40d3e8864e6932ccf375d1450778ff4e4eadee03fb97f2aeb18de9fae98294905596a12ff7d4cd1910414d8d languageName: node linkType: hard -"@emotion/weak-memoize@npm:^0.3.0": - version: 0.3.0 - resolution: "@emotion/weak-memoize@npm:0.3.0" - checksum: 1771687cc3b3280371de12698f1b78756c64654fc7d15ce76e1fb5d4adf9fd49d4411e41276bbfd5b521ef9cef647196aa9dca26f936c466fb80bf48491fa844 +"@emotion/weak-memoize@npm:^0.3.1": + version: 0.3.1 + resolution: "@emotion/weak-memoize@npm:0.3.1" + checksum: ed514b3cb94bbacece4ac2450d98898066c0a0698bdeda256e312405ca53634cb83c75889b25cd8bbbe185c80f4c05a1f0a0091e1875460ba6be61d0334f0b8a languageName: node linkType: hard @@ -578,27 +596,27 @@ __metadata: languageName: node linkType: hard -"@eslint/eslintrc@npm:^2.0.2": - version: 2.0.2 - resolution: "@eslint/eslintrc@npm:2.0.2" +"@eslint/eslintrc@npm:^2.0.3": + version: 2.0.3 + resolution: "@eslint/eslintrc@npm:2.0.3" dependencies: ajv: ^6.12.4 debug: ^4.3.2 - espree: ^9.5.1 + espree: ^9.5.2 globals: ^13.19.0 ignore: ^5.2.0 import-fresh: ^3.2.1 js-yaml: ^4.1.0 minimatch: ^3.1.2 strip-json-comments: ^3.1.1 - checksum: 6ae7360f4e45fbfa6f66ba92be506860d15f070847bdad4542c97eda98b16c1f4f5be4a8807ccb284224691eb4125bbffc51e8933f6cb186d6cf23a8e668eb5b + checksum: 46291c33bf580ab12101fb7f20adabaa60326a7de094409ab4a5ca4611552ab2325f8d677d6c1d2d9f45f83f93360b115a0b4488bc48180cca0d0f386804d829 languageName: node linkType: hard -"@eslint/js@npm:8.37.0": - version: 8.37.0 - resolution: "@eslint/js@npm:8.37.0" - checksum: 6abb3d97412ac960c7436ecdaa56eb00ac57c34782dc0901c82b259c32704e45044927b2910d786ec2127e548986d67e7ba29fec46abfb5d8fc9bedf379af2cf +"@eslint/js@npm:8.41.0": + version: 8.41.0 + resolution: "@eslint/js@npm:8.41.0" + checksum: 41cf403ccebbc90315c7190bd901fa17975327022146b2db8c846ec5b11d04c56b33ccf9064b9da7349068e96bef8f322423fbcce700d815b4097fd808aaa4c6 languageName: node linkType: hard @@ -696,21 +714,14 @@ __metadata: languageName: node linkType: hard -"@msgpack/msgpack@npm:^3.0.0-beta2": - version: 3.0.0-beta2 - resolution: "@msgpack/msgpack@npm:3.0.0-beta2" - checksum: f4dc4c210bb722f0f7407214334dda728263cce8eb0d3f9af1a40a48e13ce175b599de35a757dce5f1d92a74d3ce4d0242feb67516f308a626058d0f98f01c70 - languageName: node - linkType: hard - -"@mui/base@npm:5.0.0-alpha.124": - version: 5.0.0-alpha.124 - resolution: "@mui/base@npm:5.0.0-alpha.124" +"@mui/base@npm:5.0.0-beta.2": + version: 5.0.0-beta.2 + resolution: "@mui/base@npm:5.0.0-beta.2" dependencies: "@babel/runtime": ^7.21.0 - "@emotion/is-prop-valid": ^1.2.0 - "@mui/types": ^7.2.3 - "@mui/utils": ^5.11.13 + "@emotion/is-prop-valid": ^1.2.1 + "@mui/types": ^7.2.4 + "@mui/utils": ^5.13.1 "@popperjs/core": ^2.11.7 clsx: ^1.2.1 prop-types: ^15.8.1 @@ -722,14 +733,14 @@ __metadata: peerDependenciesMeta: "@types/react": optional: true - checksum: 103a3af33f7125cf7320a6b5de49cc6223563978066ebd4a9721de385c2b7990ff5e14139e5bd290714bd0e5db58b9b960b08051d5c14219c5835ba1a0e0c587 + checksum: 56d8aa39709ef28bd9d39ec7202f925babbf1dce019ebe1c689b6deebd7049f93ba92af6d1bbc3aeef78a052cb8557ad9ea56c0f6294f71b6f5269e805e27e19 languageName: node linkType: hard -"@mui/core-downloads-tracker@npm:^5.11.16": - version: 5.11.16 - resolution: "@mui/core-downloads-tracker@npm:5.11.16" - checksum: c9f1ed7a6f78ba9db80cb59c4a17ab4ef9af6837bf85ae87161fb745e64a617c91f7f1b35296c33c9039d42e61e49290eedae2e22ea16ded5a57e2c239c055a0 +"@mui/core-downloads-tracker@npm:^5.13.2": + version: 5.13.2 + resolution: "@mui/core-downloads-tracker@npm:5.13.2" + checksum: 6c6aef994400c96aaca3a23be4df5e0606d38f16bac241fd4dc953ef2254ac727b4a699c030ca36c14604f7294b39246d9c048b7a1356642dbb6b8e931597497 languageName: node linkType: hard @@ -749,17 +760,17 @@ __metadata: languageName: node linkType: hard -"@mui/material@npm:^5.11.16": - version: 5.11.16 - resolution: "@mui/material@npm:5.11.16" +"@mui/material@npm:^5.13.2": + version: 5.13.2 + resolution: "@mui/material@npm:5.13.2" dependencies: "@babel/runtime": ^7.21.0 - "@mui/base": 5.0.0-alpha.124 - "@mui/core-downloads-tracker": ^5.11.16 - "@mui/system": ^5.11.16 - "@mui/types": ^7.2.3 - "@mui/utils": ^5.11.13 - "@types/react-transition-group": ^4.4.5 + "@mui/base": 5.0.0-beta.2 + "@mui/core-downloads-tracker": ^5.13.2 + "@mui/system": ^5.13.2 + "@mui/types": ^7.2.4 + "@mui/utils": ^5.13.1 + "@types/react-transition-group": ^4.4.6 clsx: ^1.2.1 csstype: ^3.1.2 prop-types: ^15.8.1 @@ -778,16 +789,16 @@ __metadata: optional: true "@types/react": optional: true - checksum: a674b179edd9af749d5a7edee6e2bf031b56b06311d6d0c567ec2a53936a0416443a74dcd02d360748442260f0829ad087ed5c1172d714fec2eb29d22683d234 + checksum: 7e5ca978258ac04af2399ddf251705aa0a06b97653fc76b3363d6eecdfac56800005faaf8797da6074e7087e970282571fb980e403c0faa569843735f969408d languageName: node linkType: hard -"@mui/private-theming@npm:^5.11.13": - version: 5.11.13 - resolution: "@mui/private-theming@npm:5.11.13" +"@mui/private-theming@npm:^5.13.1": + version: 5.13.1 + resolution: "@mui/private-theming@npm:5.13.1" dependencies: "@babel/runtime": ^7.21.0 - "@mui/utils": ^5.11.13 + "@mui/utils": ^5.13.1 prop-types: ^15.8.1 peerDependencies: "@types/react": ^17.0.0 || ^18.0.0 @@ -795,16 +806,16 @@ __metadata: peerDependenciesMeta: "@types/react": optional: true - checksum: 114801958fe0153b2c7c07640d5453dc783bebdc96e35612f18b90370a461bb0d5c24d0d3ea5b48dc369299660e4246f6d739a97aa47821e5d5b955329d38ec4 + checksum: 1242f90642ee68792b9e72e7bcae8a2dd00c2c0bdd9cd825d9f154263755a4c4e213daa752e7fe64062a082cd6f60875f06eb122181c06e6b73f4d9caf68b949 languageName: node linkType: hard -"@mui/styled-engine@npm:^5.11.16": - version: 5.11.16 - resolution: "@mui/styled-engine@npm:5.11.16" +"@mui/styled-engine@npm:^5.13.2": + version: 5.13.2 + resolution: "@mui/styled-engine@npm:5.13.2" dependencies: "@babel/runtime": ^7.21.0 - "@emotion/cache": ^11.10.5 + "@emotion/cache": ^11.11.0 csstype: ^3.1.2 prop-types: ^15.8.1 peerDependencies: @@ -816,19 +827,19 @@ __metadata: optional: true "@emotion/styled": optional: true - checksum: 308b7406c7e4ade762c21339644803ad5510c96940e1bd99586cbcea9fa3468216fbace47ed603ea8106fff5d493dadba4ebac07e89175c3f12d50b7571c90d8 + checksum: ed53dbe0151067c6843989fc41245f98a56c48e7c341ce61656b71de2cb334bdf1cea8bbf625ddbc4ce600d6005717c2cd1f34473b3b41e154662353cf2c108e languageName: node linkType: hard -"@mui/system@npm:^5.11.16": - version: 5.11.16 - resolution: "@mui/system@npm:5.11.16" +"@mui/system@npm:^5.13.2": + version: 5.13.2 + resolution: "@mui/system@npm:5.13.2" dependencies: "@babel/runtime": ^7.21.0 - "@mui/private-theming": ^5.11.13 - "@mui/styled-engine": ^5.11.16 - "@mui/types": ^7.2.3 - "@mui/utils": ^5.11.13 + "@mui/private-theming": ^5.13.1 + "@mui/styled-engine": ^5.13.2 + "@mui/types": ^7.2.4 + "@mui/utils": ^5.13.1 clsx: ^1.2.1 csstype: ^3.1.2 prop-types: ^15.8.1 @@ -844,34 +855,34 @@ __metadata: optional: true "@types/react": optional: true - checksum: 53c699dd20dd0bc312b81844a5e2a55e184d13042300e5027ace490b89a41e6e01f857d7b8a21b95aad6e215b4467a0fa9bbf7b1bb1b877fe8429f986378fd4e + checksum: 34ebb580e5dd83123cc397c3fd54c3430f66ab715eb1538cf2510821d88249814294f79ea046081b61249643383fd9c23552d9791322855fa2099bf8f1c4e51b languageName: node linkType: hard -"@mui/types@npm:^7.2.3": - version: 7.2.3 - resolution: "@mui/types@npm:7.2.3" +"@mui/types@npm:^7.2.4": + version: 7.2.4 + resolution: "@mui/types@npm:7.2.4" peerDependencies: "@types/react": "*" peerDependenciesMeta: "@types/react": optional: true - checksum: d66575b6e5b37462bc4182ee5eae7f9f10792d4cdbe407fdb055136c62c3e05c6fa8969680373a723fbede0d1b1c17941c2cccd5a0409751a09aa22a712eafc0 + checksum: 17411effd184eff34d6a1a55b2249c7e1ef195bb30c48154f0b16fdce428ff55be4ec5dde8b4a556c01eda2d34e3dcc18d925f8fdee606f5bc15f91167f0ecbc languageName: node linkType: hard -"@mui/utils@npm:^5.11.13": - version: 5.11.13 - resolution: "@mui/utils@npm:5.11.13" +"@mui/utils@npm:^5.13.1": + version: 5.13.1 + resolution: "@mui/utils@npm:5.13.1" dependencies: "@babel/runtime": ^7.21.0 "@types/prop-types": ^15.7.5 - "@types/react-is": ^16.7.1 || ^17.0.0 + "@types/react-is": ^18.2.0 prop-types: ^15.8.1 react-is: ^18.2.0 peerDependencies: react: ^17.0.0 || ^18.0.0 - checksum: ba9812784d90b3bbe7c56cc0daf35cd972b576789b2dedb4d1df73005bc55af58d01c11dd21f2104206a49d93f08b9ba1a19a63c605df7b4760a1a864f675ead + checksum: 05f28ed16c7c15deecb7d55962efa21f073f09342758e01bf6a618dd4532a8bf074d6e3c306cdf5f3c6d7b92f6729b0b157e828a13aff9d2445b1da997e1a7eb languageName: node linkType: hard @@ -943,10 +954,10 @@ __metadata: languageName: node linkType: hard -"@remix-run/router@npm:1.5.0, @remix-run/router@npm:^1.5.0": - version: 1.5.0 - resolution: "@remix-run/router@npm:1.5.0" - checksum: 63c3695df0470943213f8144183501bbb6a8176671f2ed4547ffa1852f79fd71054b4b4a715795e9e250198991d9e6e121b3fcab9c27df37871dcdfbc8de83a1 +"@remix-run/router@npm:1.6.2": + version: 1.6.2 + resolution: "@remix-run/router@npm:1.6.2" + checksum: 73da6884e53873e4290abb3978373cafc3f351994273b0663eda5e12c81cb427fc6fe4df1924569d9a214f701d0106cf37122455951e0239d7e6fa35071df558 languageName: node linkType: hard @@ -966,23 +977,16 @@ __metadata: languageName: node linkType: hard -"@sindresorhus/is@npm:^4.0.0": - version: 4.6.0 - resolution: "@sindresorhus/is@npm:4.6.0" - checksum: 33b6fb1d0834ec8dd7689ddc0e2781c2bfd8b9c4e4bacbcb14111e0ae00621f2c264b8a7d36541799d74888b5dccdf422a891a5cb5a709ace26325eedc81e22e - languageName: node - linkType: hard - -"@svgr/babel-plugin-add-jsx-attribute@npm:^6.5.1": - version: 6.5.1 - resolution: "@svgr/babel-plugin-add-jsx-attribute@npm:6.5.1" +"@svgr/babel-plugin-add-jsx-attribute@npm:^7.0.0": + version: 7.0.0 + resolution: "@svgr/babel-plugin-add-jsx-attribute@npm:7.0.0" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: a13ed0797189d5497890530449029bec388310e260a96459e304e2729e7a2cf4d20d34f882d9a77ccce73dd3d36065afbb6987258fdff618d7d57955065a8ad4 + checksum: 66714c2961f21409b0d33f0f65cf52f2496838b4ed056e98c872faa9f60754fae491ca4397717991eaa9884a0a44ae8920fd550101c9877759bd73f361a49800 languageName: node linkType: hard -"@svgr/babel-plugin-remove-jsx-attribute@npm:*": +"@svgr/babel-plugin-remove-jsx-attribute@npm:^7.0.0": version: 7.0.0 resolution: "@svgr/babel-plugin-remove-jsx-attribute@npm:7.0.0" peerDependencies: @@ -991,7 +995,7 @@ __metadata: languageName: node linkType: hard -"@svgr/babel-plugin-remove-jsx-empty-expression@npm:*": +"@svgr/babel-plugin-remove-jsx-empty-expression@npm:^7.0.0": version: 7.0.0 resolution: "@svgr/babel-plugin-remove-jsx-empty-expression@npm:7.0.0" peerDependencies: @@ -1000,190 +1004,187 @@ __metadata: languageName: node linkType: hard -"@svgr/babel-plugin-replace-jsx-attribute-value@npm:^6.5.1": - version: 6.5.1 - resolution: "@svgr/babel-plugin-replace-jsx-attribute-value@npm:6.5.1" +"@svgr/babel-plugin-replace-jsx-attribute-value@npm:^7.0.0": + version: 7.0.0 + resolution: "@svgr/babel-plugin-replace-jsx-attribute-value@npm:7.0.0" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 318786787c9a217c33a7340c8856436858e1fffa5a6df635fedc6b9a371f3afea080ea074b9e3cfbbd9dd962ead924fde8bc9855a394c38dd60e391883a58c81 + checksum: 9a39807bd09fb00c121e2b6952e24b90b6d9cd2318105176b93ccc4e1ec5b87b9999b96bce6f9f5e7769033583565908b440951de89ac9c3cb82ea0e0a3db686 languageName: node linkType: hard -"@svgr/babel-plugin-svg-dynamic-title@npm:^6.5.1": - version: 6.5.1 - resolution: "@svgr/babel-plugin-svg-dynamic-title@npm:6.5.1" +"@svgr/babel-plugin-svg-dynamic-title@npm:^7.0.0": + version: 7.0.0 + resolution: "@svgr/babel-plugin-svg-dynamic-title@npm:7.0.0" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 16ef228c793b909fec47dd7dc05c1c3c2d77a824f42055df37e141e0534081b1bc4aec6dcc51be50c221df9f262f59270fc1c379923bfd4f5db302abafabfd8d + checksum: 49dd7907a63bd7643e6081d0bc4daee23e3fc095b6eafc58760f5d67314eee1ea60a6788ccbe68e2457f083ea31522c847119fe48eb6e2dc20956b9bb3316cbb languageName: node linkType: hard -"@svgr/babel-plugin-svg-em-dimensions@npm:^6.5.1": - version: 6.5.1 - resolution: "@svgr/babel-plugin-svg-em-dimensions@npm:6.5.1" +"@svgr/babel-plugin-svg-em-dimensions@npm:^7.0.0": + version: 7.0.0 + resolution: "@svgr/babel-plugin-svg-em-dimensions@npm:7.0.0" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: dfdd5cbe6ae543505eaa0da69df0735b7407294c4b0504b3e74c0e7e371f1acb914eb99fd21ff39ef5bd626b3474f064a4cccc50f41b7c556ee834f9a6d6610a + checksum: 9d5b569b75a612074b03aab20837dd1858f97d002b05fc9a2ec939aebbc8053e893960e264a1f2261bf0c426e4f8fa93c72313bcf7dface89fc09bc643147ebd languageName: node linkType: hard -"@svgr/babel-plugin-transform-react-native-svg@npm:^6.5.1": - version: 6.5.1 - resolution: "@svgr/babel-plugin-transform-react-native-svg@npm:6.5.1" +"@svgr/babel-plugin-transform-react-native-svg@npm:^7.0.0": + version: 7.0.0 + resolution: "@svgr/babel-plugin-transform-react-native-svg@npm:7.0.0" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 332fbf3bbc19d938b744440dbab9c8acd8f7a2ed6bf9c4e23f40e3f2c25615a60b3bf00902a4f1f6c20b5f382a1547b3acc6f2b2d70d80e532b5d45945f1b979 + checksum: 9091bd61d787e8506965f10a946dec463881b337aa435eedb0d5423ece1d0589fa643c2e01003cbb3447d3dbdf5d937ff7bae487a3098abbbe94ac04c84022d8 languageName: node linkType: hard -"@svgr/babel-plugin-transform-svg-component@npm:^6.5.1": - version: 6.5.1 - resolution: "@svgr/babel-plugin-transform-svg-component@npm:6.5.1" +"@svgr/babel-plugin-transform-svg-component@npm:^7.0.0": + version: 7.0.0 + resolution: "@svgr/babel-plugin-transform-svg-component@npm:7.0.0" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 8d9e1c7c62abce23837e53cdacc6d09bc1f1f2b0ad7322105001c097995e9aa8dca4fa41acf39148af69f342e40081c438106949fb083e997ca497cb0448f27d + checksum: 715c371bdae660fa9452083f2be6c1736d9ad516dc7134656c6e70374799de94eacda596504394aa6934aacb6da9099acd99569089220d66aaf91b34aa934c7b languageName: node linkType: hard -"@svgr/babel-preset@npm:^6.5.1": - version: 6.5.1 - resolution: "@svgr/babel-preset@npm:6.5.1" +"@svgr/babel-preset@npm:^7.0.0": + version: 7.0.0 + resolution: "@svgr/babel-preset@npm:7.0.0" dependencies: - "@svgr/babel-plugin-add-jsx-attribute": ^6.5.1 - "@svgr/babel-plugin-remove-jsx-attribute": "*" - "@svgr/babel-plugin-remove-jsx-empty-expression": "*" - "@svgr/babel-plugin-replace-jsx-attribute-value": ^6.5.1 - "@svgr/babel-plugin-svg-dynamic-title": ^6.5.1 - "@svgr/babel-plugin-svg-em-dimensions": ^6.5.1 - "@svgr/babel-plugin-transform-react-native-svg": ^6.5.1 - "@svgr/babel-plugin-transform-svg-component": ^6.5.1 + "@svgr/babel-plugin-add-jsx-attribute": ^7.0.0 + "@svgr/babel-plugin-remove-jsx-attribute": ^7.0.0 + "@svgr/babel-plugin-remove-jsx-empty-expression": ^7.0.0 + "@svgr/babel-plugin-replace-jsx-attribute-value": ^7.0.0 + "@svgr/babel-plugin-svg-dynamic-title": ^7.0.0 + "@svgr/babel-plugin-svg-em-dimensions": ^7.0.0 + "@svgr/babel-plugin-transform-react-native-svg": ^7.0.0 + "@svgr/babel-plugin-transform-svg-component": ^7.0.0 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 8e8d7a0049279152f9ac308fbfd4ce74063d8a376154718cba6309bae4316318804a32201c75c5839c629f8e1e5d641a87822764000998161d0fc1de24b0374a + checksum: 7d0755e2f007d4108b9ccbd7ccb2de2787ed3aa54cf873426bb211666996fe7a4fde73710a76bbdc169e1e72d7eca1dec5a6b26f14ab3124ff154ecbe387b69a languageName: node linkType: hard -"@svgr/core@npm:^6.5.1": - version: 6.5.1 - resolution: "@svgr/core@npm:6.5.1" +"@svgr/core@npm:^7.0.0": + version: 7.0.0 + resolution: "@svgr/core@npm:7.0.0" dependencies: - "@babel/core": ^7.19.6 - "@svgr/babel-preset": ^6.5.1 - "@svgr/plugin-jsx": ^6.5.1 + "@babel/core": ^7.21.3 + "@svgr/babel-preset": ^7.0.0 camelcase: ^6.2.0 - cosmiconfig: ^7.0.1 - checksum: 60cce11e13391171132115dcc8da592d23e51f155ebadf9b819bd1836b8c13d40aa5c30a03a7d429f65e70a71c50669b2e10c94e4922de4e58bc898275f46c05 + cosmiconfig: ^8.1.3 + checksum: 347617081188fc0ed5de53a8643b70949c8737a1b5baf6e4a2dd23ecb8311de111d4e76f8f005959ec66e7d53a5f8155249f6b947c8111042b978fc798f53c4c languageName: node linkType: hard -"@svgr/hast-util-to-babel-ast@npm:^6.5.1": - version: 6.5.1 - resolution: "@svgr/hast-util-to-babel-ast@npm:6.5.1" +"@svgr/hast-util-to-babel-ast@npm:^7.0.0": + version: 7.0.0 + resolution: "@svgr/hast-util-to-babel-ast@npm:7.0.0" dependencies: - "@babel/types": ^7.20.0 + "@babel/types": ^7.21.3 entities: ^4.4.0 - checksum: 18fa37b36581ba1678f5cc5a05ce0411e08df4db267f3cd900af7ffdf5bd90522f3a46465f315cd5d7345264949479133930aafdd27ce05c474e63756196256f + checksum: 2d6880fac9445559cc2e29f87782a52c37d2db7b99a4892f65def1e79a8239d7961c483934ff9ce2d37cb087f5b34c80ca5a51f7bc9eaceacfe0bd66e4e64373 languageName: node linkType: hard -"@svgr/plugin-jsx@npm:^6.5.1": - version: 6.5.1 - resolution: "@svgr/plugin-jsx@npm:6.5.1" +"@svgr/plugin-jsx@npm:^7.0.0": + version: 7.0.0 + resolution: "@svgr/plugin-jsx@npm:7.0.0" dependencies: - "@babel/core": ^7.19.6 - "@svgr/babel-preset": ^6.5.1 - "@svgr/hast-util-to-babel-ast": ^6.5.1 + "@babel/core": ^7.21.3 + "@svgr/babel-preset": ^7.0.0 + "@svgr/hast-util-to-babel-ast": ^7.0.0 svg-parser: ^2.0.4 - peerDependencies: - "@svgr/core": ^6.0.0 - checksum: 365da6e43ceeff6b49258fa2fbb3c880210300e4a85ba74831e92d2dc9c53e6ab8dda422dc33fb6a339803227cf8d9a0024ce769401c46fd87209abe36d5ae43 + checksum: bd649a306b83bc355315265046461cfa089c81604785b081fe0ccffd0112dc8bfad1e19d8e042d85339792458ab2e9022f8bf29fdd64bfea90718a40553ce00e languageName: node linkType: hard -"@swc/core-darwin-arm64@npm:1.3.46": - version: 1.3.46 - resolution: "@swc/core-darwin-arm64@npm:1.3.46" +"@swc/core-darwin-arm64@npm:1.3.56": + version: 1.3.56 + resolution: "@swc/core-darwin-arm64@npm:1.3.56" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@swc/core-darwin-x64@npm:1.3.46": - version: 1.3.46 - resolution: "@swc/core-darwin-x64@npm:1.3.46" +"@swc/core-darwin-x64@npm:1.3.56": + version: 1.3.56 + resolution: "@swc/core-darwin-x64@npm:1.3.56" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@swc/core-linux-arm-gnueabihf@npm:1.3.46": - version: 1.3.46 - resolution: "@swc/core-linux-arm-gnueabihf@npm:1.3.46" +"@swc/core-linux-arm-gnueabihf@npm:1.3.56": + version: 1.3.56 + resolution: "@swc/core-linux-arm-gnueabihf@npm:1.3.56" conditions: os=linux & cpu=arm languageName: node linkType: hard -"@swc/core-linux-arm64-gnu@npm:1.3.46": - version: 1.3.46 - resolution: "@swc/core-linux-arm64-gnu@npm:1.3.46" +"@swc/core-linux-arm64-gnu@npm:1.3.56": + version: 1.3.56 + resolution: "@swc/core-linux-arm64-gnu@npm:1.3.56" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard -"@swc/core-linux-arm64-musl@npm:1.3.46": - version: 1.3.46 - resolution: "@swc/core-linux-arm64-musl@npm:1.3.46" +"@swc/core-linux-arm64-musl@npm:1.3.56": + version: 1.3.56 + resolution: "@swc/core-linux-arm64-musl@npm:1.3.56" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard -"@swc/core-linux-x64-gnu@npm:1.3.46": - version: 1.3.46 - resolution: "@swc/core-linux-x64-gnu@npm:1.3.46" +"@swc/core-linux-x64-gnu@npm:1.3.56": + version: 1.3.56 + resolution: "@swc/core-linux-x64-gnu@npm:1.3.56" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard -"@swc/core-linux-x64-musl@npm:1.3.46": - version: 1.3.46 - resolution: "@swc/core-linux-x64-musl@npm:1.3.46" +"@swc/core-linux-x64-musl@npm:1.3.56": + version: 1.3.56 + resolution: "@swc/core-linux-x64-musl@npm:1.3.56" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard -"@swc/core-win32-arm64-msvc@npm:1.3.46": - version: 1.3.46 - resolution: "@swc/core-win32-arm64-msvc@npm:1.3.46" +"@swc/core-win32-arm64-msvc@npm:1.3.56": + version: 1.3.56 + resolution: "@swc/core-win32-arm64-msvc@npm:1.3.56" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@swc/core-win32-ia32-msvc@npm:1.3.46": - version: 1.3.46 - resolution: "@swc/core-win32-ia32-msvc@npm:1.3.46" +"@swc/core-win32-ia32-msvc@npm:1.3.56": + version: 1.3.56 + resolution: "@swc/core-win32-ia32-msvc@npm:1.3.56" conditions: os=win32 & cpu=ia32 languageName: node linkType: hard -"@swc/core-win32-x64-msvc@npm:1.3.46": - version: 1.3.46 - resolution: "@swc/core-win32-x64-msvc@npm:1.3.46" +"@swc/core-win32-x64-msvc@npm:1.3.56": + version: 1.3.56 + resolution: "@swc/core-win32-x64-msvc@npm:1.3.56" conditions: os=win32 & cpu=x64 languageName: node linkType: hard -"@swc/core@npm:^1.3.35": - version: 1.3.46 - resolution: "@swc/core@npm:1.3.46" +"@swc/core@npm:^1.3.56": + version: 1.3.56 + resolution: "@swc/core@npm:1.3.56" dependencies: - "@swc/core-darwin-arm64": 1.3.46 - "@swc/core-darwin-x64": 1.3.46 - "@swc/core-linux-arm-gnueabihf": 1.3.46 - "@swc/core-linux-arm64-gnu": 1.3.46 - "@swc/core-linux-arm64-musl": 1.3.46 - "@swc/core-linux-x64-gnu": 1.3.46 - "@swc/core-linux-x64-musl": 1.3.46 - "@swc/core-win32-arm64-msvc": 1.3.46 - "@swc/core-win32-ia32-msvc": 1.3.46 - "@swc/core-win32-x64-msvc": 1.3.46 + "@swc/core-darwin-arm64": 1.3.56 + "@swc/core-darwin-x64": 1.3.56 + "@swc/core-linux-arm-gnueabihf": 1.3.56 + "@swc/core-linux-arm64-gnu": 1.3.56 + "@swc/core-linux-arm64-musl": 1.3.56 + "@swc/core-linux-x64-gnu": 1.3.56 + "@swc/core-linux-x64-musl": 1.3.56 + "@swc/core-win32-arm64-msvc": 1.3.56 + "@swc/core-win32-ia32-msvc": 1.3.56 + "@swc/core-win32-x64-msvc": 1.3.56 peerDependencies: "@swc/helpers": ^0.5.0 dependenciesMeta: @@ -1207,22 +1208,16 @@ __metadata: optional: true "@swc/core-win32-x64-msvc": optional: true - checksum: 245c213425fecc16ae5a078ec0451f3567430609ef40ef4f1dc2fb795de86229d4203956b5b02ae304232cf93b25bbec3aaed23989d41fdb432aecc1cfdabc0e + peerDependenciesMeta: + "@swc/helpers": + optional: true + checksum: c468e281f0249742bc0ba4b7cd4076cdbf87bfc82b8bd5ad1ca8940d36372ca22754df80ab54b22613121680718eab26b92c48c8c9f5f3abb24434f05e5e1ea0 languageName: node linkType: hard -"@szmarczak/http-timer@npm:^4.0.5": - version: 4.0.6 - resolution: "@szmarczak/http-timer@npm:4.0.6" - dependencies: - defer-to-connect: ^2.0.0 - checksum: 73946918c025339db68b09abd91fa3001e87fc749c619d2e9c2003a663039d4c3cb89836c98a96598b3d47dec2481284ba85355392644911f5ecd2336536697f - languageName: node - linkType: hard - -"@table-library/react-table-library@npm:4.1.0": - version: 4.1.0 - resolution: "@table-library/react-table-library@npm:4.1.0" +"@table-library/react-table-library@npm:4.1.4": + version: 4.1.4 + resolution: "@table-library/react-table-library@npm:4.1.4" dependencies: clsx: 1.1.1 react-virtualized-auto-sizer: 1.0.7 @@ -1231,7 +1226,7 @@ __metadata: "@emotion/react": ">= 11" react: ">=16.8.0" react-dom: ">=16.8.0" - checksum: e64571d6ad5b755abbef3e30dcb56aaa2362ffb26a7cbade56828cedec2532530ddf463a275d7d264ff6822d1191694153e63d9b3b13fcd3f540fa2b3d8a2b4e + checksum: ace2711b777e596ed5fd1623108ac8fb7403566b108725f97e6710019a11f28f1f86b916932a662ad53ad18c994ba626f0b2fd7e6be91c44dff16d54e0364f0d languageName: node linkType: hard @@ -1242,25 +1237,6 @@ __metadata: languageName: node linkType: hard -"@types/cacheable-request@npm:^6.0.1": - version: 6.0.3 - resolution: "@types/cacheable-request@npm:6.0.3" - dependencies: - "@types/http-cache-semantics": "*" - "@types/keyv": ^3.1.4 - "@types/node": "*" - "@types/responselike": ^1.0.0 - checksum: 10816a88e4e5b144d43c1d15a81003f86d649776c7f410c9b5e6579d0ad9d4ca71c541962fb403077388b446e41af7ae38d313e46692144985f006ac5e11fa03 - languageName: node - linkType: hard - -"@types/emscripten@npm:^1.39.6": - version: 1.39.6 - resolution: "@types/emscripten@npm:1.39.6" - checksum: cb1ea8ccddada1d304bdf11a54daa0d1e87f29cea947eceff54c1e0a752d2cc185eeffdcf52042f24420aa8e1fa9bbfdbab1231fb2531eefcfdc788199fee2de - languageName: node - linkType: hard - "@types/estree@npm:^1.0.0": version: 1.0.0 resolution: "@types/estree@npm:1.0.0" @@ -1275,23 +1251,6 @@ __metadata: languageName: node linkType: hard -"@types/hoist-non-react-statics@npm:*": - version: 3.3.1 - resolution: "@types/hoist-non-react-statics@npm:3.3.1" - dependencies: - "@types/react": "*" - hoist-non-react-statics: ^3.3.0 - checksum: 5ed808e5fbf0979fe07acd631147420c30319383f4388a57e0fb811c6ff30abef286e937a84c7b00f4647ca7f1ab390cc42af0bfc7547a87d2e59e0e7072d92b - languageName: node - linkType: hard - -"@types/http-cache-semantics@npm:*": - version: 4.0.1 - resolution: "@types/http-cache-semantics@npm:4.0.1" - checksum: 6d6068110a04cac213bdc0fff9c7bac028b5a2da390492204328987d8ddc500adc10d9cf5747a6333dab261712655dcfe120ea1d5527c205d012a39cdccc2a7b - languageName: node - linkType: hard - "@types/json-schema@npm:^7.0.9": version: 7.0.11 resolution: "@types/json-schema@npm:7.0.11" @@ -1306,15 +1265,6 @@ __metadata: languageName: node linkType: hard -"@types/keyv@npm:^3.1.4": - version: 3.1.4 - resolution: "@types/keyv@npm:3.1.4" - dependencies: - "@types/node": "*" - checksum: ff8f54fc49621210291f815fe5b15d809fd7d032941b3180743440bd507ecdf08b9e844625fa346af568c84bf34114eb378dcdc3e921a08ba1e2a08d7e3c809c - languageName: node - linkType: hard - "@types/lodash-es@npm:^4.17.7": version: 4.17.7 resolution: "@types/lodash-es@npm:4.17.7" @@ -1331,17 +1281,10 @@ __metadata: languageName: node linkType: hard -"@types/mime-types@npm:^2": - version: 2.1.1 - resolution: "@types/mime-types@npm:2.1.1" - checksum: 131b33bfd89481f6a791996db9198c6c5ffccbb310e990d1dd9fab7a2287b5a0fd642bdd959a19281397c86f721498e09956e3892e5db17f93f38e726ca05008 - languageName: node - linkType: hard - -"@types/node@npm:*, @types/node@npm:^18.11.11, @types/node@npm:^18.15.11": - version: 18.15.11 - resolution: "@types/node@npm:18.15.11" - checksum: 670deb1a9daa812dc86b1e8964c0c6b0bef7c32672833c10578c1e5dd2682f2bd99b86d814fde86a5dd4a3da48ea039f41db30a835b245aa7c34c62fa1f23f0d +"@types/node@npm:^20.2.3": + version: 20.2.3 + resolution: "@types/node@npm:20.2.3" + checksum: de79ce435f28354dd80f1203db2a5a0606543964400738007fe653d6248a75a9bbec7e7d9b350c475f3b0de514f4d2adbd642abe14f4a6dd40de92c0b8533b5e languageName: node linkType: hard @@ -1359,21 +1302,21 @@ __metadata: languageName: node linkType: hard -"@types/react-dom@npm:^18.0.11": - version: 18.0.11 - resolution: "@types/react-dom@npm:18.0.11" +"@types/react-dom@npm:^18.2.4": + version: 18.2.4 + resolution: "@types/react-dom@npm:18.2.4" dependencies: "@types/react": "*" - checksum: 8bf1e3f710221a937613df4d192f3b9e5a30e5c3103cac52c5210fb56b79f7a8cc66137d3bc5c9d92d375165a97fae53284724191bc01cb9898564fa02595569 + checksum: dfeaabb4268d39bdd5addc6c0b7099d5c57a364e70f1087b7c3ee189374312dc65201abfd3d87fee0de11d27c225678ce39c22d14b3035cde5792678704c27b5 languageName: node linkType: hard -"@types/react-is@npm:^16.7.1 || ^17.0.0": - version: 17.0.3 - resolution: "@types/react-is@npm:17.0.3" +"@types/react-is@npm:^18.2.0": + version: 18.2.0 + resolution: "@types/react-is@npm:18.2.0" dependencies: "@types/react": "*" - checksum: 839382b66b2b2e3023647f5ba0c382ddc6aa01c1bc9f64608f82bbc871a905ba9b988838619914d8348c2a511717c6bd3701cb866bb9e4abfabdbe544efb695b + checksum: 7bbc931874da3f41917416b9e44f2e0749c99d7c94a3803b96342890579aad2abca473bf7505fd2202cd61c84d6ed9da41f951eb19ccee860554682327087c96 languageName: node linkType: hard @@ -1398,32 +1341,34 @@ __metadata: languageName: node linkType: hard -"@types/react-transition-group@npm:^4.4.5": - version: 4.4.5 - resolution: "@types/react-transition-group@npm:4.4.5" +"@types/react-transition-group@npm:^4.4.6": + version: 4.4.6 + resolution: "@types/react-transition-group@npm:4.4.6" dependencies: "@types/react": "*" - checksum: c0d81634ca5e1efac3ca6f6f006245976d584833ab9e933edf08b66551c1c7b9f0bc7878897f57ba44b137d3754583d623c932fe4b7721840ae5218ec2414942 + checksum: 154dc4e94738cff0b2fa183331427c0de3d8daac44a9b79c27aa8a95b78adde44b9f70db8a374399eabe1d44ca50304b1d7bbaeadca0fbdf6f2a91f6f9eb343d languageName: node linkType: hard -"@types/react@npm:*, @types/react@npm:^18.0.33": - version: 18.0.33 - resolution: "@types/react@npm:18.0.33" +"@types/react@npm:*": + version: 18.2.0 + resolution: "@types/react@npm:18.2.0" dependencies: "@types/prop-types": "*" "@types/scheduler": "*" csstype: ^3.0.2 - checksum: 12610df107eeac48d63f23c64b9c2f91acf6413faa9868e374433b1bab7a27ce95b0a0198b0712da34e2a1672ce43e04fa0b484e81e985baae3b056e204e27ac + checksum: e38f98b7524817459bb1214d39f4cfcb1dd7ffb31992a427b4494f3988aa6195dc349dfb66b299270b399b34568d045bf1cb6230349a6d343e183052ee486eaa languageName: node linkType: hard -"@types/responselike@npm:^1.0.0": - version: 1.0.0 - resolution: "@types/responselike@npm:1.0.0" +"@types/react@npm:^18.2.7": + version: 18.2.7 + resolution: "@types/react@npm:18.2.7" dependencies: - "@types/node": "*" - checksum: 474ac2402e6d43c007eee25f50d01eb1f67255ca83dd8e036877292bbe8dd5d2d1e50b54b408e233b50a8c38e681ff3ebeaf22f18b478056eddb65536abb003a + "@types/prop-types": "*" + "@types/scheduler": "*" + csstype: ^3.0.2 + checksum: 5ec33ea64f3abc1da2a676809a16db2914465457154ecafc4f2db486e35d9e93fdfd661763396580eb489d7e131eaa86d8e58326719048bbcc2935f8ef0825fb languageName: node linkType: hard @@ -1434,39 +1379,21 @@ __metadata: languageName: node linkType: hard -"@types/semver@npm:^7.1.0, @types/semver@npm:^7.3.12": +"@types/semver@npm:^7.3.12": version: 7.3.13 resolution: "@types/semver@npm:7.3.13" checksum: 73295bb1fee46f8c76c7a759feeae5a3022f5bedfdc17d16982092e4b33af17560234fb94861560c20992a702a1e1b9a173bb623a96f95f80892105f5e7d25e3 languageName: node linkType: hard -"@types/styled-components@npm:^5": - version: 5.1.26 - resolution: "@types/styled-components@npm:5.1.26" - dependencies: - "@types/hoist-non-react-statics": "*" - "@types/react": "*" - csstype: ^3.0.2 - checksum: 61c53b035d82bbf6071d3f15348f2e9a43af8c28c630ab472d153277082a578aa60116ddc67bcfea1340d93577d5758c359f0a4d4d1291a419cebb3f8677b63e - languageName: node - linkType: hard - -"@types/treeify@npm:^1.0.0": - version: 1.0.0 - resolution: "@types/treeify@npm:1.0.0" - checksum: 8a279d0f1897e47cc02b4b5a570141ab70de6bc5d95cafe976aaee78740c13c2e80dae69f7ae9ca1c735c653b65a4ec59a7eed6970683cd04fc0ddf4b98794ff - languageName: node - linkType: hard - -"@typescript-eslint/eslint-plugin@npm:^5.57.1": - version: 5.57.1 - resolution: "@typescript-eslint/eslint-plugin@npm:5.57.1" +"@typescript-eslint/eslint-plugin@npm:^5.59.7": + version: 5.59.7 + resolution: "@typescript-eslint/eslint-plugin@npm:5.59.7" dependencies: "@eslint-community/regexpp": ^4.4.0 - "@typescript-eslint/scope-manager": 5.57.1 - "@typescript-eslint/type-utils": 5.57.1 - "@typescript-eslint/utils": 5.57.1 + "@typescript-eslint/scope-manager": 5.59.7 + "@typescript-eslint/type-utils": 5.59.7 + "@typescript-eslint/utils": 5.59.7 debug: ^4.3.4 grapheme-splitter: ^1.0.4 ignore: ^5.2.0 @@ -1479,43 +1406,43 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: 3847db76ed4a5df9cbb0f0155afa81951323a93ba37de3dca872b325502d0203da859a67f6d201bcfdf4985188b80227b7fd039206f7b921c96d33befe7ed25d + checksum: 78b583230a7a20e6a06bc9cd1c10ef6d334bdf8d0179a66b2f3d6ebb2b1ac5685ea5266443f8c833719ea62c2768f28a625fd64c5e83b9408a6d41aaffe35db0 languageName: node linkType: hard -"@typescript-eslint/parser@npm:^5.57.1": - version: 5.57.1 - resolution: "@typescript-eslint/parser@npm:5.57.1" +"@typescript-eslint/parser@npm:^5.59.7": + version: 5.59.7 + resolution: "@typescript-eslint/parser@npm:5.59.7" dependencies: - "@typescript-eslint/scope-manager": 5.57.1 - "@typescript-eslint/types": 5.57.1 - "@typescript-eslint/typescript-estree": 5.57.1 + "@typescript-eslint/scope-manager": 5.59.7 + "@typescript-eslint/types": 5.59.7 + "@typescript-eslint/typescript-estree": 5.59.7 debug: ^4.3.4 peerDependencies: eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 peerDependenciesMeta: typescript: optional: true - checksum: 4e2ea4694b261a25bca452db502666ddb4444cced9518eb2d34bd06d099885858307c9b320fd1aaeb45513811dc1984bbba370e5a8567671bad7fc5a0eb8bcc7 + checksum: c6248149be43fbea69f93cc1c56c17265c58639aa520b8afe242e7dac43712d1dd2bbc93fedd6a017b8799d68b1a51c8d802bc402184e9d2813bfc92a08bc204 languageName: node linkType: hard -"@typescript-eslint/scope-manager@npm:5.57.1": - version: 5.57.1 - resolution: "@typescript-eslint/scope-manager@npm:5.57.1" +"@typescript-eslint/scope-manager@npm:5.59.7": + version: 5.59.7 + resolution: "@typescript-eslint/scope-manager@npm:5.59.7" dependencies: - "@typescript-eslint/types": 5.57.1 - "@typescript-eslint/visitor-keys": 5.57.1 - checksum: f0905f70939980164c0205c6c6d3638bea40fd9afb1acc83632360e66725f185eee9da595721f0bcd57cc5f951224d9d47bd8dc9c9b2373920f9d236081a8344 + "@typescript-eslint/types": 5.59.7 + "@typescript-eslint/visitor-keys": 5.59.7 + checksum: e1cc89cf4a9b5fe3cc1b7921fb64cffe7e7179347a19c42e67c4b1ebbae88d7c997d1c3b6f24d11ef184ed35853115ec1d1c249da8b15c5dbffc5606e662b23a languageName: node linkType: hard -"@typescript-eslint/type-utils@npm:5.57.1": - version: 5.57.1 - resolution: "@typescript-eslint/type-utils@npm:5.57.1" +"@typescript-eslint/type-utils@npm:5.59.7": + version: 5.59.7 + resolution: "@typescript-eslint/type-utils@npm:5.59.7" dependencies: - "@typescript-eslint/typescript-estree": 5.57.1 - "@typescript-eslint/utils": 5.57.1 + "@typescript-eslint/typescript-estree": 5.59.7 + "@typescript-eslint/utils": 5.59.7 debug: ^4.3.4 tsutils: ^3.21.0 peerDependencies: @@ -1523,23 +1450,23 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: 21fb0653398d2d6d32e4bcb8fe4f4d802d63f0cd50f2e4b982f410b075f5441edffe64924dd1ba71f89eccef3b04eaae8c23543e7618723c7344914378ce3796 + checksum: 882cb0ea0c2d660b64d0c05ff28baf2504c04ad9a4e12ceb70f9679025397ac0709455f489662a1f92ae7fa5be535447c3d26979598ef1e23ceed4cdf8078ca5 languageName: node linkType: hard -"@typescript-eslint/types@npm:5.57.1": - version: 5.57.1 - resolution: "@typescript-eslint/types@npm:5.57.1" - checksum: 549214a87a52359013270982ef4e69ce17d8a063717a8cb9d127425e76b9113a8db437b67e6802be2ba35c4c025cb37907b003cc29306a8d7d7800c58838aa38 +"@typescript-eslint/types@npm:5.59.7": + version: 5.59.7 + resolution: "@typescript-eslint/types@npm:5.59.7" + checksum: 3bb11afe5eff16bde59fc7b0f479787fdc1764b9d836d4d32a587b058bb868057080b3ead7ef4d73feb17ac15765f2fbd74bad311263eea0ac3f29198a00b645 languageName: node linkType: hard -"@typescript-eslint/typescript-estree@npm:5.57.1": - version: 5.57.1 - resolution: "@typescript-eslint/typescript-estree@npm:5.57.1" +"@typescript-eslint/typescript-estree@npm:5.59.7": + version: 5.59.7 + resolution: "@typescript-eslint/typescript-estree@npm:5.59.7" dependencies: - "@typescript-eslint/types": 5.57.1 - "@typescript-eslint/visitor-keys": 5.57.1 + "@typescript-eslint/types": 5.59.7 + "@typescript-eslint/visitor-keys": 5.59.7 debug: ^4.3.4 globby: ^11.1.0 is-glob: ^4.0.3 @@ -1548,165 +1475,46 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: 3c5483a9bb8413ba117be12de334b9e533fe75320b5308e7d21a41661dd627b8a403575e5bce84ff9ca7b0678446dd6e98e73b2f05b9f4bb2d02a0a067b421d7 + checksum: 6b16d6fbe0e2111c9cb37c5bcd8834747f8ea2db0350df54e1aa13c1b09919efad8329b9a40c7bf7f4c8426fab50ce6ddf8e9f1731b8c93678e3af51c21102b5 languageName: node linkType: hard -"@typescript-eslint/utils@npm:5.57.1": - version: 5.57.1 - resolution: "@typescript-eslint/utils@npm:5.57.1" +"@typescript-eslint/utils@npm:5.59.7": + version: 5.59.7 + resolution: "@typescript-eslint/utils@npm:5.59.7" dependencies: "@eslint-community/eslint-utils": ^4.2.0 "@types/json-schema": ^7.0.9 "@types/semver": ^7.3.12 - "@typescript-eslint/scope-manager": 5.57.1 - "@typescript-eslint/types": 5.57.1 - "@typescript-eslint/typescript-estree": 5.57.1 + "@typescript-eslint/scope-manager": 5.59.7 + "@typescript-eslint/types": 5.59.7 + "@typescript-eslint/typescript-estree": 5.59.7 eslint-scope: ^5.1.1 semver: ^7.3.7 peerDependencies: eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 - checksum: bab8a94e30fc22fa6b3db5c6eee059697ce5e5e03f355c1fc465de184129b4824186a82235e14111c644d2b337721ae9f4d294a52a6f3dd4377edd35b922e3aa + checksum: d7d0c5a12ddefb5c9aaed100a8dfab181b74b04562f46e047904979fd4dfb9c76944cf88ffba5799a57c1af67e6906a3a1e1444f1fe119dda41f9cf233e995c4 languageName: node linkType: hard -"@typescript-eslint/visitor-keys@npm:5.57.1": - version: 5.57.1 - resolution: "@typescript-eslint/visitor-keys@npm:5.57.1" +"@typescript-eslint/visitor-keys@npm:5.59.7": + version: 5.59.7 + resolution: "@typescript-eslint/visitor-keys@npm:5.59.7" dependencies: - "@typescript-eslint/types": 5.57.1 + "@typescript-eslint/types": 5.59.7 eslint-visitor-keys: ^3.3.0 - checksum: 9a84589334fe675d18ca3a572aa0caf1b4c7baa80857642aee40378f5c79cf9dddd06a0fec31d7ff6a73f577f68f910fb745d51728fac317b388b8de764561db + checksum: 5cc1d7ddd8a1715de3398651d0ae183a9beafc2e49630cf5a40fc53afac60fae9b61631ee418686c211c8cc395d8d84dc182945659700fb846f06de106f5c114 languageName: node linkType: hard -"@vitejs/plugin-react-swc@npm:^3.2.0": - version: 3.2.0 - resolution: "@vitejs/plugin-react-swc@npm:3.2.0" +"@vitejs/plugin-react-swc@npm:^3.3.1": + version: 3.3.1 + resolution: "@vitejs/plugin-react-swc@npm:3.3.1" dependencies: - "@swc/core": ^1.3.35 + "@swc/core": ^1.3.56 peerDependencies: vite: ^4 - checksum: fa2e707ee67244596a8d8d2738875af904d0e30a108cb5788ada0815efebaf5001a3168b90093c2322502be36e87b131f22fb8eb7c5e351f6eb1893eb64299f5 - languageName: node - linkType: hard - -"@yarnpkg/core@npm:^4.0.0-rc.42": - version: 4.0.0-rc.42 - resolution: "@yarnpkg/core@npm:4.0.0-rc.42" - dependencies: - "@arcanis/slice-ansi": ^1.1.1 - "@types/semver": ^7.1.0 - "@types/treeify": ^1.0.0 - "@yarnpkg/fslib": ^3.0.0-rc.42 - "@yarnpkg/libzip": ^3.0.0-rc.42 - "@yarnpkg/parsers": ^3.0.0-rc.42 - "@yarnpkg/shell": ^4.0.0-rc.42 - camelcase: ^5.3.1 - chalk: ^3.0.0 - ci-info: ^3.2.0 - clipanion: ^3.2.0-rc.10 - cross-spawn: 7.0.3 - diff: ^5.1.0 - globby: ^11.0.1 - got: ^11.7.0 - lodash: ^4.17.15 - micromatch: ^4.0.2 - p-limit: ^2.2.0 - semver: ^7.1.2 - strip-ansi: ^6.0.0 - tar: ^6.0.5 - tinylogic: ^2.0.0 - treeify: ^1.1.0 - tslib: ^2.4.0 - tunnel: ^0.0.6 - checksum: ccd1cd5d22c7ab93a9f3b05937295547ea8aa7a86d47a506bf4fa89fde60231147075e5d70577de44765c05b1b4399214243663867c1505f627d460b3c9422c9 - languageName: node - linkType: hard - -"@yarnpkg/fslib@npm:^3.0.0-rc.42": - version: 3.0.0-rc.42 - resolution: "@yarnpkg/fslib@npm:3.0.0-rc.42" - dependencies: - tslib: ^2.4.0 - checksum: de0a5cff0c92e9c0201f8908c98468e4ee7a2144673b7a945a1966fc6cfc8b0680e6863d6403f7cc1a86a036e7c9d2422b9b3a53353333a08ec037df8049f77c - languageName: node - linkType: hard - -"@yarnpkg/libzip@npm:^3.0.0-rc.42": - version: 3.0.0-rc.42 - resolution: "@yarnpkg/libzip@npm:3.0.0-rc.42" - dependencies: - "@types/emscripten": ^1.39.6 - "@yarnpkg/fslib": ^3.0.0-rc.42 - tslib: ^2.4.0 - peerDependencies: - "@yarnpkg/fslib": ^3.0.0-rc.42 - checksum: 9c6fd61b1e9fc51bf71ed96e854ac4ae10def0902a810a091d079b52fca2f20e7318f478178c5c09b39a44d575024eed7b977776ae5b70ed01a0566a66acb7c5 - languageName: node - linkType: hard - -"@yarnpkg/nm@npm:^4.0.0-rc.42": - version: 4.0.0-rc.42 - resolution: "@yarnpkg/nm@npm:4.0.0-rc.42" - dependencies: - "@yarnpkg/core": ^4.0.0-rc.42 - "@yarnpkg/fslib": ^3.0.0-rc.42 - "@yarnpkg/pnp": ^4.0.0-rc.42 - checksum: 976f25ae10528333f0fb096979d674e78778e95d6f75722b19f0872010316f955747a234363f03d80d0d82ffbd69c7549d03f260bc59d05aadcd2bd5529a065b - languageName: node - linkType: hard - -"@yarnpkg/parsers@npm:^3.0.0-rc.42": - version: 3.0.0-rc.42 - resolution: "@yarnpkg/parsers@npm:3.0.0-rc.42" - dependencies: - js-yaml: ^3.10.0 - tslib: ^2.4.0 - checksum: 31ffaecb01c903331063168e58cf9d6358bee4baecbc53a04c75a16d9403e477a9ac02c18924acfed54cb6164cd38a9858434d925baf0f4ef2b3d3e9672b4353 - languageName: node - linkType: hard - -"@yarnpkg/pnp@npm:^4.0.0-rc.42": - version: 4.0.0-rc.42 - resolution: "@yarnpkg/pnp@npm:4.0.0-rc.42" - dependencies: - "@types/node": ^18.11.11 - "@yarnpkg/fslib": ^3.0.0-rc.42 - checksum: 48d3946ae21fba474fad093d82c92699cb30f4457779607223054da049d66446f40c2e8008dcc3d0a5522a29e5c97de821b8258a75b0ca0e93d713892fc93642 - languageName: node - linkType: hard - -"@yarnpkg/pnpify@npm:^4.0.0-rc.42": - version: 4.0.0-rc.42 - resolution: "@yarnpkg/pnpify@npm:4.0.0-rc.42" - dependencies: - "@yarnpkg/core": ^4.0.0-rc.42 - "@yarnpkg/fslib": ^3.0.0-rc.42 - "@yarnpkg/nm": ^4.0.0-rc.42 - clipanion: ^3.2.0-rc.10 - tslib: ^2.4.0 - bin: - pnpify: ./lib/cli.js - checksum: 8f9b6495122eb337fbe602fcc124f0a4560723f831e180e8149658117bcbe6c957fe7169237b72e9caa686e41e3678c34015b51e7643618f1261618dc73409e6 - languageName: node - linkType: hard - -"@yarnpkg/shell@npm:^4.0.0-rc.42": - version: 4.0.0-rc.42 - resolution: "@yarnpkg/shell@npm:4.0.0-rc.42" - dependencies: - "@yarnpkg/fslib": ^3.0.0-rc.42 - "@yarnpkg/parsers": ^3.0.0-rc.42 - chalk: ^3.0.0 - clipanion: ^3.2.0-rc.10 - cross-spawn: 7.0.3 - fast-glob: ^3.2.2 - micromatch: ^4.0.2 - tslib: ^2.4.0 - bin: - shell: ./lib/cli.js - checksum: def47161092a303222398dd85f4f8fc651aa529875d2176349e136e22a30e84b05a97a45a90ed09b0e605b4d0f4980b163a67008571f4f6517f648ebaf1f49ff + checksum: 72ab0a72d41c949009a2f71836894fb0003939329a2d1bb59b1181b03d21fda5002ccd20b40b48ddc8f12511cc8717122141f49ac51e97263df3c3f3142ae937 languageName: node linkType: hard @@ -1714,31 +1522,27 @@ __metadata: version: 0.0.0-use.local resolution: "EMS-ESP@workspace:." dependencies: - "@emotion/react": ^11.10.6 - "@emotion/styled": ^11.10.6 - "@msgpack/msgpack": ^3.0.0-beta2 + "@emotion/react": ^11.11.0 + "@emotion/styled": ^11.11.0 "@mui/icons-material": ^5.11.16 - "@mui/material": ^5.11.16 - "@remix-run/router": ^1.5.0 - "@table-library/react-table-library": 4.1.0 + "@mui/material": ^5.13.2 + "@table-library/react-table-library": 4.1.4 "@types/lodash-es": ^4.17.7 - "@types/mime-types": ^2 - "@types/node": ^18.15.11 - "@types/react": ^18.0.33 - "@types/react-dom": ^18.0.11 + "@types/node": ^20.2.3 + "@types/react": ^18.2.7 + "@types/react-dom": ^18.2.4 "@types/react-router-dom": ^5.3.3 - "@types/styled-components": ^5 - "@typescript-eslint/eslint-plugin": ^5.57.1 - "@typescript-eslint/parser": ^5.57.1 - "@vitejs/plugin-react-swc": ^3.2.0 - "@yarnpkg/pnpify": ^4.0.0-rc.42 + "@typescript-eslint/eslint-plugin": ^5.59.7 + "@typescript-eslint/parser": ^5.59.7 + "@vitejs/plugin-react-swc": ^3.3.1 async-validator: ^4.2.5 - axios: ^1.3.5 - eslint: ^8.37.0 + axios: ^1.4.0 + eslint: ^8.41.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 @@ -1747,25 +1551,23 @@ __metadata: history: ^5.3.0 jwt-decode: ^3.1.2 lodash-es: ^4.17.21 - mime-types: ^2.1.35 nodemon: ^2.0.22 npm-run-all: ^4.1.5 - prettier: ^2.8.7 + prettier: ^2.8.8 react: latest react-dom: latest react-dropzone: ^14.2.3 react-icons: ^4.8.0 - react-router-dom: ^6.10.0 - react-toastify: ^9.1.2 + react-router-dom: ^6.11.2 + react-toastify: ^9.1.3 rollup-plugin-visualizer: ^5.9.0 sockette: ^2.0.6 - terser: ^5.16.8 + terser: ^5.17.6 typesafe-i18n: ^5.24.3 - typescript: ^5.0.3 - vite: ^4.2.1 - vite-plugin-minify: ^1.5.2 - vite-plugin-svgr: ^2.4.0 - vite-tsconfig-paths: ^4.0.8 + typescript: ^5.0.4 + vite: ^4.3.8 + vite-plugin-svgr: ^3.2.0 + vite-tsconfig-paths: ^4.2.0 languageName: unknown linkType: soft @@ -1888,15 +1690,6 @@ __metadata: languageName: node linkType: hard -"argparse@npm:^1.0.7": - version: 1.0.10 - resolution: "argparse@npm:1.0.10" - dependencies: - sprintf-js: ~1.0.2 - checksum: b2972c5c23c63df66bca144dbc65d180efa74f25f8fd9b7d9a0a6c88ae839db32df3d54770dcb6460cf840d232b60695d1a6b1053f599d84e73f7437087712de - languageName: node - linkType: hard - "argparse@npm:^2.0.1": version: 2.0.1 resolution: "argparse@npm:2.0.1" @@ -2022,14 +1815,14 @@ __metadata: languageName: node linkType: hard -"axios@npm:^1.3.5": - version: 1.3.5 - resolution: "axios@npm:1.3.5" +"axios@npm:^1.4.0": + version: 1.4.0 + resolution: "axios@npm:1.4.0" dependencies: follow-redirects: ^1.15.0 form-data: ^4.0.0 proxy-from-env: ^1.1.0 - checksum: 74664fbf617fa6c10a2739afa356a16f4182ab41701315a9e475fa1c6f6c1e1c975361eabd8c113156c2eff263cb0eb6da7c0b395fa2aa181971196aed51d2ee + checksum: a925a07590b0ec1d4daf28cd27890f930daab980371558deb3b883af174b881da09e5ba2cb8393a648fda5859e39934982d0b8b092fe89fc84cb6c80a70a1910 languageName: node linkType: hard @@ -2142,28 +1935,6 @@ __metadata: languageName: node linkType: hard -"cacheable-lookup@npm:^5.0.3": - version: 5.0.4 - resolution: "cacheable-lookup@npm:5.0.4" - checksum: a6547fb4954b318aa831cbdd2f7b376824bc784fb1fa67610e4147099e3074726072d9af89f12efb69121415a0e1f2918a8ddd4aafcbcf4e91fbeef4a59cd42c - languageName: node - linkType: hard - -"cacheable-request@npm:^7.0.2": - version: 7.0.2 - resolution: "cacheable-request@npm:7.0.2" - dependencies: - clone-response: ^1.0.2 - get-stream: ^5.1.0 - http-cache-semantics: ^4.0.0 - keyv: ^4.0.0 - lowercase-keys: ^2.0.0 - normalize-url: ^6.0.1 - responselike: ^2.0.0 - checksum: 681bad13691d0d5d10652d409374747a2ce8676f854b0d454ee8fc65e0a10a52ea83cd1f6c367ada08572fd4982f2aa2582dc38983d4e958e053e181c433765e - languageName: node - linkType: hard - "call-bind@npm:^1.0.0, call-bind@npm:^1.0.2": version: 1.0.2 resolution: "call-bind@npm:1.0.2" @@ -2181,23 +1952,6 @@ __metadata: languageName: node linkType: hard -"camel-case@npm:^4.1.2": - version: 4.1.2 - resolution: "camel-case@npm:4.1.2" - dependencies: - pascal-case: ^3.1.2 - tslib: ^2.0.3 - checksum: bf9eefaee1f20edbed2e9a442a226793bc72336e2b99e5e48c6b7252b6f70b080fc46d8246ab91939e2af91c36cdd422e0af35161e58dd089590f302f8f64c8a - languageName: node - linkType: hard - -"camelcase@npm:^5.3.1": - version: 5.3.1 - resolution: "camelcase@npm:5.3.1" - checksum: 92ff9b443bfe8abb15f2b1513ca182d16126359ad4f955ebc83dc4ddcc4ef3fdd2c078bc223f2673dc223488e75c99b16cc4d056624374b799e6a1555cf61b23 - languageName: node - linkType: hard - "camelcase@npm:^6.2.0": version: 6.3.0 resolution: "camelcase@npm:6.3.0" @@ -2223,16 +1977,6 @@ __metadata: languageName: node linkType: hard -"chalk@npm:^3.0.0": - version: 3.0.0 - resolution: "chalk@npm:3.0.0" - dependencies: - ansi-styles: ^4.1.0 - supports-color: ^7.1.0 - checksum: ee650b0a065b3d7a6fda258e75d3a86fc8e4effa55871da730a9e42ccb035bf5fd203525e5a1ef45ec2582ecc4f65b47eb11357c526b84dd29a14fb162c414d2 - languageName: node - linkType: hard - "chalk@npm:^4.0.0": version: 4.1.2 resolution: "chalk@npm:4.1.2" @@ -2269,22 +2013,6 @@ __metadata: languageName: node linkType: hard -"ci-info@npm:^3.2.0": - version: 3.8.0 - resolution: "ci-info@npm:3.8.0" - checksum: 0d3052193b58356372b34ab40d2668c3e62f1006d5ca33726d1d3c423853b19a85508eadde7f5908496fb41448f465263bf61c1ee58b7832cb6a924537e3863a - languageName: node - linkType: hard - -"clean-css@npm:^5.2.2": - version: 5.3.2 - resolution: "clean-css@npm:5.3.2" - dependencies: - source-map: ~0.6.0 - checksum: 315e0e81306524bd2c1905fa6823bf7658be40799b78f446e5e6922808718d2b80266fb3e96842a06176fa683bc2c1a0d2827b08d154e2f9cf136d7bda909d33 - languageName: node - linkType: hard - "clean-stack@npm:^2.0.0": version: 2.2.0 resolution: "clean-stack@npm:2.2.0" @@ -2292,17 +2020,6 @@ __metadata: languageName: node linkType: hard -"clipanion@npm:^3.2.0-rc.10": - version: 3.2.0 - resolution: "clipanion@npm:3.2.0" - dependencies: - typanion: ^3.8.0 - peerDependencies: - typanion: "*" - checksum: fcc3d44145bc0e162ccf856c27437c0770ba1be863d73296d54c352621accd7675bca33f0a1bed1b1cd7dd22c695abd35e4839da85f12fc1edb77a3f8aa100f4 - languageName: node - linkType: hard - "cliui@npm:^8.0.1": version: 8.0.1 resolution: "cliui@npm:8.0.1" @@ -2314,15 +2031,6 @@ __metadata: languageName: node linkType: hard -"clone-response@npm:^1.0.2": - version: 1.0.3 - resolution: "clone-response@npm:1.0.3" - dependencies: - mimic-response: ^1.0.0 - checksum: 06a2b611824efb128810708baee3bd169ec9a1bf5976a5258cd7eb3f7db25f00166c6eee5961f075c7e38e194f373d4fdf86b8166ad5b9c7e82bbd2e333a6087 - languageName: node - linkType: hard - "clsx@npm:1.1.1": version: 1.1.1 resolution: "clsx@npm:1.1.1" @@ -2394,13 +2102,6 @@ __metadata: languageName: node linkType: hard -"commander@npm:^8.3.0": - version: 8.3.0 - resolution: "commander@npm:8.3.0" - checksum: 8b043bb8322ea1c39664a1598a95e0495bfe4ca2fad0d84a92d7d1d8d213e2a155b441d2470c8e08de7c4a28cf2bc6e169211c49e1b21d9f7edc6ae4d9356060 - languageName: node - linkType: hard - "concat-map@npm:0.0.1": version: 0.0.1 resolution: "concat-map@npm:0.0.1" @@ -2429,7 +2130,7 @@ __metadata: languageName: node linkType: hard -"cosmiconfig@npm:^7.0.0, cosmiconfig@npm:^7.0.1": +"cosmiconfig@npm:^7.0.0": version: 7.1.0 resolution: "cosmiconfig@npm:7.1.0" dependencies: @@ -2442,14 +2143,15 @@ __metadata: languageName: node linkType: hard -"cross-spawn@npm:7.0.3, cross-spawn@npm:^7.0.2, cross-spawn@npm:^7.0.3": - version: 7.0.3 - resolution: "cross-spawn@npm:7.0.3" +"cosmiconfig@npm:^8.1.3": + version: 8.1.3 + resolution: "cosmiconfig@npm:8.1.3" dependencies: - path-key: ^3.1.0 - shebang-command: ^2.0.0 - which: ^2.0.1 - checksum: 5738c312387081c98d69c98e105b6327b069197f864a60593245d64c8089c8a0a744e16349281210d56835bb9274130d825a78b2ad6853ca13cfbeffc0c31750 + import-fresh: ^3.2.1 + js-yaml: ^4.1.0 + parse-json: ^5.0.0 + path-type: ^4.0.0 + checksum: 80144be230b89857e7c4cafd59ba8feb3f5f7e6dae90faa324629fdecf9a6fc3f5b4106c3623f69a1a3d77cb11ef90e5ab65a67f21d73ffda3d76b18f8e4e6c2 languageName: node linkType: hard @@ -2466,6 +2168,17 @@ __metadata: languageName: node linkType: hard +"cross-spawn@npm:^7.0.2, cross-spawn@npm:^7.0.3": + version: 7.0.3 + resolution: "cross-spawn@npm:7.0.3" + dependencies: + path-key: ^3.1.0 + shebang-command: ^2.0.0 + which: ^2.0.1 + checksum: 5738c312387081c98d69c98e105b6327b069197f864a60593245d64c8089c8a0a744e16349281210d56835bb9274130d825a78b2ad6853ca13cfbeffc0c31750 + languageName: node + linkType: hard + "csstype@npm:^3.0.2, csstype@npm:^3.1.2": version: 3.1.2 resolution: "csstype@npm:3.1.2" @@ -2501,15 +2214,6 @@ __metadata: languageName: node linkType: hard -"decompress-response@npm:^6.0.0": - version: 6.0.0 - resolution: "decompress-response@npm:6.0.0" - dependencies: - mimic-response: ^3.1.0 - checksum: bd89d23141b96d80577e70c54fb226b2f40e74a6817652b80a116d7befb8758261ad073a8895648a29cc0a5947021ab66705cb542fa9c143c82022b27c5b175e - languageName: node - linkType: hard - "deep-equal@npm:^2.0.5": version: 2.2.0 resolution: "deep-equal@npm:2.2.0" @@ -2542,13 +2246,6 @@ __metadata: languageName: node linkType: hard -"defer-to-connect@npm:^2.0.0": - version: 2.0.1 - resolution: "defer-to-connect@npm:2.0.1" - checksum: 625ce28e1b5ad10cf77057b9a6a727bf84780c17660f6644dab61dd34c23de3001f03cedc401f7d30a4ed9965c2e8a7336e220a329146f2cf85d4eddea429782 - languageName: node - linkType: hard - "define-lazy-prop@npm:^2.0.0": version: 2.0.0 resolution: "define-lazy-prop@npm:2.0.0" @@ -2587,13 +2284,6 @@ __metadata: languageName: node linkType: hard -"diff@npm:^5.1.0": - version: 5.1.0 - resolution: "diff@npm:5.1.0" - checksum: 77a0d9beb9ed54796154ac2511872288432124ac90a1cabb1878783c9b4d81f1847f3b746a0630b1e836181461d2c76e1e6b95559bef86ed16294d114862e364 - languageName: node - linkType: hard - "dir-glob@npm:^3.0.1": version: 3.0.1 resolution: "dir-glob@npm:3.0.1" @@ -2631,16 +2321,6 @@ __metadata: languageName: node linkType: hard -"dot-case@npm:^3.0.4": - version: 3.0.4 - resolution: "dot-case@npm:3.0.4" - dependencies: - no-case: ^3.0.4 - tslib: ^2.0.3 - checksum: 5b859ea65097a7ea870e2c91b5768b72ddf7fa947223fd29e167bcdff58fe731d941c48e47a38ec8aa8e43044c8fbd15cd8fa21689a526bc34b6548197cd5b05 - languageName: node - linkType: hard - "electron-to-chromium@npm:^1.4.284": version: 1.4.354 resolution: "electron-to-chromium@npm:1.4.354" @@ -2671,15 +2351,6 @@ __metadata: languageName: node linkType: hard -"end-of-stream@npm:^1.1.0": - version: 1.4.4 - resolution: "end-of-stream@npm:1.4.4" - dependencies: - once: ^1.4.0 - checksum: 870b423afb2d54bb8d243c63e07c170409d41e20b47eeef0727547aea5740bd6717aca45597a9f2745525667a6b804c1e7bede41f856818faee5806dd9ff3975 - languageName: node - linkType: hard - "enhanced-resolve@npm:^5.12.0": version: 5.12.0 resolution: "enhanced-resolve@npm:5.12.0" @@ -3007,6 +2678,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" @@ -3107,6 +2793,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" @@ -3117,13 +2810,13 @@ __metadata: languageName: node linkType: hard -"eslint-scope@npm:^7.1.1": - version: 7.1.1 - resolution: "eslint-scope@npm:7.1.1" +"eslint-scope@npm:^7.2.0": + version: 7.2.0 + resolution: "eslint-scope@npm:7.2.0" dependencies: esrecurse: ^4.3.0 estraverse: ^5.2.0 - checksum: 3ae3280cbea34af3b816e941b83888aca063aaa0169966ff7e4c1bfb0715dbbeac3811596e56315e8ceea84007a7403754459ae4f1d19f25487eb02acd951aa7 + checksum: 5b48a3cc2485a3a58ca0bdecfb557c349009308a9b2afb24d070b1c0c254d445ee86d78bfee2c4ed6d1b8944307604a987c92f6d7e611e29de5d06256747a0ff languageName: node linkType: hard @@ -3134,14 +2827,21 @@ __metadata: languageName: node linkType: hard -"eslint@npm:^8.37.0": - version: 8.37.0 - resolution: "eslint@npm:8.37.0" +"eslint-visitor-keys@npm:^3.4.1": + version: 3.4.1 + resolution: "eslint-visitor-keys@npm:3.4.1" + checksum: b4ebd35aed5426cd81b1fb92487825f1acf47a31e91d76597a3ee0664d69627140c4dafaf9b319cfeb1f48c1113a393e21a734c669e6565a72e6fcc311bd9911 + languageName: node + linkType: hard + +"eslint@npm:^8.41.0": + version: 8.41.0 + resolution: "eslint@npm:8.41.0" dependencies: "@eslint-community/eslint-utils": ^4.2.0 "@eslint-community/regexpp": ^4.4.0 - "@eslint/eslintrc": ^2.0.2 - "@eslint/js": 8.37.0 + "@eslint/eslintrc": ^2.0.3 + "@eslint/js": 8.41.0 "@humanwhocodes/config-array": ^0.11.8 "@humanwhocodes/module-importer": ^1.0.1 "@nodelib/fs.walk": ^1.2.8 @@ -3151,9 +2851,9 @@ __metadata: debug: ^4.3.2 doctrine: ^3.0.0 escape-string-regexp: ^4.0.0 - eslint-scope: ^7.1.1 - eslint-visitor-keys: ^3.4.0 - espree: ^9.5.1 + eslint-scope: ^7.2.0 + eslint-visitor-keys: ^3.4.1 + espree: ^9.5.2 esquery: ^1.4.2 esutils: ^2.0.2 fast-deep-equal: ^3.1.3 @@ -3161,13 +2861,12 @@ __metadata: find-up: ^5.0.0 glob-parent: ^6.0.2 globals: ^13.19.0 - grapheme-splitter: ^1.0.4 + graphemer: ^1.4.0 ignore: ^5.2.0 import-fresh: ^3.0.0 imurmurhash: ^0.1.4 is-glob: ^4.0.0 is-path-inside: ^3.0.3 - js-sdsl: ^4.1.4 js-yaml: ^4.1.0 json-stable-stringify-without-jsonify: ^1.0.1 levn: ^0.4.1 @@ -3180,11 +2879,11 @@ __metadata: text-table: ^0.2.0 bin: eslint: bin/eslint.js - checksum: 3798781652b1c32d8a4a654c2dd7f2abbff808a806ddd7fd8f86a49586a824854369943d49ee52dcda47b8b03cd49585820f2ab69fc61bb17a0d7677785a2cf7 + checksum: d71832b235bebaa67c09019dee32bf8393c5e12155e91131c4606670eb9836fbff31e11364408258cb75e2bde8a4dfa0c042aa6145bb23cd800a42e63ca4a035 languageName: node linkType: hard -"espree@npm:^9.5.1": +"espree@npm:^9.0.0": version: 9.5.1 resolution: "espree@npm:9.5.1" dependencies: @@ -3195,13 +2894,14 @@ __metadata: languageName: node linkType: hard -"esprima@npm:^4.0.0": - version: 4.0.1 - resolution: "esprima@npm:4.0.1" - bin: - esparse: ./bin/esparse.js - esvalidate: ./bin/esvalidate.js - checksum: ad4bab9ead0808cf56501750fd9d3fb276f6b105f987707d059005d57e182d18a7c9ec7f3a01794ebddcca676773e42ca48a32d67a250c9d35e009ca613caba3 +"espree@npm:^9.5.2": + version: 9.5.2 + resolution: "espree@npm:9.5.2" + dependencies: + acorn: ^8.8.0 + acorn-jsx: ^5.3.2 + eslint-visitor-keys: ^3.4.1 + checksum: 6885e57b3dcea3f65211403a10077a80a57f03e6c8b45c1e1db4bc5958e0fdb5c358819c88898ea67655d25fb646836182f688156e124a05610e714d3fcb6822 languageName: node linkType: hard @@ -3265,7 +2965,7 @@ __metadata: languageName: node linkType: hard -"fast-glob@npm:^3.2.11, fast-glob@npm:^3.2.2, fast-glob@npm:^3.2.9": +"fast-glob@npm:^3.2.11, fast-glob@npm:^3.2.9": version: 3.2.12 resolution: "fast-glob@npm:3.2.12" dependencies: @@ -3494,15 +3194,6 @@ __metadata: languageName: node linkType: hard -"get-stream@npm:^5.1.0": - version: 5.2.0 - resolution: "get-stream@npm:5.2.0" - dependencies: - pump: ^3.0.0 - checksum: 43797ffd815fbb26685bf188c8cfebecb8af87b3925091dd7b9a9c915993293d78e3c9e1bce125928ff92f2d0796f3889b92b5ec6d58d1041b574682132e0a80 - languageName: node - linkType: hard - "get-symbol-description@npm:^1.0.0": version: 1.0.0 resolution: "get-symbol-description@npm:1.0.0" @@ -3597,7 +3288,7 @@ __metadata: languageName: node linkType: hard -"globby@npm:^11.0.1, globby@npm:^11.1.0": +"globby@npm:^11.1.0": version: 11.1.0 resolution: "globby@npm:11.1.0" dependencies: @@ -3640,25 +3331,6 @@ __metadata: languageName: node linkType: hard -"got@npm:^11.7.0": - version: 11.8.6 - resolution: "got@npm:11.8.6" - dependencies: - "@sindresorhus/is": ^4.0.0 - "@szmarczak/http-timer": ^4.0.5 - "@types/cacheable-request": ^6.0.1 - "@types/responselike": ^1.0.0 - cacheable-lookup: ^5.0.3 - cacheable-request: ^7.0.2 - decompress-response: ^6.0.0 - http2-wrapper: ^1.0.0-beta.5.2 - lowercase-keys: ^2.0.0 - p-cancelable: ^2.0.0 - responselike: ^2.0.0 - checksum: 754dd44877e5cf6183f1e989ff01c648d9a4719e357457bd4c78943911168881f1cfb7b2cb15d885e2105b3ad313adb8f017a67265dd7ade771afdb261ee8cb1 - languageName: node - linkType: hard - "graceful-fs@npm:^4.1.2, graceful-fs@npm:^4.2.4, graceful-fs@npm:^4.2.6": version: 4.2.11 resolution: "graceful-fs@npm:4.2.11" @@ -3673,6 +3345,13 @@ __metadata: languageName: node linkType: hard +"graphemer@npm:^1.4.0": + version: 1.4.0 + resolution: "graphemer@npm:1.4.0" + checksum: e951259d8cd2e0d196c72ec711add7115d42eb9a8146c8eeda5b8d3ac91e5dd816b9cd68920726d9fd4490368e7ed86e9c423f40db87e2d8dfafa00fa17c3a31 + languageName: node + linkType: hard + "has-bigints@npm:^1.0.1, has-bigints@npm:^1.0.2": version: 1.0.2 resolution: "has-bigints@npm:1.0.2" @@ -3742,15 +3421,6 @@ __metadata: languageName: node linkType: hard -"he@npm:^1.2.0": - version: 1.2.0 - resolution: "he@npm:1.2.0" - bin: - he: bin/he - checksum: a27d478befe3c8192f006cdd0639a66798979dfa6e2125c6ac582a19a5ebfec62ad83e8382e6036170d873f46e4536a7e795bf8b95bf7c247f4cc0825ccc8c17 - languageName: node - linkType: hard - "history@npm:^5.3.0": version: 5.3.0 resolution: "history@npm:5.3.0" @@ -3760,7 +3430,7 @@ __metadata: languageName: node linkType: hard -"hoist-non-react-statics@npm:^3.3.0, hoist-non-react-statics@npm:^3.3.1": +"hoist-non-react-statics@npm:^3.3.1": version: 3.3.2 resolution: "hoist-non-react-statics@npm:3.3.2" dependencies: @@ -3776,24 +3446,7 @@ __metadata: languageName: node linkType: hard -"html-minifier-terser@npm:^6.1.0": - version: 6.1.0 - resolution: "html-minifier-terser@npm:6.1.0" - dependencies: - camel-case: ^4.1.2 - clean-css: ^5.2.2 - commander: ^8.3.0 - he: ^1.2.0 - param-case: ^3.0.4 - relateurl: ^0.2.7 - terser: ^5.10.0 - bin: - html-minifier-terser: cli.js - checksum: 1aa4e4f01cf7149e3ac5ea84fb7a1adab86da40d38d77a6fff42852b5ee3daccb78b615df97264e3a6a5c33e57f0c77f471d607ca1e1debd1dab9b58286f4b5a - languageName: node - linkType: hard - -"http-cache-semantics@npm:^4.0.0, http-cache-semantics@npm:^4.1.0": +"http-cache-semantics@npm:^4.1.0": version: 4.1.1 resolution: "http-cache-semantics@npm:4.1.1" checksum: ce1319b8a382eb3cbb4a37c19f6bfe14e5bb5be3d09079e885e8c513ab2d3cd9214902f8a31c9dc4e37022633ceabfc2d697405deeaf1b8f3552bb4ed996fdfc @@ -3811,16 +3464,6 @@ __metadata: languageName: node linkType: hard -"http2-wrapper@npm:^1.0.0-beta.5.2": - version: 1.0.3 - resolution: "http2-wrapper@npm:1.0.3" - dependencies: - quick-lru: ^5.1.1 - resolve-alpn: ^1.0.0 - checksum: 6a9b72a033e9812e1476b9d776ce2f387bc94bc46c88aea0d5dab6bd47d0a539b8178830e77054dd26d1142c866d515a28a4dc7c3ff4232c88ff2ebe4f5d12d1 - languageName: node - linkType: hard - "https-proxy-agent@npm:^5.0.0": version: 5.0.1 resolution: "https-proxy-agent@npm:5.0.1" @@ -4192,13 +3835,6 @@ __metadata: languageName: node linkType: hard -"js-sdsl@npm:^4.1.4": - version: 4.4.0 - resolution: "js-sdsl@npm:4.4.0" - checksum: 1eabe718867d293771074b5a14a82a115727b3d4abc9524fb9b0cb74293f447b90fe27bb74eb712b6400aeb7b869631c0a67d3347670cf22d067e77caeeb2f33 - languageName: node - linkType: hard - "js-tokens@npm:^3.0.0 || ^4.0.0, js-tokens@npm:^4.0.0": version: 4.0.0 resolution: "js-tokens@npm:4.0.0" @@ -4206,18 +3842,6 @@ __metadata: languageName: node linkType: hard -"js-yaml@npm:^3.10.0": - version: 3.14.1 - resolution: "js-yaml@npm:3.14.1" - dependencies: - argparse: ^1.0.7 - esprima: ^4.0.0 - bin: - js-yaml: bin/js-yaml.js - checksum: 6746baaaeac312c4db8e75fa22331d9a04cccb7792d126ed8ce6a0bbcfef0cedaddd0c5098fade53db067c09fe00aa1c957674b4765610a8b06a5a189e46433b - languageName: node - linkType: hard - "js-yaml@npm:^4.1.0": version: 4.1.0 resolution: "js-yaml@npm:4.1.0" @@ -4238,13 +3862,6 @@ __metadata: languageName: node linkType: hard -"json-buffer@npm:3.0.1": - version: 3.0.1 - resolution: "json-buffer@npm:3.0.1" - checksum: 0d1c91569d9588e7eef2b49b59851f297f3ab93c7b35c7c221e288099322be6b562767d11e4821da500f3219542b9afd2e54c5dc573107c1126ed1080f8e96d7 - languageName: node - linkType: hard - "json-parse-better-errors@npm:^1.0.1": version: 1.0.2 resolution: "json-parse-better-errors@npm:1.0.2" @@ -4310,15 +3927,6 @@ __metadata: languageName: node linkType: hard -"keyv@npm:^4.0.0": - version: 4.5.2 - resolution: "keyv@npm:4.5.2" - dependencies: - json-buffer: 3.0.1 - checksum: b633bf53a5afa5591f383d326746226e110e59f13c7e1e8d3e3c9580d2c2345c5eefc21cce168cd5be7fa34b9163e391927146fbd2b7ee7aa2f3aa02b7f0a7de - languageName: node - linkType: hard - "language-subtag-registry@npm:~0.3.2": version: 0.3.22 resolution: "language-subtag-registry@npm:0.3.22" @@ -4387,7 +3995,7 @@ __metadata: languageName: node linkType: hard -"lodash@npm:^4.17.15": +"lodash@npm:^4.17.20": version: 4.17.21 resolution: "lodash@npm:4.17.21" checksum: d8cbea072bb08655bb4c989da418994b073a608dffa608b09ac04b43a791b12aeae7cd7ad919aa4c925f33b48490b5cfe6c1f71d827956071dae2e7bb3a6b74c @@ -4405,22 +4013,6 @@ __metadata: languageName: node linkType: hard -"lower-case@npm:^2.0.2": - version: 2.0.2 - resolution: "lower-case@npm:2.0.2" - dependencies: - tslib: ^2.0.3 - checksum: 3d925e090315cf7dc1caa358e0477e186ffa23947740e4314a7429b6e62d72742e0bbe7536a5ae56d19d7618ce998aba05caca53c2902bd5742fdca5fc57fd7b - languageName: node - linkType: hard - -"lowercase-keys@npm:^2.0.0": - version: 2.0.0 - resolution: "lowercase-keys@npm:2.0.0" - checksum: f82a2b3568910509da4b7906362efa40f5b54ea14c2584778ddb313226f9cbf21020a5db35f9b9a0e95847a9b781d548601f31793d736b22a2b8ae8eb9ab1082 - languageName: node - linkType: hard - "lru-cache@npm:^5.1.1": version: 5.1.1 resolution: "lru-cache@npm:5.1.1" @@ -4491,7 +4083,7 @@ __metadata: languageName: node linkType: hard -"micromatch@npm:^4.0.2, micromatch@npm:^4.0.4": +"micromatch@npm:^4.0.4": version: 4.0.5 resolution: "micromatch@npm:4.0.5" dependencies: @@ -4508,7 +4100,7 @@ __metadata: languageName: node linkType: hard -"mime-types@npm:^2.1.12, mime-types@npm:^2.1.35": +"mime-types@npm:^2.1.12": version: 2.1.35 resolution: "mime-types@npm:2.1.35" dependencies: @@ -4517,20 +4109,6 @@ __metadata: languageName: node linkType: hard -"mimic-response@npm:^1.0.0": - version: 1.0.1 - resolution: "mimic-response@npm:1.0.1" - checksum: c5381a5eae997f1c3b5e90ca7f209ed58c3615caeee850e85329c598f0c000ae7bec40196580eef1781c60c709f47258131dab237cad8786f8f56750594f27fa - languageName: node - linkType: hard - -"mimic-response@npm:^3.1.0": - version: 3.1.0 - resolution: "mimic-response@npm:3.1.0" - checksum: 0d6f07ce6e03e9e4445bee655202153bdb8a98d67ee8dc965ac140900d7a2688343e6b4c9a72cfc9ef2f7944dfd76eef4ab2482eb7b293a68b84916bac735362 - languageName: node - linkType: hard - "minimatch@npm:^3.0.4, minimatch@npm:^3.0.5, minimatch@npm:^3.1.1, minimatch@npm:^3.1.2": version: 3.1.2 resolution: "minimatch@npm:3.1.2" @@ -4656,7 +4234,7 @@ __metadata: languageName: node linkType: hard -"nanoid@npm:^3.3.4": +"nanoid@npm:^3.3.6": version: 3.3.6 resolution: "nanoid@npm:3.3.6" bin: @@ -4693,16 +4271,6 @@ __metadata: languageName: node linkType: hard -"no-case@npm:^3.0.4": - version: 3.0.4 - resolution: "no-case@npm:3.0.4" - dependencies: - lower-case: ^2.0.2 - tslib: ^2.0.3 - checksum: 8ef545f0b3f8677c848f86ecbd42ca0ff3cd9dd71c158527b344c69ba14710d816d8489c746b6ca225e7b615108938a0bda0a54706f8c255933703ac1cf8e703 - languageName: node - linkType: hard - "node-gyp@npm:latest": version: 9.3.1 resolution: "node-gyp@npm:9.3.1" @@ -4791,13 +4359,6 @@ __metadata: languageName: node linkType: hard -"normalize-url@npm:^6.0.1": - version: 6.1.0 - resolution: "normalize-url@npm:6.1.0" - checksum: 95d948f9bdd2cfde91aa786d1816ae40f8262946e13700bf6628105994fe0ff361662c20af3961161c38a119dc977adeb41fc0b41b1745eb77edaaf9cb22db23 - languageName: node - linkType: hard - "npm-run-all@npm:^4.1.5": version: 4.1.5 resolution: "npm-run-all@npm:4.1.5" @@ -4917,7 +4478,7 @@ __metadata: languageName: node linkType: hard -"once@npm:^1.3.0, once@npm:^1.3.1, once@npm:^1.4.0": +"once@npm:^1.3.0": version: 1.4.0 resolution: "once@npm:1.4.0" dependencies: @@ -4951,22 +4512,6 @@ __metadata: languageName: node linkType: hard -"p-cancelable@npm:^2.0.0": - version: 2.1.1 - resolution: "p-cancelable@npm:2.1.1" - checksum: 8c6dc1f8dd4154fd8b96a10e55a3a832684c4365fb9108056d89e79fbf21a2465027c04a59d0d797b5ffe10b54a61a32043af287d5c4860f1e996cbdbc847f01 - languageName: node - linkType: hard - -"p-limit@npm:^2.2.0": - version: 2.3.0 - resolution: "p-limit@npm:2.3.0" - dependencies: - p-try: ^2.0.0 - checksum: 8da01ac53efe6a627080fafc127c873da40c18d87b3f5d5492d465bb85ec7207e153948df6b9cbaeb130be70152f874229b8242ee2be84c0794082510af97f12 - languageName: node - linkType: hard - "p-limit@npm:^3.0.2": version: 3.1.0 resolution: "p-limit@npm:3.1.0" @@ -4994,23 +4539,6 @@ __metadata: languageName: node linkType: hard -"p-try@npm:^2.0.0": - version: 2.2.0 - resolution: "p-try@npm:2.2.0" - checksum: c36c19907734c904b16994e6535b02c36c2224d433e01a2f1ab777237f4d86e6289fd5fd464850491e940379d4606ed850c03e0f9ab600b0ebddb511312e177f - languageName: node - linkType: hard - -"param-case@npm:^3.0.4": - version: 3.0.4 - resolution: "param-case@npm:3.0.4" - dependencies: - dot-case: ^3.0.4 - tslib: ^2.0.3 - checksum: ccc053f3019f878eca10e70ec546d92f51a592f762917dafab11c8b532715dcff58356118a6f350976e4ab109e321756f05739643ed0ca94298e82291e6f9e76 - languageName: node - linkType: hard - "parent-module@npm:^1.0.0": version: 1.0.1 resolution: "parent-module@npm:1.0.1" @@ -5042,16 +4570,6 @@ __metadata: languageName: node linkType: hard -"pascal-case@npm:^3.1.2": - version: 3.1.2 - resolution: "pascal-case@npm:3.1.2" - dependencies: - no-case: ^3.0.4 - tslib: ^2.0.3 - checksum: 05ff7c344809fd272fc5030ae0ee3da8e4e63f36d47a1e0a4855ca59736254192c5a27b5822ed4bae96e54048eec5f6907713cfcfff7cdf7a464eaf7490786d8 - languageName: node - linkType: hard - "path-exists@npm:^4.0.0": version: 4.0.0 resolution: "path-exists@npm:4.0.0" @@ -5133,14 +4651,14 @@ __metadata: languageName: node linkType: hard -"postcss@npm:^8.4.21": - version: 8.4.21 - resolution: "postcss@npm:8.4.21" +"postcss@npm:^8.4.23": + version: 8.4.23 + resolution: "postcss@npm:8.4.23" dependencies: - nanoid: ^3.3.4 + nanoid: ^3.3.6 picocolors: ^1.0.0 source-map-js: ^1.0.2 - checksum: a26e7cc86a1807d624d9965914c26c20faa3f237184cbd69db537387f6a4f62df97347549144284d47e9e8e27e7c60e797cb3b92ad36cb2f4c3c9cb3b73f9758 + checksum: 35c2e26496be286a63706a0b8240fc4d2075a746466df530989208f60ea33cbc80c89420221cffb7d4fdd605afc385993f5f60302447e3047a7c0a8756b6471d languageName: node linkType: hard @@ -5160,12 +4678,12 @@ __metadata: languageName: node linkType: hard -"prettier@npm:^2.8.7": - version: 2.8.7 - resolution: "prettier@npm:2.8.7" +"prettier@npm:^2.8.8": + version: 2.8.8 + resolution: "prettier@npm:2.8.8" bin: prettier: bin-prettier.js - checksum: 84c5b62f7d4909ae5b18b1a4cee67f6a30a548244c8919e67158dee1453f4fa4ff4d291c6f2e41e21d443a0c405f03ec27690502d4ad90c3a7c59bcaf38b51ba + checksum: 463ea8f9a0946cd5b828d8cf27bd8b567345cf02f56562d5ecde198b91f47a76b7ac9eae0facd247ace70e927143af6135e8cf411986b8cb8478784a4d6d724a languageName: node linkType: hard @@ -5211,16 +4729,6 @@ __metadata: languageName: node linkType: hard -"pump@npm:^3.0.0": - version: 3.0.0 - resolution: "pump@npm:3.0.0" - dependencies: - end-of-stream: ^1.1.0 - once: ^1.3.1 - checksum: bbdeda4f747cdf47db97428f3a135728669e56a0ae5f354a9ac5b74556556f5446a46f720a8f14ca2ece5be9b4d5d23c346db02b555f46739934cc6c093a5478 - languageName: node - linkType: hard - "punycode@npm:^2.1.0": version: 2.3.0 resolution: "punycode@npm:2.3.0" @@ -5235,13 +4743,6 @@ __metadata: languageName: node linkType: hard -"quick-lru@npm:^5.1.1": - version: 5.1.1 - resolution: "quick-lru@npm:5.1.1" - checksum: a24cba5da8cec30d70d2484be37622580f64765fb6390a928b17f60cd69e8dbd32a954b3ff9176fa1b86d86ff2ba05252fae55dc4d40d0291c60412b0ad096da - languageName: node - linkType: hard - "react-dom@npm:latest": version: 18.2.0 resolution: "react-dom@npm:18.2.0" @@ -5290,39 +4791,39 @@ __metadata: languageName: node linkType: hard -"react-router-dom@npm:^6.10.0": - version: 6.10.0 - resolution: "react-router-dom@npm:6.10.0" +"react-router-dom@npm:^6.11.2": + version: 6.11.2 + resolution: "react-router-dom@npm:6.11.2" dependencies: - "@remix-run/router": 1.5.0 - react-router: 6.10.0 + "@remix-run/router": 1.6.2 + react-router: 6.11.2 peerDependencies: react: ">=16.8" react-dom: ">=16.8" - checksum: c62e4b4aac2067e64af632e113b8db0626b846ec88fb668e84492ceb04179a0789120844263e35dead1e13f70f6554c1a463c7c33d8feed579bf5a2a02f916f4 + checksum: be7433bc290e56c0dd3e1008d53a76cc9866bf460980658501880876420086f11810ec3355a3abcd79ac537d6a1351eda009fade841c266456d0e8df60967b76 languageName: node linkType: hard -"react-router@npm:6.10.0": - version: 6.10.0 - resolution: "react-router@npm:6.10.0" +"react-router@npm:6.11.2": + version: 6.11.2 + resolution: "react-router@npm:6.11.2" dependencies: - "@remix-run/router": 1.5.0 + "@remix-run/router": 1.6.2 peerDependencies: react: ">=16.8" - checksum: 83b8d6a7d1572403b9227045148b34253d3e705dd196d5beff9be2e3c044ba1136100a6e45c219c109de7c764d548302242ab029b0df4c5536b8aaddb3b1aa57 + checksum: a437606078d6096a6dfa322adf80d00ce153f20cd470ad888088c8da99f44477b963425c53f5461a540b909fc274154292ed80d636482dcdc58a423915ca1433 languageName: node linkType: hard -"react-toastify@npm:^9.1.2": - version: 9.1.2 - resolution: "react-toastify@npm:9.1.2" +"react-toastify@npm:^9.1.3": + version: 9.1.3 + resolution: "react-toastify@npm:9.1.3" dependencies: clsx: ^1.1.1 peerDependencies: react: ">=16" react-dom: ">=16" - checksum: 904ba15171d3b45751fdec077ca19e9b4010005c75161ddaa805b4c0abd0342fbcd25976b8871de01a2867600dcd902f6b80cf1bfda470ca83d8a514c0d67530 + checksum: 51de1e51e9357a24773fbcd45a4db18bf74b8ec40d86a2bfb4a4fee23ca4f9fffdac5dfb7a3c21baea39971f72f72dfcdc79403a6de006f74d69e7bc12f8b3e0 languageName: node linkType: hard @@ -5422,13 +4923,6 @@ __metadata: languageName: node linkType: hard -"relateurl@npm:^0.2.7": - version: 0.2.7 - resolution: "relateurl@npm:0.2.7" - checksum: c248b4e3b32474f116a804b537fa6343d731b80056fb506dffd91e737eef4cac6be47a65aae39b522b0db9d0b1011d1a12e288d82a109ecd94a5299d82f6573a - languageName: node - linkType: hard - "require-directory@npm:^2.1.1": version: 2.1.1 resolution: "require-directory@npm:2.1.1" @@ -5436,13 +4930,6 @@ __metadata: languageName: node linkType: hard -"resolve-alpn@npm:^1.0.0": - version: 1.2.1 - resolution: "resolve-alpn@npm:1.2.1" - checksum: b70b29c1843bc39781ef946c8cd4482e6d425976599c0f9c138cec8209e4e0736161bf39319b01676a847000085dfdaf63583c6fb4427bf751a10635bd2aa0c4 - languageName: node - linkType: hard - "resolve-from@npm:^4.0.0": version: 4.0.0 resolution: "resolve-from@npm:4.0.0" @@ -5502,15 +4989,6 @@ __metadata: languageName: node linkType: hard -"responselike@npm:^2.0.0": - version: 2.0.1 - resolution: "responselike@npm:2.0.1" - dependencies: - lowercase-keys: ^2.0.0 - checksum: 360b6deb5f101a9f8a4174f7837c523c3ec78b7ca8a7c1d45a1062b303659308a23757e318b1e91ed8684ad1205721142dd664d94771cd63499353fd4ee732b5 - languageName: node - linkType: hard - "retry@npm:^0.12.0": version: 0.12.0 resolution: "retry@npm:0.12.0" @@ -5555,9 +5033,9 @@ __metadata: languageName: node linkType: hard -"rollup@npm:^3.18.0": - version: 3.20.2 - resolution: "rollup@npm:3.20.2" +"rollup@npm:^3.21.0": + version: 3.21.0 + resolution: "rollup@npm:3.21.0" dependencies: fsevents: ~2.3.2 dependenciesMeta: @@ -5565,7 +5043,7 @@ __metadata: optional: true bin: rollup: dist/bin/rollup - checksum: 1203da200a6f7dd2e62445fe31c9711134545f32d8a91f8fc3d79d5eff648d5faaf296af1d867c95439b862a39457670f5a32ecad583c11fc0de1ab2d2ca12b1 + checksum: d88fdc7ea41e1db44b903575782bd7075886e158b31d9869505a06f7afa72ff17f9c6030973736c93142904ad1a9ee730724063a2df227740e587ec62aa4cde3 languageName: node linkType: hard @@ -5630,7 +5108,7 @@ __metadata: languageName: node linkType: hard -"semver@npm:^7.1.2, semver@npm:^7.3.5, semver@npm:^7.3.7": +"semver@npm:^7.3.5, semver@npm:^7.3.7": version: 7.3.8 resolution: "semver@npm:7.3.8" dependencies: @@ -5796,7 +5274,7 @@ __metadata: languageName: node linkType: hard -"source-map@npm:^0.6.0, source-map@npm:~0.6.0": +"source-map@npm:^0.6.0": version: 0.6.1 resolution: "source-map@npm:0.6.1" checksum: ab55398007c5e5532957cb0beee2368529618ac0ab372d789806f5718123cc4367d57de3904b4e6a4170eb5a0b0f41373066d02ca0735a0c4d75c7d328d3e011 @@ -5844,13 +5322,6 @@ __metadata: languageName: node linkType: hard -"sprintf-js@npm:~1.0.2": - version: 1.0.3 - resolution: "sprintf-js@npm:1.0.3" - checksum: ecadcfe4c771890140da5023d43e190b7566d9cf8b2d238600f31bec0fc653f328da4450eb04bd59a431771a8e9cc0e118f0aa3974b683a4981b4e07abc2a5bb - languageName: node - linkType: hard - "ssri@npm:^9.0.0": version: 9.0.1 resolution: "ssri@npm:9.0.1" @@ -5869,6 +5340,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" @@ -5972,10 +5450,10 @@ __metadata: languageName: node linkType: hard -"stylis@npm:4.1.3": - version: 4.1.3 - resolution: "stylis@npm:4.1.3" - checksum: 3e4670f26f79bcfba628dcc2756d9d415edfcbf4ec51e40f3b628fd15286222257317cad57390752964eba85cca6163a7621ce90038d68dd630a674479e52334 +"stylis@npm:4.2.0": + version: 4.2.0 + resolution: "stylis@npm:4.2.0" + checksum: a7128ad5a8ed72652c6eba46bed4f416521bc9745a460ef5741edc725252cebf36ee45e33a8615a7057403c93df0866ab9ee955960792db210bb80abd5ac6543 languageName: node linkType: hard @@ -6028,7 +5506,7 @@ __metadata: languageName: node linkType: hard -"tar@npm:^6.0.5, tar@npm:^6.1.11, tar@npm:^6.1.2": +"tar@npm:^6.1.11, tar@npm:^6.1.2": version: 6.1.13 resolution: "tar@npm:6.1.13" dependencies: @@ -6042,9 +5520,9 @@ __metadata: languageName: node linkType: hard -"terser@npm:^5.10.0, terser@npm:^5.16.8": - version: 5.16.8 - resolution: "terser@npm:5.16.8" +"terser@npm:^5.17.6": + version: 5.17.6 + resolution: "terser@npm:5.17.6" dependencies: "@jridgewell/source-map": ^0.3.2 acorn: ^8.5.0 @@ -6052,7 +5530,7 @@ __metadata: source-map-support: ~0.5.20 bin: terser: bin/terser - checksum: b49772a10ed4fa12bfaf231ff018d818c48b80ae9093fe52ef9753acd25fd40bef20dd4b0a226a5d46e52e7904be6b94e51cf41be0159913e13114d0c3372f82 + checksum: 4c175f35af332320189db20e2b37b0671c51164d55a3fc44ed5b74b023858fd670cb987de7b2e8e3704b0d863f95ea9a0ecff37c8c01a48906fb7f81b512a5c8 languageName: node linkType: hard @@ -6073,13 +5551,6 @@ __metadata: languageName: node linkType: hard -"tinylogic@npm:^2.0.0": - version: 2.0.0 - resolution: "tinylogic@npm:2.0.0" - checksum: c9417c4b65dfc469c71c9eba4d43d44813ab8baceb80ba2c0e6c286de2e93e9c4b8522e4b0a7b91cb4a85353368ee93838a862262ce54bac431b884e694d1c89 - languageName: node - linkType: hard - "to-fast-properties@npm:^2.0.0": version: 2.0.0 resolution: "to-fast-properties@npm:2.0.0" @@ -6107,13 +5578,6 @@ __metadata: languageName: node linkType: hard -"treeify@npm:^1.1.0": - version: 1.1.0 - resolution: "treeify@npm:1.1.0" - checksum: 2f0dea9e89328b8a42296a3963d341ab19897a05b723d6b0bced6b28701a340d2a7b03241aef807844198e46009aaf3755139274eb082cfce6fdc1935cbd69dd - languageName: node - linkType: hard - "tsconfck@npm:^2.1.0": version: 2.1.1 resolution: "tsconfck@npm:2.1.1" @@ -6147,7 +5611,7 @@ __metadata: languageName: node linkType: hard -"tslib@npm:^2.0.3, tslib@npm:^2.4.0, tslib@npm:^2.5.0": +"tslib@npm:^2.4.0, tslib@npm:^2.5.0": version: 2.5.0 resolution: "tslib@npm:2.5.0" checksum: e32fc99cc730dd514e53c44e668d76016e738f0bcc726aad5dbd2d335cf19b87a95a9b1e4f0a9993e370f1d702b5e471cdd4acabcac428a3099d496b9af2021e @@ -6165,20 +5629,6 @@ __metadata: languageName: node linkType: hard -"tunnel@npm:^0.0.6": - version: 0.0.6 - resolution: "tunnel@npm:0.0.6" - checksum: e27e7e896f2426c1c747325b5f54efebc1a004647d853fad892b46d64e37591ccd0b97439470795e5262b5c0748d22beb4489a04a0a448029636670bfd801b75 - languageName: node - linkType: hard - -"typanion@npm:^3.8.0": - version: 3.12.1 - resolution: "typanion@npm:3.12.1" - checksum: 90cf558a34011144d4b741d85615423f594ac769c9850677989e9c42ef6f5e1ea9be5d55c6ff00291038058273a9bc25d7271bec5db0bd05e26af9a0872fb8e6 - languageName: node - linkType: hard - "type-check@npm:^0.4.0, type-check@npm:~0.4.0": version: 0.4.0 resolution: "type-check@npm:0.4.0" @@ -6217,23 +5667,23 @@ __metadata: languageName: node linkType: hard -"typescript@npm:^5.0.3": - version: 5.0.3 - resolution: "typescript@npm:5.0.3" +"typescript@npm:^5.0.4": + version: 5.0.4 + resolution: "typescript@npm:5.0.4" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: 13221c7f7dd2aa9cf8ac2fb1b5ca3825ff3d97a43b4656fd22ca36c0ec30179643b5157d620c3e4af9d3fc9234a571bd3ea175624b3d37c494d7ed159b038df2 + checksum: 2f5bd1cead194905957cb34e220b1d6ff1662399adef8ec1864f74620922d860ee35b6e50eafb3b636ea6fd437195e454e1146cb630a4236b5095ed7617395c2 languageName: node linkType: hard -"typescript@patch:typescript@^5.0.3#~builtin": - version: 5.0.3 - resolution: "typescript@patch:typescript@npm%3A5.0.3#~builtin::version=5.0.3&hash=1f5320" +"typescript@patch:typescript@^5.0.4#~builtin": + version: 5.0.4 + resolution: "typescript@patch:typescript@npm%3A5.0.4#~builtin::version=5.0.4&hash=1f5320" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: 7e50cd198d2f7f052ac4e10d9a9b6e88ed91b3434c32b6e82f278a4e33727b42fee5d33929ef8a633b409a6bf62fd4bb95df464809507b7a703a4c73269dce3c + checksum: db16dd188048c172051825c7e6eea3e6bf577020625e5635fb8265d22683897d9ed7579b41a3a3e961fba5728c58e324d52041e9ca21d0dfc4bafa8bcf8e7a4b languageName: node linkType: hard @@ -6314,32 +5764,22 @@ __metadata: languageName: node linkType: hard -"vite-plugin-minify@npm:^1.5.2": - version: 1.5.2 - resolution: "vite-plugin-minify@npm:1.5.2" - dependencies: - html-minifier-terser: ^6.1.0 - peerDependencies: - vite: "*" - checksum: e7c3d3e936b93f6ba426d72c4396864bd57e546aa621342dc7add5eddbd4d464663e7022c4792012fa2977f4c8a7c598de29ef5a5d4cf6b5b0b80b51bd3970f2 - languageName: node - linkType: hard - -"vite-plugin-svgr@npm:^2.4.0": - version: 2.4.0 - resolution: "vite-plugin-svgr@npm:2.4.0" +"vite-plugin-svgr@npm:^3.2.0": + version: 3.2.0 + resolution: "vite-plugin-svgr@npm:3.2.0" dependencies: "@rollup/pluginutils": ^5.0.2 - "@svgr/core": ^6.5.1 + "@svgr/core": ^7.0.0 + "@svgr/plugin-jsx": ^7.0.0 peerDependencies: vite: ^2.6.0 || 3 || 4 - checksum: df5cff70a7e9fff10a789575ebecf05d2da9fa0e7352b38477fa616e2d91f254ba5331ea185fe47ed3f0549b822dcb1f7cbec32a9f3cf5d8dfd430dcf76358a2 + checksum: f801759810be82e997acb26b6b0f8c6dc012d7bcb4d430e1e75ef210f6f05580c589b7f65c9729fe4993fa919433903b71a74ddfc490e41af69720cf857de9d9 languageName: node linkType: hard -"vite-tsconfig-paths@npm:^4.0.8": - version: 4.0.8 - resolution: "vite-tsconfig-paths@npm:4.0.8" +"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 @@ -6349,19 +5789,18 @@ __metadata: peerDependenciesMeta: vite: optional: true - checksum: 754e404a085668cbd1da08e4ebac7aef09efc3357524273d34bb68ab4dd705b8a0cce9e98c7bd0828f1c525669bf5f165d09c8d81c47011c6a5185fc30bd60de + checksum: 04bd792bb4f6b4fb57ec8368cff076abffba8d6923af032affb14be43b6e2dfd8b25085947a3204d702a8c8e9d79d3c361373cf98566df682420728857906289 languageName: node linkType: hard -"vite@npm:^4.2.1": - version: 4.2.1 - resolution: "vite@npm:4.2.1" +"vite@npm:^4.3.8": + version: 4.3.8 + resolution: "vite@npm:4.3.8" dependencies: esbuild: ^0.17.5 fsevents: ~2.3.2 - postcss: ^8.4.21 - resolve: ^1.22.1 - rollup: ^3.18.0 + postcss: ^8.4.23 + rollup: ^3.21.0 peerDependencies: "@types/node": ">= 14" less: "*" @@ -6387,7 +5826,7 @@ __metadata: optional: true bin: vite: bin/vite.js - checksum: a64e3a1563b9584d1fd1ca45e06ee3c9fa4956320e6d4e9d83bf09fc8e64bb9d3ef62f664b6f740141eee16643690e8a41ffdb3030f4f2e170c57894df1f9a5d + checksum: 7eb7e28b8485ee30ceb2a758ad816d741d21a0e377ca7cc98d1db0a8686076e6bf3e620767afe4d9c497fc4d5fa1fe5f95a422c24c61fd92ac5368a78af43b31 languageName: node linkType: hard diff --git a/lib/ESPAsyncWebServer/ESPAsyncWebServer.h b/lib/ESPAsyncWebServer/ESPAsyncWebServer.h index 327d177e0..02a96cb2f 100644 --- a/lib/ESPAsyncWebServer/ESPAsyncWebServer.h +++ b/lib/ESPAsyncWebServer/ESPAsyncWebServer.h @@ -1,487 +1,487 @@ -/* - Asynchronous WebServer library for Espressif MCUs - - Copyright (c) 2016 Hristo Gochkov. All rights reserved. - This file is part of the esp8266 core for Arduino environment. - - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Lesser General Public - License as published by the Free Software Foundation; either - version 2.1 of the License, or (at your option) any later version. - - This library is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - Lesser General Public License for more details. - - You should have received a copy of the GNU Lesser General Public - License along with this library; if not, write to the Free Software - Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -*/ -#ifndef _ESPAsyncWebServer_H_ -#define _ESPAsyncWebServer_H_ - -#include "Arduino.h" - -#include -#include "FS.h" - -#include "StringArray.h" - -#ifdef ESP32 -#include -#include -#elif defined(ESP8266) -#include -#include -#else -#error Platform not supported -#endif - -#ifdef ASYNCWEBSERVER_REGEX -#define ASYNCWEBSERVER_REGEX_ATTRIBUTE -#else -#define ASYNCWEBSERVER_REGEX_ATTRIBUTE __attribute__((warning("ASYNCWEBSERVER_REGEX not defined"))) -#endif - -#define DEBUGF(...) //Serial.printf(__VA_ARGS__) - -class AsyncWebServer; -class AsyncWebServerRequest; -class AsyncWebServerResponse; -class AsyncWebHeader; -class AsyncWebParameter; -class AsyncWebRewrite; -class AsyncWebHandler; -class AsyncStaticWebHandler; -class AsyncCallbackWebHandler; -class AsyncResponseStream; - -#ifndef WEBSERVER_H -typedef enum { - HTTP_GET = 0b00000001, - HTTP_POST = 0b00000010, - HTTP_DELETE = 0b00000100, - HTTP_PUT = 0b00001000, - HTTP_PATCH = 0b00010000, - HTTP_HEAD = 0b00100000, - HTTP_OPTIONS = 0b01000000, - HTTP_ANY = 0b01111111, -} WebRequestMethod; -#endif - -#ifndef HAVE_FS_FILE_OPEN_MODE -namespace fs { - class FileOpenMode { - public: - static const char *read; - static const char *write; - static const char *append; - }; -}; -#else -#include "FileOpenMode.h" -#endif - -//if this value is returned when asked for data, packet will not be sent and you will be asked for data again -#define RESPONSE_TRY_AGAIN 0xFFFFFFFF - -typedef uint8_t WebRequestMethodComposite; -typedef std::function ArDisconnectHandler; - -/* - * PARAMETER :: Chainable object to hold GET/POST and FILE parameters - * */ - -class AsyncWebParameter { - private: - String _name; - String _value; - size_t _size; - bool _isForm; - bool _isFile; - - public: - - AsyncWebParameter(const String& name, const String& value, bool form=false, bool file=false, size_t size=0): _name(name), _value(value), _size(size), _isForm(form), _isFile(file){} - const String& name() const { return _name; } - const String& value() const { return _value; } - size_t size() const { return _size; } - bool isPost() const { return _isForm; } - bool isFile() const { return _isFile; } -}; - -/* - * HEADER :: Chainable object to hold the headers - * */ - -class AsyncWebHeader { - private: - String _name; - String _value; - - public: - AsyncWebHeader(const String& name, const String& value): _name(name), _value(value){} - AsyncWebHeader(const String& data): _name(), _value(){ - if(!data) return; - int index = data.indexOf(':'); - if (index < 0) return; - _name = data.substring(0, index); - _value = data.substring(index + 2); - } - ~AsyncWebHeader(){} - const String& name() const { return _name; } - const String& value() const { return _value; } - String toString() const { return String(_name + F(": ") + _value + F("\r\n")); } -}; - -/* - * REQUEST :: Each incoming Client is wrapped inside a Request and both live together until disconnect - * */ - -typedef enum { RCT_NOT_USED = -1, RCT_DEFAULT = 0, RCT_HTTP, RCT_WS, RCT_EVENT, RCT_MAX } RequestedConnectionType; - -typedef std::function AwsResponseFiller; -typedef std::function AwsTemplateProcessor; - -class AsyncWebServerRequest { - using File = fs::File; - using FS = fs::FS; - friend class AsyncWebServer; - friend class AsyncCallbackWebHandler; - friend class HttpCookieHeader; - private: - AsyncClient* _client; - AsyncWebServer* _server; - AsyncWebHandler* _handler; - AsyncWebServerResponse* _response; - StringArray _interestingHeaders; - ArDisconnectHandler _onDisconnectfn; - - String _temp; - uint8_t _parseState; - - uint8_t _version; - WebRequestMethodComposite _method; - String _url; - String _host; - String _contentType; - String _boundary; - String _authorization; - RequestedConnectionType _reqconntype; - void _removeNotInterestingHeaders(); - bool _isDigest; - bool _isMultipart; - bool _isPlainPost; - bool _expectingContinue; - size_t _contentLength; - size_t _parsedLength; - - LinkedList _headers; - LinkedList _params; - LinkedList _pathParams; - - uint8_t _multiParseState; - uint8_t _boundaryPosition; - size_t _itemStartIndex; - size_t _itemSize; - String _itemName; - String _itemFilename; - String _itemType; - String _itemValue; - uint8_t *_itemBuffer; - size_t _itemBufferIndex; - bool _itemIsFile; - - void _onPoll(); - void _onAck(size_t len, uint32_t time); - void _onError(int8_t error); - void _onTimeout(uint32_t time); - void _onDisconnect(); - void _onData(void *buf, size_t len); - - void _addParam(AsyncWebParameter*); - void _addPathParam(const char *param); - - bool _parseReqHead(); - bool _parseReqHeader(); - void _parseLine(); - void _parsePlainPostChar(uint8_t data); - void _parseMultipartPostByte(uint8_t data, bool last); - void _addGetParams(const String& params); - - void _handleUploadStart(); - void _handleUploadByte(uint8_t data, bool last); - void _handleUploadEnd(); - - public: - File _tempFile; - void *_tempObject; - - AsyncWebServerRequest(AsyncWebServer*, AsyncClient*); - ~AsyncWebServerRequest(); - - AsyncClient* client(){ return _client; } - uint8_t version() const { return _version; } - WebRequestMethodComposite method() const { return _method; } - const String& url() const { return _url; } - const String& host() const { return _host; } - const String& contentType() const { return _contentType; } - size_t contentLength() const { return _contentLength; } - bool multipart() const { return _isMultipart; } - const __FlashStringHelper *methodToString() const; - const __FlashStringHelper *requestedConnTypeToString() const; - RequestedConnectionType requestedConnType() const { return _reqconntype; } - bool isExpectedRequestedConnType(RequestedConnectionType erct1, RequestedConnectionType erct2 = RCT_NOT_USED, RequestedConnectionType erct3 = RCT_NOT_USED); - void onDisconnect (ArDisconnectHandler fn); - - //hash is the string representation of: - // base64(user:pass) for basic or - // user:realm:md5(user:realm:pass) for digest - bool authenticate(const char * hash); - bool authenticate(const char * username, const char * password, const char * realm = NULL, bool passwordIsHash = false); - void requestAuthentication(const char * realm = NULL, bool isDigest = true); - - void setHandler(AsyncWebHandler *handler){ _handler = handler; } - void addInterestingHeader(const String& name); - - void redirect(const String& url); - - void send(AsyncWebServerResponse *response); - void send(int code, const String& contentType=String(), const String& content=String()); - void send(FS &fs, const String& path, const String& contentType=String(), bool download=false, AwsTemplateProcessor callback=nullptr); - void send(File content, const String& path, const String& contentType=String(), bool download=false, AwsTemplateProcessor callback=nullptr); - void send(Stream &stream, const String& contentType, size_t len, AwsTemplateProcessor callback=nullptr); - void send(const String& contentType, size_t len, AwsResponseFiller callback, AwsTemplateProcessor templateCallback=nullptr); - void sendChunked(const String& contentType, AwsResponseFiller callback, AwsTemplateProcessor templateCallback=nullptr); - void send_P(int code, const String& contentType, const uint8_t * content, size_t len, AwsTemplateProcessor callback=nullptr); - void send_P(int code, const String& contentType, PGM_P content, AwsTemplateProcessor callback=nullptr); - - AsyncWebServerResponse *beginResponse(int code, const String& contentType=String(), const String& content=String()); - AsyncWebServerResponse *beginResponse(FS &fs, const String& path, const String& contentType=String(), bool download=false, AwsTemplateProcessor callback=nullptr); - AsyncWebServerResponse *beginResponse(File content, const String& path, const String& contentType=String(), bool download=false, AwsTemplateProcessor callback=nullptr); - AsyncWebServerResponse *beginResponse(Stream &stream, const String& contentType, size_t len, AwsTemplateProcessor callback=nullptr); - AsyncWebServerResponse *beginResponse(const String& contentType, size_t len, AwsResponseFiller callback, AwsTemplateProcessor templateCallback=nullptr); - AsyncWebServerResponse *beginChunkedResponse(const String& contentType, AwsResponseFiller callback, AwsTemplateProcessor templateCallback=nullptr); - AsyncResponseStream *beginResponseStream(const String& contentType, size_t bufferSize=1460); - AsyncWebServerResponse *beginResponse_P(int code, const String& contentType, const uint8_t * content, size_t len, AwsTemplateProcessor callback=nullptr); - AsyncWebServerResponse *beginResponse_P(int code, const String& contentType, PGM_P content, AwsTemplateProcessor callback=nullptr); - - size_t headers() const; // get header count - bool hasHeader(const String& name) const; // check if header exists - bool hasHeader(const __FlashStringHelper * data) const; // check if header exists - - AsyncWebHeader* getHeader(const String& name) const; - AsyncWebHeader* getHeader(const __FlashStringHelper * data) const; - AsyncWebHeader* getHeader(size_t num) const; - - size_t params() const; // get arguments count - bool hasParam(const String& name, bool post=false, bool file=false) const; - bool hasParam(const __FlashStringHelper * data, bool post=false, bool file=false) const; - - AsyncWebParameter* getParam(const String& name, bool post=false, bool file=false) const; - AsyncWebParameter* getParam(const __FlashStringHelper * data, bool post, bool file) const; - AsyncWebParameter* getParam(size_t num) const; - - size_t args() const { return params(); } // get arguments count - const String& arg(const String& name) const; // get request argument value by name - const String& arg(const __FlashStringHelper * data) const; // get request argument value by F(name) - const String& arg(size_t i) const; // get request argument value by number - const String& argName(size_t i) const; // get request argument name by number - bool hasArg(const char* name) const; // check if argument exists - bool hasArg(const __FlashStringHelper * data) const; // check if F(argument) exists - - const String& ASYNCWEBSERVER_REGEX_ATTRIBUTE pathArg(size_t i) const; - - const String& header(const char* name) const;// get request header value by name - const String& header(const __FlashStringHelper * data) const;// get request header value by F(name) - const String& header(size_t i) const; // get request header value by number - const String& headerName(size_t i) const; // get request header name by number - String urlDecode(const String& text) const; -}; - -/* - * FILTER :: Callback to filter AsyncWebRewrite and AsyncWebHandler (done by the Server) - * */ - -typedef std::function ArRequestFilterFunction; - -bool ON_STA_FILTER(AsyncWebServerRequest *request); - -bool ON_AP_FILTER(AsyncWebServerRequest *request); - -/* - * REWRITE :: One instance can be handle any Request (done by the Server) - * */ - -class AsyncWebRewrite { - protected: - String _from; - String _toUrl; - String _params; - ArRequestFilterFunction _filter; - public: - AsyncWebRewrite(const char* from, const char* to): _from(from), _toUrl(to), _params(String()), _filter(NULL){ - int index = _toUrl.indexOf('?'); - if (index > 0) { - _params = _toUrl.substring(index +1); - _toUrl = _toUrl.substring(0, index); - } - } - virtual ~AsyncWebRewrite(){} - AsyncWebRewrite& setFilter(ArRequestFilterFunction fn) { _filter = fn; return *this; } - bool filter(AsyncWebServerRequest *request) const { return _filter == NULL || _filter(request); } - const String& from(void) const { return _from; } - const String& toUrl(void) const { return _toUrl; } - const String& params(void) const { return _params; } - virtual bool match(AsyncWebServerRequest *request) { return from() == request->url() && filter(request); } -}; - -/* - * HANDLER :: One instance can be attached to any Request (done by the Server) - * */ - -class AsyncWebHandler { - protected: - ArRequestFilterFunction _filter; - String _username; - String _password; - public: - AsyncWebHandler():_username(""), _password(""){} - AsyncWebHandler& setFilter(ArRequestFilterFunction fn) { _filter = fn; return *this; } - AsyncWebHandler& setAuthentication(const char *username, const char *password){ _username = String(username);_password = String(password); return *this; }; - bool filter(AsyncWebServerRequest *request){ return _filter == NULL || _filter(request); } - virtual ~AsyncWebHandler(){} - virtual bool canHandle(AsyncWebServerRequest *request __attribute__((unused))){ - return false; - } - virtual void handleRequest(AsyncWebServerRequest *request __attribute__((unused))){} - virtual void handleUpload(AsyncWebServerRequest *request __attribute__((unused)), const String& filename __attribute__((unused)), size_t index __attribute__((unused)), uint8_t *data __attribute__((unused)), size_t len __attribute__((unused)), bool final __attribute__((unused))){} - virtual void handleBody(AsyncWebServerRequest *request __attribute__((unused)), uint8_t *data __attribute__((unused)), size_t len __attribute__((unused)), size_t index __attribute__((unused)), size_t total __attribute__((unused))){} - virtual bool isRequestHandlerTrivial(){return true;} -}; - -/* - * RESPONSE :: One instance is created for each Request (attached by the Handler) - * */ - -typedef enum { - RESPONSE_SETUP, RESPONSE_HEADERS, RESPONSE_CONTENT, RESPONSE_WAIT_ACK, RESPONSE_END, RESPONSE_FAILED -} WebResponseState; - -class AsyncWebServerResponse { - protected: - int _code; - LinkedList _headers; - String _contentType; - size_t _contentLength; - bool _sendContentLength; - bool _chunked; - size_t _headLength; - size_t _sentLength; - size_t _ackedLength; - size_t _writtenLength; - WebResponseState _state; - const char* _responseCodeToString(int code); -public: - static const __FlashStringHelper *responseCodeToString(int code); - - public: - AsyncWebServerResponse(); - virtual ~AsyncWebServerResponse(); - virtual void setCode(int code); - virtual void setContentLength(size_t len); - virtual void setContentType(const String& type); - virtual void addHeader(const String& name, const String& value); - virtual String _assembleHead(uint8_t version); - virtual bool _started() const; - virtual bool _finished() const; - virtual bool _failed() const; - virtual bool _sourceValid() const; - virtual void _respond(AsyncWebServerRequest *request); - virtual size_t _ack(AsyncWebServerRequest *request, size_t len, uint32_t time); -}; - -/* - * SERVER :: One instance - * */ - -typedef std::function ArRequestHandlerFunction; -typedef std::function ArUploadHandlerFunction; -typedef std::function ArBodyHandlerFunction; - -class AsyncWebServer { - protected: - AsyncServer _server; - LinkedList _rewrites; - LinkedList _handlers; - AsyncCallbackWebHandler* _catchAllHandler; - - public: - AsyncWebServer(uint16_t port); - ~AsyncWebServer(); - - void begin(); - void end(); - -#if ASYNC_TCP_SSL_ENABLED - void onSslFileRequest(AcSSlFileHandler cb, void* arg); - void beginSecure(const char *cert, const char *private_key_file, const char *password); -#endif - - AsyncWebRewrite& addRewrite(AsyncWebRewrite* rewrite); - bool removeRewrite(AsyncWebRewrite* rewrite); - AsyncWebRewrite& rewrite(const char* from, const char* to); - - AsyncWebHandler& addHandler(AsyncWebHandler* handler); - bool removeHandler(AsyncWebHandler* handler); - - AsyncCallbackWebHandler& on(const char* uri, ArRequestHandlerFunction onRequest); - AsyncCallbackWebHandler& on(const char* uri, WebRequestMethodComposite method, ArRequestHandlerFunction onRequest); - AsyncCallbackWebHandler& on(const char* uri, WebRequestMethodComposite method, ArRequestHandlerFunction onRequest, ArUploadHandlerFunction onUpload); - AsyncCallbackWebHandler& on(const char* uri, WebRequestMethodComposite method, ArRequestHandlerFunction onRequest, ArUploadHandlerFunction onUpload, ArBodyHandlerFunction onBody); - - AsyncStaticWebHandler& serveStatic(const char* uri, fs::FS& fs, const char* path, const char* cache_control = NULL); - - void onNotFound(ArRequestHandlerFunction fn); //called when handler is not assigned - void onFileUpload(ArUploadHandlerFunction fn); //handle file uploads - void onRequestBody(ArBodyHandlerFunction fn); //handle posts with plain body content (JSON often transmitted this way as a request) - - void reset(); //remove all writers and handlers, with onNotFound/onFileUpload/onRequestBody - - void _handleDisconnect(AsyncWebServerRequest *request); - void _attachHandler(AsyncWebServerRequest *request); - void _rewriteRequest(AsyncWebServerRequest *request); -}; - -class DefaultHeaders { - using headers_t = LinkedList; - headers_t _headers; - - DefaultHeaders() - :_headers(headers_t([](AsyncWebHeader *h){ delete h; })) - {} -public: - using ConstIterator = headers_t::ConstIterator; - - void addHeader(const String& name, const String& value){ - _headers.add(new AsyncWebHeader(name, value)); - } - - ConstIterator begin() const { return _headers.begin(); } - ConstIterator end() const { return _headers.end(); } - - DefaultHeaders(DefaultHeaders const &) = delete; - DefaultHeaders &operator=(DefaultHeaders const &) = delete; - static DefaultHeaders &Instance() { - static DefaultHeaders instance; - return instance; - } -}; - -#include "WebResponseImpl.h" -#include "WebHandlerImpl.h" -#include "AsyncWebSocket.h" -#include "AsyncEventSource.h" - -#endif /* _AsyncWebServer_H_ */ +/* + Asynchronous WebServer library for Espressif MCUs + + Copyright (c) 2016 Hristo Gochkov. All rights reserved. + This file is part of the esp8266 core for Arduino environment. + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ +#ifndef _ESPAsyncWebServer_H_ +#define _ESPAsyncWebServer_H_ + +#include "Arduino.h" + +#include +#include "FS.h" + +#include "StringArray.h" + +#ifdef ESP32 +#include +#include +#elif defined(ESP8266) +#include +#include +#else +#error Platform not supported +#endif + +#ifdef ASYNCWEBSERVER_REGEX +#define ASYNCWEBSERVER_REGEX_ATTRIBUTE +#else +#define ASYNCWEBSERVER_REGEX_ATTRIBUTE __attribute__((warning("ASYNCWEBSERVER_REGEX not defined"))) +#endif + +#define DEBUGF(...) //Serial.printf(__VA_ARGS__) + +class AsyncWebServer; +class AsyncWebServerRequest; +class AsyncWebServerResponse; +class AsyncWebHeader; +class AsyncWebParameter; +class AsyncWebRewrite; +class AsyncWebHandler; +class AsyncStaticWebHandler; +class AsyncCallbackWebHandler; +class AsyncResponseStream; + +#ifndef WEBSERVER_H +typedef enum { + HTTP_GET = 0b00000001, + HTTP_POST = 0b00000010, + HTTP_DELETE = 0b00000100, + HTTP_PUT = 0b00001000, + HTTP_PATCH = 0b00010000, + HTTP_HEAD = 0b00100000, + HTTP_OPTIONS = 0b01000000, + HTTP_ANY = 0b01111111, +} WebRequestMethod; +#endif + +#ifndef HAVE_FS_FILE_OPEN_MODE +namespace fs { + class FileOpenMode { + public: + static const char *read; + static const char *write; + static const char *append; + }; +}; +#else +#include "FileOpenMode.h" +#endif + +//if this value is returned when asked for data, packet will not be sent and you will be asked for data again +#define RESPONSE_TRY_AGAIN 0xFFFFFFFF + +typedef uint8_t WebRequestMethodComposite; +typedef std::function ArDisconnectHandler; + +/* + * PARAMETER :: Chainable object to hold GET/POST and FILE parameters + * */ + +class AsyncWebParameter { + private: + String _name; + String _value; + size_t _size; + bool _isForm; + bool _isFile; + + public: + + AsyncWebParameter(const String& name, const String& value, bool form=false, bool file=false, size_t size=0): _name(name), _value(value), _size(size), _isForm(form), _isFile(file){} + const String& name() const { return _name; } + const String& value() const { return _value; } + size_t size() const { return _size; } + bool isPost() const { return _isForm; } + bool isFile() const { return _isFile; } +}; + +/* + * HEADER :: Chainable object to hold the headers + * */ + +class AsyncWebHeader { + private: + String _name; + String _value; + + public: + AsyncWebHeader(const String& name, const String& value): _name(name), _value(value){} + AsyncWebHeader(const String& data): _name(), _value(){ + if(!data) return; + int index = data.indexOf(':'); + if (index < 0) return; + _name = data.substring(0, index); + _value = data.substring(index + 2); + } + ~AsyncWebHeader(){} + const String& name() const { return _name; } + const String& value() const { return _value; } + String toString() const { return String(_name + F(": ") + _value + F("\r\n")); } +}; + +/* + * REQUEST :: Each incoming Client is wrapped inside a Request and both live together until disconnect + * */ + +typedef enum { RCT_NOT_USED = -1, RCT_DEFAULT = 0, RCT_HTTP, RCT_WS, RCT_EVENT, RCT_MAX } RequestedConnectionType; + +typedef std::function AwsResponseFiller; +typedef std::function AwsTemplateProcessor; + +class AsyncWebServerRequest { + using File = fs::File; + using FS = fs::FS; + friend class AsyncWebServer; + friend class AsyncCallbackWebHandler; + friend class HttpCookieHeader; + private: + AsyncClient* _client; + AsyncWebServer* _server; + AsyncWebHandler* _handler; + AsyncWebServerResponse* _response; + StringArray _interestingHeaders; + ArDisconnectHandler _onDisconnectfn; + + String _temp; + uint8_t _parseState; + + uint8_t _version; + WebRequestMethodComposite _method; + String _url; + String _host; + String _contentType; + String _boundary; + String _authorization; + RequestedConnectionType _reqconntype; + void _removeNotInterestingHeaders(); + bool _isDigest; + bool _isMultipart; + bool _isPlainPost; + bool _expectingContinue; + size_t _contentLength; + size_t _parsedLength; + + LinkedList _headers; + LinkedList _params; + LinkedList _pathParams; + + uint8_t _multiParseState; + uint8_t _boundaryPosition; + size_t _itemStartIndex; + size_t _itemSize; + String _itemName; + String _itemFilename; + String _itemType; + String _itemValue; + uint8_t *_itemBuffer; + size_t _itemBufferIndex; + bool _itemIsFile; + + void _onPoll(); + void _onAck(size_t len, uint32_t time); + void _onError(int8_t error); + void _onTimeout(uint32_t time); + void _onDisconnect(); + void _onData(void *buf, size_t len); + + void _addParam(AsyncWebParameter*); + void _addPathParam(const char *param); + + bool _parseReqHead(); + bool _parseReqHeader(); + void _parseLine(); + void _parsePlainPostChar(uint8_t data); + void _parseMultipartPostByte(uint8_t data, bool last); + void _addGetParams(const String& params); + + void _handleUploadStart(); + void _handleUploadByte(uint8_t data, bool last); + void _handleUploadEnd(); + + public: + File _tempFile; + void *_tempObject; + + AsyncWebServerRequest(AsyncWebServer*, AsyncClient*); + ~AsyncWebServerRequest(); + + AsyncClient* client(){ return _client; } + uint8_t version() const { return _version; } + WebRequestMethodComposite method() const { return _method; } + const String& url() const { return _url; } + const String& host() const { return _host; } + const String& contentType() const { return _contentType; } + size_t contentLength() const { return _contentLength; } + bool multipart() const { return _isMultipart; } + const char *methodToString() const; + const char *requestedConnTypeToString() const; + RequestedConnectionType requestedConnType() const { return _reqconntype; } + bool isExpectedRequestedConnType(RequestedConnectionType erct1, RequestedConnectionType erct2 = RCT_NOT_USED, RequestedConnectionType erct3 = RCT_NOT_USED); + void onDisconnect (ArDisconnectHandler fn); + + //hash is the string representation of: + // base64(user:pass) for basic or + // user:realm:md5(user:realm:pass) for digest + bool authenticate(const char * hash); + bool authenticate(const char * username, const char * password, const char * realm = NULL, bool passwordIsHash = false); + void requestAuthentication(const char * realm = NULL, bool isDigest = true); + + void setHandler(AsyncWebHandler *handler){ _handler = handler; } + void addInterestingHeader(const String& name); + + void redirect(const String& url); + + void send(AsyncWebServerResponse *response); + void send(int code, const String& contentType=String(), const String& content=String()); + void send(FS &fs, const String& path, const String& contentType=String(), bool download=false, AwsTemplateProcessor callback=nullptr); + void send(File content, const String& path, const String& contentType=String(), bool download=false, AwsTemplateProcessor callback=nullptr); + void send(Stream &stream, const String& contentType, size_t len, AwsTemplateProcessor callback=nullptr); + void send(const String& contentType, size_t len, AwsResponseFiller callback, AwsTemplateProcessor templateCallback=nullptr); + void sendChunked(const String& contentType, AwsResponseFiller callback, AwsTemplateProcessor templateCallback=nullptr); + void send_P(int code, const String& contentType, const uint8_t * content, size_t len, AwsTemplateProcessor callback=nullptr); + void send_P(int code, const String& contentType, PGM_P content, AwsTemplateProcessor callback=nullptr); + + AsyncWebServerResponse *beginResponse(int code, const String& contentType=String(), const String& content=String()); + AsyncWebServerResponse *beginResponse(FS &fs, const String& path, const String& contentType=String(), bool download=false, AwsTemplateProcessor callback=nullptr); + AsyncWebServerResponse *beginResponse(File content, const String& path, const String& contentType=String(), bool download=false, AwsTemplateProcessor callback=nullptr); + AsyncWebServerResponse *beginResponse(Stream &stream, const String& contentType, size_t len, AwsTemplateProcessor callback=nullptr); + AsyncWebServerResponse *beginResponse(const String& contentType, size_t len, AwsResponseFiller callback, AwsTemplateProcessor templateCallback=nullptr); + AsyncWebServerResponse *beginChunkedResponse(const String& contentType, AwsResponseFiller callback, AwsTemplateProcessor templateCallback=nullptr); + AsyncResponseStream *beginResponseStream(const String& contentType, size_t bufferSize=1460); + AsyncWebServerResponse *beginResponse_P(int code, const String& contentType, const uint8_t * content, size_t len, AwsTemplateProcessor callback=nullptr); + AsyncWebServerResponse *beginResponse_P(int code, const String& contentType, PGM_P content, AwsTemplateProcessor callback=nullptr); + + size_t headers() const; // get header count + bool hasHeader(const String& name) const; // check if header exists + bool hasHeader(const __FlashStringHelper * data) const; // check if header exists + + AsyncWebHeader* getHeader(const String& name) const; + AsyncWebHeader* getHeader(const __FlashStringHelper * data) const; + AsyncWebHeader* getHeader(size_t num) const; + + size_t params() const; // get arguments count + bool hasParam(const String& name, bool post=false, bool file=false) const; + bool hasParam(const __FlashStringHelper * data, bool post=false, bool file=false) const; + + AsyncWebParameter* getParam(const String& name, bool post=false, bool file=false) const; + AsyncWebParameter* getParam(const __FlashStringHelper * data, bool post, bool file) const; + AsyncWebParameter* getParam(size_t num) const; + + size_t args() const { return params(); } // get arguments count + const String& arg(const String& name) const; // get request argument value by name + const String& arg(const __FlashStringHelper * data) const; // get request argument value by F(name) + const String& arg(size_t i) const; // get request argument value by number + const String& argName(size_t i) const; // get request argument name by number + bool hasArg(const char* name) const; // check if argument exists + bool hasArg(const __FlashStringHelper * data) const; // check if F(argument) exists + + const String& ASYNCWEBSERVER_REGEX_ATTRIBUTE pathArg(size_t i) const; + + const String& header(const char* name) const;// get request header value by name + const String& header(const __FlashStringHelper * data) const;// get request header value by F(name) + const String& header(size_t i) const; // get request header value by number + const String& headerName(size_t i) const; // get request header name by number + String urlDecode(const String& text) const; +}; + +/* + * FILTER :: Callback to filter AsyncWebRewrite and AsyncWebHandler (done by the Server) + * */ + +typedef std::function ArRequestFilterFunction; + +bool ON_STA_FILTER(AsyncWebServerRequest *request); + +bool ON_AP_FILTER(AsyncWebServerRequest *request); + +/* + * REWRITE :: One instance can be handle any Request (done by the Server) + * */ + +class AsyncWebRewrite { + protected: + String _from; + String _toUrl; + String _params; + ArRequestFilterFunction _filter; + public: + AsyncWebRewrite(const char* from, const char* to): _from(from), _toUrl(to), _params(String()), _filter(NULL){ + int index = _toUrl.indexOf('?'); + if (index > 0) { + _params = _toUrl.substring(index +1); + _toUrl = _toUrl.substring(0, index); + } + } + virtual ~AsyncWebRewrite(){} + AsyncWebRewrite& setFilter(ArRequestFilterFunction fn) { _filter = fn; return *this; } + bool filter(AsyncWebServerRequest *request) const { return _filter == NULL || _filter(request); } + const String& from(void) const { return _from; } + const String& toUrl(void) const { return _toUrl; } + const String& params(void) const { return _params; } + virtual bool match(AsyncWebServerRequest *request) { return from() == request->url() && filter(request); } +}; + +/* + * HANDLER :: One instance can be attached to any Request (done by the Server) + * */ + +class AsyncWebHandler { + protected: + ArRequestFilterFunction _filter; + String _username; + String _password; + public: + AsyncWebHandler():_username(""), _password(""){} + AsyncWebHandler& setFilter(ArRequestFilterFunction fn) { _filter = fn; return *this; } + AsyncWebHandler& setAuthentication(const char *username, const char *password){ _username = String(username);_password = String(password); return *this; }; + bool filter(AsyncWebServerRequest *request){ return _filter == NULL || _filter(request); } + virtual ~AsyncWebHandler(){} + virtual bool canHandle(AsyncWebServerRequest *request __attribute__((unused))){ + return false; + } + virtual void handleRequest(AsyncWebServerRequest *request __attribute__((unused))){} + virtual void handleUpload(AsyncWebServerRequest *request __attribute__((unused)), const String& filename __attribute__((unused)), size_t index __attribute__((unused)), uint8_t *data __attribute__((unused)), size_t len __attribute__((unused)), bool final __attribute__((unused))){} + virtual void handleBody(AsyncWebServerRequest *request __attribute__((unused)), uint8_t *data __attribute__((unused)), size_t len __attribute__((unused)), size_t index __attribute__((unused)), size_t total __attribute__((unused))){} + virtual bool isRequestHandlerTrivial(){return true;} +}; + +/* + * RESPONSE :: One instance is created for each Request (attached by the Handler) + * */ + +typedef enum { + RESPONSE_SETUP, RESPONSE_HEADERS, RESPONSE_CONTENT, RESPONSE_WAIT_ACK, RESPONSE_END, RESPONSE_FAILED +} WebResponseState; + +class AsyncWebServerResponse { + protected: + int _code; + LinkedList _headers; + String _contentType; + size_t _contentLength; + bool _sendContentLength; + bool _chunked; + size_t _headLength; + size_t _sentLength; + size_t _ackedLength; + size_t _writtenLength; + WebResponseState _state; + const char* _responseCodeToString(int code); +public: + static const __FlashStringHelper *responseCodeToString(int code); + + public: + AsyncWebServerResponse(); + virtual ~AsyncWebServerResponse(); + virtual void setCode(int code); + virtual void setContentLength(size_t len); + virtual void setContentType(const String& type); + virtual void addHeader(const String& name, const String& value); + virtual String _assembleHead(uint8_t version); + virtual bool _started() const; + virtual bool _finished() const; + virtual bool _failed() const; + virtual bool _sourceValid() const; + virtual void _respond(AsyncWebServerRequest *request); + virtual size_t _ack(AsyncWebServerRequest *request, size_t len, uint32_t time); +}; + +/* + * SERVER :: One instance + * */ + +typedef std::function ArRequestHandlerFunction; +typedef std::function ArUploadHandlerFunction; +typedef std::function ArBodyHandlerFunction; + +class AsyncWebServer { + protected: + AsyncServer _server; + LinkedList _rewrites; + LinkedList _handlers; + AsyncCallbackWebHandler* _catchAllHandler; + + public: + AsyncWebServer(uint16_t port); + ~AsyncWebServer(); + + void begin(); + void end(); + +#if ASYNC_TCP_SSL_ENABLED + void onSslFileRequest(AcSSlFileHandler cb, void* arg); + void beginSecure(const char *cert, const char *private_key_file, const char *password); +#endif + + AsyncWebRewrite& addRewrite(AsyncWebRewrite* rewrite); + bool removeRewrite(AsyncWebRewrite* rewrite); + AsyncWebRewrite& rewrite(const char* from, const char* to); + + AsyncWebHandler& addHandler(AsyncWebHandler* handler); + bool removeHandler(AsyncWebHandler* handler); + + AsyncCallbackWebHandler& on(const char* uri, ArRequestHandlerFunction onRequest); + AsyncCallbackWebHandler& on(const char* uri, WebRequestMethodComposite method, ArRequestHandlerFunction onRequest); + AsyncCallbackWebHandler& on(const char* uri, WebRequestMethodComposite method, ArRequestHandlerFunction onRequest, ArUploadHandlerFunction onUpload); + AsyncCallbackWebHandler& on(const char* uri, WebRequestMethodComposite method, ArRequestHandlerFunction onRequest, ArUploadHandlerFunction onUpload, ArBodyHandlerFunction onBody); + + AsyncStaticWebHandler& serveStatic(const char* uri, fs::FS& fs, const char* path, const char* cache_control = NULL); + + void onNotFound(ArRequestHandlerFunction fn); //called when handler is not assigned + void onFileUpload(ArUploadHandlerFunction fn); //handle file uploads + void onRequestBody(ArBodyHandlerFunction fn); //handle posts with plain body content (JSON often transmitted this way as a request) + + void reset(); //remove all writers and handlers, with onNotFound/onFileUpload/onRequestBody + + void _handleDisconnect(AsyncWebServerRequest *request); + void _attachHandler(AsyncWebServerRequest *request); + void _rewriteRequest(AsyncWebServerRequest *request); +}; + +class DefaultHeaders { + using headers_t = LinkedList; + headers_t _headers; + + DefaultHeaders() + :_headers(headers_t([](AsyncWebHeader *h){ delete h; })) + {} +public: + using ConstIterator = headers_t::ConstIterator; + + void addHeader(const String& name, const String& value){ + _headers.add(new AsyncWebHeader(name, value)); + } + + ConstIterator begin() const { return _headers.begin(); } + ConstIterator end() const { return _headers.end(); } + + DefaultHeaders(DefaultHeaders const &) = delete; + DefaultHeaders &operator=(DefaultHeaders const &) = delete; + static DefaultHeaders &Instance() { + static DefaultHeaders instance; + return instance; + } +}; + +#include "WebResponseImpl.h" +#include "WebHandlerImpl.h" +#include "AsyncWebSocket.h" +#include "AsyncEventSource.h" + +#endif /* _AsyncWebServer_H_ */ diff --git a/lib/ESPAsyncWebServer/WebAuthentication.cpp b/lib/ESPAsyncWebServer/WebAuthentication.cpp index 5c2538848..288e15508 100644 --- a/lib/ESPAsyncWebServer/WebAuthentication.cpp +++ b/lib/ESPAsyncWebServer/WebAuthentication.cpp @@ -1,235 +1,235 @@ -/* - Asynchronous WebServer library for Espressif MCUs - - Copyright (c) 2016 Hristo Gochkov. All rights reserved. - This file is part of the esp8266 core for Arduino environment. - - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Lesser General Public - License as published by the Free Software Foundation; either - version 2.1 of the License, or (at your option) any later version. - - This library is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - Lesser General Public License for more details. - - You should have received a copy of the GNU Lesser General Public - License along with this library; if not, write to the Free Software - Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -*/ -#include "WebAuthentication.h" -#include -#ifdef ESP32 -#include "mbedtls/md5.h" -#else -#include "md5.h" -#endif - - -// Basic Auth hash = base64("username:password") - -bool checkBasicAuthentication(const char * hash, const char * username, const char * password){ - if(username == NULL || password == NULL || hash == NULL) - return false; - - size_t toencodeLen = strlen(username)+strlen(password)+1; - size_t encodedLen = base64_encode_expected_len(toencodeLen); - if(strlen(hash) != encodedLen) - return false; - - char *toencode = new char[toencodeLen+1]; - if(toencode == NULL){ - return false; - } - char *encoded = new char[base64_encode_expected_len(toencodeLen)+1]; - if(encoded == NULL){ - delete[] toencode; - return false; - } - sprintf_P(toencode, PSTR("%s:%s"), username, password); - if(base64_encode_chars(toencode, toencodeLen, encoded) > 0 && memcmp(hash, encoded, encodedLen) == 0){ - delete[] toencode; - delete[] encoded; - return true; - } - delete[] toencode; - delete[] encoded; - return false; -} - -static bool getMD5(uint8_t * data, uint16_t len, char * output){//33 bytes or more -#ifdef ESP32 - mbedtls_md5_context _ctx; -#else - md5_context_t _ctx; -#endif - uint8_t i; - uint8_t * _buf = (uint8_t*)malloc(16); - if(_buf == NULL) - return false; - memset(_buf, 0x00, 16); -#ifdef ESP32 - mbedtls_md5_init(&_ctx); - mbedtls_md5_update_ret (&_ctx,data,len); - mbedtls_md5_finish_ret(&_ctx,data); - mbedtls_internal_md5_process( &_ctx ,data); -#else - MD5Init(&_ctx); - MD5Update(&_ctx, data, len); - MD5Final(_buf, &_ctx); -#endif - for(i = 0; i < 16; i++) { - sprintf_P(output + (i * 2), PSTR("%02x"), _buf[i]); - } - free(_buf); - return true; -} - -static String genRandomMD5(){ -#ifdef ESP8266 - uint32_t r = RANDOM_REG32; -#else - uint32_t r = rand(); -#endif - char * out = (char*)malloc(33); - if(out == NULL || !getMD5((uint8_t*)(&r), 4, out)) - return emptyString; - String res = String(out); - free(out); - return res; -} - -static String stringMD5(const String& in){ - char * out = (char*)malloc(33); - if(out == NULL || !getMD5((uint8_t*)(in.c_str()), in.length(), out)) - return emptyString; - String res = String(out); - free(out); - return res; -} - -String generateDigestHash(const char * username, const char * password, const char * realm){ - if(username == NULL || password == NULL || realm == NULL){ - return emptyString; - } - char * out = (char*)malloc(33); - String res = String(username); - res += ':'; - res.concat(realm); - res += ':'; - String in = res; - in.concat(password); - if(out == NULL || !getMD5((uint8_t*)(in.c_str()), in.length(), out)) - return emptyString; - res.concat(out); - free(out); - return res; -} - -String requestDigestAuthentication(const char * realm){ - String header = F("realm=\""); - if(realm == NULL) - header.concat(F("asyncesp")); - else - header.concat(realm); - header.concat(F("\", qop=\"auth\", nonce=\"")); - header.concat(genRandomMD5()); - header.concat(F("\", opaque=\"")); - header.concat(genRandomMD5()); - header += '"'; - return header; -} - -bool checkDigestAuthentication(const char * header, const __FlashStringHelper *method, const char * username, const char * password, const char * realm, bool passwordIsHash, const char * nonce, const char * opaque, const char * uri){ - if(username == NULL || password == NULL || header == NULL || method == NULL){ - //os_printf("AUTH FAIL: missing requred fields\n"); - return false; - } - - String myHeader = String(header); - int nextBreak = myHeader.indexOf(','); - if(nextBreak < 0){ - //os_printf("AUTH FAIL: no variables\n"); - return false; - } - - String myUsername = String(); - String myRealm = String(); - String myNonce = String(); - String myUri = String(); - String myResponse = String(); - String myQop = String(); - String myNc = String(); - String myCnonce = String(); - - myHeader += F(", "); - do { - String avLine = myHeader.substring(0, nextBreak); - avLine.trim(); - myHeader = myHeader.substring(nextBreak+1); - nextBreak = myHeader.indexOf(','); - - int eqSign = avLine.indexOf('='); - if(eqSign < 0){ - //os_printf("AUTH FAIL: no = sign\n"); - return false; - } - String varName = avLine.substring(0, eqSign); - avLine = avLine.substring(eqSign + 1); - if(avLine.startsWith(String('"'))){ - avLine = avLine.substring(1, avLine.length() - 1); - } - - if(varName.equals(F("username"))){ - if(!avLine.equals(username)){ - //os_printf("AUTH FAIL: username\n"); - return false; - } - myUsername = avLine; - } else if(varName.equals(F("realm"))){ - if(realm != NULL && !avLine.equals(realm)){ - //os_printf("AUTH FAIL: realm\n"); - return false; - } - myRealm = avLine; - } else if(varName.equals(F("nonce"))){ - if(nonce != NULL && !avLine.equals(nonce)){ - //os_printf("AUTH FAIL: nonce\n"); - return false; - } - myNonce = avLine; - } else if(varName.equals(F("opaque"))){ - if(opaque != NULL && !avLine.equals(opaque)){ - //os_printf("AUTH FAIL: opaque\n"); - return false; - } - } else if(varName.equals(F("uri"))){ - if(uri != NULL && !avLine.equals(uri)){ - //os_printf("AUTH FAIL: uri\n"); - return false; - } - myUri = avLine; - } else if(varName.equals(F("response"))){ - myResponse = avLine; - } else if(varName.equals(F("qop"))){ - myQop = avLine; - } else if(varName.equals(F("nc"))){ - myNc = avLine; - } else if(varName.equals(F("cnonce"))){ - myCnonce = avLine; - } - } while(nextBreak > 0); - - String ha1 = (passwordIsHash) ? String(password) : stringMD5(myUsername + ':' + myRealm + ':' + String(password)); - String ha2 = String(method) + ':' + myUri; - String response = ha1 + ':' + myNonce + ':' + myNc + ':' + myCnonce + ':' + myQop + ':' + stringMD5(ha2); - - if(myResponse.equals(stringMD5(response))){ - //os_printf("AUTH SUCCESS\n"); - return true; - } - - //os_printf("AUTH FAIL: password\n"); - return false; -} +/* + Asynchronous WebServer library for Espressif MCUs + + Copyright (c) 2016 Hristo Gochkov. All rights reserved. + This file is part of the esp8266 core for Arduino environment. + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ +#include "WebAuthentication.h" +#include +#ifdef ESP32 +#include "mbedtls/md5.h" +#else +#include "md5.h" +#endif + + +// Basic Auth hash = base64("username:password") + +bool checkBasicAuthentication(const char * hash, const char * username, const char * password){ + if(username == NULL || password == NULL || hash == NULL) + return false; + + size_t toencodeLen = strlen(username)+strlen(password)+1; + size_t encodedLen = base64_encode_expected_len(toencodeLen); + if(strlen(hash) != encodedLen) + return false; + + char *toencode = new char[toencodeLen+1]; + if(toencode == NULL){ + return false; + } + char *encoded = new char[base64_encode_expected_len(toencodeLen)+1]; + if(encoded == NULL){ + delete[] toencode; + return false; + } + sprintf_P(toencode, PSTR("%s:%s"), username, password); + if(base64_encode_chars(toencode, toencodeLen, encoded) > 0 && memcmp(hash, encoded, encodedLen) == 0){ + delete[] toencode; + delete[] encoded; + return true; + } + delete[] toencode; + delete[] encoded; + return false; +} + +static bool getMD5(uint8_t * data, uint16_t len, char * output){//33 bytes or more +#ifdef ESP32 + mbedtls_md5_context _ctx; +#else + md5_context_t _ctx; +#endif + uint8_t i; + uint8_t * _buf = (uint8_t*)malloc(16); + if(_buf == NULL) + return false; + memset(_buf, 0x00, 16); +#ifdef ESP32 + mbedtls_md5_init(&_ctx); + mbedtls_md5_update_ret (&_ctx,data,len); + mbedtls_md5_finish_ret(&_ctx,data); + mbedtls_internal_md5_process( &_ctx ,data); +#else + MD5Init(&_ctx); + MD5Update(&_ctx, data, len); + MD5Final(_buf, &_ctx); +#endif + for(i = 0; i < 16; i++) { + sprintf_P(output + (i * 2), PSTR("%02x"), _buf[i]); + } + free(_buf); + return true; +} + +static String genRandomMD5(){ +#ifdef ESP8266 + uint32_t r = RANDOM_REG32; +#else + uint32_t r = rand(); +#endif + char * out = (char*)malloc(33); + if(out == NULL || !getMD5((uint8_t*)(&r), 4, out)) + return emptyString; + String res = String(out); + free(out); + return res; +} + +static String stringMD5(const String& in){ + char * out = (char*)malloc(33); + if(out == NULL || !getMD5((uint8_t*)(in.c_str()), in.length(), out)) + return emptyString; + String res = String(out); + free(out); + return res; +} + +String generateDigestHash(const char * username, const char * password, const char * realm){ + if(username == NULL || password == NULL || realm == NULL){ + return emptyString; + } + char * out = (char*)malloc(33); + String res = String(username); + res += ':'; + res.concat(realm); + res += ':'; + String in = res; + in.concat(password); + if(out == NULL || !getMD5((uint8_t*)(in.c_str()), in.length(), out)) + return emptyString; + res.concat(out); + free(out); + return res; +} + +String requestDigestAuthentication(const char * realm){ + String header = F("realm=\""); + if(realm == NULL) + header.concat(F("asyncesp")); + else + header.concat(realm); + header.concat(F("\", qop=\"auth\", nonce=\"")); + header.concat(genRandomMD5()); + header.concat(F("\", opaque=\"")); + header.concat(genRandomMD5()); + header += '"'; + return header; +} + +bool checkDigestAuthentication(const char * header, const char *method, const char * username, const char * password, const char * realm, bool passwordIsHash, const char * nonce, const char * opaque, const char * uri){ + if(username == NULL || password == NULL || header == NULL || method == NULL){ + //os_printf("AUTH FAIL: missing requred fields\n"); + return false; + } + + String myHeader = String(header); + int nextBreak = myHeader.indexOf(','); + if(nextBreak < 0){ + //os_printf("AUTH FAIL: no variables\n"); + return false; + } + + String myUsername = String(); + String myRealm = String(); + String myNonce = String(); + String myUri = String(); + String myResponse = String(); + String myQop = String(); + String myNc = String(); + String myCnonce = String(); + + myHeader += F(", "); + do { + String avLine = myHeader.substring(0, nextBreak); + avLine.trim(); + myHeader = myHeader.substring(nextBreak+1); + nextBreak = myHeader.indexOf(','); + + int eqSign = avLine.indexOf('='); + if(eqSign < 0){ + //os_printf("AUTH FAIL: no = sign\n"); + return false; + } + String varName = avLine.substring(0, eqSign); + avLine = avLine.substring(eqSign + 1); + if(avLine.startsWith(String('"'))){ + avLine = avLine.substring(1, avLine.length() - 1); + } + + if(varName.equals(F("username"))){ + if(!avLine.equals(username)){ + //os_printf("AUTH FAIL: username\n"); + return false; + } + myUsername = avLine; + } else if(varName.equals(F("realm"))){ + if(realm != NULL && !avLine.equals(realm)){ + //os_printf("AUTH FAIL: realm\n"); + return false; + } + myRealm = avLine; + } else if(varName.equals(F("nonce"))){ + if(nonce != NULL && !avLine.equals(nonce)){ + //os_printf("AUTH FAIL: nonce\n"); + return false; + } + myNonce = avLine; + } else if(varName.equals(F("opaque"))){ + if(opaque != NULL && !avLine.equals(opaque)){ + //os_printf("AUTH FAIL: opaque\n"); + return false; + } + } else if(varName.equals(F("uri"))){ + if(uri != NULL && !avLine.equals(uri)){ + //os_printf("AUTH FAIL: uri\n"); + return false; + } + myUri = avLine; + } else if(varName.equals(F("response"))){ + myResponse = avLine; + } else if(varName.equals(F("qop"))){ + myQop = avLine; + } else if(varName.equals(F("nc"))){ + myNc = avLine; + } else if(varName.equals(F("cnonce"))){ + myCnonce = avLine; + } + } while(nextBreak > 0); + + String ha1 = (passwordIsHash) ? String(password) : stringMD5(myUsername + ':' + myRealm + ':' + String(password)); + String ha2 = String(method) + ':' + myUri; + String response = ha1 + ':' + myNonce + ':' + myNc + ':' + myCnonce + ':' + myQop + ':' + stringMD5(ha2); + + if(myResponse.equals(stringMD5(response))){ + //os_printf("AUTH SUCCESS\n"); + return true; + } + + //os_printf("AUTH FAIL: password\n"); + return false; +} diff --git a/lib/ESPAsyncWebServer/WebAuthentication.h b/lib/ESPAsyncWebServer/WebAuthentication.h index a6f1966e3..951f36a22 100644 --- a/lib/ESPAsyncWebServer/WebAuthentication.h +++ b/lib/ESPAsyncWebServer/WebAuthentication.h @@ -26,7 +26,7 @@ bool checkBasicAuthentication(const char * header, const char * username, const char * password); String requestDigestAuthentication(const char * realm); -bool checkDigestAuthentication(const char * header, const __FlashStringHelper *method, const char * username, const char * password, const char * realm, bool passwordIsHash, const char * nonce, const char * opaque, const char * uri); +bool checkDigestAuthentication(const char * header, const char *method, const char * username, const char * password, const char * realm, bool passwordIsHash, const char * nonce, const char * opaque, const char * uri); //for storing hashed versions on the device that can be authenticated against String generateDigestHash(const char * username, const char * password, const char * realm); diff --git a/lib/ESPAsyncWebServer/WebRequest.cpp b/lib/ESPAsyncWebServer/WebRequest.cpp index 9acec396a..ffe2c2623 100644 --- a/lib/ESPAsyncWebServer/WebRequest.cpp +++ b/lib/ESPAsyncWebServer/WebRequest.cpp @@ -1,930 +1,930 @@ -/* - Asynchronous WebServer library for Espressif MCUs - - Copyright (c) 2016 Hristo Gochkov. All rights reserved. - This file is part of the esp8266 core for Arduino environment. - - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Lesser General Public - License as published by the Free Software Foundation; either - version 2.1 of the License, or (at your option) any later version. - - This library is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - Lesser General Public License for more details. - - You should have received a copy of the GNU Lesser General Public - License along with this library; if not, write to the Free Software - Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -*/ -#include "ESPAsyncWebServer.h" -#include "WebResponseImpl.h" -#include "WebAuthentication.h" - -#ifndef ESP8266 -#define os_strlen strlen -#endif - -#define __is_param_char(c) ((c) && ((c) != '{') && ((c) != '[') && ((c) != '&') && ((c) != '=')) - -enum { PARSE_REQ_START, PARSE_REQ_HEADERS, PARSE_REQ_BODY, PARSE_REQ_END, PARSE_REQ_FAIL }; - -AsyncWebServerRequest::AsyncWebServerRequest(AsyncWebServer* s, AsyncClient* c) - : _client(c) - , _server(s) - , _handler(NULL) - , _response(NULL) - , _temp() - , _parseState(0) - , _version(0) - , _method(HTTP_ANY) - , _url() - , _host() - , _contentType() - , _boundary() - , _authorization() - , _reqconntype(RCT_HTTP) - , _isDigest(false) - , _isMultipart(false) - , _isPlainPost(false) - , _expectingContinue(false) - , _contentLength(0) - , _parsedLength(0) - , _headers(LinkedList([](AsyncWebHeader *h){ delete h; })) - , _params(LinkedList([](AsyncWebParameter *p){ delete p; })) - , _pathParams(LinkedList([](String *p){ delete p; })) - , _multiParseState(0) - , _boundaryPosition(0) - , _itemStartIndex(0) - , _itemSize(0) - , _itemName() - , _itemFilename() - , _itemType() - , _itemValue() - , _itemBuffer(0) - , _itemBufferIndex(0) - , _itemIsFile(false) - , _tempObject(NULL) -{ - c->onError([](void *r, AsyncClient* c, int8_t error){ (void)c; AsyncWebServerRequest *req = (AsyncWebServerRequest*)r; req->_onError(error); }, this); - c->onAck([](void *r, AsyncClient* c, size_t len, uint32_t time){ (void)c; AsyncWebServerRequest *req = (AsyncWebServerRequest*)r; req->_onAck(len, time); }, this); - c->onDisconnect([](void *r, AsyncClient* c){ AsyncWebServerRequest *req = (AsyncWebServerRequest*)r; req->_onDisconnect(); delete c; }, this); - c->onTimeout([](void *r, AsyncClient* c, uint32_t time){ (void)c; AsyncWebServerRequest *req = (AsyncWebServerRequest*)r; req->_onTimeout(time); }, this); - c->onData([](void *r, AsyncClient* c, void *buf, size_t len){ (void)c; AsyncWebServerRequest *req = (AsyncWebServerRequest*)r; req->_onData(buf, len); }, this); - c->onPoll([](void *r, AsyncClient* c){ (void)c; AsyncWebServerRequest *req = ( AsyncWebServerRequest*)r; req->_onPoll(); }, this); -} - -AsyncWebServerRequest::~AsyncWebServerRequest(){ - _headers.free(); - - _params.free(); - _pathParams.free(); - - _interestingHeaders.free(); - - if(_response != NULL){ - delete _response; - } - - if(_tempObject != NULL){ - free(_tempObject); - } - - if(_tempFile){ - _tempFile.close(); - } -} - -void AsyncWebServerRequest::_onData(void *buf, size_t len){ - size_t i = 0; - while (true) { - - if(_parseState < PARSE_REQ_BODY){ - // Find new line in buf - char *str = (char*)buf; - for (i = 0; i < len; i++) { - if (str[i] == '\n') { - break; - } - } - if (i == len) { // No new line, just add the buffer in _temp - char ch = str[len-1]; - str[len-1] = 0; - _temp.reserve(_temp.length()+len); - _temp.concat(str); - _temp.concat(ch); - } else { // Found new line - extract it and parse - str[i] = 0; // Terminate the string at the end of the line. - _temp.concat(str); - _temp.trim(); - _parseLine(); - if (++i < len) { - // Still have more buffer to process - buf = str+i; - len-= i; - continue; - } - } - } else if(_parseState == PARSE_REQ_BODY){ - // A handler should be already attached at this point in _parseLine function. - // If handler does nothing (_onRequest is NULL), we don't need to really parse the body. - const bool needParse = _handler && !_handler->isRequestHandlerTrivial(); - if(_isMultipart){ - if(needParse){ - size_t i; - for(i=0; i end) equal = end; - String name = params.substring(start, equal); - String value = equal + 1 < end ? params.substring(equal + 1, end) : String(); - _addParam(new AsyncWebParameter(urlDecode(name), urlDecode(value))); - start = end + 1; - } -} - -bool AsyncWebServerRequest::_parseReqHead(){ - // Split the head into method, url and version - int index = _temp.indexOf(' '); - String m = _temp.substring(0, index); - index = _temp.indexOf(' ', index+1); - String u = _temp.substring(m.length()+1, index); - _temp = _temp.substring(index+1); - - if(m == F("GET")){ - _method = HTTP_GET; - } else if(m == F("POST")){ - _method = HTTP_POST; - } else if(m == F("DELETE")){ - _method = HTTP_DELETE; - } else if(m == F("PUT")){ - _method = HTTP_PUT; - } else if(m == F("PATCH")){ - _method = HTTP_PATCH; - } else if(m == F("HEAD")){ - _method = HTTP_HEAD; - } else if(m == F("OPTIONS")){ - _method = HTTP_OPTIONS; - } - - String g; - index = u.indexOf('?'); - if(index > 0){ - g = u.substring(index +1); - u = u.substring(0, index); - } - _url = urlDecode(u); - _addGetParams(g); - - if(!_temp.startsWith(F("HTTP/1.0"))) - _version = 1; - - _temp = String(); - return true; -} - -bool strContains(const String &src, const String &find, bool mindcase = true) { - int pos=0, i=0; - const int slen = src.length(); - const int flen = find.length(); - - if (slen < flen) return false; - while (pos <= (slen - flen)) { - for (i=0; i < flen; i++) { - if (mindcase) { - if (src[pos+i] != find[i]) i = flen + 1; // no match - } - else if (tolower(src[pos+i]) != tolower(find[i])) { - i = flen + 1; // no match - } - } - if (i == flen) return true; - pos++; - } - return false; -} - -bool AsyncWebServerRequest::_parseReqHeader(){ - int index = _temp.indexOf(':'); - if(index){ - String name = _temp.substring(0, index); - String value = _temp.substring(index + 2); - if(name.equalsIgnoreCase("Host")){ - _host = value; - } else if(name.equalsIgnoreCase(F("Content-Type"))){ - _contentType = value.substring(0, value.indexOf(';')); - if (value.startsWith(F("multipart/"))){ - _boundary = value.substring(value.indexOf('=')+1); - _boundary.replace(String('"'), String()); - _isMultipart = true; - } - } else if(name.equalsIgnoreCase(F("Content-Length"))){ - _contentLength = atoi(value.c_str()); - } else if(name.equalsIgnoreCase(F("Expect")) && value == F("100-continue")){ - _expectingContinue = true; - } else if(name.equalsIgnoreCase(F("Authorization"))){ - if(value.length() > 5 && value.substring(0,5).equalsIgnoreCase(F("Basic"))){ - _authorization = value.substring(6); - } else if(value.length() > 6 && value.substring(0,6).equalsIgnoreCase(F("Digest"))){ - _isDigest = true; - _authorization = value.substring(7); - } - } else { - if(name.equalsIgnoreCase(F("Upgrade")) && value.equalsIgnoreCase(F("websocket"))){ - // WebSocket request can be uniquely identified by header: [Upgrade: websocket] - _reqconntype = RCT_WS; - } else { - if(name.equalsIgnoreCase(F("Accept")) && strContains(value, F("text/event-stream"), false)){ - // WebEvent request can be uniquely identified by header: [Accept: text/event-stream] - _reqconntype = RCT_EVENT; - } - } - } - _headers.add(new AsyncWebHeader(name, value)); - } - _temp = String(); - return true; -} - -void AsyncWebServerRequest::_parsePlainPostChar(uint8_t data){ - if(data && (char)data != '&') - _temp += (char)data; - if(!data || (char)data == '&' || _parsedLength == _contentLength){ - String name = F("body"); - String value = _temp; - if(!_temp.startsWith(String('{')) && !_temp.startsWith(String('[')) && _temp.indexOf('=') > 0){ - name = _temp.substring(0, _temp.indexOf('=')); - value = _temp.substring(_temp.indexOf('=') + 1); - } - _addParam(new AsyncWebParameter(urlDecode(name), urlDecode(value), true)); - _temp = String(); - } -} - -void AsyncWebServerRequest::_handleUploadByte(uint8_t data, bool last){ - _itemBuffer[_itemBufferIndex++] = data; - - if(last || _itemBufferIndex == 1460){ - //check if authenticated before calling the upload - if(_handler) - _handler->handleUpload(this, _itemFilename, _itemSize - _itemBufferIndex, _itemBuffer, _itemBufferIndex, false); - _itemBufferIndex = 0; - } -} - -enum { - EXPECT_BOUNDARY, - PARSE_HEADERS, - WAIT_FOR_RETURN1, - EXPECT_FEED1, - EXPECT_DASH1, - EXPECT_DASH2, - BOUNDARY_OR_DATA, - DASH3_OR_RETURN2, - EXPECT_FEED2, - PARSING_FINISHED, - PARSE_ERROR -}; - -void AsyncWebServerRequest::_parseMultipartPostByte(uint8_t data, bool last){ -#define itemWriteByte(b) do { _itemSize++; if(_itemIsFile) _handleUploadByte(b, last); else _itemValue+=(char)(b); } while(0) - - if(!_parsedLength){ - _multiParseState = EXPECT_BOUNDARY; - _temp = String(); - _itemName = String(); - _itemFilename = String(); - _itemType = String(); - } - - if(_multiParseState == WAIT_FOR_RETURN1){ - if(data != '\r'){ - itemWriteByte(data); - } else { - _multiParseState = EXPECT_FEED1; - } - } else if(_multiParseState == EXPECT_BOUNDARY){ - if(_parsedLength < 2 && data != '-'){ - _multiParseState = PARSE_ERROR; - return; - } else if(_parsedLength - 2 < _boundary.length() && _boundary.c_str()[_parsedLength - 2] != data){ - _multiParseState = PARSE_ERROR; - return; - } else if(_parsedLength - 2 == _boundary.length() && data != '\r'){ - _multiParseState = PARSE_ERROR; - return; - } else if(_parsedLength - 3 == _boundary.length()){ - if(data != '\n'){ - _multiParseState = PARSE_ERROR; - return; - } - _multiParseState = PARSE_HEADERS; - _itemIsFile = false; - } - } else if(_multiParseState == PARSE_HEADERS){ - if((char)data != '\r' && (char)data != '\n') - _temp += (char)data; - if((char)data == '\n'){ - if(_temp.length()){ - if(_temp.length() > 12 && _temp.substring(0, 12).equalsIgnoreCase(F("Content-Type"))){ - _itemType = _temp.substring(14); - _itemIsFile = true; - } else if(_temp.length() > 19 && _temp.substring(0, 19).equalsIgnoreCase(F("Content-Disposition"))){ - _temp = _temp.substring(_temp.indexOf(';') + 2); - while(_temp.indexOf(';') > 0){ - String name = _temp.substring(0, _temp.indexOf('=')); - String nameVal = _temp.substring(_temp.indexOf('=') + 2, _temp.indexOf(';') - 1); - if(name == F("name")){ - _itemName = nameVal; - } else if(name == F("filename")){ - _itemFilename = nameVal; - _itemIsFile = true; - } - _temp = _temp.substring(_temp.indexOf(';') + 2); - } - String name = _temp.substring(0, _temp.indexOf('=')); - String nameVal = _temp.substring(_temp.indexOf('=') + 2, _temp.length() - 1); - if(name == F("name")){ - _itemName = nameVal; - } else if(name == F("filename")){ - _itemFilename = nameVal; - _itemIsFile = true; - } - } - _temp = String(); - } else { - _multiParseState = WAIT_FOR_RETURN1; - //value starts from here - _itemSize = 0; - _itemStartIndex = _parsedLength; - _itemValue = String(); - if(_itemIsFile){ - if(_itemBuffer) - free(_itemBuffer); - _itemBuffer = (uint8_t*)malloc(1460); - if(_itemBuffer == NULL){ - _multiParseState = PARSE_ERROR; - return; - } - _itemBufferIndex = 0; - } - } - } - } else if(_multiParseState == EXPECT_FEED1){ - if(data != '\n'){ - _multiParseState = WAIT_FOR_RETURN1; - itemWriteByte('\r'); _parseMultipartPostByte(data, last); - } else { - _multiParseState = EXPECT_DASH1; - } - } else if(_multiParseState == EXPECT_DASH1){ - if(data != '-'){ - _multiParseState = WAIT_FOR_RETURN1; - itemWriteByte('\r'); itemWriteByte('\n'); _parseMultipartPostByte(data, last); - } else { - _multiParseState = EXPECT_DASH2; - } - } else if(_multiParseState == EXPECT_DASH2){ - if(data != '-'){ - _multiParseState = WAIT_FOR_RETURN1; - itemWriteByte('\r'); itemWriteByte('\n'); itemWriteByte('-'); _parseMultipartPostByte(data, last); - } else { - _multiParseState = BOUNDARY_OR_DATA; - _boundaryPosition = 0; - } - } else if(_multiParseState == BOUNDARY_OR_DATA){ - if(_boundaryPosition < _boundary.length() && _boundary.c_str()[_boundaryPosition] != data){ - _multiParseState = WAIT_FOR_RETURN1; - itemWriteByte('\r'); itemWriteByte('\n'); itemWriteByte('-'); itemWriteByte('-'); - uint8_t i; - for(i=0; i<_boundaryPosition; i++) - itemWriteByte(_boundary.c_str()[i]); - _parseMultipartPostByte(data, last); - } else if(_boundaryPosition == _boundary.length() - 1){ - _multiParseState = DASH3_OR_RETURN2; - if(!_itemIsFile){ - _addParam(new AsyncWebParameter(_itemName, _itemValue, true)); - } else { - if(_itemSize){ - //check if authenticated before calling the upload - if(_handler) _handler->handleUpload(this, _itemFilename, _itemSize - _itemBufferIndex, _itemBuffer, _itemBufferIndex, true); - _itemBufferIndex = 0; - _addParam(new AsyncWebParameter(_itemName, _itemFilename, true, true, _itemSize)); - } - free(_itemBuffer); - _itemBuffer = NULL; - } - - } else { - _boundaryPosition++; - } - } else if(_multiParseState == DASH3_OR_RETURN2){ - if(data == '-' && (_contentLength - _parsedLength - 4) != 0){ - //os_printf("ERROR: The parser got to the end of the POST but is expecting %u bytes more!\nDrop an issue so we can have more info on the matter!\n", _contentLength - _parsedLength - 4); - _contentLength = _parsedLength + 4;//lets close the request gracefully - } - if(data == '\r'){ - _multiParseState = EXPECT_FEED2; - } else if(data == '-' && _contentLength == (_parsedLength + 4)){ - _multiParseState = PARSING_FINISHED; - } else { - _multiParseState = WAIT_FOR_RETURN1; - itemWriteByte('\r'); itemWriteByte('\n'); itemWriteByte('-'); itemWriteByte('-'); - uint8_t i; for(i=0; i<_boundary.length(); i++) itemWriteByte(_boundary.c_str()[i]); - _parseMultipartPostByte(data, last); - } - } else if(_multiParseState == EXPECT_FEED2){ - if(data == '\n'){ - _multiParseState = PARSE_HEADERS; - _itemIsFile = false; - } else { - _multiParseState = WAIT_FOR_RETURN1; - itemWriteByte('\r'); itemWriteByte('\n'); itemWriteByte('-'); itemWriteByte('-'); - uint8_t i; for(i=0; i<_boundary.length(); i++) itemWriteByte(_boundary.c_str()[i]); - itemWriteByte('\r'); _parseMultipartPostByte(data, last); - } - } -} - -void AsyncWebServerRequest::_parseLine(){ - if(_parseState == PARSE_REQ_START){ - if(!_temp.length()){ - _parseState = PARSE_REQ_FAIL; - _client->close(); - } else { - _parseReqHead(); - _parseState = PARSE_REQ_HEADERS; - } - return; - } - - if(_parseState == PARSE_REQ_HEADERS){ - if(!_temp.length()){ - //end of headers - _server->_rewriteRequest(this); - _server->_attachHandler(this); - _removeNotInterestingHeaders(); - if(_expectingContinue){ - String response = F("HTTP/1.1 100 Continue\r\n\r\n"); - _client->write(response.c_str(), response.length()); - } - //check handler for authentication - if(_contentLength){ - _parseState = PARSE_REQ_BODY; - } else { - _parseState = PARSE_REQ_END; - if(_handler) _handler->handleRequest(this); - else send(501); - } - } else _parseReqHeader(); - } -} - -size_t AsyncWebServerRequest::headers() const{ - return _headers.length(); -} - -bool AsyncWebServerRequest::hasHeader(const String& name) const { - for(const auto& h: _headers){ - if(h->name().equalsIgnoreCase(name)){ - return true; - } - } - return false; -} - -bool AsyncWebServerRequest::hasHeader(const __FlashStringHelper * data) const { - return hasHeader(String(data)); -} - -AsyncWebHeader* AsyncWebServerRequest::getHeader(const String& name) const { - for(const auto& h: _headers){ - if(h->name().equalsIgnoreCase(name)){ - return h; - } - } - return nullptr; -} - -AsyncWebHeader* AsyncWebServerRequest::getHeader(const __FlashStringHelper * data) const { - return getHeader(String(data)); -} - -AsyncWebHeader* AsyncWebServerRequest::getHeader(size_t num) const { - auto header = _headers.nth(num); - return header ? *header : nullptr; -} - -size_t AsyncWebServerRequest::params() const { - return _params.length(); -} - -bool AsyncWebServerRequest::hasParam(const String& name, bool post, bool file) const { - for(const auto& p: _params){ - if(p->name() == name && p->isPost() == post && p->isFile() == file){ - return true; - } - } - return false; -} - -bool AsyncWebServerRequest::hasParam(const __FlashStringHelper * data, bool post, bool file) const { - return hasParam(String(data).c_str(), post, file); -} - -AsyncWebParameter* AsyncWebServerRequest::getParam(const String& name, bool post, bool file) const { - for(const auto& p: _params){ - if(p->name() == name && p->isPost() == post && p->isFile() == file){ - return p; - } - } - return nullptr; -} - -AsyncWebParameter* AsyncWebServerRequest::getParam(const __FlashStringHelper * data, bool post, bool file) const { - return getParam(String(data).c_str(), post, file); -} - -AsyncWebParameter* AsyncWebServerRequest::getParam(size_t num) const { - auto param = _params.nth(num); - return param ? *param : nullptr; -} - -void AsyncWebServerRequest::addInterestingHeader(const String& name){ - if(!_interestingHeaders.containsIgnoreCase(name)) - _interestingHeaders.add(name); -} - -void AsyncWebServerRequest::send(AsyncWebServerResponse *response){ - _response = response; - if(_response == NULL){ - _client->close(true); - _onDisconnect(); - return; - } - if(!_response->_sourceValid()){ - delete response; - _response = NULL; - send(500); - } - else { - _client->setRxTimeout(0); - _response->_respond(this); - } -} - -AsyncWebServerResponse * AsyncWebServerRequest::beginResponse(int code, const String& contentType, const String& content){ - return new AsyncBasicResponse(code, contentType, content); -} - -AsyncWebServerResponse * AsyncWebServerRequest::beginResponse(FS &fs, const String& path, const String& contentType, bool download, AwsTemplateProcessor callback){ - if(fs.exists(path) || (!download && fs.exists(path+F(".gz")))) - return new AsyncFileResponse(fs, path, contentType, download, callback); - return NULL; -} - -AsyncWebServerResponse * AsyncWebServerRequest::beginResponse(File content, const String& path, const String& contentType, bool download, AwsTemplateProcessor callback){ - if(content == true) - return new AsyncFileResponse(content, path, contentType, download, callback); - return NULL; -} - -AsyncWebServerResponse * AsyncWebServerRequest::beginResponse(Stream &stream, const String& contentType, size_t len, AwsTemplateProcessor callback){ - return new AsyncStreamResponse(stream, contentType, len, callback); -} - -AsyncWebServerResponse * AsyncWebServerRequest::beginResponse(const String& contentType, size_t len, AwsResponseFiller callback, AwsTemplateProcessor templateCallback){ - return new AsyncCallbackResponse(contentType, len, callback, templateCallback); -} - -AsyncWebServerResponse * AsyncWebServerRequest::beginChunkedResponse(const String& contentType, AwsResponseFiller callback, AwsTemplateProcessor templateCallback){ - if(_version) - return new AsyncChunkedResponse(contentType, callback, templateCallback); - return new AsyncCallbackResponse(contentType, 0, callback, templateCallback); -} - -AsyncResponseStream * AsyncWebServerRequest::beginResponseStream(const String& contentType, size_t bufferSize){ - return new AsyncResponseStream(contentType, bufferSize); -} - -AsyncWebServerResponse * AsyncWebServerRequest::beginResponse_P(int code, const String& contentType, const uint8_t * content, size_t len, AwsTemplateProcessor callback){ - return new AsyncProgmemResponse(code, contentType, content, len, callback); -} - -AsyncWebServerResponse * AsyncWebServerRequest::beginResponse_P(int code, const String& contentType, PGM_P content, AwsTemplateProcessor callback){ - return beginResponse_P(code, contentType, (const uint8_t *)content, strlen_P(content), callback); -} - -void AsyncWebServerRequest::send(int code, const String& contentType, const String& content){ - send(beginResponse(code, contentType, content)); -} - -void AsyncWebServerRequest::send(FS &fs, const String& path, const String& contentType, bool download, AwsTemplateProcessor callback){ - if(fs.exists(path) || (!download && fs.exists(path+F(".gz")))){ - send(beginResponse(fs, path, contentType, download, callback)); - } else send(404); -} - -void AsyncWebServerRequest::send(File content, const String& path, const String& contentType, bool download, AwsTemplateProcessor callback){ - if(content == true){ - send(beginResponse(content, path, contentType, download, callback)); - } else send(404); -} - -void AsyncWebServerRequest::send(Stream &stream, const String& contentType, size_t len, AwsTemplateProcessor callback){ - send(beginResponse(stream, contentType, len, callback)); -} - -void AsyncWebServerRequest::send(const String& contentType, size_t len, AwsResponseFiller callback, AwsTemplateProcessor templateCallback){ - send(beginResponse(contentType, len, callback, templateCallback)); -} - -void AsyncWebServerRequest::sendChunked(const String& contentType, AwsResponseFiller callback, AwsTemplateProcessor templateCallback){ - send(beginChunkedResponse(contentType, callback, templateCallback)); -} - -void AsyncWebServerRequest::send_P(int code, const String& contentType, const uint8_t * content, size_t len, AwsTemplateProcessor callback){ - send(beginResponse_P(code, contentType, content, len, callback)); -} - -void AsyncWebServerRequest::send_P(int code, const String& contentType, PGM_P content, AwsTemplateProcessor callback){ - send(beginResponse_P(code, contentType, content, callback)); -} - -void AsyncWebServerRequest::redirect(const String& url){ - AsyncWebServerResponse * response = beginResponse(302); - response->addHeader(F("Location"), url); - send(response); -} - -bool AsyncWebServerRequest::authenticate(const char * username, const char * password, const char * realm, bool passwordIsHash){ - if(_authorization.length()){ - if(_isDigest) - return checkDigestAuthentication(_authorization.c_str(), methodToString(), username, password, realm, passwordIsHash, NULL, NULL, NULL); - else if(!passwordIsHash) - return checkBasicAuthentication(_authorization.c_str(), username, password); - else - return _authorization.equals(password); - } - return false; -} - -bool AsyncWebServerRequest::authenticate(const char * hash){ - if(!_authorization.length() || hash == NULL) - return false; - - if(_isDigest){ - String hStr = String(hash); - int separator = hStr.indexOf(':'); - if(separator <= 0) - return false; - String username = hStr.substring(0, separator); - hStr = hStr.substring(separator + 1); - separator = hStr.indexOf(':'); - if(separator <= 0) - return false; - String realm = hStr.substring(0, separator); - hStr = hStr.substring(separator + 1); - return checkDigestAuthentication(_authorization.c_str(), methodToString(), username.c_str(), hStr.c_str(), realm.c_str(), true, NULL, NULL, NULL); - } - - return (_authorization.equals(hash)); -} - -void AsyncWebServerRequest::requestAuthentication(const char * realm, bool isDigest){ - AsyncWebServerResponse * r = beginResponse(401); - if(!isDigest && realm == NULL){ - r->addHeader(F("WWW-Authenticate"), F("Basic realm=\"Login Required\"")); - } else if(!isDigest){ - String header = F("Basic realm=\""); - header.concat(realm); - header += '"'; - r->addHeader(F("WWW-Authenticate"), header); - } else { - String header = F("Digest "); - header.concat(requestDigestAuthentication(realm)); - r->addHeader(F("WWW-Authenticate"), header); - } - send(r); -} - -bool AsyncWebServerRequest::hasArg(const char* name) const { - for(const auto& arg: _params){ - if(arg->name() == name){ - return true; - } - } - return false; -} - -bool AsyncWebServerRequest::hasArg(const __FlashStringHelper * data) const { - return hasArg(String(data).c_str()); -} - - -const String& AsyncWebServerRequest::arg(const String& name) const { - for(const auto& arg: _params){ - if(arg->name() == name){ - return arg->value(); - } - } - return emptyString; -} - -const String& AsyncWebServerRequest::arg(const __FlashStringHelper * data) const { - return arg(String(data).c_str()); -} - -const String& AsyncWebServerRequest::arg(size_t i) const { - return getParam(i)->value(); -} - -const String& AsyncWebServerRequest::argName(size_t i) const { - return getParam(i)->name(); -} - -const String& AsyncWebServerRequest::pathArg(size_t i) const { - auto param = _pathParams.nth(i); - return param ? **param : emptyString; -} - -const String& AsyncWebServerRequest::header(const char* name) const { - AsyncWebHeader* h = getHeader(String(name)); - return h ? h->value() : emptyString; -} - -const String& AsyncWebServerRequest::header(const __FlashStringHelper * data) const { - return header(String(data).c_str()); -}; - - -const String& AsyncWebServerRequest::header(size_t i) const { - AsyncWebHeader* h = getHeader(i); - return h ? h->value() : emptyString; -} - -const String& AsyncWebServerRequest::headerName(size_t i) const { - AsyncWebHeader* h = getHeader(i); - return h ? h->name() : emptyString; -} - -String AsyncWebServerRequest::urlDecode(const String& text) const { - char temp[] = "0x00"; - unsigned int len = text.length(); - unsigned int i = 0; - String decoded = String(); - decoded.reserve(len); // Allocate the string internal buffer - never longer from source text - while (i < len){ - char decodedChar; - char encodedChar = text.charAt(i++); - if ((encodedChar == '%') && (i + 1 < len)){ - temp[2] = text.charAt(i++); - temp[3] = text.charAt(i++); - decodedChar = strtol(temp, NULL, 16); - } else if (encodedChar == '+') { - decodedChar = ' '; - } else { - decodedChar = encodedChar; // normal ascii char - } - decoded.concat(decodedChar); - } - return decoded; -} - - -const __FlashStringHelper *AsyncWebServerRequest::methodToString() const { - if(_method == HTTP_ANY) return F("ANY"); - else if(_method & HTTP_GET) return F("GET"); - else if(_method & HTTP_POST) return F("POST"); - else if(_method & HTTP_DELETE) return F("DELETE"); - else if(_method & HTTP_PUT) return F("PUT"); - else if(_method & HTTP_PATCH) return F("PATCH"); - else if(_method & HTTP_HEAD) return F("HEAD"); - else if(_method & HTTP_OPTIONS) return F("OPTIONS"); - return F("UNKNOWN"); -} - -const __FlashStringHelper *AsyncWebServerRequest::requestedConnTypeToString() const { - switch (_reqconntype) { - case RCT_NOT_USED: return F("RCT_NOT_USED"); - case RCT_DEFAULT: return F("RCT_DEFAULT"); - case RCT_HTTP: return F("RCT_HTTP"); - case RCT_WS: return F("RCT_WS"); - case RCT_EVENT: return F("RCT_EVENT"); - default: return F("ERROR"); - } -} - -bool AsyncWebServerRequest::isExpectedRequestedConnType(RequestedConnectionType erct1, RequestedConnectionType erct2, RequestedConnectionType erct3) { - bool res = false; - if ((erct1 != RCT_NOT_USED) && (erct1 == _reqconntype)) res = true; - if ((erct2 != RCT_NOT_USED) && (erct2 == _reqconntype)) res = true; - if ((erct3 != RCT_NOT_USED) && (erct3 == _reqconntype)) res = true; - return res; -} +/* + Asynchronous WebServer library for Espressif MCUs + + Copyright (c) 2016 Hristo Gochkov. All rights reserved. + This file is part of the esp8266 core for Arduino environment. + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ +#include "ESPAsyncWebServer.h" +#include "WebResponseImpl.h" +#include "WebAuthentication.h" + +#ifndef ESP8266 +#define os_strlen strlen +#endif + +#define __is_param_char(c) ((c) && ((c) != '{') && ((c) != '[') && ((c) != '&') && ((c) != '=')) + +enum { PARSE_REQ_START, PARSE_REQ_HEADERS, PARSE_REQ_BODY, PARSE_REQ_END, PARSE_REQ_FAIL }; + +AsyncWebServerRequest::AsyncWebServerRequest(AsyncWebServer* s, AsyncClient* c) + : _client(c) + , _server(s) + , _handler(NULL) + , _response(NULL) + , _temp() + , _parseState(0) + , _version(0) + , _method(HTTP_ANY) + , _url() + , _host() + , _contentType() + , _boundary() + , _authorization() + , _reqconntype(RCT_HTTP) + , _isDigest(false) + , _isMultipart(false) + , _isPlainPost(false) + , _expectingContinue(false) + , _contentLength(0) + , _parsedLength(0) + , _headers(LinkedList([](AsyncWebHeader *h){ delete h; })) + , _params(LinkedList([](AsyncWebParameter *p){ delete p; })) + , _pathParams(LinkedList([](String *p){ delete p; })) + , _multiParseState(0) + , _boundaryPosition(0) + , _itemStartIndex(0) + , _itemSize(0) + , _itemName() + , _itemFilename() + , _itemType() + , _itemValue() + , _itemBuffer(0) + , _itemBufferIndex(0) + , _itemIsFile(false) + , _tempObject(NULL) +{ + c->onError([](void *r, AsyncClient* c, int8_t error){ (void)c; AsyncWebServerRequest *req = (AsyncWebServerRequest*)r; req->_onError(error); }, this); + c->onAck([](void *r, AsyncClient* c, size_t len, uint32_t time){ (void)c; AsyncWebServerRequest *req = (AsyncWebServerRequest*)r; req->_onAck(len, time); }, this); + c->onDisconnect([](void *r, AsyncClient* c){ AsyncWebServerRequest *req = (AsyncWebServerRequest*)r; req->_onDisconnect(); delete c; }, this); + c->onTimeout([](void *r, AsyncClient* c, uint32_t time){ (void)c; AsyncWebServerRequest *req = (AsyncWebServerRequest*)r; req->_onTimeout(time); }, this); + c->onData([](void *r, AsyncClient* c, void *buf, size_t len){ (void)c; AsyncWebServerRequest *req = (AsyncWebServerRequest*)r; req->_onData(buf, len); }, this); + c->onPoll([](void *r, AsyncClient* c){ (void)c; AsyncWebServerRequest *req = ( AsyncWebServerRequest*)r; req->_onPoll(); }, this); +} + +AsyncWebServerRequest::~AsyncWebServerRequest(){ + _headers.free(); + + _params.free(); + _pathParams.free(); + + _interestingHeaders.free(); + + if(_response != NULL){ + delete _response; + } + + if(_tempObject != NULL){ + free(_tempObject); + } + + if(_tempFile){ + _tempFile.close(); + } +} + +void AsyncWebServerRequest::_onData(void *buf, size_t len){ + size_t i = 0; + while (true) { + + if(_parseState < PARSE_REQ_BODY){ + // Find new line in buf + char *str = (char*)buf; + for (i = 0; i < len; i++) { + if (str[i] == '\n') { + break; + } + } + if (i == len) { // No new line, just add the buffer in _temp + char ch = str[len-1]; + str[len-1] = 0; + _temp.reserve(_temp.length()+len); + _temp.concat(str); + _temp.concat(ch); + } else { // Found new line - extract it and parse + str[i] = 0; // Terminate the string at the end of the line. + _temp.concat(str); + _temp.trim(); + _parseLine(); + if (++i < len) { + // Still have more buffer to process + buf = str+i; + len-= i; + continue; + } + } + } else if(_parseState == PARSE_REQ_BODY){ + // A handler should be already attached at this point in _parseLine function. + // If handler does nothing (_onRequest is NULL), we don't need to really parse the body. + const bool needParse = _handler && !_handler->isRequestHandlerTrivial(); + if(_isMultipart){ + if(needParse){ + size_t i; + for(i=0; i end) equal = end; + String name = params.substring(start, equal); + String value = equal + 1 < end ? params.substring(equal + 1, end) : String(); + _addParam(new AsyncWebParameter(urlDecode(name), urlDecode(value))); + start = end + 1; + } +} + +bool AsyncWebServerRequest::_parseReqHead(){ + // Split the head into method, url and version + int index = _temp.indexOf(' '); + String m = _temp.substring(0, index); + index = _temp.indexOf(' ', index+1); + String u = _temp.substring(m.length()+1, index); + _temp = _temp.substring(index+1); + + if(m == F("GET")){ + _method = HTTP_GET; + } else if(m == F("POST")){ + _method = HTTP_POST; + } else if(m == F("DELETE")){ + _method = HTTP_DELETE; + } else if(m == F("PUT")){ + _method = HTTP_PUT; + } else if(m == F("PATCH")){ + _method = HTTP_PATCH; + } else if(m == F("HEAD")){ + _method = HTTP_HEAD; + } else if(m == F("OPTIONS")){ + _method = HTTP_OPTIONS; + } + + String g; + index = u.indexOf('?'); + if(index > 0){ + g = u.substring(index +1); + u = u.substring(0, index); + } + _url = urlDecode(u); + _addGetParams(g); + + if(!_temp.startsWith(F("HTTP/1.0"))) + _version = 1; + + _temp = String(); + return true; +} + +bool strContains(const String &src, const String &find, bool mindcase = true) { + int pos=0, i=0; + const int slen = src.length(); + const int flen = find.length(); + + if (slen < flen) return false; + while (pos <= (slen - flen)) { + for (i=0; i < flen; i++) { + if (mindcase) { + if (src[pos+i] != find[i]) i = flen + 1; // no match + } + else if (tolower(src[pos+i]) != tolower(find[i])) { + i = flen + 1; // no match + } + } + if (i == flen) return true; + pos++; + } + return false; +} + +bool AsyncWebServerRequest::_parseReqHeader(){ + int index = _temp.indexOf(':'); + if(index){ + String name = _temp.substring(0, index); + String value = _temp.substring(index + 2); + if(name.equalsIgnoreCase("Host")){ + _host = value; + } else if(name.equalsIgnoreCase(F("Content-Type"))){ + _contentType = value.substring(0, value.indexOf(';')); + if (value.startsWith(F("multipart/"))){ + _boundary = value.substring(value.indexOf('=')+1); + _boundary.replace(String('"'), String()); + _isMultipart = true; + } + } else if(name.equalsIgnoreCase(F("Content-Length"))){ + _contentLength = atoi(value.c_str()); + } else if(name.equalsIgnoreCase(F("Expect")) && value == F("100-continue")){ + _expectingContinue = true; + } else if(name.equalsIgnoreCase(F("Authorization"))){ + if(value.length() > 5 && value.substring(0,5).equalsIgnoreCase(F("Basic"))){ + _authorization = value.substring(6); + } else if(value.length() > 6 && value.substring(0,6).equalsIgnoreCase(F("Digest"))){ + _isDigest = true; + _authorization = value.substring(7); + } + } else { + if(name.equalsIgnoreCase(F("Upgrade")) && value.equalsIgnoreCase(F("websocket"))){ + // WebSocket request can be uniquely identified by header: [Upgrade: websocket] + _reqconntype = RCT_WS; + } else { + if(name.equalsIgnoreCase(F("Accept")) && strContains(value, F("text/event-stream"), false)){ + // WebEvent request can be uniquely identified by header: [Accept: text/event-stream] + _reqconntype = RCT_EVENT; + } + } + } + _headers.add(new AsyncWebHeader(name, value)); + } + _temp = String(); + return true; +} + +void AsyncWebServerRequest::_parsePlainPostChar(uint8_t data){ + if(data && (char)data != '&') + _temp += (char)data; + if(!data || (char)data == '&' || _parsedLength == _contentLength){ + String name = F("body"); + String value = _temp; + if(!_temp.startsWith(String('{')) && !_temp.startsWith(String('[')) && _temp.indexOf('=') > 0){ + name = _temp.substring(0, _temp.indexOf('=')); + value = _temp.substring(_temp.indexOf('=') + 1); + } + _addParam(new AsyncWebParameter(urlDecode(name), urlDecode(value), true)); + _temp = String(); + } +} + +void AsyncWebServerRequest::_handleUploadByte(uint8_t data, bool last){ + _itemBuffer[_itemBufferIndex++] = data; + + if(last || _itemBufferIndex == 1460){ + //check if authenticated before calling the upload + if(_handler) + _handler->handleUpload(this, _itemFilename, _itemSize - _itemBufferIndex, _itemBuffer, _itemBufferIndex, false); + _itemBufferIndex = 0; + } +} + +enum { + EXPECT_BOUNDARY, + PARSE_HEADERS, + WAIT_FOR_RETURN1, + EXPECT_FEED1, + EXPECT_DASH1, + EXPECT_DASH2, + BOUNDARY_OR_DATA, + DASH3_OR_RETURN2, + EXPECT_FEED2, + PARSING_FINISHED, + PARSE_ERROR +}; + +void AsyncWebServerRequest::_parseMultipartPostByte(uint8_t data, bool last){ +#define itemWriteByte(b) do { _itemSize++; if(_itemIsFile) _handleUploadByte(b, last); else _itemValue+=(char)(b); } while(0) + + if(!_parsedLength){ + _multiParseState = EXPECT_BOUNDARY; + _temp = String(); + _itemName = String(); + _itemFilename = String(); + _itemType = String(); + } + + if(_multiParseState == WAIT_FOR_RETURN1){ + if(data != '\r'){ + itemWriteByte(data); + } else { + _multiParseState = EXPECT_FEED1; + } + } else if(_multiParseState == EXPECT_BOUNDARY){ + if(_parsedLength < 2 && data != '-'){ + _multiParseState = PARSE_ERROR; + return; + } else if(_parsedLength - 2 < _boundary.length() && _boundary.c_str()[_parsedLength - 2] != data){ + _multiParseState = PARSE_ERROR; + return; + } else if(_parsedLength - 2 == _boundary.length() && data != '\r'){ + _multiParseState = PARSE_ERROR; + return; + } else if(_parsedLength - 3 == _boundary.length()){ + if(data != '\n'){ + _multiParseState = PARSE_ERROR; + return; + } + _multiParseState = PARSE_HEADERS; + _itemIsFile = false; + } + } else if(_multiParseState == PARSE_HEADERS){ + if((char)data != '\r' && (char)data != '\n') + _temp += (char)data; + if((char)data == '\n'){ + if(_temp.length()){ + if(_temp.length() > 12 && _temp.substring(0, 12).equalsIgnoreCase(F("Content-Type"))){ + _itemType = _temp.substring(14); + _itemIsFile = true; + } else if(_temp.length() > 19 && _temp.substring(0, 19).equalsIgnoreCase(F("Content-Disposition"))){ + _temp = _temp.substring(_temp.indexOf(';') + 2); + while(_temp.indexOf(';') > 0){ + String name = _temp.substring(0, _temp.indexOf('=')); + String nameVal = _temp.substring(_temp.indexOf('=') + 2, _temp.indexOf(';') - 1); + if(name == F("name")){ + _itemName = nameVal; + } else if(name == F("filename")){ + _itemFilename = nameVal; + _itemIsFile = true; + } + _temp = _temp.substring(_temp.indexOf(';') + 2); + } + String name = _temp.substring(0, _temp.indexOf('=')); + String nameVal = _temp.substring(_temp.indexOf('=') + 2, _temp.length() - 1); + if(name == F("name")){ + _itemName = nameVal; + } else if(name == F("filename")){ + _itemFilename = nameVal; + _itemIsFile = true; + } + } + _temp = String(); + } else { + _multiParseState = WAIT_FOR_RETURN1; + //value starts from here + _itemSize = 0; + _itemStartIndex = _parsedLength; + _itemValue = String(); + if(_itemIsFile){ + if(_itemBuffer) + free(_itemBuffer); + _itemBuffer = (uint8_t*)malloc(1460); + if(_itemBuffer == NULL){ + _multiParseState = PARSE_ERROR; + return; + } + _itemBufferIndex = 0; + } + } + } + } else if(_multiParseState == EXPECT_FEED1){ + if(data != '\n'){ + _multiParseState = WAIT_FOR_RETURN1; + itemWriteByte('\r'); _parseMultipartPostByte(data, last); + } else { + _multiParseState = EXPECT_DASH1; + } + } else if(_multiParseState == EXPECT_DASH1){ + if(data != '-'){ + _multiParseState = WAIT_FOR_RETURN1; + itemWriteByte('\r'); itemWriteByte('\n'); _parseMultipartPostByte(data, last); + } else { + _multiParseState = EXPECT_DASH2; + } + } else if(_multiParseState == EXPECT_DASH2){ + if(data != '-'){ + _multiParseState = WAIT_FOR_RETURN1; + itemWriteByte('\r'); itemWriteByte('\n'); itemWriteByte('-'); _parseMultipartPostByte(data, last); + } else { + _multiParseState = BOUNDARY_OR_DATA; + _boundaryPosition = 0; + } + } else if(_multiParseState == BOUNDARY_OR_DATA){ + if(_boundaryPosition < _boundary.length() && _boundary.c_str()[_boundaryPosition] != data){ + _multiParseState = WAIT_FOR_RETURN1; + itemWriteByte('\r'); itemWriteByte('\n'); itemWriteByte('-'); itemWriteByte('-'); + uint8_t i; + for(i=0; i<_boundaryPosition; i++) + itemWriteByte(_boundary.c_str()[i]); + _parseMultipartPostByte(data, last); + } else if(_boundaryPosition == _boundary.length() - 1){ + _multiParseState = DASH3_OR_RETURN2; + if(!_itemIsFile){ + _addParam(new AsyncWebParameter(_itemName, _itemValue, true)); + } else { + if(_itemSize){ + //check if authenticated before calling the upload + if(_handler) _handler->handleUpload(this, _itemFilename, _itemSize - _itemBufferIndex, _itemBuffer, _itemBufferIndex, true); + _itemBufferIndex = 0; + _addParam(new AsyncWebParameter(_itemName, _itemFilename, true, true, _itemSize)); + } + free(_itemBuffer); + _itemBuffer = NULL; + } + + } else { + _boundaryPosition++; + } + } else if(_multiParseState == DASH3_OR_RETURN2){ + if(data == '-' && (_contentLength - _parsedLength - 4) != 0){ + //os_printf("ERROR: The parser got to the end of the POST but is expecting %u bytes more!\nDrop an issue so we can have more info on the matter!\n", _contentLength - _parsedLength - 4); + _contentLength = _parsedLength + 4;//lets close the request gracefully + } + if(data == '\r'){ + _multiParseState = EXPECT_FEED2; + } else if(data == '-' && _contentLength == (_parsedLength + 4)){ + _multiParseState = PARSING_FINISHED; + } else { + _multiParseState = WAIT_FOR_RETURN1; + itemWriteByte('\r'); itemWriteByte('\n'); itemWriteByte('-'); itemWriteByte('-'); + uint8_t i; for(i=0; i<_boundary.length(); i++) itemWriteByte(_boundary.c_str()[i]); + _parseMultipartPostByte(data, last); + } + } else if(_multiParseState == EXPECT_FEED2){ + if(data == '\n'){ + _multiParseState = PARSE_HEADERS; + _itemIsFile = false; + } else { + _multiParseState = WAIT_FOR_RETURN1; + itemWriteByte('\r'); itemWriteByte('\n'); itemWriteByte('-'); itemWriteByte('-'); + uint8_t i; for(i=0; i<_boundary.length(); i++) itemWriteByte(_boundary.c_str()[i]); + itemWriteByte('\r'); _parseMultipartPostByte(data, last); + } + } +} + +void AsyncWebServerRequest::_parseLine(){ + if(_parseState == PARSE_REQ_START){ + if(!_temp.length()){ + _parseState = PARSE_REQ_FAIL; + _client->close(); + } else { + _parseReqHead(); + _parseState = PARSE_REQ_HEADERS; + } + return; + } + + if(_parseState == PARSE_REQ_HEADERS){ + if(!_temp.length()){ + //end of headers + _server->_rewriteRequest(this); + _server->_attachHandler(this); + _removeNotInterestingHeaders(); + if(_expectingContinue){ + String response = F("HTTP/1.1 100 Continue\r\n\r\n"); + _client->write(response.c_str(), response.length()); + } + //check handler for authentication + if(_contentLength){ + _parseState = PARSE_REQ_BODY; + } else { + _parseState = PARSE_REQ_END; + if(_handler) _handler->handleRequest(this); + else send(501); + } + } else _parseReqHeader(); + } +} + +size_t AsyncWebServerRequest::headers() const{ + return _headers.length(); +} + +bool AsyncWebServerRequest::hasHeader(const String& name) const { + for(const auto& h: _headers){ + if(h->name().equalsIgnoreCase(name)){ + return true; + } + } + return false; +} + +bool AsyncWebServerRequest::hasHeader(const __FlashStringHelper * data) const { + return hasHeader(String(data)); +} + +AsyncWebHeader* AsyncWebServerRequest::getHeader(const String& name) const { + for(const auto& h: _headers){ + if(h->name().equalsIgnoreCase(name)){ + return h; + } + } + return nullptr; +} + +AsyncWebHeader* AsyncWebServerRequest::getHeader(const __FlashStringHelper * data) const { + return getHeader(String(data)); +} + +AsyncWebHeader* AsyncWebServerRequest::getHeader(size_t num) const { + auto header = _headers.nth(num); + return header ? *header : nullptr; +} + +size_t AsyncWebServerRequest::params() const { + return _params.length(); +} + +bool AsyncWebServerRequest::hasParam(const String& name, bool post, bool file) const { + for(const auto& p: _params){ + if(p->name() == name && p->isPost() == post && p->isFile() == file){ + return true; + } + } + return false; +} + +bool AsyncWebServerRequest::hasParam(const __FlashStringHelper * data, bool post, bool file) const { + return hasParam(String(data).c_str(), post, file); +} + +AsyncWebParameter* AsyncWebServerRequest::getParam(const String& name, bool post, bool file) const { + for(const auto& p: _params){ + if(p->name() == name && p->isPost() == post && p->isFile() == file){ + return p; + } + } + return nullptr; +} + +AsyncWebParameter* AsyncWebServerRequest::getParam(const __FlashStringHelper * data, bool post, bool file) const { + return getParam(String(data).c_str(), post, file); +} + +AsyncWebParameter* AsyncWebServerRequest::getParam(size_t num) const { + auto param = _params.nth(num); + return param ? *param : nullptr; +} + +void AsyncWebServerRequest::addInterestingHeader(const String& name){ + if(!_interestingHeaders.containsIgnoreCase(name)) + _interestingHeaders.add(name); +} + +void AsyncWebServerRequest::send(AsyncWebServerResponse *response){ + _response = response; + if(_response == NULL){ + _client->close(true); + _onDisconnect(); + return; + } + if(!_response->_sourceValid()){ + delete response; + _response = NULL; + send(500); + } + else { + _client->setRxTimeout(0); + _response->_respond(this); + } +} + +AsyncWebServerResponse * AsyncWebServerRequest::beginResponse(int code, const String& contentType, const String& content){ + return new AsyncBasicResponse(code, contentType, content); +} + +AsyncWebServerResponse * AsyncWebServerRequest::beginResponse(FS &fs, const String& path, const String& contentType, bool download, AwsTemplateProcessor callback){ + if(fs.exists(path) || (!download && fs.exists(path+F(".gz")))) + return new AsyncFileResponse(fs, path, contentType, download, callback); + return NULL; +} + +AsyncWebServerResponse * AsyncWebServerRequest::beginResponse(File content, const String& path, const String& contentType, bool download, AwsTemplateProcessor callback){ + if(content == true) + return new AsyncFileResponse(content, path, contentType, download, callback); + return NULL; +} + +AsyncWebServerResponse * AsyncWebServerRequest::beginResponse(Stream &stream, const String& contentType, size_t len, AwsTemplateProcessor callback){ + return new AsyncStreamResponse(stream, contentType, len, callback); +} + +AsyncWebServerResponse * AsyncWebServerRequest::beginResponse(const String& contentType, size_t len, AwsResponseFiller callback, AwsTemplateProcessor templateCallback){ + return new AsyncCallbackResponse(contentType, len, callback, templateCallback); +} + +AsyncWebServerResponse * AsyncWebServerRequest::beginChunkedResponse(const String& contentType, AwsResponseFiller callback, AwsTemplateProcessor templateCallback){ + if(_version) + return new AsyncChunkedResponse(contentType, callback, templateCallback); + return new AsyncCallbackResponse(contentType, 0, callback, templateCallback); +} + +AsyncResponseStream * AsyncWebServerRequest::beginResponseStream(const String& contentType, size_t bufferSize){ + return new AsyncResponseStream(contentType, bufferSize); +} + +AsyncWebServerResponse * AsyncWebServerRequest::beginResponse_P(int code, const String& contentType, const uint8_t * content, size_t len, AwsTemplateProcessor callback){ + return new AsyncProgmemResponse(code, contentType, content, len, callback); +} + +AsyncWebServerResponse * AsyncWebServerRequest::beginResponse_P(int code, const String& contentType, PGM_P content, AwsTemplateProcessor callback){ + return beginResponse_P(code, contentType, (const uint8_t *)content, strlen_P(content), callback); +} + +void AsyncWebServerRequest::send(int code, const String& contentType, const String& content){ + send(beginResponse(code, contentType, content)); +} + +void AsyncWebServerRequest::send(FS &fs, const String& path, const String& contentType, bool download, AwsTemplateProcessor callback){ + if(fs.exists(path) || (!download && fs.exists(path+F(".gz")))){ + send(beginResponse(fs, path, contentType, download, callback)); + } else send(404); +} + +void AsyncWebServerRequest::send(File content, const String& path, const String& contentType, bool download, AwsTemplateProcessor callback){ + if(content == true){ + send(beginResponse(content, path, contentType, download, callback)); + } else send(404); +} + +void AsyncWebServerRequest::send(Stream &stream, const String& contentType, size_t len, AwsTemplateProcessor callback){ + send(beginResponse(stream, contentType, len, callback)); +} + +void AsyncWebServerRequest::send(const String& contentType, size_t len, AwsResponseFiller callback, AwsTemplateProcessor templateCallback){ + send(beginResponse(contentType, len, callback, templateCallback)); +} + +void AsyncWebServerRequest::sendChunked(const String& contentType, AwsResponseFiller callback, AwsTemplateProcessor templateCallback){ + send(beginChunkedResponse(contentType, callback, templateCallback)); +} + +void AsyncWebServerRequest::send_P(int code, const String& contentType, const uint8_t * content, size_t len, AwsTemplateProcessor callback){ + send(beginResponse_P(code, contentType, content, len, callback)); +} + +void AsyncWebServerRequest::send_P(int code, const String& contentType, PGM_P content, AwsTemplateProcessor callback){ + send(beginResponse_P(code, contentType, content, callback)); +} + +void AsyncWebServerRequest::redirect(const String& url){ + AsyncWebServerResponse * response = beginResponse(302); + response->addHeader(F("Location"), url); + send(response); +} + +bool AsyncWebServerRequest::authenticate(const char * username, const char * password, const char * realm, bool passwordIsHash){ + if(_authorization.length()){ + if(_isDigest) + return checkDigestAuthentication(_authorization.c_str(), methodToString(), username, password, realm, passwordIsHash, NULL, NULL, NULL); + else if(!passwordIsHash) + return checkBasicAuthentication(_authorization.c_str(), username, password); + else + return _authorization.equals(password); + } + return false; +} + +bool AsyncWebServerRequest::authenticate(const char * hash){ + if(!_authorization.length() || hash == NULL) + return false; + + if(_isDigest){ + String hStr = String(hash); + int separator = hStr.indexOf(':'); + if(separator <= 0) + return false; + String username = hStr.substring(0, separator); + hStr = hStr.substring(separator + 1); + separator = hStr.indexOf(':'); + if(separator <= 0) + return false; + String realm = hStr.substring(0, separator); + hStr = hStr.substring(separator + 1); + return checkDigestAuthentication(_authorization.c_str(), methodToString(), username.c_str(), hStr.c_str(), realm.c_str(), true, NULL, NULL, NULL); + } + + return (_authorization.equals(hash)); +} + +void AsyncWebServerRequest::requestAuthentication(const char * realm, bool isDigest){ + AsyncWebServerResponse * r = beginResponse(401); + if(!isDigest && realm == NULL){ + r->addHeader(F("WWW-Authenticate"), F("Basic realm=\"Login Required\"")); + } else if(!isDigest){ + String header = F("Basic realm=\""); + header.concat(realm); + header += '"'; + r->addHeader(F("WWW-Authenticate"), header); + } else { + String header = F("Digest "); + header.concat(requestDigestAuthentication(realm)); + r->addHeader(F("WWW-Authenticate"), header); + } + send(r); +} + +bool AsyncWebServerRequest::hasArg(const char* name) const { + for(const auto& arg: _params){ + if(arg->name() == name){ + return true; + } + } + return false; +} + +bool AsyncWebServerRequest::hasArg(const __FlashStringHelper * data) const { + return hasArg(String(data).c_str()); +} + + +const String& AsyncWebServerRequest::arg(const String& name) const { + for(const auto& arg: _params){ + if(arg->name() == name){ + return arg->value(); + } + } + return emptyString; +} + +const String& AsyncWebServerRequest::arg(const __FlashStringHelper * data) const { + return arg(String(data).c_str()); +} + +const String& AsyncWebServerRequest::arg(size_t i) const { + return getParam(i)->value(); +} + +const String& AsyncWebServerRequest::argName(size_t i) const { + return getParam(i)->name(); +} + +const String& AsyncWebServerRequest::pathArg(size_t i) const { + auto param = _pathParams.nth(i); + return param ? **param : emptyString; +} + +const String& AsyncWebServerRequest::header(const char* name) const { + AsyncWebHeader* h = getHeader(String(name)); + return h ? h->value() : emptyString; +} + +const String& AsyncWebServerRequest::header(const __FlashStringHelper * data) const { + return header(String(data).c_str()); +}; + + +const String& AsyncWebServerRequest::header(size_t i) const { + AsyncWebHeader* h = getHeader(i); + return h ? h->value() : emptyString; +} + +const String& AsyncWebServerRequest::headerName(size_t i) const { + AsyncWebHeader* h = getHeader(i); + return h ? h->name() : emptyString; +} + +String AsyncWebServerRequest::urlDecode(const String& text) const { + char temp[] = "0x00"; + unsigned int len = text.length(); + unsigned int i = 0; + String decoded = String(); + decoded.reserve(len); // Allocate the string internal buffer - never longer from source text + while (i < len){ + char decodedChar; + char encodedChar = text.charAt(i++); + if ((encodedChar == '%') && (i + 1 < len)){ + temp[2] = text.charAt(i++); + temp[3] = text.charAt(i++); + decodedChar = strtol(temp, NULL, 16); + } else if (encodedChar == '+') { + decodedChar = ' '; + } else { + decodedChar = encodedChar; // normal ascii char + } + decoded.concat(decodedChar); + } + return decoded; +} + + +const char *AsyncWebServerRequest::methodToString() const { + if(_method == HTTP_ANY) return ("ANY"); + else if(_method & HTTP_GET) return ("GET"); + else if(_method & HTTP_POST) return ("POST"); + else if(_method & HTTP_DELETE) return ("DELETE"); + else if(_method & HTTP_PUT) return ("PUT"); + else if(_method & HTTP_PATCH) return ("PATCH"); + else if(_method & HTTP_HEAD) return ("HEAD"); + else if(_method & HTTP_OPTIONS) return ("OPTIONS"); + return ("UNKNOWN"); +} + +const char *AsyncWebServerRequest::requestedConnTypeToString() const { + switch (_reqconntype) { + case RCT_NOT_USED: return ("RCT_NOT_USED"); + case RCT_DEFAULT: return ("RCT_DEFAULT"); + case RCT_HTTP: return ("RCT_HTTP"); + case RCT_WS: return ("RCT_WS"); + case RCT_EVENT: return ("RCT_EVENT"); + default: return ("ERROR"); + } +} + +bool AsyncWebServerRequest::isExpectedRequestedConnType(RequestedConnectionType erct1, RequestedConnectionType erct2, RequestedConnectionType erct3) { + bool res = false; + if ((erct1 != RCT_NOT_USED) && (erct1 == _reqconntype)) res = true; + if ((erct2 != RCT_NOT_USED) && (erct2 == _reqconntype)) res = true; + if ((erct3 != RCT_NOT_USED) && (erct3 == _reqconntype)) res = true; + return res; +} diff --git a/lib/ESPAsyncWebServer/WebResponses.cpp b/lib/ESPAsyncWebServer/WebResponses.cpp index 6e24444b7..d2c2f78a9 100644 --- a/lib/ESPAsyncWebServer/WebResponses.cpp +++ b/lib/ESPAsyncWebServer/WebResponses.cpp @@ -1,708 +1,709 @@ -/* - Asynchronous WebServer library for Espressif MCUs - - Copyright (c) 2016 Hristo Gochkov. All rights reserved. - This file is part of the esp8266 core for Arduino environment. - - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Lesser General Public - License as published by the Free Software Foundation; either - version 2.1 of the License, or (at your option) any later version. - - This library is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - Lesser General Public License for more details. - - You should have received a copy of the GNU Lesser General Public - License along with this library; if not, write to the Free Software - Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -*/ -#include "ESPAsyncWebServer.h" -#include "WebResponseImpl.h" -#include "cbuf.h" - -// Since ESP8266 does not link memchr by default, here's its implementation. -void* memchr(void* ptr, int ch, size_t count) -{ - unsigned char* p = static_cast(ptr); - while(count--) - if(*p++ == static_cast(ch)) - return --p; - return nullptr; -} - - -/* - * Abstract Response - * */ -const char* AsyncWebServerResponse::_responseCodeToString(int code) { - return reinterpret_cast(responseCodeToString(code)); -} - -const __FlashStringHelper *AsyncWebServerResponse::responseCodeToString(int code) { - switch (code) { - case 100: return F("Continue"); - case 101: return F("Switching Protocols"); - case 200: return F("OK"); - case 201: return F("Created"); - case 202: return F("Accepted"); - case 203: return F("Non-Authoritative Information"); - case 204: return F("No Content"); - case 205: return F("Reset Content"); - case 206: return F("Partial Content"); - case 300: return F("Multiple Choices"); - case 301: return F("Moved Permanently"); - case 302: return F("Found"); - case 303: return F("See Other"); - case 304: return F("Not Modified"); - case 305: return F("Use Proxy"); - case 307: return F("Temporary Redirect"); - case 400: return F("Bad Request"); - case 401: return F("Unauthorized"); - case 402: return F("Payment Required"); - case 403: return F("Forbidden"); - case 404: return F("Not Found"); - case 405: return F("Method Not Allowed"); - case 406: return F("Not Acceptable"); - case 407: return F("Proxy Authentication Required"); - case 408: return F("Request Time-out"); - case 409: return F("Conflict"); - case 410: return F("Gone"); - case 411: return F("Length Required"); - case 412: return F("Precondition Failed"); - case 413: return F("Request Entity Too Large"); - case 414: return F("Request-URI Too Large"); - case 415: return F("Unsupported Media Type"); - case 416: return F("Requested range not satisfiable"); - case 417: return F("Expectation Failed"); - case 500: return F("Internal Server Error"); - case 501: return F("Not Implemented"); - case 502: return F("Bad Gateway"); - case 503: return F("Service Unavailable"); - case 504: return F("Gateway Time-out"); - case 505: return F("HTTP Version not supported"); - default: return F(""); - } -} - -AsyncWebServerResponse::AsyncWebServerResponse() - : _code(0) - , _headers(LinkedList([](AsyncWebHeader *h){ delete h; })) - , _contentType() - , _contentLength(0) - , _sendContentLength(true) - , _chunked(false) - , _headLength(0) - , _sentLength(0) - , _ackedLength(0) - , _writtenLength(0) - , _state(RESPONSE_SETUP) -{ - for(auto header: DefaultHeaders::Instance()) { - _headers.add(new AsyncWebHeader(header->name(), header->value())); - } -} - -AsyncWebServerResponse::~AsyncWebServerResponse(){ - _headers.free(); -} - -void AsyncWebServerResponse::setCode(int code){ - if(_state == RESPONSE_SETUP) - _code = code; -} - -void AsyncWebServerResponse::setContentLength(size_t len){ - if(_state == RESPONSE_SETUP) - _contentLength = len; -} - -void AsyncWebServerResponse::setContentType(const String& type){ - if(_state == RESPONSE_SETUP) - _contentType = type; -} - -void AsyncWebServerResponse::addHeader(const String& name, const String& value){ - _headers.add(new AsyncWebHeader(name, value)); -} - -String AsyncWebServerResponse::_assembleHead(uint8_t version){ - if(version){ - addHeader(F("Accept-Ranges"), F("none")); - if(_chunked) - addHeader(F("Transfer-Encoding"), F("chunked")); - } - String out = String(); - int bufSize = 300; - char buf[bufSize]; - - snprintf_P(buf, bufSize, PSTR("HTTP/1.%d %d %s\r\n"), version, _code, _responseCodeToString(_code)); - out.concat(buf); - - if(_sendContentLength) { - snprintf_P(buf, bufSize, PSTR("Content-Length: %d\r\n"), _contentLength); - out.concat(buf); - } - if(_contentType.length()) { - snprintf_P(buf, bufSize, PSTR("Content-Type: %s\r\n"), _contentType.c_str()); - out.concat(buf); - } - - for(const auto& header: _headers){ - snprintf_P(buf, bufSize, PSTR("%s: %s\r\n"), header->name().c_str(), header->value().c_str()); - out.concat(buf); - } - _headers.free(); - - out.concat(F("\r\n")); - _headLength = out.length(); - return out; -} - -bool AsyncWebServerResponse::_started() const { return _state > RESPONSE_SETUP; } -bool AsyncWebServerResponse::_finished() const { return _state > RESPONSE_WAIT_ACK; } -bool AsyncWebServerResponse::_failed() const { return _state == RESPONSE_FAILED; } -bool AsyncWebServerResponse::_sourceValid() const { return false; } -void AsyncWebServerResponse::_respond(AsyncWebServerRequest *request){ _state = RESPONSE_END; request->client()->close(); } -size_t AsyncWebServerResponse::_ack(AsyncWebServerRequest *request, size_t len, uint32_t time){ (void)request; (void)len; (void)time; return 0; } - -/* - * String/Code Response - * */ -AsyncBasicResponse::AsyncBasicResponse(int code, const String& contentType, const String& content){ - _code = code; - _content = content; - _contentType = contentType; - if(_content.length()){ - _contentLength = _content.length(); - if(!_contentType.length()) - _contentType = F("text/plain"); - } - addHeader(F("Connection"), F("close")); -} - -void AsyncBasicResponse::_respond(AsyncWebServerRequest *request){ - _state = RESPONSE_HEADERS; - String out = _assembleHead(request->version()); - size_t outLen = out.length(); - size_t space = request->client()->space(); - if(!_contentLength && space >= outLen){ - _writtenLength += request->client()->write(out.c_str(), outLen); - _state = RESPONSE_WAIT_ACK; - } else if(_contentLength && space >= outLen + _contentLength){ - out += _content; - outLen += _contentLength; - _writtenLength += request->client()->write(out.c_str(), outLen); - _state = RESPONSE_WAIT_ACK; - } else if(space && space < outLen){ - String partial = out.substring(0, space); - _content = out.substring(space) + _content; - _contentLength += outLen - space; - _writtenLength += request->client()->write(partial.c_str(), partial.length()); - _state = RESPONSE_CONTENT; - } else if(space > outLen && space < (outLen + _contentLength)){ - size_t shift = space - outLen; - outLen += shift; - _sentLength += shift; - out += _content.substring(0, shift); - _content = _content.substring(shift); - _writtenLength += request->client()->write(out.c_str(), outLen); - _state = RESPONSE_CONTENT; - } else { - _content = out + _content; - _contentLength += outLen; - _state = RESPONSE_CONTENT; - } -} - -size_t AsyncBasicResponse::_ack(AsyncWebServerRequest *request, size_t len, uint32_t time){ - (void)time; - _ackedLength += len; - if(_state == RESPONSE_CONTENT){ - size_t available = _contentLength - _sentLength; - size_t space = request->client()->space(); - //we can fit in this packet - if(space > available){ - _writtenLength += request->client()->write(_content.c_str(), available); - _content = String(); - _state = RESPONSE_WAIT_ACK; - return available; - } - //send some data, the rest on ack - String out = _content.substring(0, space); - _content = _content.substring(space); - _sentLength += space; - _writtenLength += request->client()->write(out.c_str(), space); - return space; - } else if(_state == RESPONSE_WAIT_ACK){ - if(_ackedLength >= _writtenLength){ - _state = RESPONSE_END; - } - } - return 0; -} - - -/* - * Abstract Response - * */ - -AsyncAbstractResponse::AsyncAbstractResponse(AwsTemplateProcessor callback): _callback(callback) -{ - // In case of template processing, we're unable to determine real response size - if(callback) { - _contentLength = 0; - _sendContentLength = false; - _chunked = true; - } -} - -void AsyncAbstractResponse::_respond(AsyncWebServerRequest *request){ - addHeader(F("Connection"), F("close")); - _head = _assembleHead(request->version()); - _state = RESPONSE_HEADERS; - _ack(request, 0, 0); -} - -size_t AsyncAbstractResponse::_ack(AsyncWebServerRequest *request, size_t len, uint32_t time){ - (void)time; - if(!_sourceValid()){ - _state = RESPONSE_FAILED; - request->client()->close(); - return 0; - } - _ackedLength += len; - size_t space = request->client()->space(); - - size_t headLen = _head.length(); - if(_state == RESPONSE_HEADERS){ - if(space >= headLen){ - _state = RESPONSE_CONTENT; - space -= headLen; - } else { - String out = _head.substring(0, space); - _head = _head.substring(space); - _writtenLength += request->client()->write(out.c_str(), out.length()); - return out.length(); - } - } - - if(_state == RESPONSE_CONTENT){ - size_t outLen; - if(_chunked){ - if(space <= 8){ - return 0; - } - outLen = space; - } else if(!_sendContentLength){ - outLen = space; - } else { - outLen = ((_contentLength - _sentLength) > space)?space:(_contentLength - _sentLength); - } - - uint8_t *buf = (uint8_t *)malloc(outLen+headLen); - if (!buf) { - // os_printf("_ack malloc %d failed\n", outLen+headLen); - return 0; - } - - if(headLen){ - memcpy(buf, _head.c_str(), _head.length()); - } - - size_t readLen = 0; - - if(_chunked){ - // HTTP 1.1 allows leading zeros in chunk length. Or spaces may be added. - // See RFC2616 sections 2, 3.6.1. - readLen = _fillBufferAndProcessTemplates(buf+headLen+6, outLen - 8); - if(readLen == RESPONSE_TRY_AGAIN){ - free(buf); - return 0; - } - outLen = snprintf_P((char*)buf+headLen, sizeof(buf)-headLen-2, PSTR("%x"), readLen) + headLen; - while(outLen < headLen + 4) buf[outLen++] = ' '; - buf[outLen++] = '\r'; - buf[outLen++] = '\n'; - outLen += readLen; - buf[outLen++] = '\r'; - buf[outLen++] = '\n'; - } else { - readLen = _fillBufferAndProcessTemplates(buf+headLen, outLen); - if(readLen == RESPONSE_TRY_AGAIN){ - free(buf); - return 0; - } - outLen = readLen + headLen; - } - - if(headLen){ - _head = String(); - } - - if(outLen){ - _writtenLength += request->client()->write((const char*)buf, outLen); - } - - if(_chunked){ - _sentLength += readLen; - } else { - _sentLength += outLen - headLen; - } - - free(buf); - - if((_chunked && readLen == 0) || (!_sendContentLength && outLen == 0) || (!_chunked && _sentLength == _contentLength)){ - _state = RESPONSE_WAIT_ACK; - } - return outLen; - - } else if(_state == RESPONSE_WAIT_ACK){ - if(!_sendContentLength || _ackedLength >= _writtenLength){ - _state = RESPONSE_END; - if(!_chunked && !_sendContentLength) - request->client()->close(true); - } - } - return 0; -} - -size_t AsyncAbstractResponse::_readDataFromCacheOrContent(uint8_t* data, const size_t len) -{ - // If we have something in cache, copy it to buffer - const size_t readFromCache = std::min(len, _cache.size()); - if(readFromCache) { - memcpy(data, _cache.data(), readFromCache); - _cache.erase(_cache.begin(), _cache.begin() + readFromCache); - } - // If we need to read more... - const size_t needFromFile = len - readFromCache; - const size_t readFromContent = _fillBuffer(data + readFromCache, needFromFile); - return readFromCache + readFromContent; -} - -size_t AsyncAbstractResponse::_fillBufferAndProcessTemplates(uint8_t* data, size_t len) -{ - if(!_callback) - return _fillBuffer(data, len); - - const size_t originalLen = len; - len = _readDataFromCacheOrContent(data, len); - // Now we've read 'len' bytes, either from cache or from file - // Search for template placeholders - uint8_t* pTemplateStart = data; - while((pTemplateStart < &data[len]) && (pTemplateStart = (uint8_t*)memchr(pTemplateStart, TEMPLATE_PLACEHOLDER, &data[len - 1] - pTemplateStart + 1))) { // data[0] ... data[len - 1] - uint8_t* pTemplateEnd = (pTemplateStart < &data[len - 1]) ? (uint8_t*)memchr(pTemplateStart + 1, TEMPLATE_PLACEHOLDER, &data[len - 1] - pTemplateStart) : nullptr; - // temporary buffer to hold parameter name - uint8_t buf[TEMPLATE_PARAM_NAME_LENGTH + 1]; - String paramName; - // If closing placeholder is found: - if(pTemplateEnd) { - // prepare argument to callback - const size_t paramNameLength = std::min((size_t)sizeof(buf) - 1, (size_t)(pTemplateEnd - pTemplateStart - 1)); - if(paramNameLength) { - memcpy(buf, pTemplateStart + 1, paramNameLength); - buf[paramNameLength] = 0; - paramName = String(reinterpret_cast(buf)); - } else { // double percent sign encountered, this is single percent sign escaped. - // remove the 2nd percent sign - memmove(pTemplateEnd, pTemplateEnd + 1, &data[len] - pTemplateEnd - 1); - len += _readDataFromCacheOrContent(&data[len - 1], 1) - 1; - ++pTemplateStart; - } - } else if(&data[len - 1] - pTemplateStart + 1 < TEMPLATE_PARAM_NAME_LENGTH + 2) { // closing placeholder not found, check if it's in the remaining file data - memcpy(buf, pTemplateStart + 1, &data[len - 1] - pTemplateStart); - const size_t readFromCacheOrContent = _readDataFromCacheOrContent(buf + (&data[len - 1] - pTemplateStart), TEMPLATE_PARAM_NAME_LENGTH + 2 - (&data[len - 1] - pTemplateStart + 1)); - if(readFromCacheOrContent) { - pTemplateEnd = (uint8_t*)memchr(buf + (&data[len - 1] - pTemplateStart), TEMPLATE_PLACEHOLDER, readFromCacheOrContent); - if(pTemplateEnd) { - // prepare argument to callback - *pTemplateEnd = 0; - paramName = String(reinterpret_cast(buf)); - // Copy remaining read-ahead data into cache - _cache.insert(_cache.begin(), pTemplateEnd + 1, buf + (&data[len - 1] - pTemplateStart) + readFromCacheOrContent); - pTemplateEnd = &data[len - 1]; - } - else // closing placeholder not found in file data, store found percent symbol as is and advance to the next position - { - // but first, store read file data in cache - _cache.insert(_cache.begin(), buf + (&data[len - 1] - pTemplateStart), buf + (&data[len - 1] - pTemplateStart) + readFromCacheOrContent); - ++pTemplateStart; - } - } - else // closing placeholder not found in content data, store found percent symbol as is and advance to the next position - ++pTemplateStart; - } - else // closing placeholder not found in content data, store found percent symbol as is and advance to the next position - ++pTemplateStart; - if(paramName.length()) { - // call callback and replace with result. - // Everything in range [pTemplateStart, pTemplateEnd] can be safely replaced with parameter value. - // Data after pTemplateEnd may need to be moved. - // The first byte of data after placeholder is located at pTemplateEnd + 1. - // It should be located at pTemplateStart + numBytesCopied (to begin right after inserted parameter value). - const String paramValue(_callback(paramName)); - const char* pvstr = paramValue.c_str(); - const unsigned int pvlen = paramValue.length(); - const size_t numBytesCopied = std::min(pvlen, static_cast(&data[originalLen - 1] - pTemplateStart + 1)); - // make room for param value - // 1. move extra data to cache if parameter value is longer than placeholder AND if there is no room to store - if((pTemplateEnd + 1 < pTemplateStart + numBytesCopied) && (originalLen - (pTemplateStart + numBytesCopied - pTemplateEnd - 1) < len)) { - _cache.insert(_cache.begin(), &data[originalLen - (pTemplateStart + numBytesCopied - pTemplateEnd - 1)], &data[len]); - //2. parameter value is longer than placeholder text, push the data after placeholder which not saved into cache further to the end - memmove(pTemplateStart + numBytesCopied, pTemplateEnd + 1, &data[originalLen] - pTemplateStart - numBytesCopied); - len = originalLen; // fix issue with truncated data, not sure if it has any side effects - } else if(pTemplateEnd + 1 != pTemplateStart + numBytesCopied) - //2. Either parameter value is shorter than placeholder text OR there is enough free space in buffer to fit. - // Move the entire data after the placeholder - memmove(pTemplateStart + numBytesCopied, pTemplateEnd + 1, &data[len] - pTemplateEnd - 1); - // 3. replace placeholder with actual value - memcpy(pTemplateStart, pvstr, numBytesCopied); - // If result is longer than buffer, copy the remainder into cache (this could happen only if placeholder text itself did not fit entirely in buffer) - if(numBytesCopied < pvlen) { - _cache.insert(_cache.begin(), pvstr + numBytesCopied, pvstr + pvlen); - } else if(pTemplateStart + numBytesCopied < pTemplateEnd + 1) { // result is copied fully; if result is shorter than placeholder text... - // there is some free room, fill it from cache - const size_t roomFreed = pTemplateEnd + 1 - pTemplateStart - numBytesCopied; - const size_t totalFreeRoom = originalLen - len + roomFreed; - len += _readDataFromCacheOrContent(&data[len - roomFreed], totalFreeRoom) - roomFreed; - } else { // result is copied fully; it is longer than placeholder text - const size_t roomTaken = pTemplateStart + numBytesCopied - pTemplateEnd - 1; - len = std::min(len + roomTaken, originalLen); - } - } - } // while(pTemplateStart) - return len; -} - - -/* - * File Response - * */ - -AsyncFileResponse::~AsyncFileResponse(){ - if(_content) - _content.close(); -} - -void AsyncFileResponse::_setContentType(const String& path){ -#if HAVE_EXTERN_GET_CONTENT_TYPE_FUNCTION - extern const __FlashStringHelper *getContentType(const String &path); - _contentType = getContentType(path); -#else - if (path.endsWith(F(".html"))) _contentType = F("text/html"); - else if (path.endsWith(F(".htm"))) _contentType = F("text/html"); - else if (path.endsWith(F(".css"))) _contentType = F("text/css"); - else if (path.endsWith(F(".json"))) _contentType = F("application/json"); - else if (path.endsWith(F(".js"))) _contentType = F("application/javascript"); - else if (path.endsWith(F(".png"))) _contentType = F("image/png"); - else if (path.endsWith(F(".gif"))) _contentType = F("image/gif"); - else if (path.endsWith(F(".jpg"))) _contentType = F("image/jpeg"); - else if (path.endsWith(F(".ico"))) _contentType = F("image/x-icon"); - else if (path.endsWith(F(".svg"))) _contentType = F("image/svg+xml"); - else if (path.endsWith(F(".eot"))) _contentType = F("font/eot"); - else if (path.endsWith(F(".woff"))) _contentType = F("font/woff"); - else if (path.endsWith(F(".woff2"))) _contentType = F("font/woff2"); - else if (path.endsWith(F(".ttf"))) _contentType = F("font/ttf"); - else if (path.endsWith(F(".xml"))) _contentType = F("text/xml"); - else if (path.endsWith(F(".pdf"))) _contentType = F("application/pdf"); - else if (path.endsWith(F(".zip"))) _contentType = F("application/zip"); - else if(path.endsWith(F(".gz"))) _contentType = F("application/x-gzip"); - else _contentType = F("text/plain"); -#endif -} - -AsyncFileResponse::AsyncFileResponse(FS &fs, const String& path, const String& contentType, bool download, AwsTemplateProcessor callback): AsyncAbstractResponse(callback){ - _code = 200; - _path = path; - - if(!download && !fs.exists(_path) && fs.exists(_path + F(".gz"))){ - _path = _path + F(".gz"); - addHeader(F("Content-Encoding"), F("gzip")); - _callback = nullptr; // Unable to process zipped templates - _sendContentLength = true; - _chunked = false; - } - - _content = fs.open(_path, fs::FileOpenMode::read); - _contentLength = _content.size(); - - if(contentType.length() == 0) - _setContentType(path); - else - _contentType = contentType; - - int filenameStart = path.lastIndexOf('/') + 1; - char buf[26+path.length()-filenameStart]; - char* filename = (char*)path.c_str() + filenameStart; - - if(download) { - // set filename and force download - snprintf_P(buf, sizeof (buf), PSTR("attachment; filename=\"%s\""), filename); - } else { - // set filename and force rendering - snprintf_P(buf, sizeof (buf), PSTR("inline; filename=\"%s\""), filename); - } - addHeader(F("Content-Disposition"), buf); -} - -AsyncFileResponse::AsyncFileResponse(File content, const String& path, const String& contentType, bool download, AwsTemplateProcessor callback): AsyncAbstractResponse(callback){ - _code = 200; - _path = path; - - if(!download && String(content.name()).endsWith(F(".gz")) && !path.endsWith(F(".gz"))){ - addHeader(F("Content-Encoding"), F("gzip")); - _callback = nullptr; // Unable to process gzipped templates - _sendContentLength = true; - _chunked = false; - } - - _content = content; - _contentLength = _content.size(); - - if(contentType.length() == 0) - _setContentType(path); - else - _contentType = contentType; - - int filenameStart = path.lastIndexOf('/') + 1; - char buf[26+path.length()-filenameStart]; - char* filename = (char*)path.c_str() + filenameStart; - - if(download) { - snprintf_P(buf, sizeof (buf), PSTR("attachment; filename=\"%s\""), filename); - } else { - snprintf_P(buf, sizeof (buf), PSTR("inline; filename=\"%s\""), filename); - } - addHeader(F("Content-Disposition"), buf); -} - -size_t AsyncFileResponse::_fillBuffer(uint8_t *data, size_t len){ - return _content.read(data, len); -} - -/* - * Stream Response - * */ - -AsyncStreamResponse::AsyncStreamResponse(Stream &stream, const String& contentType, size_t len, AwsTemplateProcessor callback): AsyncAbstractResponse(callback) { - _code = 200; - _content = &stream; - _contentLength = len; - _contentType = contentType; -} - -size_t AsyncStreamResponse::_fillBuffer(uint8_t *data, size_t len){ - size_t available = _content->available(); - size_t outLen = (available > len)?len:available; - size_t i; - for(i=0;iread(); - return outLen; -} - -/* - * Callback Response - * */ - -AsyncCallbackResponse::AsyncCallbackResponse(const String& contentType, size_t len, AwsResponseFiller callback, AwsTemplateProcessor templateCallback): AsyncAbstractResponse(templateCallback) { - _code = 200; - _content = callback; - _contentLength = len; - if(!len) - _sendContentLength = false; - _contentType = contentType; - _filledLength = 0; -} - -size_t AsyncCallbackResponse::_fillBuffer(uint8_t *data, size_t len){ - size_t ret = _content(data, len, _filledLength); - if(ret != RESPONSE_TRY_AGAIN){ - _filledLength += ret; - } - return ret; -} - -/* - * Chunked Response - * */ - -AsyncChunkedResponse::AsyncChunkedResponse(const String& contentType, AwsResponseFiller callback, AwsTemplateProcessor processorCallback): AsyncAbstractResponse(processorCallback) { - _code = 200; - _content = callback; - _contentLength = 0; - _contentType = contentType; - _sendContentLength = false; - _chunked = true; - _filledLength = 0; -} - -size_t AsyncChunkedResponse::_fillBuffer(uint8_t *data, size_t len){ - size_t ret = _content(data, len, _filledLength); - if(ret != RESPONSE_TRY_AGAIN){ - _filledLength += ret; - } - return ret; -} - -/* - * Progmem Response - * */ - -AsyncProgmemResponse::AsyncProgmemResponse(int code, const String& contentType, const uint8_t * content, size_t len, AwsTemplateProcessor callback): AsyncAbstractResponse(callback) { - _code = code; - _content = content; - _contentType = contentType; - _contentLength = len; - _readLength = 0; -} - -size_t AsyncProgmemResponse::_fillBuffer(uint8_t *data, size_t len){ - size_t left = _contentLength - _readLength; - if (left > len) { - memcpy_P(data, _content + _readLength, len); - _readLength += len; - return len; - } - memcpy_P(data, _content + _readLength, left); - _readLength += left; - return left; -} - - -/* - * Response Stream (You can print/write/printf to it, up to the contentLen bytes) - * */ - -AsyncResponseStream::AsyncResponseStream(const String& contentType, size_t bufferSize){ - _code = 200; - _contentLength = 0; - _contentType = contentType; - _content = new cbuf(bufferSize); -} - -AsyncResponseStream::~AsyncResponseStream(){ - delete _content; -} - -size_t AsyncResponseStream::_fillBuffer(uint8_t *buf, size_t maxLen){ - return _content->read((char*)buf, maxLen); -} - -size_t AsyncResponseStream::write(const uint8_t *data, size_t len){ - if(_started()) - return 0; - - if(len > _content->room()){ - size_t needed = len - _content->room(); - _content->resizeAdd(needed); - } - size_t written = _content->write((const char*)data, len); - _contentLength += written; - return written; -} - -size_t AsyncResponseStream::write(uint8_t data){ - return write(&data, 1); -} +/* + Asynchronous WebServer library for Espressif MCUs + + Copyright (c) 2016 Hristo Gochkov. All rights reserved. + This file is part of the esp8266 core for Arduino environment. + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ +#include "ESPAsyncWebServer.h" +#include "WebResponseImpl.h" +#include "cbuf.h" + +// Since ESP8266 does not link memchr by default, here's its implementation. +void* memchr(void* ptr, int ch, size_t count) +{ + unsigned char* p = static_cast(ptr); + while(count--) + if(*p++ == static_cast(ch)) + return --p; + return nullptr; +} + + +/* + * Abstract Response + * */ +const char* AsyncWebServerResponse::_responseCodeToString(int code) { + switch (code) { + case 100: return ("Continue"); + case 101: return ("Switching Protocols"); + case 200: return ("OK"); + case 201: return ("Created"); + case 202: return ("Accepted"); + case 203: return ("Non-Authoritative Information"); + case 204: return ("No Content"); + case 205: return ("Reset Content"); + case 206: return ("Partial Content"); + case 300: return ("Multiple Choices"); + case 301: return ("Moved Permanently"); + case 302: return ("Found"); + case 303: return ("See Other"); + case 304: return ("Not Modified"); + case 305: return ("Use Proxy"); + case 307: return ("Temporary Redirect"); + case 400: return ("Bad Request"); + case 401: return ("Unauthorized"); + case 402: return ("Payment Required"); + case 403: return ("Forbidden"); + case 404: return ("Not Found"); + case 405: return ("Method Not Allowed"); + case 406: return ("Not Acceptable"); + case 407: return ("Proxy Authentication Required"); + case 408: return ("Request Time-out"); + case 409: return ("Conflict"); + case 410: return ("Gone"); + case 411: return ("Length Required"); + case 412: return ("Precondition Failed"); + case 413: return ("Request Entity Too Large"); + case 414: return ("Request-URI Too Large"); + case 415: return ("Unsupported Media Type"); + case 416: return ("Requested range not satisfiable"); + case 417: return ("Expectation Failed"); + case 500: return ("Internal Server Error"); + case 501: return ("Not Implemented"); + case 502: return ("Bad Gateway"); + case 503: return ("Service Unavailable"); + case 504: return ("Gateway Time-out"); + case 505: return ("HTTP Version not supported"); + case 507: return ("Insufficient Storage"); + default: return (""); + } +} + +const __FlashStringHelper *AsyncWebServerResponse::responseCodeToString(int code) { + return reinterpret_cast(responseCodeToString(code)); +} + +AsyncWebServerResponse::AsyncWebServerResponse() + : _code(0) + , _headers(LinkedList([](AsyncWebHeader *h){ delete h; })) + , _contentType() + , _contentLength(0) + , _sendContentLength(true) + , _chunked(false) + , _headLength(0) + , _sentLength(0) + , _ackedLength(0) + , _writtenLength(0) + , _state(RESPONSE_SETUP) +{ + for(auto header: DefaultHeaders::Instance()) { + _headers.add(new AsyncWebHeader(header->name(), header->value())); + } +} + +AsyncWebServerResponse::~AsyncWebServerResponse(){ + _headers.free(); +} + +void AsyncWebServerResponse::setCode(int code){ + if(_state == RESPONSE_SETUP) + _code = code; +} + +void AsyncWebServerResponse::setContentLength(size_t len){ + if(_state == RESPONSE_SETUP) + _contentLength = len; +} + +void AsyncWebServerResponse::setContentType(const String& type){ + if(_state == RESPONSE_SETUP) + _contentType = type; +} + +void AsyncWebServerResponse::addHeader(const String& name, const String& value){ + _headers.add(new AsyncWebHeader(name, value)); +} + +String AsyncWebServerResponse::_assembleHead(uint8_t version){ + if(version){ + addHeader(F("Accept-Ranges"), F("none")); + if(_chunked) + addHeader(F("Transfer-Encoding"), F("chunked")); + } + String out = String(); + int bufSize = 300; + char buf[bufSize]; + + snprintf_P(buf, bufSize, PSTR("HTTP/1.%d %d %s\r\n"), version, _code, _responseCodeToString(_code)); + out.concat(buf); + + if(_sendContentLength) { + snprintf_P(buf, bufSize, PSTR("Content-Length: %d\r\n"), _contentLength); + out.concat(buf); + } + if(_contentType.length()) { + snprintf_P(buf, bufSize, PSTR("Content-Type: %s\r\n"), _contentType.c_str()); + out.concat(buf); + } + + for(const auto& header: _headers){ + snprintf_P(buf, bufSize, PSTR("%s: %s\r\n"), header->name().c_str(), header->value().c_str()); + out.concat(buf); + } + _headers.free(); + + out.concat(F("\r\n")); + _headLength = out.length(); + return out; +} + +bool AsyncWebServerResponse::_started() const { return _state > RESPONSE_SETUP; } +bool AsyncWebServerResponse::_finished() const { return _state > RESPONSE_WAIT_ACK; } +bool AsyncWebServerResponse::_failed() const { return _state == RESPONSE_FAILED; } +bool AsyncWebServerResponse::_sourceValid() const { return false; } +void AsyncWebServerResponse::_respond(AsyncWebServerRequest *request){ _state = RESPONSE_END; request->client()->close(); } +size_t AsyncWebServerResponse::_ack(AsyncWebServerRequest *request, size_t len, uint32_t time){ (void)request; (void)len; (void)time; return 0; } + +/* + * String/Code Response + * */ +AsyncBasicResponse::AsyncBasicResponse(int code, const String& contentType, const String& content){ + _code = code; + _content = content; + _contentType = contentType; + if(_content.length()){ + _contentLength = _content.length(); + if(!_contentType.length()) + _contentType = F("text/plain"); + } + addHeader(F("Connection"), F("close")); +} + +void AsyncBasicResponse::_respond(AsyncWebServerRequest *request){ + _state = RESPONSE_HEADERS; + String out = _assembleHead(request->version()); + size_t outLen = out.length(); + size_t space = request->client()->space(); + if(!_contentLength && space >= outLen){ + _writtenLength += request->client()->write(out.c_str(), outLen); + _state = RESPONSE_WAIT_ACK; + } else if(_contentLength && space >= outLen + _contentLength){ + out += _content; + outLen += _contentLength; + _writtenLength += request->client()->write(out.c_str(), outLen); + _state = RESPONSE_WAIT_ACK; + } else if(space && space < outLen){ + String partial = out.substring(0, space); + _content = out.substring(space) + _content; + _contentLength += outLen - space; + _writtenLength += request->client()->write(partial.c_str(), partial.length()); + _state = RESPONSE_CONTENT; + } else if(space > outLen && space < (outLen + _contentLength)){ + size_t shift = space - outLen; + outLen += shift; + _sentLength += shift; + out += _content.substring(0, shift); + _content = _content.substring(shift); + _writtenLength += request->client()->write(out.c_str(), outLen); + _state = RESPONSE_CONTENT; + } else { + _content = out + _content; + _contentLength += outLen; + _state = RESPONSE_CONTENT; + } +} + +size_t AsyncBasicResponse::_ack(AsyncWebServerRequest *request, size_t len, uint32_t time){ + (void)time; + _ackedLength += len; + if(_state == RESPONSE_CONTENT){ + size_t available = _contentLength - _sentLength; + size_t space = request->client()->space(); + //we can fit in this packet + if(space > available){ + _writtenLength += request->client()->write(_content.c_str(), available); + _content = String(); + _state = RESPONSE_WAIT_ACK; + return available; + } + //send some data, the rest on ack + String out = _content.substring(0, space); + _content = _content.substring(space); + _sentLength += space; + _writtenLength += request->client()->write(out.c_str(), space); + return space; + } else if(_state == RESPONSE_WAIT_ACK){ + if(_ackedLength >= _writtenLength){ + _state = RESPONSE_END; + } + } + return 0; +} + + +/* + * Abstract Response + * */ + +AsyncAbstractResponse::AsyncAbstractResponse(AwsTemplateProcessor callback): _callback(callback) +{ + // In case of template processing, we're unable to determine real response size + if(callback) { + _contentLength = 0; + _sendContentLength = false; + _chunked = true; + } +} + +void AsyncAbstractResponse::_respond(AsyncWebServerRequest *request){ + addHeader(F("Connection"), F("close")); + _head = _assembleHead(request->version()); + _state = RESPONSE_HEADERS; + _ack(request, 0, 0); +} + +size_t AsyncAbstractResponse::_ack(AsyncWebServerRequest *request, size_t len, uint32_t time){ + (void)time; + if(!_sourceValid()){ + _state = RESPONSE_FAILED; + request->client()->close(); + return 0; + } + _ackedLength += len; + size_t space = request->client()->space(); + + size_t headLen = _head.length(); + if(_state == RESPONSE_HEADERS){ + if(space >= headLen){ + _state = RESPONSE_CONTENT; + space -= headLen; + } else { + String out = _head.substring(0, space); + _head = _head.substring(space); + _writtenLength += request->client()->write(out.c_str(), out.length()); + return out.length(); + } + } + + if(_state == RESPONSE_CONTENT){ + size_t outLen; + if(_chunked){ + if(space <= 8){ + return 0; + } + outLen = space; + } else if(!_sendContentLength){ + outLen = space; + } else { + outLen = ((_contentLength - _sentLength) > space)?space:(_contentLength - _sentLength); + } + + uint8_t *buf = (uint8_t *)malloc(outLen+headLen); + if (!buf) { + // os_printf("_ack malloc %d failed\n", outLen+headLen); + return 0; + } + + if(headLen){ + memcpy(buf, _head.c_str(), _head.length()); + } + + size_t readLen = 0; + + if(_chunked){ + // HTTP 1.1 allows leading zeros in chunk length. Or spaces may be added. + // See RFC2616 sections 2, 3.6.1. + readLen = _fillBufferAndProcessTemplates(buf+headLen+6, outLen - 8); + if(readLen == RESPONSE_TRY_AGAIN){ + free(buf); + return 0; + } + outLen = snprintf_P((char*)buf+headLen, sizeof(buf)-headLen-2, PSTR("%x"), readLen) + headLen; + while(outLen < headLen + 4) buf[outLen++] = ' '; + buf[outLen++] = '\r'; + buf[outLen++] = '\n'; + outLen += readLen; + buf[outLen++] = '\r'; + buf[outLen++] = '\n'; + } else { + readLen = _fillBufferAndProcessTemplates(buf+headLen, outLen); + if(readLen == RESPONSE_TRY_AGAIN){ + free(buf); + return 0; + } + outLen = readLen + headLen; + } + + if(headLen){ + _head = String(); + } + + if(outLen){ + _writtenLength += request->client()->write((const char*)buf, outLen); + } + + if(_chunked){ + _sentLength += readLen; + } else { + _sentLength += outLen - headLen; + } + + free(buf); + + if((_chunked && readLen == 0) || (!_sendContentLength && outLen == 0) || (!_chunked && _sentLength == _contentLength)){ + _state = RESPONSE_WAIT_ACK; + } + return outLen; + + } else if(_state == RESPONSE_WAIT_ACK){ + if(!_sendContentLength || _ackedLength >= _writtenLength){ + _state = RESPONSE_END; + if(!_chunked && !_sendContentLength) + request->client()->close(true); + } + } + return 0; +} + +size_t AsyncAbstractResponse::_readDataFromCacheOrContent(uint8_t* data, const size_t len) +{ + // If we have something in cache, copy it to buffer + const size_t readFromCache = std::min(len, _cache.size()); + if(readFromCache) { + memcpy(data, _cache.data(), readFromCache); + _cache.erase(_cache.begin(), _cache.begin() + readFromCache); + } + // If we need to read more... + const size_t needFromFile = len - readFromCache; + const size_t readFromContent = _fillBuffer(data + readFromCache, needFromFile); + return readFromCache + readFromContent; +} + +size_t AsyncAbstractResponse::_fillBufferAndProcessTemplates(uint8_t* data, size_t len) +{ + if(!_callback) + return _fillBuffer(data, len); + + const size_t originalLen = len; + len = _readDataFromCacheOrContent(data, len); + // Now we've read 'len' bytes, either from cache or from file + // Search for template placeholders + uint8_t* pTemplateStart = data; + while((pTemplateStart < &data[len]) && (pTemplateStart = (uint8_t*)memchr(pTemplateStart, TEMPLATE_PLACEHOLDER, &data[len - 1] - pTemplateStart + 1))) { // data[0] ... data[len - 1] + uint8_t* pTemplateEnd = (pTemplateStart < &data[len - 1]) ? (uint8_t*)memchr(pTemplateStart + 1, TEMPLATE_PLACEHOLDER, &data[len - 1] - pTemplateStart) : nullptr; + // temporary buffer to hold parameter name + uint8_t buf[TEMPLATE_PARAM_NAME_LENGTH + 1]; + String paramName; + // If closing placeholder is found: + if(pTemplateEnd) { + // prepare argument to callback + const size_t paramNameLength = std::min((size_t)sizeof(buf) - 1, (size_t)(pTemplateEnd - pTemplateStart - 1)); + if(paramNameLength) { + memcpy(buf, pTemplateStart + 1, paramNameLength); + buf[paramNameLength] = 0; + paramName = String(reinterpret_cast(buf)); + } else { // double percent sign encountered, this is single percent sign escaped. + // remove the 2nd percent sign + memmove(pTemplateEnd, pTemplateEnd + 1, &data[len] - pTemplateEnd - 1); + len += _readDataFromCacheOrContent(&data[len - 1], 1) - 1; + ++pTemplateStart; + } + } else if(&data[len - 1] - pTemplateStart + 1 < TEMPLATE_PARAM_NAME_LENGTH + 2) { // closing placeholder not found, check if it's in the remaining file data + memcpy(buf, pTemplateStart + 1, &data[len - 1] - pTemplateStart); + const size_t readFromCacheOrContent = _readDataFromCacheOrContent(buf + (&data[len - 1] - pTemplateStart), TEMPLATE_PARAM_NAME_LENGTH + 2 - (&data[len - 1] - pTemplateStart + 1)); + if(readFromCacheOrContent) { + pTemplateEnd = (uint8_t*)memchr(buf + (&data[len - 1] - pTemplateStart), TEMPLATE_PLACEHOLDER, readFromCacheOrContent); + if(pTemplateEnd) { + // prepare argument to callback + *pTemplateEnd = 0; + paramName = String(reinterpret_cast(buf)); + // Copy remaining read-ahead data into cache + _cache.insert(_cache.begin(), pTemplateEnd + 1, buf + (&data[len - 1] - pTemplateStart) + readFromCacheOrContent); + pTemplateEnd = &data[len - 1]; + } + else // closing placeholder not found in file data, store found percent symbol as is and advance to the next position + { + // but first, store read file data in cache + _cache.insert(_cache.begin(), buf + (&data[len - 1] - pTemplateStart), buf + (&data[len - 1] - pTemplateStart) + readFromCacheOrContent); + ++pTemplateStart; + } + } + else // closing placeholder not found in content data, store found percent symbol as is and advance to the next position + ++pTemplateStart; + } + else // closing placeholder not found in content data, store found percent symbol as is and advance to the next position + ++pTemplateStart; + if(paramName.length()) { + // call callback and replace with result. + // Everything in range [pTemplateStart, pTemplateEnd] can be safely replaced with parameter value. + // Data after pTemplateEnd may need to be moved. + // The first byte of data after placeholder is located at pTemplateEnd + 1. + // It should be located at pTemplateStart + numBytesCopied (to begin right after inserted parameter value). + const String paramValue(_callback(paramName)); + const char* pvstr = paramValue.c_str(); + const unsigned int pvlen = paramValue.length(); + const size_t numBytesCopied = std::min(pvlen, static_cast(&data[originalLen - 1] - pTemplateStart + 1)); + // make room for param value + // 1. move extra data to cache if parameter value is longer than placeholder AND if there is no room to store + if((pTemplateEnd + 1 < pTemplateStart + numBytesCopied) && (originalLen - (pTemplateStart + numBytesCopied - pTemplateEnd - 1) < len)) { + _cache.insert(_cache.begin(), &data[originalLen - (pTemplateStart + numBytesCopied - pTemplateEnd - 1)], &data[len]); + //2. parameter value is longer than placeholder text, push the data after placeholder which not saved into cache further to the end + memmove(pTemplateStart + numBytesCopied, pTemplateEnd + 1, &data[originalLen] - pTemplateStart - numBytesCopied); + len = originalLen; // fix issue with truncated data, not sure if it has any side effects + } else if(pTemplateEnd + 1 != pTemplateStart + numBytesCopied) + //2. Either parameter value is shorter than placeholder text OR there is enough free space in buffer to fit. + // Move the entire data after the placeholder + memmove(pTemplateStart + numBytesCopied, pTemplateEnd + 1, &data[len] - pTemplateEnd - 1); + // 3. replace placeholder with actual value + memcpy(pTemplateStart, pvstr, numBytesCopied); + // If result is longer than buffer, copy the remainder into cache (this could happen only if placeholder text itself did not fit entirely in buffer) + if(numBytesCopied < pvlen) { + _cache.insert(_cache.begin(), pvstr + numBytesCopied, pvstr + pvlen); + } else if(pTemplateStart + numBytesCopied < pTemplateEnd + 1) { // result is copied fully; if result is shorter than placeholder text... + // there is some free room, fill it from cache + const size_t roomFreed = pTemplateEnd + 1 - pTemplateStart - numBytesCopied; + const size_t totalFreeRoom = originalLen - len + roomFreed; + len += _readDataFromCacheOrContent(&data[len - roomFreed], totalFreeRoom) - roomFreed; + } else { // result is copied fully; it is longer than placeholder text + const size_t roomTaken = pTemplateStart + numBytesCopied - pTemplateEnd - 1; + len = std::min(len + roomTaken, originalLen); + } + } + } // while(pTemplateStart) + return len; +} + + +/* + * File Response + * */ + +AsyncFileResponse::~AsyncFileResponse(){ + if(_content) + _content.close(); +} + +void AsyncFileResponse::_setContentType(const String& path){ +#if HAVE_EXTERN_GET_CONTENT_TYPE_FUNCTION + extern const __FlashStringHelper *getContentType(const String &path); + _contentType = getContentType(path); +#else + if (path.endsWith(F(".html"))) _contentType = F("text/html"); + else if (path.endsWith(F(".htm"))) _contentType = F("text/html"); + else if (path.endsWith(F(".css"))) _contentType = F("text/css"); + else if (path.endsWith(F(".json"))) _contentType = F("application/json"); + else if (path.endsWith(F(".js"))) _contentType = F("application/javascript"); + else if (path.endsWith(F(".png"))) _contentType = F("image/png"); + else if (path.endsWith(F(".gif"))) _contentType = F("image/gif"); + else if (path.endsWith(F(".jpg"))) _contentType = F("image/jpeg"); + else if (path.endsWith(F(".ico"))) _contentType = F("image/x-icon"); + else if (path.endsWith(F(".svg"))) _contentType = F("image/svg+xml"); + else if (path.endsWith(F(".eot"))) _contentType = F("font/eot"); + else if (path.endsWith(F(".woff"))) _contentType = F("font/woff"); + else if (path.endsWith(F(".woff2"))) _contentType = F("font/woff2"); + else if (path.endsWith(F(".ttf"))) _contentType = F("font/ttf"); + else if (path.endsWith(F(".xml"))) _contentType = F("text/xml"); + else if (path.endsWith(F(".pdf"))) _contentType = F("application/pdf"); + else if (path.endsWith(F(".zip"))) _contentType = F("application/zip"); + else if(path.endsWith(F(".gz"))) _contentType = F("application/x-gzip"); + else _contentType = F("text/plain"); +#endif +} + +AsyncFileResponse::AsyncFileResponse(FS &fs, const String& path, const String& contentType, bool download, AwsTemplateProcessor callback): AsyncAbstractResponse(callback){ + _code = 200; + _path = path; + + if(!download && !fs.exists(_path) && fs.exists(_path + F(".gz"))){ + _path = _path + F(".gz"); + addHeader(F("Content-Encoding"), F("gzip")); + _callback = nullptr; // Unable to process zipped templates + _sendContentLength = true; + _chunked = false; + } + + _content = fs.open(_path, fs::FileOpenMode::read); + _contentLength = _content.size(); + + if(contentType.length() == 0) + _setContentType(path); + else + _contentType = contentType; + + int filenameStart = path.lastIndexOf('/') + 1; + char buf[26+path.length()-filenameStart]; + char* filename = (char*)path.c_str() + filenameStart; + + if(download) { + // set filename and force download + snprintf_P(buf, sizeof (buf), PSTR("attachment; filename=\"%s\""), filename); + } else { + // set filename and force rendering + snprintf_P(buf, sizeof (buf), PSTR("inline; filename=\"%s\""), filename); + } + addHeader(F("Content-Disposition"), buf); +} + +AsyncFileResponse::AsyncFileResponse(File content, const String& path, const String& contentType, bool download, AwsTemplateProcessor callback): AsyncAbstractResponse(callback){ + _code = 200; + _path = path; + + if(!download && String(content.name()).endsWith(F(".gz")) && !path.endsWith(F(".gz"))){ + addHeader(F("Content-Encoding"), F("gzip")); + _callback = nullptr; // Unable to process gzipped templates + _sendContentLength = true; + _chunked = false; + } + + _content = content; + _contentLength = _content.size(); + + if(contentType.length() == 0) + _setContentType(path); + else + _contentType = contentType; + + int filenameStart = path.lastIndexOf('/') + 1; + char buf[26+path.length()-filenameStart]; + char* filename = (char*)path.c_str() + filenameStart; + + if(download) { + snprintf_P(buf, sizeof (buf), PSTR("attachment; filename=\"%s\""), filename); + } else { + snprintf_P(buf, sizeof (buf), PSTR("inline; filename=\"%s\""), filename); + } + addHeader(F("Content-Disposition"), buf); +} + +size_t AsyncFileResponse::_fillBuffer(uint8_t *data, size_t len){ + return _content.read(data, len); +} + +/* + * Stream Response + * */ + +AsyncStreamResponse::AsyncStreamResponse(Stream &stream, const String& contentType, size_t len, AwsTemplateProcessor callback): AsyncAbstractResponse(callback) { + _code = 200; + _content = &stream; + _contentLength = len; + _contentType = contentType; +} + +size_t AsyncStreamResponse::_fillBuffer(uint8_t *data, size_t len){ + size_t available = _content->available(); + size_t outLen = (available > len)?len:available; + size_t i; + for(i=0;iread(); + return outLen; +} + +/* + * Callback Response + * */ + +AsyncCallbackResponse::AsyncCallbackResponse(const String& contentType, size_t len, AwsResponseFiller callback, AwsTemplateProcessor templateCallback): AsyncAbstractResponse(templateCallback) { + _code = 200; + _content = callback; + _contentLength = len; + if(!len) + _sendContentLength = false; + _contentType = contentType; + _filledLength = 0; +} + +size_t AsyncCallbackResponse::_fillBuffer(uint8_t *data, size_t len){ + size_t ret = _content(data, len, _filledLength); + if(ret != RESPONSE_TRY_AGAIN){ + _filledLength += ret; + } + return ret; +} + +/* + * Chunked Response + * */ + +AsyncChunkedResponse::AsyncChunkedResponse(const String& contentType, AwsResponseFiller callback, AwsTemplateProcessor processorCallback): AsyncAbstractResponse(processorCallback) { + _code = 200; + _content = callback; + _contentLength = 0; + _contentType = contentType; + _sendContentLength = false; + _chunked = true; + _filledLength = 0; +} + +size_t AsyncChunkedResponse::_fillBuffer(uint8_t *data, size_t len){ + size_t ret = _content(data, len, _filledLength); + if(ret != RESPONSE_TRY_AGAIN){ + _filledLength += ret; + } + return ret; +} + +/* + * Progmem Response + * */ + +AsyncProgmemResponse::AsyncProgmemResponse(int code, const String& contentType, const uint8_t * content, size_t len, AwsTemplateProcessor callback): AsyncAbstractResponse(callback) { + _code = code; + _content = content; + _contentType = contentType; + _contentLength = len; + _readLength = 0; +} + +size_t AsyncProgmemResponse::_fillBuffer(uint8_t *data, size_t len){ + size_t left = _contentLength - _readLength; + if (left > len) { + memcpy_P(data, _content + _readLength, len); + _readLength += len; + return len; + } + memcpy_P(data, _content + _readLength, left); + _readLength += left; + return left; +} + + +/* + * Response Stream (You can print/write/printf to it, up to the contentLen bytes) + * */ + +AsyncResponseStream::AsyncResponseStream(const String& contentType, size_t bufferSize){ + _code = 200; + _contentLength = 0; + _contentType = contentType; + _content = new cbuf(bufferSize); +} + +AsyncResponseStream::~AsyncResponseStream(){ + delete _content; +} + +size_t AsyncResponseStream::_fillBuffer(uint8_t *buf, size_t maxLen){ + return _content->read((char*)buf, maxLen); +} + +size_t AsyncResponseStream::write(const uint8_t *data, size_t len){ + if(_started()) + return 0; + + if(len > _content->room()){ + size_t needed = len - _content->room(); + _content->resizeAdd(needed); + } + size_t written = _content->write((const char*)data, len); + _contentLength += written; + return written; +} + +size_t AsyncResponseStream::write(uint8_t data){ + return write(&data, 1); +} diff --git a/mock-api/server.js b/mock-api/server.js index e42412c42..2f53de7a0 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'; @@ -315,12 +314,13 @@ const EMSESP_DEVICEDATA_ENDPOINT = REST_ENDPOINT_ROOT + 'deviceData'; const EMSESP_DEVICEENTITIES_ENDPOINT = REST_ENDPOINT_ROOT + 'deviceEntities'; const EMSESP_STATUS_ENDPOINT = REST_ENDPOINT_ROOT + 'status'; const EMSESP_BOARDPROFILE_ENDPOINT = REST_ENDPOINT_ROOT + 'boardProfile'; -const EMSESP_WRITE_VALUE_ENDPOINT = REST_ENDPOINT_ROOT + 'writeValue'; -const EMSESP_WRITE_SENSOR_ENDPOINT = REST_ENDPOINT_ROOT + 'writeSensor'; -const EMSESP_WRITE_ANALOG_ENDPOINT = REST_ENDPOINT_ROOT + 'writeAnalog'; +const EMSESP_WRITE_VALUE_ENDPOINT = REST_ENDPOINT_ROOT + 'writeDeviceValue'; +const EMSESP_WRITE_SENSOR_ENDPOINT = REST_ENDPOINT_ROOT + 'writeTemperatureSensor'; +const EMSESP_WRITE_ANALOG_ENDPOINT = REST_ENDPOINT_ROOT + 'writeAnalogSensor'; const EMSESP_CUSTOM_ENTITIES_ENDPOINT = REST_ENDPOINT_ROOT + 'customEntities'; const EMSESP_RESET_CUSTOMIZATIONS_ENDPOINT = REST_ENDPOINT_ROOT + 'resetCustomizations'; const EMSESP_WRITE_SCHEDULE_ENDPOINT = REST_ENDPOINT_ROOT + 'schedule'; +const EMSESP_WRITE_ENTITIES_ENDPOINT = REST_ENDPOINT_ROOT + 'entities'; settings = { locale: 'en', @@ -359,17 +359,18 @@ settings = { fahrenheit: false }; +// this is used in customizations const emsesp_devices = { devices: [ { - i: 1, + i: 2, s: 'Thermostat (RC20/Moduline 300)', t: 5, tn: 'thermostat' }, { - i: 2, - s: 'Boiler (Nefit GBx72/Trendline/Cerapur/Greenstar Si/27i)', + i: 7, + s: 'Boiler (GBx72/Trendline/Cerapur/Greenstar Si/27i)', t: 4, tn: 'boiler' }, @@ -387,63 +388,109 @@ const emsesp_coredata = { // devices: [], devices: [ { - id: '2', + id: 7, t: 4, tn: 'Boiler', b: 'Nefit', n: 'GBx72/Trendline/Cerapur/Greenstar Si/27i', d: 8, p: 123, - v: '06.01', - e: 68 + v: '06.01' }, { - id: '1', + id: 3, + t: 4, + tn: 'Boiler', + b: 'Buderus', + n: 'GB125/GB135/MC10', + d: 8, + p: 123, + v: '06.01' + }, + { + id: 1, + t: 5, + tn: 'Thermostat', + b: 'Buderus', + n: 'RC35', + d: 24, + p: 86, + v: '04.01' + }, + { + id: 2, t: 5, tn: 'Thermostat', b: '', n: 'RC20/Moduline 300', d: 23, p: 77, - v: '03.03', - e: 5 + v: '03.03' }, { - id: '4', + id: 4, t: 5, tn: 'Thermostat', b: 'Buderus', n: 'RC100/Moduline 1000/1010', d: 16, p: 165, - v: '04.01', - e: 3 + v: '04.01' + }, + { + id: 5, + t: 6, + tn: 'Mixer Module', + b: 'Buderus', + n: 'MM10', + d: 32, + p: 69, + v: '01.01' + }, + { + id: 6, + t: 7, + tn: 'Solar Module', + b: 'Buderus', + n: 'SM10', + d: 48, + p: 73, + v: '01.02' + }, + { + id: 99, + t: 17, + tn: 'Custom', + b: '', + n: 'User defined entities', + d: 1, + p: 1, + v: '' } - ], - s_n: 'Sensors', - active_sensors: 8, - analog_enabled: true + ] }; const emsesp_sensordata = { - sensors: [ + // ts: [], + ts: [ { id: '28-233D-9497-0C03', n: 'Dallas 1', t: 25.7, o: 1.2, u: 1 }, { id: '28-243D-7437-1E3A', n: 'Dallas 2 outside', t: 26.1, o: 0, u: 1 }, { id: '28-243E-7437-1E3B', n: 'Zolder', t: 27.1, o: 0, u: 16 }, { id: '28-183D-1892-0C33', n: 'Roof', o: 2, u: 1 } ], - // sensors: [], - analogs: [ - { id: '1', g: 36, n: 'motor', v: 0, u: 0, o: 17, f: 0, t: 0 }, - { id: '2', g: 37, n: 'External switch', v: 13, u: 0, o: 17, f: 0, t: 1 }, - { id: '3', g: 39, n: 'Pulse count', v: 144, u: 0, o: 0, f: 0, t: 2 }, - { id: '4', g: 40, n: 'Pressure', v: 16, u: 17, o: 0, f: 0, t: 3 } - ] - // analogs: [], + // as: [], + as: [ + { id: 1, g: 36, n: 'motor', v: 0, u: 0, o: 17, f: 0, t: 0, d: false }, + { id: 2, g: 37, n: 'External switch', v: 13, u: 0, o: 17, f: 0, t: 1, d: false }, + { id: 3, g: 39, n: 'Pulse count', v: 144, u: 0, o: 0, f: 0, t: 2, d: false }, + { id: 4, g: 40, n: 'Pressure', v: 16, u: 17, o: 0, f: 0, t: 3, d: false } + ], + analog_enabled: true }; const status = { status: 0, + // status: 2, tx_mode: 1, uptime: 77186, num_devices: 2, @@ -461,45 +508,20 @@ const status = { }; // Dashboard data -const emsesp_devicedata_1 = { - label: 'Thermostat: RC20/Moduline 300', - data: [ - { - v: '(0)', - u: 0, - id: '08my custom error code' - }, - { - v: '14:54:39 06/06/2021', - u: 0, - id: '00date/time' - }, - { - v: 18, - u: 1, - id: '00hc1 selected room temperature', - c: 'hc1/seltemp' - }, - { - v: 22.6, - u: 1, - id: '00hc1 current room temperature' - }, - { - v: 'auto', - u: 0, - id: '00hc1 mode', - c: 'hc1/mode' - } - ] -}; +// 7 - Nefit Trendline boiler +// 1 - RC35 thermo +// 2 - RC20 thermo +// 3 - Buderus GB125 boiler +// 4 - RC100 themo +// 5 - Mixer MM10 +// 6 - Solar SM10 +// 99 - Custom -const emsesp_devicedata_2 = { - label: 'Boiler: Nefit GBx72/Trendline/Cerapur/Greenstar Si/27i', +const emsesp_devicedata_7 = { data: [ { v: '', u: 0, id: '08reset', c: 'reset', l: ['-', 'maintenance', 'error'] }, - { v: 'false', u: 0, id: '08heating active' }, - { v: 'false', u: 0, id: '04tapwater active' }, + { v: 'off', u: 0, id: '08heating active' }, + { v: 'off', u: 0, id: '04tapwater active' }, { v: 5, u: 1, id: '04selected flow temperature', c: 'selflowtemp' }, { v: 0, u: 3, id: '0Eburner selected max power', c: 'selburnpow' }, { v: 0, u: 3, id: '00heating pump modulation' }, @@ -507,14 +529,14 @@ const emsesp_devicedata_2 = { { v: 52.7, u: 1, id: '00return temperature' }, { v: 1.3, u: 10, id: '00system pressure' }, { v: 54.9, u: 1, id: '00actual boiler temperature' }, - { v: 'false', u: 0, id: '00gas' }, - { v: 'false', u: 0, id: '00gas stage 2' }, + { v: 'off', u: 0, id: '00gas' }, + { v: 'off', u: 0, id: '00gas stage 2' }, { v: 0, u: 9, id: '00flame current' }, - { v: 'false', u: 0, id: '00heating pump' }, - { v: 'false', u: 0, id: '00fan' }, - { v: 'false', u: 0, id: '00ignition' }, - { v: 'false', u: 0, id: '00oil preheating' }, - { v: 'true', u: 0, id: '00heating activated', c: 'heatingactivated', l: ['off', 'on'] }, + { v: 'off', u: 0, id: '00heating pump' }, + { v: 'off', u: 0, id: '00fan' }, + { v: 'off', u: 0, id: '00ignition' }, + { v: 'off', u: 0, id: '00oil preheating' }, + { v: 'on', u: 0, id: '00heating activated', c: 'heatingactivated', l: ['off', 'on'] }, { v: 80, u: 1, id: '00heating temperature', c: 'heatingtemp' }, { v: 70, u: 3, id: '00burner pump max power', c: 'pumpmodmax' }, { v: 30, u: 3, id: '00burner pump min power', c: 'pumpmodmin' }, @@ -538,14 +560,14 @@ const emsesp_devicedata_2 = { { v: 'manual', u: 0, id: '00maintenance scheduled', c: 'maintenance', l: ['off', 'time', 'date', 'manual'] }, { v: 6000, u: 7, id: '00time to next maintenance', c: 'maintenancetime' }, { v: '01.01.2012', u: 0, id: '00next maintenance date', c: 'maintenancedate', o: 'Format: < dd.mm.yyyy >' }, - { v: 'true', u: 0, id: '00dhw turn on/off', c: 'wwtapactivated', l: ['off', 'on'] }, + { v: 'on', u: 0, id: '00dhw turn on/off', c: 'wwtapactivated', l: ['off', 'on'] }, { v: 62, u: 1, id: '00dhw set temperature' }, { v: 60, u: 1, id: '00dhw selected temperature', c: 'wwseltemp' }, { v: 'flow', u: 0, id: '00dhw type' }, { v: 'hot', u: 0, id: '00dhw comfort', c: 'wwcomfort', l: ['hot', 'eco', 'intelligent'] }, { v: 40, u: 2, id: '00dhw flow temperature offset', c: 'wwflowtempoffset' }, { v: 100, u: 3, id: '00dhw max power', c: 'wwmaxpower' }, - { v: 'false', u: 0, id: '00dhw circulation pump available', c: 'wwcircpump', l: ['off', 'on'] }, + { v: 'off', u: 0, id: '00dhw circulation pump available', c: 'wwcircpump', l: ['off', 'on'] }, { v: '3-way valve', u: 0, id: '00dhw charging type' }, { v: -5, u: 2, id: '00dhw hysteresis on temperature', c: 'wwhyston' }, { v: 0, u: 2, id: '00dhw hysteresis off temperature', c: 'wwhystoff' }, @@ -557,26 +579,972 @@ const emsesp_devicedata_2 = { c: 'wwcircmode', l: ['off', '1x3min', '2x3min', '3x3min', '4x3min', '5x3min', '6x3min', 'continuous'] }, - { v: 'false', u: 0, id: '00dhw circulation active', c: 'wwcirc', l: ['off', 'on'] }, + { v: 'off', u: 0, id: '00dhw circulation active', c: 'wwcirc', l: ['off', 'on'] }, { v: 47.3, u: 1, id: '00dhw current intern temperature' }, { v: 0, u: 4, id: '00dhw current tap water flow' }, { v: 47.3, u: 1, id: '00dhw storage intern temperature' }, - { v: 'true', u: 0, id: '00dhw activated', c: 'wwactivated', l: ['off', 'on'] }, - { v: 'false', u: 0, id: '00dhw one time charging', c: 'wwonetime', l: ['off', 'on'] }, - { v: 'false', u: 0, id: '00dhw disinfecting', c: 'wwdisinfecting', l: ['off', 'on'] }, - { v: 'false', u: 0, id: '00dhw charging' }, - { v: 'false', u: 0, id: '00dhw recharging' }, - { v: 'true', u: 0, id: '00dhw temperature ok' }, - { v: 'false', u: 0, id: '00dhw active' }, - { v: 'true', u: 0, id: '00dhw 3way valve active' }, + { v: 'on', u: 0, id: '00dhw activated', c: 'wwactivated', l: ['off', 'on'] }, + { v: 'off', u: 0, id: '00dhw one time charging', c: 'wwonetime', l: ['off', 'on'] }, + { v: 'off', u: 0, id: '00dhw disinfecting', c: 'wwdisinfecting', l: ['off', 'on'] }, + { v: 'off', u: 0, id: '00dhw charging' }, + { v: 'off', u: 0, id: '00dhw recharging' }, + { v: 'on', u: 0, id: '00dhw temperature ok' }, + { v: 'off', u: 0, id: '00dhw active' }, + { v: 'on', u: 0, id: '00dhw 3way valve active' }, { v: 0, u: 3, id: '00dhw set pump power' }, { v: 288768, u: 0, id: '00dhw starts' }, { v: 102151, u: 8, id: '00dhw active time' } ] }; +const emsesp_devicedata_1 = { + data: [ + { + v: '22(816) 01.05.2023 13:07 (1 min)', + u: 0, + id: '00last error code' + }, + { + v: '05.05.2023 09:44', + u: 0, + id: '00date/time', + c: 'datetime', + h: '< NTP | dd.mm.yyyy-hh:mm:ss-day(0-6)-dst(0/1) >' + }, + { + v: -2.4, + u: 2, + id: '00internal temperature offset', + c: 'intoffset', + m: -5, + x: 5, + s: '0.1' + }, + { + v: -11, + u: 1, + id: '00minimal external temperature', + c: 'minexttemp', + m: -30, + x: 0, + s: '1' + }, + { + v: 29.5, + u: 1, + id: '00temperature sensor 1' + }, + { + v: 32.5, + u: 1, + id: '00temperature sensor 2' + }, + { + v: 'on', + u: 0, + id: '00damping outdoor temperature', + c: 'damping', + l: ['off', 'on'] + }, + { + v: 13, + u: 1, + id: '00damped outdoor temperature' + }, + { + v: 'medium', + u: 0, + id: '00building type', + c: 'building', + l: ['light', 'medium', 'heavy'] + }, + { + v: 'auto', + u: 0, + id: '00dhw mode', + c: 'wwmode', + l: ['off', 'on', 'auto'] + }, + { + v: 'off', + u: 0, + id: '00dhw circulation pump mode', + c: 'wwcircmode', + l: ['off', 'on', 'auto'] + }, + { + v: 'std prog', + u: 0, + id: '00dhw program', + c: 'wwprogmode', + l: ['std prog', 'own prog'] + }, + { + v: 'std prog', + u: 0, + id: '00dhw circulation program', + c: 'wwcircprog', + l: ['std prog', 'own prog'] + }, + { + v: 'off', + u: 0, + id: '00dhw disinfecting', + c: 'wwdisinfecting', + l: ['off', 'on'] + }, + { + v: 'tu', + u: 0, + id: '00dhw disinfection day', + c: 'wwdisinfectday', + l: ['mo', 'tu', 'we', 'th', 'fr', 'sa', 'su', 'all'] + }, + { + v: 1, + u: 0, + id: '00dhw disinfection hour', + c: 'wwdisinfecthour', + m: 0, + x: 23, + s: '1' + }, + { + v: 60, + u: 1, + id: '00dhw maximum temperature', + c: 'wwmaxtemp', + m: 60, + x: 80, + s: '1' + }, + { + v: 'on', + u: 0, + id: '00dhw one time key function', + c: 'wwonetimekey', + l: ['off', 'on'] + }, + { + v: '00 mo 06:00 on', + u: 0, + id: '00dhw program switchtime', + c: 'wwswitchtime', + h: ' [ not_set | day hh:mm on|off ]' + }, + { + v: '00 mo 06:30 on', + u: 0, + id: '00dhw circulation program switchtime', + c: 'wwcircswitchtime', + h: ' [ not_set | day hh:mm on|off ]' + }, + { + v: '01.01.2000-01.01.2000', + u: 0, + id: '00dhw holiday dates', + c: 'wwholidays', + h: '< dd.mm.yyyy-dd.mm.yyyy >' + }, + { + v: '01.01.2019-12.01.2019', + u: 0, + id: '00dhw vacation dates', + c: 'wwvacations', + h: '< dd.mm.yyyy-dd.mm.yyyy >' + }, + { + v: 21, + u: 1, + id: '00hc2 selected room temperature', + c: 'hc2/seltemp', + m: 0, + x: 30, + s: '0.5' + }, + { + v: 'auto', + u: 0, + id: '00hc2 mode', + c: 'hc2/mode', + l: ['night', 'day', 'auto'] + }, + { + v: 'day', + u: 0, + id: '00hc2 mode type' + }, + { + v: 21, + u: 1, + id: '00hc2 day temperature', + c: 'hc2/daytemp', + m: 5, + x: 30, + s: '0.5' + }, + { + v: 17, + u: 1, + id: '00hc2 night temperature', + c: 'hc2/nighttemp', + m: 5, + x: 30, + s: '0.5' + }, + { + v: 58, + u: 1, + id: '00hc2 design temperature', + c: 'hc2/designtemp', + m: 30, + x: 90, + s: '1' + }, + { + v: 0, + u: 2, + id: '00hc2 offset temperature', + c: 'hc2/offsettemp', + m: -5, + x: 5, + s: '0.5' + }, + { + v: 15, + u: 1, + id: '00hc2 holiday temperature', + c: 'hc2/holidaytemp', + m: 5, + x: 30, + s: '0.5' + }, + { + v: 34, + u: 1, + id: '00hc2 target flow temperature' + }, + { + v: 17, + u: 1, + id: '00hc2 summer temperature', + c: 'hc2/summertemp', + m: 9, + x: 25, + s: '1' + }, + { + v: 'winter', + u: 0, + id: '00hc2 summer mode' + }, + { + v: 'off', + u: 0, + id: '00hc2 holiday mode' + }, + { + v: -10, + u: 1, + id: '00hc2 nofrost temperature', + c: 'hc2/nofrosttemp', + m: -20, + x: 10, + s: '1' + }, + { + v: 'outdoor', + u: 0, + id: '00hc2 nofrost mode', + c: 'hc2/nofrostmode', + l: ['off', 'outdoor', 'room'] + }, + { + v: 0, + u: 2, + id: '00hc2 room influence', + c: 'hc2/roominfluence', + m: 0, + x: 10, + s: '1' + }, + { + v: 15, + u: 1, + id: '00hc2 min flow temperature', + c: 'hc2/minflowtemp', + m: 5, + x: 70, + s: '1' + }, + { + v: 85, + u: 1, + id: '00hc2 max flow temperature', + c: 'hc2/maxflowtemp', + m: 30, + x: 90, + s: '1' + }, + { + v: 0, + u: 2, + id: '00hc2 flow temperature offset for mixer', + c: 'hc2/flowtempoffset', + m: 0, + x: 20, + s: '1' + }, + { + v: 'radiator', + u: 0, + id: '00hc2 heating type', + c: 'hc2/heatingtype', + l: ['off', 'radiator', 'convector', 'floor'] + }, + { + v: 'outdoor', + u: 0, + id: '00hc2 reduce mode', + c: 'hc2/reducemode', + l: ['nofrost', 'reduce', 'room', 'outdoor'] + }, + { + v: 'outdoor', + u: 0, + id: '00hc2 control mode', + c: 'hc2/controlmode', + l: ['outdoor', 'room'] + }, + { + v: 'RC3x', + u: 0, + id: '00hc2 control device', + c: 'hc2/control', + l: ['off', 'RC20', 'RC3x'] + }, + { + v: '01.01.2000-01.01.2000', + u: 0, + id: '00hc2 holiday dates', + c: 'hc2/holidays', + h: '< dd.mm.yyyy-dd.mm.yyyy >' + }, + { + v: '01.01.2020-12.01.2020', + u: 0, + id: '00hc2 vacation dates', + c: 'hc2/vacations', + h: '< dd.mm.yyyy-dd.mm.yyyy >' + }, + { + v: 'own 1', + u: 0, + id: '00hc2 program', + c: 'hc2/program', + l: ['own 1', 'family', 'morning', 'evening', 'am', 'pm', 'midday', 'singles', 'seniors', 'new', 'own 2'] + }, + { + v: 0, + u: 7, + id: '00hc2 pause time', + c: 'hc2/pause', + m: 0, + x: 99, + s: '1' + }, + { + v: 0, + u: 7, + id: '00hc2 party time', + c: 'hc2/party', + m: 0, + x: 99, + s: '1' + }, + { + v: 0, + u: 1, + id: '00hc2 temporary set temperature automode', + c: 'hc2/tempautotemp', + m: 0, + x: 30, + s: '0.5' + }, + { + v: -20, + u: 1, + id: '00hc2 no reduce below temperature', + c: 'hc2/noreducetemp', + m: -30, + x: 10, + s: '1' + }, + { + v: 8, + u: 1, + id: '00hc2 off/reduce switch temperature', + c: 'hc2/reducetemp', + m: -20, + x: 10, + s: '1' + }, + { + v: 5, + u: 1, + id: '00hc2 vacations off/reduce switch temperature', + c: 'hc2/vacreducetemp', + m: -20, + x: 10, + s: '1' + }, + { + v: 'outdoor', + u: 0, + id: '00hc2 vacations reduce mode', + c: 'hc2/vacreducemode', + l: ['nofrost', 'reduce', 'room', 'outdoor'] + }, + { + v: 'off', + u: 0, + id: '00hc2 dhw priority', + c: 'hc2/wwprio', + l: ['off', 'on'] + }, + { + v: '00 mo 05:50 on', + u: 0, + id: '00hc2 own1 program switchtime', + c: 'hc2/switchtime1', + h: ' [ not_set | day hh:mm on|off ]' + }, + { + v: '00 mo 06:30 on', + u: 0, + id: '00hc2 own2 program switchtime', + c: 'hc2/switchtime2', + h: ' [ not_set | day hh:mm on|off ]' + } + ] +}; + +const emsesp_devicedata_2 = { + data: [ + { + v: '(0)', + u: 0, + id: '08my custom error code' + }, + { + v: '14:54:39 06/06/2021', + u: 0, + id: '00date/time' + }, + { + v: 18.2, + u: 1, + id: '00Chosen Room Temperature', + c: 'hc1/seltemp', + m: 5, + x: 52, + s: 0.5 + }, + { + v: 22.6, + u: 1, + id: '00hc1 current room temperature' + }, + { + v: 'auto', + u: 0, + id: '00hc1 mode', + c: 'hc1/mode', + l: ['off', 'on', 'auto'] + }, + { + v: '00 mo 00:00 T1', + u: 0, + id: '00hc1 program switchtime', + c: 'hc1/switchtime', + h: ' [ not_set | day hh:mm Tn ]' + } + ] +}; + +const emsesp_devicedata_3 = { + data: [ + { + v: '', + u: 0, + id: '08reset', + c: 'reset', + l: ['-', 'maintenance', 'error'] + }, + { + v: 34, + u: 1, + id: '08selected flow temperature', + c: 'selflowtemp', + m: 0, + x: 90, + s: '1' + }, + { + v: 30.7, + u: 1, + id: '08current flow temperature' + }, + { + v: 176544, + u: 0, + id: '08burner starts' + }, + { + v: '6L(517) 18.01.2023 10:18 (0 min)', + u: 0, + id: '08last error code' + }, + { + v: 'off', + u: 0, + id: '00force heating off', + c: 'heatingoff', + l: ['off', 'on'] + }, + { + v: 'off', + u: 0, + id: '00heating active' + }, + { + v: 'off', + u: 0, + id: '00tapwater active' + }, + { + v: 0, + u: 3, + id: '00heating pump modulation' + }, + { + v: 15, + u: 1, + id: '00outside temperature' + }, + { + v: 30.7, + u: 1, + id: '00actual boiler temperature' + }, + { + v: 29, + u: 1, + id: '00exhaust temperature' + }, + { + v: 'off', + u: 0, + id: '00gas' + }, + { + v: 'off', + u: 0, + id: '00gas stage 2' + }, + { + v: 0, + u: 9, + id: '00flame current' + }, + { + v: 'off', + u: 0, + id: '00heating pump' + }, + { + v: 'off', + u: 0, + id: '00fan' + }, + { + v: 'off', + u: 0, + id: '00ignition' + }, + { + v: 'off', + u: 0, + id: '00oil preheating' + }, + { + v: 'on', + u: 0, + id: '00heating activated', + c: 'heatingactivated', + l: ['off', 'on'] + }, + { + v: 90, + u: 1, + id: '00heating temperature', + c: 'heatingtemp', + m: 0, + x: 90, + s: '1' + }, + { + v: 100, + u: 3, + id: '00boiler pump max power', + c: 'pumpmodmax', + m: 0, + x: 100, + s: '1' + }, + { + v: 100, + u: 3, + id: '00boiler pump min power', + c: 'pumpmodmin', + m: 0, + x: 100, + s: '1' + }, + { + v: 'deltaP-2', + u: 0, + id: '00boiler pump mode', + c: 'pumpmode', + l: ['proportional', 'deltaP-1', 'deltaP-2', 'deltaP-3', 'deltaP-4'] + }, + { + v: 6, + u: 8, + id: '00pump delay', + c: 'pumpdelay', + m: 0, + x: 60, + s: '1' + }, + { + v: 15, + u: 8, + id: '00burner min period', + c: 'burnminperiod', + m: 0, + x: 120, + s: '1' + }, + { + v: 0, + u: 3, + id: '00burner min power', + c: 'burnminpower', + m: 0, + x: 100, + s: '1' + }, + { + v: 100, + u: 3, + id: '00burner max power', + c: 'burnmaxpower', + m: 0, + x: 254, + s: '1' + }, + { + v: -8, + u: 2, + id: '00hysteresis on temperature', + c: 'boilhyston', + m: -20, + x: 0, + s: '1' + }, + { + v: 15, + u: 2, + id: '00hysteresis off temperature', + c: 'boilhystoff', + m: 0, + x: 20, + s: '1' + }, + { + v: -8, + u: 2, + id: '00hysteresis stage 2 on temperature', + c: 'boil2hyston', + m: -20, + x: 0, + s: '1' + }, + { + v: 8, + u: 2, + id: '00hysteresis stage 2 off temperature', + c: 'boil2hystoff', + m: 0, + x: 20, + s: '1' + }, + { + v: 34, + u: 1, + id: '00set flow temperature' + }, + { + v: 100, + u: 3, + id: '00burner set power' + }, + { + v: 100, + u: 3, + id: '00burner selected max power', + c: 'selburnpow', + m: 0, + x: 254, + s: '1' + }, + { + v: 0, + u: 3, + id: '00burner current power' + }, + { + v: 822273, + u: 8, + id: '00total burner operating time' + }, + { + v: 0, + u: 8, + id: '00burner stage 2 operating time' + }, + { + v: 787124, + u: 8, + id: '00total heat operating time' + }, + { + v: 173700, + u: 0, + id: '00burner starts heating' + }, + { + v: 5495341, + u: 8, + id: '00total UBA operating time' + }, + { + v: '0Y', + u: 0, + id: '00service code' + }, + { + v: 0, + u: 0, + id: '00service code number' + }, + { + v: 'H00', + u: 0, + id: '00maintenance message' + }, + { + v: 'date', + u: 0, + id: '00maintenance scheduled', + c: 'maintenance', + l: ['off', 'time', 'date', 'manual'] + }, + { + v: 6000, + u: 7, + id: '00time to next maintenance', + c: 'maintenancetime', + m: 0, + x: 31999, + s: '1' + }, + { + v: '30.06.2023', + u: 0, + id: '00next maintenance date', + c: 'maintenancedate', + h: '< dd.mm.yyyy >' + }, + { + v: 46, + u: 1, + id: '00dhw set temperature' + }, + { + v: 47, + u: 1, + id: '00dhw selected temperature', + c: 'wwseltemp', + m: 0, + x: 254, + s: '1' + }, + { + v: 'buffer', + u: 0, + id: '00dhw type' + }, + { + v: 'hot', + u: 0, + id: '00dhw comfort', + c: 'wwcomfort', + l: ['hot', 'eco', 'intelligent'] + }, + { + v: 40, + u: 2, + id: '00dhw flow temperature offset', + c: 'wwflowtempoffset', + m: 0, + x: 100, + s: '1' + }, + { + v: 'on', + u: 0, + id: '00dhw circulation pump available', + c: 'wwcircpump', + l: ['off', 'on'] + }, + { + v: 'chargepump', + u: 0, + id: '00dhw charging type' + }, + { + v: -5, + u: 2, + id: '00dhw hysteresis on temperature', + c: 'wwhyston', + m: -126, + x: 126, + s: '1' + }, + { + v: -1, + u: 2, + id: '00dhw hysteresis off temperature', + c: 'wwhystoff', + m: -126, + x: 126, + s: '1' + }, + { + v: 70, + u: 1, + id: '00dhw disinfection temperature', + c: 'wwdisinfectiontemp', + m: 0, + x: 254, + s: '1' + }, + { + v: 'continuous', + u: 0, + id: '00dhw circulation pump mode', + c: 'wwcircmode', + l: ['off', '1x3min', '2x3min', '3x3min', '4x3min', '5x3min', '6x3min', 'continuous'] + }, + { + v: 'off', + u: 0, + id: '00dhw circulation active', + c: 'wwcirc', + l: ['off', 'on'] + }, + { + v: 60.7, + u: 1, + id: '00dhw current intern temperature' + }, + { + v: 0, + u: 4, + id: '00dhw current tap water flow' + }, + { + v: 60.7, + u: 1, + id: '00dhw storage intern temperature' + }, + { + v: 'on', + u: 0, + id: '00dhw activated', + c: 'wwactivated', + l: ['off', 'on'] + }, + { + v: 'off', + u: 0, + id: '00dhw one time charging', + c: 'wwonetime', + l: ['off', 'on'] + }, + { + v: 'off', + u: 0, + id: '00dhw disinfecting', + c: 'wwdisinfecting', + l: ['off', 'on'] + }, + { + v: 'off', + u: 0, + id: '00dhw charging' + }, + { + v: 'off', + u: 0, + id: '00dhw recharging' + }, + { + v: 'on', + u: 0, + id: '00dhw temperature ok' + }, + { + v: 'off', + u: 0, + id: '00dhw active' + }, + { + v: 'off', + u: 0, + id: '00dhw 3-way valve active' + }, + { + v: 0, + u: 3, + id: '00dhw set pump power' + }, + { + v: 6976, + u: 0, + id: '00dhw starts' + }, + { + v: 80644, + u: 8, + id: '00dhw active time' + } + ] +}; + const emsesp_devicedata_4 = { - label: 'Thermostat: RC100/Moduline 1000/1010', data: [ { v: 16, @@ -594,7 +1562,233 @@ const emsesp_devicedata_4 = { v: 'off', u: 0, id: '02hc2 mode', - c: 'hc2/mode' + c: 'hc2/mode', + l: ['off', 'on', 'auto'] + } + ] +}; + +const emsesp_devicedata_5 = { + data: [ + { + v: 30, + u: 1, + id: '00hc2 flow temperature (TC1)' + }, + { + v: 100, + u: 3, + id: '00hc2 mixing valve actuator (VC1)' + }, + { + v: 34, + u: 1, + id: '00hc2 setpoint flow temperature', + c: 'hc2/flowsettemp', + m: 0, + x: 254, + s: '1' + }, + { + v: 'off', + u: 0, + id: '00hc2 pump status (PC1)', + c: 'hc2/pumpstatus', + l: ['off', 'on'] + }, + { + v: 'on', + u: 0, + id: '00hc2 activated', + c: 'hc2/activated', + l: ['off', 'on'] + }, + { + v: 120, + u: 14, + id: '00hc2 time to set valve', + c: 'hc2/valvesettime', + m: 10, + x: 120, + s: '10' + } + ] +}; + +const emsesp_devicedata_6 = { + data: [ + { + v: 43.9, + u: 1, + id: '00collector temperature (TS1)' + }, + { + v: 28.3, + u: 1, + id: '00cylinder bottom temperature (TS2)' + }, + { + v: 'on', + u: 0, + id: '00pump (PS1)' + }, + { + v: 181884, + u: 8, + id: '00pump working time' + }, + { + v: 90, + u: 1, + id: '00maximum cylinder temperature', + c: 'cylmaxtemp', + m: 0, + x: 254, + s: '1' + }, + { + v: 'off', + u: 0, + id: '00collector shutdown' + }, + { + v: 'off', + u: 0, + id: '00cyl heated' + }, + { + v: 32, + u: 3, + id: '00pump modulation (PS1)' + }, + { + v: 30, + u: 3, + id: '00minimum pump modulation', + c: 'pumpminmod', + m: 0, + x: 100, + s: '1' + }, + { + v: 10, + u: 2, + id: '00pump turn on difference', + c: 'turnondiff', + m: 0, + x: 254, + s: '1' + }, + { + v: 5, + u: 2, + id: '00pump turn off difference', + c: 'turnoffdiff', + m: 0, + x: 254, + s: '1' + }, + { + v: 899, + u: 12, + id: '00actual solar power' + }, + { + v: 94, + u: 6, + id: '00energy last hour' + }, + { + v: 3, + u: 4, + id: '00maximum solar flow', + c: 'maxflow', + m: 0, + x: 25, + s: '0.1' + }, + { + v: 37, + u: 1, + id: '00dhw minimum temperature', + c: 'wwmintemp', + m: 0, + x: 254, + s: '1' + }, + { + v: 'on', + u: 0, + id: '00solarmodule enabled', + c: 'solarenabled', + l: ['off', 'on'] + }, + { + v: 11, + u: 0, + id: '00unknown setting 3', + c: 'setting3', + m: 0, + x: 254, + s: '1' + }, + { + v: 2, + u: 0, + id: '00unknown setting 4', + c: 'setting4', + m: 0, + x: 254, + s: '1' + }, + { + v: 0, + u: 0, + id: '00unknown datafield 11' + }, + { + v: 1, + u: 0, + id: '00unknown datafield 12' + }, + { + v: 0, + u: 0, + id: '00unknown datafield 1' + }, + { + v: 0, + u: 0, + id: '00unknown datafield 0' + } + ] +}; + +const emsesp_devicedata_99 = { + data: [ + { + v: 5, + u: 1, + id: '00boiler_flowtemp', + c: 'boiler_flowtemp' + } + ] +}; + +// CUSTOM ENTITIES +let emsesp_entities = { + // entities: [] + entities: [ + { + id: 0, + device_id: 8, + type_id: 24, + offset: 0, + factor: 1, + name: 'boiler_flowtemp', + uom: 1, + value_type: 1, + writeable: true } ] }; @@ -643,7 +1837,7 @@ let emsesp_schedule = { // CUSTOMIZATIONS -const emsesp_deviceentities_1 = [ +const emsesp_deviceentities_2 = [ { v: '(0)', n: 'error code', @@ -661,7 +1855,7 @@ const emsesp_deviceentities_1 = [ }, { v: 18.2, - n: 'hc1 selected room temperature', + n: 'Chosen Room Temperature', id: 'hc1/seltemp', m: 0, mi: 5, @@ -684,7 +1878,7 @@ const emsesp_deviceentities_1 = [ } ]; -const emsesp_deviceentities_2 = [ +const emsesp_deviceentities_7 = [ { u: 0, n: '!reset', id: 'reset', m: 8, w: false }, { v: false, n: 'heating active', id: 'heatingactive', m: 8, w: false }, { v: false, n: 'tapwater active', id: 'tapwateractive', m: 4, w: false }, @@ -794,6 +1988,7 @@ const emsesp_deviceentities_4 = [ // LOG rest_server.get(FETCH_LOG_ENDPOINT, (req, res) => { const encoded = msgpack.encode(fetch_log); + console.log('fetchlog'); res.write(encoded, 'binary'); res.end(null, 'binary'); }); @@ -933,6 +2128,7 @@ rest_server.get(EMSESP_CORE_DATA_ENDPOINT, (req, res) => { }); rest_server.get(EMSESP_SENSOR_DATA_ENDPOINT, (req, res) => { console.log('send back sensor data...'); + // console.log(emsesp_sensordata); res.json(emsesp_sensordata); }); rest_server.get(EMSESP_DEVICES_ENDPOINT, (req, res) => { @@ -948,40 +2144,63 @@ rest_server.get(EMSESP_STATUS_ENDPOINT, (req, res) => { rest_server.post(EMSESP_DEVICEDATA_ENDPOINT, (req, res) => { const id = req.body.id; console.log('send back device data for ' + id); + let data = {}; + if (id === 7) { + data = emsesp_devicedata_7; + } if (id === 1) { - const encoded = msgpack.encode(emsesp_devicedata_1); - res.write(encoded, 'binary'); - res.end(null, 'binary'); + data = emsesp_devicedata_1; } if (id === 2) { - const encoded = msgpack.encode(emsesp_devicedata_2); - res.write(encoded, 'binary'); - res.end(null, 'binary'); + data = emsesp_devicedata_2; + } + if (id === 3) { + data = emsesp_devicedata_3; } if (id === 4) { - const encoded = msgpack.encode(emsesp_devicedata_4); - res.write(encoded, 'binary'); - res.end(null, 'binary'); + data = emsesp_devicedata_4; } + if (id === 5) { + data = emsesp_devicedata_5; + } + if (id === 6) { + data = emsesp_devicedata_6; + } + if (id === 99) { + data = emsesp_devicedata_99; + } + res.write(msgpack.encode(data), 'binary'); + res.end(null, 'binary'); }); rest_server.post(EMSESP_DEVICEENTITIES_ENDPOINT, (req, res) => { const id = req.body.id; + let data = null; + + if (id === 7) { + data = emsesp_deviceentities_7; + } if (id === 1) { - const encoded = msgpack.encode(emsesp_deviceentities_1); - res.write(encoded, 'binary'); - res.end(null, 'binary'); + data = emsesp_deviceentities_1; } if (id === 2) { - const encoded = msgpack.encode(emsesp_deviceentities_2); - res.write(encoded, 'binary'); - res.end(null, 'binary'); + data = emsesp_deviceentities_2; + } + if (id === 3) { + data = emsesp_deviceentities_3; } if (id === 4) { - const encoded = msgpack.encode(emsesp_deviceentities_4); - res.write(encoded, 'binary'); - res.end(null, 'binary'); + data = emsesp_deviceentities_4; } + if (id === 5) { + data = emsesp_deviceentities_5; + } + if (id === 6) { + data = emsesp_deviceentities_6; + } + + res.write(msgpack.encode(data), 'binary'); + res.end(null, 'binary'); }); function updateMask(entity, de, dd) { @@ -989,13 +2208,14 @@ function updateMask(entity, de, dd) { // strip of any min/max ranges const shortname_with_customname = entity.slice(2).split('>')[0]; - const shortname = shortname_with_customname.split('|')[0]; const new_custom_name = shortname_with_customname.split('|')[1]; + const has_min_max = entity.slice(2).split('>')[1]; // find in de de_objIndex = de.findIndex((obj) => obj.id === shortname); if (de_objIndex !== -1) { + // get current name if (de[de_objIndex].cn) { fullname = de[de_objIndex].cn; } else { @@ -1016,7 +2236,7 @@ function updateMask(entity, de, dd) { // see if the custom name has changed const old_custom_name = dd.data[dd_objIndex].cn; - console.log('comparing old ' + old_custom_name + ' with new ' + new_custom_name); + console.log('comparing names, old (' + old_custom_name + ') with new (' + new_custom_name + ')'); if (old_custom_name !== new_custom_name) { changed = true; new_fullname = new_custom_name; @@ -1025,12 +2245,33 @@ function updateMask(entity, de, dd) { new_fullname = fullname; } - if (changed) { + // see if min or max has changed + // get current min/max values if they exist + const current_min = dd.data[dd_objIndex].min; + const current_max = dd.data[dd_objIndex].max; + new_min = current_min; + new_max = current_max; + if (has_min_max) { + new_min = parseInt(has_min_max.split('<')[0]); + new_max = parseInt(has_min_max.split('<')[1]); + if (current_min !== new_min || current_max !== new_max) { + changed = true; + console.log('min/max has changed to ' + new_min + '/' + new_max); + } + } + + if (changed === true) { console.log( 'Updating ' + dd.data[dd_objIndex].id + ' -> ' + current_mask.toString(16).padStart(2, '0') + new_fullname ); de[de_objIndex].m = current_mask; de[de_objIndex].cn = new_fullname; + if (new_min) { + de[de_objIndex].mi = new_min; + } + if (new_max) { + de[de_objIndex].ma = new_max; + } dd.data[dd_objIndex].id = current_mask.toString(16).padStart(2, '0') + new_fullname; dd.data[dd_objIndex].cn = new_fullname; } @@ -1052,12 +2293,20 @@ rest_server.post(EMSESP_CUSTOM_ENTITIES_ENDPOINT, (req, res) => { console.log('customization id = ' + id); console.log(req.body.entity_ids); for (const entity of req.body.entity_ids) { - if (id === 1) { + if (id === 7) { + updateMask(entity, emsesp_deviceentities_7, emsesp_devicedata_7); + } else if (id === 1) { updateMask(entity, emsesp_deviceentities_1, emsesp_devicedata_1); } else if (id === 2) { updateMask(entity, emsesp_deviceentities_2, emsesp_devicedata_2); + } else if (id === 3) { + updateMask(entity, emsesp_deviceentities_3, emsesp_devicedata_3); } else if (id === 4) { updateMask(entity, emsesp_deviceentities_4, emsesp_devicedata_4); + } else if (id === 5) { + updateMask(entity, emsesp_deviceentities_5, emsesp_devicedata_5); + } else if (id === 6) { + updateMask(entity, emsesp_deviceentities_6, emsesp_devicedata_6); } } res.sendStatus(200); @@ -1070,35 +2319,67 @@ rest_server.post(EMSESP_WRITE_SCHEDULE_ENDPOINT, (req, res) => { res.sendStatus(200); }); +rest_server.post(EMSESP_WRITE_ENTITIES_ENDPOINT, (req, res) => { + console.log('write entities'); + console.log(req.body.entities); + emsesp_entities = req.body; + res.sendStatus(200); +}); + rest_server.post(EMSESP_WRITE_VALUE_ENDPOINT, (req, res) => { const devicevalue = req.body.devicevalue; const id = req.body.id; + if (id === 7) { + console.log('Write device value for: ' + JSON.stringify(devicevalue)); + objIndex = emsesp_devicedata_7.data.findIndex((obj) => obj.c == devicevalue.c); + emsesp_devicedata_7.data[objIndex] = devicevalue; + } if (id === 1) { - console.log('Write device value for Thermostat: ' + JSON.stringify(devicevalue)); + console.log('Write device value for: ' + JSON.stringify(devicevalue)); objIndex = emsesp_devicedata_1.data.findIndex((obj) => obj.c == devicevalue.c); emsesp_devicedata_1.data[objIndex] = devicevalue; } if (id === 2) { - console.log('Write device value for Boiler: ' + JSON.stringify(devicevalue)); + console.log('Write device value for: ' + JSON.stringify(devicevalue)); objIndex = emsesp_devicedata_2.data.findIndex((obj) => obj.c == devicevalue.c); emsesp_devicedata_2.data[objIndex] = devicevalue; } + if (id === 3) { + console.log('Write device value for: ' + JSON.stringify(devicevalue)); + objIndex = emsesp_devicedata_3.data.findIndex((obj) => obj.c == devicevalue.c); + emsesp_devicedata_3.data[objIndex] = devicevalue; + } if (id === 4) { - console.log('Write device value for Thermostat2: ' + JSON.stringify(devicevalue)); + console.log('Write device value for: ' + JSON.stringify(devicevalue)); objIndex = emsesp_devicedata_4.data.findIndex((obj) => obj.c == devicevalue.c); emsesp_devicedata_4.data[objIndex] = devicevalue; } + if (id === 5) { + console.log('Write device value for: ' + JSON.stringify(devicevalue)); + objIndex = emsesp_devicedata_5.data.findIndex((obj) => obj.c == devicevalue.c); + emsesp_devicedata_5.data[objIndex] = devicevalue; + } + if (id === 6) { + console.log('Write device value for: ' + JSON.stringify(devicevalue)); + objIndex = emsesp_devicedata_6.data.findIndex((obj) => obj.c == devicevalue.c); + emsesp_devicedata_6.data[objIndex] = devicevalue; + } + if (id === 99) { + console.log('Write device value for: ' + JSON.stringify(devicevalue)); + objIndex = emsesp_devicedata_99.data.findIndex((obj) => obj.c == devicevalue.c); + emsesp_devicedata_99.data[objIndex] = devicevalue; + } res.sendStatus(200); }); rest_server.post(EMSESP_WRITE_SENSOR_ENDPOINT, (req, res) => { - const sensor = req.body; - console.log('Write sensor: ' + JSON.stringify(sensor)); - objIndex = emsesp_sensordata.sensors.findIndex((obj) => obj.id == sensor.id); + const ts = req.body; + console.log('Write temperaure sensor: ' + JSON.stringify(ts)); + objIndex = emsesp_sensordata.ts.findIndex((obj) => obj.id == ts.id); if (objIndex !== -1) { - emsesp_sensordata.sensors[objIndex].n = sensor.name; - emsesp_sensordata.sensors[objIndex].o = sensor.offset; + emsesp_sensordata.ts[objIndex].n = ts.name; + emsesp_sensordata.ts[objIndex].o = ts.offset; } else { console.log('not found'); } @@ -1106,32 +2387,37 @@ rest_server.post(EMSESP_WRITE_SENSOR_ENDPOINT, (req, res) => { }); rest_server.post(EMSESP_WRITE_ANALOG_ENDPOINT, (req, res) => { - const analog = req.body; - console.log('Write analog: ' + JSON.stringify(analog)); - objIndex = emsesp_sensordata.analogs.findIndex((obj) => obj.g == analog.gpio); + const as = req.body; + console.log('Write analog sensor: ' + JSON.stringify(as)); + objIndex = emsesp_sensordata.as.findIndex((obj) => obj.g == as.gpio); if (objIndex === -1) { - console.log('new analog'); - emsesp_sensordata.analogs.push({ - id: analog.i.toString(), - g: analog.gpio, - n: analog.name, - f: analog.factor, - o: analog.offset, - u: analog.uom, - t: analog.type + console.log('new analog entry found'); + emsesp_sensordata.as.push({ + id: as.id, + g: as.gpio, + n: as.name, + f: as.factor, + o: as.offset, + u: as.uom, + t: as.type, + d: as.deleted }); } else { - if (analog.type === -1) { - console.log('removing analog gpio' + analog.gpio + ' index ' + objIndex); - emsesp_sensordata.analogs[objIndex].t = -1; + if (as.deleted) { + console.log('removing analog gpio' + as.gpio + ' index ' + objIndex); + emsesp_sensordata.as[objIndex].d = true; + var filtered = emsesp_sensordata.as.filter(function (value, index, arr) { + return !value.d; + }); + emsesp_sensordata.as = filtered; } else { - console.log('updating analog gpio' + analog.gpio + ' index ' + objIndex); - emsesp_sensordata.analogs[objIndex].n = analog.name; - emsesp_sensordata.analogs[objIndex].f = analog.factor; - emsesp_sensordata.analogs[objIndex].o = analog.offset; - emsesp_sensordata.analogs[objIndex].u = analog.uom; - emsesp_sensordata.analogs[objIndex].t = analog.type; + console.log('updating analog gpio' + as.gpio + ' index ' + objIndex); + emsesp_sensordata.as[objIndex].n = as.name; + emsesp_sensordata.as[objIndex].f = as.factor; + emsesp_sensordata.as[objIndex].o = as.offset; + emsesp_sensordata.as[objIndex].u = as.uom; + emsesp_sensordata.as[objIndex].t = as.type; } } @@ -1278,7 +2564,7 @@ const emsesp_info = { 'uptime (seconds)': 110434, freemem: 131, 'reset reason': 'Software reset CPU / Software reset CPU', - 'Dallas sensors': 3 + 'Sensor sensors': 3 }, Network: { connection: 'Ethernet', @@ -1301,8 +2587,8 @@ const emsesp_info = { MQTT: 'connected', 'MQTT publishes': 46336, 'MQTT publish fails': 0, - 'Dallas reads': 22086, - 'Dallas fails': 0 + 'Sensor reads': 22086, + 'Sensor fails': 0 }, Devices: [ { @@ -1369,6 +2655,12 @@ rest_server.get(GET_SCHEDULE_ENDPOINT, (req, res) => { res.json(emsesp_schedule); }); +const GET_ENTITIES_ENDPOINT = REST_ENDPOINT_ROOT + 'entities'; +rest_server.get(GET_ENTITIES_ENDPOINT, (req, res) => { + console.log('Sending Entities data'); + res.json(emsesp_entities); +}); + // start server const expressServer = rest_server.listen(port, () => console.log(`EMS-ESP REST API server running on http://localhost:${port}/`) @@ -1400,7 +2692,7 @@ rest_server.get(ES_LOG_ENDPOINT, function (req, res) { res.write(sseFormattedResponse); res.flush(); // this is important - // if buffer full start over + // if buffer is full, start over if (log_index > 50) { fetch_log.events = []; log_index = 0; diff --git a/pio_local.ini_example b/pio_local.ini_example index eed96ed5e..f436048a9 100644 --- a/pio_local.ini_example +++ b/pio_local.ini_example @@ -6,7 +6,7 @@ ; -DEMSESP_UART_DEBUG ; debugging UART ; -DEMSESP_DEBUG ; enables DEBUG to the log. Will generate a lot of extra traffic on Console and Syslog ; -DEMSESP_DEFAULT_BOARD_PROFILE=\"NODEMCU\" ; hard code the default board name -; -DEMSESP_DEBUG_SENSOR ; additional debug for the dallas sensors +; -DEMSESP_DEBUG_SENSOR ; additional debug for the sensors ; -DCORE_DEBUG_LEVEL=5 ; verbose core level debugging 5=verbose, 4=debug, 3=info ; -DEMSESP_TEST ; enable the tests. EN language only ; -DEMSESP_DE_ONLY ; only DE translated entity names diff --git a/platformio.ini b/platformio.ini index 02ab00239..e0e7bed85 100644 --- a/platformio.ini +++ b/platformio.ini @@ -7,7 +7,7 @@ default_envs = esp32_4M ; default_envs = esp32_16M ; default_envs = standalone -extra_configs = +extra_configs = factory_settings.ini pio_local.ini @@ -19,8 +19,8 @@ core_build_flags = -D ESP32=1 ; -std=gnu++17 -; core_unbuild_flags = -std=gnu++11 -; core_unbuild_flags = -std=gnu++17 +; core_unbuild_flags = -std=gnu++11 +; core_unbuild_flags = -std=gnu++17 core_unbuild_flags = ; my_build_flags is set in pio_local.ini @@ -67,7 +67,7 @@ framework = arduino extra_scripts = scripts/rename_fw.py board = esp32dev board_build.partitions = esp32_partition_4M.csv -board_build.filesystem = littlefs +board_build.filesystem = littlefs build_flags = ${common.build_flags} build_unflags = ${common.unbuild_flags} @@ -83,7 +83,7 @@ board_build.partitions = esp32_partition_4M.csv build_flags = ${common.build_flags} -Os build_unflags = ${common.unbuild_flags} -[env:esp32_4M+] +[env:esp32_4Mplus] extends = espressi32_base extra_scripts = pre:scripts/build_interface.py diff --git a/src/analogsensor.cpp b/src/analogsensor.cpp index 0fc13f839..659f368a2 100644 --- a/src/analogsensor.cpp +++ b/src/analogsensor.cpp @@ -301,7 +301,8 @@ void AnalogSensor::loop() { } // update analog information name and offset -bool AnalogSensor::update(uint8_t gpio, const std::string & name, double offset, double factor, uint8_t uom, int8_t type) { +// a type of -1 is used to delete the sensor +bool AnalogSensor::update(uint8_t gpio, const std::string & name, double offset, double factor, uint8_t uom, int8_t type, bool deleted) { boolean found_sensor = false; // see if we can find the sensor in our customization list EMSESP::webCustomizationService.update( @@ -313,7 +314,7 @@ bool AnalogSensor::update(uint8_t gpio, const std::string & name, double offset, if (AnalogCustomization.gpio == gpio) { found_sensor = true; // found the record // see if it's marked for deletion - if (type == AnalogType::MARK_DELETED) { + if (deleted) { LOG_DEBUG("Removing analog sensor GPIO %02d", gpio); settings.analogCustomizations.remove(AnalogCustomization); } else { diff --git a/src/analogsensor.h b/src/analogsensor.h index b6c20dc42..e1ecb67cf 100644 --- a/src/analogsensor.h +++ b/src/analogsensor.h @@ -112,8 +112,7 @@ class AnalogSensor { ~AnalogSensor() = default; enum AnalogType : int8_t { - MARK_DELETED = -1, // mark for deletion - NOTUSED, // 0 - disabled + NOTUSED, // 0 - disabled DIGITAL_IN, COUNTER, ADC, @@ -157,7 +156,7 @@ class AnalogSensor { return sensors_.size(); } - bool update(uint8_t gpio, const std::string & name, double offset, double factor, uint8_t uom, int8_t type); + bool update(uint8_t gpio, const std::string & name, double offset, double factor, uint8_t uom, int8_t type, bool deleted = false); bool get_value_info(JsonObject & output, const char * cmd, const int8_t id) const; #if defined(EMSESP_TEST) diff --git a/src/command.cpp b/src/command.cpp index d75d58ebb..cfd81b943 100644 --- a/src/command.cpp +++ b/src/command.cpp @@ -380,7 +380,7 @@ void Command::add(const uint8_t device_type, const uint8_t device_id, const char } // add a command with no json output -// system/dallas/analog devices uses device_id 0 +// system/temperature/analog devices uses device_id 0 void Command::add(const uint8_t device_type, const char * cmd, const cmd_function_p cb, const char * const * description, uint8_t flags) { add(device_type, 0, cmd, cb, description, flags); } @@ -543,8 +543,8 @@ bool Command::device_has_commands(const uint8_t device_type) { return (EMSESP::webEntityService.count_entities() != 0); } - if (device_type == EMSdevice::DeviceType::DALLASSENSOR) { - return (EMSESP::dallassensor_.have_sensors()); + if (device_type == EMSdevice::DeviceType::TEMPERATURESENSOR) { + return (EMSESP::temperaturesensor_.have_sensors()); } if (device_type == EMSdevice::DeviceType::ANALOGSENSOR) { @@ -572,8 +572,8 @@ void Command::show_devices(uuid::console::Shell & shell) { if (EMSESP::webSchedulerService.has_commands()) { shell.printf("%s ", EMSdevice::device_type_2_device_name(EMSdevice::DeviceType::SCHEDULER)); } - if (EMSESP::dallassensor_.have_sensors()) { - shell.printf("%s ", EMSdevice::device_type_2_device_name(EMSdevice::DeviceType::DALLASSENSOR)); + if (EMSESP::temperaturesensor_.have_sensors()) { + shell.printf("%s ", EMSdevice::device_type_2_device_name(EMSdevice::DeviceType::TEMPERATURESENSOR)); } if (EMSESP::analogsensor_.have_sensors()) { shell.printf("%s ", EMSdevice::device_type_2_device_name(EMSdevice::DeviceType::ANALOGSENSOR)); @@ -616,12 +616,12 @@ void Command::show_all(uuid::console::Shell & shell) { } // show sensors - if (EMSESP::dallassensor_.have_sensors()) { + if (EMSESP::temperaturesensor_.have_sensors()) { shell.print(COLOR_BOLD_ON); shell.print(COLOR_YELLOW); - shell.printf(" %s: ", EMSdevice::device_type_2_device_name(EMSdevice::DeviceType::DALLASSENSOR)); + shell.printf(" %s: ", EMSdevice::device_type_2_device_name(EMSdevice::DeviceType::TEMPERATURESENSOR)); shell.print(COLOR_RESET); - show(shell, EMSdevice::DeviceType::DALLASSENSOR, true); + show(shell, EMSdevice::DeviceType::TEMPERATURESENSOR, true); } if (EMSESP::analogsensor_.have_sensors()) { shell.print(COLOR_BOLD_ON); diff --git a/src/command.h b/src/command.h index a9f815692..f0b54bbf2 100644 --- a/src/command.h +++ b/src/command.h @@ -107,7 +107,7 @@ class Command { const char * const * description, uint8_t flags = CommandFlag::MQTT_SUB_FLAG_DEFAULT); - // same for system/dallas/analog devices + // same for system/temperature/analog devices static void add(const uint8_t device_type, const char * cmd, const cmd_function_p cb, diff --git a/src/console.cpp b/src/console.cpp index e7e4ff3fc..7b420fabd 100644 --- a/src/console.cpp +++ b/src/console.cpp @@ -569,7 +569,7 @@ static void setup_commands(std::shared_ptr & commands) { if (current_arguments.size() == 0) { std::vector devices_list; devices_list.emplace_back(EMSdevice::device_type_2_device_name(EMSdevice::DeviceType::SYSTEM)); - devices_list.emplace_back(EMSdevice::device_type_2_device_name(EMSdevice::DeviceType::DALLASSENSOR)); + devices_list.emplace_back(EMSdevice::device_type_2_device_name(EMSdevice::DeviceType::TEMPERATURESENSOR)); devices_list.emplace_back(EMSdevice::device_type_2_device_name(EMSdevice::DeviceType::ANALOGSENSOR)); for (const auto & device_class : EMSFactory::device_handlers()) { if (Command::device_has_commands(device_class.first)) { @@ -619,12 +619,12 @@ void EMSESPShell::stopped() { // show welcome banner void EMSESPShell::display_banner() { println(); - printfln("┌──────────────────────────────────────┐"); - printfln("│ %sEMS-ESP version %-12s%s │", COLOR_BOLD_ON, EMSESP_APP_VERSION, COLOR_BOLD_OFF); - printfln("│ %s%shttps://github.com/emsesp/EMS-ESP32%s │", COLOR_BRIGHT_GREEN, COLOR_UNDERLINE, COLOR_RESET); - printfln("│ │"); - printfln("│ type %shelp%s to show available commands │", COLOR_UNDERLINE, COLOR_RESET); - printfln("└──────────────────────────────────────┘"); + printfln("┌───────────────────────────────────────┐"); + printfln("│ %sEMS-ESP version %-12s%s │", COLOR_BOLD_ON, EMSESP_APP_VERSION, COLOR_BOLD_OFF); + printfln("│ %s%shttps://github.com/emsesp/EMS-ESP32%s │", COLOR_BRIGHT_GREEN, COLOR_UNDERLINE, COLOR_RESET); + printfln("│ │"); + printfln("│ type %shelp%s to show available commands │", COLOR_UNDERLINE, COLOR_RESET); + printfln("└───────────────────────────────────────┘"); println(); // set console name diff --git a/src/device_library.h b/src/device_library.h index 93deb1d35..80e5c47fe 100644 --- a/src/device_library.h +++ b/src/device_library.h @@ -32,7 +32,7 @@ {115, DeviceType::BOILER, "Topline/GB162", DeviceFlags::EMS_DEVICE_FLAG_NONE}, {121, DeviceType::BOILER, "Cascade MCM10", DeviceFlags::EMS_DEVICE_FLAG_NONE}, {122, DeviceType::BOILER, "Proline", DeviceFlags::EMS_DEVICE_FLAG_NONE}, -{123, DeviceType::BOILER, "GBx72/Trendline/Cerapur/Greenstar Si/27i-30i", DeviceFlags::EMS_DEVICE_FLAG_NONE}, +{123, DeviceType::BOILER, "GBx72/Trendline/Cerapur/Greenstar Si", DeviceFlags::EMS_DEVICE_FLAG_NONE}, {131, DeviceType::BOILER, "GB212", DeviceFlags::EMS_DEVICE_FLAG_NONE}, {132, DeviceType::BOILER, "GC7000F", DeviceFlags::EMS_DEVICE_FLAG_NONE}, {133, DeviceType::BOILER, "Logano GB125/KB195i/Logamatic MC110", DeviceFlags::EMS_DEVICE_FLAG_NONE}, diff --git a/src/devices/boiler.cpp b/src/devices/boiler.cpp index 2d0cbb4bb..f8b50d0dc 100644 --- a/src/devices/boiler.cpp +++ b/src/devices/boiler.cpp @@ -833,7 +833,7 @@ Boiler::Boiler(uint8_t device_type, int8_t device_id, uint8_t product_id, const // Check if hot tap water or heating is active // Values will always be posted first time as heatingActive_ and tapwaterActive_ will have values EMS_VALUE_BOOL_NOTSET -void Boiler::check_active(const bool force) { +void Boiler::check_active() { if (!Helpers::hasValue(boilerState_)) { return; } @@ -844,7 +844,7 @@ void Boiler::check_active(const bool force) { // check if heating is active, bits 2 and 4 must be set b = ((boilerState_ & 0x09) == 0x09); val = b ? EMS_VALUE_BOOL_ON : EMS_VALUE_BOOL_OFF; - if (heatingActive_ != val || force) { + if (heatingActive_ != val) { heatingActive_ = val; char s[12]; Mqtt::queue_publish(F_(heating_active), Helpers::render_boolean(s, b)); @@ -868,7 +868,7 @@ void Boiler::check_active(const bool force) { } val = b ? EMS_VALUE_BOOL_ON : EMS_VALUE_BOOL_OFF; - if (tapwaterActive_ != val || force) { + if (tapwaterActive_ != val) { tapwaterActive_ = val; char s[12]; Mqtt::queue_publish(F_(tapwater_active), Helpers::render_boolean(s, b)); diff --git a/src/devices/boiler.h b/src/devices/boiler.h index c0f81c07a..3e5988d05 100644 --- a/src/devices/boiler.h +++ b/src/devices/boiler.h @@ -35,7 +35,7 @@ class Boiler : public EMSdevice { return (flags() & 0x0F); } - void check_active(const bool force = false); + void check_active(); uint8_t boilerState_ = EMS_VALUE_UINT_NOTSET; // Boiler state flag - FOR INTERNAL USE diff --git a/src/emsdevice.cpp b/src/emsdevice.cpp index f7bd9d2e8..891aca0e0 100644 --- a/src/emsdevice.cpp +++ b/src/emsdevice.cpp @@ -119,8 +119,8 @@ const char * EMSdevice::device_type_2_device_name(const uint8_t device_type) { return F_(connect); case DeviceType::MIXER: return F_(mixer); - case DeviceType::DALLASSENSOR: - return F_(dallassensor); + case DeviceType::TEMPERATURESENSOR: + return F_(temperaturesensor); case DeviceType::ANALOGSENSOR: return F_(analogsensor); case DeviceType::CONTROLLER: @@ -143,7 +143,7 @@ const char * EMSdevice::device_type_2_device_name(const uint8_t device_type) { } // returns the translated name of a specific EMS device -// excludes dallassensor, analogsensor and system +// excludes temperaturesensor, analogsensor and system const char * EMSdevice::device_type_2_device_name_translated() { switch (device_type_) { case DeviceType::BOILER: @@ -210,8 +210,8 @@ uint8_t EMSdevice::device_name_2_device_type(const char * topic) { if (!strcmp(lowtopic, F_(mixer))) { return DeviceType::MIXER; } - if (!strcmp(lowtopic, F_(dallassensor))) { - return DeviceType::DALLASSENSOR; + if (!strcmp(lowtopic, F_(temperaturesensor))) { + return DeviceType::TEMPERATURESENSOR; } if (!strcmp(lowtopic, F_(analogsensor))) { return DeviceType::ANALOGSENSOR; @@ -513,7 +513,7 @@ void EMSdevice::add_device_value(uint8_t tag, // to b // get fullname, getting translation if it exists const char * const * fullname; if (Helpers::count_items(name) == 1) { - fullname = nullptr; // no translations available, use empty + fullname = nullptr; // no translations available, use empty } else { fullname = &name[1]; // translations start at index 1 } @@ -824,8 +824,9 @@ std::string EMSdevice::get_value_uom(const char * key) const { // except additional data is stored in the JSON document needed for the Web UI like the UOM and command // v=value, u=uom, n=name, c=cmd, h=help string, s=step, m=min, x=max void EMSdevice::generate_values_web(JsonObject & output) { - output["label"] = to_string_short(); - JsonArray data = output.createNestedArray("data"); + // output["label"] = to_string_short(); + // output["label"] = name_; + JsonArray data = output.createNestedArray("data"); for (auto & dv : devicevalues_) { auto fullname = dv.get_fullname(); @@ -838,7 +839,7 @@ void EMSdevice::generate_values_web(JsonObject & output) { JsonObject obj = data.createNestedObject(); // create the object, we know there is a value uint8_t fahrenheit = 0; - // handle Booleans (true, false), use strings, no native true/false) + // handle Booleans (true, false), output as strings according to the user settings if (dv.type == DeviceValueType::BOOL) { auto value_b = (bool)*(uint8_t *)(dv.value_p); char s[12]; @@ -922,20 +923,19 @@ void EMSdevice::generate_values_web(JsonObject & output) { } } // handle INTs - // add steps to numeric values with numeric_operator + // add min and max values and steps, as integer values else { - char s[10]; if (dv.numeric_operator > 0) { - obj["s"] = Helpers::render_value(s, (float)1 / dv.numeric_operator, 1); + obj["s"] = (float)1 / dv.numeric_operator; } else if (dv.numeric_operator < 0) { - obj["s"] = Helpers::render_value(s, (float)(-1) * dv.numeric_operator, 0); + obj["s"] = (float)(-1) * dv.numeric_operator; } int16_t dv_set_min; uint16_t dv_set_max; if (dv.get_min_max(dv_set_min, dv_set_max)) { - obj["m"] = Helpers::render_value(s, dv_set_min, 0); - obj["x"] = Helpers::render_value(s, dv_set_max, 0); + obj["m"] = dv_set_min; + obj["x"] = dv_set_max; } } } @@ -1028,9 +1028,8 @@ void EMSdevice::generate_values_web_customization(JsonArray & output) { int16_t dv_set_min; uint16_t dv_set_max; if (dv.get_min_max(dv_set_min, dv_set_max)) { - char s[10]; - obj["mi"] = Helpers::render_value(s, dv_set_min, 0, fahrenheit); - obj["ma"] = Helpers::render_value(s, dv_set_max, 0, fahrenheit); + obj["mi"] = dv_set_min; + obj["ma"] = dv_set_max; } } } @@ -1305,7 +1304,7 @@ void EMSdevice::dump_value_info() { if (dv.type == DeviceValueType::BOOL) { snprintf(entityid, sizeof(entityid), "binary_sensor.%s", entity_with_tag); // binary sensor (for booleans) } else { - snprintf(entityid, sizeof(entityid), "sensor.%s", entity_with_tag); // normal HA sensor + snprintf(entityid, sizeof(entityid), "sensor.%s", entity_with_tag); // normal HA sensor } } diff --git a/src/emsdevice.h b/src/emsdevice.h index 5e2729e88..c1f3f811e 100644 --- a/src/emsdevice.h +++ b/src/emsdevice.h @@ -319,9 +319,9 @@ class EMSdevice { }; enum DeviceType : uint8_t { - SYSTEM = 0, // this is us (EMS-ESP) - DALLASSENSOR, // for internal dallas sensors - ANALOGSENSOR, // for internal analog sensors + SYSTEM = 0, // this is us (EMS-ESP) + TEMPERATURESENSOR, // for internal temperature sensors + ANALOGSENSOR, // for internal analog sensors SCHEDULER, BOILER, THERMOSTAT, diff --git a/src/emsesp.cpp b/src/emsesp.cpp index f2ed214ed..e693c627c 100644 --- a/src/emsesp.cpp +++ b/src/emsesp.cpp @@ -62,13 +62,13 @@ uuid::syslog::SyslogService System::syslog_; #endif // The services -RxService EMSESP::rxservice_; // incoming Telegram Rx handler -TxService EMSESP::txservice_; // outgoing Telegram Tx handler -Mqtt EMSESP::mqtt_; // mqtt handler -System EMSESP::system_; // core system services -DallasSensor EMSESP::dallassensor_; // Dallas sensors -AnalogSensor EMSESP::analogsensor_; // Analog sensors -Shower EMSESP::shower_; // Shower logic +RxService EMSESP::rxservice_; // incoming Telegram Rx handler +TxService EMSESP::txservice_; // outgoing Telegram Tx handler +Mqtt EMSESP::mqtt_; // mqtt handler +System EMSESP::system_; // core system services +TemperatureSensor EMSESP::temperaturesensor_; // Temperature sensors +AnalogSensor EMSESP::analogsensor_; // Analog sensors +Shower EMSESP::shower_; // Shower logic // static/common variables uint16_t EMSESP::watch_id_ = WATCH_ID_NONE; // for when log is TRACE. 0 means no trace set @@ -340,7 +340,7 @@ void EMSESP::dump_all_values(uuid::console::Shell & shell) { if (device.product_id == 160) { // MM100 device_id = 0x28; // wwc } else { - device_id = 0x20; // hc + device_id = 0x20; // hc } } else { device_id = 0x20; // should cover all the other device types @@ -408,15 +408,15 @@ void EMSESP::show_device_values(uuid::console::Shell & shell) { } } -// show Dallas temperature sensors and Analog sensors +// show temperature sensors and Analog sensors void EMSESP::show_sensor_values(uuid::console::Shell & shell) { - if (dallassensor_.have_sensors()) { + if (temperaturesensor_.have_sensors()) { shell.printfln("Temperature sensors:"); char s[10]; char s2[10]; uint8_t fahrenheit = EMSESP::system_.fahrenheit() ? 2 : 0; - for (const auto & sensor : dallassensor_.sensors()) { + for (const auto & sensor : temperaturesensor_.sensors()) { if (Helpers::hasValue(sensor.temperature_c)) { shell.printfln(" %s: %s%s °%c%s (offset %s, ID: %s)", sensor.name().c_str(), @@ -481,7 +481,7 @@ void EMSESP::publish_all(bool force) { publish_other_values(); // switch and heat pump, ... webSchedulerService.publish(); webEntityService.publish(); - publish_sensor_values(true); // includes dallas and analog sensors + publish_sensor_values(true); // includes temperature and analog sensors system_.send_heartbeat(); } } @@ -541,8 +541,8 @@ void EMSESP::reset_mqtt_ha() { emsdevice->ha_config_clear(); } - // force the re-creating of the dallas and analog sensor topics (for HA) - dallassensor_.reload(); + // force the re-creating of the temperature and analog sensor topics (for HA) + temperaturesensor_.reload(); analogsensor_.reload(); } @@ -606,11 +606,11 @@ void EMSESP::publish_other_values() { webEntityService.publish(); } -// publish both the dallas and analog sensor values +// publish both the temperature and analog sensor values void EMSESP::publish_sensor_values(const bool time, const bool force) { - if (dallas_enabled()) { - if (dallassensor_.updated_values() || time || force) { - dallassensor_.publish_values(force); + if (sensor_enabled()) { + if (temperaturesensor_.updated_values() || time || force) { + temperaturesensor_.publish_values(force); } } @@ -644,7 +644,7 @@ void EMSESP::publish_response(std::shared_ptr telegram) { Mqtt::queue_publish("response", doc.as()); } -// builds json with the detail of each value, for a specific EMS device type or the dallas sensor +// builds json with the detail of each value, for a specific EMS device type or the temperature sensor bool EMSESP::get_device_value_info(JsonObject & root, const char * cmd, const int8_t id, const uint8_t devicetype) { for (const auto & emsdevice : emsdevices) { if (emsdevice->device_type() == devicetype) { @@ -654,9 +654,9 @@ bool EMSESP::get_device_value_info(JsonObject & root, const char * cmd, const in } } - // specific for the dallassensor - if (devicetype == DeviceType::DALLASSENSOR) { - return EMSESP::dallassensor_.get_value_info(root, cmd, id); + // specific for the temperaturesensor + if (devicetype == DeviceType::TEMPERATURESENSOR) { + return EMSESP::temperaturesensor_.get_value_info(root, cmd, id); } // analog sensor @@ -906,7 +906,7 @@ bool EMSESP::process_telegram(std::shared_ptr telegram) { if (telegram->type_id == publish_id_) { publish_id_ = 0; } - emsdevice->has_update(false); // reset flag + emsdevice->has_update(false); // reset flag if (!Mqtt::publish_single()) { publish_device_values(emsdevice->device_type()); // publish to MQTT if we explicitly have too } @@ -1098,7 +1098,7 @@ bool EMSESP::add_device(const uint8_t device_id, const uint8_t product_id, const name = "Modem"; device_type = DeviceType::CONNECT; } else if (device_id == EMSdevice::EMS_DEVICE_ID_CONVERTER) { - name = "Converter"; // generic + name = "Converter"; // generic } else if (device_id == EMSdevice::EMS_DEVICE_ID_CLOCK) { name = "Clock"; // generic device_type = DeviceType::CONTROLLER; @@ -1124,6 +1124,7 @@ bool EMSESP::add_device(const uint8_t device_id, const uint8_t product_id, const emsdevices.push_back(EMSFactory::add(device_type, device_id, product_id, version, name, flags, brand)); // assign a unique ID. Note that this is not actual unique after a restart as it's dependent on the order that devices are found + // can't be 0 otherwise web won't work emsdevices.back()->unique_id(++unique_id_count_); // sort devices based on type @@ -1365,7 +1366,7 @@ void EMSESP::incoming_telegram(uint8_t * data, const uint8_t length) { #endif Roomctrl::check((data[1] ^ 0x80 ^ rxservice_.ems_mask()), data); // check if there is a message for the roomcontroller - rxservice_.add(data, length); // add to RxQueue + rxservice_.add(data, length); // add to RxQueue } } @@ -1465,7 +1466,7 @@ void EMSESP::start() { system_.system_restart(); }; - system_.reload_settings(); // ... and store some of the settings locally + system_.reload_settings(); // ... and store some of the settings locally webCustomizationService.begin(); // load the customizations webSchedulerService.begin(); // load the scheduler events @@ -1483,16 +1484,16 @@ void EMSESP::start() { } // start all the EMS-ESP services - mqtt_.start(); // mqtt init + mqtt_.start(); // mqtt init system_.start(); // starts commands, led, adc, button, network, syslog & uart LOG_INFO(("Starting EMS-ESP version %s (hostname: %s)"), EMSESP_APP_VERSION, system_.hostname().c_str()); // welcome message - shower_.start(); // initialize shower timer and shower alert - dallassensor_.start(); // Dallas external sensors - analogsensor_.start(); // Analog external sensors - webLogService.start(); // apply settings to weblog service + shower_.start(); // initialize shower timer and shower alert + temperaturesensor_.start(); // Temperature external sensors + analogsensor_.start(); // Analog external sensors + webLogService.start(); // apply settings to weblog service // Load our library of known devices into stack mem. Names are stored in Flash memory device_library_ = { @@ -1518,7 +1519,7 @@ void EMSESP::loop() { webLogService.loop(); // log in Web UI rxservice_.loop(); // process any incoming Rx telegrams shower_.loop(); // check for shower on/off - dallassensor_.loop(); // read dallas sensor temperatures + temperaturesensor_.loop(); // read sensor temperatures analogsensor_.loop(); // read analog sensor values publish_all_loop(); // with HA messages in parts to avoid flooding the mqtt queue mqtt_.loop(); // sends out anything in the MQTT queue diff --git a/src/emsesp.h b/src/emsesp.h index 3e4fa77ac..4108ab099 100644 --- a/src/emsesp.h +++ b/src/emsesp.h @@ -54,7 +54,7 @@ #include "telegram.h" #include "mqtt.h" #include "system.h" -#include "dallassensor.h" +#include "temperaturesensor.h" #include "analogsensor.h" #include "console.h" #include "console_stream.h" @@ -148,8 +148,8 @@ class EMSESP { static void incoming_telegram(uint8_t * data, const uint8_t length); - static bool dallas_enabled() { - return (dallassensor_.dallas_enabled()); + static bool sensor_enabled() { + return (temperaturesensor_.sensor_enabled()); } static bool analog_enabled() { @@ -214,13 +214,13 @@ class EMSESP { static std::vector> emsdevices; // services - static Mqtt mqtt_; - static System system_; - static DallasSensor dallassensor_; - static AnalogSensor analogsensor_; - static Shower shower_; - static RxService rxservice_; - static TxService txservice_; + static Mqtt mqtt_; + static System system_; + static TemperatureSensor temperaturesensor_; + static AnalogSensor analogsensor_; + static Shower shower_; + static RxService rxservice_; + static TxService txservice_; // web controllers static ESP8266React esp8266React; diff --git a/src/emsesp_stub.hpp b/src/emsesp_stub.hpp index 72ea5e7eb..b00cad107 100644 --- a/src/emsesp_stub.hpp +++ b/src/emsesp_stub.hpp @@ -20,7 +20,7 @@ #include "system.h" #include "mqtt.h" -#include "dallassensor.h" +#include "temperaturesensor.h" #include "version.h" #include "default_settings.h" @@ -33,9 +33,9 @@ namespace emsesp { class EMSESP { public: - static Mqtt mqtt_; - static System system_; - static DallasSensor dallassensor_; + static Mqtt mqtt_; + static System system_; + static TemperatureSensor temperaturesensor_; static uuid::log::Logger logger(); static ESP8266React esp8266React; diff --git a/src/locale_common.h b/src/locale_common.h index e45141b1a..edc2a4d3e 100644 --- a/src/locale_common.h +++ b/src/locale_common.h @@ -93,7 +93,7 @@ MAKE_WORD(connect) MAKE_WORD(heatpump) MAKE_WORD(generic) MAKE_WORD(analogsensor) -MAKE_WORD(dallassensor) +MAKE_WORD(temperaturesensor) MAKE_WORD(alert) MAKE_WORD(pump) MAKE_WORD(heatsource) @@ -194,13 +194,13 @@ MAKE_NOTRANSLATION(6kW, "6 kW") MAKE_NOTRANSLATION(9kW, "9 kW") // templates - this are not translated and will be saved under options_single -MAKE_NOTRANSLATION(tpl_datetime, "Format: < NTP | dd.mm.yyyy-hh:mm:ss-day(0-6)-dst(0/1) >") -MAKE_NOTRANSLATION(tpl_switchtime, "Format: [ not_set | day hh:mm on|off ]") -MAKE_NOTRANSLATION(tpl_switchtime1, "Format: [ not_set | day hh:mm Tn ]") -MAKE_NOTRANSLATION(tpl_holidays, "Format: < dd.mm.yyyy-dd.mm.yyyy >") -MAKE_NOTRANSLATION(tpl_date, "Format: < dd.mm.yyyy >") -MAKE_NOTRANSLATION(tpl_input, "Format: []") -MAKE_NOTRANSLATION(tpl_input4, "Format: []") +MAKE_NOTRANSLATION(tpl_datetime, "< NTP | dd.mm.yyyy-hh:mm:ss-day(0-6)-dst(0/1) >") +MAKE_NOTRANSLATION(tpl_switchtime, " [ not_set | day hh:mm on|off ]") +MAKE_NOTRANSLATION(tpl_switchtime1, " [ not_set | day hh:mm Tn ]") +MAKE_NOTRANSLATION(tpl_holidays, "< dd.mm.yyyy-dd.mm.yyyy >") +MAKE_NOTRANSLATION(tpl_date, "< dd.mm.yyyy >") +MAKE_NOTRANSLATION(tpl_input, "[]") +MAKE_NOTRANSLATION(tpl_input4, "[]") #if defined(EMSESP_TEST) MAKE_NOTRANSLATION(test_cmd, "run a test") diff --git a/src/locale_translations.h b/src/locale_translations.h index 00f918cc3..4639bf451 100644 --- a/src/locale_translations.h +++ b/src/locale_translations.h @@ -49,7 +49,8 @@ MAKE_WORD_TRANSLATION(pump_device, "Pump Module", "Pumpenmodul", "Pump Module", MAKE_WORD_TRANSLATION(heatsource_device, "Heatsource", "Heizquelle", "Heatsource", "Värmekälla", "Źródło ciepła", "Varmekilde", "", "Isı Kaynağı") // TODO translate MAKE_WORD_TRANSLATION(sensors_device, "Sensors", "Sensoren", "Sensoren", "Sensorer", "Czujniki", "Sensorer", "Capteurs", "Sensör Cihazı") MAKE_WORD_TRANSLATION(unknown_device, "Unknown", "Unbekannt", "Onbekend", "Okänt", "Nieznane urządzenie", "Ukjent", "Inconnu", "") // TODO translate -MAKE_WORD_TRANSLATION(custom_device, "User defined entities", "Nutzer deklarierte Entitäten", "", "", "", "", "", "") // TODO translate +MAKE_WORD_TRANSLATION(custom_device, "Custom", "", "", "", "Niestandardowe", "", "", "") // TODO translate +MAKE_WORD_TRANSLATION(custom_device_name, "User defined entities", "Nutzer deklarierte Entitäten", "", "", "Encje zdefiniowane przez użytkownika", "", "", "") // TODO translate // commands // TODO translate @@ -61,11 +62,11 @@ MAKE_WORD_TRANSLATION(setiovalue_cmd, "set io value", "Setze Wertevorgabe", "", MAKE_WORD_TRANSLATION(changeloglevel_cmd, "change log level", "Ändere Sysloglevel", "", "", "zmień poziom log-u", "endre loggnivå", "", "Kayıt seviyesini değiştir") // TODO translate MAKE_WORD_TRANSLATION(fetch_cmd, "refresh all EMS values", "Lese alle EMS-Werte neu", "", "", "odśwież wszystkie wartości EMS", "oppfrisk alle EMS verdier", "", "Bütün EMS değerlerini yenile") // TODO translate MAKE_WORD_TRANSLATION(restart_cmd, "restart EMS-ESP", "Neustart", "", "", "uruchom ponownie EMS-ESP", "restart EMS-ESP", "", "EMS-ESPyi yeniden başlat") // TODO translate -MAKE_WORD_TRANSLATION(watch_cmd, "watch incoming telegrams", "Watch auf eingehende Telegramme", "", "", "obserwuj przyczodzące telegramy", "se innkommende telegrammer", "", "Gelen telegramları ") // TODO translate +MAKE_WORD_TRANSLATION(watch_cmd, "watch incoming telegrams", "Watch auf eingehende Telegramme", "", "", "obserwuj przychodzące telegramy", "se innkommende telegrammer", "", "Gelen telegramları ") // TODO translate MAKE_WORD_TRANSLATION(publish_cmd, "publish all to MQTT", "Publiziere MQTT", "", "", "opublikuj wszystko na MQTT", "Publiser alt til MQTT", "", "Hepsini MQTTye gönder") // TODO translate MAKE_WORD_TRANSLATION(system_info_cmd, "show system status", "Zeige System-Status", "", "", "pokaż status systemu", "vis system status", "", "Sistem Durumunu Göster") // TODO translate MAKE_WORD_TRANSLATION(schedule_cmd, "enable schedule item", "Aktiviere Zeitplan", "", "", "aktywuj wybrany harmonogram", "", "", "") // TODO translate -MAKE_WORD_TRANSLATION(entity_cmd, "set custom value on ems", "Sende eigene Entitäten zu EMS", "", "", "", "", "", "") // TODO translate +MAKE_WORD_TRANSLATION(entity_cmd, "set custom value on ems", "Sende eigene Entitäten zu EMS", "", "", "wyślij własną wartość na EMS", "", "", "") // TODO translate // tags MAKE_WORD_TRANSLATION(tag_boiler_data_ww, "dhw", "WW", "dhw", "VV", "CWU", "dhw", "ecs", "SKS") diff --git a/src/mqtt.cpp b/src/mqtt.cpp index f8d93f36e..971674fc2 100644 --- a/src/mqtt.cpp +++ b/src/mqtt.cpp @@ -149,7 +149,7 @@ void Mqtt::loop() { EMSESP::system_.send_heartbeat(); // send heartbeat } - // dallas publish on change + // temperature and analog sensor publish on change if (!publish_time_sensor_) { EMSESP::publish_sensor_values(false); } diff --git a/src/system.cpp b/src/system.cpp index a814c704a..ed3f1b6ad 100644 --- a/src/system.cpp +++ b/src/system.cpp @@ -141,7 +141,7 @@ bool System::command_publish(const char * value, const int8_t id) { } else if (value_s == "other") { EMSESP::publish_other_values(); // switch and heat pump return true; - } else if ((value_s == (F_(dallassensor))) || (value_s == (F_(analogsensor)))) { + } else if ((value_s == (F_(temperaturesensor))) || (value_s == (F_(analogsensor)))) { EMSESP::publish_sensor_values(true); return true; } @@ -611,9 +611,9 @@ bool System::heartbeat_json(JsonObject & output) { output["apicalls"] = WebAPIService::api_count(); // + WebAPIService::api_fails(); output["apifails"] = WebAPIService::api_fails(); - if (EMSESP::dallas_enabled() || EMSESP::analog_enabled()) { - output["sensorreads"] = EMSESP::dallassensor_.reads() + EMSESP::analogsensor_.reads(); - output["sensorfails"] = EMSESP::dallassensor_.fails() + EMSESP::analogsensor_.fails(); + if (EMSESP::sensor_enabled() || EMSESP::analog_enabled()) { + output["sensorreads"] = EMSESP::temperaturesensor_.reads() + EMSESP::analogsensor_.reads(); + output["sensorfails"] = EMSESP::temperaturesensor_.fails() + EMSESP::analogsensor_.fails(); } #ifndef EMSESP_STANDALONE @@ -1267,10 +1267,10 @@ bool System::command_info(const char * value, const int8_t id, JsonObject & outp // Sensor Status node = output.createNestedObject("Sensor Info"); - if (EMSESP::dallas_enabled()) { - node["temperature sensors"] = EMSESP::dallassensor_.no_sensors(); - node["temperature sensor reads"] = EMSESP::dallassensor_.reads(); - node["temperature sensor fails"] = EMSESP::dallassensor_.fails(); + if (EMSESP::sensor_enabled()) { + node["temperature sensors"] = EMSESP::temperaturesensor_.no_sensors(); + node["temperature sensor reads"] = EMSESP::temperaturesensor_.reads(); + node["temperature sensor fails"] = EMSESP::temperaturesensor_.fails(); } if (EMSESP::analog_enabled()) { node["analog sensors"] = EMSESP::analogsensor_.no_sensors(); @@ -1418,7 +1418,7 @@ bool System::load_board_profile(std::vector & data, const std::string & } else if (board_profile == "LOLIN") { data = {2, 18, 17, 16, 0, PHY_type::PHY_TYPE_NONE, 0, 0, 0}; // Lolin D32 } else if (board_profile == "OLIMEX") { - data = {0, 0, 36, 4, 34, PHY_type::PHY_TYPE_LAN8720, -1, 0, 0}; // Olimex ESP32-EVB (uses U1TXD/U1RXD/BUTTON, no LED or Dallas) + data = {0, 0, 36, 4, 34, PHY_type::PHY_TYPE_LAN8720, -1, 0, 0}; // Olimex ESP32-EVB (uses U1TXD/U1RXD/BUTTON, no LED or Temperature sensor) } else if (board_profile == "OLIMEXPOE") { data = {0, 0, 36, 4, 34, PHY_type::PHY_TYPE_LAN8720, 12, 0, 3}; // Olimex ESP32-POE } else if (board_profile == "C3MINI") { diff --git a/src/dallassensor.cpp b/src/temperaturesensor.cpp similarity index 86% rename from src/dallassensor.cpp rename to src/temperaturesensor.cpp index e10a9f7f5..d3266410d 100644 --- a/src/dallassensor.cpp +++ b/src/temperaturesensor.cpp @@ -18,7 +18,7 @@ // code originally written by nomis - https://github.com/nomis -#include "dallassensor.h" +#include "temperaturesensor.h" #include "emsesp.h" #ifdef ESP32 @@ -29,10 +29,10 @@ namespace emsesp { -uuid::log::Logger DallasSensor::logger_{F_(dallassensor), uuid::log::Facility::DAEMON}; +uuid::log::Logger TemperatureSensor::logger_{F_(temperaturesensor), uuid::log::Facility::DAEMON}; // start the 1-wire -void DallasSensor::start() { +void TemperatureSensor::start() { reload(); if (!dallas_gpio_) { @@ -42,32 +42,32 @@ void DallasSensor::start() { #ifndef EMSESP_STANDALONE bus_.begin(dallas_gpio_); - LOG_INFO("Starting Dallas sensor service"); + LOG_INFO("Starting Temperature sensor service"); #endif // Add API calls Command::add( - EMSdevice::DeviceType::DALLASSENSOR, + EMSdevice::DeviceType::TEMPERATURESENSOR, F_(info), [&](const char * value, const int8_t id, JsonObject & output) { return command_info(value, id, output); }, FL_(info_cmd)); Command::add( - EMSdevice::DeviceType::DALLASSENSOR, + EMSdevice::DeviceType::TEMPERATURESENSOR, F_(values), [&](const char * value, const int8_t id, JsonObject & output) { return command_info(value, 0, output); }, nullptr, CommandFlag::HIDDEN); // this command is hidden Command::add( - EMSdevice::DeviceType::DALLASSENSOR, + EMSdevice::DeviceType::TEMPERATURESENSOR, F_(commands), [&](const char * value, const int8_t id, JsonObject & output) { return command_commands(value, id, output); }, FL_(commands_cmd)); - Mqtt::subscribe(EMSdevice::DeviceType::DALLASSENSOR, "dallasssensor/#", nullptr); // use empty function callback + Mqtt::subscribe(EMSdevice::DeviceType::TEMPERATURESENSOR, "temperaturesensor/#", nullptr); // use empty function callback } // load settings -void DallasSensor::reload() { +void TemperatureSensor::reload() { // load the service settings EMSESP::webSettingsService.read([&](WebSettings & settings) { dallas_gpio_ = settings.dallas_gpio; @@ -80,7 +80,7 @@ void DallasSensor::reload() { } } -void DallasSensor::loop() { +void TemperatureSensor::loop() { if (!dallas_gpio_) { return; // dallas gpio is 0 (disabled) } @@ -119,13 +119,13 @@ void DallasSensor::loop() { } else if (state_ == State::READING) { if (temperature_convert_complete() && (time_now - last_activity_ > CONVERSION_MS)) { #ifdef EMSESP_DEBUG_SENSOR - LOG_DEBUG("Scanning for sensors"); + LOG_DEBUG("Scanning for temperature sensors"); #endif bus_.reset_search(); state_ = State::SCANNING; } else if (time_now - last_activity_ > READ_TIMEOUT_MS) { #ifdef EMSESP_DEBUG_SENSOR - LOG_WARNING("Dallas sensor read timeout"); + LOG_WARNING("Sensor read timeout"); #endif state_ = State::IDLE; sensorfails_++; @@ -133,7 +133,7 @@ void DallasSensor::loop() { } else if (state_ == State::SCANNING) { if (time_now - last_activity_ > SCAN_TIMEOUT_MS) { #ifdef EMSESP_DEBUG_SENSOR - LOG_ERROR("Dallas sensor scan timeout"); + LOG_ERROR("Sensor scan timeout"); #endif state_ = State::IDLE; sensorfails_++; @@ -188,12 +188,12 @@ void DallasSensor::loop() { default: sensorfails_++; - LOG_ERROR("Unknown dallas sensor %s", Sensor(addr).id().c_str()); + LOG_ERROR("Unknown sensor %s", Sensor(addr).id().c_str()); break; } } else { sensorfails_++; - LOG_ERROR("Invalid dallas sensor %s", Sensor(addr).id().c_str()); + LOG_ERROR("Invalid sensor %s", Sensor(addr).id().c_str()); } } else { if (!parasite_) { @@ -211,7 +211,7 @@ void DallasSensor::loop() { scancnt_ = 0; } else if (scancnt_ == SCAN_START + 1) { // startup firstscan_ = sensors_.size(); - // LOG_DEBUG("Adding %d dallas sensor(s) from first scan", firstscan_); + // LOG_DEBUG("Adding %d sensor(s) from first scan", firstscan_); } else if ((scancnt_ <= 0) && (firstscan_ != sensors_.size())) { // check 2 times for no change of sensor # scancnt_ = SCAN_START; sensors_.clear(); // restart scanning and clear to get correct numbering @@ -223,7 +223,7 @@ void DallasSensor::loop() { #endif } -bool DallasSensor::temperature_convert_complete() { +bool TemperatureSensor::temperature_convert_complete() { #ifndef EMSESP_STANDALONE if (parasite_) { return true; // don't care, use the minimum time in loop @@ -234,7 +234,7 @@ bool DallasSensor::temperature_convert_complete() { #endif } -int16_t DallasSensor::get_temperature_c(const uint8_t addr[]) { +int16_t TemperatureSensor::get_temperature_c(const uint8_t addr[]) { #ifndef EMSESP_STANDALONE if (!bus_.reset()) { LOG_ERROR("Bus reset failed before reading scratchpad from %s", Sensor(addr).id().c_str()); @@ -297,8 +297,8 @@ int16_t DallasSensor::get_temperature_c(const uint8_t addr[]) { #endif } -// update dallas information name and offset -bool DallasSensor::update(const std::string & id, const std::string & name, int16_t offset) { +// update temperature sensor information name and offset +bool TemperatureSensor::update(const std::string & id, const std::string & name, int16_t offset) { // find the sensor for (auto & sensor : sensors_) { if (sensor.id() == id) { @@ -346,7 +346,7 @@ bool DallasSensor::update(const std::string & id, const std::string & name, int1 } // check to see if values have been updated -bool DallasSensor::updated_values() { +bool TemperatureSensor::updated_values() { if (changed_) { changed_ = false; return true; @@ -355,13 +355,13 @@ bool DallasSensor::updated_values() { } // list commands -bool DallasSensor::command_commands(const char * value, const int8_t id, JsonObject & output) { - return Command::list(EMSdevice::DeviceType::DALLASSENSOR, output); +bool TemperatureSensor::command_commands(const char * value, const int8_t id, JsonObject & output) { + return Command::list(EMSdevice::DeviceType::TEMPERATURESENSOR, output); } // creates JSON doc from values // returns false if there are no sensors -bool DallasSensor::command_info(const char * value, const int8_t id, JsonObject & output) { +bool TemperatureSensor::command_info(const char * value, const int8_t id, JsonObject & output) { if (sensors_.empty()) { return false; } @@ -387,7 +387,7 @@ bool DallasSensor::command_info(const char * value, const int8_t id, JsonObject } // called from emsesp.cpp, similar to the emsdevice->get_value_info -bool DallasSensor::get_value_info(JsonObject & output, const char * cmd, const int8_t id) { +bool TemperatureSensor::get_value_info(JsonObject & output, const char * cmd, const int8_t id) { if (sensors_.empty()) { return false; } @@ -438,13 +438,13 @@ bool DallasSensor::get_value_info(JsonObject & output, const char * cmd, const i } // publish a single sensor to MQTT -void DallasSensor::publish_sensor(const Sensor & sensor) { +void TemperatureSensor::publish_sensor(const Sensor & sensor) { if (Mqtt::enabled() && Mqtt::publish_single()) { char topic[Mqtt::MQTT_TOPIC_MAX_SIZE]; if (Mqtt::publish_single2cmd()) { - snprintf(topic, sizeof(topic), "%s/%s", (F_(dallassensor)), sensor.name().c_str()); + snprintf(topic, sizeof(topic), "%s/%s", (F_(temperaturesensor)), sensor.name().c_str()); } else { - snprintf(topic, sizeof(topic), "%s%s/%s", (F_(dallassensor)), "_data", sensor.name().c_str()); + snprintf(topic, sizeof(topic), "%s%s/%s", (F_(temperaturesensor)), "_data", sensor.name().c_str()); } char payload[10]; Mqtt::queue_publish(topic, Helpers::render_value(payload, sensor.temperature_c, 10, EMSESP::system_.fahrenheit() ? 2 : 0)); @@ -452,7 +452,7 @@ void DallasSensor::publish_sensor(const Sensor & sensor) { } // send empty config topic to remove the entry from HA -void DallasSensor::remove_ha_topic(const std::string & id) { +void TemperatureSensor::remove_ha_topic(const std::string & id) { if (!Mqtt::ha_enabled()) { return; } @@ -461,12 +461,12 @@ void DallasSensor::remove_ha_topic(const std::string & id) { std::string sensorid = id; std::replace(sensorid.begin(), sensorid.end(), '-', '_'); char topic[Mqtt::MQTT_TOPIC_MAX_SIZE]; - snprintf(topic, sizeof(topic), "sensor/%s/dallassensor_%s/config", Mqtt::basename().c_str(), sensorid.c_str()); + snprintf(topic, sizeof(topic), "sensor/%s/temperaturesensor_%s/config", Mqtt::basename().c_str(), sensorid.c_str()); Mqtt::queue_remove_topic(topic); } -// send all dallas sensor values as a JSON package to MQTT -void DallasSensor::publish_values(const bool force) { +// send all temperature sensor values as a JSON package to MQTT +void TemperatureSensor::publish_values(const bool force) { if (!Mqtt::enabled()) { return; } @@ -499,7 +499,7 @@ void DallasSensor::publish_values(const bool force) { } // create the HA MQTT config - // to e.g. homeassistant/sensor/ems-esp/dallassensor_28-233D-9497-0C03/config + // to e.g. homeassistant/sensor/ems-esp/temperaturesensor_28-233D-9497-0C03/config if (Mqtt::ha_enabled()) { if (!has_value && sensor.ha_registered) { remove_ha_topic(sensor.id()); @@ -511,7 +511,7 @@ void DallasSensor::publish_values(const bool force) { config["dev_cla"] = "temperature"; char stat_t[50]; - snprintf(stat_t, sizeof(stat_t), "%s/dallassensor_data", Mqtt::base().c_str()); // use base path + snprintf(stat_t, sizeof(stat_t), "%s/temperaturesensor_data", Mqtt::base().c_str()); // use base path config["stat_t"] = stat_t; config["unit_of_meas"] = EMSdevice::uom_to_string(DeviceValueUOM::DEGREES); @@ -529,9 +529,9 @@ void DallasSensor::publish_values(const bool force) { char uniq_s[70]; if (Mqtt::entity_format() == Mqtt::entitiyFormat::MULTI_SHORT) { - snprintf(uniq_s, sizeof(uniq_s), "%s_dallassensor_%s", Mqtt::basename().c_str(), sensor.id().c_str()); + snprintf(uniq_s, sizeof(uniq_s), "%s_temperaturesensor_%s", Mqtt::basename().c_str(), sensor.id().c_str()); } else { - snprintf(uniq_s, sizeof(uniq_s), "dallassensor_%s", sensor.id().c_str()); + snprintf(uniq_s, sizeof(uniq_s), "temperaturesensor_%s", sensor.id().c_str()); } config["obj_id"] = uniq_s; @@ -553,7 +553,7 @@ void DallasSensor::publish_values(const bool force) { std::string sensorid = sensor.id(); std::replace(sensorid.begin(), sensorid.end(), '-', '_'); - snprintf(topic, sizeof(topic), "sensor/%s/dallassensor_%s/config", Mqtt::basename().c_str(), sensorid.c_str()); + snprintf(topic, sizeof(topic), "sensor/%s/temperaturesensor_%s/config", Mqtt::basename().c_str(), sensorid.c_str()); Mqtt::queue_ha(topic, config.as()); @@ -562,12 +562,12 @@ void DallasSensor::publish_values(const bool force) { } } - Mqtt::queue_publish("dallassensor_data", doc.as()); + Mqtt::queue_publish("temperaturesensor_data", doc.as()); } // skip crc from id -DallasSensor::Sensor::Sensor(const uint8_t addr[]) +TemperatureSensor::Sensor::Sensor(const uint8_t addr[]) : internal_id_(((uint64_t)addr[0] << 48) | ((uint64_t)addr[1] << 40) | ((uint64_t)addr[2] << 32) | ((uint64_t)addr[3] << 24) | ((uint64_t)addr[4] << 16) | ((uint64_t)addr[5] << 8) | ((uint64_t)addr[6])) { // create ID string @@ -584,14 +584,14 @@ DallasSensor::Sensor::Sensor(const uint8_t addr[]) offset_ = 0; // 0 degrees offset } -uint64_t DallasSensor::get_id(const uint8_t addr[]) { +uint64_t TemperatureSensor::get_id(const uint8_t addr[]) { return (((uint64_t)addr[0] << 48) | ((uint64_t)addr[1] << 40) | ((uint64_t)addr[2] << 32) | ((uint64_t)addr[3] << 24) | ((uint64_t)addr[4] << 16) | ((uint64_t)addr[5] << 8) | ((uint64_t)addr[6])); } // find the name from the customization service // if empty, return the ID as a string -std::string DallasSensor::Sensor::name() const { +std::string TemperatureSensor::Sensor::name() const { if (name_.empty()) { return id_; } @@ -600,12 +600,12 @@ std::string DallasSensor::Sensor::name() const { // look up in customization service for a specific sensor // and set the name and offset from that entry if it exists -bool DallasSensor::Sensor::apply_customization() { +bool TemperatureSensor::Sensor::apply_customization() { EMSESP::webCustomizationService.read([&](WebCustomization & settings) { auto sensors = settings.sensorCustomizations; if (!sensors.empty()) { for (const auto & sensor : sensors) { - LOG_DEBUG("Loading customization for dallas sensor %s", sensor.id.c_str()); + LOG_DEBUG("Loading customization for temperature sensor %s", sensor.id.c_str()); if (id_ == sensor.id) { set_name(sensor.name); set_offset(sensor.offset); @@ -621,8 +621,8 @@ bool DallasSensor::Sensor::apply_customization() { // hard coded tests #if defined(EMSESP_TEST) -void DallasSensor::test() { - // add 2 dallas sensors +void TemperatureSensor::test() { + // add 2 temperature sensors uint8_t addr[ADDR_LEN] = {1, 2, 3, 4, 5, 6, 7, 8}; sensors_.emplace_back(addr); sensors_.back().temperature_c = 123; diff --git a/src/dallassensor.h b/src/temperaturesensor.h similarity index 95% rename from src/dallassensor.h rename to src/temperaturesensor.h index 868b958e1..353b8f8fa 100644 --- a/src/dallassensor.h +++ b/src/temperaturesensor.h @@ -18,8 +18,8 @@ // code originally written by nomis - https://github.com/nomis -#ifndef EMSESP_DALLASSENSOR_H -#define EMSESP_DALLASSENSOR_H +#ifndef EMSESP_TEMPERATURESENSOR_H +#define EMSESP_TEMPERATURESENSOR_H #include "helpers.h" #include "mqtt.h" @@ -33,7 +33,7 @@ namespace emsesp { -class DallasSensor { +class TemperatureSensor { public: class Sensor { public: @@ -73,8 +73,8 @@ class DallasSensor { int16_t offset_; }; - DallasSensor() = default; - ~DallasSensor() = default; + TemperatureSensor() = default; + ~TemperatureSensor() = default; void start(); void loop(); @@ -97,7 +97,7 @@ class DallasSensor { return sensorfails_; } - bool dallas_enabled() { + bool sensor_enabled() { return (dallas_gpio_ != 0); } @@ -128,7 +128,7 @@ class DallasSensor { static constexpr size_t SCRATCHPAD_CONFIG = 4; static constexpr size_t SCRATCHPAD_CNT_REM = 6; - // dallas chips + // dallas chip types static constexpr uint8_t TYPE_DS18B20 = 0x28; static constexpr uint8_t TYPE_DS18S20 = 0x10; static constexpr uint8_t TYPE_DS1822 = 0x22; diff --git a/src/test/test.cpp b/src/test/test.cpp index 84540d8ae..d6b56e9d3 100644 --- a/src/test/test.cpp +++ b/src/test/test.cpp @@ -643,24 +643,24 @@ void Test::run_test(uuid::console::Shell & shell, const std::string & cmd, const ok = true; } - if (command == "dallas") { - shell.printfln("Testing adding Dallas sensor"); - emsesp::EMSESP::dallassensor_.test(); + if (command == "temperature") { + shell.printfln("Testing adding Temperature sensor"); + emsesp::EMSESP::temperaturesensor_.test(); ok = true; } - if (command == "dallas_full") { - shell.printfln("Testing adding and changing Dallas sensor"); + if (command == "temperature_full") { + shell.printfln("Testing adding and changing Temperature sensor"); Mqtt::ha_enabled(true); Mqtt::nested_format(1); // Mqtt::nested_format(0); - emsesp::EMSESP::dallassensor_.test(); + emsesp::EMSESP::temperaturesensor_.test(); shell.invoke_command("show"); shell.invoke_command("call system publish"); // rename - EMSESP::dallassensor_.update("01-0203-0405-0607", "testdallas", 2); + EMSESP::temperaturesensor_.update("01-0203-0405-0607", "testtemperature", 2); shell.invoke_command("show"); shell.invoke_command("call system publish"); ok = true; @@ -923,10 +923,10 @@ void Test::run_test(uuid::console::Shell & shell, const std::string & cmd, const */ /* - requestX.url("/api/dallassensor/xxxx"); + requestX.url("/api/temperaturesensor/xxxx"); EMSESP::webAPIService.webAPIService_get(&requestX); emsesp::EMSESP::logger().notice("****"); - requestX.url("/api/dallassensor/info"); + requestX.url("/api/temperaturesensor/info"); EMSESP::webAPIService.webAPIService_get(&requestX); return; */ @@ -1091,15 +1091,15 @@ void Test::run_test(uuid::console::Shell & shell, const std::string & cmd, const char data6[] = "{\"id\":2,\"devicevalue\":{\"v\":\"44\",\"u\":1,\"n\":\"hc2 selected room temperature\",\"c\":\"hc2/seltemp\"}"; deserializeJson(doc, data6); json = doc.as(); - request.url("/rest/writeValue"); - EMSESP::webDataService.write_value(&request, json); + request.url("/rest/writeDeviceValue"); + EMSESP::webDataService.write_device_value(&request, json); // write value from web - testing hc9/seltemp - should fail! char data7[] = "{\"id\":2,\"devicevalue\":{\"v\":\"55\",\"u\":1,\"n\":\"hc2 selected room temperature\",\"c\":\"hc9/seltemp\"}"; deserializeJson(doc, data7); json = doc.as(); - request.url("/rest/writeValue"); - EMSESP::webDataService.write_value(&request, json); + request.url("/rest/writeDeviceValue"); + EMSESP::webDataService.write_device_value(&request, json); // emsesp::EMSESP::logger().notice("*"); diff --git a/src/test/test.h b/src/test/test.h index c2c5d7b0a..36823b166 100644 --- a/src/test/test.h +++ b/src/test/test.h @@ -45,7 +45,7 @@ namespace emsesp { // #define EMSESP_DEBUG_DEFAULT "dv" // #define EMSESP_DEBUG_DEFAULT "lastcode" // #define EMSESP_DEBUG_DEFAULT "2thermostats" -// #define EMSESP_DEBUG_DEFAULT "dallas" +// #define EMSESP_DEBUG_DEFAULT "temperature" // #define EMSESP_DEBUG_DEFAULT "analog" // #define EMSESP_DEBUG_DEFAULT "api_values" // #define EMSESP_DEBUG_DEFAULT "mqtt_post" diff --git a/src/version.h b/src/version.h index 1335264e9..8afdc2259 100644 --- a/src/version.h +++ b/src/version.h @@ -1 +1 @@ -#define EMSESP_APP_VERSION "3.6.0-dev.9" +#define EMSESP_APP_VERSION "3.6.0-dev.11" diff --git a/src/web/WebCustomizationService.cpp b/src/web/WebCustomizationService.cpp index c1eb0963f..01430cdfd 100644 --- a/src/web/WebCustomizationService.cpp +++ b/src/web/WebCustomizationService.cpp @@ -57,17 +57,17 @@ WebCustomizationService::WebCustomizationService(AsyncWebServer * server, FS * f // this creates the customization file, saving it to the FS void WebCustomization::read(WebCustomization & settings, JsonObject & root) { - // Dallas Sensor customization - JsonArray sensorsJson = root.createNestedArray("sensors"); + // Temperature Sensor customization + JsonArray sensorsJson = root.createNestedArray("ts"); for (const SensorCustomization & sensor : settings.sensorCustomizations) { JsonObject sensorJson = sensorsJson.createNestedObject(); - sensorJson["id"] = sensor.id; // is + sensorJson["id"] = sensor.id; // ID of chip sensorJson["name"] = sensor.name; // n sensorJson["offset"] = sensor.offset; // o } // Analog Sensor customization - JsonArray analogJson = root.createNestedArray("analogs"); + JsonArray analogJson = root.createNestedArray("as"); for (const AnalogCustomization & sensor : settings.analogCustomizations) { JsonObject sensorJson = analogJson.createNestedObject(); sensorJson["gpio"] = sensor.gpio; // g @@ -98,7 +98,7 @@ void WebCustomization::read(WebCustomization & settings, JsonObject & root) { StateUpdateResult WebCustomization::update(JsonObject & root, WebCustomization & settings) { #ifdef EMSESP_STANDALONE // invoke some fake data for testing - const char * json = "{\"sensors\":[],\"analogs\":[],\"masked_entities\":[{\"product_id\":123,\"device_id\":8,\"entity_ids\":[\"08heatingactive|my custom " + const char * json = "{\"ts\":[],\"as\":[],\"masked_entities\":[{\"product_id\":123,\"device_id\":8,\"entity_ids\":[\"08heatingactive|my custom " "name for heating active\",\"08tapwateractive\"]}]}"; StaticJsonDocument<500> doc; @@ -111,10 +111,10 @@ StateUpdateResult WebCustomization::update(JsonObject & root, WebCustomization & Serial.println(COLOR_RESET); #endif - // Dallas Sensor customization + // Temperature Sensor customization settings.sensorCustomizations.clear(); - if (root["sensors"].is()) { - for (const JsonObject sensorJson : root["sensors"].as()) { + if (root["ts"].is()) { + for (const JsonObject sensorJson : root["ts"].as()) { // create each of the sensor, overwriting any previous settings auto sensor = SensorCustomization(); sensor.id = sensorJson["id"].as(); @@ -126,8 +126,8 @@ StateUpdateResult WebCustomization::update(JsonObject & root, WebCustomization & // Analog Sensor customization settings.analogCustomizations.clear(); - if (root["analogs"].is()) { - for (const JsonObject analogJson : root["analogs"].as()) { + if (root["as"].is()) { + for (const JsonObject analogJson : root["as"].as()) { // create each of the sensor, overwriting any previous settings auto sensor = AnalogCustomization(); sensor.gpio = analogJson["gpio"]; diff --git a/src/web/WebCustomizationService.h b/src/web/WebCustomizationService.h index 003d46cfe..7e977959a 100644 --- a/src/web/WebCustomizationService.h +++ b/src/web/WebCustomizationService.h @@ -32,7 +32,7 @@ namespace emsesp { -// Customization for dallas sensor +// Customization for temperature sensor class SensorCustomization { public: std::string id; diff --git a/src/web/WebDataService.cpp b/src/web/WebDataService.cpp index 05ecdd447..054082011 100644 --- a/src/web/WebDataService.cpp +++ b/src/web/WebDataService.cpp @@ -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 @@ -25,12 +25,13 @@ using namespace std::placeholders; // for `_1` etc WebDataService::WebDataService(AsyncWebServer * server, SecurityManager * securityManager) : _device_data_handler(DEVICE_DATA_SERVICE_PATH, securityManager->wrapCallback(std::bind(&WebDataService::device_data, this, _1, _2), AuthenticationPredicates::IS_AUTHENTICATED)) - , _write_value_handler(WRITE_VALUE_SERVICE_PATH, - securityManager->wrapCallback(std::bind(&WebDataService::write_value, this, _1, _2), AuthenticationPredicates::IS_ADMIN)) - , _write_sensor_handler(WRITE_SENSOR_SERVICE_PATH, - securityManager->wrapCallback(std::bind(&WebDataService::write_sensor, this, _1, _2), AuthenticationPredicates::IS_ADMIN)) - , _write_analog_handler(WRITE_ANALOG_SERVICE_PATH, - securityManager->wrapCallback(std::bind(&WebDataService::write_analog, this, _1, _2), AuthenticationPredicates::IS_ADMIN)) { + , _write_value_handler(WRITE_DEVICE_VALUE_SERVICE_PATH, + securityManager->wrapCallback(std::bind(&WebDataService::write_device_value, this, _1, _2), AuthenticationPredicates::IS_ADMIN)) + , _write_temperature_handler(WRITE_TEMPERATURE_SENSOR_SERVICE_PATH, + securityManager->wrapCallback(std::bind(&WebDataService::write_temperature_sensor, this, _1, _2), + AuthenticationPredicates::IS_ADMIN)) + , _write_analog_handler(WRITE_ANALOG_SENSOR_SERVICE_PATH, + securityManager->wrapCallback(std::bind(&WebDataService::write_analog_sensor, this, _1, _2), AuthenticationPredicates::IS_ADMIN)) { server->on(CORE_DATA_SERVICE_PATH, HTTP_GET, securityManager->wrapRequest(std::bind(&WebDataService::core_data, this, _1), AuthenticationPredicates::IS_AUTHENTICATED)); @@ -51,9 +52,9 @@ WebDataService::WebDataService(AsyncWebServer * server, SecurityManager * securi _write_value_handler.setMaxContentLength(256); server->addHandler(&_write_value_handler); - _write_sensor_handler.setMethod(HTTP_POST); - _write_sensor_handler.setMaxContentLength(256); - server->addHandler(&_write_sensor_handler); + _write_temperature_handler.setMethod(HTTP_POST); + _write_temperature_handler.setMaxContentLength(256); + server->addHandler(&_write_temperature_handler); _write_analog_handler.setMethod(HTTP_POST); _write_analog_handler.setMaxContentLength(256); @@ -75,40 +76,37 @@ void WebDataService::core_data(AsyncWebServerRequest * request) { // list is already sorted by device type JsonArray devices = root.createNestedArray("devices"); - char buffer[3]; for (const auto & emsdevice : EMSESP::emsdevices) { // ignore controller if (emsdevice && (emsdevice->device_type() != EMSdevice::DeviceType::CONTROLLER || emsdevice->count_entities() > 0)) { JsonObject obj = devices.createNestedObject(); - obj["id"] = Helpers::smallitoa(buffer, emsdevice->unique_id()); // a unique id as a string - obj["tn"] = emsdevice->device_type_2_device_name_translated(); // translated device type name - obj["t"] = emsdevice->device_type(); // device type number - obj["b"] = emsdevice->brand_to_char(); // brand - obj["n"] = emsdevice->name(); // name - obj["d"] = emsdevice->device_id(); // deviceid - obj["p"] = emsdevice->product_id(); // productid - obj["v"] = emsdevice->version(); // version - obj["e"] = emsdevice->count_entities(); // number of entities (device values) + obj["id"] = emsdevice->unique_id(); // a unique id + obj["tn"] = emsdevice->device_type_2_device_name_translated(); // translated device type name + obj["t"] = emsdevice->device_type(); // device type number + obj["b"] = emsdevice->brand_to_char(); // brand + obj["n"] = emsdevice->name(); // name + obj["d"] = emsdevice->device_id(); // deviceid + obj["p"] = emsdevice->product_id(); // productid + obj["v"] = emsdevice->version(); // version + // obj["e"] = emsdevice->count_entities(); // number of entities (device values) } } + + // add any custom entities if (EMSESP::webEntityService.count_entities()) { JsonObject obj = devices.createNestedObject(); - obj["id"] = "99"; // the last unique id as a string - obj["tn"] = "Custom"; // translated device type name - obj["t"] = EMSdevice::DeviceType::CUSTOM; // device type number - obj["b"] = 0; // brand - obj["n"] = Helpers::translated_word(FL_(custom_device)); // name - obj["d"] = 0; // deviceid - obj["p"] = 0; // productid - obj["v"] = 0; // version - obj["e"] = EMSESP::webEntityService.count_entities(); // number of entities (device values) + obj["id"] = 99; // the last unique id + obj["tn"] = Helpers::translated_word(FL_(custom_device)); // translated device type name + obj["t"] = EMSdevice::DeviceType::CUSTOM; // device type number + obj["b"] = Helpers::translated_word(FL_(na)); // brand + obj["n"] = Helpers::translated_word(FL_(custom_device_name)); // name + obj["d"] = 0; // deviceid + obj["p"] = 0; // productid + obj["v"] = 0; // version + // obj["e"] = EMSESP::webEntityService.count_entities(); // number of entities (device values) } - // sensors stuff - root["s_n"] = Helpers::translated_word(FL_(sensors_device)); - root["active_sensors"] = EMSESP::dallassensor_.no_sensors() + (EMSESP::analogsensor_.analog_enabled() ? EMSESP::analogsensor_.no_sensors() : 0); - root["analog_enabled"] = EMSESP::analogsensor_.analog_enabled(); - root["connected"] = EMSESP::bus_status() != 2; + root["connected"] = EMSESP::bus_status() != 2; response->setLength(); request->send(response); @@ -116,15 +114,14 @@ void WebDataService::core_data(AsyncWebServerRequest * request) { // sensor data - sends back to web // /sensorData endpoint -// the "sensors" and "analogs" are arrays and must exist void WebDataService::sensor_data(AsyncWebServerRequest * request) { auto * response = new AsyncJsonResponse(false, EMSESP_JSON_SIZE_XXLARGE); JsonObject root = response->getRoot(); - // dallas sensors - JsonArray sensors = root.createNestedArray("sensors"); - if (EMSESP::dallassensor_.have_sensors()) { - for (const auto & sensor : EMSESP::dallassensor_.sensors()) { + // temperature sensors + JsonArray sensors = root.createNestedArray("ts"); + if (EMSESP::temperaturesensor_.have_sensors()) { + for (const auto & sensor : EMSESP::temperaturesensor_.sensors()) { JsonObject obj = sensors.createNestedObject(); obj["id"] = sensor.id(); // id as string obj["n"] = sensor.name(); // name @@ -145,32 +142,29 @@ void WebDataService::sensor_data(AsyncWebServerRequest * request) { } // analog sensors - JsonArray analogs = root.createNestedArray("analogs"); + JsonArray analogs = root.createNestedArray("as"); if (EMSESP::analog_enabled() && EMSESP::analogsensor_.have_sensors()) { uint8_t count = 0; - char buffer[3]; for (const auto & sensor : EMSESP::analogsensor_.sensors()) { - // don't send if it's marked for removal - if (sensor.type() != AnalogSensor::AnalogType::MARK_DELETED) { - count++; - JsonObject obj = analogs.createNestedObject(); - obj["id"] = Helpers::smallitoa(buffer, count); // needed for sorting table - obj["g"] = sensor.gpio(); - obj["n"] = sensor.name(); - obj["u"] = sensor.uom(); - obj["o"] = sensor.offset(); - obj["f"] = sensor.factor(); - obj["t"] = sensor.type(); + JsonObject obj = analogs.createNestedObject(); + obj["id"] = ++count; // needed for sorting table + obj["g"] = sensor.gpio(); + obj["n"] = sensor.name(); + obj["u"] = sensor.uom(); + obj["o"] = sensor.offset(); + obj["f"] = sensor.factor(); + obj["t"] = sensor.type(); - if (sensor.type() != AnalogSensor::AnalogType::NOTUSED) { - obj["v"] = Helpers::transformNumFloat(sensor.value(), 0); // is optional and is a float - } else { - obj["v"] = 0; // must have a value for web sorting to work - } + if (sensor.type() != AnalogSensor::AnalogType::NOTUSED) { + obj["v"] = Helpers::transformNumFloat(sensor.value(), 0); // is optional and is a float + } else { + obj["v"] = 0; // must have a value for web sorting to work } } } + root["analog_enabled"] = EMSESP::analogsensor_.analog_enabled(); + response->setLength(); request->send(response); } @@ -227,7 +221,7 @@ void WebDataService::device_data(AsyncWebServerRequest * request, JsonVariant & // takes a command and its data value from a specific EMS Device, from the Web // assumes the service has been checked for admin authentication -void WebDataService::write_value(AsyncWebServerRequest * request, JsonVariant & json) { +void WebDataService::write_device_value(AsyncWebServerRequest * request, JsonVariant & json) { if (json.is()) { JsonObject dv = json["devicevalue"]; uint8_t unique_id = json["id"]; @@ -314,9 +308,9 @@ void WebDataService::write_value(AsyncWebServerRequest * request, JsonVariant & request->send(response); } -// takes a dallas sensor name and optional offset from the WebUI and update the customization settings -// via the Dallas service -void WebDataService::write_sensor(AsyncWebServerRequest * request, JsonVariant & json) { +// takes a temperaturesensor name and optional offset from the WebUI and update the customization settings +// via the temperaturesensor service +void WebDataService::write_temperature_sensor(AsyncWebServerRequest * request, JsonVariant & json) { bool ok = false; if (json.is()) { JsonObject sensor = json; @@ -330,7 +324,7 @@ void WebDataService::write_sensor(AsyncWebServerRequest * request, JsonVariant & if (EMSESP::system_.fahrenheit()) { offset10 = offset / 0.18; } - ok = EMSESP::dallassensor_.update(id, name, offset10); + ok = EMSESP::temperaturesensor_.update(id, name, offset10); } AsyncWebServerResponse * response = request->beginResponse(ok ? 200 : 204); @@ -338,18 +332,19 @@ void WebDataService::write_sensor(AsyncWebServerRequest * request, JsonVariant & } // update the analog record, or create a new one -void WebDataService::write_analog(AsyncWebServerRequest * request, JsonVariant & json) { +void WebDataService::write_analog_sensor(AsyncWebServerRequest * request, JsonVariant & json) { bool ok = false; if (json.is()) { JsonObject analog = json; - uint8_t gpio = analog["gpio"]; // this is the unique key, the GPIO - std::string name = analog["name"]; - double factor = analog["factor"]; - double offset = analog["offset"]; - uint8_t uom = analog["uom"]; - int8_t type = analog["type"]; - ok = EMSESP::analogsensor_.update(gpio, name, offset, factor, uom, type); + uint8_t gpio = analog["gpio"]; + std::string name = analog["name"]; + double factor = analog["factor"]; + double offset = analog["offset"]; + uint8_t uom = analog["uom"]; + int8_t type = analog["type"]; + bool deleted = analog["deleted"]; + ok = EMSESP::analogsensor_.update(gpio, name, offset, factor, uom, type, deleted); } AsyncWebServerResponse * response = request->beginResponse(ok ? 200 : 204); diff --git a/src/web/WebDataService.h b/src/web/WebDataService.h index 428093d5e..c064ab6ea 100644 --- a/src/web/WebDataService.h +++ b/src/web/WebDataService.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 @@ -26,9 +26,9 @@ #define SENSOR_DATA_SERVICE_PATH "/rest/sensorData" // POST -#define WRITE_VALUE_SERVICE_PATH "/rest/writeValue" -#define WRITE_SENSOR_SERVICE_PATH "/rest/writeSensor" -#define WRITE_ANALOG_SERVICE_PATH "/rest/writeAnalog" +#define WRITE_DEVICE_VALUE_SERVICE_PATH "/rest/writeDeviceValue" +#define WRITE_TEMPERATURE_SENSOR_SERVICE_PATH "/rest/writeTemperatureSensor" +#define WRITE_ANALOG_SENSOR_SERVICE_PATH "/rest/writeAnalogSensor" namespace emsesp { @@ -47,12 +47,12 @@ class WebDataService { // POST void device_data(AsyncWebServerRequest * request, JsonVariant & json); - void write_value(AsyncWebServerRequest * request, JsonVariant & json); - void write_sensor(AsyncWebServerRequest * request, JsonVariant & json); - void write_analog(AsyncWebServerRequest * request, JsonVariant & json); + void write_device_value(AsyncWebServerRequest * request, JsonVariant & json); + void write_temperature_sensor(AsyncWebServerRequest * request, JsonVariant & json); + void write_analog_sensor(AsyncWebServerRequest * request, JsonVariant & json); void scan_devices(AsyncWebServerRequest * request); - AsyncCallbackJsonWebHandler _device_data_handler, _write_value_handler, _write_sensor_handler, _write_analog_handler; + AsyncCallbackJsonWebHandler _device_data_handler, _write_value_handler, _write_temperature_handler, _write_analog_handler; }; } // namespace emsesp diff --git a/src/web/WebEntityService.cpp b/src/web/WebEntityService.cpp index 956dcfa87..e377a8ace 100644 --- a/src/web/WebEntityService.cpp +++ b/src/web/WebEntityService.cpp @@ -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 @@ -33,25 +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("entities"); + 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; - ei["write"] = entityItem.writeable; + JsonObject ei = entity.createNestedObject(); + ei["id"] = counter++; // id is only used to render the table and must be unique + ei["device_id"] = entityItem.device_id; + ei["type_id"] = entityItem.type_id; + 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) { @@ -59,33 +61,34 @@ StateUpdateResult WebEntity::update(JsonObject & root, WebEntity & webEntity) { } webEntity.entityItems.clear(); - 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"]; - entityItem.writeable = ei["write"]; + if (root["entities"].is()) { + for (const JsonObject ei : root["entities"].as()) { + auto entityItem = EntityItem(); + entityItem.device_id = ei["device_id"]; // send as numeric, will be converted to string in web + entityItem.type_id = 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 + if (entityItem.writeable) { Command::add( EMSdevice::DeviceType::CUSTOM, @@ -106,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; @@ -118,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 { @@ -128,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(); @@ -142,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: @@ -332,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); } @@ -343,8 +349,10 @@ 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; @@ -352,39 +360,40 @@ void WebEntityService::generate_value_web(JsonObject & output) { if (entity.writeable) { obj["c"] = entity.name; } - switch (entity.valuetype) { + + 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: @@ -410,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 172d484fa..37a94e15f 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 @@ -21,21 +21,22 @@ #define WebEntityService_h #define EMSESP_ENTITY_FILE "/config/emsespEntity.json" -#define EMSESP_ENTITY_SERVICE_PATH "/rest/entity" // GET and POST +#define EMSESP_ENTITY_SERVICE_PATH "/rest/entities" // GET and POST 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; bool writeable; - uint32_t val; + uint32_t value; }; class WebEntity { @@ -66,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; }; diff --git a/src/web/WebSchedulerService.cpp b/src/web/WebSchedulerService.cpp index 62bf6c9f5..4d29f0834 100644 --- a/src/web/WebSchedulerService.cpp +++ b/src/web/WebSchedulerService.cpp @@ -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 @@ -56,7 +56,7 @@ StateUpdateResult WebScheduler::update(JsonObject & root, WebScheduler & webSche #ifdef EMSESP_STANDALONE // invoke some fake data for testing const char * json = - "{[{\"id\":\"01\",\"active\":true,\"flags\":31,\"time\": \"07:30\",\"cmd\": \"hc1/mode\",\"value\": \"day\",\"name\": \"turn on central heating\"}]}"; + "{[{\"id\":1,\"active\":true,\"flags\":31,\"time\": \"07:30\",\"cmd\": \"hc1/mode\",\"value\": \"day\",\"name\": \"turn on central heating\"}]}"; StaticJsonDocument<500> doc; deserializeJson(doc, json); root = doc.as(); diff --git a/src/web/WebSchedulerService.h b/src/web/WebSchedulerService.h index 5b919fe3b..43501d66b 100644 --- a/src/web/WebSchedulerService.h +++ b/src/web/WebSchedulerService.h @@ -29,7 +29,6 @@ namespace emsesp { class ScheduleItem { public: - std::string id; // unqiue id boolean active; uint8_t flags; uint16_t elapsed_min; // total mins from 00:00 diff --git a/src/web/WebSettingsService.cpp b/src/web/WebSettingsService.cpp index 4022cc4b0..759d47a32 100644 --- a/src/web/WebSettingsService.cpp +++ b/src/web/WebSettingsService.cpp @@ -176,13 +176,13 @@ StateUpdateResult WebSettings::update(JsonObject & root, WebSettings & settings) settings.pbutton_gpio = root["pbutton_gpio"] | default_pbutton_gpio; check_flag(prev, settings.pbutton_gpio, ChangeFlags::BUTTON); - // dallas + // temperaturesensor prev = settings.dallas_gpio; settings.dallas_gpio = root["dallas_gpio"] | default_dallas_gpio; - check_flag(prev, settings.dallas_gpio, ChangeFlags::DALLAS); + check_flag(prev, settings.dallas_gpio, ChangeFlags::SENSOR); prev = settings.dallas_parasite; settings.dallas_parasite = root["dallas_parasite"] | EMSESP_DEFAULT_DALLAS_PARASITE; - check_flag(prev, settings.dallas_parasite, ChangeFlags::DALLAS); + check_flag(prev, settings.dallas_parasite, ChangeFlags::SENSOR); // shower prev = settings.shower_timer; @@ -301,8 +301,8 @@ void WebSettingsService::onUpdate() { EMSESP::shower_.start(); } - if (WebSettings::has_flags(WebSettings::ChangeFlags::DALLAS)) { - EMSESP::dallassensor_.start(); + if (WebSettings::has_flags(WebSettings::ChangeFlags::SENSOR)) { + EMSESP::temperaturesensor_.start(); } if (WebSettings::has_flags(WebSettings::ChangeFlags::UART)) { diff --git a/src/web/WebSettingsService.h b/src/web/WebSettingsService.h index 25c7b8d11..61af4161b 100644 --- a/src/web/WebSettingsService.h +++ b/src/web/WebSettingsService.h @@ -79,7 +79,7 @@ class WebSettings { UART = (1 << 0), // 1 SYSLOG = (1 << 1), // 2 ADC = (1 << 2), // 4 - analog - DALLAS = (1 << 3), // 8 + SENSOR = (1 << 3), // 8 SHOWER = (1 << 4), // 16 LED = (1 << 5), // 32 BUTTON = (1 << 6), // 64 diff --git a/src/web/WebStatusService.cpp b/src/web/WebStatusService.cpp index 0b76bba61..9777f2118 100644 --- a/src/web/WebStatusService.cpp +++ b/src/web/WebStatusService.cpp @@ -124,7 +124,7 @@ void WebStatusService::webStatusService(AsyncWebServerRequest * request) { root["tx_mode"] = EMSESP::txservice_.tx_mode(); root["uptime"] = EMSbus::bus_uptime(); root["num_devices"] = EMSESP::count_devices(); // excluding Controller - root["num_sensors"] = EMSESP::dallassensor_.no_sensors(); + root["num_sensors"] = EMSESP::temperaturesensor_.no_sensors(); root["num_analogs"] = EMSESP::analogsensor_.no_sensors(); JsonArray statsJson = root.createNestedArray("stats"); @@ -148,12 +148,13 @@ void WebStatusService::webStatusService(AsyncWebServerRequest * request) { statJson["f"] = EMSESP::txservice_.telegram_write_fail_count(); statJson["q"] = EMSESP::txservice_.write_quality(); - if (EMSESP::dallassensor_.dallas_enabled()) { + if (EMSESP::temperaturesensor_.sensor_enabled()) { statJson = statsJson.createNestedObject(); statJson["id"] = 3; - statJson["s"] = EMSESP::dallassensor_.reads(); - statJson["f"] = EMSESP::dallassensor_.fails(); - statJson["q"] = EMSESP::dallassensor_.reads() == 0 ? 100 : 100 - (uint8_t)((100 * EMSESP::dallassensor_.fails()) / EMSESP::dallassensor_.reads()); + statJson["s"] = EMSESP::temperaturesensor_.reads(); + statJson["f"] = EMSESP::temperaturesensor_.fails(); + statJson["q"] = + EMSESP::temperaturesensor_.reads() == 0 ? 100 : 100 - (uint8_t)((100 * EMSESP::temperaturesensor_.fails()) / EMSESP::temperaturesensor_.reads()); } if (EMSESP::analog_enabled()) { statJson = statsJson.createNestedObject();