diff --git a/interface/package-lock.json b/interface/package-lock.json index 68b45c481..a9ca3a7a6 100644 --- a/interface/package-lock.json +++ b/interface/package-lock.json @@ -1,11 +1,11 @@ { - "name": "esp8266-react", + "name": "emsesp-react", "version": "0.1.0", "lockfileVersion": 2, "requires": true, "packages": { "": { - "name": "esp8266-react", + "name": "emsesp-react", "version": "0.1.0", "dependencies": { "@material-ui/core": "^4.11.4", @@ -8860,18 +8860,6 @@ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" }, - "node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", @@ -19797,284 +19785,6 @@ "watchpack-chokidar2": "^2.0.1" } }, - "node_modules/watchpack-chokidar2": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/watchpack-chokidar2/-/watchpack-chokidar2-2.0.1.tgz", - "integrity": "sha512-nCFfBIPKr5Sh61s4LPpy1Wtfi0HE8isJ3d2Yb5/Ppw2P2B/3eVSEBjKfN0fmHJSK14+31KwMKmcrzs2GM4P0Ww==", - "optional": true, - "dependencies": { - "chokidar": "^2.1.8" - } - }, - "node_modules/watchpack-chokidar2/node_modules/anymatch": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", - "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", - "optional": true, - "dependencies": { - "micromatch": "^3.1.4", - "normalize-path": "^2.1.1" - } - }, - "node_modules/watchpack-chokidar2/node_modules/anymatch/node_modules/normalize-path": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", - "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", - "optional": true, - "dependencies": { - "remove-trailing-separator": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/watchpack-chokidar2/node_modules/binary-extensions": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz", - "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==", - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/watchpack-chokidar2/node_modules/braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "optional": true, - "dependencies": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/watchpack-chokidar2/node_modules/braces/node_modules/extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "optional": true, - "dependencies": { - "is-extendable": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/watchpack-chokidar2/node_modules/chokidar": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", - "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==", - "optional": true, - "dependencies": { - "anymatch": "^2.0.0", - "async-each": "^1.0.1", - "braces": "^2.3.2", - "fsevents": "^1.2.7", - "glob-parent": "^3.1.0", - "inherits": "^2.0.3", - "is-binary-path": "^1.0.0", - "is-glob": "^4.0.0", - "normalize-path": "^3.0.0", - "path-is-absolute": "^1.0.0", - "readdirp": "^2.2.1", - "upath": "^1.1.1" - } - }, - "node_modules/watchpack-chokidar2/node_modules/fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", - "optional": true, - "dependencies": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/watchpack-chokidar2/node_modules/fill-range/node_modules/extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "optional": true, - "dependencies": { - "is-extendable": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/watchpack-chokidar2/node_modules/fsevents": { - "version": "1.2.13", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", - "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 4.0" - } - }, - "node_modules/watchpack-chokidar2/node_modules/glob-parent": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", - "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", - "optional": true, - "dependencies": { - "is-glob": "^3.1.0", - "path-dirname": "^1.0.0" - } - }, - "node_modules/watchpack-chokidar2/node_modules/glob-parent/node_modules/is-glob": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", - "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", - "optional": true, - "dependencies": { - "is-extglob": "^2.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/watchpack-chokidar2/node_modules/is-binary-path": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", - "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", - "optional": true, - "dependencies": { - "binary-extensions": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/watchpack-chokidar2/node_modules/is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "optional": true, - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/watchpack-chokidar2/node_modules/is-number/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "optional": true, - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/watchpack-chokidar2/node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "optional": true - }, - "node_modules/watchpack-chokidar2/node_modules/micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "optional": true, - "dependencies": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/watchpack-chokidar2/node_modules/readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "optional": true, - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/watchpack-chokidar2/node_modules/readdirp": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", - "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", - "optional": true, - "dependencies": { - "graceful-fs": "^4.1.11", - "micromatch": "^3.1.10", - "readable-stream": "^2.0.2" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/watchpack-chokidar2/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "optional": true - }, - "node_modules/watchpack-chokidar2/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "optional": true, - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/watchpack-chokidar2/node_modules/to-regex-range": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", - "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", - "optional": true, - "dependencies": { - "is-number": "^3.0.0", - "repeat-string": "^1.6.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/wbuf": { "version": "1.7.3", "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", @@ -20376,19 +20086,6 @@ "node": ">=6" } }, - "node_modules/webpack-dev-server/node_modules/fsevents": { - "version": "1.2.13", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", - "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 4.0" - } - }, "node_modules/webpack-dev-server/node_modules/glob-parent": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", @@ -29146,12 +28843,6 @@ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" }, - "fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "optional": true - }, "function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", @@ -38130,250 +37821,6 @@ "watchpack-chokidar2": "^2.0.1" } }, - "watchpack-chokidar2": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/watchpack-chokidar2/-/watchpack-chokidar2-2.0.1.tgz", - "integrity": "sha512-nCFfBIPKr5Sh61s4LPpy1Wtfi0HE8isJ3d2Yb5/Ppw2P2B/3eVSEBjKfN0fmHJSK14+31KwMKmcrzs2GM4P0Ww==", - "optional": true, - "requires": { - "chokidar": "^2.1.8" - }, - "dependencies": { - "anymatch": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", - "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", - "optional": true, - "requires": { - "micromatch": "^3.1.4", - "normalize-path": "^2.1.1" - }, - "dependencies": { - "normalize-path": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", - "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", - "optional": true, - "requires": { - "remove-trailing-separator": "^1.0.1" - } - } - } - }, - "binary-extensions": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz", - "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==", - "optional": true - }, - "braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "optional": true, - "requires": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "optional": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "chokidar": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", - "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==", - "optional": true, - "requires": { - "anymatch": "^2.0.0", - "async-each": "^1.0.1", - "braces": "^2.3.2", - "fsevents": "^1.2.7", - "glob-parent": "^3.1.0", - "inherits": "^2.0.3", - "is-binary-path": "^1.0.0", - "is-glob": "^4.0.0", - "normalize-path": "^3.0.0", - "path-is-absolute": "^1.0.0", - "readdirp": "^2.2.1", - "upath": "^1.1.1" - } - }, - "fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", - "optional": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "optional": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "fsevents": { - "version": "1.2.13", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", - "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", - "optional": true - }, - "glob-parent": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", - "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", - "optional": true, - "requires": { - "is-glob": "^3.1.0", - "path-dirname": "^1.0.0" - }, - "dependencies": { - "is-glob": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", - "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", - "optional": true, - "requires": { - "is-extglob": "^2.1.0" - } - } - } - }, - "is-binary-path": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", - "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", - "optional": true, - "requires": { - "binary-extensions": "^1.0.0" - } - }, - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "optional": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "optional": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "optional": true - }, - "micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "optional": true, - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - } - }, - "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "optional": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "readdirp": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", - "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", - "optional": true, - "requires": { - "graceful-fs": "^4.1.11", - "micromatch": "^3.1.10", - "readable-stream": "^2.0.2" - } - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "optional": true - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "optional": true, - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "to-regex-range": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", - "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", - "optional": true, - "requires": { - "is-number": "^3.0.0", - "repeat-string": "^1.6.1" - } - } - } - }, "wbuf": { "version": "1.7.3", "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", @@ -38852,12 +38299,6 @@ "locate-path": "^3.0.0" } }, - "fsevents": { - "version": "1.2.13", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", - "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", - "optional": true - }, "glob-parent": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", diff --git a/interface/package.json b/interface/package.json index 5e34e6b2a..fd3d99c94 100644 --- a/interface/package.json +++ b/interface/package.json @@ -1,5 +1,5 @@ { - "name": "esp8266-react", + "name": "emsesp-react", "version": "0.1.0", "private": true, "dependencies": { diff --git a/interface/src/api/Env.ts b/interface/src/api/Env.ts index 59a1263ae..3bfef1d74 100644 --- a/interface/src/api/Env.ts +++ b/interface/src/api/Env.ts @@ -3,6 +3,7 @@ export const PROJECT_PATH = process.env.REACT_APP_PROJECT_PATH!; export const ENDPOINT_ROOT = calculateEndpointRoot('/rest/'); export const WEB_SOCKET_ROOT = calculateWebSocketRoot('/ws/'); +export const EVENT_SOURCE_ROOT = calculateEndpointRoot('/es/'); function calculateEndpointRoot(endpointPath: string) { const httpRoot = process.env.REACT_APP_HTTP_ROOT; diff --git a/interface/src/components/SectionContent.tsx b/interface/src/components/SectionContent.tsx index 3d200b66a..fdd0ede8a 100644 --- a/interface/src/components/SectionContent.tsx +++ b/interface/src/components/SectionContent.tsx @@ -15,13 +15,14 @@ const useStyles = makeStyles((theme: Theme) => interface SectionContentProps { title: string; titleGutter?: boolean; + id?: string; } const SectionContent: React.FC = (props) => { - const { children, title, titleGutter } = props; + const { children, title, titleGutter, id } = props; const classes = useStyles(); return ( - + {title} diff --git a/interface/src/components/WindowSize.tsx b/interface/src/components/WindowSize.tsx new file mode 100644 index 000000000..d69405ae8 --- /dev/null +++ b/interface/src/components/WindowSize.tsx @@ -0,0 +1,14 @@ +import { useLayoutEffect, useState } from 'react'; + +export function 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; +} diff --git a/interface/src/components/index.ts b/interface/src/components/index.ts index 4a6cc6a7b..47348a94d 100644 --- a/interface/src/components/index.ts +++ b/interface/src/components/index.ts @@ -15,3 +15,5 @@ export * from './RestController'; export * from './WebSocketFormLoader'; export * from './WebSocketController'; + +export * from './WindowSize'; diff --git a/interface/src/project/EMSESPDashboard.tsx b/interface/src/project/EMSESPDashboard.tsx index 55a396085..b567aa79f 100644 --- a/interface/src/project/EMSESPDashboard.tsx +++ b/interface/src/project/EMSESPDashboard.tsx @@ -24,7 +24,10 @@ class EMSESP extends Component { onChange={(e, path) => this.handleTabChange(path)} variant="fullWidth" > - + diff --git a/interface/src/project/EMSESPDevicesController.tsx b/interface/src/project/EMSESPDevicesController.tsx index 66a52bd3a..7300a5a48 100644 --- a/interface/src/project/EMSESPDevicesController.tsx +++ b/interface/src/project/EMSESPDevicesController.tsx @@ -6,6 +6,7 @@ import { RestFormLoader, SectionContent } from '../components'; + import { ENDPOINT_ROOT } from '../api'; import EMSESPDevicesForm from './EMSESPDevicesForm'; import { EMSESPDevices } from './EMSESPtypes'; @@ -21,7 +22,7 @@ class EMSESPDevicesController extends Component { render() { return ( - + } diff --git a/interface/src/project/EMSESPDevicesForm.tsx b/interface/src/project/EMSESPDevicesForm.tsx index bb604151c..98630dae2 100644 --- a/interface/src/project/EMSESPDevicesForm.tsx +++ b/interface/src/project/EMSESPDevicesForm.tsx @@ -36,6 +36,7 @@ import { withAuthenticatedContext, AuthenticatedContextProps } from '../authentication'; + import { RestFormProps, FormButton, extractEventValue } from '../components'; import { diff --git a/interface/src/project/EMSESPSettingsController.tsx b/interface/src/project/EMSESPSettingsController.tsx index c173e74e6..08a0eba45 100644 --- a/interface/src/project/EMSESPSettingsController.tsx +++ b/interface/src/project/EMSESPSettingsController.tsx @@ -23,14 +23,12 @@ class EMSESPSettingsController extends Component render() { return ( - // } /> - // ); } } diff --git a/interface/src/project/EMSESPSettingsForm.tsx b/interface/src/project/EMSESPSettingsForm.tsx index 16c075243..6045d5556 100644 --- a/interface/src/project/EMSESPSettingsForm.tsx +++ b/interface/src/project/EMSESPSettingsForm.tsx @@ -1,4 +1,5 @@ -import React from 'react'; +import { Component } from 'react'; + import { ValidatorForm, TextValidator, @@ -11,13 +12,13 @@ import { Box, Link, withWidth, - WithWidthProps + WithWidthProps, + Grid } from '@material-ui/core'; + import SaveIcon from '@material-ui/icons/Save'; import MenuItem from '@material-ui/core/MenuItem'; -import Grid from '@material-ui/core/Grid'; - import { redirectingAuthorizedFetch, withAuthenticatedContext, @@ -48,7 +49,7 @@ interface EMSESPSettingsFormState { processing: boolean; } -class EMSESPSettingsForm extends React.Component { +class EMSESPSettingsForm extends Component { state: EMSESPSettingsFormState = { processing: false }; diff --git a/interface/src/project/EMSESPStatusController.tsx b/interface/src/project/EMSESPStatusController.tsx index 83ba70b50..837b8db3b 100644 --- a/interface/src/project/EMSESPStatusController.tsx +++ b/interface/src/project/EMSESPStatusController.tsx @@ -21,7 +21,7 @@ class EMSESPStatusController extends Component { render() { return ( - + } diff --git a/interface/src/setupProxy.js b/interface/src/setupProxy.js index 7c34fb4c6..5a8e4ac3d 100644 --- a/interface/src/setupProxy.js +++ b/interface/src/setupProxy.js @@ -9,4 +9,13 @@ module.exports = function (app) { changeOrigin: true }) ); + + app.use( + '/es/*', + createProxyMiddleware({ + target: 'http://localhost:3090', + secure: false, + changeOrigin: true + }) + ); }; diff --git a/interface/src/system/LogEventConsole.tsx b/interface/src/system/LogEventConsole.tsx new file mode 100644 index 000000000..1942639a0 --- /dev/null +++ b/interface/src/system/LogEventConsole.tsx @@ -0,0 +1,121 @@ +import { FC } from 'react'; + +import { LogEvent, LogLevel } from './types'; +import { Theme, makeStyles, Box } from '@material-ui/core'; +import { useWindowSize } from '../components'; + +interface LogEventConsoleProps { + events: LogEvent[]; +} + +interface Offsets { + topOffset: () => number; + leftOffset: () => number; +} + +const topOffset = () => + document.getElementById('log-window')?.getBoundingClientRect().bottom || 0; + +const leftOffset = () => + document.getElementById('log-window')?.getBoundingClientRect().left || 0; + +const useStyles = makeStyles((theme: Theme) => ({ + console: { + padding: theme.spacing(1), + position: 'absolute', + left: (offsets: Offsets) => offsets.leftOffset(), + right: 24, + top: (offsets: Offsets) => offsets.topOffset(), + bottom: 24, + backgroundColor: 'black', + overflow: 'auto' + }, + entry: { + color: '#bbbbbb', + fontFamily: 'Courier New, monospace', + fontSize: '13px', + letterSpacing: 'normal', + whiteSpace: 'nowrap' + }, + debug: { + color: '#00FFFF' + }, + info: { + color: '#ffff00' + }, + notice: { + color: '#ffff00' + }, + err: { + color: '#ff0000' + }, + unknown: { + color: '#ffffff' + } +})); + +const LogEventConsole: FC = (props) => { + useWindowSize(); + const classes = useStyles({ topOffset, leftOffset }); + const { events } = props; + + const styleLevel = (level: LogLevel) => { + switch (level) { + case LogLevel.DEBUG: + return classes.debug; + case LogLevel.INFO: + return classes.info; + case LogLevel.NOTICE: + return classes.notice; + case LogLevel.WARNING: + case LogLevel.ERROR: + return classes.err; + default: + return classes.unknown; + } + }; + + const levelLabel = (level: LogLevel) => { + switch (level) { + case LogLevel.DEBUG: + return 'DEBUG'; + case LogLevel.INFO: + return 'INFO'; + case LogLevel.ERROR: + return 'ERROR'; + case LogLevel.NOTICE: + return 'NOTICE'; + case LogLevel.WARNING: + return 'WARNING'; + case LogLevel.TRACE: + return 'TRACE'; + default: + return '?'; + } + }; + + const paddedLevelLabel = (level: LogLevel) => { + const label = levelLabel(level); + return label.padStart(8, '\xa0'); + }; + + const paddedNameLabel = (name: string) => { + const label = '[' + name + ']'; + return label.padStart(8, '\xa0'); + }; + + return ( + + {events.map((e) => ( +
+ {e.t} + {paddedLevelLabel(e.l)} + {paddedNameLabel(e.n)} + {e.m} +
+ ))} +
+ ); +}; + +export default LogEventConsole; diff --git a/interface/src/system/LogEventController.tsx b/interface/src/system/LogEventController.tsx new file mode 100644 index 000000000..cdeffafb1 --- /dev/null +++ b/interface/src/system/LogEventController.tsx @@ -0,0 +1,118 @@ +import { Component } from 'react'; + +import { + restController, + RestControllerProps, + RestFormLoader, + SectionContent +} from '../components'; + +import { addAccessTokenParameter } from '../authentication'; + +import { ENDPOINT_ROOT, EVENT_SOURCE_ROOT } from '../api'; +export const FETCH_LOG_ENDPOINT = ENDPOINT_ROOT + 'fetchLog'; +export const LOG_SETTINGS_ENDPOINT = ENDPOINT_ROOT + 'logSettings'; + +export const LOG_EVENT_EVENT_SOURCE_URL = EVENT_SOURCE_ROOT + 'log'; + +import LogEventForm from './LogEventForm'; +import LogEventConsole from './LogEventConsole'; + +import { LogEvent, LogSettings } from './types'; + +import { Decoder } from '@msgpack/msgpack'; +const decoder = new Decoder(); + +interface LogEventControllerState { + eventSource?: EventSource; + events: LogEvent[]; +} + +type LogEventControllerProps = RestControllerProps; + +class LogEventController extends Component< + LogEventControllerProps, + LogEventControllerState +> { + eventSource?: EventSource; + reconnectTimeout?: NodeJS.Timeout; + + constructor(props: LogEventControllerProps) { + super(props); + this.state = { + events: [] + }; + } + + componentDidMount() { + this.props.loadData(); + this.fetchLog(); + this.configureEventSource(); + } + + componentWillUnmount() { + if (this.eventSource) { + this.eventSource.close(); + } + if (this.reconnectTimeout) { + clearTimeout(this.reconnectTimeout); + } + } + + fetchLog = () => { + fetch(FETCH_LOG_ENDPOINT) + .then((response) => { + if (response.status === 200) { + return response.arrayBuffer(); + } else { + throw Error('Unexpected status code: ' + response.status); + } + }) + .then((arrayBuffer) => { + const json: any = decoder.decode(arrayBuffer); + this.setState({ events: json.events }); + }) + .catch((error) => { + this.setState({ events: [] }); + throw Error('Unexpected error: ' + error); + }); + }; + + configureEventSource = () => { + this.eventSource = new EventSource( + addAccessTokenParameter(LOG_EVENT_EVENT_SOURCE_URL) + ); + this.eventSource.onmessage = this.onMessage; + this.eventSource.onerror = this.onError; + }; + + onError = () => { + if (this.eventSource && this.reconnectTimeout) { + this.eventSource.close(); + this.eventSource = undefined; + this.reconnectTimeout = setTimeout(this.configureEventSource, 1000); + } + }; + + onMessage = (event: MessageEvent) => { + const rawData = event.data; + if (typeof rawData === 'string' || rawData instanceof String) { + const event = JSON.parse(rawData as string) as LogEvent; + this.setState((state) => ({ events: [...state.events, event] })); + } + }; + + render() { + return ( + + } + /> + + + ); + } +} + +export default restController(LOG_SETTINGS_ENDPOINT, LogEventController); diff --git a/interface/src/system/LogEventForm.tsx b/interface/src/system/LogEventForm.tsx new file mode 100644 index 000000000..f234e8a2d --- /dev/null +++ b/interface/src/system/LogEventForm.tsx @@ -0,0 +1,107 @@ +import { Component } from 'react'; + +import { + ValidatorForm, + SelectValidator +} from 'react-material-ui-form-validator'; + +import { Typography, Grid } from '@material-ui/core'; + +import MenuItem from '@material-ui/core/MenuItem'; + +import { + redirectingAuthorizedFetch, + withAuthenticatedContext, + AuthenticatedContextProps +} from '../authentication'; + +import { RestFormProps } from '../components'; +import { LogSettings } from './types'; + +import { ENDPOINT_ROOT } from '../api'; +export const LOG_SETTINGS_ENDPOINT = ENDPOINT_ROOT + 'logSettings'; + +type LogEventFormProps = AuthenticatedContextProps & RestFormProps; + +class LogEventForm extends Component { + changeLevel = (event: React.ChangeEvent) => { + const { data, setData } = this.props; + setData({ + ...data, + level: parseInt(event.target.value) + }); + + redirectingAuthorizedFetch(LOG_SETTINGS_ENDPOINT, { + method: 'POST', + body: JSON.stringify({ level: event.target.value }), + headers: { + 'Content-Type': 'application/json' + } + }) + .then((response) => { + if (response.status === 200) { + return response.json(); + } + throw Error('Unexpected response code: ' + response.status); + }) + .then((json) => { + this.props.enqueueSnackbar('Log settings changed', { + variant: 'success' + }); + setData({ + ...data, + level: json.level + }); + }) + .catch((error) => { + this.props.enqueueSnackbar( + error.message || 'Problem changing log settings', + { variant: 'warning' } + ); + }); + }; + + render() { + const { data, saveData } = this.props; + return ( + + + + + OFF + ERROR + WARNING + NOTICE + INFO + DEBUG + TRACE + + + + + + (the last {data.max_messages} messages are buffered and new log + events are shown in real time) + + + + + + ); + } +} + +export default withAuthenticatedContext(LogEventForm); diff --git a/interface/src/system/OTASettingsController.tsx b/interface/src/system/OTASettingsController.tsx index 0ad07d6ce..b18f5ce5a 100644 --- a/interface/src/system/OTASettingsController.tsx +++ b/interface/src/system/OTASettingsController.tsx @@ -1,4 +1,4 @@ -import React, { Component } from 'react'; +import { Component } from 'react'; import { restController, diff --git a/interface/src/system/OTASettingsForm.tsx b/interface/src/system/OTASettingsForm.tsx index c511e9db9..394eba4a8 100644 --- a/interface/src/system/OTASettingsForm.tsx +++ b/interface/src/system/OTASettingsForm.tsx @@ -11,6 +11,7 @@ import { FormButton, FormActions } from '../components'; + import { isIP, isHostname, or } from '../validators'; import { OTASettings } from './types'; diff --git a/interface/src/system/System.tsx b/interface/src/system/System.tsx index 8b41c5deb..e9bdf0394 100644 --- a/interface/src/system/System.tsx +++ b/interface/src/system/System.tsx @@ -15,6 +15,7 @@ import { MenuAppBar } from '../components'; import SystemStatusController from './SystemStatusController'; import OTASettingsController from './OTASettingsController'; import UploadFirmwareController from './UploadFirmwareController'; +import LogEventController from './LogEventController'; type SystemProps = AuthenticatedContextProps & RouteComponentProps & @@ -35,6 +36,7 @@ class System extends Component { variant="fullWidth" > + {features.ota && ( { path="/system/status" component={SystemStatusController} /> + {features.ota && ( ) When developing and testing the web interface, it's handy not to bother with re-flashing an ESP32 each time. The idea is to mimic the ESP using a mock/stub server that responds to the REST (HTTP POST & GET) and WebSocket calls. @@ -16,7 +15,7 @@ and to run it ```sh % cd interface -% npm run dev +% npm run standalone ``` ## Notes @@ -24,6 +23,18 @@ and to run it - new file `interface/src/setupProxy.js` - new files `mock-api/server.js` with the hardcoded data. Requires its own npm packages for express -## ToDo +## Testing + +```bash +% curl -i http://localhost:3080/rest/emsespSettings +``` + +or from a browser use port 3000 since `setupProxy.js` is redirecting, like http://172.22.227.82:3000/rest/emsespSettings + +http://172.22.227.82:3090/es/log?access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiYWRtaW4iOnRydWUsInZlcnNpb24iOiIzLjAuMmIwIn0.MsHSgoJKI1lyYz77EiT5ZN3ECMrb4mPv9FNy3udq0TU + +Testing the EventSource/SSE use http://172.22.227.82:3090/es/log + +## To Do - add filter rule to prevent from exposing yourself to malicious attacks when running the dev server() diff --git a/mock-api/server.js b/mock-api/server.js index aac713f1c..7cda4f533 100644 --- a/mock-api/server.js +++ b/mock-api/server.js @@ -1,21 +1,72 @@ const express = require('express') const path = require('path') - const msgpack = require('@msgpack/msgpack') -// import { encode } from "@msgpack/msgpack"; +// REST API const app = express() const port = process.env.PORT || 3080 - +const REST_ENDPOINT_ROOT = '/rest/' app.use(express.static(path.join(__dirname, '../interface/build'))) app.use(express.json()) -const ENDPOINT_ROOT = '/rest/' +// ES API +const server = express() +const es_port = 3090 +const ES_ENDPOINT_ROOT = '/es/' + +// LOG +const LOG_SETTINGS_ENDPOINT = REST_ENDPOINT_ROOT + 'logSettings' +const log_settings = { + level: 6, + max_messages: 30, +} + +const FETCH_LOG_ENDPOINT = REST_ENDPOINT_ROOT + 'fetchLog' +const fetch_log = { + events: [ + { + t: '000+00:00:00.001', + l: 3, + n: 'system', + m: 'this is message 3', + }, + { + t: '000+00:00:00.002', + l: 4, + n: 'system', + m: 'this is message 4', + }, + { + t: '000+00:00:00.002', + l: 5, + n: 'system', + m: 'this is message 5', + }, + { + t: '000+00:00:00.002', + l: 6, + n: 'system', + m: 'this is message 6', + }, + { + t: '000+00:00:00.002', + l: 7, + n: 'emsesp', + m: 'this is message 7', + }, + { + t: '000+00:00:00.002', + l: 8, + n: 'mqtt', + m: 'this is message 8', + }, + ], +} // NTP -const NTP_STATUS_ENDPOINT = ENDPOINT_ROOT + 'ntpStatus' -const NTP_SETTINGS_ENDPOINT = ENDPOINT_ROOT + 'ntpSettings' -const TIME_ENDPOINT = ENDPOINT_ROOT + 'time' +const NTP_STATUS_ENDPOINT = REST_ENDPOINT_ROOT + 'ntpStatus' +const NTP_SETTINGS_ENDPOINT = REST_ENDPOINT_ROOT + 'ntpSettings' +const TIME_ENDPOINT = REST_ENDPOINT_ROOT + 'time' const ntp_settings = { enabled: true, server: 'time.google.com', @@ -31,8 +82,8 @@ const ntp_status = { } // AP -const AP_SETTINGS_ENDPOINT = ENDPOINT_ROOT + 'apSettings' -const AP_STATUS_ENDPOINT = ENDPOINT_ROOT + 'apStatus' +const AP_SETTINGS_ENDPOINT = REST_ENDPOINT_ROOT + 'apSettings' +const AP_STATUS_ENDPOINT = REST_ENDPOINT_ROOT + 'apStatus' const ap_settings = { provision_mode: 1, ssid: 'ems-esp', @@ -49,10 +100,10 @@ const ap_status = { } // NETWORK -const NETWORK_SETTINGS_ENDPOINT = ENDPOINT_ROOT + 'networkSettings' -const NETWORK_STATUS_ENDPOINT = ENDPOINT_ROOT + 'networkStatus' -const SCAN_NETWORKS_ENDPOINT = ENDPOINT_ROOT + 'scanNetworks' -const LIST_NETWORKS_ENDPOINT = ENDPOINT_ROOT + 'listNetworks' +const NETWORK_SETTINGS_ENDPOINT = REST_ENDPOINT_ROOT + 'networkSettings' +const NETWORK_STATUS_ENDPOINT = REST_ENDPOINT_ROOT + 'networkStatus' +const SCAN_NETWORKS_ENDPOINT = REST_ENDPOINT_ROOT + 'scanNetworks' +const LIST_NETWORKS_ENDPOINT = REST_ENDPOINT_ROOT + 'listNetworks' const network_settings = { ssid: 'myWifi', password: 'myPassword', @@ -134,7 +185,7 @@ const list_networks = { } // OTA -const OTA_SETTINGS_ENDPOINT = ENDPOINT_ROOT + 'otaSettings' +const OTA_SETTINGS_ENDPOINT = REST_ENDPOINT_ROOT + 'otaSettings' const ota_settings = { enabled: true, port: 8266, @@ -142,8 +193,8 @@ const ota_settings = { } // MQTT -const MQTT_SETTINGS_ENDPOINT = ENDPOINT_ROOT + 'mqttSettings' -const MQTT_STATUS_ENDPOINT = ENDPOINT_ROOT + 'mqttStatus' +const MQTT_SETTINGS_ENDPOINT = REST_ENDPOINT_ROOT + 'mqttSettings' +const MQTT_STATUS_ENDPOINT = REST_ENDPOINT_ROOT + 'mqttStatus' const mqtt_settings = { enabled: true, host: '192.168.1.4', @@ -179,15 +230,15 @@ const mqtt_status = { } // SYSTEM -const FEATURES_ENDPOINT = ENDPOINT_ROOT + 'features' -const VERIFY_AUTHORIZATION_ENDPOINT = ENDPOINT_ROOT + 'verifyAuthorization' -const SYSTEM_STATUS_ENDPOINT = ENDPOINT_ROOT + 'systemStatus' -const SECURITY_SETTINGS_ENDPOINT = ENDPOINT_ROOT + 'securitySettings' -const RESTART_ENDPOINT = ENDPOINT_ROOT + 'restart' -const FACTORY_RESET_ENDPOINT = ENDPOINT_ROOT + 'factoryReset' -const UPLOAD_FIRMWARE_ENDPOINT = ENDPOINT_ROOT + 'uploadFirmware' -const SIGN_IN_ENDPOINT = ENDPOINT_ROOT + 'signIn' -const GENERATE_TOKEN_ENDPOINT = ENDPOINT_ROOT + 'generateToken' +const FEATURES_ENDPOINT = REST_ENDPOINT_ROOT + 'features' +const VERIFY_AUTHORIZATION_ENDPOINT = REST_ENDPOINT_ROOT + 'verifyAuthorization' +const SYSTEM_STATUS_ENDPOINT = REST_ENDPOINT_ROOT + 'systemStatus' +const SECURITY_SETTINGS_ENDPOINT = REST_ENDPOINT_ROOT + 'securitySettings' +const RESTART_ENDPOINT = REST_ENDPOINT_ROOT + 'restart' +const FACTORY_RESET_ENDPOINT = REST_ENDPOINT_ROOT + 'factoryReset' +const UPLOAD_FIRMWARE_ENDPOINT = REST_ENDPOINT_ROOT + 'uploadFirmware' +const SIGN_IN_ENDPOINT = REST_ENDPOINT_ROOT + 'signIn' +const GENERATE_TOKEN_ENDPOINT = REST_ENDPOINT_ROOT + 'generateToken' const system_status = { emsesp_version: '3.1 demo', esp_platform: 'ESP32', @@ -226,13 +277,13 @@ const signin = { const generate_token = { token: '1234' } // EMS-ESP Project specific -const EMSESP_SETTINGS_ENDPOINT = ENDPOINT_ROOT + 'emsespSettings' -const EMSESP_ALLDEVICES_ENDPOINT = ENDPOINT_ROOT + 'allDevices' -const EMSESP_SCANDEVICES_ENDPOINT = ENDPOINT_ROOT + 'scanDevices' -const EMSESP_DEVICEDATA_ENDPOINT = ENDPOINT_ROOT + 'deviceData' -const EMSESP_STATUS_ENDPOINT = ENDPOINT_ROOT + 'emsespStatus' -const EMSESP_BOARDPROFILE_ENDPOINT = ENDPOINT_ROOT + 'boardProfile' -const WRITE_VALUE_ENDPOINT = ENDPOINT_ROOT + 'writeValue' +const EMSESP_SETTINGS_ENDPOINT = REST_ENDPOINT_ROOT + 'emsespSettings' +const EMSESP_ALLDEVICES_ENDPOINT = REST_ENDPOINT_ROOT + 'allDevices' +const EMSESP_SCANDEVICES_ENDPOINT = REST_ENDPOINT_ROOT + 'scanDevices' +const EMSESP_DEVICEDATA_ENDPOINT = REST_ENDPOINT_ROOT + 'deviceData' +const EMSESP_STATUS_ENDPOINT = REST_ENDPOINT_ROOT + 'emsespStatus' +const EMSESP_BOARDPROFILE_ENDPOINT = REST_ENDPOINT_ROOT + 'boardProfile' +const WRITE_VALUE_ENDPOINT = REST_ENDPOINT_ROOT + 'writeValue' const emsesp_settings = { tx_mode: 1, tx_delay: 0, @@ -701,6 +752,23 @@ const emsesp_devicedata_3 = { data: [], } +// LOG +app.get(FETCH_LOG_ENDPOINT, (req, res) => { + const encoded = msgpack.encode(fetch_log) + res.write(encoded, 'binary') + res.end(null, 'binary') +}) +app.get(LOG_SETTINGS_ENDPOINT, (req, res) => { + res.json(log_settings) +}) +app.post(LOG_SETTINGS_ENDPOINT, (req, res) => { + console.log('New log level is ' + req.body.level) + const data = { + level: req.body.level, + } + res.json(data) +}) + // NETWORK app.get(NETWORK_STATUS_ENDPOINT, (req, res) => { res.json(network_status) @@ -912,5 +980,61 @@ app.post(EMSESP_BOARDPROFILE_ENDPOINT, (req, res) => { res.json(data) }) +// create helper middleware so we can reuse server-sent events +const useServerSentEventsMiddleware = (req, res, next) => { + res.setHeader('Content-Type', 'text/event-stream') + res.setHeader('Cache-Control', 'no-cache') + + // only if you want anyone to access this endpoint + res.setHeader('Access-Control-Allow-Origin', '*') + + res.flushHeaders() + + const sendEventStreamData = (data) => { + const sseFormattedResponse = `data: ${JSON.stringify(data)}\n\n` + res.write(sseFormattedResponse) + } + + // we are attaching sendEventStreamData to res, so we can use it later + Object.assign(res, { + sendEventStreamData, + }) + + next() +} + +const streamLog = (req, res) => { + let interval = setInterval(function generateAndSendLog() { + count = count + 1 + + const data = { + time: '000+00:00:00.000', + level: 3, + name: 'system', + message: 'this is message #' + count, + } + + res.sendEventStreamData(data) + }, 1000) + + res.on('close', () => { + clearInterval(interval) + res.end() + }) +} + +// event source, server-sent events SSE +const ES_LOG_ENDPOINT = ES_ENDPOINT_ROOT + 'log' +let count = 0 +server.get(ES_LOG_ENDPOINT, useServerSentEventsMiddleware, streamLog) +server.listen(es_port, () => + console.log( + `Mock EventSource server for EMS-ESP listening at http://localhost:${es_port}`, + ), +) + +// rest API app.listen(port) -console.log(`Mock API Server is up and running at: http://localhost:${port}`) +console.log( + `Mock RESTful API server for EMS-ESP is up and running at http://localhost:${port}`, +) diff --git a/src/emsesp.cpp b/src/emsesp.cpp index 3435b5844..9fda46f59 100644 --- a/src/emsesp.cpp +++ b/src/emsesp.cpp @@ -40,6 +40,7 @@ WebSettingsService EMSESP::webSettingsService = WebSettingsService(&webServer, & WebStatusService EMSESP::webStatusService = WebStatusService(&webServer, EMSESP::esp8266React.getSecurityManager()); WebDevicesService EMSESP::webDevicesService = WebDevicesService(&webServer, EMSESP::esp8266React.getSecurityManager()); WebAPIService EMSESP::webAPIService = WebAPIService(&webServer, EMSESP::esp8266React.getSecurityManager()); +WebLogService EMSESP::webLogService = WebLogService(&webServer, EMSESP::esp8266React.getSecurityManager()); using DeviceFlags = EMSdevice; using DeviceType = EMSdevice::DeviceType; @@ -1209,11 +1210,12 @@ void EMSESP::start() { // main loop calling all services void EMSESP::loop() { - esp8266React.loop(); // web + esp8266React.loop(); // web services system_.loop(); // does LED and checks system health, and syslog service // if we're doing an OTA upload, skip MQTT and EMS if (!system_.upload_status()) { + 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 diff --git a/src/emsesp.h b/src/emsesp.h index 08306d59c..c262663ea 100644 --- a/src/emsesp.h +++ b/src/emsesp.h @@ -35,10 +35,11 @@ #include -#include "WebStatusService.h" -#include "WebDevicesService.h" -#include "WebSettingsService.h" -#include "WebAPIService.h" +#include "web/WebStatusService.h" +#include "web/WebDevicesService.h" +#include "web/WebSettingsService.h" +#include "web/WebAPIService.h" +#include "web/WebLogService.h" #include "emsdevice.h" #include "emsfactory.h" @@ -205,6 +206,7 @@ class EMSESP { static WebStatusService webStatusService; static WebDevicesService webDevicesService; static WebAPIService webAPIService; + static WebLogService webLogService; static uuid::log::Logger logger(); diff --git a/src/system.cpp b/src/system.cpp index 6473b32e0..5368f1cae 100644 --- a/src/system.cpp +++ b/src/system.cpp @@ -413,7 +413,6 @@ void System::loop() { send_heartbeat(); } - /* #ifndef EMSESP_STANDALONE #if defined(EMSESP_DEBUG) static uint32_t last_memcheck_ = 0; @@ -423,7 +422,6 @@ void System::loop() { } #endif #endif -*/ #endif } @@ -849,6 +847,7 @@ bool System::command_settings(const char * value, const int8_t id, JsonObject & EMSESP::webSettingsService.read([&](WebSettings & settings) { node = json.createNestedObject("Settings"); node["tx_mode"] = settings.tx_mode; + node["tx_delay"] = settings.tx_delay; node["ems_bus_id"] = settings.ems_bus_id; node["syslog_enabled"] = settings.syslog_enabled; node["syslog_level"] = settings.syslog_level; diff --git a/src/test/test.cpp b/src/test/test.cpp index bf531746b..c2d35469d 100644 --- a/src/test/test.cpp +++ b/src/test/test.cpp @@ -930,6 +930,7 @@ void Test::run_test(uuid::console::Shell & shell, const std::string & cmd) { } if (command == "api") { +#if defined(EMSESP_STANDALONE) shell.printfln(F("Testing RESTful API...")); Mqtt::ha_enabled(false); run_test("general"); @@ -942,6 +943,7 @@ void Test::run_test(uuid::console::Shell & shell, const std::string & cmd) { request.url("/api/boiler/syspress"); EMSESP::webAPIService.webAPIService_get(&request); +#endif } } diff --git a/src/version.h b/src/version.h index c3735ba6b..05391a6b1 100644 --- a/src/version.h +++ b/src/version.h @@ -1 +1 @@ -#define EMSESP_APP_VERSION "3.1.1b6" +#define EMSESP_APP_VERSION "3.1.1b7" diff --git a/src/WebAPIService.cpp b/src/web/WebAPIService.cpp similarity index 98% rename from src/WebAPIService.cpp rename to src/web/WebAPIService.cpp index 557936849..2cd541b3f 100644 --- a/src/WebAPIService.cpp +++ b/src/web/WebAPIService.cpp @@ -223,11 +223,8 @@ void WebAPIService::send_message_response(AsyncWebServerRequest * request, uint1 } /** - * Extract only the path component from the passed URI - * and normalized it. - * Ex. //one/two////three/// - * becomes - * /one/two/three + * Extract only the path component from the passed URI and normalized it. + * Ex. //one/two////three/// becomes /one/two/three */ std::string SUrlParser::path() { std::string s = "/"; // set up the beginning slash diff --git a/src/WebAPIService.h b/src/web/WebAPIService.h similarity index 100% rename from src/WebAPIService.h rename to src/web/WebAPIService.h diff --git a/src/WebDevicesService.cpp b/src/web/WebDevicesService.cpp similarity index 100% rename from src/WebDevicesService.cpp rename to src/web/WebDevicesService.cpp diff --git a/src/WebDevicesService.h b/src/web/WebDevicesService.h similarity index 99% rename from src/WebDevicesService.h rename to src/web/WebDevicesService.h index 010a09bdc..a5bbe87e7 100644 --- a/src/WebDevicesService.h +++ b/src/web/WebDevicesService.h @@ -29,7 +29,6 @@ #define DEVICE_DATA_SERVICE_PATH "/rest/deviceData" #define WRITE_VALUE_SERVICE_PATH "/rest/writeValue" - namespace emsesp { class WebDevicesService { diff --git a/src/web/WebLogService.cpp b/src/web/WebLogService.cpp new file mode 100644 index 000000000..05675c5c4 --- /dev/null +++ b/src/web/WebLogService.cpp @@ -0,0 +1,171 @@ +/* + * EMS-ESP - https://github.com/emsesp/EMS-ESP + * Copyright 2020 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 + * (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "emsesp.h" + +using namespace std::placeholders; + +namespace emsesp { + +WebLogService::WebLogService(AsyncWebServer * server, SecurityManager * securityManager) + : _events(EVENT_SOURCE_LOG_PATH) + , _setLevel(LOG_SETTINGS_PATH, std::bind(&WebLogService::setLevel, this, _1, _2), 256) { // for POSTS + + _events.setFilter(securityManager->filterRequest(AuthenticationPredicates::IS_ADMIN)); + server->addHandler(&_events); + server->on(EVENT_SOURCE_LOG_PATH, HTTP_GET, std::bind(&WebLogService::forbidden, this, _1)); + + // for bring back the whole log + server->on(FETCH_LOG_PATH, HTTP_GET, std::bind(&WebLogService::fetchLog, this, _1)); + + // get when page is loaded + server->on(LOG_SETTINGS_PATH, HTTP_GET, std::bind(&WebLogService::getLevel, this, _1)); + + // for setting a level + server->addHandler(&_setLevel); + + // start event source service + start(); +} + +void WebLogService::forbidden(AsyncWebServerRequest * request) { + request->send(403); +} + +void WebLogService::start() { + uuid::log::Logger::register_handler(this, uuid::log::Level::INFO); // default is INFO +} + +uuid::log::Level WebLogService::log_level() const { + return uuid::log::Logger::get_log_level(this); +} + +void WebLogService::log_level(uuid::log::Level level) { + uuid::log::Logger::register_handler(this, level); +} + +size_t WebLogService::maximum_log_messages() const { + return maximum_log_messages_; +} + +void WebLogService::maximum_log_messages(size_t count) { + maximum_log_messages_ = std::max((size_t)1, count); + while (log_messages_.size() > maximum_log_messages_) { + log_messages_.pop_front(); + } +} + +WebLogService::QueuedLogMessage::QueuedLogMessage(unsigned long id, std::shared_ptr && content) + : id_(id) + , content_(std::move(content)) { +} + +void WebLogService::operator<<(std::shared_ptr message) { + if (log_messages_.size() >= maximum_log_messages_) { + log_messages_.pop_front(); + } + log_messages_.emplace_back(log_message_id_++, std::move(message)); +} + +void WebLogService::loop() { + if (!_events.count() || log_messages_.empty()) { + return; + } + + // put a small delay in + const uint64_t now = uuid::get_uptime_ms(); + if (now < last_transmit_ || now - last_transmit_ < 100) { + return; + } + + // see if we've advanced + if (log_messages_.back().id_ > log_message_id_tail_) { + transmit(log_messages_.back()); + log_message_id_tail_ = log_messages_.back().id_; + last_transmit_ = uuid::get_uptime_ms(); + } +} + +// send to web eventsource +void WebLogService::transmit(const QueuedLogMessage & message) { + DynamicJsonDocument jsonDocument = DynamicJsonDocument(EMSESP_JSON_SIZE_SMALL); + JsonObject logEvent = jsonDocument.to(); + logEvent["t"] = uuid::log::format_timestamp_ms(message.content_->uptime_ms, 3); + logEvent["l"] = message.content_->level; + logEvent["n"] = message.content_->name; + logEvent["m"] = message.content_->text; + + size_t len = measureJson(jsonDocument); + char * buffer = new char[len + 1]; + if (buffer) { + serializeJson(jsonDocument, buffer, len + 1); + _events.send(buffer, "message", millis()); + } + delete[] buffer; +} + +// send the current log buffer to the API +void WebLogService::fetchLog(AsyncWebServerRequest * request) { + MsgpackAsyncJsonResponse * response = new MsgpackAsyncJsonResponse(false, EMSESP_JSON_SIZE_XXLARGE_DYN); // 8kb buffer + JsonObject root = response->getRoot(); + + JsonArray log = root.createNestedArray("events"); + + for (const auto & msg : log_messages_) { + JsonObject logEvent = log.createNestedObject(); + auto message = std::move(msg); + logEvent["t"] = uuid::log::format_timestamp_ms(message.content_->uptime_ms, 3); + logEvent["l"] = message.content_->level; + logEvent["n"] = message.content_->name; + logEvent["m"] = message.content_->text; + } + + log_message_id_tail_ = log_messages_.back().id_; + + response->setLength(); + request->send(response); +} + +// sets the level after a POST +void WebLogService::setLevel(AsyncWebServerRequest * request, JsonVariant & json) { + if (not json.is()) { + return; + } + auto && body = json.as(); + uuid::log::Level level = body["level"]; + log_level(level); + + // send the value back + AsyncJsonResponse * response = new AsyncJsonResponse(false, EMSESP_JSON_SIZE_SMALL); + JsonObject root = response->getRoot(); + root["level"] = log_level(); + response->setLength(); + request->send(response); +} + +// return the current log level after a GET +void WebLogService::getLevel(AsyncWebServerRequest * request) { + AsyncJsonResponse * response = new AsyncJsonResponse(false, EMSESP_JSON_SIZE_SMALL); + JsonObject root = response->getRoot(); + root["level"] = log_level(); + root["max_messages"] = maximum_log_messages(); + response->setLength(); + request->send(response); +} + +} // namespace emsesp diff --git a/src/web/WebLogService.h b/src/web/WebLogService.h new file mode 100644 index 000000000..12bf8afe6 --- /dev/null +++ b/src/web/WebLogService.h @@ -0,0 +1,80 @@ +/* + * EMS-ESP - https://github.com/emsesp/EMS-ESP + * Copyright 2020 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 + * (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef WebLogService_h +#define WebLogService_h + +#include +#include +#include +#include + +#include + +#define EVENT_SOURCE_LOG_PATH "/es/log" +#define FETCH_LOG_PATH "/rest/fetchLog" +#define LOG_SETTINGS_PATH "/rest/logSettings" + +namespace emsesp { + +class WebLogService : public uuid::log::Handler { + public: + static constexpr size_t MAX_LOG_MESSAGES = 30; + + WebLogService(AsyncWebServer * server, SecurityManager * securityManager); + + void start(); + uuid::log::Level log_level() const; + void log_level(uuid::log::Level level); + size_t maximum_log_messages() const; + void maximum_log_messages(size_t count); + void loop(); + + virtual void operator<<(std::shared_ptr message); + + private: + AsyncEventSource _events; + + class QueuedLogMessage { + public: + QueuedLogMessage(unsigned long id, std::shared_ptr && content); + ~QueuedLogMessage() = default; + + unsigned long id_; // Sequential identifier for this log message + struct timeval time_; // Time message was received + const std::shared_ptr content_; // Log message content + }; + + void forbidden(AsyncWebServerRequest * request); + void transmit(const QueuedLogMessage & message); + void fetchLog(AsyncWebServerRequest * request); + void getLevel(AsyncWebServerRequest * request); + + void setLevel(AsyncWebServerRequest * request, JsonVariant & json); + AsyncCallbackJsonWebHandler _setLevel; // for POSTs + + uint64_t last_transmit_ = 0; // Last transmit time + size_t maximum_log_messages_ = MAX_LOG_MESSAGES; // Maximum number of log messages to buffer before they are output + unsigned long log_message_id_ = 0; // The next identifier to use for queued log messages + unsigned long log_message_id_tail_ = 0; // last event shown on the screen after fetch + std::list log_messages_; // Queued log messages, in the order they were received +}; + +} // namespace emsesp + +#endif diff --git a/src/WebSettingsService.cpp b/src/web/WebSettingsService.cpp similarity index 100% rename from src/WebSettingsService.cpp rename to src/web/WebSettingsService.cpp diff --git a/src/WebSettingsService.h b/src/web/WebSettingsService.h similarity index 98% rename from src/WebSettingsService.h rename to src/web/WebSettingsService.h index e90b30b6b..dccad6961 100644 --- a/src/WebSettingsService.h +++ b/src/web/WebSettingsService.h @@ -22,7 +22,7 @@ #include #include -#include "default_settings.h" +#include "../default_settings.h" #define EMSESP_SETTINGS_FILE "/config/emsespSettings.json" #define EMSESP_SETTINGS_SERVICE_PATH "/rest/emsespSettings" diff --git a/src/WebStatusService.cpp b/src/web/WebStatusService.cpp similarity index 100% rename from src/WebStatusService.cpp rename to src/web/WebStatusService.cpp diff --git a/src/WebStatusService.h b/src/web/WebStatusService.h similarity index 100% rename from src/WebStatusService.h rename to src/web/WebStatusService.h