Merge remote-tracking branch 'origin/ft_webui_log' into dev

This commit is contained in:
proddy
2021-06-18 20:38:28 +02:00
39 changed files with 880 additions and 628 deletions

View File

@@ -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",

View File

@@ -1,5 +1,5 @@
{
"name": "esp8266-react",
"name": "emsesp-react",
"version": "0.1.0",
"private": true,
"dependencies": {

View File

@@ -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;

View File

@@ -15,13 +15,14 @@ const useStyles = makeStyles((theme: Theme) =>
interface SectionContentProps {
title: string;
titleGutter?: boolean;
id?: string;
}
const SectionContent: React.FC<SectionContentProps> = (props) => {
const { children, title, titleGutter } = props;
const { children, title, titleGutter, id } = props;
const classes = useStyles();
return (
<Paper className={classes.content}>
<Paper id={id} className={classes.content}>
<Typography variant="h6" gutterBottom={titleGutter}>
{title}
</Typography>

View File

@@ -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;
}

View File

@@ -15,3 +15,5 @@ export * from './RestController';
export * from './WebSocketFormLoader';
export * from './WebSocketController';
export * from './WindowSize';

View File

@@ -24,7 +24,10 @@ class EMSESP extends Component<RouteComponentProps> {
onChange={(e, path) => this.handleTabChange(path)}
variant="fullWidth"
>
<Tab value={`/${PROJECT_PATH}/devices`} label="Devices & Sensors" />
<Tab
value={`/${PROJECT_PATH}/devices`}
label="Devices &amp; Sensors"
/>
<Tab value={`/${PROJECT_PATH}/status`} label="EMS Status" />
<Tab value={`/${PROJECT_PATH}/help`} label="EMS-ESP Help" />
</Tabs>

View File

@@ -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<EMSESPDevicesControllerProps> {
render() {
return (
<SectionContent title="Devices & Sensors">
<SectionContent title="Devices &amp; Sensors">
<RestFormLoader
{...this.props}
render={(formProps) => <EMSESPDevicesForm {...formProps} />}

View File

@@ -36,6 +36,7 @@ import {
withAuthenticatedContext,
AuthenticatedContextProps
} from '../authentication';
import { RestFormProps, FormButton, extractEventValue } from '../components';
import {

View File

@@ -23,14 +23,12 @@ class EMSESPSettingsController extends Component<EMSESPSettingsControllerProps>
render() {
return (
// <Container maxWidth="md" disableGutters>
<SectionContent title="" titleGutter>
<RestFormLoader
{...this.props}
render={(formProps) => <EMSESPSettingsForm {...formProps} />}
/>
</SectionContent>
// </Container>
);
}
}

View File

@@ -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<EMSESPSettingsFormProps> {
class EMSESPSettingsForm extends Component<EMSESPSettingsFormProps> {
state: EMSESPSettingsFormState = {
processing: false
};

View File

@@ -21,7 +21,7 @@ class EMSESPStatusController extends Component<EMSESPStatusControllerProps> {
render() {
return (
<SectionContent title="EMS Status">
<SectionContent title="EMS Status" titleGutter>
<RestFormLoader
{...this.props}
render={(formProps) => <EMSESPStatusForm {...formProps} />}

View File

@@ -9,4 +9,13 @@ module.exports = function (app) {
changeOrigin: true
})
);
app.use(
'/es/*',
createProxyMiddleware({
target: 'http://localhost:3090',
secure: false,
changeOrigin: true
})
);
};

View File

@@ -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<LogEventConsoleProps> = (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 (
<Box id="log-window" className={classes.console}>
{events.map((e) => (
<div className={classes.entry}>
<span>{e.t}</span>
<span className={styleLevel(e.l)}>{paddedLevelLabel(e.l)} </span>
<span>{paddedNameLabel(e.n)} </span>
<span>{e.m}</span>
</div>
))}
</Box>
);
};
export default LogEventConsole;

View File

@@ -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<LogSettings>;
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 (
<SectionContent title="System Log" id="log-window">
<RestFormLoader
{...this.props}
render={(formProps) => <LogEventForm {...formProps} />}
/>
<LogEventConsole events={this.state.events} />
</SectionContent>
);
}
}
export default restController(LOG_SETTINGS_ENDPOINT, LogEventController);

View File

@@ -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<LogSettings>;
class LogEventForm extends Component<LogEventFormProps> {
changeLevel = (event: React.ChangeEvent<HTMLSelectElement>) => {
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 (
<ValidatorForm onSubmit={saveData}>
<Grid
container
spacing={0}
direction="row"
justify="flex-start"
alignItems="center"
>
<Grid item xs={2}>
<SelectValidator
name="level"
label="Log Level"
value={data.level}
variant="outlined"
onChange={this.changeLevel}
margin="normal"
>
<MenuItem value={-1}>OFF</MenuItem>
<MenuItem value={3}>ERROR</MenuItem>
<MenuItem value={4}>WARNING</MenuItem>
<MenuItem value={5}>NOTICE</MenuItem>
<MenuItem value={6}>INFO</MenuItem>
<MenuItem value={7}>DEBUG</MenuItem>
<MenuItem value={8}>TRACE</MenuItem>
</SelectValidator>
</Grid>
<Grid item md>
<Typography color="primary" variant="body2">
<i>
(the last {data.max_messages} messages are buffered and new log
events are shown in real time)
</i>
</Typography>
</Grid>
</Grid>
</ValidatorForm>
);
}
}
export default withAuthenticatedContext(LogEventForm);

View File

@@ -1,4 +1,4 @@
import React, { Component } from 'react';
import { Component } from 'react';
import {
restController,

View File

@@ -11,6 +11,7 @@ import {
FormButton,
FormActions
} from '../components';
import { isIP, isHostname, or } from '../validators';
import { OTASettings } from './types';

View File

@@ -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<SystemProps> {
variant="fullWidth"
>
<Tab value="/system/status" label="System Status" />
<Tab value="/system/log" label="System Log" />
{features.ota && (
<Tab
value="/system/ota"
@@ -56,6 +58,11 @@ class System extends Component<SystemProps> {
path="/system/status"
component={SystemStatusController}
/>
<AuthenticatedRoute
exact
path="/system/log"
component={LogEventController}
/>
{features.ota && (
<AuthenticatedRoute
exact

View File

@@ -36,3 +36,24 @@ export interface OTASettings {
port: number;
password: string;
}
export enum LogLevel {
ERROR = 3,
WARNING = 4,
NOTICE = 5,
INFO = 6,
DEBUG = 7,
TRACE = 8
}
export interface LogEvent {
t: string;
l: LogLevel;
n: string;
m: string;
}
export interface LogSettings {
level: LogLevel;
max_messages: number;
}

View File

@@ -13,6 +13,7 @@ class AsyncWebServerResponse;
class AsyncJsonResponse;
class PrettyAsyncJsonResponse;
class MsgpackAsyncJsonResponse;
class AsyncEventSource;
class AsyncWebParameter {
private:
@@ -197,6 +198,10 @@ class AsyncWebHandler {
virtual bool isRequestHandlerTrivial() {
return true;
}
AsyncWebHandler & setFilter(ArRequestFilterFunction fn) {
return *this;
}
};
class AsyncWebServerResponse {
@@ -230,4 +235,17 @@ class AsyncWebServer {
};
class AsyncEventSource : public AsyncWebHandler {
public:
AsyncEventSource(const String & url){};
~AsyncEventSource(){};
size_t count() const {
return 1;
}
void send(const char * message, const char * event = NULL, uint32_t id = 0, uint32_t reconnect = 0){};
};
#endif

View File

@@ -17,8 +17,8 @@ MAKEFLAGS+="j "
#TARGET := $(notdir $(CURDIR))
TARGET := emsesp
BUILD := build
SOURCES := src lib_standalone lib/uuid-common/src lib/uuid-console/src lib/uuid-log/src src/devices lib/ArduinoJson/src lib/PButton src/test
INCLUDES := lib/ArduinoJson/src lib_standalone lib/uuid-common/src lib/uuid-console/src lib/uuid-log/src lib/uuid-telnet/src lib/uuid-syslog/src lib/PButton src/devices lib src
SOURCES := src src/* lib_standalone lib/uuid-common/src lib/uuid-console/src lib/uuid-log/src src/devices lib/ArduinoJson/src lib/PButton
INCLUDES := src lib_standalone lib/ArduinoJson/src lib/uuid-common/src lib/uuid-console/src lib/uuid-log/src lib/uuid-telnet/src lib/uuid-syslog/src lib/* src/devices
LIBRARIES :=
CPPCHECK = cppcheck

View File

@@ -1,4 +1,3 @@
(<https://github.com/emsesp/EMS-ESP32/issues/41>)
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(<https://medium.com/webpack/webpack-dev-server-middleware-security-issues-1489d950874a>)

View File

@@ -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}`,
)

View File

@@ -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

View File

@@ -35,10 +35,11 @@
#include <ESP8266React.h>
#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();

View File

@@ -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;

View File

@@ -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
}
}

View File

@@ -1 +1 @@
#define EMSESP_APP_VERSION "3.1.1b6"
#define EMSESP_APP_VERSION "3.1.1b7"

View File

@@ -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

View File

@@ -29,7 +29,6 @@
#define DEVICE_DATA_SERVICE_PATH "/rest/deviceData"
#define WRITE_VALUE_SERVICE_PATH "/rest/writeValue"
namespace emsesp {
class WebDevicesService {

171
src/web/WebLogService.cpp Normal file
View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
#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<uuid::log::Message> && content)
: id_(id)
, content_(std::move(content)) {
}
void WebLogService::operator<<(std::shared_ptr<uuid::log::Message> 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<JsonObject>();
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<JsonObject>()) {
return;
}
auto && body = json.as<JsonObject>();
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

80
src/web/WebLogService.h Normal file
View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
#ifndef WebLogService_h
#define WebLogService_h
#include <ArduinoJson.h>
#include <AsyncJson.h>
#include <ESPAsyncWebServer.h>
#include <SecurityManager.h>
#include <uuid/log.h>
#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<uuid::log::Message> message);
private:
AsyncEventSource _events;
class QueuedLogMessage {
public:
QueuedLogMessage(unsigned long id, std::shared_ptr<uuid::log::Message> && content);
~QueuedLogMessage() = default;
unsigned long id_; // Sequential identifier for this log message
struct timeval time_; // Time message was received
const std::shared_ptr<const uuid::log::Message> 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<QueuedLogMessage> log_messages_; // Queued log messages, in the order they were received
};
} // namespace emsesp
#endif

View File

@@ -22,7 +22,7 @@
#include <HttpEndpoint.h>
#include <FSPersistence.h>
#include "default_settings.h"
#include "../default_settings.h"
#define EMSESP_SETTINGS_FILE "/config/emsespSettings.json"
#define EMSESP_SETTINGS_SERVICE_PATH "/rest/emsespSettings"