diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index d4dce094f..4b4d06884 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -55,7 +55,8 @@ further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported by contacting the project team at leadership@OpenEnergyDashboard.org. All +reported by contacting the project team via the [Contact page](https://openenergydashboard.org/contact/), +using the contact form and selecting the leadership topic/area. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. @@ -63,10 +64,10 @@ Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other -members of the project's leadership. If your concern revolves around a project -maintainer or you have concerns contacting us via the leadership email then you -can directly contact the main project coordinator at -StevenHussLederman@OpenEnergyDashboard.org. If the issue involves the main project +members of the project's leadership. If your concern revolves around a project +maintainer or you have concerns contacting us via the contact form then you +can directly contact the main project coordinator via Discord (@SteveOED). +If the issue involves the main project coordinator then please contact other project leadership or other project members. ## Attribution diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 51b81b51d..b452c585a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,15 +4,15 @@ We're thrilled that you're interested in contributing to the OED project. We welcome all open source contributions, whether bug reports, patches, or anything else. -Check out the [developer getting started](https://openenergydashboard.github.io/developer/gettingStarted.html) page to learn how you can set up OED on your +Check out the [developer first steps](https://openenergydashboard.org/developer/gettingStarted/) page to learn how you can set up OED on your own system. ## Legal Stuff -Please sign our [Contributor License Agreement](https://openenergydashboard.github.io/developer/cla.html) before +Please sign our [Contributor License Agreement](https://openenergydashboard.org/developer/cla/) before contributing code to OED. It is not necessary to sign the CLA in order to contribute by filing or discussing issues. ## Working as a Developer on OED -Web pages with [information for developers](https://openenergydashboard.github.io/developer/) is available to explain how to develop and contribute code to OED. +Web pages with [information for developers](https://openenergydashboard.org/developer/developer/) is available to explain how to develop and contribute code to OED. diff --git a/README.md b/README.md index 19d421e2e..31e7e57ce 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ![Github Build](https://github.com/OpenEnergyDashboard/OED/workflows/Build/badge.svg) -Open Energy Dashboard is a user-friendly way to display energy information from smart energy meters or uploaded via CSV files. It is available to anyone and is optimized for non-technical users with a simple interface that provides access to your organization's energy data. To learn more, see [our website](https://openenergydashboard.github.io/). +Open Energy Dashboard is a user-friendly way to display energy information from smart energy meters or uploaded via CSV files. It is available to anyone and is optimized for non-technical users with a simple interface that provides access to your organization's energy data. To learn more, see [our website](https://openenergydashboard.org/). Open Energy Dashboard is available under the Mozilla Public License v2, and contributions, in the form of bug reports, feature requests, and code contributions, are welcome. @@ -30,11 +30,11 @@ For a list of contributors, [click here](https://github.com/OpenEnergyDashboard/ This project is licensed under the MPL version 2.0. -See the full licensing agreement [here](LICENSE.txt) +See the full licensing agreement [here](License.txt) ## Contributions ## -We welcome others to contribute to this project by writing code for submission or collaborating with us. Before contributing, please sign our [Contributor License Agreement](https://openenergydashboard.github.io/developer/cla.html). Web pages with [information for developers](https://openenergydashboard.github.io/developer/) is available. If you have any questions or concerns feel free to at engage@OpenEnergyDashboard.org. +We welcome others to contribute to this project by writing code for submission or collaborating with us. Before contributing, please sign our [Contributor License Agreement](https://openenergydashboard.org/developer/cla/). Web pages with [information for developers](https://openenergydashboard.org/developer/developer/) is available. If you have any questions or concerns feel free to [contact us](https://OpenEnergyDashboard.org/contact/). ## Code of Conduct ## @@ -53,4 +53,4 @@ If you think there is a security concern in the OED software, the please visit o ## Contact ## -To contact us, see our [contact web page](https://openenergydashboard.github.io/contact.html), send an email to info@OpenEnergyDashboard.org or open an issue on GitHub. +To contact us, see our [contact web page](https://openenergydashboard.org/contact/) or open an issue on GitHub. diff --git a/SECURITY.md b/SECURITY.md index 1de3e1d8f..00ac40717 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -2,7 +2,7 @@ OED welcomes input at any time. Having said that, security issues can be special as they could represent a risk to those using the OED software. Given this, OED suggests these avenues for reporting a security concern: -1. If the concern may represent a real risk to sites, you should report it to [security@OpenEnergyDashboard.org](mailto:security@OpenEnergyDashboard.org). This email is regularly monitored and you should receive a response indicating your concern has been received by the project. Please provide the type of information that would be in an issue including what you are concerned about, where it is located in the code base (if possible), any links to security notices about this issue and any thoughts on how to address the issue. Additionally, you are welcome to indicate you would like us to publicly acknowledge you when an issue and/or pull request is created. If you would like this then please also provide your GitHub ID if you have one. We may delay doing this until a security patch is available for sites to install. +1. If the concern may represent a real risk to sites, you should report it via our [contact page](https://OpenEnergyDashboard.org/contact/), click to get to the "Contact Form" and select "Security" as the topic/area. Submissions are regularly monitored and you should receive a response indicating your concern has been received by the project (if you provide contact information). Please provide the type of information that would be in an issue including what you are concerned about, where it is located in the code base (if possible), any links to security notices about this issue and any thoughts on how to address the issue. Additionally, you are welcome to indicate you would like us to publicly acknowledge you when an issue and/or pull request is created. If you would like this then please also provide your GitHub ID if you have one. We may delay doing this until a security patch is available for sites to install. 2. You can open an [issue](https://github.com/OpenEnergyDashboard/OED/issues) on our GitHub repository. The same information as requested in the previous item is appreciated. The major difference is that issues are usually public so anyone (including bad actors) can see the issue before the OED project can act. 3. You can create a draft security advisory by using the "Report a vulnerability" as the issue type on the OED [issues](https://github.com/OpenEnergyDashboard/OED/issues) on our GitHub repository. Since OED is not a package included in other software, this may be less applicable but still available. These are not public unless the project chooses to make them so. diff --git a/SUPPORT.md b/SUPPORT.md index eaa632759..72ad8c484 100644 --- a/SUPPORT.md +++ b/SUPPORT.md @@ -1,3 +1,3 @@ # OED Support -OED maintains a [website](https://openenergydashboard.github.io/), [help ages](https://openenergydashboard.github.io/help/index.html) and [contact info](https://openenergydashboard.github.io/contact.html) to support you in interacting with the project. We welcome your input, thoughts, ideas, questions and requests for help. +OED maintains a [website](https://openenergydashboard.org/), [help pages](https://openenergydashboard.org/helpV1_0_0/user/) and [contact info](https://openenergydashboard.org/contact/) to support you in interacting with the project. We welcome your input, thoughts, ideas, questions and requests for help. diff --git a/USAGE.md b/USAGE.md index cace7e26d..d7f1aa238 100644 --- a/USAGE.md +++ b/USAGE.md @@ -2,8 +2,8 @@ ## Developers -The [developer website](https://openenergydashboard.github.io/developer/) is available to learn about working with the OED project. +The [developer website](https://openenergydashboard.org/developer/developer/) is available to learn about working with the OED project. ## Production/Sites -The [admin installation page](https://OpenEnergyDashboard.github.io/help/v0.7.0/adminInstallation.html) is available to learn about installing OED for a production environment (normal OED site). These are part of the overall admin help pages for OED. +The [admin documentation page](https://openenergydashboard.org/helpV1_0_0/admin/) has a section about "Site Installation" to learn about installing OED for a production environment (normal OED site). These are part of the overall admin help pages for OED. diff --git a/package-lock.json b/package-lock.json index c39434628..68bc1cd28 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "bootstrap": "~5.3.3", "csv": "~5.3.2", "csv-stringify": "~5.6.5", + "d3": "~7.8.5", "dotenv": "~16.4.5", "escape-html": "~1.0.3", "express": "~4.19.2", @@ -57,6 +58,7 @@ }, "devDependencies": { "@redux-devtools/extension": "~3.2.5", + "@types/d3": "~7.4.3", "@types/lodash": "~4.17.4", "@types/node": "~20.14.10", "@types/plotly.js": "~2.29.2", @@ -976,6 +978,259 @@ "integrity": "sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==", "dev": true }, + "node_modules/@types/d3": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", + "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==", + "dev": true, + "dependencies": { + "@types/d3-array": "*", + "@types/d3-axis": "*", + "@types/d3-brush": "*", + "@types/d3-chord": "*", + "@types/d3-color": "*", + "@types/d3-contour": "*", + "@types/d3-delaunay": "*", + "@types/d3-dispatch": "*", + "@types/d3-drag": "*", + "@types/d3-dsv": "*", + "@types/d3-ease": "*", + "@types/d3-fetch": "*", + "@types/d3-force": "*", + "@types/d3-format": "*", + "@types/d3-geo": "*", + "@types/d3-hierarchy": "*", + "@types/d3-interpolate": "*", + "@types/d3-path": "*", + "@types/d3-polygon": "*", + "@types/d3-quadtree": "*", + "@types/d3-random": "*", + "@types/d3-scale": "*", + "@types/d3-scale-chromatic": "*", + "@types/d3-selection": "*", + "@types/d3-shape": "*", + "@types/d3-time": "*", + "@types/d3-time-format": "*", + "@types/d3-timer": "*", + "@types/d3-transition": "*", + "@types/d3-zoom": "*" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz", + "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==", + "dev": true + }, + "node_modules/@types/d3-axis": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz", + "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==", + "dev": true, + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-brush": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz", + "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==", + "dev": true, + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-chord": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz", + "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==", + "dev": true + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "dev": true + }, + "node_modules/@types/d3-contour": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz", + "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==", + "dev": true, + "dependencies": { + "@types/d3-array": "*", + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==", + "dev": true + }, + "node_modules/@types/d3-dispatch": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.6.tgz", + "integrity": "sha512-4fvZhzMeeuBJYZXRXrRIQnvUYfyXwYmLsdiN7XXmVNQKKw1cM8a5WdID0g1hVFZDqT9ZqZEY5pD44p24VS7iZQ==", + "dev": true + }, + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", + "dev": true, + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-dsv": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", + "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==", + "dev": true + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "dev": true + }, + "node_modules/@types/d3-fetch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz", + "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==", + "dev": true, + "dependencies": { + "@types/d3-dsv": "*" + } + }, + "node_modules/@types/d3-force": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz", + "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==", + "dev": true + }, + "node_modules/@types/d3-format": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz", + "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==", + "dev": true + }, + "node_modules/@types/d3-geo": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz", + "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", + "dev": true, + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-hierarchy": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", + "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==", + "dev": true + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "dev": true, + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-P2dlU/q51fkOc/Gfl3Ul9kicV7l+ra934qBFXCFhrZMOL6du1TM0pm1ThYvENukyOn5h9v+yMJ9Fn5JK4QozrQ==", + "dev": true + }, + "node_modules/@types/d3-polygon": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz", + "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==", + "dev": true + }, + "node_modules/@types/d3-quadtree": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", + "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==", + "dev": true + }, + "node_modules/@types/d3-random": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz", + "integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==", + "dev": true + }, + "node_modules/@types/d3-scale": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.8.tgz", + "integrity": "sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ==", + "dev": true, + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-scale-chromatic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.0.3.tgz", + "integrity": "sha512-laXM4+1o5ImZv3RpFAsTRn3TEkzqkytiOY0Dz0sq5cnd1dtNlk6sHLon4OvqaiJb28T0S/TdsBI3Sjsy+keJrw==", + "dev": true + }, + "node_modules/@types/d3-selection": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.10.tgz", + "integrity": "sha512-cuHoUgS/V3hLdjJOLTT691+G2QoqAjCVLmr4kJXR4ha56w1Zdu8UUQ5TxLRqudgNjwXeQxKMq4j+lyf9sWuslg==", + "dev": true + }, + "node_modules/@types/d3-shape": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.6.tgz", + "integrity": "sha512-5KKk5aKGu2I+O6SONMYSNflgiP0WfZIQvVUMan50wHsLG1G94JlxEVnCpQARfTtzytuY0p/9PXXZb3I7giofIA==", + "dev": true, + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.3.tgz", + "integrity": "sha512-2p6olUZ4w3s+07q3Tm2dbiMZy5pCDfYwtLXXHUnVzXgQlZ/OyPtUz6OL382BkOuGlLXqfT+wqv8Fw2v8/0geBw==", + "dev": true + }, + "node_modules/@types/d3-time-format": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", + "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==", + "dev": true + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "dev": true + }, + "node_modules/@types/d3-transition": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.8.tgz", + "integrity": "sha512-ew63aJfQ/ms7QQ4X7pk5NxQ9fZH/z+i24ZfJ6tJSfqxJMrYLiK01EAs2/Rtw/JreGUsS3pLPNV644qXFGnoZNQ==", + "dev": true, + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", + "dev": true, + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, "node_modules/@types/eslint": { "version": "8.44.7", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.44.7.tgz", @@ -1002,6 +1257,12 @@ "integrity": "sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==", "dev": true }, + "node_modules/@types/geojson": { + "version": "7946.0.14", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.14.tgz", + "integrity": "sha512-WCfD5Ht3ZesJUsONdhvm84dmzWOiOzOAqOncN0++w0lBw1o8OuDNJF2McvvCef/yBqb/HYRahp1BYtODFQ8bRg==", + "dev": true + }, "node_modules/@types/hoist-non-react-statics": { "version": "3.3.5", "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.5.tgz", @@ -3010,11 +3271,85 @@ "type": "^1.0.1" } }, + "node_modules/d3": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/d3/-/d3-7.8.5.tgz", + "integrity": "sha512-JgoahDG51ncUfJu6wX/1vWQEqOflgXyl4MaHqlcSruTez7yhaRKR9i8VjjcQGeS2en/jnFivXuaIMnseMMt0XA==", + "dependencies": { + "d3-array": "3", + "d3-axis": "3", + "d3-brush": "3", + "d3-chord": "3", + "d3-color": "3", + "d3-contour": "4", + "d3-delaunay": "6", + "d3-dispatch": "3", + "d3-drag": "3", + "d3-dsv": "3", + "d3-ease": "3", + "d3-fetch": "3", + "d3-force": "3", + "d3-format": "3", + "d3-geo": "3", + "d3-hierarchy": "3", + "d3-interpolate": "3", + "d3-path": "3", + "d3-polygon": "3", + "d3-quadtree": "3", + "d3-random": "3", + "d3-scale": "4", + "d3-scale-chromatic": "3", + "d3-selection": "3", + "d3-shape": "3", + "d3-time": "3", + "d3-time-format": "4", + "d3-timer": "3", + "d3-transition": "3", + "d3-zoom": "3" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/d3-array": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-1.2.4.tgz", "integrity": "sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw==" }, + "node_modules/d3-axis": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-brush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-chord": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", + "dependencies": { + "d3-path": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/d3-collection": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/d3-collection/-/d3-collection-1.0.7.tgz", @@ -3028,11 +3363,118 @@ "node": ">=12" } }, + "node_modules/d3-contour": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", + "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", + "dependencies": { + "d3-array": "^3.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-contour/node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "dependencies": { + "delaunator": "5" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/d3-dispatch": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-1.0.6.tgz", "integrity": "sha512-fVjoElzjhCEy+Hbn8KygnmMS7Or0a9sI2UzGwoB7cCtvI1XpVN9GpoYlnb3xt2YV66oXYb1fLJ8GMvP4hdU1RA==" }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "dependencies": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "bin": { + "csv2json": "bin/dsv2json.js", + "csv2tsv": "bin/dsv2dsv.js", + "dsv2dsv": "bin/dsv2dsv.js", + "dsv2json": "bin/dsv2json.js", + "json2csv": "bin/json2dsv.js", + "json2dsv": "bin/json2dsv.js", + "json2tsv": "bin/json2dsv.js", + "tsv2csv": "bin/dsv2dsv.js", + "tsv2json": "bin/dsv2json.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "engines": { + "node": ">= 10" + } + }, + "node_modules/d3-dsv/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-fetch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", + "dependencies": { + "d3-dsv": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/d3-force": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-1.2.1.tgz", @@ -3096,11 +3538,84 @@ "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz", "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==" }, + "node_modules/d3-polygon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", + "engines": { + "node": ">=12" + } + }, "node_modules/d3-quadtree": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-1.0.7.tgz", "integrity": "sha512-RKPAeXnkC59IDGD0Wu5mANy0Q2V28L+fNe65pOCXVdVuTJS3WPKaJlFHer32Rbh9gIo9qMuJXio8ra4+YmIymA==" }, + "node_modules/d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", + "dependencies": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale/node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale/node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "engines": { + "node": ">=12" + } + }, "node_modules/d3-shape": { "version": "1.3.7", "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz", @@ -3127,6 +3642,155 @@ "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-1.0.10.tgz", "integrity": "sha512-B1JDm0XDaQC+uvo4DT79H0XmBskgS3l6Ve+1SBCfxgmtIb1AVrPIoqd+nPSv+loMX8szQ0sVUhGngL7D5QPiXw==" }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3/node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3/node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3/node_modules/d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3/node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3/node_modules/d3-geo": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz", + "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", + "dependencies": { + "d3-array": "2.5.0 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3/node_modules/d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3/node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3/node_modules/d3-quadtree": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3/node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3/node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3/node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3/node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "engines": { + "node": ">=12" + } + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -3222,6 +3886,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delaunator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", + "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -5226,6 +5898,14 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "engines": { + "node": ">=12" + } + }, "node_modules/interpret": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", @@ -8321,6 +9001,11 @@ "inherits": "^2.0.1" } }, + "node_modules/robust-predicates": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", + "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==" + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", diff --git a/package.json b/package.json index f9a48c1bb..fa5f6d8de 100644 --- a/package.json +++ b/package.json @@ -64,6 +64,7 @@ "bootstrap": "~5.3.3", "csv": "~5.3.2", "csv-stringify": "~5.6.5", + "d3": "~7.8.5", "dotenv": "~16.4.5", "escape-html": "~1.0.3", "express": "~4.19.2", @@ -104,6 +105,7 @@ }, "devDependencies": { "@redux-devtools/extension": "~3.2.5", + "@types/d3": "~7.4.3", "@types/lodash": "~4.17.4", "@types/node": "~20.14.10", "@types/plotly.js": "~2.29.2", diff --git a/pull_request_template.md b/pull_request_template.md index a9e491d1a..982321649 100644 --- a/pull_request_template.md +++ b/pull_request_template.md @@ -17,9 +17,9 @@ Fixes #[issue] (Note what you have done by placing an "x" instead of the space in the [ ] so it becomes [x]. It is hoped you do all of them.) -- [ ] I have followed the [OED pull request](https://openenergydashboard.github.io/developer/pr.html) ideas +- [ ] I have followed the [OED pull request](https://openenergydashboard.org/developer/pr/) ideas - [ ] I have removed text in ( ) from the issue request -- [ ] You acknowledge that every person contributing to this work has signed the [OED Contributing License Agreement](https://openenergydashboard.github.io/developer/cla.html) and each author is listed in the Description section. +- [ ] You acknowledge that every person contributing to this work has signed the [OED Contributing License Agreement](https://openenergydashboard.org/developer/cla/) and each author is listed in the Description section. ## Limitations diff --git a/src/client/app/components/AreaUnitSelectComponent.tsx b/src/client/app/components/AreaUnitSelectComponent.tsx index a42850e1e..b3fe612f3 100644 --- a/src/client/app/components/AreaUnitSelectComponent.tsx +++ b/src/client/app/components/AreaUnitSelectComponent.tsx @@ -11,7 +11,7 @@ import { selectUnitDataById } from '../redux/api/unitsApi'; import { StringSelectOption } from '../types/items'; import { UnitRepresentType } from '../types/redux/units'; import { AreaUnitType } from '../utils/getAreaUnitConversion'; -import translate from '../utils/translate'; +import { useTranslate } from '../redux/componentHooks'; import TooltipMarkerComponent from './TooltipMarkerComponent'; /** @@ -19,8 +19,8 @@ import TooltipMarkerComponent from './TooltipMarkerComponent'; * @returns Area unit select element */ export default function AreaUnitSelectComponent() { + const translate = useTranslate(); const dispatch = useAppDispatch(); - const graphState = useAppSelector(selectGraphState); const unitDataById = useAppSelector(selectUnitDataById); diff --git a/src/client/app/components/BarChartComponent.tsx b/src/client/app/components/BarChartComponent.tsx index 22d323fae..680eec4bc 100644 --- a/src/client/app/components/BarChartComponent.tsx +++ b/src/client/app/components/BarChartComponent.tsx @@ -19,7 +19,7 @@ import { selectSelectedLanguage } from '../redux/slices/appStateSlice'; import { selectBarStacking } from '../redux/slices/graphSlice'; import Locales from '../types/locales'; import SpinnerComponent from './SpinnerComponent'; -import { useTranslate } from 'redux/componentHooks'; +import { useTranslate } from '../redux/componentHooks'; /** * Passes the current redux state of the barchart, and turns it into props for the React @@ -28,6 +28,7 @@ import { useTranslate } from 'redux/componentHooks'; * @returns Plotly BarChart */ export default function BarChartComponent() { + const translate = useTranslate(); const dispatch = useAppDispatch(); const { barMeterDeps, barGroupDeps } = useAppSelector(selectPlotlyBarDeps); const { meterArgs, groupArgs, meterShouldSkip, groupShouldSkip } = useAppSelector(selectBarChartQueryArgs); @@ -64,8 +65,6 @@ export default function BarChartComponent() { // useQueryHooks for data fetching const datasets: Partial[] = meterReadings.concat(groupData); - const translate = useTranslate(); - if (meterIsFetching || groupIsFetching) { return ; } @@ -91,6 +90,7 @@ export default function BarChartComponent() { data={datasets} style={{ width: '100%', height: '100%', minHeight: '700px' }} layout={{ + margin: { t: 0, b: 0, r: 3 }, // Eliminate top, bottom, and right margins barmode: (barStacking ? 'stack' : 'group'), bargap: 0.2, // Gap between different times of readings bargroupgap: 0.1, // Gap between different meter's readings under the same timestamp diff --git a/src/client/app/components/BarControlsComponent.tsx b/src/client/app/components/BarControlsComponent.tsx index 03e858d11..673141392 100644 --- a/src/client/app/components/BarControlsComponent.tsx +++ b/src/client/app/components/BarControlsComponent.tsx @@ -2,147 +2,39 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import * as moment from 'moment'; import * as React from 'react'; -import { FormattedMessage } from 'react-intl'; -import { FormFeedback, FormGroup, Input, Label } from 'reactstrap'; import { useAppDispatch, useAppSelector } from '../redux/reduxHooks'; -import { graphSlice, selectBarStacking, selectBarWidthDays } from '../redux/slices/graphSlice'; -import translate from '../utils/translate'; +import { graphSlice, selectBarStacking } from '../redux/slices/graphSlice'; +import { useTranslate } from '../redux/componentHooks'; import TooltipMarkerComponent from './TooltipMarkerComponent'; +import IntervalControlsComponent from './IntervalControlsComponent'; /** - * @returns controls for the Options Ui page. + * @returns controls for bar page. */ export default function BarControlsComponent() { + const translate = useTranslate(); const dispatch = useAppDispatch(); - // The min/max days allowed for user selection - const MIN_BAR_DAYS = 1; - const MAX_BAR_DAYS = 366; - // Special value if custom input for standard menu. - const CUSTOM_INPUT = '-99'; - - // This is the current bar interval for graphic. - const barDuration = useAppSelector(selectBarWidthDays); const barStacking = useAppSelector(selectBarStacking); - // Holds the value of standard bar duration choices used so decoupled from custom. - const [barDays, setBarDays] = React.useState(barDuration.asDays().toString()); - // Holds the value during custom bar duration input so only update graphic when done entering and - // separate from standard choices. - const [barDaysCustom, setBarDaysCustom] = React.useState(barDuration.asDays()); - // True if custom bar duration input is active. - const [showCustomBarDuration, setShowCustomBarDuration] = React.useState(false); const handleChangeBarStacking = () => { dispatch(graphSlice.actions.changeBarStacking()); }; - // Keeps react-level state, and redux state in sync. - // Two different layers in state may differ especially when externally updated (chart link, history buttons.) - React.useEffect(() => { - // Assume value is valid since it is coming from state. - // Do not allow bad values in state. - const isCustom = !(['1', '7', '28'].find(days => days == barDuration.asDays().toString())); - setShowCustomBarDuration(isCustom); - setBarDaysCustom(barDuration.asDays()); - setBarDays(isCustom ? CUSTOM_INPUT : barDuration.asDays().toString()); - }, [barDuration]); - - // Returns true if this is a valid bar duration. - const barDaysValid = (barDays: number) => { - return Number.isInteger(barDays) && barDays >= MIN_BAR_DAYS && barDays <= MAX_BAR_DAYS; - }; - - // Updates values when the standard bar duration menu is used. - const handleBarDaysChange = (value: string) => { - if (value === CUSTOM_INPUT) { - // Set menu value for standard bar to special value to show custom - // and show the custom input area. - setBarDays(CUSTOM_INPUT); - setShowCustomBarDuration(true); - } else { - // Set the standard menu value, hide the custom bar duration input - // and bar duration for graphing. - // Since controlled values know it is a valid integer. - setShowCustomBarDuration(false); - updateBarDurationChange(Number(value)); - } - }; - - // Updates value when the custom bar duration input is used. - const handleCustomBarDaysChange = (value: number) => { - setBarDaysCustom(value); - }; - - const handleEnter = (key: string) => { - // This detects the enter key and then uses the previously entered custom - // bar duration to set the bar duration for the graphic. - if (key == 'Enter') { - updateBarDurationChange(barDaysCustom); - } - }; - - const updateBarDurationChange = (value: number) => { - // Update if okay value. May not be okay if this came from user entry in custom form. - if (barDaysValid(value)) { - dispatch(graphSlice.actions.updateBarDuration(moment.duration(value, 'days'))); - } - }; - return (
-
+
-
-

- {translate('bar.interval')}: - -

- handleBarDaysChange(e.target.value)} - > - - - - - - {/* This has a little more spacing at bottom than optimal. */} - {showCustomBarDuration && - - - handleCustomBarDaysChange(Number(e.target.value))} - // This grabs each key hit and then finishes input when hit enter. - onKeyDown={e => { handleEnter(e.key); }} - step='1' - min={MIN_BAR_DAYS} - max={MAX_BAR_DAYS} - value={barDaysCustom} - invalid={!barDaysValid(barDaysCustom)} /> - - - - - } -
+ {}
); } const divTopBottomPadding: React.CSSProperties = { - paddingTop: '15px', + paddingTop: '0px', paddingBottom: '15px' }; - -const labelStyle: React.CSSProperties = { - fontWeight: 'bold', - margin: 0 -}; diff --git a/src/client/app/components/ChartLinkComponent.tsx b/src/client/app/components/ChartLinkComponent.tsx index 8eb0987e2..6de7aabae 100644 --- a/src/client/app/components/ChartLinkComponent.tsx +++ b/src/client/app/components/ChartLinkComponent.tsx @@ -11,13 +11,14 @@ import { selectChartLink } from '../redux/selectors/uiSelectors'; import { selectChartLinkHideOptions, setChartLinkOptionsVisibility } from '../redux/slices/appStateSlice'; import { selectSelectedGroups, selectSelectedMeters } from '../redux/slices/graphSlice'; import { showErrorNotification, showInfoNotification } from '../utils/notifications'; -import translate from '../utils/translate'; +import { useTranslate } from '../redux/componentHooks'; import TooltipMarkerComponent from './TooltipMarkerComponent'; /** * @returns chartLinkComponent */ export default function ChartLinkComponent() { + const translate = useTranslate(); const dispatch = useAppDispatch(); const [linkTextVisible, setLinkTextVisible] = React.useState(false); const linkText = useAppSelector(selectChartLink); diff --git a/src/client/app/components/ChartSelectComponent.tsx b/src/client/app/components/ChartSelectComponent.tsx index de47867ca..f6e32177f 100644 --- a/src/client/app/components/ChartSelectComponent.tsx +++ b/src/client/app/components/ChartSelectComponent.tsx @@ -2,7 +2,7 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { sortBy, values } from 'lodash'; +import { values } from 'lodash'; import * as React from 'react'; import { useState } from 'react'; import { FormattedMessage } from 'react-intl'; @@ -13,8 +13,9 @@ import { graphSlice, selectChartToRender } from '../redux/slices/graphSlice'; import { SelectOption } from '../types/items'; import { ChartTypes } from '../types/redux/graph'; import { State } from '../types/redux/state'; -import { useTranslate } from 'redux/componentHooks'; +import { useTranslate } from '../redux/componentHooks'; import TooltipMarkerComponent from './TooltipMarkerComponent'; +import { selectSelectedLanguage } from '../redux/slices/appStateSlice'; /** * A component that allows users to select which chart should be displayed. @@ -23,12 +24,16 @@ import TooltipMarkerComponent from './TooltipMarkerComponent'; export default function ChartSelectComponent() { const translate = useTranslate(); const currentChartToRender = useAppSelector(selectChartToRender); + const locale = useAppSelector(selectSelectedLanguage); const dispatch = useAppDispatch(); const [expand, setExpand] = useState(false); const mapsById = useSelector((state: State) => state.maps.byMapID); - const sortedMaps = sortBy(values(mapsById).map(map => ( + + const maps = values(mapsById).map(map => ( { value: map.id, label: map.name, isDisabled: !(map.origin && map.opposite) } as SelectOption - )), 'label'); + )); + const sortedMaps = maps.sort((mapA, mapB) => mapA.label.toLowerCase(). + localeCompare(mapB.label.toLowerCase(), String(locale), { sensitivity: 'accent' })); return ( <> diff --git a/src/client/app/components/CompareControlsComponent.tsx b/src/client/app/components/CompareControlsComponent.tsx index d990852cd..0d1a2c266 100644 --- a/src/client/app/components/CompareControlsComponent.tsx +++ b/src/client/app/components/CompareControlsComponent.tsx @@ -1,89 +1,60 @@ /* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +* License, v. 2.0. If a copy of the MPL was not distributed with this +* file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import * as moment from 'moment'; import * as React from 'react'; -import { Button, ButtonGroup, Dropdown, DropdownItem, DropdownMenu, DropdownToggle } from 'reactstrap'; -import { graphSlice, selectComparePeriod, selectSortingOrder } from '../redux/slices/graphSlice'; +import { Input } from 'reactstrap'; import { useAppDispatch, useAppSelector } from '../redux/reduxHooks'; -import { ComparePeriod, SortingOrder } from '../utils/calculateCompare'; -import translate from '../utils/translate'; +import { graphSlice, selectSortingOrder } from '../redux/slices/graphSlice'; +import { SortingOrder } from '../utils/calculateCompare'; +import { useTranslate } from '../redux/componentHooks'; import TooltipMarkerComponent from './TooltipMarkerComponent'; +import IntervalControlsComponent from './IntervalControlsComponent'; /** - * @returns controls for the compare page + * @returns controls for compare page. */ export default function CompareControlsComponent() { + const translate = useTranslate(); const dispatch = useAppDispatch(); - const comparePeriod = useAppSelector(selectComparePeriod); + + // This is the current sorting order for graphic const compareSortingOrder = useAppSelector(selectSortingOrder); - const [compareSortingDropdownOpen, setCompareSortingDropdownOpen] = React.useState(false); - const handleCompareButton = (comparePeriod: ComparePeriod) => { - dispatch(graphSlice.actions.updateComparePeriod({ comparePeriod, currentTime: moment() })); - }; - const handleSortingButton = (sortingOrder: SortingOrder) => { + + // Updates sorting order when the sort order menu is used. + const handleSortingChange = (value: string) => { + const sortingOrder = value as unknown as SortingOrder; dispatch(graphSlice.actions.changeCompareSortingOrder(sortingOrder)); }; return (
- - - - - - - setCompareSortingDropdownOpen(current => !current)}> - - {translate('sort')} - - - - handleSortingButton(SortingOrder.Alphabetical)} - > - {translate('alphabetically')} - - handleSortingButton(SortingOrder.Ascending)} - > - {translate('ascending')} - - handleSortingButton(SortingOrder.Descending)} - > - {translate('descending')} - - - -
+ + + + +
+ ); } -const zIndexFix: React.CSSProperties = { - zIndex: 0 -}; \ No newline at end of file +const divTopBottomPadding: React.CSSProperties = { + paddingTop: '0px', + paddingBottom: '15px' +}; + +const labelStyle: React.CSSProperties = { + fontWeight: 'bold', + margin: 0 +}; diff --git a/src/client/app/components/ConfirmActionModalComponent.tsx b/src/client/app/components/ConfirmActionModalComponent.tsx index e3730aa62..9f15d58b7 100644 --- a/src/client/app/components/ConfirmActionModalComponent.tsx +++ b/src/client/app/components/ConfirmActionModalComponent.tsx @@ -4,7 +4,7 @@ import * as React from 'react'; import '../styles/modal.css'; -import translate from '../utils/translate'; +import { useTranslate } from '../redux/componentHooks'; import { Button, Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap'; interface ConfirmActionModalComponentProps { @@ -41,7 +41,7 @@ interface ConfirmActionModalComponentProps { * @returns A modal component that executes the actionFunction on confirmation and handleClose on rejection. */ export default function ConfirmActionModalComponent(props: ConfirmActionModalComponentProps) { - + const translate = useTranslate(); const handleClose = () => { props.handleClose(); }; diff --git a/src/client/app/components/DashboardComponent.tsx b/src/client/app/components/DashboardComponent.tsx index 92fbcf3a2..5e16f9fc8 100644 --- a/src/client/app/components/DashboardComponent.tsx +++ b/src/client/app/components/DashboardComponent.tsx @@ -31,7 +31,7 @@ export default function DashboardComponent() {
-
+
{chartToRender === ChartTypes.line && } {chartToRender === ChartTypes.bar && } diff --git a/src/client/app/components/DateRangeComponent.tsx b/src/client/app/components/DateRangeComponent.tsx index 22f7846ed..033b311e3 100644 --- a/src/client/app/components/DateRangeComponent.tsx +++ b/src/client/app/components/DateRangeComponent.tsx @@ -13,7 +13,7 @@ import { changeSliderRange, selectQueryTimeInterval, updateTimeInterval, selectC import '../styles/DateRangeCustom.css'; import { Dispatch } from '../types/redux/actions'; import { dateRangeToTimeInterval, timeIntervalToDateRange } from '../utils/dateRangeCompatibility'; -import translate from '../utils/translate'; +import { useTranslate } from '../redux/componentHooks'; import TooltipMarkerComponent from './TooltipMarkerComponent'; import { ChartTypes } from '../types/redux/graph'; @@ -22,6 +22,7 @@ import { ChartTypes } from '../types/redux/graph'; * @returns Date Range Calendar Picker */ export default function DateRangeComponent() { + const translate = useTranslate(); const dispatch: Dispatch = useAppDispatch(); const queryTimeInterval = useAppSelector(selectQueryTimeInterval); const locale = useAppSelector(selectSelectedLanguage); diff --git a/src/client/app/components/ErrorBarComponent.tsx b/src/client/app/components/ErrorBarComponent.tsx index c8116ed3e..957adb04b 100644 --- a/src/client/app/components/ErrorBarComponent.tsx +++ b/src/client/app/components/ErrorBarComponent.tsx @@ -5,7 +5,7 @@ import * as React from 'react'; import { useAppDispatch, useAppSelector } from '../redux/reduxHooks'; import { graphSlice, selectShowMinMax } from '../redux/slices/graphSlice'; -import translate from '../utils/translate'; +import { useTranslate } from '../redux/componentHooks'; import TooltipMarkerComponent from './TooltipMarkerComponent'; /** @@ -13,6 +13,7 @@ import TooltipMarkerComponent from './TooltipMarkerComponent'; * @returns Error Bar checkbox with tooltip and label */ export default function ErrorBarComponent() { + const translate = useTranslate(); const dispatch = useAppDispatch(); const showMinMax = useAppSelector(selectShowMinMax); diff --git a/src/client/app/components/FooterComponent.tsx b/src/client/app/components/FooterComponent.tsx index 18e8b4f53..93e27bdb3 100644 --- a/src/client/app/components/FooterComponent.tsx +++ b/src/client/app/components/FooterComponent.tsx @@ -16,16 +16,17 @@ export default function FooterComponent() { return ( ); } @@ -36,4 +37,4 @@ const footerStyle: React.CSSProperties = { padding: '10px 0px', textAlign: 'center', width: '100%' -}; \ No newline at end of file +}; diff --git a/src/client/app/components/FormFileUploaderComponent.tsx b/src/client/app/components/FormFileUploaderComponent.tsx index 813569e43..714001f63 100644 --- a/src/client/app/components/FormFileUploaderComponent.tsx +++ b/src/client/app/components/FormFileUploaderComponent.tsx @@ -3,14 +3,12 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import * as React from 'react'; -import { FormattedMessage } from 'react-intl'; -import { Col, Input, FormGroup, FormText, Label } from 'reactstrap'; +import { Col, Input, FormGroup, Label } from 'reactstrap'; +import { useTranslate } from '../redux/componentHooks'; interface FileUploader { - reference: React.RefObject; - required: boolean; - formText: string; - labelStyle?: React.CSSProperties; + isInvalid: boolean; + onFileChange: (file: File | null) => void; } /** @@ -19,17 +17,28 @@ interface FileUploader { * @returns File uploader element */ export default function FileUploaderComponent(props: FileUploader) { + const translate = useTranslate(); + const handleFileChange = (event: React.ChangeEvent) => { + const file = event.target.files?.[0] || null; + props.onFileChange(file); + }; + return ( - ); } diff --git a/src/client/app/components/GraphicRateMenuComponent.tsx b/src/client/app/components/GraphicRateMenuComponent.tsx index 163d51965..febe41352 100644 --- a/src/client/app/components/GraphicRateMenuComponent.tsx +++ b/src/client/app/components/GraphicRateMenuComponent.tsx @@ -11,7 +11,7 @@ import { graphSlice, selectGraphState } from '../redux/slices/graphSlice'; import { SelectOption } from '../types/items'; import { ChartTypes, LineGraphRate, LineGraphRates } from '../types/redux/graph'; import { UnitRepresentType } from '../types/redux/units'; -import translate from '../utils/translate'; +import { useTranslate } from '../redux/componentHooks'; import TooltipMarkerComponent from './TooltipMarkerComponent'; /** @@ -19,6 +19,7 @@ import TooltipMarkerComponent from './TooltipMarkerComponent'; * @returns Rate selection element */ export default function GraphicRateMenuComponent() { + const translate = useTranslate(); const dispatch = useAppDispatch(); // Graph state diff --git a/src/client/app/components/HeaderButtonsComponent.tsx b/src/client/app/components/HeaderButtonsComponent.tsx index 39fda6430..e800c5895 100644 --- a/src/client/app/components/HeaderButtonsComponent.tsx +++ b/src/client/app/components/HeaderButtonsComponent.tsx @@ -6,7 +6,7 @@ import * as React from 'react'; import { useEffect, useState } from 'react'; import { FormattedMessage } from 'react-intl'; import { Link, useLocation } from 'react-router-dom'; -import { DropdownItem, DropdownMenu, DropdownToggle, Nav, NavLink, Navbar, UncontrolledDropdown } from 'reactstrap'; +import { DropdownItem, DropdownMenu, DropdownToggle, Modal, ModalBody, ModalHeader, Nav, NavLink, Navbar, UncontrolledDropdown } from 'reactstrap'; import TooltipHelpComponent from '../components/TooltipHelpComponent'; import { clearGraphHistory } from '../redux/actions/extraActions'; import { authApi } from '../redux/api/authApi'; @@ -16,15 +16,17 @@ import { selectHelpUrl } from '../redux/slices/adminSlice'; import { selectOptionsVisibility, toggleOptionsVisibility } from '../redux/slices/appStateSlice'; import { selectHasRolePermissions, selectIsAdmin, selectIsLoggedIn } from '../redux/slices/currentUserSlice'; import { UserRole } from '../types/items'; -import translate from '../utils/translate'; +import { useTranslate } from '../redux/componentHooks'; import LanguageSelectorComponent from './LanguageSelectorComponent'; import TooltipMarkerComponent from './TooltipMarkerComponent'; +import LoginComponent from './LoginComponent'; /** * React Component that defines the header buttons at the top of a page * @returns Header buttons element */ export default function HeaderButtonsComponent() { + const translate = useTranslate(); const [logout] = authApi.useLogoutMutation(); const dispatch = useAppDispatch(); // Get the current page so know which one should not be shown in menu. @@ -34,7 +36,7 @@ export default function HeaderButtonsComponent() { const version = useAppSelector(selectOEDVersion); const helpUrl = useAppSelector(selectHelpUrl); // options help - const optionsHelp = helpUrl + '/optionsMenu.html'; + const optionsHelp = helpUrl + '/optionsMenu/'; const loggedInAsAdmin = useAppSelector(selectIsAdmin); const loggedIn = useAppSelector(selectIsLoggedIn); @@ -58,9 +60,11 @@ export default function HeaderButtonsComponent() { shouldGroupsButtonDisabled: true, shouldMetersButtonDisabled: true, shouldMapsButtonDisabled: true, - shouldCSVButtonDisabled: true, + shouldCSVMetersButtonDisabled: true, + shouldCSVReadingsButtonDisabled: true, shouldUnitsButtonDisabled: true, shouldConversionsButtonDisabled: true, + shouldVisualUnitMapButtonDisabled: true, // Translated menu title that depend on whether logged in. menuTitle: '', // link to help page for page choices. Should not see default but use general help URL. @@ -93,9 +97,11 @@ export default function HeaderButtonsComponent() { shouldGroupsButtonDisabled: pathname === '/groups', shouldMetersButtonDisabled: pathname === '/meters', shouldMapsButtonDisabled: pathname === '/maps', - shouldCSVButtonDisabled: pathname === '/csv', + shouldCSVMetersButtonDisabled: pathname === '/csvMeters', + shouldCSVReadingsButtonDisabled: pathname === '/csvReadings', shouldUnitsButtonDisabled: pathname === '/units', - shouldConversionsButtonDisabled: pathname === '/conversions' + shouldConversionsButtonDisabled: pathname === '/conversions', + shouldVisualUnitMapButtonDisabled: pathname === '/visual-unit' })); }, [pathname]); @@ -123,7 +129,7 @@ export default function HeaderButtonsComponent() { display: pathname === '/' ? 'block' : 'none' }; // Admin help or regular user page - const neededPage = loggedInAsAdmin ? '/adminPageChoices.html' : '/pageChoices.html'; + const neededPage = loggedInAsAdmin ? '/adminPageChoices/' : '/pageChoices/'; const currentPageChoicesHelp = helpUrl + neededPage; setState(prevState => ({ @@ -148,6 +154,17 @@ export default function HeaderButtonsComponent() { logout(); } }; + // Handle modal visibility + const [showModal, setShowModal] = useState(false); + + const handleClose = () => { + setShowModal(false); + }; + + const handleShow = () => { + setShowModal(true); + }; + return (
@@ -172,12 +189,19 @@ export default function HeaderButtonsComponent() { to="/conversions"> + + + - + to="/csvReadings"> + + + + + onClick={handleShow}> @@ -272,6 +300,17 @@ export default function HeaderButtonsComponent() { + <> + + + {translate('log.in')} + + + + + + +
); } diff --git a/src/client/app/components/IntervalControlsComponent.tsx b/src/client/app/components/IntervalControlsComponent.tsx new file mode 100644 index 000000000..eea3cd0a6 --- /dev/null +++ b/src/client/app/components/IntervalControlsComponent.tsx @@ -0,0 +1,182 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import * as moment from 'moment'; +import * as React from 'react'; +import { FormattedMessage } from 'react-intl'; +import { FormFeedback, FormGroup, Input, Label } from 'reactstrap'; +import { useAppDispatch, useAppSelector } from '../redux/reduxHooks'; +import { selectChartToRender, graphSlice, selectWidthDays, selectComparePeriod } from '../redux/slices/graphSlice'; +import { ChartTypes } from '../types/redux/graph'; +import { ComparePeriod } from '../utils/calculateCompare'; +import translate from '../utils/translate'; +import TooltipMarkerComponent from './TooltipMarkerComponent'; + +/** + * @returns Interval controls for the bar, map, and compare pages + */ +export default function IntervalControlsComponent() { + const dispatch = useAppDispatch(); + const chartType = useAppSelector(selectChartToRender); + + // The min/max days allowed for user selection + const MIN_DAYS = 1; + const MAX_DAYS = 366; + // Special value if custom input for standard menu. + const CUSTOM_INPUT = '-99'; + + // This is the current interval for the bar and map graphics. + const duration = useAppSelector(selectWidthDays); + // This is the current compare period for graphic + const comparePeriod = chartType === ChartTypes.compare ? useAppSelector(selectComparePeriod) : undefined; + + // Holds the value of standard duration choices used for bar and map, decoupled from custom. + const [days, setDays] = React.useState(duration.asDays().toString()); + // Holds the value during custom duration input for bar and map, so only update graphic + // when done entering and separate from standard choices. + const [daysCustom, setDaysCustom] = React.useState(duration.asDays()); + // True if custom duration input for bar or map is active. + const [showCustomDuration, setShowCustomDuration] = React.useState(false); + // Define a flag to track if custom input is actively being used + const [isCustomInput, setIsCustomInput] = React.useState(false); + + // Keeps react-level state, and redux state in sync. + // Two different layers in state may differ especially when externally updated (chart link, history buttons.) + React.useEffect(() => { + // If user is in custom input mode, don't reset to standard options + if (!isCustomInput) { + const durationValues = Object.values(ComparePeriod) as string[]; + const isCustom = !(durationValues.includes(duration.asDays().toString())); + setShowCustomDuration(isCustom); + setDaysCustom(duration.asDays()); + setDays(isCustom ? CUSTOM_INPUT : duration.asDays().toString()); + } + }, [duration, isCustomInput]); + + // Returns true if this is a valid duration. + const daysValid = (days: number) => { + return Number.isInteger(days) && days >= MIN_DAYS && days <= MAX_DAYS; + }; + + // Updates values when the standard duration menu for bar or map is used. + const handleDaysChange = (value: string) => { + setIsCustomInput(false); + if (value === CUSTOM_INPUT) { + // Set menu value from standard value to special value to show custom + // and show the custom input area. + setShowCustomDuration(true); + setDays(CUSTOM_INPUT); + } else { + // Set the standard menu value, hide the custom duration input + // and duration for graphing. + // Since controlled values know it is a valid integer. + setShowCustomDuration(false); + updateDurationChange(Number(value)); + } + }; + + // Updates value when the custom duration input is used for bar or map. + const handleCustomDaysChange = (value: number) => { + setIsCustomInput(true); + setDaysCustom(value); + }; + + const handleEnter = (key: string) => { + // This detects the enter key and then uses the previously entered custom + // duration to set the duration for the graphic. + if (key === 'Enter') { + updateDurationChange(daysCustom); + } + }; + + const updateDurationChange = (value: number) => { + // Update if okay value. May not be okay if this came from user entry in custom form. + if (daysValid(value)) { + dispatch(graphSlice.actions.updateDuration(moment.duration(value, 'days'))); + } + }; + + // Handles change for compare period dropdown + const handleComparePeriodChange = (value: string) => { + const period = value as unknown as ComparePeriod; + dispatch(graphSlice.actions.updateComparePeriod({ comparePeriod: period, currentTime: moment() })); + }; + + const comparePeriodTranslations: Record = { + Day: 'day', + Week: 'week', + FourWeeks: '4.weeks' + }; + + return ( +
+
+

+ {translate( + chartType === ChartTypes.bar ? 'bar.interval' : + chartType === ChartTypes.map ? 'map.interval' : + 'compare.period' + )}: + +

+ chartType === ChartTypes.compare ? handleComparePeriodChange(e.target.value) : handleDaysChange(e.target.value)} + > + {Object.entries(ComparePeriod).map( + ([key, value]) => ( + + ) + )} + {/* TODO: Compare is currently not ready for the custom option. */} + {chartType !== ChartTypes.compare && + + } + + {showCustomDuration && chartType !== ChartTypes.compare && + + + handleCustomDaysChange(Number(e.target.value))} + // This grabs each key hit and then finishes input when hit enter. + onKeyDown={e => handleEnter(e.key)} + step='1' + min={MIN_DAYS} + max={MAX_DAYS} + value={daysCustom} + invalid={!daysValid(daysCustom)} + /> + + + + + } +
+
+ ); +} + +const divTopBottomPadding: React.CSSProperties = { + paddingTop: '0px', + paddingBottom: '15px' +}; + +const labelStyle: React.CSSProperties = { + fontWeight: 'bold', + margin: 0 +}; diff --git a/src/client/app/components/LanguageSelectorComponent.tsx b/src/client/app/components/LanguageSelectorComponent.tsx index 5a5c3f035..cb081f4a4 100644 --- a/src/client/app/components/LanguageSelectorComponent.tsx +++ b/src/client/app/components/LanguageSelectorComponent.tsx @@ -6,10 +6,9 @@ import * as React from 'react'; import { FormattedMessage } from 'react-intl'; import { DropdownItem, DropdownMenu, DropdownToggle, UncontrolledDropdown } from 'reactstrap'; import { selectSelectedLanguage, updateSelectedLanguage } from '../redux/slices/appStateSlice'; -import { selectOEDVersion } from '../redux/api/versionApi'; import { useAppDispatch, useAppSelector } from '../redux/reduxHooks'; import { LanguageTypes } from '../types/redux/i18n'; -import { selectBaseHelpUrl } from '../redux/slices/adminSlice'; +import { selectHelpUrl } from '../redux/slices/adminSlice'; /** * A component that allows users to select which language the page should be displayed in. @@ -19,10 +18,7 @@ export default function LanguageSelectorComponent() { const dispatch = useAppDispatch(); const selectedLanguage = useAppSelector(selectSelectedLanguage); - const version = useAppSelector(selectOEDVersion); - const baseHelpUrl = useAppSelector(selectBaseHelpUrl); - - const helpUrl = baseHelpUrl + version; + const helpUrl = useAppSelector(selectHelpUrl); return ( <> @@ -48,7 +44,7 @@ export default function LanguageSelectorComponent() { + href={helpUrl + '/language/'}> diff --git a/src/client/app/components/LineChartComponent.tsx b/src/client/app/components/LineChartComponent.tsx index 2aeed6dd2..c0bc899f3 100644 --- a/src/client/app/components/LineChartComponent.tsx +++ b/src/client/app/components/LineChartComponent.tsx @@ -17,7 +17,7 @@ import { selectLineChartDeps, selectPlotlyGroupData, selectPlotlyMeterData } fro import { selectLineUnitLabel } from '../redux/selectors/plotlyDataSelectors'; import { selectSelectedLanguage } from '../redux/slices/appStateSlice'; import Locales from '../types/locales'; -import { useTranslate } from 'redux/componentHooks'; +import { useTranslate } from '../redux/componentHooks'; import SpinnerComponent from './SpinnerComponent'; @@ -25,6 +25,7 @@ import SpinnerComponent from './SpinnerComponent'; * @returns plotlyLine graphic */ export default function LineChartComponent() { + const translate = useTranslate(); const dispatch = useAppDispatch(); // get current data fetching arguments const { meterArgs, groupArgs, meterShouldSkip, groupShouldSkip } = useAppSelector(selectLineChartQueryArgs); @@ -57,8 +58,6 @@ export default function LineChartComponent() { // Use Query Data to derive plotly datasets memoized selector const unitLabel = useAppSelector(selectLineUnitLabel); - const translate = useTranslate(); - // Display Plotly Buttons Feature // The number of items in defaultButtons and advancedButtons must differ as discussed below const defaultButtons: Plotly.ModeBarDefaultButtons[] = ['zoom2d', 'pan2d', 'select2d', 'lasso2d', 'zoomIn2d', @@ -89,6 +88,7 @@ export default function LineChartComponent() { data={data} style={{ width: '100%', height: '100%', minHeight: '700px' }} layout={{ + margin: { t: 0, b: 0, r: 3 }, // Eliminate top, bottom, and right margins autosize: true, showlegend: true, legend: { x: 0, y: 1.1, orientation: 'h' }, // 'fixedrange' on the yAxis means that dragging is only allowed on the xAxis which we utilize for selecting dateRanges diff --git a/src/client/app/components/LoginComponent.tsx b/src/client/app/components/LoginComponent.tsx index 538c03d99..de94319c4 100644 --- a/src/client/app/components/LoginComponent.tsx +++ b/src/client/app/components/LoginComponent.tsx @@ -5,24 +5,29 @@ import * as React from 'react'; import { useRef, useState } from 'react'; import { FormattedMessage } from 'react-intl'; -import { useNavigate } from 'react-router-dom'; import { Button, Form, FormGroup, Input, Label } from 'reactstrap'; import { authApi } from '../redux/api/authApi'; import { showErrorNotification, showSuccessNotification } from '../utils/notifications'; -import translate from '../utils/translate'; +import { useTranslate } from '../redux/componentHooks'; +interface LoginProp { + handleClose: () => void; +} + /** + * @param handleClose Function to close modal after login + * @param handleClose.handleClose Needed by ESLint see above * @returns The login page for users or admins. */ -export default function LoginComponent() { +export default function LoginComponent({ handleClose }: LoginProp) { + const translate = useTranslate(); // Local State const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); // Html Element Reference used for focus() const inputRef = useRef(null); - const navigate = useNavigate(); // Grab the derived loginMutation from the API // The naming of the returned objects is arbitrary @@ -36,7 +41,7 @@ export default function LoginComponent() { .then(() => { // No error, success! showSuccessNotification(translate('login.success')); - navigate('/'); + handleClose(); }) .catch(() => { // Error on login Mutation @@ -69,16 +74,31 @@ export default function LoginComponent() { innerRef={inputRef} /> - +
+
+ +
+
+ +
+
+ + -
+
); } diff --git a/src/client/app/components/MapChartComponent.tsx b/src/client/app/components/MapChartComponent.tsx index e1a5dde28..28b0d082c 100644 --- a/src/client/app/components/MapChartComponent.tsx +++ b/src/client/app/components/MapChartComponent.tsx @@ -8,7 +8,7 @@ import * as React from 'react'; import Plot from 'react-plotly.js'; import { useSelector } from 'react-redux'; import { - selectAreaUnit, selectBarWidthDays, + selectAreaUnit, selectWidthDays, selectGraphAreaNormalization, selectSelectedGroups, selectSelectedMeters, selectSelectedUnit } from '../redux/slices/graphSlice'; @@ -18,6 +18,7 @@ import { readingsApi } from '../redux/api/readingsApi'; import { selectUnitDataById } from '../redux/api/unitsApi'; import { useAppSelector } from '../redux/reduxHooks'; import { selectMapChartQueryArgs } from '../redux/selectors/chartQuerySelectors'; +import { selectAreaScalingFromEntity } from '../redux/selectors/entitySelectors'; import { DataType } from '../types/Datasources'; import { State } from '../types/redux/state'; import { UnitRepresentType } from '../types/redux/units'; @@ -30,23 +31,24 @@ import { itemMapInfoOk, normalizeImageDimensions } from '../utils/calibration'; -import { AreaUnitType, getAreaUnitConversion } from '../utils/getAreaUnitConversion'; +import { AreaUnitType } from '../utils/getAreaUnitConversion'; import getGraphColor from '../utils/getGraphColor'; -import translate from '../utils/translate'; +import { useTranslate } from '../redux/componentHooks'; import SpinnerComponent from './SpinnerComponent'; +import { showInfoNotification } from '../utils/notifications'; /** * @returns map component */ export default function MapChartComponent() { - + const translate = useTranslate(); const { meterArgs, groupArgs, meterShouldSkip, groupShouldSkip } = useAppSelector(selectMapChartQueryArgs); const { data: meterReadings, isLoading: meterIsFetching } = readingsApi.useBarQuery(meterArgs, { skip: meterShouldSkip }); const { data: groupData, isLoading: groupIsFetching } = readingsApi.useBarQuery(groupArgs, { skip: groupShouldSkip }); // converting maps to RTK has been proving troublesome, therefore using a combination of old/new stateSelectors const unitID = useAppSelector(selectSelectedUnit); - const barDuration = useAppSelector(selectBarWidthDays); + const mapDuration = useAppSelector(selectWidthDays); const areaNormalization = useAppSelector(selectGraphAreaNormalization); const selectedAreaUnit = useAppSelector(selectAreaUnit); const selectedMeters = useAppSelector(selectSelectedMeters); @@ -82,7 +84,7 @@ export default function MapChartComponent() { // Holds the hover text for each point for Plotly const hoverText: string[] = []; // Holds the size of each circle for Plotly. - const size: number[] = []; + let size: number[] = []; // Holds the color of each circle for Plotly. const colors: string[] = []; // If there is no map then use a new, empty image as the map. I believe this avoids errors @@ -93,7 +95,7 @@ export default function MapChartComponent() { const y: number[] = []; // const timeInterval = state.graph.queryTimeInterval; - // const barDuration = state.graph.barDuration + // const mapDuration = state.graph.mapDuration // Make sure there is a map with values so avoid issues. if (map && map.origin && map.opposite) { // The size of the original map loaded into OED. @@ -143,37 +145,32 @@ export default function MapChartComponent() { for (const meterID of selectedMeters) { // Get meter id number. - // Get meter GPS value. - const gps = meterDataById[meterID].gps; + const entity = meterDataById[meterID]; // filter meters with actual gps coordinates. - if (gps !== undefined && gps !== null && meterReadings !== undefined) { - let meterArea = meterDataById[meterID].area; + if (entity.gps !== undefined && entity.gps !== null && meterReadings !== undefined) { // we either don't care about area, or we do in which case there needs to be a nonzero area - if (!areaNormalization || (meterArea > 0 && meterDataById[meterID].areaUnit != AreaUnitType.none)) { - if (areaNormalization) { - // convert the meter area into the proper unit, if needed - meterArea *= getAreaUnitConversion(meterDataById[meterID].areaUnit, selectedAreaUnit); - } + if (!areaNormalization || (entity.area > 0 && entity.areaUnit != AreaUnitType.none)) { + const meterArea = selectAreaScalingFromEntity(entity, selectedAreaUnit, areaNormalization); // Convert the gps value to the equivalent Plotly grid coordinates on user map. // First, convert from GPS to grid units. Since we are doing a GPS calculation, this happens on the true north map. // It must be on true north map since only there are the GPS axis parallel to the map axis. // To start, calculate the user grid coordinates (Plotly) from the GPS value. This involves calculating // it coordinates on the true north map and then rotating/shifting to the user map. - const meterGPSInUserGrid: CartesianPoint = gpsToUserGrid(imageDimensionNormalized, gps, origin, scaleOfMap, map.northAngle); + const meterGPSInUserGrid: CartesianPoint = gpsToUserGrid(imageDimensionNormalized, entity.gps, origin, scaleOfMap, map.northAngle); // Only display items within valid info and within map. - if (itemMapInfoOk(meterID, DataType.Meter, map, gps) && itemDisplayableOnMap(imageDimensionNormalized, meterGPSInUserGrid)) { + if (itemMapInfoOk(meterID, DataType.Meter, map, entity.gps) && itemDisplayableOnMap(imageDimensionNormalized, meterGPSInUserGrid)) { // The x, y value for Plotly to use that are on the user map. x.push(meterGPSInUserGrid.x); y.push(meterGPSInUserGrid.y); - // Make sure the bar reading data is available. The timeInterval should be fine (but checked) but the barDuration might have changed + // Make sure the bar reading data is available. The timeInterval should be fine (but checked) but the mapDuration might have changed // and be fetching. The unit could change from that menu so also need to check. // Get the bar data to use for the map circle. - // const readingsData = meterReadings[timeInterval.toString()][barDuration.toISOString()][unitID]; + // const readingsData = meterReadings[timeInterval.toString()][mapDuration.toISOString()][unitID]; const readingsData = meterReadings[meterID]; // This protects against there being no readings or that the data is being updated. if (readingsData !== undefined && !meterIsFetching) { // Meter name to include in hover on graph. - const label = meterDataById[meterID].identifier; + const label = entity.identifier; // The usual color for this meter. colors.push(getGraphColor(meterID, DataType.Meter)); if (!readingsData) { @@ -196,13 +193,13 @@ export default function MapChartComponent() { // only display a range of dates for the hover text if there is more than one day in the range // Shift to UTC since want database time not local/browser time which is what moment does. timeReading = `${moment.utc(mapReading.startTimestamp).format('ll')}`; - if (barDuration.asDays() != 1) { + if (mapDuration.asDays() != 1) { // subtracting one extra day caused by day ending at midnight of the next day. // Going from DB unit timestamp that is UTC so force UTC with moment, as usual. timeReading += ` - ${moment.utc(mapReading.endTimestamp).subtract(1, 'days').format('ll')}`; } // The value for the circle is the average daily usage. - averagedReading = mapReading.reading / barDuration.asDays(); + averagedReading = mapReading.reading / mapDuration.asDays(); if (areaNormalization) { averagedReading /= meterArea; } @@ -219,35 +216,31 @@ export default function MapChartComponent() { for (const groupID of selectedGroups) { // Get group id number. - // Get group GPS value. - const gps = groupDataById[groupID].gps; + const entity = groupDataById[groupID]; // Filter groups with actual gps coordinates. - if (gps !== undefined && gps !== null && groupData !== undefined) { - let groupArea = groupDataById[groupID].area; - if (!areaNormalization || (groupArea > 0 && groupDataById[groupID].areaUnit != AreaUnitType.none)) { - if (areaNormalization) { - // convert the meter area into the proper unit, if needed - groupArea *= getAreaUnitConversion(groupDataById[groupID].areaUnit, selectedAreaUnit); - } + if (entity.gps !== undefined && entity.gps !== null && groupData !== undefined) { + // we either don't care about area, or we do in which case there needs to be a nonzero area + if (!areaNormalization || (entity.area > 0 && entity.areaUnit != AreaUnitType.none)) { + const groupArea = selectAreaScalingFromEntity(entity, selectedAreaUnit, areaNormalization); // Convert the gps value to the equivalent Plotly grid coordinates on user map. // First, convert from GPS to grid units. Since we are doing a GPS calculation, this happens on the true north map. // It must be on true north map since only there are the GPS axis parallel to the map axis. // To start, calculate the user grid coordinates (Plotly) from the GPS value. This involves calculating // it coordinates on the true north map and then rotating/shifting to the user map. - const groupGPSInUserGrid: CartesianPoint = gpsToUserGrid(imageDimensionNormalized, gps, origin, scaleOfMap, map.northAngle); + const groupGPSInUserGrid: CartesianPoint = gpsToUserGrid(imageDimensionNormalized, entity.gps, origin, scaleOfMap, map.northAngle); // Only display items within valid info and within map. - if (itemMapInfoOk(groupID, DataType.Group, map, gps) && itemDisplayableOnMap(imageDimensionNormalized, groupGPSInUserGrid)) { + if (itemMapInfoOk(groupID, DataType.Group, map, entity.gps) && itemDisplayableOnMap(imageDimensionNormalized, groupGPSInUserGrid)) { // The x, y value for Plotly to use that are on the user map. x.push(groupGPSInUserGrid.x); y.push(groupGPSInUserGrid.y); - // Make sure the bar reading data is available. The timeInterval should be fine (but checked) but the barDuration might have changed + // Make sure the bar reading data is available. The timeInterval should be fine (but checked) but the mapDuration might have changed // and be fetching. The unit could change from that menu so also need to check. // Get the bar data to use for the map circle. const readingsData = groupData[groupID]; // This protects against there being no readings or that the data is being updated. if (readingsData && !groupIsFetching) { // Group name to include in hover on graph. - const label = groupDataById[groupID].name; + const label = entity.name; // The usual color for this group. colors.push(getGraphColor(groupID, DataType.Group)); if (!readingsData) { @@ -269,13 +262,13 @@ export default function MapChartComponent() { } else { // only display a range of dates for the hover text if there is more than one day in the range timeReading = `${moment.utc(mapReading.startTimestamp).format('ll')}`; - if (barDuration.asDays() != 1) { + if (mapDuration.asDays() != 1) { // subtracting one extra day caused by day ending at midnight of the next day. // Going from DB unit timestamp that is UTC so force UTC with moment, as usual. timeReading += ` - ${moment.utc(mapReading.endTimestamp).subtract(1, 'days').format('ll')}`; } // The value for the circle is the average daily usage. - averagedReading = mapReading.reading / barDuration.asDays(); + averagedReading = mapReading.reading / mapDuration.asDays(); if (areaNormalization) { averagedReading /= groupArea; } @@ -289,10 +282,10 @@ export default function MapChartComponent() { } } } + // TODO Using the following seems to have no impact on the code. It has been noticed that this function is called // many times for each change. Someone should look at why that is happening and why some have no items in the arrays. // if (size.length > 0) { - // TODO The max circle diameter should come from admin/DB. const maxFeatureFraction = map.circleSize; // Find the smaller of width and height. This is used since it means the circle size will be // scaled to that dimension and smaller relative to the other coordinate. @@ -300,11 +293,87 @@ export default function MapChartComponent() { // The circle size is set to area below. Thus, we need to convert from wanting a max // diameter of minDimension * maxFeatureFraction to an area. const maxCircleSize = Math.PI * Math.pow(minDimension * maxFeatureFraction / 2, 2); - // Find the largest circle which is usage. - const largestCircleSize = Math.max(...size); - // Scale largest circle to the max size and others will be scaled to be smaller. - // Not that < 1 => a larger circle. - const scaling = largestCircleSize / maxCircleSize; + // What fraction of the max circle size that the min circle size will be. Determine empirically. + let minFeatureFractionOfMax = 0.05; + // If the maxFeatureFraction is too small then it is possible that the min circle will be very + // small and difficult to see. This value is the min circle size to make sure that does not happen. + // The value used was empirically determined so it would not be too small. + const circleSizeThreshold = 75; + // If the circle will be too small then force to min, otherwise use standard value. + minFeatureFractionOfMax = minFeatureFractionOfMax * maxCircleSize < circleSizeThreshold ? + circleSizeThreshold / maxCircleSize : minFeatureFractionOfMax; + // Find the min and max of the values to graph. + const min = Math.min(...size); + const max = Math.max(...size); + // Fix the range/difference between the max and min value. + const range = max - min; + // This is the min value that should be graphed if all the values were positive. This treats the + // values as if they started at zero. Thus, the minValue is the fraction of the max on this + // shifted range that will have a circle size at least the fraction of the max that is allowed. + const minValue = minFeatureFractionOfMax * range; + // TODO As also noted above, this component is rerendering multiple times. This is causing the + // information message to appear multiple times. This needs to be figured out. + // Debugging indicates that this happens when selecting a meter/group not recently used so the + // data is not in Redux store. Each time it has readingsData as undefined so it does not process + // that meter. Need to see how can avoid this. + + // Stores the amount to shift values for circle size for graphing where it is normally negative. + // Since subtract it adds to the value. It is negative for when the min is negative. + let shift; + if (range === 0) { + // All the values are the same including only one value and only zero. + // Plotly does not show circles of size 0 even if sizemin is set (hover does happen). + // To fix this, make the shift be min - 0.000123 (arbitrary value) so it will have a circle + // of the max size. The shifted value will be slightly positive (0.000123) so the circle shows. + // Note other cases avoid zero values if not the only one. + shift = min - 0.000123; + } else if (size.length != 0 && max < 0) { + // Need to test the size.length because the value is -infinity if no values. + // TODO Must be internationalized. + showInfoNotification('All values are negative so the circle sizes act as if value range was positive which may change the relative sizes.'); + // All the values are negative. Plotly will only show circles that are positive. It isn't clear + // there is a perfect solution. This will show the size as if it was positive. For example, if + // it is -100, -200 & -300 then it will use 100, 200, 300. Note the ratio of -100 to -200 (the + // two largest values) is 2x whereas the shifted ones are 300 to 200 which is 1.5. This was + // decided the best of the options considered. + if (Math.abs(max) < minValue) { + // This takes care of case where the max is small so the shifted values will have a very small + // circle size. Force max to be at least minValue (note min negative so shift other way). + // TODO Must be internationalized. + showInfoNotification('Some values are close to zero so the small circle sizes may be a little larger to be visible'); + shift = min - minValue; + } else { + shift = min + max; + } + } else if (min >= minValue) { + // There are no values smaller than desired so all circles will be big enough. + // There is no need to shift the range of values. + shift = 0; + } else { + // Some values are too small. Shifting by min would start the range at zero. + // Subtracting minValue shifts more to a larger value so the smallest one + // is the min value to give the smallest circle desired. + // TODO Must be internationalized. Same as message above. + showInfoNotification('Some values are close to zero so the small circle sizes may be a little larger to be visible.'); + shift = min - minValue; + if (max > 0 && min < 0) { + // Tell user that there are negative and positive values. + // Unlike all positive that sets the range to effectively start at zero to give users circle size + // that scale proportional to value, it is unclear the correct range for mixed sign values. + // Given this, shift as usual where the small value (large negative) will wind up near 0 and + // have a small circle size. This informs the user of the situation. + // TODO Must be internationalized. + showInfoNotification('There are negative and positive values and this impacts relative circle size.'); + } + } + // Change all the sizes by the desired shift. Note the hover is not changed so + // this only impacts the circle size but not the value seen by the user. + size = size.map(size => size - shift); + // This is how much Plotly will scale all circle sizes. (max - shift) is + // the new max value in the range of sizes + // given the shift just done. Dividing my maxCircleSize means the max value + // will have the a circle as big as the largest one desired. + const scaling = (max - shift) / maxCircleSize; // Per https://plotly.com/javascript/reference/scatter/: // The opacity of 0.5 makes it possible to see the map even when there is a circle but the hover @@ -325,7 +394,9 @@ export default function MapChartComponent() { color: colors, opacity: 0.5, size, - sizemin: 6, + // The best sizemin of a circle is unclear. Given the shifting of sizes above this probably + // should never happen but left to be safe. + sizemin: 5, sizeref: scaling, sizemode: 'area' }, @@ -340,6 +411,7 @@ export default function MapChartComponent() { // set map background image const layout: any = { + margin: { b: 0, l: 0, r: 0 }, // Eliminate bottom, left, and right margins // Either the actual map name or text to say it is not available. title: { text: (map) ? map.name : translate('map.unavailable') diff --git a/src/client/app/components/MapChartSelectComponent.tsx b/src/client/app/components/MapChartSelectComponent.tsx index bea22af7f..51cba77e1 100644 --- a/src/client/app/components/MapChartSelectComponent.tsx +++ b/src/client/app/components/MapChartSelectComponent.tsx @@ -3,13 +3,15 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import * as React from 'react'; -import { sortBy, values } from 'lodash'; +import { values } from 'lodash'; import { useDispatch, useSelector } from 'react-redux'; import { State } from '../types/redux/state'; import { SelectOption } from '../types/items'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import SingleSelectComponent from './SingleSelectComponent'; import TooltipMarkerComponent from './TooltipMarkerComponent'; +import { selectSelectedLanguage } from '../redux/slices/appStateSlice'; +import { useAppSelector } from '../redux/reduxHooks'; /** * Component used to select the desired map @@ -30,9 +32,12 @@ export default function MapChartSelectComponent() { // TODO When this is converted to RTK then should use useAppDispatch(). //Utilizes useDispatch and useSelector hooks const dispatch = useDispatch(); - const sortedMaps = sortBy(values(useSelector((state: State) => state.maps.byMapID)).map(map => ( + const locale = useAppSelector(selectSelectedLanguage); + const maps = values(useSelector((state: State) => state.maps.byMapID)).map(map => ( { value: map.id, label: map.name, isDisabled: !(map.origin && map.opposite) } as SelectOption - )), 'label'); + )); + const sortedMaps = maps.sort((mapA, mapB) => mapA.label.toLowerCase(). + localeCompare(mapB.label.toLowerCase(), String(locale), { sensitivity: 'accent' })); const selectedMap = { label: useSelector((state: State) => state.maps.byMapID[state.maps.selectedMap] ? state.maps.byMapID[state.maps.selectedMap].name : ''), diff --git a/src/client/app/components/MapControlsComponent.tsx b/src/client/app/components/MapControlsComponent.tsx index ac0934d4e..536cc0744 100644 --- a/src/client/app/components/MapControlsComponent.tsx +++ b/src/client/app/components/MapControlsComponent.tsx @@ -1,53 +1,19 @@ /* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +* License, v. 2.0. If a copy of the MPL was not distributed with this +* file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -import * as moment from 'moment'; import * as React from 'react'; -import { Button, ButtonGroup } from 'reactstrap'; -import { useAppDispatch, useAppSelector } from '../redux/reduxHooks'; -import { selectMapBarWidthDays, updateMapsBarDuration } from '../redux/slices/graphSlice'; -import translate from '../utils/translate'; +import IntervalControlsComponent from './IntervalControlsComponent'; import MapChartSelectComponent from './MapChartSelectComponent'; -import TooltipMarkerComponent from './TooltipMarkerComponent'; + /** - * @returns Map page controls + * @returns controls for map page. */ export default function MapControlsComponent() { - const dispatch = useAppDispatch(); - const barDuration = useAppSelector(selectMapBarWidthDays); - - const handleDurationChange = (value: number) => { - dispatch(updateMapsBarDuration(moment.duration(value, 'days'))); - }; - - const barDurationDays = barDuration.asDays(); - return (
- -
-

- {translate('map.interval')}: -

- - - - - - -
-
+ {} + {} +
); } - - -const labelStyle: React.CSSProperties = { - fontWeight: 'bold', - margin: 0 -}; - -const zIndexFix: React.CSSProperties = { - zIndex: 0 -}; diff --git a/src/client/app/components/MeterAndGroupSelectComponent.tsx b/src/client/app/components/MeterAndGroupSelectComponent.tsx index 52d1e9f7d..67f0b8015 100644 --- a/src/client/app/components/MeterAndGroupSelectComponent.tsx +++ b/src/client/app/components/MeterAndGroupSelectComponent.tsx @@ -16,7 +16,7 @@ import { selectMeterGroupSelectData } from '../redux/selectors/uiSelectors'; import { selectChartToRender, updateSelectedMetersOrGroups, updateThreeDMeterOrGroupInfo } from '../redux/slices/graphSlice'; import { GroupedOption, SelectOption } from '../types/items'; import { ChartTypes, MeterOrGroup } from '../types/redux/graph'; -import translate from '../utils/translate'; +import { useTranslate } from '../redux/componentHooks'; import TooltipMarkerComponent from './TooltipMarkerComponent'; import { selectAnythingFetching } from '../redux/selectors/apiSelectors'; /** @@ -25,6 +25,7 @@ import { selectAnythingFetching } from '../redux/selectors/apiSelectors'; * @returns A React-Select component. */ export default function MeterAndGroupSelectComponent(props: MeterAndGroupSelectProps) { + const translate = useTranslate(); const dispatch = useAppDispatch(); const { meterGroupedOptions, groupsGroupedOptions, allSelectedMeterValues, allSelectedGroupValues } = useAppSelector(selectMeterGroupSelectData); const somethingIsFetching = useAppSelector(selectAnythingFetching); diff --git a/src/client/app/components/MoreOptionsComponent.tsx b/src/client/app/components/MoreOptionsComponent.tsx index 4441d882c..bdc860ab3 100644 --- a/src/client/app/components/MoreOptionsComponent.tsx +++ b/src/client/app/components/MoreOptionsComponent.tsx @@ -15,13 +15,14 @@ import DateRangeComponent from './DateRangeComponent'; import ErrorBarComponent from './ErrorBarComponent'; import ExportComponent from '../components/ExportComponent'; import GraphicRateMenuComponent from './GraphicRateMenuComponent'; -import translate from '../utils/translate'; +import { useTranslate } from '../redux/componentHooks'; /** * Modal popup control for various graph types * @returns Custom Modal depending on selected graph type */ export default function MoreOptionsComponent() { + const translate = useTranslate(); const chartToRender = useAppSelector(selectChartToRender); const [showModal, setShowModal] = useState(false); const handleShow = () => setShowModal(true); diff --git a/src/client/app/components/MultiCompareChartComponent.tsx b/src/client/app/components/MultiCompareChartComponent.tsx index 43f60deb4..3e0427ca4 100644 --- a/src/client/app/components/MultiCompareChartComponent.tsx +++ b/src/client/app/components/MultiCompareChartComponent.tsx @@ -14,6 +14,8 @@ import { useAppSelector } from '../redux/reduxHooks'; import { selectCompareChartQueryArgs } from '../redux/selectors/chartQuerySelectors'; import { SortingOrder } from '../utils/calculateCompare'; import { AreaUnitType } from '../utils/getAreaUnitConversion'; +import { selectSelectedLanguage } from '../redux/slices/appStateSlice'; +import { LanguageTypes } from 'types/redux/i18n'; /** * Component that defines compare chart @@ -28,6 +30,7 @@ export default function MultiCompareChartComponent() { const sortingOrder = useAppSelector(selectSortingOrder); const selectedMeters = useAppSelector(selectSelectedMeters); const selectedGroups = useAppSelector(selectSelectedGroups); + const locale = useAppSelector(selectSelectedLanguage); const meterDataByID = useAppSelector(selectMeterDataById); const groupDataById = useAppSelector(selectGroupDataById); @@ -74,7 +77,7 @@ export default function MultiCompareChartComponent() { } }); - selectedCompareEntities = sortIDs(selectedCompareEntities, sortingOrder); + selectedCompareEntities = sortIDs(selectedCompareEntities, sortingOrder, locale); // Compute how much space should be used in the bootstrap grid system @@ -139,22 +142,13 @@ function calculateChange(currentPeriodUsage: number, usedToThisPointLastTimePeri /** * @param ids An array of items being compared that contain but are more than the id * @param sortingOrder The desired order or the comparison items based on change + * @param locale Current language selected from Redux state * @returns An array of items being compared in desired sortingOrder */ -function sortIDs(ids: CompareEntity[], sortingOrder: SortingOrder): CompareEntity[] { +function sortIDs(ids: CompareEntity[], sortingOrder: SortingOrder, locale: LanguageTypes): CompareEntity[] { switch (sortingOrder) { case SortingOrder.Alphabetical: - ids.sort((a, b) => { - const identifierA = a.identifier.toLowerCase(); - const identifierB = b.identifier.toLowerCase(); - if (identifierA < identifierB) { - return -1; - } - if (identifierA > identifierB) { - return 1; - } - return 0; - }); + ids.sort((idA, idB) => idA.identifier.toLowerCase().localeCompare(idB.identifier.toLowerCase(), locale, { sensitivity: 'accent' })); break; case SortingOrder.Ascending: ids.sort((a, b) => { diff --git a/src/client/app/components/RadarChartComponent.tsx b/src/client/app/components/RadarChartComponent.tsx index 9779ffa57..dc320b5ee 100644 --- a/src/client/app/components/RadarChartComponent.tsx +++ b/src/client/app/components/RadarChartComponent.tsx @@ -14,22 +14,24 @@ import { readingsApi } from '../redux/api/readingsApi'; import { selectUnitDataById } from '../redux/api/unitsApi'; import { useAppSelector } from '../redux/reduxHooks'; import { selectRadarChartQueryArgs } from '../redux/selectors/chartQuerySelectors'; +import { selectScalingFromEntity } from '../redux/selectors/entitySelectors'; import { selectAreaUnit, selectGraphAreaNormalization, selectLineGraphRate, selectSelectedGroups, selectSelectedMeters, selectSelectedUnit } from '../redux/slices/graphSlice'; import { DataType } from '../types/Datasources'; import Locales from '../types/locales'; -import { AreaUnitType, getAreaUnitConversion } from '../utils/getAreaUnitConversion'; +import { AreaUnitType } from '../utils/getAreaUnitConversion'; import getGraphColor from '../utils/getGraphColor'; import { lineUnitLabel } from '../utils/graphics'; -import { useTranslate } from 'redux/componentHooks'; +import { useTranslate } from '../redux/componentHooks'; import SpinnerComponent from './SpinnerComponent'; /** * @returns radar plotly component */ export default function RadarChartComponent() { + const translate = useTranslate(); const { meterArgs, groupArgs, meterShouldSkip, groupShouldSkip } = useAppSelector(selectRadarChartQueryArgs); const { data: meterReadings, isLoading: meterIsLoading } = readingsApi.useLineQuery(meterArgs, { skip: meterShouldSkip }); const { data: groupData, isLoading: groupIsLoading } = readingsApi.useLineQuery(groupArgs, { skip: groupShouldSkip }); @@ -47,8 +49,6 @@ export default function RadarChartComponent() { const meterDataById = useAppSelector(selectMeterDataById); const groupDataById = useAppSelector(selectGroupDataById); - const translate = useTranslate(); - if (meterIsLoading || groupIsLoading) { return ; // return @@ -81,17 +81,13 @@ export default function RadarChartComponent() { // Add all valid data from existing meters to the radar plot for (const meterID of selectedMeters) { if (meterReadings) { - const meterArea = meterDataById[meterID].area; + const entity = meterDataById[meterID]; // We either don't care about area, or we do in which case there needs to be a nonzero area. - if (!areaNormalization || (meterArea > 0 && meterDataById[meterID].areaUnit != AreaUnitType.none)) { - // Convert the meter area into the proper unit if normalizing by area or use 1 if not so won't change reading values. - const areaScaling = areaNormalization ? - meterArea * getAreaUnitConversion(meterDataById[meterID].areaUnit, selectedAreaUnit) : 1; - // Divide areaScaling into the rate so have complete scaling factor for readings. - const scaling = rateScaling / areaScaling; + if (!areaNormalization || (entity.area > 0 && entity.areaUnit != AreaUnitType.none)) { + const scaling = selectScalingFromEntity(entity, selectedAreaUnit, areaNormalization, rateScaling); const readingsData = meterReadings[meterID]; if (readingsData) { - const label = meterDataById[meterID].identifier; + const label = entity.identifier; const colorID = meterID; // TODO If we are sure the data is always defined then remove this commented out code. // Be consistent for all graphing and groups below. @@ -142,17 +138,13 @@ export default function RadarChartComponent() { for (const groupID of selectedGroups) { // const byGroupID = state.readings.line.byGroupID[groupID]; if (groupData) { - const groupArea = groupDataById[groupID].area; + const entity = groupDataById[groupID]; // We either don't care about area, or we do in which case there needs to be a nonzero area. - if (!areaNormalization || (groupArea > 0 && groupDataById[groupID].areaUnit != AreaUnitType.none)) { - // Convert the group area into the proper unit if normalizing by area or use 1 if not so won't change reading values. - const areaScaling = areaNormalization ? - groupArea * getAreaUnitConversion(groupDataById[groupID].areaUnit, selectedAreaUnit) : 1; - // Divide areaScaling into the rate so have complete scaling factor for readings. - const scaling = rateScaling / areaScaling; + if (!areaNormalization || (entity.area > 0 && entity.areaUnit != AreaUnitType.none)) { + const scaling = selectScalingFromEntity(entity, selectedAreaUnit, areaNormalization, rateScaling); const readingsData = groupData[groupID]; if (readingsData) { - const label = groupDataById[groupID].name; + const label = entity.name; const colorID = groupID; // if (readingsData.readings === undefined) { // throw new Error('Unacceptable condition: readingsData.readings is undefined.'); diff --git a/src/client/app/components/ReadingsPerDaySelectComponent.tsx b/src/client/app/components/ReadingsPerDaySelectComponent.tsx index a75d6a513..40a56cbd0 100644 --- a/src/client/app/components/ReadingsPerDaySelectComponent.tsx +++ b/src/client/app/components/ReadingsPerDaySelectComponent.tsx @@ -10,7 +10,7 @@ import { selectThreeDQueryArgs } from '../redux/selectors/chartQuerySelectors'; import { selectReadingsPerDaySelectData } from '../redux/selectors/threeDSelectors'; import { selectThreeDReadingInterval, updateThreeDReadingInterval } from '../redux/slices/graphSlice'; import { ReadingInterval } from '../types/redux/graph'; -import translate from '../utils/translate'; +import { useTranslate } from '../redux/componentHooks'; import TooltipMarkerComponent from './TooltipMarkerComponent'; /** @@ -18,6 +18,7 @@ import TooltipMarkerComponent from './TooltipMarkerComponent'; * @returns A Select menu with Readings per day options. */ export default function ReadingsPerDaySelect() { + const translate = useTranslate(); const dispatch = useAppDispatch(); const readingInterval = useAppSelector(selectThreeDReadingInterval); const { args, shouldSkipQuery } = useAppSelector(selectThreeDQueryArgs); diff --git a/src/client/app/components/RouteComponent.tsx b/src/client/app/components/RouteComponent.tsx index 5edad7795..cdf4389a3 100644 --- a/src/client/app/components/RouteComponent.tsx +++ b/src/client/app/components/RouteComponent.tsx @@ -4,7 +4,8 @@ import * as React from 'react'; import { IntlProvider } from 'react-intl'; import { RouterProvider, createBrowserRouter } from 'react-router-dom'; -import UploadCSVContainer from '../containers/csv/UploadCSVContainer'; +import ReadingsCSVUploadComponent from '../components/csv/ReadingsCSVUploadComponent'; +import MetersCSVUploadComponent from '../components/csv/MetersCSVUploadComponent'; import MapCalibrationContainer from '../containers/maps/MapCalibrationContainer'; import MapsDetailContainer from '../containers/maps/MapsDetailContainer'; import { useAppSelector } from '../redux/reduxHooks'; @@ -12,7 +13,6 @@ import LocaleTranslationData from '../translations/data'; import { UserRole } from '../types/items'; import AppLayout from './AppLayout'; import HomeComponent from './HomeComponent'; -import LoginComponent from './LoginComponent'; import AdminComponent from './admin/AdminComponent'; import UsersDetailComponent from './admin/users/UsersDetailComponent'; import ConversionsDetailComponent from './conversion/ConversionsDetailComponent'; @@ -25,6 +25,7 @@ import RoleOutlet from './router/RoleOutlet'; import UnitsDetailComponent from './unit/UnitsDetailComponent'; import ErrorComponent from './router/ErrorComponent'; import { selectSelectedLanguage } from '../redux/slices/appStateSlice'; +import VisualUnitDetailComponent from './visual-unit/VisualUnitDetailComponent'; /** * @returns the router component Responsible for client side routing. @@ -46,7 +47,6 @@ const router = createBrowserRouter([ path: '/', element: , errorElement: , children: [ { index: true, element: }, - { path: 'login', element: }, { path: 'groups', element: }, { path: 'meters', element: }, { path: 'graph', element: }, @@ -55,19 +55,21 @@ const router = createBrowserRouter([ children: [ { path: 'admin', element: }, { path: 'calibration', element: }, + { path: 'conversions', element: }, + { path: 'csvMeters', element: }, { path: 'maps', element: }, { path: 'units', element: }, - { path: 'conversions', element: }, - { path: 'users', element: } + { path: 'users', element: }, + { path: 'visual-unit', element: } ] }, { element: , children: [ - { path: 'csv', element: } + { path: 'csvReadings', element: } ] }, { path: '*', element: } ] } -]); \ No newline at end of file +]); diff --git a/src/client/app/components/ThreeDComponent.tsx b/src/client/app/components/ThreeDComponent.tsx index 358e34ade..2310e8671 100644 --- a/src/client/app/components/ThreeDComponent.tsx +++ b/src/client/app/components/ThreeDComponent.tsx @@ -11,6 +11,7 @@ import { selectUnitDataById } from '../redux/api/unitsApi'; import { useAppSelector } from '../redux/reduxHooks'; import { selectThreeDQueryArgs } from '../redux/selectors/chartQuerySelectors'; import { selectThreeDComponentInfo } from '../redux/selectors/threeDSelectors'; +import { selectScalingFromEntity } from '../redux/selectors/entitySelectors'; import { selectGraphState } from '../redux/slices/graphSlice'; import { ThreeDReading } from '../types/readings'; import { GraphState, MeterOrGroup } from '../types/redux/graph'; @@ -18,9 +19,12 @@ import { GroupDataByID } from '../types/redux/groups'; import { MeterDataByID } from '../types/redux/meters'; import { UnitDataById } from '../types/redux/units'; import { isValidThreeDInterval, roundTimeIntervalForFetch } from '../utils/dateRangeCompatibility'; -import { AreaUnitType, getAreaUnitConversion } from '../utils/getAreaUnitConversion'; +import { AreaUnitType } from '../utils/getAreaUnitConversion'; import { lineUnitLabel } from '../utils/graphics'; -import { useTranslate } from 'redux/componentHooks'; +// Both translates are used since some are in the function component where the React Hook is okay +// and some are in other functions where the older method is needed. +import { useTranslate } from '../redux/componentHooks'; +import translate from '../utils/translate'; import SpinnerComponent from './SpinnerComponent'; import ThreeDPillComponent from './ThreeDPillComponent'; import Plot from 'react-plotly.js'; @@ -33,6 +37,7 @@ import Locales from '../types/locales'; * @returns 3D Plotly 3D Surface Graph */ export default function ThreeDComponent() { + const translate = useTranslate(); const { args, shouldSkipQuery } = useAppSelector(selectThreeDQueryArgs); const { data, isFetching } = readingsApi.endpoints.threeD.useQuery(args, { skip: shouldSkipQuery }); const meterDataById = useAppSelector(selectMeterDataById); @@ -41,7 +46,6 @@ export default function ThreeDComponent() { const graphState = useAppSelector(selectGraphState); const locale = useAppSelector(selectSelectedLanguage); const { meterOrGroupID, meterOrGroupName, isAreaCompatible } = useAppSelector(selectThreeDComponentInfo); - const translate = useTranslate(); // Initialize Default values const threeDData = data; let layout = {}; @@ -127,7 +131,6 @@ function formatThreeDData( graphState: GraphState, unitDataById: UnitDataById ) { - const translate = useTranslate(); // Initialize Plotly Data const xDataToRender: string[] = []; @@ -153,23 +156,13 @@ function formatThreeDData( // The rate will be 1 if it is per hour (since state readings are per hour) or no rate scaling so no change. const rateScaling = needsRateScaling ? currentSelectedRate.rate : 1; - const meterArea = meterOrGroup === MeterOrGroup.meters ? - meterDataById[selectedMeterOrGroupID].area + const entity = meterOrGroup === MeterOrGroup.meters ? + meterDataById[selectedMeterOrGroupID] : - groupDataById[selectedMeterOrGroupID].area; + groupDataById[selectedMeterOrGroupID]; + const scaling = selectScalingFromEntity(entity, graphState.selectedAreaUnit, graphState.areaNormalization, rateScaling); - const areaUnit = meterOrGroup === MeterOrGroup.meters ? - meterDataById[selectedMeterOrGroupID].areaUnit - : - groupDataById[selectedMeterOrGroupID].areaUnit; - - // We either don't care about area, or we do in which case there needs to be a nonzero area. - if (!graphState.areaNormalization || (meterArea > 0 && areaUnit != AreaUnitType.none)) { - // Convert the meter area into the proper unit if normalizing by area or use 1 if not so won't change reading values. - const areaScaling = graphState.areaNormalization ? - meterArea * getAreaUnitConversion(areaUnit, graphState.selectedAreaUnit) : 1; - // Divide areaScaling into the rate so have complete scaling factor for readings. - const scaling = rateScaling / areaScaling; + if (!graphState.areaNormalization || (entity.area > 0 && entity.areaUnit != AreaUnitType.none)) { zDataToRender = data.zData.map(day => day.map(reading => reading === null ? null : reading * scaling)); } } @@ -251,7 +244,6 @@ function setThreeDLayout(zLabelText: string = 'Resource Usage', yDataToRender: s const dataMin = Math.min(...dateObjects.map(date => date.getTime())); const dataMax = Math.max(...dateObjects.map(date => date.getTime())); const dataRange = dataMax - dataMin; - const translate = useTranslate(); //Calculate nTicks for small num of days on y-axis; possibly a better way let nTicks, dTick = 'd1'; diff --git a/src/client/app/components/ThreeDPillComponent.tsx b/src/client/app/components/ThreeDPillComponent.tsx index a5c7b8ae2..2d19df5a6 100644 --- a/src/client/app/components/ThreeDPillComponent.tsx +++ b/src/client/app/components/ThreeDPillComponent.tsx @@ -3,6 +3,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import * as React from 'react'; +import { useEffect } from 'react'; import { Badge } from 'reactstrap'; import { selectGraphState, selectThreeDState, updateThreeDMeterOrGroupInfo } from '../redux/slices/graphSlice'; import { selectGroupDataById } from '../redux/api/groupsApi'; @@ -10,13 +11,14 @@ import { useAppDispatch, useAppSelector } from '../redux/reduxHooks'; import { MeterOrGroup, MeterOrGroupPill } from '../types/redux/graph'; import { AreaUnitType } from '../utils/getAreaUnitConversion'; import { selectMeterDataById } from '../redux/api/metersApi'; -import translate from '../utils/translate'; +import { useTranslate } from '../redux/componentHooks'; /** * A component used in the threeD graphics to select a single meter from the currently selected meters and groups. * @returns List of selected groups and meters as reactstrap Pills Badges */ export default function ThreeDPillComponent() { + const translate = useTranslate(); const dispatch = useAppDispatch(); const meterDataById = useAppSelector(selectMeterDataById); const groupDataById = useAppSelector(selectGroupDataById); @@ -40,6 +42,19 @@ export default function ThreeDPillComponent() { return { meterOrGroupID: groupID, isDisabled: isDisabled, meterOrGroup: MeterOrGroup.groups } as MeterOrGroupPill; }); + // when there is only one choice, it must be selected as a default (there is no other option) + useEffect(() => { + const combinedPillData = [...meterPillData, ...groupPillData]; + + if (combinedPillData.length === 1) { + const singlePill = combinedPillData[0]; + dispatch(updateThreeDMeterOrGroupInfo({ + meterOrGroupID: singlePill.meterOrGroupID, + meterOrGroup: singlePill.meterOrGroup + })); + } + }, [meterPillData, groupPillData]); + // When a Pill Badge is clicked update threeD state to indicate new meter or group to render. const handlePillClick = (pillData: MeterOrGroupPill) => dispatch( updateThreeDMeterOrGroupInfo({ diff --git a/src/client/app/components/TimeZoneSelect.tsx b/src/client/app/components/TimeZoneSelect.tsx index b8dd891b6..9abc63b95 100644 --- a/src/client/app/components/TimeZoneSelect.tsx +++ b/src/client/app/components/TimeZoneSelect.tsx @@ -5,7 +5,7 @@ import * as React from 'react'; import Select from 'react-select'; import { TimeZoneOption } from 'types/timezone'; -import translate from '../utils/translate'; +import { useTranslate } from '../redux/componentHooks'; import * as moment from 'moment-timezone'; interface TimeZoneSelectProps { @@ -15,7 +15,7 @@ interface TimeZoneSelectProps { } const TimeZoneSelect: React.FC = ({ current, handleClick }) => { - + const translate = useTranslate(); const getTimeZones = () => { const zoneNames = moment.tz.names(); return zoneNames.map(zoneName => { diff --git a/src/client/app/components/TooltipHelpComponent.tsx b/src/client/app/components/TooltipHelpComponent.tsx index 6f446a65e..f48de775e 100644 --- a/src/client/app/components/TooltipHelpComponent.tsx +++ b/src/client/app/components/TooltipHelpComponent.tsx @@ -9,7 +9,7 @@ import { selectOEDVersion } from '../redux/api/versionApi'; import { useAppSelector } from '../redux/reduxHooks'; import { selectHelpUrl } from '../redux/slices/adminSlice'; import '../styles/tooltip.css'; -import translate from '../utils/translate'; +import { useTranslate } from '../redux/componentHooks'; interface TooltipHelpProps { page: string; // Specifies which page the tip is in. @@ -20,11 +20,10 @@ interface TooltipHelpProps { * @returns ToolTipHelpComponent */ export default function TooltipHelpComponent(props: TooltipHelpProps) { - /** * @returns JSX to create the help icons with links */ - + const translate = useTranslate(); const version = useAppSelector(selectOEDVersion); const helpUrl = useAppSelector(selectHelpUrl); @@ -44,8 +43,9 @@ export default function TooltipHelpComponent(props: TooltipHelpProps) { 'help.admin.unitcreate': { link: `${helpUrl}/adminUnitCreating/` }, 'help.admin.unitedit': { link: `${helpUrl}/adminUnitEditing/` }, 'help.admin.unitview': { link: `${helpUrl}/adminUnitViewing/` }, - 'help.admin.user': { link: `${helpUrl}/adminUser/` }, - 'help.csv.header': { link: `${helpUrl}/adminDataAcquisition/` }, + 'help.admin.users': { link: `${helpUrl}/adminUser/` }, + 'help.csv.meters': { link: `${helpUrl}/adminMetersImport/` }, + 'help.csv.readings': { link: `${helpUrl}/adminReadingsImport/` }, 'help.home.area.normalize': { link: `${helpUrl}/areaNormalization/` }, 'help.home.bar.days.tip': { link: `${helpUrl}/barGraphic/#usage` }, 'help.home.bar.interval.tip': { link: `${helpUrl}/barGraphic/#usage` }, @@ -53,7 +53,7 @@ export default function TooltipHelpComponent(props: TooltipHelpProps) { 'help.home.chart.plotly.controls': { link: 'https://plotly.com/chart-studio-help/getting-to-know-the-plotly-modebar/' }, 'help.home.chart.redraw.restore': { link: `${helpUrl}/lineGraphic/#redrawRestore` }, 'help.home.chart.select': { link: `${helpUrl}/graphType/` }, - 'help.home.compare.interval.tip': { link: `${helpUrl}/compareGraphic/#usage` }, + 'help.home.compare.period.tip': { link: `${helpUrl}/compareGraphic/#usage` }, 'help.home.compare.sort.tip': { link: `${helpUrl}/compareGraphic/#usage` }, 'help.home.error.bar': { link: `${helpUrl}/errorBar/#usage` }, 'help.home.export.graph.data': { link: `${helpUrl}/export/` }, @@ -70,7 +70,8 @@ export default function TooltipHelpComponent(props: TooltipHelpProps) { 'help.home.toggle.chart.link': { link: `${helpUrl}/chartLink/` }, 'help.groups.groupdetails': { link: `${helpUrl}/groupViewing/#groupDetails` }, 'help.groups.groupview': { link: `${helpUrl}/groupViewing/` }, - 'help.meters.meterview': { link: `${helpUrl}/meterViewing/` } + 'help.meters.meterview': { link: `${helpUrl}/meterViewing/` }, + 'help.admin.unitconversionvisuals': { link: `${helpUrl}/adminUnitVisual/` } }; return ( diff --git a/src/client/app/components/UnitSelectComponent.tsx b/src/client/app/components/UnitSelectComponent.tsx index aee27bdbc..f07e478de 100644 --- a/src/client/app/components/UnitSelectComponent.tsx +++ b/src/client/app/components/UnitSelectComponent.tsx @@ -11,7 +11,7 @@ import { GroupedOption, SelectOption } from '../types/items'; // import { FormattedMessage } from 'react-intl'; import { Badge } from 'reactstrap'; import { graphSlice, selectSelectedUnit } from '../redux/slices/graphSlice'; -import translate from '../utils/translate'; +import { useTranslate } from '../redux/componentHooks'; import TooltipMarkerComponent from './TooltipMarkerComponent'; import { selectUnitDataById, unitsApi } from '../redux/api/unitsApi'; @@ -19,6 +19,7 @@ import { selectUnitDataById, unitsApi } from '../redux/api/unitsApi'; * @returns A React-Select component for UI Options Panel */ export default function UnitSelectComponent() { + const translate = useTranslate(); const dispatch = useAppDispatch(); const unitSelectOptions = useAppSelector(selectUnitSelectData); const selectedUnitID = useAppSelector(selectSelectedUnit); diff --git a/src/client/app/components/UnsavedWarningComponent.tsx b/src/client/app/components/UnsavedWarningComponent.tsx index 651c94eb4..eb5bcd3d0 100644 --- a/src/client/app/components/UnsavedWarningComponent.tsx +++ b/src/client/app/components/UnsavedWarningComponent.tsx @@ -10,7 +10,7 @@ import { useBlocker } from 'react-router-dom'; import { Button, Modal, ModalBody, ModalFooter } from 'reactstrap'; import { LocaleDataKey } from '../translations/data'; import { showErrorNotification, showSuccessNotification } from '../utils/notifications'; -import translate from '../utils/translate'; +import { useTranslate } from '../redux/componentHooks'; export interface UnsavedWarningProps { changes: any; @@ -25,20 +25,19 @@ export interface UnsavedWarningProps { * @returns Component that prompts before navigating away from current page */ export function UnsavedWarningComponent(props: UnsavedWarningProps) { + const translate = useTranslate(); const { hasUnsavedChanges, submitChanges, changes } = props; const blocker = useBlocker(hasUnsavedChanges); const handleSubmit = async () => { submitChanges(changes) .unwrap() .then(() => { - //TODO translate me showSuccessNotification(translate('unsaved.success')); if (blocker.state === 'blocked') { blocker.proceed(); } }) .catch(() => { - //TODO translate me showErrorNotification(translate('unsaved.failure')); if (blocker.state === 'blocked') { blocker.proceed(); diff --git a/src/client/app/components/admin/PreferencesComponent.tsx b/src/client/app/components/admin/PreferencesComponent.tsx index 5a5de41ec..ffc2717cb 100644 --- a/src/client/app/components/admin/PreferencesComponent.tsx +++ b/src/client/app/components/admin/PreferencesComponent.tsx @@ -13,7 +13,7 @@ import { ChartTypes } from '../../types/redux/graph'; import { LanguageTypes } from '../../types/redux/i18n'; import { AreaUnitType } from '../../utils/getAreaUnitConversion'; import { showErrorNotification, showSuccessNotification } from '../../utils/notifications'; -import translate from '../../utils/translate'; +import { useTranslate } from '../../redux/componentHooks'; import TimeZoneSelect from '../TimeZoneSelect'; import { defaultAdminState } from '../../redux/slices/adminSlice'; @@ -23,6 +23,7 @@ import { defaultAdminState } from '../../redux/slices/adminSlice'; * @returns Preferences Component for Administrative use */ export default function PreferencesComponent() { + const translate = useTranslate(); const { data: adminPreferences = defaultAdminState } = preferencesApi.useGetPreferencesQuery(); const [localAdminPref, setLocalAdminPref] = React.useState(cloneDeep(adminPreferences)); const [submitPreferences] = preferencesApi.useSubmitPreferencesMutation(); @@ -38,6 +39,10 @@ export default function PreferencesComponent() { setLocalAdminPref({ ...localAdminPref, [key]: value }); }; + const discardChanges = () => { + setLocalAdminPref(cloneDeep(adminPreferences)); + }; + return (
makeLocalChanges('defaultHelpUrl', e.target.value)} />
- - +
+ + +
); } diff --git a/src/client/app/components/admin/users/CreateUserModalComponent.tsx b/src/client/app/components/admin/users/CreateUserModalComponent.tsx index f2a875b4e..0594c73ff 100644 --- a/src/client/app/components/admin/users/CreateUserModalComponent.tsx +++ b/src/client/app/components/admin/users/CreateUserModalComponent.tsx @@ -11,7 +11,7 @@ import { import { userApi } from '../../../redux/api/userApi'; import { User, UserRole, userDefaults } from '../../../types/items'; import { showErrorNotification, showSuccessNotification } from '../../../utils/notifications'; -import translate from '../../../utils/translate'; +import { useTranslate } from '../../../redux/componentHooks'; import TooltipHelpComponent from '../../TooltipHelpComponent'; import TooltipMarkerComponent from '../../TooltipMarkerComponent'; import { tooltipBaseStyle } from '../../../styles/modalStyle'; @@ -21,7 +21,7 @@ import { tooltipBaseStyle } from '../../../styles/modalStyle'; * @returns CreateUserModal component */ export default function CreateUserModal() { - + const translate = useTranslate(); // create user form state and use the defaults const [userDetails, setUserDetails] = useState(userDefaults); diff --git a/src/client/app/components/admin/users/EditUserModalComponent.tsx b/src/client/app/components/admin/users/EditUserModalComponent.tsx index 2048c2bfc..4568b2704 100644 --- a/src/client/app/components/admin/users/EditUserModalComponent.tsx +++ b/src/client/app/components/admin/users/EditUserModalComponent.tsx @@ -10,7 +10,7 @@ import { useAppSelector } from '../../../redux/reduxHooks'; import { selectCurrentUserProfile } from '../../../redux/slices/currentUserSlice'; import { User, UserRole, userDefaults } from '../../../types/items'; import { showErrorNotification, showSuccessNotification } from '../../../utils/notifications'; -import translate from '../../../utils/translate'; +import { useTranslate } from '../../../redux/componentHooks'; import ConfirmActionModalComponent from '../../ConfirmActionModalComponent'; import TooltipHelpComponent from '../../TooltipHelpComponent'; import TooltipMarkerComponent from '../../TooltipMarkerComponent'; @@ -29,7 +29,7 @@ interface EditUserModalComponentProps { * @returns User edit element */ export default function EditUserModalComponent(props: EditUserModalComponentProps) { - + const translate = useTranslate(); // get current logged in user const currentLoggedInUser = useAppSelector(selectCurrentUserProfile) as User; diff --git a/src/client/app/components/admin/users/UserViewComponent.tsx b/src/client/app/components/admin/users/UserViewComponent.tsx index 24650438f..0420c3032 100644 --- a/src/client/app/components/admin/users/UserViewComponent.tsx +++ b/src/client/app/components/admin/users/UserViewComponent.tsx @@ -8,7 +8,7 @@ import { useState } from 'react'; import { Button } from 'reactstrap'; import '../../../styles/card-page.css'; import { User } from '../../../types/items'; -import translate from '../../../utils/translate'; +import { useTranslate } from '../../../redux/componentHooks'; import EditUserModalComponent from './EditUserModalComponent'; interface UserViewComponentProps { @@ -21,6 +21,7 @@ interface UserViewComponentProps { * @returns User card element */ export default function UserViewComponent(props: UserViewComponentProps) { + const translate = useTranslate(); const [showEditModal, setShowEditModal] = useState(false); const handleShow = () => { diff --git a/src/client/app/components/admin/users/UsersDetailComponent.tsx b/src/client/app/components/admin/users/UsersDetailComponent.tsx index ff5a8742e..3d64cb60c 100644 --- a/src/client/app/components/admin/users/UsersDetailComponent.tsx +++ b/src/client/app/components/admin/users/UsersDetailComponent.tsx @@ -5,11 +5,13 @@ import * as React from 'react'; import { Col, Container, Row } from 'reactstrap'; import { stableEmptyUsers, userApi } from '../../../redux/api/userApi'; -import translate from '../../../utils/translate'; +import { useTranslate } from '../../../redux/componentHooks'; import TooltipHelpComponent from '../../TooltipHelpComponent'; import TooltipMarkerComponent from '../../TooltipMarkerComponent'; import CreateUserModalComponent from './CreateUserModalComponent'; import UserViewComponent from './UserViewComponent'; +import { selectSelectedLanguage } from '../../../redux/slices/appStateSlice'; +import { useAppSelector } from '../../../redux/reduxHooks'; const tooltipStyle = { display: 'inline-block', @@ -22,6 +24,8 @@ const tooltipStyle = { * @returns User Detail element */ export default function UserDetailComponent() { + const locale = useAppSelector(selectSelectedLanguage); + const translate = useTranslate(); const { data: users = stableEmptyUsers } = userApi.useGetUsersQuery(); return ( @@ -41,7 +45,7 @@ export default function UserDetailComponent() { {// display users and sort by username alphabetically [...users] - .sort((a, b) => a.username.localeCompare(b.username)) + .sort((a, b) => a.username.localeCompare(b.username, locale, { sensitivity : 'accent' })) .map(user => ( - ((unitDataById[conversionA.sourceId]?.identifier + unitDataById[conversionA.destinationId]?.identifier).toLowerCase() > - (unitDataById[conversionB.sourceId]?.identifier + unitDataById[conversionB.destinationId]?.identifier).toLowerCase()) ? 1 : - (((unitDataById[conversionB.sourceId]?.identifier + unitDataById[conversionB.destinationId]?.identifier).toLowerCase() > - (unitDataById[conversionA.sourceId]?.identifier + unitDataById[conversionA.destinationId]?.identifier).toLowerCase()) ? -1 : 0)) + ((unitDataById[conversionA.sourceId]?.identifier + unitDataById[conversionA.destinationId]?.identifier).toLowerCase().localeCompare(( + unitDataById[conversionB.sourceId]?.identifier + unitDataById[conversionB.destinationId]?.identifier).toLowerCase(), locale, + { sensitivity: 'accent'}))) .map(conversionData => ( { - return { - resetApiCache: () => dispatch(baseApi.util.invalidateTags(['MeterData'])) +import TooltipHelpComponent from '../TooltipHelpComponent'; +import TooltipMarkerComponent from '../TooltipMarkerComponent'; +import { useAppDispatch, useAppSelector } from '../../redux/reduxHooks'; +import { authApi, authPollInterval } from '../../redux/api/authApi'; +import { selectIsAdmin } from '../../redux/slices/currentUserSlice'; +import { selectVisibleMeterAndGroupData } from '../../redux/selectors/adminSelectors'; +import SpinnerComponent from '../SpinnerComponent'; + +/** + * Defines the CSV Meters page + * @returns CSV Meters page element + */ +export default function MetersCSVUploadComponent() { + const translate = useTranslate(); + const [meterData, setMeterData] = React.useState(MetersCSVUploadDefaults); + const [selectedFile, setSelectedFile] = React.useState(null); + const [isValidFileType, setIsValidFileType] = React.useState(false); + // tracks if should show spinner (true while loading data, false otherwise) + const [showSpinner, setShowSpinner] = React.useState(false); + const dispatch = useAppDispatch(); + // Check for admin status + const isAdmin = useAppSelector(selectIsAdmin); + // page may contain admin info so verify admin status while admin is authenticated. + authApi.useTokenPollQuery(undefined, { skip: !isAdmin, pollingInterval: authPollInterval }); + // We only want displayable meters if non-admins because they still have + // non-displayable in state. + const { visibleMeters } = useAppSelector(selectVisibleMeterAndGroupData); + // tracks whether or not a meter has been selected + const meterIsSelected = meterData.meterIdentifier !== ''; + + const handleSelectedMeterChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + setMeterData(prevData => ({ + ...prevData, + [name]: value + })); }; -}; -type ResetProp = { resetApiCache: () => void } -type MetersCsvUploadPropWithCacheDispatch = MetersCSVUploadProps & ResetProp -class MetersCSVUploadComponent extends React.Component { - private fileInput: React.RefObject; - constructor(props: MetersCsvUploadPropWithCacheDispatch) { - super(props); - this.handleSubmit = this.handleSubmit.bind(this); - this.handleSetMeterName = this.handleSetMeterName.bind(this); - this.fileInput = React.createRef(); - } + const handleCheckboxChange = (e: React.ChangeEvent) => { + const { name, checked } = e.target; + setMeterData(prevData => ({ + ...prevData, + [name]: checked + })); + }; - private async handleSubmit(e: React.MouseEvent) { - try { - e.preventDefault(); - const current = this.fileInput.current as HTMLInputElement; - const { files } = current; - if (files && (files as FileList).length !== 0) { - await this.props.submitCSV(files[0]); - // TODO Using an alert is not the best. At some point this should be integrated - // with react. - showSuccessNotification('

SUCCESS

The meter upload was a success.'); - } - } catch (error) { - // A failed axios request should result in an error. - showErrorNotification(error.response.data as string); + const handleFileChange = (file: File) => { + setSelectedFile(file); + if (file.name.slice(-4) === '.csv' || file.name.slice(-3) === '.gz') { + setIsValidFileType(true); + } else { + setIsValidFileType(false); + setSelectedFile(null); + showErrorNotification(translate('csv.file.error') + file.name); } - // Refetch meters details by invalidating its api cache. - this.props.resetApiCache(); + }; - } + const handleClear = () => { + setMeterData(MetersCSVUploadDefaults); + setIsValidFileType(false); + }; - private handleSetMeterName(e: React.ChangeEvent) { - const target = e.target; - this.props.setMeterName(MODE.meters, target.value); - } + const handleSubmit = async (e: React.MouseEvent) => { + e.preventDefault(); + if (selectedFile) { + // show spinner before calling api, then stop it immediately after + setShowSpinner(true); + const { success, message } = await submitMeters(meterData, selectedFile, dispatch); + setShowSpinner(false); + if (success) { + showSuccessNotification(message); + } else { + showErrorNotification(message); + } + } + }; - public render() { - const titleStyle: React.CSSProperties = { - fontWeight: 'bold', - paddingBottom: '5px' - }; + const spinContainerStyle = { + display: 'flex', + justifyContent: 'center' + }; - const checkboxStyle: React.CSSProperties = { - paddingBottom: '15px' - }; + const tooltipStyle = { + display: 'inline-block', + fontSize: '50%', + tooltipReadings: 'help.csv.meters' + }; - const formStyle: React.CSSProperties = { - display: 'flex', - justifyContent: 'center', - padding: '20px' - }; + const checkBox = { + display: 'flex' + }; - return ( -
-
- - - - - - - - - - - - - - - + return ( + + {showSpinner ? ( +
+ +
+ ) : (<> + + + + +
+

+ {translate('csv.upload.meters')} +
+ +
+

+
+ + + + + + + + + + + + + + + + + + + {meterData.update && ( + + + + { + + } + { + Array.from(visibleMeters).map(meter => { + return (); + }) + } + + + )} +
+
+ +
+
+ +
+
+ +
-
- ); - } - -} -export default connect(null, mapDispatchToProps)(MetersCSVUploadComponent); + )} + + ); +} \ No newline at end of file diff --git a/src/client/app/components/csv/ReadingsCSVUploadComponent.tsx b/src/client/app/components/csv/ReadingsCSVUploadComponent.tsx index d99c9112c..9a539b56f 100644 --- a/src/client/app/components/csv/ReadingsCSVUploadComponent.tsx +++ b/src/client/app/components/csv/ReadingsCSVUploadComponent.tsx @@ -2,351 +2,573 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +import { range } from 'lodash'; import * as React from 'react'; -import { FormattedMessage } from 'react-intl'; -import { Button, Col, Form, FormGroup, Input, Label } from 'reactstrap'; -import { MODE } from '../../containers/csv/UploadCSVContainer'; -import { BooleanMeterTypes, ReadingsCSVUploadProps, TimeSortTypes } from '../../types/csvUploadForm'; +import { useEffect, useState } from 'react'; +import { Button, Col, Container, Form, FormGroup, Input, Label, Row } from 'reactstrap'; +import { authApi, authPollInterval } from '../../redux/api/authApi'; +import { useAppDispatch, useAppSelector } from '../../redux/reduxHooks'; +import { selectVisibleMeterAndGroupData } from '../../redux/selectors/adminSelectors'; +import { selectIsAdmin } from '../../redux/slices/currentUserSlice'; +import { ReadingsCSVUploadPreferences } from '../../types/csvUploadForm'; +import { TrueFalseType } from '../../types/items'; +import { MeterData, MeterTimeSortType } from '../../types/redux/meters'; +import { submitReadings } from '../../utils/api/UploadCSVApi'; import { ReadingsCSVUploadDefaults } from '../../utils/csvUploadDefaults'; -import { showErrorNotification, showInfoNotification } from '../../utils/notifications'; -import translate from '../../utils/translate'; +import { showErrorNotification, showSuccessNotification } from '../../utils/notifications'; +import { useTranslate } from '../../redux/componentHooks'; import FormFileUploaderComponent from '../FormFileUploaderComponent'; +import TooltipHelpComponent from '../TooltipHelpComponent'; +import TooltipMarkerComponent from '../TooltipMarkerComponent'; +import CreateMeterModalComponent from '../meters/CreateMeterModalComponent'; +import SpinnerComponent from '../SpinnerComponent'; /** - * Returns a range of values between the specified lower and upper bounds. - * @param lower The lower bound, which will be included in the range. - * @param upper The upper bound, which will be excluded from the range. - * @returns An array of values between starting from the lower bound and up to and excluding the upper bound. + * Defines the CSV Readings page + * @returns CSV Readings page element */ -function range(lower: number, upper: number): number[] { - const arr = []; - for (let i = lower; i < upper; i++) { - arr.push(i); - } - return arr; -} - -export default class ReadingsCSVUploadComponent extends React.Component { - private fileInput: React.RefObject; - constructor(props: ReadingsCSVUploadProps) { - super(props); - this.handleSetMeterName = this.handleSetMeterName.bind(this); - this.handleSetTimeSort = this.handleSetTimeSort.bind(this); - this.handleSetDuplications = this.handleSetDuplications.bind(this); - this.handleSetCumulative = this.handleSetCumulative.bind(this); - this.handleSetCumulativeReset = this.handleSetCumulativeReset.bind(this); - this.handleSetCumulativeResetStart = this.handleSetCumulativeResetStart.bind(this); - this.handleSetCumulativeResetEnd = this.handleSetCumulativeResetEnd.bind(this); - this.handleSetLengthGap = this.handleSetLengthGap.bind(this); - this.handleSetLengthVariation = this.handleSetLengthVariation.bind(this); - this.handleSetEndOnly = this.handleSetEndOnly.bind(this); - this.handleSetHonorDst = this.handleSetHonorDst.bind(this); - this.handleSetRelaxedParsing = this.handleSetRelaxedParsing.bind(this); - this.handleSetUseMeterZone = this.handleSetUseMeterZone.bind(this); - this.handleSubmit = this.handleSubmit.bind(this); - this.fileInput = React.createRef(); - } +export default function ReadingsCSVUploadComponent() { + const translate = useTranslate(); + const dispatch = useAppDispatch(); + // Check for admin status + const isAdmin = useAppSelector(selectIsAdmin); + // page may contain admin info so verify admin status while admin is authenticated. + authApi.useTokenPollQuery(undefined, { skip: !isAdmin, pollingInterval: authPollInterval }); + // We only want displayable meters if non-admins because they still have + // non-displayable in state. + const { visibleMeters } = useAppSelector(selectVisibleMeterAndGroupData); + // This is the state for the form data for readings + const [readingsData, setReadingsData] = useState(ReadingsCSVUploadDefaults); + // This is the state for the file to be uploaded + const [selectedFile, setSelectedFile] = useState(null); + // tracks whether or not a meter has been selected + const meterIsSelected = readingsData.meterIdentifier !== ''; + // tracks if a new meter was created + const [newMeterIdentifier, setNewMeterIdentifier] = useState(''); + // tracks if file has .gzip or .csv extension + const [isValidFileType, setIsValidFileType] = React.useState(false); + // tracks if should show spinner (true while loading data, false otherwise) + const [showSpinner, setShowSpinner] = React.useState(false); - private async handleSubmit(e: React.MouseEvent) { - try { - e.preventDefault(); - const current = this.fileInput.current as HTMLInputElement; - const { files } = current; - if (files && (files as FileList).length !== 0) { - const msg = await this.props.submitCSV(files[0]); - // If the meter was created then update the meter state so it is available. - // Getting this needed state is a pain with the old React system. Thus, - // this fetches the meter state in all cases. This should be fairly fast, - // esp. compared to the upload time. - // TODO When this is converted to React hooks it is hoped that getting the - // value about whether the meter was created will be easier and can be done then. - // Also, you cannot dispatch with Hooks so this is left until then. For now, - // we reload after the alert. - // dispatch(fetchMetersDetails()); - // TODO Using an alert is not the best. At some point this should be integrated - // with react. - // There should be a message (not void) but that is not the type so overriding. - showInfoNotification(msg as unknown as string); - // TODO remove when the above TODO is done. - window.location.reload(); - } - } catch (error) { - // A failed axios request should result in an error. - showErrorNotification(error.response.data as string); - } - } + /* Handlers for each type of input change */ + const handleChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + setReadingsData(prevData => ({ + ...prevData, + [name]: value + })); + }; - private handleSetMeterName(e: React.ChangeEvent) { - const target = e.target; - this.props.setMeterName(MODE.readings, target.value); - } + const handleNumberChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + setReadingsData(prevData => ({ + ...prevData, + [name]: Number(value) + })); + }; - private handleSetTimeSort(e: React.ChangeEvent) { - const target = e.target; - this.props.selectTimeSort(target.value as TimeSortTypes); - } + const handleCheckboxChange = (e: React.ChangeEvent) => { + const { name, checked } = e.target; + setReadingsData(prevData => ({ + ...prevData, + [name]: checked + })); + }; - private handleSetDuplications(e: React.ChangeEvent) { - const target = e.target; - this.props.selectDuplications(target.value); - } + const handleTimeSortChange = (e: React.ChangeEvent) => { + const newTimeSort = MeterTimeSortType[e.target.value as keyof typeof MeterTimeSortType]; + setReadingsData(prevData => ({ + ...prevData, + timeSort: newTimeSort + })); + }; - private handleSetCumulative(e: React.ChangeEvent) { - const target = e.target; - this.props.selectCumulative(target.value as BooleanMeterTypes); - } + // need this change function because input type select expects a string as value + const handleTrueFalseSelectChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + setReadingsData(prevData => ({ + ...prevData, + [name]: value === 'true' + })); + }; - private handleSetCumulativeReset(e: React.ChangeEvent) { - const target = e.target; - this.props.selectCumulativeReset(target.value as BooleanMeterTypes); - } - - private handleSetCumulativeResetStart(e: React.ChangeEvent) { - const target = e.target; - this.props.setCumulativeResetStart(target.value); - } + const handleFileChange = (file: File) => { + if (file) { + setSelectedFile(file); + if (file.name.slice(-4) === '.csv' || file.name.slice(-3) === '.gz') { + setIsValidFileType(true); + } else { + setIsValidFileType(false); + setSelectedFile(null); + showErrorNotification(translate('csv.file.error') + file.name); + } + } + }; + /* END of Handlers for each type of input change */ - private handleSetCumulativeResetEnd(e: React.ChangeEvent) { - const target = e.target; - this.props.setCumulativeResetEnd(target.value); - } + useEffect(() => { + if (newMeterIdentifier) { + const foundMeter = visibleMeters.find(meter => meter.identifier === newMeterIdentifier); + if (foundMeter) { + updateReadingsData(newMeterIdentifier); + } + } + }, [visibleMeters, newMeterIdentifier]); - private handleSetLengthGap(e: React.ChangeEvent) { - const target = e.target; - this.props.setLengthGap(target.value); - } + useEffect(() => { + if (readingsData.cumulative === false) { + setReadingsData(prevData => ({ + ...prevData, + cumulativeReset: false + })); + } + }, [readingsData.cumulative]); - private handleSetLengthVariation(e: React.ChangeEvent) { - const target = e.target; - this.props.setLengthVariation(target.value); - } + // This gets the meter identifier from a newly created meter and updates the readingsData settings + // with the meterData settings, although the settings only update if user is an admin because a + // CSV user doesn't have access to this data + const handleCreateMeter = async (meterIdentifier: string) => { + setNewMeterIdentifier(meterIdentifier); + }; - private handleSetEndOnly(e: React.ChangeEvent) { - const target = e.target; - this.props.selectEndOnly(target.value as BooleanMeterTypes); - } + const handleSelectedMeterChange = (e: React.ChangeEvent) => { + updateReadingsData(e.target.value); + }; - private handleSetHonorDst() { - this.props.toggleHonorDst(); - } + // method to update readingData state + const updateReadingsData = (meterIdentifier: string) => { + const selectedMeter = visibleMeters.find(meter => meter.identifier === meterIdentifier) as MeterData; + setReadingsData(prevData => ({ + ...prevData, + meterIdentifier: selectedMeter.identifier, + cumulative: selectedMeter.cumulative, + cumulativeReset: selectedMeter.cumulativeReset, + cumulativeResetStart: selectedMeter.cumulativeResetStart, + cumulativeResetEnd: selectedMeter.cumulativeResetEnd, + duplications: Number(selectedMeter.readingDuplication), + lengthGap: selectedMeter.readingGap, + lengthVariation: selectedMeter.readingVariation, + endOnly: selectedMeter.endOnlyTime, + timeSort: MeterTimeSortType[selectedMeter.timeSort as keyof typeof MeterTimeSortType], + useMeterZone: false + })); + }; - private handleSetRelaxedParsing() { - this.props.toggleRelaxedParsing(); - } + const handleClear = () => { + setReadingsData(ReadingsCSVUploadDefaults); + setIsValidFileType(false); + }; - private handleSetUseMeterZone() { - this.props.toggleUseMeterZone(); - } + const handleSubmit = async (e: React.MouseEvent) => { + e.preventDefault(); + if (selectedFile) { + // show spinner before calling api, then stop it immediately after + setShowSpinner(true); + const { success, message } = await submitReadings(readingsData, selectedFile, dispatch); + setShowSpinner(false); + if (success) { + showSuccessNotification(message); + } else { + showErrorNotification(message); + } + } + }; - public render() { - const titleStyle: React.CSSProperties = { - fontWeight: 'bold', - paddingBottom: '5px' - }; + const spinContainerStyle = { + display: 'flex', + justifyContent: 'center' + }; - const checkboxStyle: React.CSSProperties = { - paddingBottom: '15px' - }; + const tooltipStyle = { + display: 'inline-block', + fontSize: '50%', + tooltipReadings: 'help.csv.readings' + }; - const formStyle: React.CSSProperties = { - display: 'flex', - justifyContent: 'center', - padding: '20px' - }; + const checkBox = { + display: 'flex' + }; - return ( -
-
- - - - - - - - - - - - - - - - - - - - - {range(1, 10).map(i => ( - - ))} + return ( + + {showSpinner ? ( +
+ +
+ ) : (<> + + + + +
+

+ {translate('csv.upload.readings')} +
+ +
+

+
+ + + { + + } + { + Array.from(visibleMeters).map(meter => { + return (); + }) + } - -
- - - - - - - - - - - - - +
+ {isAdmin && <>} +
+ + - - - - - - - - + + +
+ + +
-
- - - - + +
+ + +
-
- - - - + + + +
+ + +
-
- -
- - - - - - - + +
+ + +
-
- - - - + + + +
+ + +
-
- -
- - - - - - - - + +
+ + +
+ + +
+ + + + + + {Object.keys(TrueFalseType).map(key => { + return (); + })} + + + + + + + {readingsData.cumulative === true ? ( + + {Object.keys(TrueFalseType).map(key => { + return (); + })} + + ) : ( + + + + )} + + + + + + + + + + + + + + + + + + + + + + + {Object.keys(TrueFalseType).map(key => { + return (); + })} + + + + + + + + + + + + + + + + + + + + + + {range(1, 10).map(i => ( + + ))} + + + + + + + + + + {Object.keys(MeterTimeSortType).map(key => { + return (); + })} + + + + + {/* TODO This feature is not working perfectly so disabling from web page but allowing in curl. + Rest of changes left so easy to add back in. + This feature was added to help a site import data. It works but can have issues. + Thus, it is only allowed for script uploading since that was the need. It was + originally added to the web page input of import but decided to not allow + it at this time. Thus, the code was commented out. As of now + there is no plan to make this generally available due to its limitations.*/} + {/* + + */} +
+
+ +
+
+ +
+
+
+
-
- - - - - - - - - - - - - - - - - - - - - - - - - {/* TODO This feature is not working perfectly so disabling from web page but allowing in curl. Rest of changes left so easy to add back in. */} - {/* - - */} - + -
- ); - } + )} + + ); } diff --git a/src/client/app/components/groups/CreateGroupModalComponent.tsx b/src/client/app/components/groups/CreateGroupModalComponent.tsx index 21be8a6a7..5eddfe35f 100644 --- a/src/client/app/components/groups/CreateGroupModalComponent.tsx +++ b/src/client/app/components/groups/CreateGroupModalComponent.tsx @@ -1,8 +1,8 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { sortBy } from 'lodash'; import * as React from 'react'; +import { selectSelectedLanguage } from '../../redux/slices/appStateSlice'; import { useEffect, useState } from 'react'; import { FormattedMessage } from 'react-intl'; import { @@ -29,7 +29,7 @@ import { import { AreaUnitType, getAreaUnitConversion } from '../../utils/getAreaUnitConversion'; import { getGPSString } from '../../utils/input'; import { showErrorNotification, showWarnNotification } from '../../utils/notifications'; -import translate from '../../utils/translate'; +import { useTranslate } from '../../redux/componentHooks'; import ListDisplayComponent from '../ListDisplayComponent'; import MultiSelectComponent from '../MultiSelectComponent'; import TooltipHelpComponent from '../TooltipHelpComponent'; @@ -40,6 +40,7 @@ import TooltipMarkerComponent from '../TooltipMarkerComponent'; * @returns Group create element */ export default function CreateGroupModalComponent() { + const translate = useTranslate(); const [createGroup] = groupsApi.useCreateGroupMutation(); // Meters state @@ -50,7 +51,8 @@ export default function CreateGroupModalComponent() { const unitsDataById = useAppSelector(selectUnitDataById); // Which units are possible for graphing state const possibleGraphicUnits = useAppSelector(selectPossibleGraphicUnits); - + // Obtaining language + const locale = useAppSelector(selectSelectedLanguage); // Since creating group the initial values are effectively nothing or the desired defaults. const defaultValues: GroupData = { // ID not needed, assigned by DB, add here for TS @@ -232,11 +234,11 @@ export default function CreateGroupModalComponent() { // The id is not really needed so set to -1 since same function for edit. const groupDeepMeter = metersInChangedGroup(state); // Get meters that okay for this group in a format the component can display. - const possibleMeters = getMeterMenuOptionsForGroup(state.defaultGraphicUnit, groupDeepMeter); + const possibleMeters = getMeterMenuOptionsForGroup(state.defaultGraphicUnit, groupDeepMeter, locale); // Get groups okay for this group. Similar to meters. // Since creating a group, the group cannot yet exist in the Redux state. Thus, the id is not used // in this case so set to -1 so it never matches in this function. - const possibleGroups = getGroupMenuOptionsForGroup(-1, state.defaultGraphicUnit, groupDeepMeter); + const possibleGroups = getGroupMenuOptionsForGroup(-1, state.defaultGraphicUnit, groupDeepMeter, locale); // Update the state setGroupChildrenState(groupChildrenState => ({ ...groupChildrenState, @@ -522,7 +524,8 @@ export default function CreateGroupModalComponent() { ); }); // Want chosen in sorted order. - return sortBy(selectedMetersUnsorted, item => item.label.toLowerCase(), 'asc'); + return selectedMetersUnsorted.sort((meterA, meterB) => meterA.label.toLowerCase()?. + localeCompare(meterB.label.toLowerCase(), String(locale), { sensitivity: 'accent'})); } /** @@ -541,7 +544,8 @@ export default function CreateGroupModalComponent() { ); }); // Want chosen in sorted order. - return sortBy(selectedGroupsUnsorted, item => item.label.toLowerCase(), 'asc'); + return selectedGroupsUnsorted.sort((groupA, groupB) => groupA.label.toLowerCase()?. + localeCompare(groupB.label.toLowerCase(), String(locale), { sensitivity: 'accent'})); } /** diff --git a/src/client/app/components/groups/EditGroupModalComponent.tsx b/src/client/app/components/groups/EditGroupModalComponent.tsx index d72d0b42e..04d9495ce 100644 --- a/src/client/app/components/groups/EditGroupModalComponent.tsx +++ b/src/client/app/components/groups/EditGroupModalComponent.tsx @@ -2,8 +2,9 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { cloneDeep, isEqual, difference, sortBy, filter } from 'lodash'; +import { cloneDeep, isEqual, difference, filter } from 'lodash'; import * as React from 'react'; +import { selectSelectedLanguage } from '../../redux/slices/appStateSlice'; // Realize that * is already imported from react import { useEffect, useState } from 'react'; import { FormattedMessage } from 'react-intl'; @@ -35,7 +36,7 @@ import { import { AreaUnitType, getAreaUnitConversion } from '../../utils/getAreaUnitConversion'; import { getGPSString, nullToEmptyString } from '../../utils/input'; import { showErrorNotification } from '../../utils/notifications'; -import translate from '../../utils/translate'; +import { useTranslate } from '../../redux/componentHooks'; import ConfirmActionModalComponent from '../ConfirmActionModalComponent'; import ListDisplayComponent from '../ListDisplayComponent'; import MultiSelectComponent from '../MultiSelectComponent'; @@ -57,12 +58,14 @@ interface EditGroupModalComponentProps { * @returns Group edit element */ export default function EditGroupModalComponent(props: EditGroupModalComponentProps) { + const locale = useAppSelector(selectSelectedLanguage); + const translate = useTranslate(); const [submitGroupEdits] = groupsApi.useEditGroupMutation(); const [deleteGroup] = groupsApi.useDeleteGroupMutation(); // Meter state const meterDataById = useAppSelector(selectMeterDataById); - // Group state used on other pages const groupDataById = useAppSelector(selectGroupDataById); + // Group state used on other pages // Make a local copy of the group data so we can update during the edit process. // When the group is saved the values will be synced again with the global state. // This needs to be a deep clone so the changes are only local. @@ -345,9 +348,9 @@ export default function EditGroupModalComponent(props: EditGroupModalComponentPr // Can only vary if admin and only used then. if (loggedInAsAdmin) { // Get meters that okay for this group in a format the component can display. - const possibleMeters = getMeterMenuOptionsForGroup(groupState.defaultGraphicUnit, groupState.deepMeters); + const possibleMeters = getMeterMenuOptionsForGroup(groupState.defaultGraphicUnit, groupState.deepMeters, locale); // Get groups okay for this group. Similar to meters. - const possibleGroups = getGroupMenuOptionsForGroup(groupState.id, groupState.defaultGraphicUnit, groupState.deepMeters); + const possibleGroups = getGroupMenuOptionsForGroup(groupState.id, groupState.defaultGraphicUnit, groupState.deepMeters, locale); // Update the state setGroupChildrenState(groupChildrenState => ({ ...groupChildrenState, @@ -927,7 +930,8 @@ export default function EditGroupModalComponent(props: EditGroupModalComponentPr ); }); // Want chosen in sorted order. - return sortBy(selectedMetersUnsorted, item => item.label.toLowerCase(), 'asc'); + return selectedMetersUnsorted.sort((meterA, meterB) => meterA.label.toLowerCase()?. + localeCompare(meterB.label.toLowerCase(), String(locale), { sensitivity: 'accent'})); } /** @@ -948,7 +952,8 @@ export default function EditGroupModalComponent(props: EditGroupModalComponentPr ); }); // Want chosen in sorted order. - return sortBy(selectedGroupsUnsorted, item => item.label.toLowerCase(), 'asc'); + return selectedGroupsUnsorted.sort((groupA, groupB) => groupA.label.toLowerCase()?. + localeCompare(groupB.label.toLowerCase(), String(locale), { sensitivity: 'accent'})); } /** @@ -972,7 +977,7 @@ export default function EditGroupModalComponent(props: EditGroupModalComponentPr } }); // Sort for display. Before were sorted by id so not okay here. - listedMeters.sort(); + listedMeters.sort((meterA, meterB) => meterA.toLowerCase().localeCompare(meterB.toLowerCase(), locale, { sensitivity : 'accent' })); if (hasHidden) { // There are hidden meters so note at bottom of list. listedMeters.push(translate('meter.hidden')); @@ -1003,7 +1008,8 @@ export default function EditGroupModalComponent(props: EditGroupModalComponentPr } }); // Sort for display. Before were sorted by id so not okay here. - listedGroups.sort(); + listedGroups.sort((groupA, groupB) => groupA.toLowerCase().localeCompare( + groupB.toLowerCase(), locale, { sensitivity : 'accent' })); if (hasHidden) { // There are hidden groups so note at bottom of list. listedGroups.push(translate('group.hidden')); @@ -1031,7 +1037,8 @@ export default function EditGroupModalComponent(props: EditGroupModalComponentPr } }); // Sort for display. - listedDeepMeters.sort(); + listedDeepMeters.sort((deepMeterA, deepMeterB) => deepMeterA.toLowerCase().localeCompare( + deepMeterB.toLowerCase(), locale, { sensitivity : 'accent' })); if (hasHidden) { // There are hidden meters so note at bottom of list. // This should never happen to an admin. diff --git a/src/client/app/components/groups/GroupViewComponent.tsx b/src/client/app/components/groups/GroupViewComponent.tsx index 273459da2..8b30ae65d 100644 --- a/src/client/app/components/groups/GroupViewComponent.tsx +++ b/src/client/app/components/groups/GroupViewComponent.tsx @@ -13,7 +13,7 @@ import { useAppSelector } from '../../redux/reduxHooks'; import { selectIsAdmin } from '../../redux/slices/currentUserSlice'; import '../../styles/card-page.css'; import { noUnitTranslated } from '../../utils/input'; -import translate from '../../utils/translate'; +import { useTranslate } from '../../redux/componentHooks'; import EditGroupModalComponent from './EditGroupModalComponent'; interface GroupViewComponentProps { @@ -26,6 +26,7 @@ interface GroupViewComponentProps { * @returns Group info card element */ export default function GroupViewComponent(props: GroupViewComponentProps) { + const translate = useTranslate(); // Don't check if admin since only an admin is allowed to route to this page. // Edit Modal Show diff --git a/src/client/app/components/meters/CreateMeterModalComponent.tsx b/src/client/app/components/meters/CreateMeterModalComponent.tsx index ab7125bb5..1c0ba7702 100644 --- a/src/client/app/components/meters/CreateMeterModalComponent.tsx +++ b/src/client/app/components/meters/CreateMeterModalComponent.tsx @@ -2,6 +2,7 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +import { range } from 'lodash'; import * as moment from 'moment'; import * as React from 'react'; import { useState } from 'react'; @@ -14,7 +15,8 @@ import { MAX_ERRORS, MAX_VAL, MIN_DATE, MIN_DATE_MOMENT, MIN_VAL, isValidCreateMeter, - selectDefaultCreateMeterValues, selectCreateMeterUnitCompatibility + selectCreateMeterUnitCompatibility, + selectDefaultCreateMeterValues } from '../../redux/selectors/adminSelectors'; import '../../styles/modal.css'; import { tooltipBaseStyle } from '../../styles/modalStyle'; @@ -23,16 +25,22 @@ import { MeterData, MeterTimeSortType, MeterType } from '../../types/redux/meter import { GPSPoint, isValidGPSInput } from '../../utils/calibration'; import { AreaUnitType } from '../../utils/getAreaUnitConversion'; import { showErrorNotification, showSuccessNotification } from '../../utils/notifications'; -import translate from '../../utils/translate'; +import { useTranslate } from '../../redux/componentHooks'; import TimeZoneSelect from '../TimeZoneSelect'; import TooltipHelpComponent from '../TooltipHelpComponent'; import TooltipMarkerComponent from '../TooltipMarkerComponent'; +interface CreateMeterModalProps { + onCreateMeter?: (meterIdentifier: string) => void; // Define the type of the callback function +} + /** * Defines the create meter modal form + * @param props for create meter to return the identifier * @returns Meter create element */ -export default function CreateMeterModalComponent() { +export default function CreateMeterModalComponent(props: CreateMeterModalProps): React.JSX.Element { + const translate = useTranslate(); // Tracks whether a unit/ default unit has been selected. // RTKQ Mutation to submit add meter const [submitAddMeter] = metersApi.endpoints.addMeter.useMutation(); @@ -152,6 +160,15 @@ export default function CreateMeterModalComponent() { // if successful, the mutation will invalidate existing cache causing all meter details to be retrieved showSuccessNotification(translate('meter.successfully.create.meter')); resetState(); + // if props exist, then return the identifier + // or return the name if identifier is not set because the identifier will be set from the name + if (props.onCreateMeter) { + if (meterDetails.identifier === '') { + props.onCreateMeter(meterDetails.name); + } else { + props.onCreateMeter(meterDetails.identifier); + } + } }) .catch(err => { showErrorNotification(translate('meter.failed.to.create.meter') + '"' + err.data + '"'); @@ -162,7 +179,6 @@ export default function CreateMeterModalComponent() { } }; - const tooltipStyle = { ...tooltipBaseStyle, // Only an admin can create a meter. @@ -367,11 +383,15 @@ export default function CreateMeterModalComponent() { {/* Area input */} - handleNumberChange(e)} - invalid={meterDetails.area < 0} /> + invalid={meterDetails.area < 0} + /> @@ -495,15 +515,9 @@ export default function CreateMeterModalComponent() { handleNumberChange(e)}> - - - - - - - - - + {range(1, 10).map(i => ( + + ))}
diff --git a/src/client/app/components/meters/EditMeterModalComponent.tsx b/src/client/app/components/meters/EditMeterModalComponent.tsx index 517715a78..59afc37a7 100644 --- a/src/client/app/components/meters/EditMeterModalComponent.tsx +++ b/src/client/app/components/meters/EditMeterModalComponent.tsx @@ -2,7 +2,7 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { cloneDeep, isEqual } from 'lodash'; +import { cloneDeep, isEqual, range } from 'lodash'; import * as moment from 'moment'; import * as React from 'react'; import { useEffect, useState } from 'react'; @@ -26,7 +26,7 @@ import { GPSPoint, isValidGPSInput } from '../../utils/calibration'; import { AreaUnitType } from '../../utils/getAreaUnitConversion'; import { getGPSString, nullToEmptyString } from '../../utils/input'; import { showErrorNotification } from '../../utils/notifications'; -import translate from '../../utils/translate'; +import { useTranslate } from '../../redux/componentHooks'; import TimeZoneSelect from '../TimeZoneSelect'; import TooltipHelpComponent from '../TooltipHelpComponent'; import TooltipMarkerComponent from '../TooltipMarkerComponent'; @@ -43,6 +43,7 @@ interface EditMeterModalComponentProps { * @returns Meter edit element */ export default function EditMeterModalComponent(props: EditMeterModalComponentProps) { + const translate = useTranslate(); const [editMeter] = metersApi.useEditMeterMutation(); // since this selector is shared amongst many other modals, we must use a selector factory in order // to have a single selector per modal instance. Memo ensures that this is a stable reference @@ -180,6 +181,34 @@ export default function EditMeterModalComponent(props: EditMeterModalComponentPr setLocalMeterEdits({ ...localMeterEdits, [e.target.name]: JSON.parse(e.target.value) }); }; + // Function handles the selection of a new displayable. + const handleDisplayableChange = (e: React.ChangeEvent) => { + // If there is a potential issue then the admin will decide if save happens. Otherwise, the value is put into state. + let save = true; + if (!JSON.parse(e.target.value)) { + // This will hold the overall message for the admin alert. + let msg = ''; + // This will hold the names of groups that are affected. + let groups = ''; + // Tells if the change should be cancelled. + // Checks for groups that include the meter being edited. + for (const groupId of Object.values(groupDataByID)) { + if (groupId.displayable && groupId.deepMeters.includes(meterState.id)) { + groups += `${groupId.name}\n`; + } + } + if (groups != '') { + // There is a message to display to the user. + msg += `${translate('meter')} "${meterState.name}" ${translate('meter.edit.displayable.warning')}\n`; + msg += `${groups + '\n' + translate('meter.edit.displayable.verify')}\n`; + save = window.confirm(msg); + } + } + if (save) { + handleBooleanChange(e); + } + }; + const handleNumberChange = (e: React.ChangeEvent) => { setLocalMeterEdits({ ...localMeterEdits, [e.target.name]: Number(e.target.value) }); }; @@ -306,7 +335,7 @@ export default function EditMeterModalComponent(props: EditMeterModalComponentPr name='displayable' type='select' value={localMeterEdits.displayable?.toString()} - onChange={e => handleBooleanChange(e)} + onChange={e => handleDisplayableChange(e)} invalid={localMeterEdits.displayable && localMeterEdits.unitId === -99}> {Object.keys(TrueFalseType).map(key => { return (); @@ -538,15 +567,9 @@ export default function EditMeterModalComponent(props: EditMeterModalComponentPr handleNumberChange(e)} defaultValue={localMeterEdits?.readingDuplication} > - - - - - - - - - + {range(1, 10).map(i => ( + + ))} diff --git a/src/client/app/components/meters/MeterViewComponent.tsx b/src/client/app/components/meters/MeterViewComponent.tsx index a40c427fb..f3117e342 100644 --- a/src/client/app/components/meters/MeterViewComponent.tsx +++ b/src/client/app/components/meters/MeterViewComponent.tsx @@ -10,7 +10,7 @@ import { MeterData } from 'types/redux/meters'; import { useAppSelector } from '../../redux/reduxHooks'; import { selectGraphicName, selectUnitName } from '../../redux/selectors/adminSelectors'; import '../../styles/card-page.css'; -import translate from '../../utils/translate'; +import { useTranslate } from '../../redux/componentHooks'; import EditMeterModalComponent from './EditMeterModalComponent'; import { selectIsAdmin } from '../../redux/slices/currentUserSlice'; @@ -24,6 +24,7 @@ interface MeterViewComponentProps { * @returns Meter info card element */ export default function MeterViewComponent(props: MeterViewComponentProps) { + const translate = useTranslate(); // Edit Modal Show const [showEditModal, setShowEditModal] = useState(false); // Check for admin status diff --git a/src/client/app/components/router/ErrorComponent.tsx b/src/client/app/components/router/ErrorComponent.tsx index bc58cbcee..58098df9e 100644 --- a/src/client/app/components/router/ErrorComponent.tsx +++ b/src/client/app/components/router/ErrorComponent.tsx @@ -6,12 +6,13 @@ import * as React from 'react'; import { useNavigate } from 'react-router-dom'; import { Button } from 'reactstrap'; import AppLayout from '../../components/AppLayout'; -import translate from '../../utils/translate'; +import { useTranslate } from '../../redux/componentHooks'; /** * @returns A error page that then returns to main dashboard page. */ export default function ErrorComponent() { + const translate = useTranslate(); const nav = useNavigate(); const refreshPage = () => { nav('/'); diff --git a/src/client/app/components/router/InitializingComponent.tsx b/src/client/app/components/router/InitializingComponent.tsx index bd2346de5..25e449da9 100644 --- a/src/client/app/components/router/InitializingComponent.tsx +++ b/src/client/app/components/router/InitializingComponent.tsx @@ -4,12 +4,13 @@ import * as React from 'react'; import SpinnerComponent from '../SpinnerComponent'; -import translate from '../../utils/translate'; +import { useTranslate } from '../../redux/componentHooks'; /** * @returns A simple loading spinner used to indicate that the startup init sequence is in progress */ export default function InitializingComponent() { + const translate = useTranslate(); return (
= ({ + conversions, + isCik = false +}) => { + const intl = useIntl(); + /* Get unit data from redux */ + const units = useAppSelector(selectAllUnits); + + /* creating color schema for nodes based on their unit type */ + const colors = ['#1F77B4', '#2CA02C', '#fd7e14', '#e377c2']; + const colorSchema = d3.scaleOrdinal() + .domain(['meter', 'unit', 'suffix', 'suffix.input']) + .range(colors); + + /* Create data container to pass to D3 force graph */ + const data: { nodes: any[], links: any[] } = { + nodes: [], + links: [] + }; + + units.map(value => + data.nodes.push({ + 'name': value.name, + 'id': value.id, + 'typeOfUnit': value.typeOfUnit, + 'suffix': value.suffix + }) + ); + + conversions.map(value => { + if (isCik) { + const cikValue = value as CikData; + data.links.push({ + 'source': cikValue.meterUnitId, + 'target': cikValue.nonMeterUnitId, + 'bidirectional': false + }); + } else { + const conversionValue = value as ConversionData; + data.links.push({ + 'source': conversionValue.sourceId, + 'target': conversionValue.destinationId, + /* boolean value */ + 'bidirectional': conversionValue.bidirectional + }); + } + }); + + /* Visuals start here */ + useEffect(() => { + /* View-box dimensions */ + const width = window.innerWidth; + const height = isCik ? 1000 : 750; + + /* Grab data */ + const nodes = data.nodes.map(d => ({...d})); + const links = data.links.map(d => ({...d})); + + const simulation = d3.forceSimulation(nodes) + .force('link', d3.forceLink(links) + /* Set all link ids (from data.links) */ + .id((d: any) => d.id) + /* This controls how long each link is */ + .distance(isCik ? 120 : 90) + ) + /* Create new many-body force */ + .force('charge', d3.forceManyBody() + /* This controls the 'repelling' force on each node */ + .strength(isCik ? -800 : -500) + ) + .force('x', d3.forceX()) + .force('y', d3.forceY()); + + const svg = d3.select(isCik ? '#sample-cik' : '#sample') + .append('svg') + .attr('width', width) + .attr('height', height) + .attr('viewBox', [-width / 2, -height / 2, width, height]) + .attr('style', 'max-width: 100%; height: auto;') + .append('g'); + + /* End arrow head */ + svg.append('defs').append('marker') + .attr('id', 'arrow-end') + .attr('viewBox', '0 -5 10 10') + .attr('refX', 25) + .attr('refY', 0) + .attr('markerWidth', 4) + .attr('markerHeight', 4) + /* auto: point towards dest. node */ + .attr('orient', 'auto') + .append('svg:path') + .attr('d', 'M0,-5L10,0L0,5'); + + if (!isCik) { + /* Start arrow head (for bidirectional edges) */ + svg.append('defs').append('marker') + .attr('id', 'arrow-start') + .attr('viewBox', '0 -5 10 10') + .attr('refX', 25) + .attr('refY', 0) + .attr('markerWidth', 4) + .attr('markerHeight', 4) + /* auto-start-reverse: point towards src. node */ + .attr('orient', 'auto-start-reverse') + .append('svg:path') + .attr('d', 'M0,-5L10,0L0,5'); + } + + /* Link style */ + const link = svg.selectAll('line') + .data(links) + .enter().append('line') + .style('stroke', '#aaa') + .attr('stroke-width', 3) + .attr('marker-end', 'url(#arrow-end)') + /* Only draw start arrow head if bidirectional */ + .attr('marker-start', d => d.bidirectional === true ? 'url(#arrow-start)' : ''); + + /* Node style */ + const node = svg.selectAll('.node') + .data(nodes) + .enter().append('circle') + /* Node radius */ + .attr('r', 20) + /* checks if unit has a non empty suffix to color differently */ + .attr('fill', d => d.suffix && d.typeOfUnit === 'unit' ? colorSchema('suffix.input') : colorSchema(d.typeOfUnit)); + + /* Drag behavior */ + node.call(d3.drag() + .on('start', dragstart) + .on('drag', dragged) + .on('end', dragend)); + + /* Node label style */ + const label = svg.selectAll('.label') + .data(nodes) + .enter() + .append('text') + .text(function (d) { return d.name; }) + .style('text-anchor', 'middle') + .style('fill', '#000') + .style('font-family', 'Arial') + .style('font-size', 14); + + /* Update element positions when moved */ + simulation.on('tick', () => { + link + .attr('x1', d => d.source.x) + .attr('y1', d => d.source.y) + .attr('x2', d => d.target.x) + .attr('y2', d => d.target.y); + + node + .attr('cx', d => d.x) + .attr('cy', d => d.y); + + label + .attr('x', function(d){ return d.x; }) + .attr('y', function (d) {return d.y - 25; }); + }); + + // eslint-disable-next-line jsdoc/require-jsdoc + function dragstart(event: any) { + if (!event.active) simulation.alphaTarget(0.3).restart(); + event.subject.fx = event.subject.x; + event.subject.fy = event.subject.y; + } + + // eslint-disable-next-line jsdoc/require-jsdoc + function dragged(event: any) { + event.subject.fx = event.x; + event.subject.fy = event.y; + } + + // eslint-disable-next-line jsdoc/require-jsdoc + function dragend(event: any) { + if (!event.active) simulation.alphaTarget(0); + event.subject.fx = null; + event.subject.fy = null; + } + + /* Color Legend */ + const legend = svg.append('g') + .attr('transform', `translate(${-width / 2 + 20}, ${-height / 2 + 20})`); + + colorSchema.domain().forEach((item, i) => { + const legendEntry = legend.append('g') + .attr('transform', `translate(0, ${i * 30})`); + + // Rectangle color box + legendEntry.append('circle') + .attr('r', 15) + .attr('cx', 15) // Center the circle horizontally + .attr('cy', 15) // Center the circle vertically + .attr('fill', colorSchema(item)); + + // Text label + legendEntry.append('text') + .attr('x', 40) // Position the text to the right of the circle + .attr('y', 20) // Align the text vertically with the circle + .style('fill', '#000') + .style('font-size', '14px') + .style('alignment-middle', 'middle') + /* internationalizing color legend text */ + .text(intl.formatMessage({id : `legend.graph.text.${item}`})); + }); + + // Empty dependency array to run the effect only once + }, []); + + return( +
+
+
+ ); +}; \ No newline at end of file diff --git a/src/client/app/components/visual-unit/VisualUnitDetailComponent.tsx b/src/client/app/components/visual-unit/VisualUnitDetailComponent.tsx new file mode 100644 index 000000000..bc4e8dc28 --- /dev/null +++ b/src/client/app/components/visual-unit/VisualUnitDetailComponent.tsx @@ -0,0 +1,66 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public +* License, v. 2.0. If a copy of the MPL was not distributed with this +* file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { FormattedMessage } from 'react-intl'; +import TooltipHelpComponent from '../TooltipHelpComponent'; +import * as React from 'react'; +import { CreateVisualUnitComponent } from './CreateVisualUnitComponent'; +import TooltipMarkerComponent from '../TooltipMarkerComponent'; +import { selectCik } from '../../redux/api/conversionsApi'; +import { selectConversionsDetails } from '../../redux/api/conversionsApi'; +import { useAppSelector } from '../../redux/reduxHooks'; + +/** + * Defines the units and conversion graphics view. + * @returns Units visual graphics page element + */ +export default function VisualUnitDetailComponent() { + /* Get conversion data from redux */ + const conversionData = useAppSelector(selectConversionsDetails); + const cikData = useAppSelector(selectCik); + + + + const titleStyle: React.CSSProperties = { + textAlign: 'center' + }; + + const tooltipStyle = { + display: 'inline-block', + fontSize: '50%', + // For now, only an admin can see the unit visualization page. + tooltipVisualUnitView: 'help.admin.unitconversionvisuals' + }; + + return ( +
+ + +
+

+ +
+ +
+

+ +

+ +

+ +
+ +
+ +

+ +

+ +
+ +
+
+
+ ); +} diff --git a/src/client/app/containers/CompareChartContainer.ts b/src/client/app/containers/CompareChartContainer.ts index 525fcbe65..1f9d33213 100644 --- a/src/client/app/containers/CompareChartContainer.ts +++ b/src/client/app/containers/CompareChartContainer.ts @@ -6,9 +6,11 @@ import { connect } from 'react-redux'; import { getComparePeriodLabels, getCompareChangeSummary, calculateCompareShift } from '../utils/calculateCompare'; -import { useTranslate } from 'redux/componentHooks'; +// import { useTranslate } from 'redux/componentHooks'; // import * as React from 'react'; Convert from containers to components // import { useState } from 'react'; +// When this container gets converted to component,migrate to useTranslate() from componentHooks.ts +import translate from '../utils/translate'; import Plot from 'react-plotly.js'; // import { Icons } from 'plotly.js'; import Locales from '../types/locales'; @@ -63,7 +65,6 @@ function mapStateToProps(state: RootState, ownProps: CompareChartContainerProps) const groupDataById = selectGroupDataById(state); const selectUnitState = unitDataById[graphingUnit]; const locale = selectSelectedLanguage(state); - const translate = useTranslate(); let unitLabel: string = ''; // If graphingUnit is -99 then none selected and nothing to graph so label is empty. // This will probably happen when the page is first loaded. diff --git a/src/client/app/containers/MapChartContainer.ts b/src/client/app/containers/MapChartContainer.ts index 45a8049c6..94cef902b 100644 --- a/src/client/app/containers/MapChartContainer.ts +++ b/src/client/app/containers/MapChartContainer.ts @@ -55,7 +55,7 @@ function mapStateToProps(state: State) { // Figure out what time interval the bar is using since user bar data for now. const timeInterval = state.graph.queryTimeInterval; - const barDuration = state.graph.barDuration; + const mapDuration = state.graph.duration; // Make sure there is a map with values so avoid issues. if (map && map.origin && map.opposite) { // The size of the original map loaded into OED. @@ -129,11 +129,11 @@ function mapStateToProps(state: State) { // The x, y value for Plotly to use that are on the user map. x.push(meterGPSInUserGrid.x); y.push(meterGPSInUserGrid.y); - // Make sure the bar reading data is available. The timeInterval should be fine (but checked) but the barDuration might have changed + // Make sure the bar reading data is available. The timeInterval should be fine (but checked) but the mapDuration might have changed // and be fetching. The unit could change from that menu so also need to check. - if (byMeterID[timeInterval.toString()] !== undefined && byMeterID[timeInterval.toString()][barDuration.toISOString()] !== undefined) { + if (byMeterID[timeInterval.toString()] !== undefined && byMeterID[timeInterval.toString()][mapDuration.toISOString()] !== undefined) { // Get the bar data to use for the map circle. - const readingsData = byMeterID[timeInterval.toString()][barDuration.toISOString()][unitID]; + const readingsData = byMeterID[timeInterval.toString()][mapDuration.toISOString()][unitID]; // This protects against there being no readings or that the data is being updated. if (readingsData !== undefined && !readingsData.isFetching) { // Meter name to include in hover on graph. @@ -160,13 +160,13 @@ function mapStateToProps(state: State) { // only display a range of dates for the hover text if there is more than one day in the range // Shift to UTC since want database time not local/browser time which is what moment does. timeReading = `${moment.utc(mapReading.startTimestamp).format('ll')}`; - if (barDuration.asDays() != 1) { + if (mapDuration.asDays() != 1) { // subtracting one extra day caused by day ending at midnight of the next day. // Going from DB unit timestamp that is UTC so force UTC with moment, as usual. timeReading += ` - ${moment.utc(mapReading.endTimestamp).subtract(1, 'days').format('ll')}`; } // The value for the circle is the average daily usage. - averagedReading = mapReading.reading / barDuration.asDays(); + averagedReading = mapReading.reading / mapDuration.asDays(); if (state.graph.areaNormalization) { averagedReading /= meterArea; } @@ -206,11 +206,11 @@ function mapStateToProps(state: State) { // The x, y value for Plotly to use that are on the user map. x.push(groupGPSInUserGrid.x); y.push(groupGPSInUserGrid.y); - // Make sure the bar reading data is available. The timeInterval should be fine (but checked) but the barDuration might have changed + // Make sure the bar reading data is available. The timeInterval should be fine (but checked) but the mapDuration might have changed // and be fetching. The unit could change from that menu so also need to check. - if (byGroupID[timeInterval.toString()] !== undefined && byGroupID[timeInterval.toString()][barDuration.toISOString()] !== undefined) { + if (byGroupID[timeInterval.toString()] !== undefined && byGroupID[timeInterval.toString()][mapDuration.toISOString()] !== undefined) { // Get the bar data to use for the map circle. - const readingsData = byGroupID[timeInterval.toString()][barDuration.toISOString()][unitID]; + const readingsData = byGroupID[timeInterval.toString()][mapDuration.toISOString()][unitID]; // This protects against there being no readings or that the data is being updated. if (readingsData !== undefined && !readingsData.isFetching) { // Group name to include in hover on graph. @@ -236,13 +236,13 @@ function mapStateToProps(state: State) { } else { // only display a range of dates for the hover text if there is more than one day in the range timeReading = `${moment.utc(mapReading.startTimestamp).format('ll')}`; - if (barDuration.asDays() != 1) { + if (mapDuration.asDays() != 1) { // subtracting one extra day caused by day ending at midnight of the next day. // Going from DB unit timestamp that is UTC so force UTC with moment, as usual. timeReading += ` - ${moment.utc(mapReading.endTimestamp).subtract(1, 'days').format('ll')}`; } // The value for the circle is the average daily usage. - averagedReading = mapReading.reading / barDuration.asDays(); + averagedReading = mapReading.reading / mapDuration.asDays(); if (state.graph.areaNormalization) { averagedReading /= groupArea; } @@ -308,6 +308,7 @@ function mapStateToProps(state: State) { // set map background image const layout: any = { + margin: { b: 0, l: 0, r: 0 }, // Eliminate bottom, left, and right margins // Either the actual map name or text to say it is not available. title: { text: (map) ? map.name : translate('map.unavailable') diff --git a/src/client/app/containers/csv/UploadCSVContainer.tsx b/src/client/app/containers/csv/UploadCSVContainer.tsx deleted file mode 100644 index ddbdffeb9..000000000 --- a/src/client/app/containers/csv/UploadCSVContainer.tsx +++ /dev/null @@ -1,337 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -import * as React from 'react'; -import { FormattedMessage } from 'react-intl'; -import { Nav, NavItem, NavLink, TabContent, TabPane } from 'reactstrap'; -import TooltipMarkerComponent from '../../components/TooltipMarkerComponent'; -import MetersCSVUploadComponent from '../../components/csv/MetersCSVUploadComponent'; -import ReadingsCSVUploadComponent from '../../components/csv/ReadingsCSVUploadComponent'; -import { BooleanMeterTypes, MetersCSVUploadPreferencesItem, ReadingsCSVUploadPreferencesItem, TimeSortTypes } from '../../types/csvUploadForm'; -import { uploadCSVApi } from '../../utils/api'; -import { MetersCSVUploadDefaults, ReadingsCSVUploadDefaults } from '../../utils/csvUploadDefaults'; -import TooltipHelpComponent from '../../components/TooltipHelpComponent'; - -export const enum MODE { - meters = 'meters', - readings = 'readings' -} - -interface UploadCSVContainerState { - uploadReadingsPreferences: ReadingsCSVUploadPreferencesItem; - uploadMetersPreferences: MetersCSVUploadPreferencesItem; - activeTab: MODE; -} - -const tooltipStyle = { - display: 'inline-block', - fontSize: '100%' -}; - -export default class UploadCSVContainer extends React.Component<{}, UploadCSVContainerState> { - constructor(props: {}) { - super(props); - this.setMeterName = this.setMeterName.bind(this); - this.selectTimeSort = this.selectTimeSort.bind(this); - this.selectDuplications = this.selectDuplications.bind(this); - this.selectCumulative = this.selectCumulative.bind(this); - this.selectCumulativeReset = this.selectCumulativeReset.bind(this); - this.setCumulativeResetStart = this.setCumulativeResetStart.bind(this); - this.setCumulativeResetEnd = this.setCumulativeResetEnd.bind(this); - this.setLengthGap = this.setLengthGap.bind(this); - this.setLengthVariation = this.setLengthVariation.bind(this); - this.selectEndOnly = this.selectEndOnly.bind(this); - this.submitReadings = this.submitReadings.bind(this); - this.submitMeters = this.submitMeters.bind(this); - this.toggleCreateMeter = this.toggleCreateMeter.bind(this); - this.toggleGzip = this.toggleGzip.bind(this); - this.toggleHeaderRow = this.toggleHeaderRow.bind(this); - this.toggleRefreshHourlyReadings = this.toggleRefreshHourlyReadings.bind(this); - this.toggleRefreshReadings = this.toggleRefreshReadings.bind(this); - this.toggleUpdate = this.toggleUpdate.bind(this); - this.toggleTab = this.toggleTab.bind(this); - this.toggleHonorDst = this.toggleHonorDst.bind(this); - this.toggleRelaxedParsing = this.toggleRelaxedParsing.bind(this); - this.toggleUseMeterZone = this.toggleUseMeterZone.bind(this); - } - - state = { - activeTab: MODE.readings, - uploadMetersPreferences: { - ...MetersCSVUploadDefaults - }, - uploadReadingsPreferences: { - ...ReadingsCSVUploadDefaults - } - }; - - private setMeterName(mode: MODE, value: string) { - const preference = (mode === MODE.readings) ? 'uploadReadingsPreferences' : 'uploadMetersPreferences'; - this.setState(previousState => ({ - ...previousState, - [preference]: { - ...previousState[preference], - meterName: value - } - })); - } - private selectTimeSort(value: TimeSortTypes) { - this.setState(previousState => ({ - ...previousState, - uploadReadingsPreferences: { - ...previousState.uploadReadingsPreferences, - timeSort: value - } - })); - } - private selectDuplications(value: string) { - this.setState(previousState => ({ - ...previousState, - uploadReadingsPreferences: { - ...previousState.uploadReadingsPreferences, - duplications: value - } - })); - } - private selectCumulative(value: BooleanMeterTypes) { - this.setState(previousState => ({ - ...previousState, - uploadReadingsPreferences: { - ...previousState.uploadReadingsPreferences, - cumulative: value - } - })); - } - private selectCumulativeReset(value: BooleanMeterTypes) { - this.setState(previousState => ({ - ...previousState, - uploadReadingsPreferences: { - ...previousState.uploadReadingsPreferences, - cumulativeReset: value - } - })); - } - private setCumulativeResetStart(value: string) { - this.setState(previousState => ({ - ...previousState, - uploadReadingsPreferences: { - ...previousState.uploadReadingsPreferences, - cumulativeResetStart: value - } - })); - } - private setCumulativeResetEnd(value: string) { - this.setState(previousState => ({ - ...previousState, - uploadReadingsPreferences: { - ...previousState.uploadReadingsPreferences, - cumulativeResetEnd: value - } - })); - } - private setLengthGap(value: string) { - this.setState(previousState => ({ - ...previousState, - uploadReadingsPreferences: { - ...previousState.uploadReadingsPreferences, - lengthGap: value - } - })); - } - private setLengthVariation(value: string) { - this.setState(previousState => ({ - ...previousState, - uploadReadingsPreferences: { - ...previousState.uploadReadingsPreferences, - lengthVariation: value - } - })); - } - private selectEndOnly(value: BooleanMeterTypes) { - this.setState(previousState => ({ - ...previousState, - uploadReadingsPreferences: { - ...previousState.uploadReadingsPreferences, - endOnly: value - } - })); - } - private toggleCreateMeter() { - this.setState(previousState => ({ - ...previousState, - uploadReadingsPreferences: { - ...previousState.uploadReadingsPreferences, - createMeter: !previousState.uploadReadingsPreferences.createMeter - } - })); - } - private toggleGzip(mode: MODE) { - const preference = (mode === MODE.readings) ? 'uploadReadingsPreferences' : 'uploadMetersPreferences'; - return () => { - this.setState(previousState => ({ - ...previousState, - [preference]: { - ...previousState[preference], - gzip: !previousState[preference].gzip - } - })); - }; - } - private toggleHeaderRow(mode: MODE) { - const preference = (mode === MODE.readings) ? 'uploadReadingsPreferences' : 'uploadMetersPreferences'; - return () => { - this.setState(previousState => ({ - ...previousState, - [preference]: { - ...previousState[preference], - headerRow: !previousState[preference].headerRow - } - })); - }; - } - - private toggleRefreshHourlyReadings() { - this.setState(previousState => ({ - ...previousState, - uploadReadingsPreferences: { - ...previousState.uploadReadingsPreferences, - refreshHourlyReadings: !previousState.uploadReadingsPreferences.refreshHourlyReadings - } - })); - } - - private toggleRefreshReadings() { - this.setState(previousState => ({ - ...previousState, - uploadReadingsPreferences: { - ...previousState.uploadReadingsPreferences, - refreshReadings: !previousState.uploadReadingsPreferences.refreshReadings - } - })); - } - - private toggleHonorDst() { - this.setState(previousState => ({ - ...previousState, - uploadReadingsPreferences: { - ...previousState.uploadReadingsPreferences, - honorDst: !previousState.uploadReadingsPreferences.honorDst - } - })); - } - - private toggleRelaxedParsing() { - this.setState(previousState => ({ - ...previousState, - uploadReadingsPreferences: { - ...previousState.uploadReadingsPreferences, - relaxedParsing: !previousState.uploadReadingsPreferences.relaxedParsing - } - })); - } - - private toggleUseMeterZone() { - this.setState(previousState => ({ - ...previousState, - uploadReadingsPreferences: { - ...previousState.uploadReadingsPreferences, - useMeterZone: !previousState.uploadReadingsPreferences.useMeterZone - } - })); - } - - private toggleUpdate(mode: MODE) { - const preference = (mode === MODE.readings) ? 'uploadReadingsPreferences' : 'uploadMetersPreferences'; - return () => { - this.setState(previousState => ({ - ...previousState, - [preference]: { - ...previousState[preference], - update: !previousState[preference].update - } - })); - }; - } - - private async submitReadings(file: File) { - const uploadPreferences = this.state.uploadReadingsPreferences; - return await uploadCSVApi.submitReadings(uploadPreferences, file); - } - - private async submitMeters(file: File) { - const uploadPreferences = this.state.uploadMetersPreferences; - await uploadCSVApi.submitMeters(uploadPreferences, file); - } - - private toggleTab(tab: MODE) { - if (this.state.activeTab !== tab) { - this.setState({ - activeTab: tab - }); - } - } - - public render() { - const navStyle: React.CSSProperties = { - cursor: 'pointer' - }; - return ( -
- - - - - - - - - - -
- ); - } -} diff --git a/src/client/app/redux/selectors/adminSelectors.ts b/src/client/app/redux/selectors/adminSelectors.ts index 6f057c5d7..eaefebe3e 100644 --- a/src/client/app/redux/selectors/adminSelectors.ts +++ b/src/client/app/redux/selectors/adminSelectors.ts @@ -2,7 +2,6 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { sortBy } from 'lodash'; import * as moment from 'moment'; import { selectConversionsDetails } from '../../redux/api/conversionsApi'; import { selectAllGroups } from '../../redux/api/groupsApi'; @@ -18,6 +17,7 @@ import translate from '../../utils/translate'; import { selectAllUnits, selectUnitDataById } from '../api/unitsApi'; import { selectVisibleMetersAndGroups } from './authVisibilitySelectors'; import { createAppSelector } from './selectors'; +import { selectSelectedLanguage } from '../../redux/slices/appStateSlice'; export const MIN_VAL = Number.MIN_SAFE_INTEGER; export const MAX_VAL = Number.MAX_SAFE_INTEGER; @@ -26,20 +26,22 @@ export const MAX_DATE_MOMENT = moment(0).utc().add(5000, 'years'); export const MIN_DATE = MIN_DATE_MOMENT.format('YYYY-MM-DD HH:mm:ssZ'); export const MAX_DATE = MAX_DATE_MOMENT.format('YYYY-MM-DD HH:mm:ssZ'); export const MAX_ERRORS = 75; - export const selectPossibleGraphicUnits = createAppSelector( selectUnitDataById, - unitDataById => potentialGraphicUnits(unitDataById) + selectSelectedLanguage, + (unitDataById, selectSelectedLanguage) => potentialGraphicUnits(unitDataById, selectSelectedLanguage) ); /** * Calculates the set of all possible meter units for a meter. * This is any unit that is of type meter. + * @param state # Redux state passed in as argument + * @param locale # Language selected as retrieved from state * @returns The set of all possible graphic units for a meter */ export const selectPossibleMeterUnits = createAppSelector( - selectAllUnits, - unitData => { + [selectAllUnits, (state, locale) => locale], + (unitData, locale) => { let possibleMeterUnits = new Set(); // The meter unit can be any unit of type meter. unitData.forEach(unit => { @@ -48,7 +50,8 @@ export const selectPossibleMeterUnits = createAppSelector( } }); // Put in alphabetical order. - possibleMeterUnits = new Set(sortBy(Array.from(possibleMeterUnits), unit => unit.identifier.toLowerCase(), 'asc')); + possibleMeterUnits = new Set(Array.from(possibleMeterUnits).sort((unitA, unitB) => unitA.identifier.toLowerCase(). + localeCompare(unitB.identifier.toLowerCase(), locale, { sensitivity: 'accent'}))); // The default graphic unit can also be no unit/-99 but that is not desired so put last in list. return possibleMeterUnits.add(noUnitTranslated()); } @@ -217,7 +220,6 @@ export const selectIsValidConversion = createAppSelector( Cannot mix unit represent TODO Some of these can go away when we make the menus dynamic. */ - // The destination cannot be a meter unit. if (destinationId !== -999 && unitDataById[destinationId].typeOfUnit === UnitType.meter) { return [false, translate('conversion.create.destination.meter')]; diff --git a/src/client/app/redux/selectors/barChartSelectors.ts b/src/client/app/redux/selectors/barChartSelectors.ts index 9e2e067ff..ea60ad0a4 100644 --- a/src/client/app/redux/selectors/barChartSelectors.ts +++ b/src/client/app/redux/selectors/barChartSelectors.ts @@ -5,7 +5,7 @@ import { createSelector } from '@reduxjs/toolkit'; import * as moment from 'moment'; import { BarReadings } from 'types/readings'; -import { selectBarWidthDays } from '../../redux/slices/graphSlice'; +import { selectWidthDays } from '../../redux/slices/graphSlice'; import { DataType } from '../../types/Datasources'; import { MeterOrGroup } from '../../types/redux/graph'; import getGraphColor from '../../utils/getGraphColor'; @@ -18,7 +18,7 @@ export const selectPlotlyBarDeps = createAppSelector( [ selectPlotlyMeterDeps, selectPlotlyGroupDeps, - selectBarWidthDays + selectWidthDays ], (meterDeps, groupDeps, barDuration) => { const barMeterDeps = { ...meterDeps, barDuration }; diff --git a/src/client/app/redux/selectors/chartQuerySelectors.ts b/src/client/app/redux/selectors/chartQuerySelectors.ts index b0253fcc4..73e68216b 100644 --- a/src/client/app/redux/selectors/chartQuerySelectors.ts +++ b/src/client/app/redux/selectors/chartQuerySelectors.ts @@ -8,8 +8,8 @@ import { MeterOrGroup, ReadingInterval } from '../../types/redux/graph'; import { calculateCompareShift } from '../../utils/calculateCompare'; import { roundTimeIntervalForFetch } from '../../utils/dateRangeCompatibility'; import { - selectBarWidthDays, selectComparePeriod, - selectCompareTimeInterval, selectMapBarWidthDays, selectQueryTimeInterval, + selectWidthDays, selectComparePeriod, + selectCompareTimeInterval, selectQueryTimeInterval, selectSelectedGroups, selectSelectedMeters, selectSelectedUnit, selectThreeDState } from '../slices/graphSlice'; @@ -91,7 +91,7 @@ export const selectRadarChartQueryArgs = createSelector( export const selectBarChartQueryArgs = createSelector( selectCommonQueryArgs, - selectBarWidthDays, + selectWidthDays, (common, barWidthDays) => { // QueryArguments to pass into the bar chart component const barWidthAsDays = Math.round(barWidthDays.asDays()); @@ -139,7 +139,7 @@ export const selectCompareChartQueryArgs = createSelector( export const selectMapChartQueryArgs = createSelector( selectBarChartQueryArgs, - selectMapBarWidthDays, + selectWidthDays, (state: RootState) => state.maps, (barChartArgs, barWidthDays, maps) => { const durationDays = Math.round(barWidthDays.asDays()); diff --git a/src/client/app/redux/selectors/uiSelectors.ts b/src/client/app/redux/selectors/uiSelectors.ts index 6baecc5db..a0089af84 100644 --- a/src/client/app/redux/selectors/uiSelectors.ts +++ b/src/client/app/redux/selectors/uiSelectors.ts @@ -1,12 +1,11 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -import { sortBy } from 'lodash'; +import { LanguageTypes } from 'types/redux/i18n'; import { selectGroupDataById } from '../../redux/api/groupsApi'; import { selectMeterDataById } from '../../redux/api/metersApi'; import { selectUnitDataById } from '../../redux/api/unitsApi'; -import { selectChartLinkHideOptions } from '../../redux/slices/appStateSlice'; +import { selectChartLinkHideOptions, selectSelectedLanguage } from '../../redux/slices/appStateSlice'; import { DataType } from '../../types/Datasources'; import { GroupedOption, SelectOption } from '../../types/items'; import { ChartTypes } from '../../types/redux/graph'; @@ -236,9 +235,10 @@ export const selectMeterGroupSelectData = createAppSelector( selectMeterDataById, selectGroupDataById, selectSelectedMeters, - selectSelectedGroups + selectSelectedGroups, + selectSelectedLanguage ], - (chartTypeCompatibility, meterDataById, groupDataById, selectedMeters, selectedGroups) => { + (chartTypeCompatibility, meterDataById, groupDataById, selectedMeters, selectedGroups, selectSelectedLanguage) => { // Destructure Previous Selectors's values const { compatibleMeters, incompatibleMeters, compatibleGroups, incompatibleGroups } = chartTypeCompatibility; @@ -257,16 +257,16 @@ export const selectMeterGroupSelectData = createAppSelector( }); // The Multiselect's current selected value(s) as compatible/ incompatible options - const selectedMeterOptions = getSelectOptionsByEntity(compatibleSelectedMeters, incompatibleSelectedMeters, meterDataById); - const selectedGroupOptions = getSelectOptionsByEntity(compatibleSelectedGroups, incompatibleSelectedGroups, groupDataById); + const selectedMeterOptions = getSelectOptionsByEntity(compatibleSelectedMeters, incompatibleSelectedMeters, meterDataById, selectSelectedLanguage); + const selectedGroupOptions = getSelectOptionsByEntity(compatibleSelectedGroups, incompatibleSelectedGroups, groupDataById, selectSelectedLanguage); // All selected values even if not graph-able. Non compatible will be visually marked as disabled in custom react-select component(s) const allSelectedMeterValues = selectedMeterOptions.compatible.concat(selectedMeterOptions.incompatible); const allSelectedGroupValues = selectedGroupOptions.compatible.concat(selectedGroupOptions.incompatible); // List of options with metadata for react-select independent of currently selected. (Used to Populate the Select List(s)) - const meterSelectOptions = getSelectOptionsByEntity(compatibleMeters, incompatibleMeters, meterDataById); - const groupSelectOptions = getSelectOptionsByEntity(compatibleGroups, incompatibleGroups, groupDataById); + const meterSelectOptions = getSelectOptionsByEntity(compatibleMeters, incompatibleMeters, meterDataById, selectSelectedLanguage); + const groupSelectOptions = getSelectOptionsByEntity(compatibleGroups, incompatibleGroups, groupDataById, selectSelectedLanguage); // Format The generated selectOptions into grouped options for the React-Select component const meterGroupedOptions: GroupedOption[] = [ @@ -292,9 +292,10 @@ export const selectUnitSelectData = createAppSelector( selectVisibleUnitOrSuffixState, selectSelectedMeters, selectSelectedGroups, - selectGraphAreaNormalization + selectGraphAreaNormalization, + selectSelectedLanguage ], - (unitDataById, visibleUnitsOrSuffixes, selectedMeters, selectedGroups, areaNormalization) => { + (unitDataById, visibleUnitsOrSuffixes, selectedMeters, selectedGroups, areaNormalization, selectSelectedLanguage) => { // Holds all units that are compatible with selected meters/groups const compatibleUnits = new Set(); // Holds all units that are not compatible with selected meters/groups @@ -345,7 +346,7 @@ export const selectUnitSelectData = createAppSelector( } // Ready to display unit. Put selectable ones before non-selectable ones. - const unitOptions = getSelectOptionsByEntity(compatibleUnits, incompatibleUnits, unitDataById); + const unitOptions = getSelectOptionsByEntity(compatibleUnits, incompatibleUnits, unitDataById, selectSelectedLanguage); const unitsGroupedOptions: GroupedOption[] = [ { label: 'Units', @@ -366,12 +367,14 @@ export const selectUnitSelectData = createAppSelector( * @param compatibleItems - compatible items to make select options for * @param incompatibleItems - incompatible items to make select options for * @param entityDataById - current redux state, must be one of UnitsState, MetersState, or GroupsState + * @param locale - the current selected language taken from redux state * @returns Two Lists: Compatible, and Incompatible selectOptions for use as grouped React-Select options */ export function getSelectOptionsByEntity( compatibleItems: Set, incompatibleItems: Set, - entityDataById: MeterDataByID | GroupDataByID | UnitDataById + entityDataById: MeterDataByID | GroupDataByID | UnitDataById, + locale: LanguageTypes ) { //The final list of select options to be displayed const compatibleItemOptions = Object.entries(entityDataById) @@ -409,9 +412,10 @@ export function getSelectOptionsByEntity( defaultGraphicUnit: defaultGraphicUnit } as SelectOption; }); - - const compatible = sortBy(compatibleItemOptions, item => item.label.toLowerCase(), 'asc'); - const incompatible = sortBy(incompatibleItemOptions, item => item.label.toLowerCase(), 'asc'); + const compatible = compatibleItemOptions.sort((itemA, itemB) => itemA.label.toLowerCase(). + localeCompare(itemB.label.toLowerCase(), String(locale), { sensitivity: 'accent' })); + const incompatible = incompatibleItemOptions.sort((itemA, itemB) => itemA.label.toLowerCase()?. + localeCompare(itemB.label.toLowerCase(), String(locale), { sensitivity: 'accent' })); return { compatible, incompatible }; } @@ -463,7 +467,7 @@ export const selectChartLink = createAppSelector( linkText += `&serverRange=${current.queryTimeInterval.toString()}`; switch (current.chartToRender) { case ChartTypes.bar: - linkText += `&barDuration=${current.barDuration.asDays()}`; + linkText += `&duration=${current.duration.asDays()}`; linkText += `&barStacking=${current.barStacking}`; break; case ChartTypes.line: diff --git a/src/client/app/redux/slices/graphSlice.ts b/src/client/app/redux/slices/graphSlice.ts index 1114225e4..91e38edd2 100644 --- a/src/client/app/redux/slices/graphSlice.ts +++ b/src/client/app/redux/slices/graphSlice.ts @@ -25,8 +25,7 @@ const defaultState: GraphState = { selectedAreaUnit: AreaUnitType.none, queryTimeInterval: TimeInterval.unbounded(), rangeSliderInterval: TimeInterval.unbounded(), - barDuration: moment.duration(4, 'weeks'), - mapsBarDuration: moment.duration(4, 'weeks'), + duration: moment.duration(4, 'weeks'), comparePeriod: ComparePeriod.Week, compareTimeInterval: calculateCompareTimeInterval(ComparePeriod.Week, moment()), compareSortingOrder: SortingOrder.Descending, @@ -79,11 +78,8 @@ export const graphSlice = createSlice({ updateSelectedAreaUnit: (state, action: PayloadAction) => { state.current.selectedAreaUnit = action.payload; }, - updateBarDuration: (state, action: PayloadAction) => { - state.current.barDuration = action.payload; - }, - updateMapsBarDuration: (state, action: PayloadAction) => { - state.current.mapsBarDuration = action.payload; + updateDuration: (state, action: PayloadAction) => { + state.current.duration = action.payload; }, updateTimeInterval: (state, action: PayloadAction) => { // always update if action is bounded, else only set unbounded if current isn't already unbounded. @@ -298,8 +294,8 @@ export const graphSlice = createSlice({ case 'areaUnit': current.selectedAreaUnit = value as AreaUnitType; break; - case 'barDuration': - current.barDuration = moment.duration(parseInt(value), 'days'); + case 'duration': + current.duration = moment.duration(parseInt(value), 'days'); break; case 'barStacking': current.barStacking = value === 'true'; @@ -372,8 +368,7 @@ export const graphSlice = createSlice({ selectThreeDState: state => state.current.threeD, selectShowMinMax: state => state.current.showMinMax, selectBarStacking: state => state.current.barStacking, - selectBarWidthDays: state => state.current.barDuration, - selectMapBarWidthDays: state => state.current.mapsBarDuration, + selectWidthDays: state => state.current.duration, selectAreaUnit: state => state.current.selectedAreaUnit, selectSelectedUnit: state => state.current.selectedUnit, selectChartToRender: state => state.current.chartToRender, @@ -401,7 +396,7 @@ export const { selectAreaUnit, selectShowMinMax, selectGraphState, selectPrevHistory, selectThreeDState, selectBarStacking, - selectSortingOrder, selectBarWidthDays, + selectSortingOrder, selectWidthDays, selectSelectedUnit, selectLineGraphRate, selectComparePeriod, selectChartToRender, selectForwardHistory, selectSelectedMeters, @@ -410,8 +405,7 @@ export const { selectThreeDMeterOrGroupID, selectThreeDReadingInterval, selectGraphAreaNormalization, selectSliderRangeInterval, selectDefaultGraphState, selectHistoryIsDirty, - selectPlotlySliderMax, selectPlotlySliderMin, - selectMapBarWidthDays + selectPlotlySliderMax, selectPlotlySliderMin } = graphSlice.selectors; // actionCreators exports @@ -419,7 +413,7 @@ export const { setShowMinMax, setGraphState, setBarStacking, toggleShowMinMax, changeBarStacking, resetTimeInterval, - updateBarDuration, changeSliderRange, + updateDuration, changeSliderRange, updateTimeInterval, updateSelectedUnit, changeChartToRender, updateComparePeriod, updateSelectedMeters, updateLineGraphRate, @@ -428,6 +422,6 @@ export const { toggleAreaNormalization, updateThreeDMeterOrGroup, changeCompareSortingOrder, updateThreeDMeterOrGroupID, updateThreeDReadingInterval, updateThreeDMeterOrGroupInfo, - updateSelectedMetersOrGroups, updateMapsBarDuration + updateSelectedMetersOrGroups } = graphSlice.actions; diff --git a/src/client/app/translations/data.ts b/src/client/app/translations/data.ts index 75c9e7c09..91795e891 100644 --- a/src/client/app/translations/data.ts +++ b/src/client/app/translations/data.ts @@ -29,7 +29,6 @@ const LocaleTranslationData = { "as.meter.unit": "as meter unit", "as.meter.defaultgraphicunit": "as meter default graphic unit", "bar": "Bar", - "bar.days.enter": "Enter in days and then hit enter", "bar.interval": "Bar Interval", "bar.raw": "Cannot create bar graph on raw units such as temperature", "bar.stacking": "Bar Stacking", @@ -49,6 +48,7 @@ const LocaleTranslationData = { "clipboard.not.copied": "Failed to Copy To Clipboard", "close": "Close", "compare": "Compare", + "compare.period": "Compare Period", "compare.raw": "Cannot create comparison graph on raw units such as temperature", "confirm.action": "Confirm Action", "contact.us": "Contact us", @@ -86,7 +86,11 @@ const LocaleTranslationData = { "create.user": "Create a User", "create.unit": "Create a Unit", "csv": "CSV", - "csv.file": "CSV File", + "csvMeters": "CSV Meters", + "csvReadings": "CSV Readings", + "csv.file": "CSV File:", + "csv.file.error": "File must be in CSV format or GZIP format (.csv or .gz). ", + "csv.clear.button": "Clear Form", "csv.common.param.gzip": "Gzip", "csv.common.param.header.row": "Header Row", "csv.common.param.update": "Update", @@ -94,32 +98,24 @@ const LocaleTranslationData = { "csv.download.size.warning.size": "Total size of all files will be about (usually within 10% for large exports).", "csv.download.size.warning.verify": "Are you sure you want to download", "csv.readings.param.create.meter": "Create Meter", - "csv.readings.param.cumulative": "Cumulative", - "csv.readings.param.cumulative.reset": "Cumulative Reset", - "csv.readings.param.cumulative.reset.end": "Cumulative Reset End", - "csv.readings.param.cumulative.reset.start": "Cumulative Reset Start", - "csv.readings.param.duplications": "Duplications", - "csv.readings.param.endOnly": "End Only times", "csv.readings.param.honor.dst": "Honor Daylight Savings Time", - "csv.readings.param.lengthGap": "Length Gap", - "csv.readings.param.length.variation": "Length Variation", - "csv.readings.param.meter.name": "Meter name", - "csv.readings.param.refresh.hourlyReadings": "Refresh Hourly Readings", - "csv.readings.param.refresh.readings": "Refresh Daily Readings", + "csv.readings.param.meter.identifier": "Meter Identifier:", + "csv.readings.param.refresh.readings": "Refresh Readings", "csv.readings.param.relaxed.parsing": "Relaxed Parsing", - "csv.readings.param.time.sort": "Time Sort", + "csv.readings.param.time.sort": "Time Sort:", "csv.readings.param.use.meter.zone": "Use Meter Zone", "csv.readings.section.cumulative.data": "Cumulative Data", "csv.readings.section.time.gaps": "Time Gaps", "csv.submit.button": "Submit CSV Data", "csv.tab.meters": "Meters", "csv.tab.readings": "Readings", - "csv.upload.meters": "Upload Meters CSV ", - "csv.upload.readings": "Upload Readings CSV ", + "csv.upload.meters": "Upload Meters", + "csv.upload.readings": "Upload Readings", "custom.value": "Custom value", "date.range": 'Date Range', "day": "Day", "days": "Days", + "days.enter": "Enter in days and then hit enter", "decreasing": "decreasing", "default.area.normalize": "Normalize readings by area by default", "default.area.unit": "Default Area Unit", @@ -229,11 +225,13 @@ const LocaleTranslationData = { "help.admin.metercreate": "This page allows admins to create meters. Please visit {link} for further details and information.", "help.admin.meteredit": "This page allows admins to edit meters. Please visit {link} for further details and information.", "help.admin.meterview": "This page allows admins to view and edit meters. Please visit {link} for further details and information.", + "help.admin.unitconversionvisuals": "The \"Input Units Visual Graphic\" visually shows the units and conversions made by admins. The \"Analyzed Units Visual Graphic\" visually shows the analysis OED does on the units and conversions entered by admins. Please visit {link} for further details and information.", "help.admin.unitcreate": "This page allows admins to create units. Please visit {link} for further details and information.", "help.admin.unitedit": "This page allows admins to edit units. Please visit {link} for further details and information.", "help.admin.unitview": "This page shows information on units. Please visit {link} for further details and information.", "help.admin.users": "This page allows admins to view and edit users. Please visit {link} for further details and information.", - "help.csv.header": "This page allows certain users to upload meters and readings via a CSV file. Please visit {link} for further details and information.", + "help.csv.meters": "This page allows admins to upload meters via a CSV file. Please visit {link} for further details and information.", + "help.csv.readings": "This page allows certain users to upload readings via a CSV file. Please visit {link} for further details and information.", "help.groups.groupdetails": "This page shows detailed information on a group. Please visit {link} for further details and information.", "help.groups.groupview": "This page shows information on groups. Please visit {link} for further details and information.", "help.groups.area.calculate": "This will sum together the area of all meters in this group with a nonzero area with an area unit. It will ignore any meters which have no area or area unit. If this group has no area unit, it will do nothing.", @@ -245,7 +243,7 @@ const LocaleTranslationData = { "help.home.chart.plotly.controls": "These controls are provided by Plotly, the graphics package used by OED. You generally do not need them but they are provided in case you want that level of control. Note that some of these options may not interact nicely with OED features. See Plotly documentation at {link}.", "help.home.chart.redraw.restore": "OED automatically averages data when necessary so the graphs have a reasonable number of points. If you use the controls under the graph to scroll and/or zoom, you may find the resolution at this averaged level is not what you desire. Clicking the \"Redraw\" button will have OED recalculate the averaging and bring in higher resolution for the number of points it displays. If you want to restore the graph to the full range of dates, then click the \"Restore\" button. Please visit {link} for further details and information.", "help.home.chart.select": "Any graph type can be used with any combination of groups and meters. Line graphs show the usage (e.g., kW) vs. time. You can zoom and scroll with the controls right below the graph. Bar shows the total usage (e.g., kWh) for the time frame of each bar where you can control the time frame. Compare allows you to see the current usage vs. the usage in the last previous period for a day, week and four weeks. Map graphs show a spatial image of each meter where the circle size is related to four weeks of usage. 3D graphs show usage vs. day vs. hours in the day. Clicking on one of the choices renders that graphic. Please visit {link} for further details and information.", - "help.home.compare.interval.tip": "Selects the time interval (Day, Week or 4 Weeks) to compare for current to previous. Please see {link} for further details and information.", + "help.home.compare.period.tip": "Selects the time interval (Day, Week or 4 Weeks) to compare for current to previous. Please see {link} for further details and information.", "help.home.compare.sort.tip": "Allows user to select the order of multiple comparison graphs to be Alphabetical (by name), Ascending (greatest to least reduction in usage) and Descending (least to greatest reduction in usage). Please see {link} for further details and information.", "help.home.error.bar": "Toggle error bars with min and max value. Please visit {link} for further details and information.", "help.home.export.graph.data": "With the \"Export graph data\" button, one can export the data for the graph when viewing either a line or bar graphic. The zoom and scroll feature on the line graph allows you to control the time frame of the data exported. The \"Export graph data\" button gives the data points for the graph and not the original meter data. The \"Export graph meter data\" gives the underlying meter data (line graphs only). Please visit {link} for further details and information on when meter data export is allowed.", @@ -285,6 +283,10 @@ const LocaleTranslationData = { "last.four.weeks": "Last four weeks", "last.week": "Last week", "leave": "Leave", + "legend.graph.text.meter": "Meter", + "legend.graph.text.suffix": "Suffix Analyzed", + "legend.graph.text.suffix.input": "Suffix Input", + "legend.graph.text.unit": "Unit", "less.energy": "less energy", "line": "Line", "log.in": "Log in", @@ -332,6 +334,8 @@ const LocaleTranslationData = { "meter.cumulativeReset": "Cumulative Reset:", "meter.cumulativeResetEnd": "Cumulative Reset End:", "meter.cumulativeResetStart": "Cumulative Reset Start:", + "meter.edit.displayable.warning": "is not displayable but is used by the following displayable groups:", + "meter.edit.displayable.verify": "Given the group(s) listed above, do you want to cancel this change (click Cancel) or continue (click OK)?", "meter.enabled": "Updates:", "meter.endOnlyTime": "Only End Times:", "meter.endTimeStamp": "End Time Stamp:", @@ -396,10 +400,16 @@ const LocaleTranslationData = { "radar.lines.incompatible": "These meters/groups are not compatible for radar graphs", "radar.no.data": "There are no readings:
likely the data range is outside meter/group reading range", "rate": "Rate", + "rate.limit.error.first": "You have been rate limited by your OED site", + "rate.limit.error.second": "We suggest you try these in this order:", "reading": "Reading:", "redo.cik.and.refresh.db.views": "Processing changes. This may take a while.", "readings.per.day": "Readings per Day", "redraw": "Redraw", + "refresh.page.first": "Click the Refresh this page' button below to try again", + "refresh.page.second": "If you keep returning to this page wait longer and click 'Refresh this page' button", + "refresh.page.third": "Contact your site to find why the rate limit is denying access to the OED site", + "refresh.window": "Refresh this page", "remove": "Remove", "restore": "Restore", "return.dashboard": "Return To Dashboard", @@ -420,7 +430,7 @@ const LocaleTranslationData = { "show.options": "Show options", "site.settings": "Site Settings", "site.title": "Site Title", - "sort": "Sort", + "sort": "Sort Order", "submit": "Submit", "submitting": "submitting", "submit.changes": "Submit changes", @@ -439,7 +449,6 @@ const LocaleTranslationData = { "threeD.y.axis.label": "Days of Calendar Year", "TimeSortTypes.decreasing": "decreasing", "TimeSortTypes.increasing": "increasing", - "TimeSortTypes.meter": "meter value or default", "today": "Today", "toggle.link": "Toggle chart link", "toggle.options" : "Toggle options", @@ -478,6 +487,7 @@ const LocaleTranslationData = { "unit.type.of.unit": "Type of Unit:", "unit.type.of.unit.suffix": "Added suffix will set type of unit to suffix", "units": "Units", + "units.conversion.page.title": "Units and Conversions Visual Graphics", "unsaved.failure": "Changes failed to save", "unsaved.success": "Changes saved", "unsaved.warning": "You have unsaved change(s). Are you sure you want to leave?", @@ -509,6 +519,9 @@ const LocaleTranslationData = { "uses": "uses", "view.groups": "View Groups", "visit": " or visit our ", + "visual.unit": "Units Visual Graphics", + "visual.input.units.graphic": "Input Units Visual Graphic", + "visual.analyzed.units.graphic": "Analyzed Units Visual Graphic", "website": "website", "week": "Week", "yes": "yes", @@ -538,7 +551,6 @@ const LocaleTranslationData = { "as.meter.unit": "as meter unit\u{26A1}", "as.meter.defaultgraphicunit": "as meter default graphic unit\u{26A1}", "bar": "Bande", - "bar.days.enter": "Enter in days and then hit enter\u{26A1}", "bar.interval": "Intervalle du Diagramme à Bandes", "bar.raw": "Cannot create bar graph on raw units such as temperature\u{26A1}", "bar.stacking": "Empilage de Bandes", @@ -558,6 +570,7 @@ const LocaleTranslationData = { "clipboard.not.copied": "Failed to Copy To Clipboard\u{26A1}", "close": "Close\u{26A1}", "compare": "Comparer", + "compare.period": "Compare Period\u{26A1}", "compare.raw": "Cannot create comparison graph on raw units such as temperature\u{26A1}", "confirm.action": "Confirm Action\u{26A1}", "contact.us": "Contactez nous", @@ -595,40 +608,36 @@ const LocaleTranslationData = { "create.unit": "Create a Unit\u{26A1}", "create.user": "Créer un utilisateur", "csv": "CSV", - "csv.file": "Fichier CSV", - "csv.common.param.gzip": "GzipComment", + "csvMeters": "CSV Meters\u{26A1}", + "csvReadings": "CSV Readings\u{26A1}", + "csv.file": "Fichier CSV:", + "csv.file.error": "Le fichier doit être au format CSV ou GZIP (.csv ou .gz). ", + "csv.clear.button": "Forme claire", + "csv.common.param.gzip": "Gzip", "csv.common.param.header.row": "Ligne d'en-tête", "csv.common.param.update": "Mise à jour", "csv.download.size.limit": "Sorry you don't have permissions to download due to large number of points.\u{26A1}", "csv.download.size.warning.size": "Total size of all files will be about (usually within 10% for large exports).\u{26A1}", "csv.download.size.warning.verify": "Are you sure you want to download\u{26A1}", "csv.readings.param.create.meter": "Créer un compteur", - "csv.readings.param.cumulative": "Cumulatif", - "csv.readings.param.cumulative.reset": "Réinitialisation cumulative", - "csv.readings.param.cumulative.reset.end": "Fin de la réinitialisation cumulative", - "csv.readings.param.cumulative.reset.start": "Début de réinitialisation cumulée", - "csv.readings.param.duplications": "Duplications", - "csv.readings.param.endOnly": "Heures de fin uniquement", "csv.readings.param.honor.dst": "Honor Daylight Savings Time\u{26A1}", - "csv.readings.param.lengthGap": "Écart de longueur", - "csv.readings.param.length.variation": "Variation de longueur", - "csv.readings.param.meter.name": "Nom du compteur", - "csv.readings.param.refresh.hourlyReadings": "Actualiser les relevés horaires", - "csv.readings.param.refresh.readings": "Actualiser les lectures quotidiennes", + "csv.readings.param.meter.identifier": "Identifiant du compteur:", + "csv.readings.param.refresh.readings": "Actualiser les lectures", "csv.readings.param.relaxed.parsing": "Relaxed Parsing\u{26A1}", - "csv.readings.param.time.sort": "Tri de l'heure", + "csv.readings.param.time.sort": "Tri de l'heure:", "csv.readings.param.use.meter.zone": "Use Meter Zone\u{26A1}", "csv.readings.section.cumulative.data": "Données cumulées", "csv.readings.section.time.gaps": "Intervalles de temps", "csv.submit.button": "Soumettre les données CSV", "csv.tab.meters": "Mètres", "csv.tab.readings": "Lectures", - "csv.upload.meters": "Télécharger les compteurs au format CSV", - "csv.upload.readings": "Télécharger les lectures CSV", + "csv.upload.meters": "Téléverser Mètres", + "csv.upload.readings": "Téléverser Lectures", "custom.value": "Custom value\u{26A1}", "date.range": 'Plage de dates', "day": "Journée", "days": "Journées", + "days.enter": "Enter in days and then hit enter\u{26A1}", "decreasing": "decreasing\u{26A1}", "default.area.normalize": "Normalize readings by area by default\u{26A1}", "default.area.unit": "Default Area Unit\u{26A1}", @@ -711,7 +720,7 @@ const LocaleTranslationData = { "group.edit.empty": "Removing this meter/group means there are no child meters or groups which is not allowed. Delete the group if you want to remove it.\u{26A1}", "group.edit.nocompatible": "would have no compatible units by the edit to this group so the edit is cancelled\u{26A1}", "group.edit.nounit": "will have its compatible units changed and its default graphic unit set to \"no unit\" by the edit to this group\u{26A1}", - "group.edit.verify": "or continue (click OK)?\u{26A1}", + "group.edit.verify": "Given the messages, do you want to cancel this change (click Cancel) or continue (click OK)?\u{26A1}", "group.failed.to.create.group": "Failed to create a group with message: \u{26A1}", "group.failed.to.edit.group": "Failed to edit group with message: \u{26A1}", "group.gps.error": "Please input a valid GPS: (latitude, longitude)\u{26A1}", @@ -738,11 +747,13 @@ const LocaleTranslationData = { "help.admin.metercreate": "This page allows admins to create meters. Please visit {link} for further details and information.\u{26A1}", "help.admin.meteredit": "This page allows admins to edit meters. Please visit {link} for further details and information.\u{26A1}", "help.admin.meterview": "This page allows admins to view and edit meters. Please visit {link} for further details and information.\u{26A1}", + "help.admin.unitconversionvisuals": "The \"Input Units Visual Graphic\" visually shows the units and conversions made by admins. The \"Analyzed Units Visual Graphic\" visually shows the analysis OED does on the units and conversions entered by admins. Please visit {link} for further details and information.\u{26A1}", "help.admin.unitcreate": "This page allows admins to create units. Please visit {link} for further details and information.\u{26A1}", "help.admin.unitedit": "This page allows admins to edit units. Please visit {link} for further details and information.", "help.admin.unitview": "This page shows information on units. Please visit {link} for further details and information.\u{26A1}", "help.admin.users": "This page allows admins to view and edit users. Please visit {link} for further details and information.\u{26A1}", - "help.csv.header": "This page allows certain users to upload meters and readings via a CSV file. Please visit {link} for further details and information.\u{26A1}", + "help.csv.meters": "Cette page permet aux administrateurs de téléverser des mètres via un fichier CSV. Veuillez visiter {link} pour plus de détails et d'informations.\u{26A1}", + "help.csv.readings": "Cette page permet à certains utilisateurs de télécharger des lectures via un fichier CSV. Veuillez visiter {link} pour plus de détails et d'informations.\u{26A1}", "help.groups.groupdetails": "This page shows detailed information on a group. Please visit {link} for further details and information.\u{26A1}", "help.groups.groupview": "This page shows information on groups. Please visit {link} for further details and information.\u{26A1}", "help.groups.area.calculate": "This will sum together the area of all meters in this group with a nonzero area with an area unit. It will ignore any meters which have no area or area unit. If this group has no area unit, it will do nothing.\u{26A1}", @@ -754,7 +765,7 @@ const LocaleTranslationData = { "help.home.chart.plotly.controls": "These controls are provided by Plotly, the graphics package used by OED. You generally do not need them but they are provided in case you want that level of control. Note that some of these options may not interact nicely with OED features. See Plotly documentation at {link}.\u{26A1}", "help.home.chart.redraw.restore": "OED automatically averages data when necessary so the graphs have a reasonable number of points. If you use the controls under the graph to scroll and/or zoom, you may find the resolution at this averaged level is not what you desire. Clicking the \"Redraw\" button will have OED recalculate the averaging and bring in higher resolution for the number of points it displays. If you want to restore the graph to the full range of dates, then click the \"Restore\" button. Please visit {link} for further details and information.\u{26A1}", "help.home.chart.select": "for the time frame of each bar where you can control the time frame. Compare allows you to see the current usage vs. the usage in the last previous period for a day, week and four weeks. Map graphs show a spatial image of each meter where the circle size is related to four weeks of usage. 3D graphs show usage vs. day vs. hours in the day. Clicking on one of the choices renders that graphic. Please visit {link} for further details and information.\u{26A1}", - "help.home.compare.interval.tip": "to compare for current to previous. Please see {link} for further details and information.\u{26A1}", + "help.home.compare.period.tip": "to compare for current to previous. Please see {link} for further details and information.\u{26A1}", "help.home.compare.sort.tip": "and Descending (least to greatest reduction in usage). Please see {link} for further details and information.\u{26A1}", "help.home.error.bar": "Toggle error bars with min and max value. Please visit {link} for further details and information.\u{26A1}", "help.home.export.graph.data": "With the \"Export graph data\" button, one can export the data for the graph when viewing either a line or bar graphic. The zoom and scroll feature on the line graph allows you to control the time frame of the data exported. The \"Export graph data\" button gives the data points for the graph and not the original meter data. The \"Export graph meter data\" gives the underlying meter data (line graphs only). Please visit {link} for further details and information on when meter data export is allowed.\u{26A1}", @@ -794,6 +805,10 @@ const LocaleTranslationData = { "last.four.weeks": "Quatre dernières semaines", "last.week": "La semaine dernière", "leave": "Leave\u{26A1}", + "legend.graph.text.meter": "Meter\u{26A1}", + "legend.graph.text.suffix": "Suffix Analyzed\u{26A1}", + "legend.graph.text.suffix.input": "Suffix Input\u{26A1}", + "legend.graph.text.unit": "Unit\u{26A1}", "less.energy": "moins d'énergie", "line": "Ligne", "log.in": "Se Connecter", @@ -841,6 +856,8 @@ const LocaleTranslationData = { "meter.cumulativeReset": "Cumulative Reset:\u{26A1}", "meter.cumulativeResetEnd": "Cumulative Reset End:\u{26A1}", "meter.cumulativeResetStart": "Cumulative Reset Start:\u{26A1}", + "meter.edit.displayable.warning": "is not displayable but is used by the following displayable groups:\u{26A1}", + "meter.edit.displayable.verify": "Given the group(s) listed above, do you want to cancel this change (click Cancel) or continue (click OK)?\u{26A1}", "meter.enabled": "Mises à Jour du Mèters", "meter.endOnlyTime": "End Only Time:\u{26A1}", "meter.endTimeStamp": "End Time Stamp:\u{26A1}", @@ -905,10 +922,16 @@ const LocaleTranslationData = { "radar.lines.incompatible": "These meters/groups are not compatible for radar graphs\u{26A1}", "radar.no.data": "There are no readings:
likely the data range is outside meter/group reading range\u{26A1}", "rate": "Rate\u{26A1}", + "rate.limit.error.first": "You have been rate limited by your OED site\u{26A1}", + "rate.limit.error.second": "We suggest you try these in this order:\u{26A1}", "reading": "Reading:\u{26A1}", "redo.cik.and.refresh.db.views": "Processing changes. This may take a while\u{26A1}", "readings.per.day": "Readings per Day\u{26A1}", "redraw": "Redessiner", + "refresh.page.first": "Click the Refresh this page' button below to try again\u{26A1}", + "refresh.page.second": "If you keep returning to this page wait longer and click 'Refresh this page' button\u{26A1}", + "refresh.page.third": "Contact your site to find why the rate limit is denying access to the OED site\u{26A1}", + "refresh.window": "Refresh this page\u{26A1}", "remove": "Remove\u{26A1}", "restore": "Restaurer", "return.dashboard": "Return To Dashboard\u{26A1}", @@ -929,7 +952,7 @@ const LocaleTranslationData = { "show.options": "Options de désancrage", "site.settings": "Site Settings\u{26A1}", "site.title": "Site Title\u{26A1}", - "sort": "Trier", + "sort": "Sort Order\u{26A1}", "submit": "Soumettre", "submitting": "submitting\u{26A1}", "submit.changes": "Soumettre les changements", @@ -948,7 +971,6 @@ const LocaleTranslationData = { "timezone.no": "Pas de fuseau horaire", "TimeSortTypes.decreasing": "décroissant", "TimeSortTypes.increasing": "en augmentant", - "TimeSortTypes.meter": "valeur du compteur ou valeur par défaut", "today": "Aujourd'hui", "toggle.link": "Bascule du lien du diagramme", "toggle.options" : "Basculer les options", @@ -987,6 +1009,7 @@ const LocaleTranslationData = { "unit.type.of.unit": "Type of Unit:\u{26A1}", "unit.type.of.unit.suffix": "Added suffix will set type of unit to suffix\u{26A1}", "units": "Units\u{26A1}", + "units.conversion.page.title": "Units and Conversions Visual Graphics\u{26A1}", "unsaved.failure": "Changes failed to save\u{26A1}", "unsaved.success": "Changes saved\u{26A1}", "unsaved.warning": "You have unsaved change(s). Are you sure you want to leave?\u{26A1}", @@ -1018,6 +1041,9 @@ const LocaleTranslationData = { "uses": "uses\u{26A1}", "view.groups": "Visionner les groupes", "visit": " ou visitez notre ", + "visual.unit": "Units Visual Graphics\u{26A1}", + "visual.input.units.graphic": "Graphique Visuel des Unités D'Entrée", + "visual.analyzed.units.graphic": "Graphique Visuel des Unités Analysées", "website": "site web", "week": "Semaine", "yes": " yes\u{26A1}", @@ -1047,7 +1073,6 @@ const LocaleTranslationData = { "as.meter.unit": "como unidad de medidor", "as.meter.defaultgraphicunit": "como unidad gráfica predeterminada del medidor", "bar": "Barra", - "bar.days.enter": "Ingrese los días y presione \"Enter\"", "bar.interval": "Intervalo de barra", "bar.raw": "No se puede crear un gráfico de barras con unidades crudas como la temperatura", "bar.stacking": "Apilamiento de barras", @@ -1067,6 +1092,7 @@ const LocaleTranslationData = { "clipboard.not.copied": "Error al copiar al portapapeles", "close": "Cerrar", "compare": "Comparar", + "compare.period": "Compare Period\u{26A1}", "compare.raw": "No se puede crear un gráfico de comparación con unidades crudas como temperatura", "confirm.action": "Confirmar acción", "contact.us": "Contáctenos", @@ -1104,7 +1130,11 @@ const LocaleTranslationData = { "create.unit": "Crear una unidad", "create.user": "Crear un usuario", "csv": "CSV", - "csv.file": "Archivo CSV", + "csvMeters": "CSV Meters\u{26A1}", + "csvReadings": "CSV Readings\u{26A1}", + "csv.file": "Archivo CSV:", + "csv.file.error": "El archivo debe estar en formato CSV o GZIP (.csv o .gz). ", + "csv.clear.button": "Forma clara", "csv.common.param.gzip": "Gzip", "csv.common.param.header.row": "Fila de cabecera", "csv.common.param.update": "Actualización", @@ -1112,20 +1142,11 @@ const LocaleTranslationData = { "csv.download.size.warning.size": "El tamaño todos los archivos juntos será de unos (usualmente dentro de 10% para exportaciones largas).", "csv.download.size.warning.verify": "Estás seguro que quieres descargar", "csv.readings.param.create.meter": "Crear medidor", - "csv.readings.param.cumulative": "Acumulado", - "csv.readings.param.cumulative.reset": "Reinicio acumulativo", - "csv.readings.param.cumulative.reset.end": "Fin del reinicio acumulativo", - "csv.readings.param.cumulative.reset.start": "Comienzo del reinicio acumulativo", - "csv.readings.param.duplications": "Duplicaciones", - "csv.readings.param.endOnly": "Únicamente tiempos finales", "csv.readings.param.honor.dst": "Seguir el horario de verano", - "csv.readings.param.lengthGap": "Duración entre mediciones", - "csv.readings.param.length.variation": "Variación de longitud de medición", - "csv.readings.param.meter.name": "Nombre de medidor", - "csv.readings.param.refresh.hourlyReadings": "Actualizar lecturas por hora", - "csv.readings.param.refresh.readings": "Actualizar lecturas diarias", + "csv.readings.param.meter.identifier": "Identificador del medidor:", + "csv.readings.param.refresh.readings": "Actualizar lecturas", "csv.readings.param.relaxed.parsing": "Análisis sintactico no estricto", - "csv.readings.param.time.sort": "Ordenar el tiempo", + "csv.readings.param.time.sort": "Ordenar el tiempo:", "csv.readings.param.use.meter.zone": "Use Meter Zone\u{26A1}", "csv.readings.section.cumulative.data": "Datos acumulativos", "csv.readings.section.time.gaps": "Longitud de tiempos", @@ -1138,6 +1159,7 @@ const LocaleTranslationData = { "date.range": 'Rango de fechas', "day": "Día", "days": "Días", + "days.enter": "Ingrese los días y presione \"Enter\"", "decreasing": "decreciente", "default.area.normalize": "Normalizar lecturas según el área por defecto", "default.area.unit": "Unidad de área predeterminada", @@ -1248,11 +1270,13 @@ const LocaleTranslationData = { "help.admin.metercreate": "Esta página permite a los administradores crear medidores. Por favor, visite {link} para más detalles e información.", "help.admin.meteredit": "Esta página permite a los administradores editar medidores. Por favor, visite {link} para más detalles e información.", "help.admin.meterview": "Esta página permite a los administradores ver y editar mediores. Por favor, visite {link} para más detalles e información.", + "help.admin.unitconversionvisuals": "El \"Gráfico Visual de Unidades de Entrada\" muestra visualmente las unidades y las conversiones ingresadas por los administradores. El \"Gráfico Visual de Unidades Analizadas\" muestra visualmente el análisis que realiza el OED en las unidades y las conversiones ingresadas por los administradores. Por favor, visite {link} para más detalles e información.", "help.admin.unitcreate": "Esta página permite a los administradores crear unidades. Por favor, visite {link} para más detalles e información.", "help.admin.unitedit": "Esta página permite a los administradores editar unidades Por favor, visite {link} para más detalles e información.", "help.admin.unitview": "Esta página muestra información sobre unidades. Por favor, visite {link} para más detalles e información.", "help.admin.users": "Esta página permite a los administradores ver y editar usarios. Por favor, visite {link} para más detalles e información.", - "help.csv.header": "Esta página permite unos usuarios subir medidores y lecturas a través de un archivo CSV. Por favor, visite {link} para más detalles e información.", + "help.csv.meters": "Esta página permite a los administradores cargar medidores a través de un archivo CSV. Visite {enlace} para obtener más detalles e información.", + "help.csv.readings": "Esta página permite a ciertos usuarios cargar lecturas a través de un archivo CSV. Visite {link} para obtener más detalles e información.", "help.groups.groupdetails": "Esta página muestra información detallada de un grupo. Por favor visite {link} para obtener más detalles e información.", "help.groups.groupview": "Esta página muestra información sobre grupos. Por favor, visite {link} para más detalles e información.", "help.groups.area.calculate": "Esto sumará el área de todos los medidores en este grupo que tengan un área distinta a cero y una unidad de área. Ignorará cualquier medidor que no tiene área o unidad de área. Si este grupo no tiene unidad de área, no hará nada.", @@ -1264,7 +1288,7 @@ const LocaleTranslationData = { "help.home.chart.plotly.controls": "Estos controles son proporcionados por Plotly, el paquete de gráficos utilizado por OED. Por lo general no se necesitan pero se proporcionan por si se desea ese nivel de control. Tenga en cuenta que es posible que algunas de estas opciones no interactúen bien con las funciones de OED. Consulte la documentación de Plotly en {link}.", "help.home.chart.redraw.restore": "OED automáticamente toma el promedio de los datos cuando es necesario para que los gráficos tengan un número razonable de puntos. Si usa los controles debajo del gráfico para desplazarse y / o acercarse, puede encontrar que la resolución en este nivel de promedio no es la que desea. Al hacer clic en el botón \"Redraw\" OED volverá a calcular el promedio y obtendrá una resolución más alta para el número de puntos que muestra. Si desea restaurar el gráfico al rango completo de fechas, haga clic en el botón \"Restore\" button. Por favor visite {link} para obtener más detalles e información.", "help.home.chart.select": "Se puede usar cualquier tipo de gráfico con cualquier combinación de grupos y medidores. Los gráficos de líneas muestran el uso (por ejemplo, kW) con el tiempo. Puede hacer zoom y desplazarse con los controles justo debajo del gráfico. La barra muestra el uso total (por ejemplo, kWh) para el período de tiempo de cada barra donde se puede controlar el período de tiempo. Comparar le permite ver el uso actual comparado con el uso del período anterior durante un día, una semana y cuatro semanas. Los gráficos del mapa muestran una imagen espacial de cada medidor donde el tamaño del círculo está relacionado con cuatro semanas de uso. Las gráficas 3D muestran el uso por día y el uso por hora del día. Hacer clic en uno de estas opciones las registra en ese gráfico. Por favor visite {link} para obtener más detalles e información.", - "help.home.compare.interval.tip": "Selecciona el intervalo de tiempo (día, semana o 4 semanas) para comparar el actual con el anterior. Por favor, visite {link} para más detalles e información.", + "help.home.compare.period.tip": "Selecciona el intervalo de tiempo (día, semana o 4 semanas) para comparar el actual con el anterior. Por favor, visite {link} para más detalles e información.", "help.home.compare.sort.tip": "Permite al usuario seleccionar el orden de múltiples gráficos de comparación de forma alfabética (por nombre), ascendente (de mayor a menor reducción de uso) y descendente (de menor a mayor reducción de uso). Por favor, visite {link} para más detalles e información.", "help.home.error.bar": "Alternar barras de error con el valor mínimo y máximo. Por favor, vea {link} para más detalles e información.", "help.home.export.graph.data": "Con el botón \"Exportar datos del gráfico\", uno puede exportar los datos del gráfico al ver una línea o barra el gráfico. La función de zoom y desplazamiento en el gráfico de líneas le permite controlar el período de tiempo de los datos exportados. El botón \"Exportar data de gráfico\" da los puntos de datos para el gráfico y no los datos originales del medidor. \"Exportar el dato gráfhico de medidor\" proporciona los datos subyacentes del medidor (solo gráficos de líneas). Por favor visite {link} para obtener más detalles e información.", @@ -1304,6 +1328,10 @@ const LocaleTranslationData = { "last.four.weeks": "Últimas cuatro semanas", "last.week": "La semana pasada", "leave": "Salir", + "legend.graph.text.meter": "Medidor", + "legend.graph.text.suffix": "Sufijo Analizado", + "legend.graph.text.suffix.input": "Sufijo Entrado", + "legend.graph.text.unit": "Unidad", "less.energy": "menos energía", "line": "Línea", "log.in": "Iniciar sesión", @@ -1351,6 +1379,8 @@ const LocaleTranslationData = { "meter.cumulativeReset": "Reinicio cumulativo:", "meter.cumulativeResetEnd": "Final del reinicio cumulativo:", "meter.cumulativeResetStart": "Comienzo del reinicio cumulativo:", + "meter.edit.displayable.warning": "is not displayable but is used by the following displayable groups:\u{26A1}", + "meter.edit.displayable.verify": "Given the group(s) listed above, do you want to cancel this change (click Cancel) or continue (click OK)?\u{26A1}", "meter.enabled": "Medidor activado", "meter.endOnlyTime": "Solo tiempos finales.", "meter.endTimeStamp": "Marca de tiempo al final:", @@ -1415,10 +1445,16 @@ const LocaleTranslationData = { "radar.lines.incompatible": "Estos medidores/grupos no son compatibles para gráficos de radares", "radar.no.data": "No hay lecturas:
es probable que el rango de los datos esté fuera del rango de lecturas de los medidores/grupos", "rate": "Tasa", + "rate.limit.error.first": "You have been rate limited by your OED site\u{26A1}", + "rate.limit.error.second": "We suggest you try these in this order:\u{26A1}", "reading": "Lectura:", "redo.cik.and.refresh.db.views": "Procesando los cambios. Esto tardará un momento.", "readings.per.day": "Lecturas por día", "redraw": "Redibujar", + "refresh.page.first": "Click the Refresh this page' button below to try again\u{26A1}", + "refresh.page.second": "If you keep returning to this page wait longer and click 'Refresh this page' button\u{26A1}", + "refresh.page.third": "Contact your site to find why the rate limit is denying access to the OED site\u{26A1}", + "refresh.window": "Refresh this page\u{26A1}", "remove": "Eliminar", "restore": "Restaurar", "return.dashboard": "Regresar al panel principal", @@ -1439,7 +1475,7 @@ const LocaleTranslationData = { "show.options": "Mostrar opciones", "site.settings": "Site Settings\u{26A1}", "site.title": "Site Title\u{26A1}", - "sort": "Ordenar", + "sort": "Sort Order\u{26A1}", "submit": "Enviar", "submitting": "Enviando", "submit.changes": "Ingresar los cambios", @@ -1458,7 +1494,6 @@ const LocaleTranslationData = { "threeD.y.axis.label": "Días del año calendario", "TimeSortTypes.decreasing": "decreciente", "TimeSortTypes.increasing": "creciente", - "TimeSortTypes.meter": "valor del medidor o predeterminado", "today": "Hoy", "toggle.link": "Alternar enlace de gráfico", "toggle.options" : "Alternar opciones", @@ -1497,6 +1532,7 @@ const LocaleTranslationData = { "unit.type.of.unit": "Tipo de unidad:", "unit.type.of.unit.suffix": "El sufijo agregado determina que el tipo de la unidad es sufijo", "units": "Unidades", + "units.conversion.page.title": "Gráficos Visuales de Unidades y Conversiones", "unsaved.failure": "No se pudieron guardar los cambios", "unsaved.success": "Se guardaron los cambios", "unsaved.warning": "Tienes cambios sin guardar. ¿Estás seguro que quieres salir?", @@ -1528,6 +1564,9 @@ const LocaleTranslationData = { "uses": "uses\u{26A1}", "view.groups": "Ver grupos", "visit": " o visite nuestro ", + "visual.unit": "Units Visual Graphics\u{26A1}", + "visual.input.units.graphic": "Gráfico Visual de Unidades de Entrada", + "visual.analyzed.units.graphic": "Gráfico Visual de Unidades Analizadas", "website": "sitio web", "week": "semana", "yes": "sí", diff --git a/src/client/app/types/csvUploadForm.ts b/src/client/app/types/csvUploadForm.ts index aa1cd8d84..e85217c01 100644 --- a/src/client/app/types/csvUploadForm.ts +++ b/src/client/app/types/csvUploadForm.ts @@ -2,118 +2,33 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { MODE } from '../containers/csv/UploadCSVContainer'; +import { MeterTimeSortType } from '../types/redux/meters'; -interface CSVUploadPreferences { - meterName: string; +export interface CSVUploadPreferences { + meterIdentifier: string; gzip: boolean; headerRow: boolean; update: boolean; } -// Very similar to CSVUploadPreferences but uses a different Boolean type that is expected when the form is submitted. -export interface CSVUploadPreferencesForm { - meterName: string; - gzip: BooleanTypes; - headerRow: BooleanTypes; - update: BooleanTypes; -} - -interface CSVUploadProps extends CSVUploadPreferences { - submitCSV: (file: File) => Promise; - setMeterName: (mode: MODE, value: string) => void; - toggleGzip: () => void; - toggleHeaderRow: () => void; - toggleUpdate: () => void; -} - -// This relates to MeterTimeSortTypes in src/client/app/types/redux/meters.ts but also has 'meter value or default'. -// They should be kept in sync. -export const enum TimeSortTypes { - // Normally the values here are not used when displayed to user but the ones in data.js so translated. - increasing = 'increasing', - decreasing = 'decreasing', - // meter means to use value stored on meter or the default if not. - meter = 'meter value or default' -} - -export const enum BooleanTypes { - // Normally the values here are not used when displayed to user but the ones in data.js so translated. - true = 'yes', - false = 'no' -} - -// Unusual boolean that also allows for meter so 3-way. -export const enum BooleanMeterTypes { - // Normally the values here are not used when displayed to user but the ones in data.js so translated. - true = 'yes', - false = 'no', - // meter means to use value stored on meter or the default if not. - meter = 'meter value or default' -} - -export interface ReadingsCSVUploadPreferencesItem extends CSVUploadPreferences { - createMeter: boolean; - cumulative: BooleanMeterTypes; - cumulativeReset: BooleanMeterTypes; +export interface ReadingsCSVUploadPreferences extends CSVUploadPreferences { + cumulative: boolean; + cumulativeReset: boolean; cumulativeResetStart: string; cumulativeResetEnd: string; - duplications: string; // Not sure how to type this an integer string; - meterName: string; - lengthGap: string; - lengthVariation: string; - endOnly: BooleanMeterTypes; - refreshHourlyReadings: boolean; - refreshReadings: boolean; - timeSort: TimeSortTypes; + duplications: number; + endOnly: boolean; honorDst: boolean; + lengthGap: number; + lengthVariation: number; + meterIdentifier: string; + refreshReadings: boolean; relaxedParsing: boolean; + timeSort: MeterTimeSortType; useMeterZone: boolean; } -export interface ReadingsCSVUploadPreferencesForm extends CSVUploadPreferencesForm { - createMeter: BooleanTypes; - cumulative: BooleanMeterTypes; - cumulativeReset: BooleanMeterTypes; - cumulativeResetStart: string; - cumulativeResetEnd: string; - duplications: string; // Not sure how to type this an integer string; - meterName: string; - lengthGap: string; - lengthVariation: string; - endOnly: BooleanMeterTypes; - refreshHourlyReadings: BooleanTypes; - refreshReadings: BooleanTypes; - timeSort: TimeSortTypes; - honorDst: BooleanTypes; - relaxedParsing: BooleanTypes; - useMeterZone: BooleanTypes; -} - -export interface ReadingsCSVUploadProps extends ReadingsCSVUploadPreferencesItem, CSVUploadProps{ - // Note: each of these will have to change in consideration of redux; - selectTimeSort: (value: TimeSortTypes) => void; - selectDuplications: (value: string) => void; - selectCumulative: (value: BooleanMeterTypes) => void; - selectCumulativeReset: (value: BooleanMeterTypes) => void; - setCumulativeResetStart: (value: string) => void; - setCumulativeResetEnd: (value: string) => void; - setLengthGap: (value: string) => void; - setLengthVariation: (value: string) => void; - selectEndOnly: (value: string) => void; - toggleCreateMeter: () => void; - toggleRefreshHourlyReadings: () => void; - toggleRefreshReadings: () => void; - toggleHonorDst: () => void; - toggleRelaxedParsing: () => void; - toggleUseMeterZone: () => void; -} - // MetersCSVUpload, MetersCSVUploadPreferencesItem, MetersCSVUploadProps should be interfaces. However, at the moment does not add anything new. // Hence, we define a new type rather than a new interface that extends CSVUploadPreferences and CSVUploadProps to pass our linter. -export type MetersCSVUpload = CSVUploadPreferences; - -export type MetersCSVUploadPreferencesItem = MetersCSVUpload; - -export type MetersCSVUploadProps = CSVUploadProps; +export type MetersCSVUploadPreferences = CSVUploadPreferences; \ No newline at end of file diff --git a/src/client/app/types/redux/graph.ts b/src/client/app/types/redux/graph.ts index e649dd143..601dd51b9 100644 --- a/src/client/app/types/redux/graph.ts +++ b/src/client/app/types/redux/graph.ts @@ -62,8 +62,7 @@ export interface GraphState { selectedUnit: number; selectedAreaUnit: AreaUnitType; rangeSliderInterval: TimeInterval; - barDuration: moment.Duration; - mapsBarDuration: moment.Duration; + duration: moment.Duration; comparePeriod: ComparePeriod; compareTimeInterval: TimeInterval; compareSortingOrder: SortingOrder; diff --git a/src/client/app/types/redux/meters.ts b/src/client/app/types/redux/meters.ts index 686fc31d3..d5490568e 100644 --- a/src/client/app/types/redux/meters.ts +++ b/src/client/app/types/redux/meters.ts @@ -15,7 +15,7 @@ export enum MeterType { OTHER = 'other' } -// This relates to TimeSortTypes in src/client/app/types/csvUploadForm.ts but does not have 'meter value or default'. +// This relates to TimeSortTypes in src/client/app/types/csvUploadForm.ts // They should be kept in sync. export enum MeterTimeSortType { increasing = 'increasing', diff --git a/src/client/app/utils/api/UploadCSVApi.ts b/src/client/app/utils/api/UploadCSVApi.ts index 0845ae672..a810d0bec 100644 --- a/src/client/app/utils/api/UploadCSVApi.ts +++ b/src/client/app/utils/api/UploadCSVApi.ts @@ -4,53 +4,73 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import ApiBackend from './ApiBackend'; +import { Dispatch } from '@reduxjs/toolkit'; +import { baseApi } from '../../redux/api/baseApi'; import { - ReadingsCSVUploadPreferencesItem, MetersCSVUploadPreferencesItem, BooleanTypes, CSVUploadPreferencesForm, ReadingsCSVUploadPreferencesForm + CSVUploadPreferences, + MetersCSVUploadPreferences, + ReadingsCSVUploadPreferences } from '../../types/csvUploadForm'; +import ApiBackend from './ApiBackend'; + +interface ApiResponse { + success: boolean, + message: string +} -export default class UploadCSVApi { - private readonly backend: ApiBackend; +export const submitReadings = async (uploadPreferences: ReadingsCSVUploadPreferences, readingsFile: File, + dispatch: Dispatch): Promise => { + const backend = new ApiBackend(); + const formData = new FormData(); + // The Boolean values in state must be converted to the submitted values of yes and no. + const uploadPreferencesForm: ReadingsCSVUploadPreferences = { + ...uploadPreferences, + gzip: uploadPreferences.gzip, + headerRow: uploadPreferences.headerRow, + update: uploadPreferences.update, + refreshReadings: uploadPreferences.refreshReadings, + honorDst: uploadPreferences.honorDst, + relaxedParsing: uploadPreferences.relaxedParsing, + useMeterZone: uploadPreferences.useMeterZone + }; + for (const [preference, value] of Object.entries(uploadPreferencesForm)) { + formData.append(preference, value.toString()); + } + formData.append('csvfile', readingsFile); // It is important for the server that the file is attached last. - constructor(backend: ApiBackend) { - this.backend = backend; + let message = ''; + try { + message = await backend.doPostRequest('/api/csv/readings', formData); + dispatch(baseApi.util.invalidateTags(['Readings'])); + return { success: true, message: message }; + } catch (error) { + return { success: false, message: error.response.data }; } +}; - public async submitReadings(uploadPreferences: ReadingsCSVUploadPreferencesItem, readingsFile: File): Promise { - const formData = new FormData(); - // The Boolean values in state must be converted to the submitted values of yes and no. - const uploadPreferencesForm: ReadingsCSVUploadPreferencesForm = { - ...uploadPreferences, - gzip: uploadPreferences.gzip ? BooleanTypes.true : BooleanTypes.false, - headerRow: uploadPreferences.headerRow ? BooleanTypes.true : BooleanTypes.false, - update: uploadPreferences.update ? BooleanTypes.true : BooleanTypes.false, - createMeter: uploadPreferences.createMeter ? BooleanTypes.true : BooleanTypes.false, - refreshHourlyReadings: uploadPreferences.refreshHourlyReadings ? BooleanTypes.true : BooleanTypes.false, - refreshReadings: uploadPreferences.refreshReadings ? BooleanTypes.true : BooleanTypes.false, - honorDst: uploadPreferences.honorDst ? BooleanTypes.true : BooleanTypes.false, - relaxedParsing: uploadPreferences.relaxedParsing ? BooleanTypes.true : BooleanTypes.false, - useMeterZone: uploadPreferences.useMeterZone ? BooleanTypes.true : BooleanTypes.false - }; - for (const [preference, value] of Object.entries(uploadPreferencesForm)) { - formData.append(preference, value.toString()); - } - formData.append('csvfile', readingsFile); // It is important for the server that the file is attached last. - return await this.backend.doPostRequest('/api/csv/readings', formData); +export const submitMeters = async (uploadPreferences: MetersCSVUploadPreferences, metersFile: File, + dispatch: Dispatch): Promise => { + const backend = new ApiBackend(); + const formData = new FormData(); + // The Boolean values in state must be converted to the submitted values of yes and no. + const uploadPreferencesForm: CSVUploadPreferences = { + ...uploadPreferences, + gzip: uploadPreferences.gzip, + headerRow: uploadPreferences.headerRow, + update: uploadPreferences.update + }; + for (const [preference, value] of Object.entries(uploadPreferencesForm)) { + formData.append(preference, value.toString()); } + formData.append('csvfile', metersFile); // It is important for the server that the file is attached last. - public async submitMeters(uploadPreferences: MetersCSVUploadPreferencesItem, metersFile: File): Promise { - const formData = new FormData(); - // The Boolean values in state must be converted to the submitted values of yes and no. - const uploadPreferencesForm: CSVUploadPreferencesForm = { - ...uploadPreferences, - gzip: uploadPreferences.gzip ? BooleanTypes.true : BooleanTypes.false, - headerRow: uploadPreferences.headerRow ? BooleanTypes.true : BooleanTypes.false, - update: uploadPreferences.update ? BooleanTypes.true : BooleanTypes.false - }; - for (const [preference, value] of Object.entries(uploadPreferencesForm)) { - formData.append(preference, value.toString()); - } - formData.append('csvfile', metersFile); // It is important for the server that the file is attached last. - await this.backend.doPostRequest('/api/csv/meters', formData); + try { + const response = await backend.doPostRequest('/api/csv/meters', formData); + // Meter Data was sent to the DB, invalidate meters for now + dispatch(baseApi.util.invalidateTags(['MeterData'])); + // meters were invalidated so all meter changes will now reflect in Redux state, now return + return { success: true, message: response }; + } catch (error) { + return { success: false, message: error.response.data }; } -} +}; diff --git a/src/client/app/utils/api/index.ts b/src/client/app/utils/api/index.ts index 436a1e970..4330a2919 100644 --- a/src/client/app/utils/api/index.ts +++ b/src/client/app/utils/api/index.ts @@ -5,20 +5,17 @@ */ import ApiBackend from './ApiBackend'; -import UploadCSVApi from './UploadCSVApi'; import MapsApi from './MapsApi'; import LogsApi from './LogsApi'; const apiBackend = new ApiBackend(); // All specific backends share the same ApiBackend -const uploadCSVApi = new UploadCSVApi(apiBackend); const mapsApi = new MapsApi(apiBackend); const logsApi = new LogsApi(apiBackend); export { mapsApi, - logsApi, - uploadCSVApi + logsApi }; diff --git a/src/client/app/utils/calculateCompare.ts b/src/client/app/utils/calculateCompare.ts index 499c5bac1..07409cb20 100644 --- a/src/client/app/utils/calculateCompare.ts +++ b/src/client/app/utils/calculateCompare.ts @@ -4,15 +4,15 @@ import { TimeInterval } from '../../../common/TimeInterval'; import * as moment from 'moment'; -import translate from '../utils/translate'; +import translate from './translate'; /** * 'Day', 'Week' or 'FourWeeks' */ export enum ComparePeriod { - Day = 'Day', - Week = 'Week', - FourWeeks = 'FourWeeks' + Day = '1', + Week = '7', + FourWeeks = '28' } /** @@ -30,11 +30,11 @@ export enum SortingOrder { */ export function validateComparePeriod(comparePeriod: string): ComparePeriod { switch (comparePeriod) { - case 'Day': + case '1': return ComparePeriod.Day; - case 'Week': + case '7': return ComparePeriod.Week; - case 'FourWeeks': + case '28': return ComparePeriod.FourWeeks; default: throw new Error(`Unknown period value: ${comparePeriod}`); diff --git a/src/client/app/utils/csvUploadDefaults.ts b/src/client/app/utils/csvUploadDefaults.ts index 8053bd03e..49a73ff57 100644 --- a/src/client/app/utils/csvUploadDefaults.ts +++ b/src/client/app/utils/csvUploadDefaults.ts @@ -2,35 +2,34 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { ReadingsCSVUploadPreferencesItem, MetersCSVUploadPreferencesItem, TimeSortTypes, BooleanMeterTypes } from '../types/csvUploadForm'; +import { ReadingsCSVUploadPreferences, MetersCSVUploadPreferences } from '../types/csvUploadForm'; +import { MeterTimeSortType } from '../types/redux/meters'; // This file contains the default parameters for uploading readings and meters CSV files. These defaults should be consistent with the defaults // specified in /src/server/services/csvPipeline/validateCsvUploadParams and with /src/server/sql/create_meters_table.sql. -export const ReadingsCSVUploadDefaults: ReadingsCSVUploadPreferencesItem = { - meterName: '', - timeSort: TimeSortTypes.meter, - duplications: '', - cumulative: BooleanMeterTypes.meter, - cumulativeReset: BooleanMeterTypes.meter, +export const ReadingsCSVUploadDefaults: ReadingsCSVUploadPreferences = { + cumulative: false, + cumulativeReset: false, cumulativeResetStart: '', cumulativeResetEnd: '', - lengthGap: '', - lengthVariation: '', - endOnly: BooleanMeterTypes.meter, - createMeter: false, + duplications: 1, + endOnly: false, gzip: false, headerRow: false, - refreshHourlyReadings: false, - refreshReadings: false, - update: false, honorDst: false, + lengthGap: 0, + lengthVariation: 0, + meterIdentifier: '', + refreshReadings: false, relaxedParsing: false, + timeSort: MeterTimeSortType.increasing, + update: false, useMeterZone: false }; -export const MetersCSVUploadDefaults: MetersCSVUploadPreferencesItem = { - meterName: '', +export const MetersCSVUploadDefaults: MetersCSVUploadPreferences = { gzip: false, headerRow: false, + meterIdentifier: '', update: false -}; +}; \ No newline at end of file diff --git a/src/client/app/utils/determineCompatibleUnits.ts b/src/client/app/utils/determineCompatibleUnits.ts index 214f90831..11389c00d 100644 --- a/src/client/app/utils/determineCompatibleUnits.ts +++ b/src/client/app/utils/determineCompatibleUnits.ts @@ -3,8 +3,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ // TODO it is a bad practice to import store anywhere other than index.tsx These utils need to be converted into selectors. - -import { get, sortBy } from 'lodash'; +import { get } from 'lodash'; import React from 'react'; import { selectCik } from '../redux/api/conversionsApi'; import { selectAllGroups, selectGroupDataById } from '../redux/api/groupsApi'; @@ -13,6 +12,7 @@ import { store } from '../store'; import { DataType } from '../types/Datasources'; import { SelectOption } from '../types/items'; import { GroupData } from '../types/redux/groups'; +import { LanguageTypes } from 'types/redux/i18n'; /** * The intersect operation of two sets. @@ -139,9 +139,10 @@ export function metersInChangedGroup(changedGroupState: GroupData): number[] { * Get options for the meter menu on the group page. * @param defaultGraphicUnit The groups current default graphic unit which may have been updated from what is in Redux state. * @param deepMeters The groups current deep meters (all recursively) which may have been updated from what is in Redux state. + * @param locale Current language from Redux state. * @returns The current meter options for this group. */ -export function getMeterMenuOptionsForGroup(defaultGraphicUnit: number, deepMeters: number[] = []): SelectOption[] { +export function getMeterMenuOptionsForGroup(defaultGraphicUnit: number, deepMeters: number[] = [], locale: LanguageTypes): SelectOption[] { // deepMeters has a default value since it is optional for the type of state but it should always be set in the code. const state = store.getState(); // Get the currentGroup's compatible units. We need to use the current deep meters to get it right. @@ -176,7 +177,8 @@ export function getMeterMenuOptionsForGroup(defaultGraphicUnit: number, deepMete // We want the options sorted by meter identifier. // Had to make item.label? potentially undefined due to start up race conditions - return sortBy(options, item => item.label?.toLowerCase(), 'asc'); + return options.sort((itemA, itemB) => itemA.label.toLowerCase()?. + localeCompare(itemB.label.toLowerCase(), String(locale), { sensitivity: 'accent' })); } /** @@ -184,9 +186,11 @@ export function getMeterMenuOptionsForGroup(defaultGraphicUnit: number, deepMete * @param groupId The id of the group being worked on. * @param defaultGraphicUnit The group's current default graphic unit which may have been updated from what is in Redux state. * @param deepMeters The group's current deep meters (all recursively) which may have been updated from what is in Redux state. + * @param locale Current language from Redux state. * @returns The current group options for this group. */ -export function getGroupMenuOptionsForGroup(groupId: number, defaultGraphicUnit: number, deepMeters: number[] = []): SelectOption[] { +export function getGroupMenuOptionsForGroup(groupId: number, defaultGraphicUnit: number, deepMeters: number[] = [], locale: LanguageTypes): +SelectOption[] { // Get the currentGroup's compatible units. We need to use the current deep meters to get it right. // First must get a set from the array of meter numbers. const deepMetersSet = new Set(deepMeters); @@ -221,7 +225,9 @@ export function getGroupMenuOptionsForGroup(groupId: number, defaultGraphicUnit: // We want the options sorted by group name. // Had to make item.label? potentially undefined due to start up race conditions - return sortBy(options, item => item.label?.toLowerCase(), 'asc'); + return options.sort((itemA, itemB) => itemA.label.toLowerCase()?. + localeCompare(itemB.label.toLowerCase(), String(locale), { sensitivity: 'accent' })); + } /** diff --git a/src/client/app/utils/graphics.ts b/src/client/app/utils/graphics.ts index cb2e64d34..2a2d45e14 100644 --- a/src/client/app/utils/graphics.ts +++ b/src/client/app/utils/graphics.ts @@ -4,7 +4,7 @@ import { LineGraphRate } from 'types/redux/graph'; import { UnitData, UnitRepresentType } from '../types/redux/units'; -import translate from '../utils/translate'; +import translate from './translate'; import { AreaUnitType } from './getAreaUnitConversion'; // Has functions for use with graphics @@ -19,7 +19,6 @@ import { AreaUnitType } from './getAreaUnitConversion'; */ export function lineUnitLabel(selectUnitState: UnitData, currentSelectedRate: LineGraphRate, areaNormalization: boolean, selectedAreaUnit: AreaUnitType): { unitLabel: string, needsRateScaling: boolean } { - let unitLabel: string = ''; let needsRateScaling = false; // Quantity and flow units have different unit labels. diff --git a/src/client/app/utils/input.ts b/src/client/app/utils/input.ts index d6c67fae4..cbdeb7074 100644 --- a/src/client/app/utils/input.ts +++ b/src/client/app/utils/input.ts @@ -5,8 +5,7 @@ import { GPSPoint } from './calibration'; import { UnitData, DisplayableType, UnitRepresentType, UnitType, UnitDataById } from '../types/redux/units'; import translate from './translate'; -import { sortBy } from 'lodash'; - +import { LanguageTypes } from 'types/redux/i18n'; /** * get string value from GPSPoint or null. * @param gps GPS point to get value from and can be null @@ -48,9 +47,10 @@ export function nullToEmptyString(item: any) { * Calculates the set of all possible graphic units for a meter/group. * This is any unit that is of type unit or suffix. * @param units candidate graphic units + * @param locale Current language selected from Redux state * @returns The set of all possible graphic units for a meter/group */ -export function potentialGraphicUnits(units: UnitDataById) { +export function potentialGraphicUnits(units: UnitDataById, locale: LanguageTypes) { // Set of possible graphic units let possibleGraphicUnits = new Set(); @@ -61,7 +61,10 @@ export function potentialGraphicUnits(units: UnitDataById) { } }); // Put in alphabetical order. - possibleGraphicUnits = new Set(sortBy(Array.from(possibleGraphicUnits), unit => unit.identifier.toLowerCase(), 'asc')); + // TODO: change second argument of locale from undefined to current language if needed, but alphabetical ordering works + // with accents using undefined locale arg + possibleGraphicUnits = new Set(Array.from(possibleGraphicUnits).sort((unitA, unitB) => unitA.identifier.toLowerCase(). + localeCompare(unitB.identifier.toLowerCase(), String(locale), { sensitivity: 'accent' }))); // The default graphic unit can also be no unit/-99 but that is not desired so put last in list. possibleGraphicUnits.add(noUnitTranslated()); return possibleGraphicUnits; diff --git a/src/server/app.js b/src/server/app.js index 58774f9cc..ad297a27d 100644 --- a/src/server/app.js +++ b/src/server/app.js @@ -41,7 +41,40 @@ const generalLimiter = rateLimit({ windowMs: 5 * 1000, // 5 seconds limit: 200, // 200 requests standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers - legacyHeaders: false // Disable the `X-RateLimit-*` headers + legacyHeaders: false, // Disable the `X-RateLimit-*` headers + // If rate limit is 10, OED won't load and bad things will happen + message: async (req, res) => { + const string = ` +

+ You have been rate limited by your OED site. +

+

+ We suggest you try these in this order: +

+

+

+
+
    +
  1. + Click the 'Refresh this page' button below to try again. +
  2. +
  3. + If you keep returning to this page wait longer and click 'Refresh this page' button. +
  4. +
  5. + Contact your site to find why the rate limit is denying access to the OED site. +
  6. +
+
+

+ +

+ ` + return string + } }); // Apply the limit to overall requests const app = express().use(generalLimiter); diff --git a/src/server/migrations/0.7.0-0.8.0/sql/readings/replace_compressed_readings_2.sql b/src/server/migrations/0.7.0-0.8.0/sql/readings/replace_compressed_readings_2.sql index 028fe9623..227bb4217 100644 --- a/src/server/migrations/0.7.0-0.8.0/sql/readings/replace_compressed_readings_2.sql +++ b/src/server/migrations/0.7.0-0.8.0/sql/readings/replace_compressed_readings_2.sql @@ -45,13 +45,9 @@ DECLARE +------+------------------- - | TODO | + | Note | +------+ - Note that the above documentation does not account for the increase in data when packaged in JSON format. - - This function is complicated by the fact that groups can contain meters with different levels of granularity. - We address this now by having sites pick a site-level reading frequency. (Not done) - - When resource generalization is implemented in OED, this function will need to be done in a - meter-by-meter basis, since different resource meters will have different levels of granularity. */ BEGIN diff --git a/src/server/migrations/1.0.0-1.1.0/index.js b/src/server/migrations/1.0.0-2.0.0/index.js similarity index 50% rename from src/server/migrations/1.0.0-1.1.0/index.js rename to src/server/migrations/1.0.0-2.0.0/index.js index 6273473bb..8444613e3 100644 --- a/src/server/migrations/1.0.0-1.1.0/index.js +++ b/src/server/migrations/1.0.0-2.0.0/index.js @@ -7,17 +7,18 @@ const sqlFile = database.sqlFile; module.exports = { fromVersion: '1.0.0', - toVersion: '1.1.0', + toVersion: '2.0.0', up: async db => { - await db.none(sqlFile('../migrations/1.0.0-1.1.0/sql/readings/create_reading_views.sql')); - await db.none(sqlFile('../migrations/1.0.0-1.1.0/sql/meter/add_meter_pipeline_checks.sql')); - await db.none(sqlFile('../migrations/1.0.0-1.1.0/sql/preferences/add_preferences_pipeline_checks.sql')); - await db.none(sqlFile('../migrations/1.0.0-1.1.0/sql/preferences/add_graph_type.sql')); - await db.none(sqlFile('../migrations/1.0.0-1.1.0/sql/preferences/add_preferences_help_url.sql')); - await db.none(sqlFile('../migrations/1.0.0-1.1.0/sql/users/add_users_note.sql')); - await db.none(sqlFile('../migrations/1.0.0-1.1.0/sql/users/alter_users_table.sql')); + await db.none(sqlFile('../migrations/1.0.0-2.0.0/sql/readings/create_reading_views.sql')); + await db.none(sqlFile('../migrations/1.0.0-2.0.0/sql/readings/create_function_get_compare_readings.sql')); + await db.none(sqlFile('../migrations/1.0.0-2.0.0/sql/meter/add_meter_pipeline_checks.sql')); + await db.none(sqlFile('../migrations/1.0.0-2.0.0/sql/preferences/add_preferences_pipeline_checks.sql')); + await db.none(sqlFile('../migrations/1.0.0-2.0.0/sql/preferences/add_graph_type.sql')); + await db.none(sqlFile('../migrations/1.0.0-2.0.0/sql/preferences/add_preferences_help_url.sql')); + await db.none(sqlFile('../migrations/1.0.0-2.0.0/sql/users/add_users_note.sql')); + await db.none(sqlFile('../migrations/1.0.0-2.0.0/sql/users/alter_users_table.sql')); // It should not matter but first rename cik and then do units. - await db.none(sqlFile('../migrations/1.0.0-1.1.0/sql/cik/alter_cik_table.sql')); - await db.none(sqlFile('../migrations/1.0.0-1.1.0/sql/units/alter_units_table.sql')); + await db.none(sqlFile('../migrations/1.0.0-2.0.0/sql/cik/alter_cik_table.sql')); + await db.none(sqlFile('../migrations/1.0.0-2.0.0/sql/units/alter_units_table.sql')); } }; diff --git a/src/server/migrations/1.0.0-1.1.0/sql/cik/alter_cik_table.sql b/src/server/migrations/1.0.0-2.0.0/sql/cik/alter_cik_table.sql similarity index 93% rename from src/server/migrations/1.0.0-1.1.0/sql/cik/alter_cik_table.sql rename to src/server/migrations/1.0.0-2.0.0/sql/cik/alter_cik_table.sql index 29e7c58a8..354593fa8 100644 --- a/src/server/migrations/1.0.0-1.1.0/sql/cik/alter_cik_table.sql +++ b/src/server/migrations/1.0.0-2.0.0/sql/cik/alter_cik_table.sql @@ -14,4 +14,4 @@ CREATE TABLE IF NOT EXISTS cik ( ); -- TODO It is important that a redoCik is done after this to recreate cik. --- MUST ADD THIS TO THE 1.1 MIGRATION. +-- MUST ADD THIS TO THE 2.0 MIGRATION. diff --git a/src/server/migrations/1.0.0-1.1.0/sql/meter/add_meter_pipeline_checks.sql b/src/server/migrations/1.0.0-2.0.0/sql/meter/add_meter_pipeline_checks.sql similarity index 100% rename from src/server/migrations/1.0.0-1.1.0/sql/meter/add_meter_pipeline_checks.sql rename to src/server/migrations/1.0.0-2.0.0/sql/meter/add_meter_pipeline_checks.sql diff --git a/src/server/migrations/1.0.0-1.1.0/sql/preferences/add_graph_type.sql b/src/server/migrations/1.0.0-2.0.0/sql/preferences/add_graph_type.sql similarity index 100% rename from src/server/migrations/1.0.0-1.1.0/sql/preferences/add_graph_type.sql rename to src/server/migrations/1.0.0-2.0.0/sql/preferences/add_graph_type.sql diff --git a/src/server/migrations/1.0.0-1.1.0/sql/preferences/add_preferences_help_url.sql b/src/server/migrations/1.0.0-2.0.0/sql/preferences/add_preferences_help_url.sql similarity index 92% rename from src/server/migrations/1.0.0-1.1.0/sql/preferences/add_preferences_help_url.sql rename to src/server/migrations/1.0.0-2.0.0/sql/preferences/add_preferences_help_url.sql index e52a9aace..401d2bcdf 100644 --- a/src/server/migrations/1.0.0-1.1.0/sql/preferences/add_preferences_help_url.sql +++ b/src/server/migrations/1.0.0-2.0.0/sql/preferences/add_preferences_help_url.sql @@ -6,7 +6,7 @@ -- You need to set default so any existing rows get that value since NOT NULL. ALTER TABLE preferences - ADD COLUMN IF NOT EXISTS default_help_url TEXT NOT NULL DEFAULT 'https://openenergydashboard.github.io/' + ADD COLUMN IF NOT EXISTS default_help_url TEXT NOT NULL DEFAULT 'https://openenergydashboard.org/' ; -- Now remove default since not desired. ALTER TABLE preferences diff --git a/src/server/migrations/1.0.0-1.1.0/sql/preferences/add_preferences_pipeline_checks.sql b/src/server/migrations/1.0.0-2.0.0/sql/preferences/add_preferences_pipeline_checks.sql similarity index 100% rename from src/server/migrations/1.0.0-1.1.0/sql/preferences/add_preferences_pipeline_checks.sql rename to src/server/migrations/1.0.0-2.0.0/sql/preferences/add_preferences_pipeline_checks.sql diff --git a/src/server/migrations/1.0.0-2.0.0/sql/readings/create_function_get_compare_readings.sql b/src/server/migrations/1.0.0-2.0.0/sql/readings/create_function_get_compare_readings.sql new file mode 100644 index 000000000..72c0d7612 --- /dev/null +++ b/src/server/migrations/1.0.0-2.0.0/sql/readings/create_function_get_compare_readings.sql @@ -0,0 +1,152 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* +This isn't technically needed as no function signature changed and a startup of OED should +replace the functions but do just to show/be safe. + */ + +/* +This shouldn't ever be looking at more than a few weeks of data, so we don't need to deal with compression. + */ + +/* +TODO This function can probably be improved for two reasons: +1) While it was noted that you don't look at too many days, it does limit its usage to modest time lengths. +There has been thought to allowing comparisons, for example, or a whole year. It also assumes that the +frequency of readings is not too high so the number of readings looked at could be large even over +modest time frames. +2) Readings are only included if they are within the current time period. This means that if you have +a reading that crosses the timeframe they are excluded. This can be viewed as either a good thing or +a bad thing. However, if the frequency of readings is high, then large segments of time can be excluded. +For example, monthly readings would not be includes in either day, week or four week comparisons that +OED currently does. Also note that the daily and hourly views do include readings that span a time frame +so including them would be consistent. + +We need to think about how best to deal with this but one option is to use the daily and hourly tables +to get the reading values as in done with bar graphs. This needs to be a little different since partial +days can be involved. However, getting the full days from the daily table and then the hours from the +hourly table to be combined would solve this. (One could get the subhour from the raw readings but it is +unclear users would want that.) Another consider is that the current system, as is the case for bar graphs, +does not take into account missing times in readings which can lead to lower than expected values. The +daily and hourly readings would help fix this. +*/ + +/* +The following function returns data for plotting compare graphs. It works on meters. +It should not be used on raw readings. +This only works when the curr start/end times are on the hour as it uses the hourly reading view. +It will truncate to lose partial hours if they are given. The current client code will only +send full hours so this should be fine. +It is the new version of compare_readings that works with units. It takes these parameters: +meter_ids: A array of meter ids to query. +graphic_unit_id: The unit id of the unit to use for the graph. +curr_start: When the current/this time period begins for the compare. +curr_end: When the current/this time period ends for the compare. +shift: How far back in time to shift the curr_start and curr_end date/time to get the previous + times to compare. + */ +CREATE OR REPLACE FUNCTION meter_compare_readings_unit ( + meter_ids INTEGER[], + graphic_unit_id INTEGER, + curr_start TIMESTAMP, + curr_end TIMESTAMP, + shift INTERVAL +) + RETURNS TABLE(meter_id INTEGER, curr_use FLOAT, prev_use FLOAT) +AS $$ +DECLARE + curr_tsrange TSRANGE; + prev_tsrange TSRANGE; +BEGIN + curr_tsrange := tsrange(curr_start, curr_end); + prev_tsrange := tsrange(curr_start - shift, curr_end - shift); + + RETURN QUERY + WITH + curr_period AS ( + SELECT + meters.id AS meter_id, + -- Convert the reading based on the conversion found below. + -- It is okay to sum the flow units because hourly readings has a rate of per hour so + -- to convert to a quantity would multiply by 1 so it is the same formula. + SUM(hourly.reading_rate) * c.slope + c.intercept AS reading + FROM (((hourly_readings_unit hourly + INNER JOIN unnest(meter_ids) meters(id) ON hourly.meter_id = meters.id) + INNER JOIN meters m ON m.id = meters.id) + -- This is getting the conversion for the meter and unit to graph. + -- The slope and intercept are used above the transform the reading to the desired unit. + INNER JOIN cik c on c.source_id = m.unit_id AND c.destination_id = graphic_unit_id) + WHERE curr_tsrange @> hourly.time_interval + GROUP BY meters.id, c.slope, c.intercept + ), + prev_period AS ( + SELECT + meters.id AS meter_id, + -- Convert the reading based on the conversion found below. + -- It is okay to sum the flow units because hourly readings has a rate of per hour so + -- to convert to a quantity would multiply by 1 so it is the same formula. + SUM(hourly.reading_rate) * c.slope + c.intercept AS reading + FROM (((hourly_readings_unit hourly + INNER JOIN unnest(meter_ids) meters(id) ON hourly.meter_id = meters.id) + INNER JOIN meters m ON m.id = meters.id) + -- This is getting the conversion for the meter and unit to graph. + -- The slope and intercept are used above the transform the reading to the desired unit. + INNER JOIN cik c on c.source_id = m.unit_id AND c.destination_id = graphic_unit_id) + WHERE prev_tsrange @> hourly.time_interval + GROUP BY meters.id, c.slope, c.intercept + ) + SELECT + meters.id AS meter_id, + curr_period.reading::FLOAT AS curr_use, + prev_period.reading::FLOAT AS prev_use + FROM + unnest(meter_ids) meters(id) + -- Left joins here so we get nulls instead of missing rows if readings don't exist for some time intervals + LEFT JOIN prev_period ON meters.id = prev_period.meter_id + LEFT JOIN curr_period ON meters.id = curr_period.meter_id; +END; +$$ LANGUAGE 'plpgsql'; + + +/* +The following function returns data for plotting compare graphs. It works on groups. +It should not be used on raw readings. +See meter version for needing start/end on the hour. +It is the new version of group_compare_readings that works with units. It takes these parameters: +group_ids: A array of group ids to query. +graphic_unit_id: The unit id of the unit to use for the graph. +curr_start: When the current/this time period begins for the compare. +curr_end: When the current/this time period ends for the compare. +shift: How far back in time to shift the curr_start and curr_end date/time to get the previous + times to compare. + */ +CREATE OR REPLACE FUNCTION group_compare_readings_unit ( + group_ids INTEGER[], + graphic_unit_id INTEGER, + curr_start TIMESTAMP, + curr_end TIMESTAMP, + shift INTERVAL +) + RETURNS TABLE(group_id INTEGER, curr_use FLOAT, prev_use FLOAT) +AS $$ +DECLARE + meter_ids INTEGER[]; +BEGIN + SELECT array_agg(DISTINCT meter_id) INTO meter_ids + FROM unnest(group_ids) gids(id) + INNER JOIN groups_deep_meters gdm ON gdm.group_id = gids.id; + + RETURN QUERY + SELECT + gids.id AS group_id, + SUM(cr.curr_use) AS curr_use, + SUM(cr.prev_use) AS prev_use + FROM unnest(group_ids) gids(id) + INNER JOIN groups_deep_meters gdm ON gdm.group_id = gids.id + INNER JOIN meter_compare_readings_unit(meter_ids, graphic_unit_id, curr_start, curr_end, shift) cr + ON cr.meter_id = gdm.meter_id + GROUP by gids.id; +END; +$$ LANGUAGE 'plpgsql'; diff --git a/src/server/migrations/1.0.0-1.1.0/sql/readings/create_reading_views.sql b/src/server/migrations/1.0.0-2.0.0/sql/readings/create_reading_views.sql similarity index 100% rename from src/server/migrations/1.0.0-1.1.0/sql/readings/create_reading_views.sql rename to src/server/migrations/1.0.0-2.0.0/sql/readings/create_reading_views.sql diff --git a/src/server/migrations/1.0.0-1.1.0/sql/units/alter_units_table.sql b/src/server/migrations/1.0.0-2.0.0/sql/units/alter_units_table.sql similarity index 100% rename from src/server/migrations/1.0.0-1.1.0/sql/units/alter_units_table.sql rename to src/server/migrations/1.0.0-2.0.0/sql/units/alter_units_table.sql diff --git a/src/server/migrations/1.0.0-1.1.0/sql/users/add_users_note.sql b/src/server/migrations/1.0.0-2.0.0/sql/users/add_users_note.sql similarity index 100% rename from src/server/migrations/1.0.0-1.1.0/sql/users/add_users_note.sql rename to src/server/migrations/1.0.0-2.0.0/sql/users/add_users_note.sql diff --git a/src/server/migrations/1.0.0-1.1.0/sql/users/alter_users_table.sql b/src/server/migrations/1.0.0-2.0.0/sql/users/alter_users_table.sql similarity index 100% rename from src/server/migrations/1.0.0-1.1.0/sql/users/alter_users_table.sql rename to src/server/migrations/1.0.0-2.0.0/sql/users/alter_users_table.sql diff --git a/src/server/models/Meter.js b/src/server/models/Meter.js index 758210541..a8a7dcd36 100644 --- a/src/server/models/Meter.js +++ b/src/server/models/Meter.js @@ -166,6 +166,17 @@ class Meter { return Meter.mapRow(row); } + /** + * Returns a promise to retrieve the meter with the given identifier from the database. + * @param identifier the meter's identifier + * @param conn the connection to be used. + * @returns {Promise.} + */ + static async getByIdentifier(identifier, conn) { + const row = await conn.one(sqlFile('meter/get_meter_by_identifier.sql'), { identifier: identifier }); + return Meter.mapRow(row); + } + /** * Returns a promise to get all of the meters from the database * @param conn the connection to be used. diff --git a/src/server/routes/csv.js b/src/server/routes/csv.js index 9ccf0e631..df4701b15 100644 --- a/src/server/routes/csv.js +++ b/src/server/routes/csv.js @@ -21,13 +21,11 @@ const saveCsv = require('../services/csvPipeline/saveCsv'); const uploadMeters = require('../services/csvPipeline/uploadMeters'); const uploadReadings = require('../services/csvPipeline/uploadReadings'); const zlib = require('zlib'); -const { refreshReadingViews } = require('../services/refreshReadingViews'); -const { refreshHourlyReadingViews } = require('../services/refreshHourlyReadingViews'); +const { refreshAllReadingViews } = require('../services/refreshAllReadingViews'); const { success, failure } = require('../services/csvPipeline/success'); -const { BooleanTypesJS } = require('../services/csvPipeline/validateCsvUploadParams'); /** Middleware validation */ -const { validateMetersCsvUploadParams, validateReadingsCsvUploadParams } = require('../services/csvPipeline/validateCsvUploadParams'); +const { normalizeBoolean, validateMetersCsvUploadParams, validateReadingsCsvUploadParams } = require('../services/csvPipeline/validateCsvUploadParams'); const { CSVPipelineError } = require('../services/csvPipeline/CustomErrors'); const { isTokenAuthorized, isUserAuthorized } = require('../util/userRoles'); @@ -73,7 +71,7 @@ router.use(function (req, res, next) { // Allowing for backwards compatibility if any users are still using the 'email' parameter instead of // the 'username' parameter to login. Developers need to decide in the future if we should deprecate email // or continue to allow this backwards compatibility - const user = username || email; + const user = username || email; const verifiedUser = await verifyCredentials(user, password, true); if (verifiedUser) { isUserAuthorized(verifiedUser, csvRole) ? cb(null, true) : cb(new Error('Invalid credentials')); @@ -110,7 +108,7 @@ router.use(function (req, res, next) { }); router.post('/meters', validateMetersCsvUploadParams, async (req, res) => { - const isGzip = req.body.gzip === BooleanTypesJS.true; + const isGzip = normalizeBoolean(req.body.gzip); const uploadedFilepath = req.file.path; let csvFilepath; try { @@ -133,6 +131,7 @@ router.post('/meters', validateMetersCsvUploadParams, async (req, res) => { success(req, res, 'Successfully inserted the meters.'); } catch (error) { failure(req, res, error); + } finally { // Clean up files fs.unlink(uploadedFilepath) // Delete the uploaded file. @@ -154,9 +153,8 @@ router.post('/meters', validateMetersCsvUploadParams, async (req, res) => { }); router.post('/readings', validateReadingsCsvUploadParams, async (req, res) => { - const isGzip = req.body.gzip === BooleanTypesJS.true; - const isRefreshReadings = req.body.refreshReadings === BooleanTypesJS.true; - const isRefreshHourlyReadings = req.body.refreshHourlyReadings === BooleanTypesJS.true; + const isGzip = normalizeBoolean(req.body.gzip); + const isRefreshReadings = normalizeBoolean(req.body.refreshReadings); const uploadedFilepath = req.file.path; let csvFilepath; let isAllReadingsOk; @@ -179,11 +177,7 @@ router.post('/readings', validateReadingsCsvUploadParams, async (req, res) => { ({ isAllReadingsOk, msgTotal } = await uploadReadings(req, res, csvFilepath, conn)); if (isRefreshReadings) { // Refresh readings so show when daily data is used. - await refreshReadingViews(); - } - if (isRefreshHourlyReadings) { - // Refresh readings so show when hourly data is used. - await refreshHourlyReadingViews(); + await refreshAllReadingViews(); } } catch (error) { failure(req, res, error); diff --git a/src/server/routes/meters.js b/src/server/routes/meters.js index e7aaea313..5bd411ca5 100644 --- a/src/server/routes/meters.js +++ b/src/server/routes/meters.js @@ -25,10 +25,10 @@ router.use(optionalAuthenticator); /** * Defines the format in which we want to send meters and controls what information we send to the client, if logged in and an Admin or not. * @param meter - * @param loggedInAsAdmin + * @param hasFullAccess * @returns {{id, name}} */ -function formatMeterForResponse(meter, loggedInAsAdmin) { +function formatMeterForResponse(meter, hasFullAccess) { const formattedMeter = { id: meter.id, name: null, @@ -68,7 +68,7 @@ function formatMeterForResponse(meter, loggedInAsAdmin) { // Only logged in Admins can see url, types, timezones, and internal names // and lots of other items now. - if (loggedInAsAdmin) { + if (hasFullAccess) { formattedMeter.name = meter.name; formattedMeter.url = meter.url; formattedMeter.meterType = meter.type; @@ -108,13 +108,13 @@ router.get('/', async (req, res) => { const conn = getConnection(); let query; const token = req.headers.token || req.body.token || req.query.token; - const loggedInAsAdmin = req.hasValidAuthToken && (await isTokenAuthorized(token, User.role.ADMIN)); + const isAuthorizedCSV = req.hasValidAuthToken && (await isTokenAuthorized(token, User.role.CSV)); // Because groups can use hidden meters, everyone gets all meters but we filter the // information given about the meter after getting it. query = Meter.getAll; const rows = await query(conn); - res.json(rows.map(row => formatMeterForResponse(row, loggedInAsAdmin))); + res.json(rows.map(row => formatMeterForResponse(row, isAuthorizedCSV))); } catch (err) { log.error(`Error while performing GET all meters query: ${err}`, err); } diff --git a/src/server/services/csvPipeline/uploadMeters.js b/src/server/services/csvPipeline/uploadMeters.js index db80dd14f..78f8cb521 100644 --- a/src/server/services/csvPipeline/uploadMeters.js +++ b/src/server/services/csvPipeline/uploadMeters.js @@ -7,7 +7,7 @@ const { CSVPipelineError } = require('./CustomErrors'); const Meter = require('../../models/Meter'); const readCsv = require('../pipeline-in-progress/readCsv'); const Unit = require('../../models/Unit'); -const { BooleanTypesJS } = require('./validateCsvUploadParams'); +const { normalizeBoolean } = require('./validateCsvUploadParams'); /** * Middleware that uploads meters via the pipeline. This should be the final stage of the CSV Pipeline. @@ -28,7 +28,7 @@ async function uploadMeters(req, res, filepath, conn) { }); // If there is a header row, we remove and ignore it for now. - const meters = (req.body.headerRow === BooleanTypesJS.true) ? temp.slice(1) : temp; + const meters = normalizeBoolean(req.body.headerRow) ? temp.slice(1) : temp; // The original code used a Promise.all to run through the meters. The issue is that the promises are run in parallel. // If the meters are independent as expected then this works fine. However, in the error case where one CSV file has // the same meter name listed twice, the order of the attempts to add to the database was arbitrary. This meant one of them @@ -37,6 +37,7 @@ async function uploadMeters(req, res, filepath, conn) { // as this makes the most logical sense (no update here) and it is consistent. To make this happen a for loop is used as it // is sequential. A small negative is the database requests do not run in parallel in the usual case without an error. // However, uploading meters is not common so slowing it down slightly seems a reasonable price to get this behavior. + try { for (let i = 0; i < meters.length; i++) { let meter = meters[i]; @@ -75,34 +76,40 @@ async function uploadMeters(req, res, filepath, conn) { // Replace the default graphic unit's name by its id. meter[24] = defaultGraphicUnitId; - if (req.body.update === BooleanTypesJS.true) { + if (normalizeBoolean(req.body.update)) { // Updating the new meters. // First get its id. - let nameOfMeter = req.body.meterName; - if (!nameOfMeter) { - // Seems no name provided so use one in CSV file. - nameOfMeter = meter[0]; + let identifierOfMeter = req.body.meterIdentifier; + if (!identifierOfMeter) { + // Seems no identifier provided so use one in CSV file. + if (!meter[7]) { + // There is no identifier given for meter in CSV so use name as identifier since would be automatically set. + identifierOfMeter = meter[0]; + } else { + identifierOfMeter = meter[7]; + } } else if (meters.length !== 1) { // This error could be thrown a number of times, one per meter in CSV, but should only see one of them. - throw new CSVPipelineError(`Meter name provided (\"${nameOfMeter}\") in request with update for meters but more than one meter in CSV so not processing`, undefined, 500); + throw new CSVPipelineError(`Meter identifier provided (\"${identifierOfMeter}\") in request with update for meters but more than one meter in CSV so not processing`, undefined, 500); } let currentMeter; - currentMeter = await Meter.getByName(nameOfMeter, conn) + currentMeter = await Meter.getByIdentifier(identifierOfMeter, conn) .catch(error => { // Did not find the meter. - let msg = `Meter name of \"${nameOfMeter}\" does not seem to exist with update for meters and got DB error of: ${error.message}`; + let msg = `Meter identifier of \"${identifierOfMeter}\" does not seem to exist with update for meters and got DB error of: ${error.message}`; throw new CSVPipelineError(msg, undefined, 500); }); currentMeter.merge(...meter); await currentMeter.update(conn); } else { - // Inserting the new meters. + // Inserting the new meter await new Meter(undefined, ...meter).insert(conn) .catch(error => { // Probably duplicate meter. throw new CSVPipelineError( `Meter name of \"${meter[0]}\" got database error of: ${error.message}`, undefined, 500); - }); + } + ); } } } catch (error) { diff --git a/src/server/services/csvPipeline/uploadReadings.js b/src/server/services/csvPipeline/uploadReadings.js index f27b35fc2..1bdcfd947 100644 --- a/src/server/services/csvPipeline/uploadReadings.js +++ b/src/server/services/csvPipeline/uploadReadings.js @@ -5,11 +5,8 @@ const express = require('express'); const { CSVPipelineError } = require('./CustomErrors'); const { loadCsvInput } = require('../pipeline-in-progress/loadCsvInput'); -const { TimeSortTypesJS, BooleanMeterTypesJS, BooleanTypesJS } = require('./validateCsvUploadParams'); +const { normalizeBoolean, MeterTimeSortTypesJS } = require('./validateCsvUploadParams'); const Meter = require('../../models/Meter'); -const { log } = require('../../log'); -const moment = require('moment'); -const Preferences = require('../../models/Preferences'); /** * Middleware that uploads readings via the pipeline. This should be the final stage of the CSV Pipeline. @@ -20,73 +17,35 @@ const Preferences = require('../../models/Preferences'); * @returns */ async function uploadReadings(req, res, filepath, conn) { - const { meterName, createMeter, headerRow, update, honorDst, relaxedParsing, useMeterZone } = req.body; // extract query parameters + const { meterIdentifier, meterName, headerRow, update, honorDst, relaxedParsing, useMeterZone } = req.body; // extract query parameters // The next few have no value in the DB for a meter so always use the value passed. - const hasHeaderRow = (headerRow === BooleanTypesJS.true); - const shouldUpdate = (update === BooleanTypesJS.true); - let shouldHonorDst = (honorDst === BooleanTypesJS.true); - let shouldRelaxedParsing = (relaxedParsing === BooleanTypesJS.true); - let shouldUseMeterZone = (useMeterZone === BooleanTypesJS.true); - let meterCreated = false; - let meter = await Meter.getByName(meterName, conn) - .catch(async err => { - // Meter#getByNames throws an error when no meter is found. We need the catch clause to account for this error. - if (createMeter !== BooleanTypesJS.true) { - // If createMeter is not set to true, we do not know what to do with the readings so we error out. - throw new CSVPipelineError( - `User Error: Meter with name '${meterName}' not found. createMeter needs to be set true in order to automatically create meter.`, - err.message - ); - } else { - const preferences = await Preferences.get(conn); - // If createMeter is true, we will create the meter for the user. - // The meter type is unknown so set to other. Most parameters take on default values. - const tempMeter = new Meter( - undefined, // id - meterName, // name - undefined, // URL - false, // enabled - false, // displayable - Meter.type.OTHER, // type - undefined, // timezone - undefined, // gps - meterName, // identifier - 'created via reading upload on ' + moment().format(), // note - undefined, //area - undefined, // cumulative - undefined, // cumulativeReset - undefined, // cumulativeResetStart - undefined, // cumulativeResetEnd - preferences.defaultMeterReadingGap, // readingGap - undefined, // readingVariation - undefined, // readingDuplication - undefined, // timeSort - undefined, // endOnlyTime - undefined, // reading - undefined, // startTimestamp - undefined, // endTimestamp - undefined, // previousEnd - undefined, // unit - undefined, // default graphic unit - undefined, // area unit - preferences.defaultMeterReadingFrequency, // reading frequency - preferences.defaultMeterMinimumValue, // minVal - preferences.defaultMeterMaximumValue, // maxVal - preferences.defaultMeterMinimumDate, // minDate - preferences.defaultMeterMaximumDate, // maxDate - preferences.defaultMeterMaximumErrors, // maxError - preferences.defaultMeterDisableChecks // disableChecks - ) - await tempMeter.insert(conn); - meterCreated = true; - log.info('Creating meter ' + tempMeter.name); - return await Meter.getByName(tempMeter.name, conn); // Get meter from DB after insert because some defaults are set within the DB. - } - }); - if (!meterCreated && createMeter === BooleanTypesJS.true) { - log.warn('The create meter was set but the meter already existed for meter ' + meter.name); - } + const hasHeaderRow = normalizeBoolean(headerRow); + const shouldUpdate = normalizeBoolean(update); + let shouldHonorDst = normalizeBoolean(honorDst); + let shouldRelaxedParsing = normalizeBoolean(relaxedParsing); + let shouldUseMeterZone = normalizeBoolean(useMeterZone); + // TODO: + // Allowing for backwards compatibility if any users are still using the 'meterName' parameter instead of + // the 'meterIdentifier' parameter to login. Developers need to decide in the future if we should deprecate + // using 'meterName' or continue to allow this backwards compatibility + let meter; + try { + if (meterIdentifier) { + meter = await Meter.getByIdentifier(meterIdentifier, conn); + } else { + meter = await Meter.getByName(meterName, conn); + } + } catch (error) { + // If Meter does not exist, we do not know what to do with the readings so we error out. + let errorMessage = meterIdentifier + ? `User Error: Meter with identifier '${meterIdentifier}' not found.` + : `User Error: Meter with name '${meterName}' not found.`; + throw new CSVPipelineError( + errorMessage, + error.message + ); + } // Handle other parameter defaults let { timeSort, duplications, cumulative, cumulativeReset, cumulativeResetStart, cumulativeResetEnd, lengthGap, lengthVariation, endOnly } = req.body; @@ -99,7 +58,7 @@ async function uploadReadings(req, res, filepath, conn) { let readingGap; let readingLengthVariation; let readingEndOnly; - // For the parameters, they either have one of the desired values or a "empty" value. + // For the parameters, they either have one of the desired values or an "empty" value. // An empty value means use the value from the DB meter or, in the unlikely event it is not set, // then use the default value. For values coming from the web page: // In the case of timeSort, cumulative, cumulativeReset & endOnly empty is the enum 'meter' value. @@ -126,43 +85,43 @@ async function uploadReadings(req, res, filepath, conn) { readingRepetition = parseInt(duplications, 10); } - if (timeSort === undefined || timeSort === TimeSortTypesJS.meter) { + if (timeSort === undefined) { if (meter.timeSort === null) { // This probably should not happen with a new DB but keep just in case. // No variation allowed. - readingTimeSort = TimeSortTypesJS.increasing; + readingTimeSort = MeterTimeSortTypesJS.increasing; } else { - readingTimeSort = TimeSortTypesJS[meter.timeSort]; + readingTimeSort = MeterTimeSortTypesJS[meter.timeSort]; } } else { readingTimeSort = timeSort; } - if (cumulative === undefined || cumulative === BooleanMeterTypesJS.meter) { + if (cumulative === undefined) { if (meter.cumulative === null) { // This probably should not happen with a new DB but keep just in case. // No variation allowed. - readingsCumulative = BooleanMeterTypesJS.false; + readingsCumulative = false; } else { - readingsCumulative = BooleanMeterTypesJS[meter.cumulative]; + readingsCumulative = meter.cumulative; } } else { - readingsCumulative = cumulative; + readingsCumulative = normalizeBoolean(cumulative); } - const areReadingsCumulative = (readingsCumulative === BooleanMeterTypesJS.true); + const areReadingsCumulative = readingsCumulative; - if (cumulativeReset === undefined || cumulativeReset === BooleanMeterTypesJS.meter) { + if (cumulativeReset === undefined) { if (meter.cumulativeReset === null) { // This probably should not happen with a new DB but keep just in case. // No variation allowed. - readingsReset = BooleanMeterTypesJS.false; + readingsReset = false; } else { - readingsReset = BooleanMeterTypesJS[meter.cumulativeReset]; + readingsReset = meter.cumulativeReset; } } else { - readingsReset = cumulativeReset; + readingsReset = normalizeBoolean(cumulativeReset); } - const doReadingsReset = (readingsReset === BooleanMeterTypesJS.true); + const doReadingsReset = readingsReset; if (cumulativeResetStart === undefined || cumulativeResetStart === '') { if (meter.cumulativeResetStart === null) { @@ -214,18 +173,18 @@ async function uploadReadings(req, res, filepath, conn) { readingLengthVariation = parseFloat(lengthVariation); } - if (endOnly === undefined || endOnly === BooleanMeterTypesJS.meter) { + if (endOnly === undefined) { if (meter.endOnlyTime === null) { // This probably should not happen with a new DB but keep just in case. // No variation allowed. - readingEndOnly = BooleanMeterTypesJS.false; + readingEndOnly = false; } else { - readingEndOnly = BooleanMeterTypesJS[meter.endOnlyTime]; + readingEndOnly = meter.endOnlyTime; } } else { - readingEndOnly = endOnly; + readingEndOnly = normalizeBoolean(endOnly); } - const areReadingsEndOnly = (readingEndOnly === BooleanMeterTypesJS.true); + const areReadingsEndOnly = readingEndOnly; const mapRowToModel = row => { return row; }; // STUB function to satisfy the parameter of loadCsvInput. @@ -238,7 +197,7 @@ async function uploadReadings(req, res, filepath, conn) { maxError: meter.maxError, disableChecks: meter.disableChecks } - + return await loadCsvInput( filepath, meter.id, diff --git a/src/server/services/csvPipeline/validateCsvUploadParams.js b/src/server/services/csvPipeline/validateCsvUploadParams.js index 159c2df11..d2bca74ad 100644 --- a/src/server/services/csvPipeline/validateCsvUploadParams.js +++ b/src/server/services/csvPipeline/validateCsvUploadParams.js @@ -8,18 +8,6 @@ const { Param, EnumParam, BooleanParam, StringParam } = require('./ValidationSch const failure = require('./failure'); const validate = require('jsonschema').validate; -/** - * Enum of CSV input type sorting. - * This enum needs to be kept in sync with the enum in src/client/app/types/csvUploadForm.ts - * @enum {string} - */ -TimeSortTypesJS = Object.freeze({ - increasing: 'increasing', - decreasing: 'decreasing', - // meter means to use value stored on meter or the default if not. - meter: 'meter value or default' -}); - // This is only used for meter page inputs but put here so next one above that related to. /** * Enum of CSV input type sorting. @@ -31,27 +19,30 @@ MeterTimeSortTypesJS = Object.freeze({ decreasing: 'decreasing', }); -/** - * Enum of Boolean types. - * This enum needs to be kept in sync with the enum in src/client/app/types/csvUploadForm.ts - * @enum {string} - */ -BooleanTypesJS = Object.freeze({ - true: 'yes', - false: 'no', -}); +// This function allows for curl users to continue to use 'yes' or 'no' and also allows string +// values of true or false if the change is made. +const normalizeBoolean = (input) => { + if (typeof input === 'string') { + input = input.toLowerCase(); + } + + switch (input) { + case 'yes': + case 'true': + case true: + return true; + case 'no': + case 'false': + case false: + return false; + default: + return 'normalizeBoolean error'; // Return the original value if it does not match any of the boolean representations + } +}; -/** - * Enum of Boolean types. - * This enum needs to be kept in sync with the enum in src/client/app/types/csvUploadForm.ts - * @enum {string} - */ -BooleanMeterTypesJS = Object.freeze({ - true: 'yes', - false: 'no', - // meter means to use value stored on meter or the default if not. - meter: 'meter value or default' -}); +// This is to allow curl users to still send 'yes' and 'no' for parameters sent, +// The frontend will just use standard boolean values to send to the API +const BooleanCheckArray = ['yes', 'no', 'true', 'false', true, false]; // These are the default values of CSV Pipeline upload parameters. If a user does not specify // a choice for a particular parameter, then these defaults will be used. @@ -66,28 +57,26 @@ BooleanMeterTypesJS = Object.freeze({ // we should change these strings to booleans. const DEFAULTS = { common: { - gzip: BooleanTypesJS.true, - headerRow: BooleanTypesJS.false, - update: BooleanTypesJS.false + gzip: true, + headerRow: false, + update: false }, meters: { }, readings: { - timeSort: undefined, - duplications: undefined, - cumulative: BooleanMeterTypesJS.meter, - cumulativeReset: BooleanMeterTypesJS.meter, + cumulative: undefined, + cumulativeReset: undefined, cumulativeResetStart: undefined, cumulativeResetEnd: undefined, + duplications: undefined, + endOnly: undefined, + honorDst: false, lengthGap: undefined, lengthVariation: undefined, - endOnly: undefined, - createMeter: BooleanTypesJS.false, - refreshReadings: BooleanTypesJS.false, - refreshHourlyReadings: BooleanTypesJS.false, - honorDst: BooleanTypesJS.false, - relaxedParsing: BooleanTypesJS.false, - useMeterZone: BooleanTypesJS.false + refreshReadings: false, + relaxedParsing: false, + timeSort: undefined, + useMeterZone: false } } @@ -97,17 +86,18 @@ const DEFAULTS = { // (i.e. when the user performs a curl request to the pipeline). Thus, we list these properties // here so that they do not falsely trigger the 'additionalProperties' User Error. const COMMON_PROPERTIES = { - meterName: new StringParam('meterName', undefined, undefined), + meterIdentifier: new StringParam('meterIdentifier', undefined, undefined), username: new StringParam('username', undefined, undefined), // TODO: // Allowing for backwards compatibility to allow for curl users to use the 'email' parameter instead of - // the 'username' parameter to login. Developers need to decide in the future if we should deprecate email - // or continue to allow this backwards compatibility + // the 'username' parameter to login. Also allowing to use 'meterName' vs 'meterIdentifier'. Developers need + // to decide in the future if we should deprecate email & meterName or continue to allow this backwards compatibility. + meterName: new StringParam('meterName', undefined, undefined), email: new StringParam('email', undefined, undefined), password: new StringParam('password', undefined, undefined), - gzip: new BooleanParam('gzip'), - headerRow: new BooleanParam('headerRow'), - update: new BooleanParam('update') + gzip: new EnumParam('gzip', BooleanCheckArray), + headerRow: new EnumParam('headerRow', BooleanCheckArray), + update: new EnumParam('update', BooleanCheckArray) } // This sets the validation schemas for jsonschema. @@ -122,25 +112,26 @@ const VALIDATION = { }, readings: { type: 'object', - required: ['meterName'], properties: { ...COMMON_PROPERTIES, - timeSort: new EnumParam('timeSort', [TimeSortTypesJS.increasing, TimeSortTypesJS.decreasing, TimeSortTypesJS.meter]), - duplications: new StringParam('duplications', '^\\d+$|^(?![\s\S])', 'duplications must be an integer or empty.'), - cumulative: new EnumParam('cumulative', [BooleanMeterTypesJS.true, BooleanMeterTypesJS.false, BooleanMeterTypesJS.meter]), - cumulativeReset: new EnumParam('cumulativeReset', [BooleanMeterTypesJS.true, BooleanMeterTypesJS.false, BooleanMeterTypesJS.meter]), + cumulative: new EnumParam('cumulative', BooleanCheckArray), + cumulativeReset: new EnumParam('cumulativeReset', BooleanCheckArray), cumulativeResetStart: new StringParam('cumulativeResetStart', undefined, undefined), cumulativeResetEnd: new StringParam('cumulativeResetEnd', undefined, undefined), + duplications: new StringParam('duplications', '^\\d+$|^(?![\s\S])', 'duplications must be an integer or empty.'), + endOnly: new EnumParam('endOnly', BooleanCheckArray), + honorDst: new EnumParam('honorDst', BooleanCheckArray), lengthGap: new StringParam('lengthGap', undefined, undefined), lengthVariation: new StringParam('lengthVariation', undefined, undefined), - endOnly: new EnumParam('endOnly', [BooleanMeterTypesJS.true, BooleanMeterTypesJS.false, BooleanMeterTypesJS.meter]), - createMeter: new BooleanParam('createMeter'), - refreshReadings: new BooleanParam('refreshReadings'), - refreshHourlyReadings: new BooleanParam('refreshHourlyReadings'), - honorDst: new BooleanParam('honorDst'), - relaxedParsing: new BooleanParam('relaxedParsing'), - useMeterZone: new BooleanParam('useMeterZone') + refreshReadings: new EnumParam('refreshReadings', BooleanCheckArray), + relaxedParsing: new EnumParam('relaxedParsing', BooleanCheckArray), + timeSort: new EnumParam('timeSort', [MeterTimeSortTypesJS.increasing, MeterTimeSortTypesJS.decreasing]), + useMeterZone: new EnumParam('useMeterZone', BooleanCheckArray), }, + anyOf: [ + { required: ['meterIdentifier'] }, + { required: ['meterName'] } + ], additionalProperties: false // This protects us from unintended parameters as well as typos. } } @@ -158,7 +149,7 @@ function validateRequestParams(body, schema) { } else if (err.name === 'required') { responseMessage = 'User Error: ' + responseMessage + err.path + ': ' + `${err.argument} must be provided as the field ${err.argument}=.\n`; } else if (err.name === 'additionalProperties') { - responseMessage = 'User Error: ' + responseMessage + err.path + ': '+ err.argument + ' is an unexpected argument.\n'; + responseMessage = 'User Error: ' + responseMessage + err.path + ': ' + err.argument + ' is an unexpected argument.\n'; } else { responseMessage = responseMessage + err.path + ': ' + 'has message: ' + err.message; } @@ -174,7 +165,6 @@ function validateRequestParams(body, schema) { } } - /** * Middleware that validates a request to upload readings via the CSV Pipeline and sets defaults for upload parameters. * @param {express.Request} req @@ -188,38 +178,39 @@ function validateReadingsCsvUploadParams(req, res, next) { failure(req, res, new CSVPipelineError(responseMessage)); return; } + + const { cumulative, cumulativeReset, duplications, gzip, headerRow, timeSort, update, honorDst, + relaxedParsing, useMeterZone } = req.body; // extract query parameters - const { createMeter, cumulative, duplications, - gzip, headerRow, timeSort, update, honorDst, relaxedParsing, useMeterZone } = req.body; // extract query parameters // Set default values of not supplied parameters. - if (!createMeter) { - req.body.createMeter = DEFAULTS.readings.createMeter; - } - if (!cumulative) { + if (cumulative === undefined) { req.body.cumulative = DEFAULTS.readings.cumulative; } - if (!duplications) { + if (cumulativeReset === undefined) { + req.body.cumulativeReset = DEFAULTS.readings.cumulativeReset; + } + if (duplications === undefined) { req.body.duplications = DEFAULTS.readings.duplications; } - if (!gzip) { + if (gzip === undefined) { req.body.gzip = DEFAULTS.common.gzip; } - if (!headerRow) { + if (headerRow === undefined) { req.body.headerRow = DEFAULTS.common.headerRow; } - if (!timeSort) { + if (timeSort === undefined) { req.body.timeSort = DEFAULTS.readings.timeSort; } - if (!update) { + if (update === undefined) { req.body.update = DEFAULTS.common.update; } - if (!honorDst) { + if (honorDst === undefined) { req.body.honorDst = DEFAULTS.readings.honorDst; } - if (!relaxedParsing) { + if (relaxedParsing === undefined) { req.body.relaxedParsing = DEFAULTS.readings.relaxedParsing; } - if (!useMeterZone) { + if (useMeterZone === undefined) { req.body.useMeterZone = DEFAULTS.readings.useMeterZone; } next(); @@ -242,13 +233,13 @@ function validateMetersCsvUploadParams(req, res, next) { const { gzip, headerRow, update } = req.body; // Extract query parameters // Set default values of not supplied parameters. - if (!gzip) { + if (gzip === undefined) { req.body.gzip = DEFAULTS.common.gzip; } - if (!headerRow) { + if (headerRow === undefined) { req.body.headerRow = DEFAULTS.common.headerRow; } - if (!update) { + if (update === undefined) { req.body.update = DEFAULTS.common.update; } next(); @@ -257,8 +248,6 @@ function validateMetersCsvUploadParams(req, res, next) { module.exports = { validateMetersCsvUploadParams, validateReadingsCsvUploadParams, - TimeSortTypesJS, MeterTimeSortTypesJS, - BooleanMeterTypesJS, - BooleanTypesJS + normalizeBoolean }; diff --git a/src/server/services/pipeline-in-progress/processData.js b/src/server/services/pipeline-in-progress/processData.js index 55cf8557d..f458e1d3c 100644 --- a/src/server/services/pipeline-in-progress/processData.js +++ b/src/server/services/pipeline-in-progress/processData.js @@ -10,7 +10,7 @@ const Meter = require('../../models/Meter'); const Reading = require('../../models/Reading'); const handleCumulativeReset = require('./handleCumulativeReset'); const { validateReadings } = require('./validateReadings'); -const { TimeSortTypesJS } = require('../csvPipeline/validateCsvUploadParams'); +const { MeterTimeSortTypesJS } = require('../csvPipeline/validateCsvUploadParams'); const { meterTimezone } = require('../meterTimezone'); // The default start/end timestamps that are set to the first @@ -53,7 +53,7 @@ const E0 = moment(0).utc() * be avoided except in special circumstances. * @returns {object[]} {array of readings accepted, true if all readings accepted and false otherwise, all messages from processing} */ -async function processData(rows, meterID, timeSort = TimeSortTypesJS.increasing, readingRepetition, isCumulative, cumulativeReset, +async function processData(rows, meterID, timeSort = MeterTimeSortTypesJS.increasing, readingRepetition, isCumulative, cumulativeReset, resetStart = '00:00:00.000', resetEnd = '23:59:99.999', readingGap = 0, readingLengthVariation = 0, isEndTime = false, conditionSet, conn, honorDst = false, relaxedParsing = false, useMeterZone = false) { // Holds all the warning message to pass back to inform user. @@ -71,7 +71,7 @@ async function processData(rows, meterID, timeSort = TimeSortTypesJS.increasing, // Usually holds current message(s) that are yet to be added to msgTotal. let errMsg; // Tells sorted order of readings. - const isAscending = (timeSort === TimeSortTypesJS.increasing); + const isAscending = (timeSort === MeterTimeSortTypesJS.increasing); // Convert readingGap and readingLengthVariation to milliseconds to stay consistent with moment.diff() which returns the difference in milliseconds const msReadingGap = readingGap * 1000; const msReadingLengthVariation = readingLengthVariation * 1000; diff --git a/src/server/services/pipeline-in-progress/validateReadings.js b/src/server/services/pipeline-in-progress/validateReadings.js index 882626bdd..7e9affa49 100644 --- a/src/server/services/pipeline-in-progress/validateReadings.js +++ b/src/server/services/pipeline-in-progress/validateReadings.js @@ -11,12 +11,12 @@ const { log } = require('../../log'); * Validate an array of Readings value according to certain criteria * @param {Reading[]} arrayToValidate * @param {dict} conditionSet used to validate readings (minVal, maxVal, minDate, maxDate, threshold, maxError) - * @param {string} meterName name of meter being checked + * @param {string} meterIdentifier identifier of meter being checked */ -function validateReadings(arrayToValidate, conditionSet, meterName = undefined) { +function validateReadings(arrayToValidate, conditionSet, meterIdentifier = undefined) { /* tslint:disable:no-string-literal */ - const { validDates, errMsg: errMsgDate } = checkDate(arrayToValidate, conditionSet['minDate'], conditionSet['maxDate'], conditionSet['maxError'] / 2, meterName); - const { validValues, errMsg: errMsgValue } = checkValue(arrayToValidate, conditionSet['minVal'], conditionSet['maxVal'], conditionSet['maxError'] / 2, meterName); + const { validDates, errMsg: errMsgDate } = checkDate(arrayToValidate, conditionSet['minDate'], conditionSet['maxDate'], conditionSet['maxError'] / 2, meterIdentifier); + const { validValues, errMsg: errMsgValue } = checkValue(arrayToValidate, conditionSet['minVal'], conditionSet['maxVal'], conditionSet['maxError'] / 2, meterIdentifier); /* tslint:enable:no-string-literal */ const errMsg = errMsgDate + errMsgValue; return { @@ -31,8 +31,9 @@ function validateReadings(arrayToValidate, conditionSet, meterName = undefined) * @param {Moment} minDate inclusive earliest acceptable date (won't be rejected) * @param {Moment} maxDate inclusive latest acceptable date (won't be rejected) * @param {number} maxError maximum number of errors to be reported, ignore the rest + * @param {string} meterIdentifier identifier of meter being checked. */ -function checkDate(arrayToValidate, minDate, maxDate, maxError, meterName) { +function checkDate(arrayToValidate, minDate, maxDate, maxError, meterIdentifier) { let validDates = true; let errMsg = ''; if (minDate === null && maxDate === null) { @@ -45,7 +46,7 @@ function checkDate(arrayToValidate, minDate, maxDate, maxError, meterName) { break; } if (reading.startTimestamp < minDate) { - const newErrMsg = `error when checking reading time for #${readingNumber} on meter ${meterName}: ` + + const newErrMsg = `error when checking reading time for #${readingNumber} on meter ${meterIdentifier}: ` + `time ${reading.startTimestamp} is earlier than lower bound ${minDate} ` + `with reading ${reading.reading} and endTimestamp ${reading.endTimestamp}`; log.error(newErrMsg); @@ -54,9 +55,9 @@ function checkDate(arrayToValidate, minDate, maxDate, maxError, meterName) { validDates = false; } if (reading.endTimestamp > maxDate) { - const newErrMsg = `error when checking reading time for #${readingNumber} on meter ${meterName}: ` + - `time ${reading.endTimestamp} is later than upper bound ${maxDate} ` + - `with reading ${reading.reading} and startTimestamp ${reading.startTimestamp}`; + const newErrMsg = `error when checking reading time for #${readingNumber} on meter ${meterIdentifier}: ` + + `time ${reading.endTimestamp} is later than upper bound ${maxDate} ` + + `with reading ${reading.reading} and startTimestamp ${reading.startTimestamp}`; log.error(newErrMsg); errMsg += '
' + newErrMsg + '
'; --maxError; @@ -72,8 +73,9 @@ function checkDate(arrayToValidate, minDate, maxDate, maxError, meterName) { * @param {number} minVal inclusive minimum acceptable reading value (won't be rejected) * @param {number} maxVal inclusive maximum acceptable reading value (won't be rejected) * @param {number} maxError maximum number of errors to be reported, ignore the rest + * @param {string} meterIdentifier identifier of meter being checked. */ -function checkValue(arrayToValidate, minVal, maxVal, maxError, meterName) { +function checkValue(arrayToValidate, minVal, maxVal, maxError, meterIdentifier) { let validValues = true; let errMsg = ''; let readingNumber = 0; @@ -83,17 +85,17 @@ function checkValue(arrayToValidate, minVal, maxVal, maxError, meterName) { break; } if (reading.reading < minVal) { - const newErrMsg = `error when checking reading value for #${readingNumber} on meter ${meterName}: ` + - `value ${reading.reading} is smaller than lower bound ${minVal} ` + - `with startTimestamp ${reading.startTimestamp} and endTimestamp ${reading.endTimestamp}`; + const newErrMsg = `error when checking reading value for #${readingNumber} on meter ${meterIdentifier}: ` + + `value ${reading.reading} is smaller than lower bound ${minVal} ` + + `with startTimestamp ${reading.startTimestamp} and endTimestamp ${reading.endTimestamp}`; log.error(newErrMsg); errMsg += '
' + newErrMsg + '
'; --maxError; validValues = false; } else if (reading.reading > maxVal) { - const newErrMsg = `error when checking reading value for #${readingNumber} on meter ${meterName}: ` + - `value ${reading.reading} is larger than upper bound ${maxVal} ` + - `with startTimestamp ${reading.startTimestamp} and endTimestamp ${reading.endTimestamp}`; + const newErrMsg = `error when checking reading value for #${readingNumber} on meter ${meterIdentifier}: ` + + `value ${reading.reading} is larger than upper bound ${maxVal} ` + + `with startTimestamp ${reading.startTimestamp} and endTimestamp ${reading.endTimestamp}`; log.error(newErrMsg); errMsg += '
' + newErrMsg + '
'; --maxError; @@ -107,8 +109,9 @@ function checkValue(arrayToValidate, minVal, maxVal, maxError, meterName) { * Check and report unequal intervals. Can be ignore by passing null interval * @param {Readings[]} arrayToValidate * @param {number} threshold the maximum allowed difference between consecutive data points' intervals + * @param {string} meterIdentifier identifier of meter being checked. */ -function checkIntervals(arrayToValidate, threshold, meterName) { +function checkIntervals(arrayToValidate, threshold, meterIdentifier) { let validIntervals = true; let errMsg = ''; @@ -130,9 +133,9 @@ function checkIntervals(arrayToValidate, threshold, meterName) { const currGap = reading.startTimestamp.diff(lastTime, 'seconds'); // Compare the current time gap with the expected interval. Terminate if the difference is larger than the accepted threshold if (Math.abs(currGap - interval) > threshold) { - const newErrMsg = `warning when checking reading gap for #${readingNumber} on meter ${meterName}: ` + - `time gap is detected between current start time ${reading.startTimestamp} and previous end time ${lastTime} that exceeds threshold of ${threshold} ` + - `with current reading ${reading.reading} and endTimestamp ${reading.endTimestamp}`; + const newErrMsg = `warning when checking reading gap for #${readingNumber} on meter ${meterIdentifier}: ` + + `time gap is detected between current start time ${reading.startTimestamp} and previous end time ${lastTime} that exceeds threshold of ${threshold} ` + + `with current reading ${reading.reading} and endTimestamp ${reading.endTimestamp}`; log.error(newErrMsg); errMsg += '
' + newErrMsg + '
'; validIntervals = false; diff --git a/src/server/sql/meter/get_meter_by_identifier.sql b/src/server/sql/meter/get_meter_by_identifier.sql new file mode 100644 index 000000000..3c4f22dd8 --- /dev/null +++ b/src/server/sql/meter/get_meter_by_identifier.sql @@ -0,0 +1,5 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +SELECT * FROM meters WHERE identifier=${identifier}; diff --git a/src/server/sql/preferences/insert_default_row.sql b/src/server/sql/preferences/insert_default_row.sql index 9b1285a46..ab2bd8f04 100644 --- a/src/server/sql/preferences/insert_default_row.sql +++ b/src/server/sql/preferences/insert_default_row.sql @@ -21,7 +21,7 @@ IF NOT EXISTS(SELECT * default_meter_disable_checks, default_help_url) VALUES ('', 'line', FALSE, 'en', NULL, 5, 25, FALSE, 'meters', '00:15:00', -9007199254740991, 9007199254740991, '1970-01-01 00:00:00+00:00', '6970-01-01 00:00:00+00:00', - 0, 75, FALSE, 'https://openenergydashboard.github.io/'); + 0, 75, FALSE, 'https://openenergydashboard.org/'); END IF ; END; diff --git a/src/server/sql/reading/create_function_get_compare_readings.sql b/src/server/sql/reading/create_function_get_compare_readings.sql index cee092b6c..98166c287 100644 --- a/src/server/sql/reading/create_function_get_compare_readings.sql +++ b/src/server/sql/reading/create_function_get_compare_readings.sql @@ -31,6 +31,9 @@ daily and hourly readings would help fix this. /* The following function returns data for plotting compare graphs. It works on meters. It should not be used on raw readings. +This only works when the curr start/end times are on the hour as it uses the hourly reading view. +It will truncate to lose partial hours if they are given. The current client code will only +send full hours so this should be fine. It is the new version of compare_readings that works with units. It takes these parameters: meter_ids: A array of meter ids to query. graphic_unit_id: The unit id of the unit to use for the graph. @@ -49,11 +52,11 @@ CREATE OR REPLACE FUNCTION meter_compare_readings_unit ( RETURNS TABLE(meter_id INTEGER, curr_use FLOAT, prev_use FLOAT) AS $$ DECLARE - prev_start TIMESTAMP; - prev_end TIMESTAMP; + curr_tsrange TSRANGE; + prev_tsrange TSRANGE; BEGIN - prev_start := curr_start - shift; - prev_end := curr_end - shift; + curr_tsrange := tsrange(curr_start, curr_end); + prev_tsrange := tsrange(curr_start - shift, curr_end - shift); RETURN QUERY WITH @@ -61,28 +64,32 @@ BEGIN SELECT meters.id AS meter_id, -- Convert the reading based on the conversion found below. - SUM(r.reading) * c.slope + c.intercept AS reading - FROM (((readings r - INNER JOIN unnest(meter_ids) meters(id) ON r.meter_id = meters.id) + -- It is okay to sum the flow units because hourly readings has a rate of per hour so + -- to convert to a quantity would multiply by 1 so it is the same formula. + SUM(hourly.reading_rate) * c.slope + c.intercept AS reading + FROM (((hourly_readings_unit hourly + INNER JOIN unnest(meter_ids) meters(id) ON hourly.meter_id = meters.id) INNER JOIN meters m ON m.id = meters.id) -- This is getting the conversion for the meter and unit to graph. -- The slope and intercept are used above the transform the reading to the desired unit. INNER JOIN cik c on c.source_id = m.unit_id AND c.destination_id = graphic_unit_id) - WHERE r.start_timestamp >= curr_start AND r.end_timestamp <= curr_end + WHERE curr_tsrange @> hourly.time_interval GROUP BY meters.id, c.slope, c.intercept ), prev_period AS ( SELECT meters.id AS meter_id, -- Convert the reading based on the conversion found below. - SUM(r.reading) * c.slope + c.intercept AS reading - FROM (((readings r - INNER JOIN unnest(meter_ids) meters(id) ON r.meter_id = meters.id) + -- It is okay to sum the flow units because hourly readings has a rate of per hour so + -- to convert to a quantity would multiply by 1 so it is the same formula. + SUM(hourly.reading_rate) * c.slope + c.intercept AS reading + FROM (((hourly_readings_unit hourly + INNER JOIN unnest(meter_ids) meters(id) ON hourly.meter_id = meters.id) INNER JOIN meters m ON m.id = meters.id) -- This is getting the conversion for the meter and unit to graph. -- The slope and intercept are used above the transform the reading to the desired unit. INNER JOIN cik c on c.source_id = m.unit_id AND c.destination_id = graphic_unit_id) - WHERE r.start_timestamp >= prev_start AND r.end_timestamp <= prev_end + WHERE prev_tsrange @> hourly.time_interval GROUP BY meters.id, c.slope, c.intercept ) SELECT @@ -99,8 +106,9 @@ $$ LANGUAGE 'plpgsql'; /* -The following function returns data for plotting bacompare graphs. It works on groups. +The following function returns data for plotting compare graphs. It works on groups. It should not be used on raw readings. +See meter version for needing start/end on the hour. It is the new version of group_compare_readings that works with units. It takes these parameters: group_ids: A array of group ids to query. graphic_unit_id: The unit id of the unit to use for the graph. diff --git a/src/server/test/db/compareTests.js b/src/server/test/db/compareTests.js index 5398b9769..03f0914bc 100644 --- a/src/server/test/db/compareTests.js +++ b/src/server/test/db/compareTests.js @@ -13,6 +13,7 @@ const Unit = require('../../models/Unit'); const { insertStandardUnits, insertStandardConversions } = require('../../util/insertData'); const { insertSpecialUnits, insertSpecialConversions } = require('../../data/automatedTestingData'); const { redoCik } = require('../../services/graph/redoCik'); +const { refreshAllReadingViews } = require('../../services/refreshAllReadingViews'); mocha.describe('Compare readings', () => { let meter, graphicUnitId, conversionSlope, conn; @@ -72,13 +73,15 @@ mocha.describe('Compare readings', () => { graphicUnitId = (await Unit.getByName('MJ', conn)).id; // The conversion should be 3.6 from kWh -> MJ. conversionSlope = 3.6; + await refreshAllReadingViews(); }); // TODO test readings, units. mocha.it('Works for meters', async () => { const result = await Reading.getMeterCompareReadings([meter.id], graphicUnitId, currStart, currEnd, shift, conn); - expect(result).to.deep.equal({ [meter.id]: { curr_use: 10 * conversionSlope, prev_use: 1 * conversionSlope } }); + expect(result).to.have.property(`${meter.id}`).to.have.property('curr_use').to.be.closeTo(10 * conversionSlope, 0.0000001); + expect(result).to.have.property(`${meter.id}`).to.have.property('prev_use').to.be.closeTo(1 * conversionSlope, 0.0000001); }); mocha.it('Works for groups', async () => { @@ -86,6 +89,7 @@ mocha.describe('Compare readings', () => { const group = await Group.getByName('Group', conn); await group.adoptMeter(meter.id, conn); const result = await Reading.getGroupCompareReadings([group.id], graphicUnitId, currStart, currEnd, shift, conn); - expect(result).to.deep.equal({ [group.id]: { curr_use: 10 * conversionSlope, prev_use: 1 * conversionSlope } }); + expect(result).to.have.property(`${group.id}`).to.have.property('curr_use').to.be.closeTo(10 * conversionSlope, 0.0000001); + expect(result).to.have.property(`${group.id}`).to.have.property('prev_use').to.be.closeTo(1 * conversionSlope, 0.0000001); }); }); diff --git a/src/server/test/web/csvPipelineTest.js b/src/server/test/web/csvPipelineTest.js index 0366dec06..7abc7fcea 100644 --- a/src/server/test/web/csvPipelineTest.js +++ b/src/server/test/web/csvPipelineTest.js @@ -9,7 +9,6 @@ const Point = require('../../models/Point'); const Unit = require('../../models/Unit'); const { insertStandardUnits, insertStandardConversions, insertUnits, insertConversions } = require('../../util/insertData') const { redoCik } = require('../../services/graph/redoCik'); -const { BooleanTypesJS } = require('../../services/csvPipeline/validateCsvUploadParams'); const util = require('util'); const fs = require('fs'); const csv = require('csv'); @@ -42,189 +41,216 @@ const CHAI_METERS_REQUEST_EMAIL = `chai.request(app).post('${UPLOAD_METERS_ROUTE const testCases = { pipe1: { description: 'Ascending time readings', - chaiRequest: [CHAI_READINGS_REQUEST + ".field('createMeter', BooleanTypesJS.true).field('meterName', 'pipe1').field('gzip', BooleanTypesJS.false)"], + chaiRequest: [CHAI_READINGS_REQUEST + ".field('meterIdentifier', 'pipe1').field('gzip', false)"], fileName: ['pipe1Input.csv'], + createMeter: true, responseCode: [200], responseString: ['

SUCCESS

It looks like the insert of the readings was a success.

'] }, pipe2: { description: 'Descending time readings', - chaiRequest: [CHAI_READINGS_REQUEST + ".field('createMeter', BooleanTypesJS.true).field('meterName', 'pipe2').field('gzip', BooleanTypesJS.false).field('timeSort', 'decreasing')"], + chaiRequest: [CHAI_READINGS_REQUEST + ".field('meterIdentifier', 'pipe2').field('gzip', false).field('timeSort', 'decreasing')"], fileName: ['pipe2Input.csv'], + createMeter: true, responseCode: [200], responseString: ['

SUCCESS

It looks like the insert of the readings was a success.

'] }, pipe3: { description: 'Cumulative time readings', - chaiRequest: [CHAI_READINGS_REQUEST + ".field('createMeter', BooleanTypesJS.true).field('meterName', 'pipe3').field('gzip', BooleanTypesJS.false).field('cumulative', BooleanTypesJS.true)"], + chaiRequest: [CHAI_READINGS_REQUEST + ".field('meterIdentifier', 'pipe3').field('gzip', false).field('cumulative', true)"], fileName: ['pipe3Input.csv'], + createMeter: true, responseCode: [400], responseString: ['

FAILURE

It looks like the insert of the readings had issues with some or all of the readings where the processing of the readings returned these warning(s)/error(s):


For meter pipe3: Error parsing Reading #1. Reading value gives 24 with error message:
The first ever reading must be dropped when dealing with cumulative data.
For reading #1 on meter pipe3 in pipeline: previous reading has value 0 start time 1970-01-01T00:00:00Z end time 1970-01-01T00:00:00Z and current reading has value 24 start time 2021-06-01T00:00:00Z end time 2021-06-02T00:00:00Z with timeSort increasing; duplications 1; cumulative true; cumulativeReset false; cumulativeResetStart 00:00:00; cumulativeResetEnd 23:59:59.999999; lengthGap 0; lengthVariation 0; onlyEndTime false

Readings Dropped and should have previous messages

  1. Dropped Reading #1 for meter pipe3
'] }, pipe4: { description: 'Cumulative, descending time readings', - chaiRequest: [CHAI_READINGS_REQUEST + ".field('timeSort','decreasing').field('cumulative',BooleanTypesJS.true).field('meterName','pipe4').field('createMeter',BooleanTypesJS.true).field('gzip', BooleanTypesJS.false)"], + chaiRequest: [CHAI_READINGS_REQUEST + ".field('timeSort','decreasing').field('cumulative',true).field('meterIdentifier','pipe4').field('gzip', false)"], fileName: ['pipe4Input.csv'], + createMeter: true, responseCode: [400], responseString: ['

FAILURE

It looks like the insert of the readings had issues with some or all of the readings where the processing of the readings returned these warning(s)/error(s):


For meter pipe4: Error parsing Reading #1. Reading value gives 24 with error message:
The first ever reading must be dropped when dealing with cumulative data.
For reading #1 on meter pipe4 in pipeline: previous reading has value 0 start time 1970-01-01T00:00:00Z end time 1970-01-01T00:00:00Z and current reading has value 24 start time 2021-06-01T00:00:00Z end time 2021-06-02T00:00:00Z with timeSort decreasing; duplications 1; cumulative true; cumulativeReset false; cumulativeResetStart 00:00:00; cumulativeResetEnd 23:59:59.999999; lengthGap 0; lengthVariation 0; onlyEndTime false

Readings Dropped and should have previous messages

  1. Dropped Reading #1 for meter pipe4
'] }, pipe5: { description: 'Cumulative time readings with reset with default cumulative reset', - chaiRequest: [CHAI_READINGS_REQUEST + ".field('createMeter', BooleanTypesJS.true).field('meterName', 'pipe5').field('gzip', BooleanTypesJS.false).field('cumulative', BooleanTypesJS.true).field('cumulativeReset',BooleanTypesJS.true)"], + chaiRequest: [CHAI_READINGS_REQUEST + ".field('meterIdentifier', 'pipe5').field('gzip', false).field('cumulative', true).field('cumulativeReset',true)"], + createMeter: true, fileName: ['pipe5Input.csv'], responseCode: [400], responseString: ['

FAILURE

It looks like the insert of the readings had issues with some or all of the readings where the processing of the readings returned these warning(s)/error(s):


For meter pipe5: Error parsing Reading #1. Reading value gives 24 with error message:
The first ever reading must be dropped when dealing with cumulative data.
For reading #1 on meter pipe5 in pipeline: previous reading has value 0 start time 1970-01-01T00:00:00Z end time 1970-01-01T00:00:00Z and current reading has value 24 start time 2021-06-01T00:00:00Z end time 2021-06-02T00:00:00Z with timeSort increasing; duplications 1; cumulative true; cumulativeReset true; cumulativeResetStart 00:00:00; cumulativeResetEnd 23:59:59.999999; lengthGap 0; lengthVariation 0; onlyEndTime false

Readings Dropped and should have previous messages

  1. Dropped Reading #1 for meter pipe5
'] }, pipe6: { description: 'Cumulative time readings with reset with cumulative reset around midnight', - chaiRequest: [CHAI_READINGS_REQUEST + ".field('cumulative',BooleanTypesJS.true).field('cumulativeReset',BooleanTypesJS.true).field('cumulativeResetStart','23:45').field('cumulativeResetEnd','00:15').field('meterName','pipe6').field('createMeter',BooleanTypesJS.true).field('gzip',BooleanTypesJS.false)"], + chaiRequest: [CHAI_READINGS_REQUEST + ".field('cumulative',true).field('cumulativeReset',true).field('cumulativeResetStart','23:45').field('cumulativeResetEnd','00:15').field('meterIdentifier','pipe6').field('gzip',false)"], fileName: ['pipe6Input.csv'], + createMeter: true, responseCode: [400], responseString: ['

FAILURE

It looks like the insert of the readings had issues with some or all of the readings where the processing of the readings returned these warning(s)/error(s):


For meter pipe6: Error parsing Reading #1. Reading value gives 24 with error message:
The first ever reading must be dropped when dealing with cumulative data.
For reading #1 on meter pipe6 in pipeline: previous reading has value 0 start time 1970-01-01T00:00:00Z end time 1970-01-01T00:00:00Z and current reading has value 24 start time 2021-06-01T00:00:00Z end time 2021-06-02T00:00:00Z with timeSort increasing; duplications 1; cumulative true; cumulativeReset true; cumulativeResetStart 23:45; cumulativeResetEnd 00:15; lengthGap 0; lengthVariation 0; onlyEndTime false

Readings Dropped and should have previous messages

  1. Dropped Reading #1 for meter pipe6
'] }, pipe7: { description: 'Cumulative time readings with reset with cumulative reset around noon which is incorrect', - chaiRequest: [CHAI_READINGS_REQUEST + ".field('createMeter', BooleanTypesJS.true).field('meterName', 'pipe7').field('gzip', BooleanTypesJS.false).field('cumulative', BooleanTypesJS.true).field('cumulativeReset',BooleanTypesJS.true).field('cumulativeResetStart','11:45').field('cumulativeResetEnd','12:15')"], + chaiRequest: [CHAI_READINGS_REQUEST + ".field('meterIdentifier', 'pipe7').field('gzip', false).field('cumulative', true).field('cumulativeReset',true).field('cumulativeResetStart','11:45').field('cumulativeResetEnd','12:15')"], fileName: ['pipe7Input.csv'], + createMeter: true, responseCode: [400], responseString: ['

FAILURE

It looks like the insert of the readings had issues with some or all of the readings where the processing of the readings returned these warning(s)/error(s):


For meter pipe7: Error parsing Reading #1. Reading value gives 24 with error message:
The first ever reading must be dropped when dealing with cumulative data.
For reading #1 on meter pipe7 in pipeline: previous reading has value 0 start time 1970-01-01T00:00:00Z end time 1970-01-01T00:00:00Z and current reading has value 24 start time 2021-06-01T00:00:00Z end time 2021-06-02T00:00:00Z with timeSort increasing; duplications 1; cumulative true; cumulativeReset true; cumulativeResetStart 11:45; cumulativeResetEnd 12:15; lengthGap 0; lengthVariation 0; onlyEndTime false

For meter pipe7: Error parsing Reading #4. Reading value of 96 gives -48 with error message:
A negative meterReading has been detected but either cumulativeReset is not enabled, or the start time and end time of this reading is out of the reset range. Reject all readings.
For reading #4 on meter pipe7 in pipeline: previous reading has value 72 start time 2021-06-03T00:00:00Z end time 2021-06-04T00:00:00Z and current reading has value -48 start time 2021-06-04T00:00:00Z end time 2021-06-05T00:00:00Z with timeSort increasing; duplications 1; cumulative true; cumulativeReset true; cumulativeResetStart 11:45; cumulativeResetEnd 12:15; lengthGap 0; lengthVariation 0; onlyEndTime false
'], }, pipe8: { description: 'Cumulative time readings with reset with cumulative reset tight around midnight', - chaiRequest: [CHAI_READINGS_REQUEST + ".field('cumulative',BooleanTypesJS.true).field('cumulativeReset',BooleanTypesJS.true).field('cumulativeResetStart','00:00').field('cumulativeResetEnd','00:00.001').field('meterName','pipe8').field('createMeter',BooleanTypesJS.true).field('gzip',BooleanTypesJS.false)"], + chaiRequest: [CHAI_READINGS_REQUEST + ".field('cumulative',true).field('cumulativeReset',true).field('cumulativeResetStart','00:00').field('cumulativeResetEnd','00:00.001').field('meterIdentifier','pipe8').field('gzip',false)"], fileName: ['pipe8Input.csv'], + createMeter: true, responseCode: [400], responseString: ['

FAILURE

It looks like the insert of the readings had issues with some or all of the readings where the processing of the readings returned these warning(s)/error(s):


For meter pipe8: Error parsing Reading #1. Reading value gives 24 with error message:
The first ever reading must be dropped when dealing with cumulative data.
For reading #1 on meter pipe8 in pipeline: previous reading has value 0 start time 1970-01-01T00:00:00Z end time 1970-01-01T00:00:00Z and current reading has value 24 start time 2021-06-01T00:00:00Z end time 2021-06-02T00:00:00Z with timeSort increasing; duplications 1; cumulative true; cumulativeReset true; cumulativeResetStart 00:00; cumulativeResetEnd 00:00.001; lengthGap 0; lengthVariation 0; onlyEndTime false

Readings Dropped and should have previous messages

  1. Dropped Reading #1 for meter pipe8
'] }, pipe9: { description: 'Cumulative time readings with reset without cumulative reset which is incorrect', - chaiRequest: [CHAI_READINGS_REQUEST + ".field('cumulative',BooleanTypesJS.true).field('meterName','pipe9').field('createMeter',BooleanTypesJS.true).field('gzip', BooleanTypesJS.false)"], + chaiRequest: [CHAI_READINGS_REQUEST + ".field('cumulative',true).field('meterIdentifier','pipe9').field('gzip', false)"], fileName: ['pipe9Input.csv'], + createMeter: true, responseCode: [400], responseString: ['

FAILURE

It looks like the insert of the readings had issues with some or all of the readings where the processing of the readings returned these warning(s)/error(s):


For meter pipe9: Error parsing Reading #1. Reading value gives 24 with error message:
The first ever reading must be dropped when dealing with cumulative data.
For reading #1 on meter pipe9 in pipeline: previous reading has value 0 start time 1970-01-01T00:00:00Z end time 1970-01-01T00:00:00Z and current reading has value 24 start time 2021-06-01T00:00:00Z end time 2021-06-02T00:00:00Z with timeSort increasing; duplications 1; cumulative true; cumulativeReset false; cumulativeResetStart 00:00:00; cumulativeResetEnd 23:59:59.999999; lengthGap 0; lengthVariation 0; onlyEndTime false

For meter pipe9: Error parsing Reading #4. Reading value of 96 gives -48 with error message:
A negative meterReading has been detected but either cumulativeReset is not enabled, or the start time and end time of this reading is out of the reset range. Reject all readings.
For reading #4 on meter pipe9 in pipeline: previous reading has value 72 start time 2021-06-03T00:00:00Z end time 2021-06-04T00:00:00Z and current reading has value -48 start time 2021-06-04T00:00:00Z end time 2021-06-05T00:00:00Z with timeSort increasing; duplications 1; cumulative true; cumulativeReset false; cumulativeResetStart 00:00:00; cumulativeResetEnd 23:59:59.999999; lengthGap 0; lengthVariation 0; onlyEndTime false
'] }, pipe10: { description: 'Cumulative time readings changing at noon with default cumulative reset', - chaiRequest: [CHAI_READINGS_REQUEST + ".field('cumulative',BooleanTypesJS.true).field('cumulativeReset',BooleanTypesJS.true).field('meterName','pipe10').field('createMeter',BooleanTypesJS.true).field('gzip',BooleanTypesJS.false)"], + chaiRequest: [CHAI_READINGS_REQUEST + ".field('cumulative',true).field('cumulativeReset',true).field('meterIdentifier','pipe10').field('gzip',false)"], fileName: ['pipe10Input.csv'], + createMeter: true, responseCode: [400], responseString: ['

FAILURE

It looks like the insert of the readings had issues with some or all of the readings where the processing of the readings returned these warning(s)/error(s):


For meter pipe10: Error parsing Reading #1. Reading value gives 24 with error message:
The first ever reading must be dropped when dealing with cumulative data.
For reading #1 on meter pipe10 in pipeline: previous reading has value 0 start time 1970-01-01T00:00:00Z end time 1970-01-01T00:00:00Z and current reading has value 24 start time 2021-06-01T12:00:00Z end time 2021-06-02T12:00:00Z with timeSort increasing; duplications 1; cumulative true; cumulativeReset true; cumulativeResetStart 00:00:00; cumulativeResetEnd 23:59:59.999999; lengthGap 0; lengthVariation 0; onlyEndTime false

Readings Dropped and should have previous messages

  1. Dropped Reading #1 for meter pipe10
'] }, pipe11: { description: 'Cumulative time readings changing at noon without cumulative reset which is incorrect', - chaiRequest: [CHAI_READINGS_REQUEST + ".field('cumulative',BooleanTypesJS.true).field('meterName','pipe11').field('createMeter',BooleanTypesJS.true).field('gzip',BooleanTypesJS.false)"], + chaiRequest: [CHAI_READINGS_REQUEST + ".field('cumulative',true).field('meterIdentifier','pipe11').field('gzip',false)"], fileName: ['pipe11Input.csv'], + createMeter: true, responseCode: [400], responseString: ['

FAILURE

It looks like the insert of the readings had issues with some or all of the readings where the processing of the readings returned these warning(s)/error(s):


For meter pipe11: Error parsing Reading #1. Reading value gives 24 with error message:
The first ever reading must be dropped when dealing with cumulative data.
For reading #1 on meter pipe11 in pipeline: previous reading has value 0 start time 1970-01-01T00:00:00Z end time 1970-01-01T00:00:00Z and current reading has value 24 start time 2021-06-01T12:00:00Z end time 2021-06-02T12:00:00Z with timeSort increasing; duplications 1; cumulative true; cumulativeReset false; cumulativeResetStart 00:00:00; cumulativeResetEnd 23:59:59.999999; lengthGap 0; lengthVariation 0; onlyEndTime false

For meter pipe11: Error parsing Reading #4. Reading value of 96 gives -48 with error message:
A negative meterReading has been detected but either cumulativeReset is not enabled, or the start time and end time of this reading is out of the reset range. Reject all readings.
For reading #4 on meter pipe11 in pipeline: previous reading has value 72 start time 2021-06-03T12:00:00Z end time 2021-06-04T12:00:00Z and current reading has value -48 start time 2021-06-04T12:00:00Z end time 2021-06-05T12:00:00Z with timeSort increasing; duplications 1; cumulative true; cumulativeReset false; cumulativeResetStart 00:00:00; cumulativeResetEnd 23:59:59.999999; lengthGap 0; lengthVariation 0; onlyEndTime false
'] }, pipe12: { description: 'Cumulative time readings changing at noon with cumulative reset at noon', - chaiRequest: [CHAI_READINGS_REQUEST + ".field('cumulative',BooleanTypesJS.true).field('cumulativeReset',BooleanTypesJS.true).field('cumulativeResetStart','11:45').field('cumulativeResetEnd','12:15').field('meterName','pipe12').field('createMeter',BooleanTypesJS.true).field('gzip',BooleanTypesJS.false)"], + chaiRequest: [CHAI_READINGS_REQUEST + ".field('cumulative',true).field('cumulativeReset',true).field('cumulativeResetStart','11:45').field('cumulativeResetEnd','12:15').field('meterIdentifier','pipe12').field('gzip',false)"], fileName: ['pipe12Input.csv'], + createMeter: true, responseCode: [400], responseString: ['

FAILURE

It looks like the insert of the readings had issues with some or all of the readings where the processing of the readings returned these warning(s)/error(s):


For meter pipe12: Error parsing Reading #1. Reading value gives 24 with error message:
The first ever reading must be dropped when dealing with cumulative data.
For reading #1 on meter pipe12 in pipeline: previous reading has value 0 start time 1970-01-01T00:00:00Z end time 1970-01-01T00:00:00Z and current reading has value 24 start time 2021-06-01T12:00:00Z end time 2021-06-02T12:00:00Z with timeSort increasing; duplications 1; cumulative true; cumulativeReset true; cumulativeResetStart 11:45; cumulativeResetEnd 12:15; lengthGap 0; lengthVariation 0; onlyEndTime false

Readings Dropped and should have previous messages

  1. Dropped Reading #1 for meter pipe12
'] }, pipe13: { description: 'Cumulative time readings changing at noon with cumulative reset at midnight which is incorrect', - chaiRequest: [CHAI_READINGS_REQUEST + ".field('cumulative',BooleanTypesJS.true).field('cumulativeReset',BooleanTypesJS.true).field('cumulativeResetStart','23:45').field('cumulativeResetEnd','00:15').field('meterName','pipe13').field('createMeter',BooleanTypesJS.true).field('gzip',BooleanTypesJS.false)"], + chaiRequest: [CHAI_READINGS_REQUEST + ".field('cumulative',true).field('cumulativeReset',true).field('cumulativeResetStart','23:45').field('cumulativeResetEnd','00:15').field('meterIdentifier','pipe13').field('gzip',false)"], fileName: ['pipe13Input.csv'], + createMeter: true, responseCode: [400], responseString: ['

FAILURE

It looks like the insert of the readings had issues with some or all of the readings where the processing of the readings returned these warning(s)/error(s):


For meter pipe13: Error parsing Reading #1. Reading value gives 24 with error message:
The first ever reading must be dropped when dealing with cumulative data.
For reading #1 on meter pipe13 in pipeline: previous reading has value 0 start time 1970-01-01T00:00:00Z end time 1970-01-01T00:00:00Z and current reading has value 24 start time 2021-06-01T12:00:00Z end time 2021-06-02T12:00:00Z with timeSort increasing; duplications 1; cumulative true; cumulativeReset true; cumulativeResetStart 23:45; cumulativeResetEnd 00:15; lengthGap 0; lengthVariation 0; onlyEndTime false

For meter pipe13: Error parsing Reading #4. Reading value of 96 gives -48 with error message:
A negative meterReading has been detected but either cumulativeReset is not enabled, or the start time and end time of this reading is out of the reset range. Reject all readings.
For reading #4 on meter pipe13 in pipeline: previous reading has value 72 start time 2021-06-03T12:00:00Z end time 2021-06-04T12:00:00Z and current reading has value -48 start time 2021-06-04T12:00:00Z end time 2021-06-05T12:00:00Z with timeSort increasing; duplications 1; cumulative true; cumulativeReset true; cumulativeResetStart 23:45; cumulativeResetEnd 00:15; lengthGap 0; lengthVariation 0; onlyEndTime false
'] }, pipe14: { description: 'Ascending time readings with length variation and default time variation', - chaiRequest: [CHAI_READINGS_REQUEST + ".field('meterName','pipe14').field('createMeter',BooleanTypesJS.true).field('gzip',BooleanTypesJS.false)"], + chaiRequest: [CHAI_READINGS_REQUEST + ".field('meterIdentifier','pipe14').field('gzip',false)"], fileName: ['pipe14Input.csv'], + createMeter: true, responseCode: [200], responseString: ['

SUCCESS

It looks like the insert of the readings was a success.

However, note that the processing of the readings returned these warning(s):


For meter pipe14: Warning parsing Reading #2. Reading value gives 48 with warning message:
The previous reading has a different time length than the current reading and exceeds the tolerance of 0 seconds. Note this is treated only as a warning since this may be expected for certain meters.
For reading #2 on meter pipe14 in pipeline: previous reading has value 24 start time 2021-06-01T00:00:00Z end time 2021-06-02T00:00:00Z and current reading has value 48 start time 2021-06-02T00:00:00Z end time 2021-06-03T00:01:00Z with timeSort increasing; duplications 1; cumulative false; cumulativeReset false; cumulativeResetStart 00:00:00; cumulativeResetEnd 23:59:59.999999; lengthGap 0; lengthVariation 0; onlyEndTime false

For meter pipe14: Warning parsing Reading #3. Reading value gives 72 with warning message:
The previous reading has a different time length than the current reading and exceeds the tolerance of 0 seconds. Note this is treated only as a warning since this may be expected for certain meters.
For reading #3 on meter pipe14 in pipeline: previous reading has value 48 start time 2021-06-02T00:00:00Z end time 2021-06-03T00:01:00Z and current reading has value 72 start time 2021-06-03T00:01:00Z end time 2021-06-04T00:00:00Z with timeSort increasing; duplications 1; cumulative false; cumulativeReset false; cumulativeResetStart 00:00:00; cumulativeResetEnd 23:59:59.999999; lengthGap 0; lengthVariation 0; onlyEndTime false

For meter pipe14: Warning parsing Reading #4. Reading value gives 96 with warning message:
The previous reading has a different time length than the current reading and exceeds the tolerance of 0 seconds. Note this is treated only as a warning since this may be expected for certain meters.
For reading #4 on meter pipe14 in pipeline: previous reading has value 72 start time 2021-06-03T00:01:00Z end time 2021-06-04T00:00:00Z and current reading has value 96 start time 2021-06-04T00:00:00Z end time 2021-06-04T23:58:00Z with timeSort increasing; duplications 1; cumulative false; cumulativeReset false; cumulativeResetStart 00:00:00; cumulativeResetEnd 23:59:59.999999; lengthGap 0; lengthVariation 0; onlyEndTime false

For meter pipe14: Warning parsing Reading #5. Reading value gives 120 with warning message:
The previous reading has a different time length than the current reading and exceeds the tolerance of 0 seconds. Note this is treated only as a warning since this may be expected for certain meters.
For reading #5 on meter pipe14 in pipeline: previous reading has value 96 start time 2021-06-04T00:00:00Z end time 2021-06-04T23:58:00Z and current reading has value 120 start time 2021-06-04T23:58:00Z end time 2021-06-06T00:00:00Z with timeSort increasing; duplications 1; cumulative false; cumulativeReset false; cumulativeResetStart 00:00:00; cumulativeResetEnd 23:59:59.999999; lengthGap 0; lengthVariation 0; onlyEndTime false
'] }, pipe15: { description: 'Ascending time readings with length variation where length variation set small so warns on 2 readings', - chaiRequest: [CHAI_READINGS_REQUEST + ".field('meterName','pipe15').field('lengthVariation','60').field('createMeter',BooleanTypesJS.true).field('gzip',BooleanTypesJS.false)"], + chaiRequest: [CHAI_READINGS_REQUEST + ".field('meterIdentifier','pipe15').field('lengthVariation','60').field('gzip',false)"], fileName: ['pipe15Input.csv'], + createMeter: true, responseCode: [200], responseString: ['

SUCCESS

It looks like the insert of the readings was a success.

However, note that the processing of the readings returned these warning(s):


For meter pipe15: Warning parsing Reading #3. Reading value gives 72 with warning message:
The previous reading has a different time length than the current reading and exceeds the tolerance of 60 seconds. Note this is treated only as a warning since this may be expected for certain meters.
For reading #3 on meter pipe15 in pipeline: previous reading has value 48 start time 2021-06-02T00:00:00Z end time 2021-06-03T00:01:00Z and current reading has value 72 start time 2021-06-03T00:01:00Z end time 2021-06-04T00:00:00Z with timeSort increasing; duplications 1; cumulative false; cumulativeReset false; cumulativeResetStart 00:00:00; cumulativeResetEnd 23:59:59.999999; lengthGap 0; lengthVariation 60; onlyEndTime false

For meter pipe15: Warning parsing Reading #5. Reading value gives 120 with warning message:
The previous reading has a different time length than the current reading and exceeds the tolerance of 60 seconds. Note this is treated only as a warning since this may be expected for certain meters.
For reading #5 on meter pipe15 in pipeline: previous reading has value 96 start time 2021-06-04T00:00:00Z end time 2021-06-04T23:58:00Z and current reading has value 120 start time 2021-06-04T23:58:00Z end time 2021-06-06T00:00:00Z with timeSort increasing; duplications 1; cumulative false; cumulativeReset false; cumulativeResetStart 00:00:00; cumulativeResetEnd 23:59:59.999999; lengthGap 0; lengthVariation 60; onlyEndTime false
'] }, pipe16: { description: 'Ascending time readings with length variation and length variation set so warns on 1 reading', - chaiRequest: [CHAI_READINGS_REQUEST + ".field('meterName','pipe16').field('lengthVariation','120').field('createMeter',BooleanTypesJS.true).field('gzip',BooleanTypesJS.false)"], + chaiRequest: [CHAI_READINGS_REQUEST + ".field('meterIdentifier','pipe16').field('lengthVariation','120').field('gzip',false)"], fileName: ['pipe16Input.csv'], + createMeter: true, responseCode: [200], responseString: ['

SUCCESS

It looks like the insert of the readings was a success.

However, note that the processing of the readings returned these warning(s):


For meter pipe16: Warning parsing Reading #5. Reading value gives 120 with warning message:
The previous reading has a different time length than the current reading and exceeds the tolerance of 120 seconds. Note this is treated only as a warning since this may be expected for certain meters.
For reading #5 on meter pipe16 in pipeline: previous reading has value 96 start time 2021-06-04T00:00:00Z end time 2021-06-04T23:58:00Z and current reading has value 120 start time 2021-06-04T23:58:00Z end time 2021-06-06T00:00:00Z with timeSort increasing; duplications 1; cumulative false; cumulativeReset false; cumulativeResetStart 00:00:00; cumulativeResetEnd 23:59:59.999999; lengthGap 0; lengthVariation 120; onlyEndTime false
'] }, pipe17: { description: 'Ascending time readings with length variation and gap where length variation set so all pass but gap not big enough so warns on 2 reading', - chaiRequest: [CHAI_READINGS_REQUEST + ".field('meterName','pipe17').field('lengthVariation','121').field('createMeter',BooleanTypesJS.true).field('gzip',BooleanTypesJS.false)"], + chaiRequest: [CHAI_READINGS_REQUEST + ".field('meterIdentifier','pipe17').field('lengthVariation','121').field('gzip',false)"], fileName: ['pipe17Input.csv'], + createMeter: true, responseCode: [200], responseString: ['

SUCCESS

It looks like the insert of the readings was a success.

However, note that the processing of the readings returned these warning(s):


For meter pipe17: Warning parsing Reading #2. Reading value gives 48 with warning message:
There is a gap in time between this reading and the previous reading that exceeds the allowed amount of 0 seconds.
For reading #2 on meter pipe17 in pipeline: previous reading has value 24 start time 2021-06-01T00:00:00Z end time 2021-06-02T00:00:00Z and current reading has value 48 start time 2021-06-02T00:01:00Z end time 2021-06-03T00:00:00Z with timeSort increasing; duplications 1; cumulative false; cumulativeReset false; cumulativeResetStart 00:00:00; cumulativeResetEnd 23:59:59.999999; lengthGap 0; lengthVariation 121; onlyEndTime false

For meter pipe17: Warning parsing Reading #4. Reading value gives 96 with warning message:
There is a gap in time between this reading and the previous reading that exceeds the allowed amount of 0 seconds.
For reading #4 on meter pipe17 in pipeline: previous reading has value 72 start time 2021-06-03T00:00:00Z end time 2021-06-04T00:00:00Z and current reading has value 96 start time 2021-06-04T00:02:00Z end time 2021-06-05T00:00:00Z with timeSort increasing; duplications 1; cumulative false; cumulativeReset false; cumulativeResetStart 00:00:00; cumulativeResetEnd 23:59:59.999999; lengthGap 0; lengthVariation 121; onlyEndTime false
'] }, pipe18: { description: 'Ascending time readings with gaps and small time gap so 1 passes and 1 warns', - chaiRequest: [CHAI_READINGS_REQUEST + ".field('meterName','pipe18').field('lengthGap','60').field('lengthVariation','121').field('createMeter',BooleanTypesJS.true).field('gzip',BooleanTypesJS.false)"], + chaiRequest: [CHAI_READINGS_REQUEST + ".field('meterIdentifier','pipe18').field('lengthGap','60').field('lengthVariation','121').field('gzip',false)"], fileName: ['pipe18Input.csv'], + createMeter: true, responseCode: [200], responseString: ['

SUCCESS

It looks like the insert of the readings was a success.

However, note that the processing of the readings returned these warning(s):


For meter pipe18: Warning parsing Reading #4. Reading value gives 96 with warning message:
There is a gap in time between this reading and the previous reading that exceeds the allowed amount of 60 seconds.
For reading #4 on meter pipe18 in pipeline: previous reading has value 72 start time 2021-06-03T00:00:00Z end time 2021-06-04T00:00:00Z and current reading has value 96 start time 2021-06-04T00:02:00Z end time 2021-06-05T00:00:00Z with timeSort increasing; duplications 1; cumulative false; cumulativeReset false; cumulativeResetStart 00:00:00; cumulativeResetEnd 23:59:59.999999; lengthGap 60; lengthVariation 121; onlyEndTime false
'] }, pipe19: { description: 'Ascending time readings with gap and just right size time gap', - chaiRequest: [CHAI_READINGS_REQUEST + ".field('meterName','pipe19').field('lengthGap','120').field('lengthVariation','121').field('createMeter',BooleanTypesJS.true).field('gzip',BooleanTypesJS.false)"], + chaiRequest: [CHAI_READINGS_REQUEST + ".field('meterIdentifier','pipe19').field('lengthGap','120').field('lengthVariation','121').field('gzip',false)"], fileName: ['pipe19Input.csv'], + createMeter: true, responseCode: [200], responseString: ['

SUCCESS

It looks like the insert of the readings was a success.

'] }, pipe20: { description: 'Cumulative time readings with header', - chaiRequest: [CHAI_READINGS_REQUEST + ".field('headerRow',BooleanTypesJS.true).field('cumulative',BooleanTypesJS.true).field('meterName','pipe20').field('createMeter',BooleanTypesJS.true).field('gzip',BooleanTypesJS.false)"], + chaiRequest: [CHAI_READINGS_REQUEST + ".field('headerRow',true).field('cumulative',true).field('meterIdentifier','pipe20').field('gzip',false)"], fileName: ['pipe20Input.csv'], + createMeter: true, responseCode: [400], responseString: ['

FAILURE

It looks like the insert of the readings had issues with some or all of the readings where the processing of the readings returned these warning(s)/error(s):


For meter pipe20: Error parsing Reading #1. Reading value gives 24 with error message:
The first ever reading must be dropped when dealing with cumulative data.
For reading #1 on meter pipe20 in pipeline: previous reading has value 0 start time 1970-01-01T00:00:00Z end time 1970-01-01T00:00:00Z and current reading has value 24 start time 2021-06-01T00:00:00Z end time 2021-06-02T00:00:00Z with timeSort increasing; duplications 1; cumulative true; cumulativeReset false; cumulativeResetStart 00:00:00; cumulativeResetEnd 23:59:59.999999; lengthGap 0; lengthVariation 0; onlyEndTime false

Readings Dropped and should have previous messages

  1. Dropped Reading #1 for meter pipe20
'] }, pipe21: { description: 'Cumulative time readings with duplication', - chaiRequest: [CHAI_READINGS_REQUEST + ".field('duplications','3').field('cumulative',BooleanTypesJS.true).field('meterName','pipe21').field('createMeter',BooleanTypesJS.true).field('gzip',BooleanTypesJS.false)"], + chaiRequest: [CHAI_READINGS_REQUEST + ".field('duplications','3').field('cumulative',true).field('meterIdentifier','pipe21').field('gzip',false)"], fileName: ['pipe21Input.csv'], + createMeter: true, responseCode: [400], responseString: ['

FAILURE

It looks like the insert of the readings had issues with some or all of the readings where the processing of the readings returned these warning(s)/error(s):


For meter pipe21: Error parsing Reading #1. Reading value gives 24 with error message:
The first ever reading must be dropped when dealing with cumulative data.
For reading #1 on meter pipe21 in pipeline: previous reading has value 0 start time 1970-01-01T00:00:00Z end time 1970-01-01T00:00:00Z and current reading has value 24 start time 2021-06-01T00:00:00Z end time 2021-06-02T00:00:00Z with timeSort increasing; duplications 3; cumulative true; cumulativeReset false; cumulativeResetStart 00:00:00; cumulativeResetEnd 23:59:59.999999; lengthGap 0; lengthVariation 0; onlyEndTime false

Readings Dropped and should have previous messages

  1. Dropped Reading #1 for meter pipe21
'] }, pipe22: { description: 'Cumulative time readings with default cumulative reset with negative reading which in incorrect', - chaiRequest: [CHAI_READINGS_REQUEST + ".field('cumulative',BooleanTypesJS.true).field('cumulativeReset',BooleanTypesJS.true).field('meterName','pipe22').field('createMeter',BooleanTypesJS.true).field('gzip',BooleanTypesJS.false)"], + chaiRequest: [CHAI_READINGS_REQUEST + ".field('cumulative',true).field('cumulativeReset',true).field('meterIdentifier','pipe22').field('gzip',false)"], fileName: ['pipe22Input.csv'], + createMeter: true, responseCode: [400], responseString: ['

FAILURE

It looks like the insert of the readings had issues with some or all of the readings where the processing of the readings returned these warning(s)/error(s):


For meter pipe22: Error parsing Reading #1. Reading value gives 24 with error message:
The first ever reading must be dropped when dealing with cumulative data.
For reading #1 on meter pipe22 in pipeline: previous reading has value 0 start time 1970-01-01T00:00:00Z end time 1970-01-01T00:00:00Z and current reading has value 24 start time 2021-06-01T00:00:00Z end time 2021-06-02T00:00:00Z with timeSort increasing; duplications 1; cumulative true; cumulativeReset true; cumulativeResetStart 00:00:00; cumulativeResetEnd 23:59:59.999999; lengthGap 0; lengthVariation 0; onlyEndTime false
For meter pipe22:
Error parsing Reading #4. Detected a negative value while handling cumulative readings so all reading are rejected.
For reading #4 on meter pipe22 in pipeline: previous reading has value 72 start time 2021-06-03T00:00:00Z end time 2021-06-04T00:00:00Z and current reading has value -145 start time 2021-06-04T00:00:00Z end time 2021-06-05T00:00:00Z with timeSort increasing; duplications 1; cumulative true; cumulativeReset true; cumulativeResetStart 00:00:00; cumulativeResetEnd 23:59:59.999999; lengthGap 0; lengthVariation 0; onlyEndTime false
'] }, pipe23: { description: 'Ascending time readings that are end only', - chaiRequest: [CHAI_READINGS_REQUEST + ".field('endOnly',BooleanTypesJS.true).field('meterName','pipe23').field('createMeter',BooleanTypesJS.true).field('gzip',BooleanTypesJS.false)"], + chaiRequest: [CHAI_READINGS_REQUEST + ".field('endOnly',true).field('meterIdentifier','pipe23').field('gzip',false)"], fileName: ['pipe23Input.csv'], + createMeter: true, responseCode: [400], responseString: ['

FAILURE

It looks like the insert of the readings had issues with some or all of the readings where the processing of the readings returned these warning(s)/error(s):


For meter pipe23: Error parsing Reading #1. Reading value gives 24 with error message:
The first ever reading must be dropped when dealing only with endTimestamps.
For reading #1 on meter pipe23 in pipeline: previous reading has value 0 start time 1970-01-01T00:00:00Z end time 1970-01-01T00:00:00Z and current reading has value 24 start time 1970-01-01T00:00:00Z end time 2021-06-02T00:00:00Z with timeSort increasing; duplications 1; cumulative false; cumulativeReset false; cumulativeResetStart 00:00:00; cumulativeResetEnd 23:59:59.999999; lengthGap 0; lengthVariation 0; onlyEndTime true

Readings Dropped and should have previous messages

  1. Dropped Reading #1 for meter pipe23
'] }, pipe24: { description: 'Descending time readings that are end only', - chaiRequest: [CHAI_READINGS_REQUEST + ".field('timeSort','decreasing').field('endOnly',BooleanTypesJS.true).field('meterName','pipe24').field('createMeter',BooleanTypesJS.true).field('gzip',BooleanTypesJS.false)"], + chaiRequest: [CHAI_READINGS_REQUEST + ".field('timeSort','decreasing').field('endOnly',true).field('meterIdentifier','pipe24').field('gzip',false)"], fileName: ['pipe24Input.csv'], + createMeter: true, responseCode: [400], responseString: ['

FAILURE

It looks like the insert of the readings had issues with some or all of the readings where the processing of the readings returned these warning(s)/error(s):


For meter pipe24: Error parsing Reading #1. Reading value gives 24 with error message:
The first ever reading must be dropped when dealing only with endTimestamps.
For reading #1 on meter pipe24 in pipeline: previous reading has value 0 start time 1970-01-01T00:00:00Z end time 1970-01-01T00:00:00Z and current reading has value 24 start time 1970-01-01T00:00:00Z end time 2021-06-02T00:00:00Z with timeSort decreasing; duplications 1; cumulative false; cumulativeReset false; cumulativeResetStart 00:00:00; cumulativeResetEnd 23:59:59.999999; lengthGap 0; lengthVariation 0; onlyEndTime true

Readings Dropped and should have previous messages

  1. Dropped Reading #1 for meter pipe24
'] }, pipe25: { description: 'Descending, cumulative time readings that are end only', - chaiRequest: [CHAI_READINGS_REQUEST + ".field('cumulative',BooleanTypesJS.true).field('timeSort','decreasing').field('endOnly',BooleanTypesJS.true).field('meterName','pipe25').field('createMeter',BooleanTypesJS.true).field('gzip',BooleanTypesJS.false)"], + chaiRequest: [CHAI_READINGS_REQUEST + ".field('cumulative',true).field('timeSort','decreasing').field('endOnly',true).field('meterIdentifier','pipe25').field('gzip',false)"], fileName: ['pipe25Input.csv'], + createMeter: true, responseCode: [400], responseString: ['

FAILURE

It looks like the insert of the readings had issues with some or all of the readings where the processing of the readings returned these warning(s)/error(s):


For meter pipe25: Error parsing Reading #1. Reading value gives 24 with error message:
The first ever reading must be dropped when dealing with cumulative data.
The first ever reading must be dropped when dealing only with endTimestamps.
For reading #1 on meter pipe25 in pipeline: previous reading has value 0 start time 1970-01-01T00:00:00Z end time 1970-01-01T00:00:00Z and current reading has value 24 start time 1970-01-01T00:00:00Z end time 2021-06-02T00:00:00Z with timeSort decreasing; duplications 1; cumulative true; cumulativeReset false; cumulativeResetStart 00:00:00; cumulativeResetEnd 23:59:59.999999; lengthGap 0; lengthVariation 0; onlyEndTime true

Readings Dropped and should have previous messages

  1. Dropped Reading #1 for meter pipe25
'] }, pipe26: { description: 'Ascending time readings with bad start date/time which is incorrect', - chaiRequest: [CHAI_READINGS_REQUEST + ".field('meterName','pipe26').field('createMeter',BooleanTypesJS.true).field('gzip',BooleanTypesJS.false)"], + chaiRequest: [CHAI_READINGS_REQUEST + ".field('meterIdentifier','pipe26').field('gzip',false)"], fileName: ['pipe26Input.csv'], + createMeter: true, responseCode: [400], responseString: ['

FAILURE

It looks like the insert of the readings had issues with some or all of the readings where the processing of the readings returned these warning(s)/error(s):

For meter pipe26: Error parsing Reading #2 The start (2021-06-02 00:00:00 x) and/or end time (2021-06-03 00:00:00) provided did not parse into a valid date/time so all reading are rejected.
For reading #2 on meter pipe26 in pipeline: previous reading has value 24 start time 2021-06-01T00:00:00Z end time 2021-06-02T00:00:00Z and current reading has value unknown start time Invalid date end time 2021-06-03T00:00:00Z with timeSort increasing; duplications 1; cumulative false; cumulativeReset false; cumulativeResetStart 00:00:00; cumulativeResetEnd 23:59:59.999999; lengthGap 0; lengthVariation 0; onlyEndTime false
'] }, pipe27: { description: 'Ascending time readings with bad end date/time which is incorrect', - chaiRequest: [CHAI_READINGS_REQUEST + ".field('meterName','pipe27').field('createMeter',BooleanTypesJS.true).field('gzip',BooleanTypesJS.false)"], + chaiRequest: [CHAI_READINGS_REQUEST + ".field('meterIdentifier','pipe27').field('gzip',false)"], + createMeter: true, fileName: ['pipe27Input.csv'], responseCode: [400], responseString: ['

FAILURE

It looks like the insert of the readings had issues with some or all of the readings where the processing of the readings returned these warning(s)/error(s):

For meter pipe27: Error parsing Reading #2 The start (2021-06-02 00:00:00) and/or end time (2021-06-32 00:00:00) provided did not parse into a valid date/time so all reading are rejected.
For reading #2 on meter pipe27 in pipeline: previous reading has value 24 start time 2021-06-01T00:00:00Z end time 2021-06-02T00:00:00Z and current reading has value unknown start time 2021-06-02T00:00:00Z end time Invalid date with timeSort increasing; duplications 1; cumulative false; cumulativeReset false; cumulativeResetStart 00:00:00; cumulativeResetEnd 23:59:59.999999; lengthGap 0; lengthVariation 0; onlyEndTime false
'] @@ -232,255 +258,268 @@ const testCases = { // Pipe28 removed since stronger tests on dates cause it to fail. pipe29: { description: 'Cumulative time readings with bad reading value which in incorrect', - chaiRequest: [CHAI_READINGS_REQUEST + ".field('cumulative',BooleanTypesJS.true).field('meterName','pipe29').field('createMeter',BooleanTypesJS.true).field('gzip',BooleanTypesJS.false)"], + chaiRequest: [CHAI_READINGS_REQUEST + ".field('cumulative',true).field('meterIdentifier','pipe29').field('gzip',false)"], fileName: ['pipe29Input.csv'], + createMeter: true, responseCode: [400], responseString: ['

FAILURE

It looks like the insert of the readings had issues with some or all of the readings where the processing of the readings returned these warning(s)/error(s):


For meter pipe29: Error parsing Reading #1. Reading value gives 24 with error message:
The first ever reading must be dropped when dealing with cumulative data.
For reading #1 on meter pipe29 in pipeline: previous reading has value 0 start time 1970-01-01T00:00:00Z end time 1970-01-01T00:00:00Z and current reading has value 24 start time 2021-06-01T00:00:00Z end time 2021-06-02T00:00:00Z with timeSort increasing; duplications 1; cumulative true; cumulativeReset false; cumulativeResetStart 00:00:00; cumulativeResetEnd 23:59:59.999999; lengthGap 0; lengthVariation 0; onlyEndTime false
For meter pipe29: Error parsing Reading #4 with cumulative data. The reading value provided of 240.x is not considered a number so all reading are rejected.
'] }, pipe30: { description: 'Ascending time readings with bad reading value which in incorrect', - chaiRequest: [CHAI_READINGS_REQUEST + ".field('meterName','pipe30').field('createMeter',BooleanTypesJS.true).field('gzip',BooleanTypesJS.false)"], + chaiRequest: [CHAI_READINGS_REQUEST + ".field('meterIdentifier','pipe30').field('gzip',false)"], fileName: ['pipe30Input.csv'], + createMeter: true, responseCode: [400], responseString: ['

FAILURE

It looks like the insert of the readings had issues with some or all of the readings where the processing of the readings returned these warning(s)/error(s):

For meter pipe30: Error parsing Reading #4 The reading value provided of 9a is not considered a number so all reading are rejected.
'] }, pipe31: { description: 'Cumulative time readings with gaps with length variation but still needs to drop 2 readings with gap', - chaiRequest: [CHAI_READINGS_REQUEST + ".field('meterName','pipe31').field('cumulative',BooleanTypesJS.true).field('lengthVariation','121').field('createMeter',BooleanTypesJS.true).field('gzip',BooleanTypesJS.false)"], + chaiRequest: [CHAI_READINGS_REQUEST + ".field('meterIdentifier','pipe31').field('cumulative',true).field('lengthVariation','121').field('gzip',false)"], fileName: ['pipe31Input.csv'], + createMeter: true, responseCode: [400], responseString: ['

FAILURE

It looks like the insert of the readings had issues with some or all of the readings where the processing of the readings returned these warning(s)/error(s):


For meter pipe31: Error parsing Reading #1. Reading value gives 24 with error message:
The first ever reading must be dropped when dealing with cumulative data.
For reading #1 on meter pipe31 in pipeline: previous reading has value 0 start time 1970-01-01T00:00:00Z end time 1970-01-01T00:00:00Z and current reading has value 24 start time 2021-06-01T00:00:00Z end time 2021-06-02T00:00:00Z with timeSort increasing; duplications 1; cumulative true; cumulativeReset false; cumulativeResetStart 00:00:00; cumulativeResetEnd 23:59:59.999999; lengthGap 0; lengthVariation 121; onlyEndTime false

For meter pipe31: Error parsing Reading #2. Reading value gives 48 with error message:
The end of the previous reading is too far from the start of the next readings in cumulative data so drop this reading.
For reading #2 on meter pipe31 in pipeline: previous reading has value 24 start time 2021-06-01T00:00:00Z end time 2021-06-02T00:00:00Z and current reading has value 48 start time 2021-06-02T00:01:00Z end time 2021-06-03T00:00:00Z with timeSort increasing; duplications 1; cumulative true; cumulativeReset false; cumulativeResetStart 00:00:00; cumulativeResetEnd 23:59:59.999999; lengthGap 0; lengthVariation 121; onlyEndTime false

For meter pipe31: Error parsing Reading #4. Reading value gives 96 with error message:
The end of the previous reading is too far from the start of the next readings in cumulative data so drop this reading.
For reading #4 on meter pipe31 in pipeline: previous reading has value 72 start time 2021-06-03T00:00:00Z end time 2021-06-04T00:00:00Z and current reading has value 96 start time 2021-06-04T00:02:00Z end time 2021-06-05T00:00:00Z with timeSort increasing; duplications 1; cumulative true; cumulativeReset false; cumulativeResetStart 00:00:00; cumulativeResetEnd 23:59:59.999999; lengthGap 0; lengthVariation 121; onlyEndTime false

Readings Dropped and should have previous messages

  1. Dropped Reading #1 for meter pipe31
  2. Dropped Reading #2 for meter pipe31
  3. Dropped Reading #4 for meter pipe31
'] }, pipe32: { description: 'Cumulative time readings with one reading start before end of previous so dropped', - chaiRequest: [CHAI_READINGS_REQUEST + ".field('meterName','pipe32').field('cumulative',BooleanTypesJS.true).field('createMeter',BooleanTypesJS.true).field('gzip',BooleanTypesJS.false)"], + chaiRequest: [CHAI_READINGS_REQUEST + ".field('meterIdentifier','pipe32').field('cumulative',true).field('gzip',false)"], fileName: ['pipe32Input.csv'], + createMeter: true, responseCode: [400], responseString: ['

FAILURE

It looks like the insert of the readings had issues with some or all of the readings where the processing of the readings returned these warning(s)/error(s):


For meter pipe32: Error parsing Reading #1. Reading value gives 24 with error message:
The first ever reading must be dropped when dealing with cumulative data.
For reading #1 on meter pipe32 in pipeline: previous reading has value 0 start time 1970-01-01T00:00:00Z end time 1970-01-01T00:00:00Z and current reading has value 24 start time 2021-06-01T00:00:00Z end time 2021-06-02T00:00:00Z with timeSort increasing; duplications 1; cumulative true; cumulativeReset false; cumulativeResetStart 00:00:00; cumulativeResetEnd 23:59:59.999999; lengthGap 0; lengthVariation 0; onlyEndTime false

For meter pipe32: Error parsing Reading #2. Reading value gives 48 with error message:
The reading start time is before the previous end time and the data is cumulative so OED cannot use this reading.
For reading #2 on meter pipe32 in pipeline: previous reading has value 24 start time 2021-06-01T00:00:00Z end time 2021-06-02T00:00:00Z and current reading has value 48 start time 2021-06-01T23:59:59Z end time 2021-06-03T00:00:00Z with timeSort increasing; duplications 1; cumulative true; cumulativeReset false; cumulativeResetStart 00:00:00; cumulativeResetEnd 23:59:59.999999; lengthGap 0; lengthVariation 0; onlyEndTime false

For meter pipe32: Warning parsing Reading #3. Reading value gives 72 with warning message:
The previous reading has a different time length than the current reading and exceeds the tolerance of 0 seconds. Note this is treated only as a warning since this may be expected for certain meters.
For reading #3 on meter pipe32 in pipeline: previous reading has value 48 start time 2021-06-01T23:59:59Z end time 2021-06-03T00:00:00Z and current reading has value 72 start time 2021-06-03T00:00:00Z end time 2021-06-04T00:00:00Z with timeSort increasing; duplications 1; cumulative true; cumulativeReset false; cumulativeResetStart 00:00:00; cumulativeResetEnd 23:59:59.999999; lengthGap 0; lengthVariation 0; onlyEndTime false

Readings Dropped and should have previous messages

  1. Dropped Reading #1 for meter pipe32
  2. Dropped Reading #2 for meter pipe32
'] }, pipe33: { description: 'Cumulative time readings with negative reading which is incorrect', - chaiRequest: [CHAI_READINGS_REQUEST + ".field('meterName','pipe33').field('cumulative',BooleanTypesJS.true).field('createMeter',BooleanTypesJS.true).field('gzip',BooleanTypesJS.false)"], + chaiRequest: [CHAI_READINGS_REQUEST + ".field('meterIdentifier','pipe33').field('cumulative',true).field('gzip',false)"], fileName: ['pipe33Input.csv'], + createMeter: true, responseCode: [400], responseString: ['

FAILURE

It looks like the insert of the readings had issues with some or all of the readings where the processing of the readings returned these warning(s)/error(s):


For meter pipe33: Error parsing Reading #1. Reading value gives 24 with error message:
The first ever reading must be dropped when dealing with cumulative data.
For reading #1 on meter pipe33 in pipeline: previous reading has value 0 start time 1970-01-01T00:00:00Z end time 1970-01-01T00:00:00Z and current reading has value 24 start time 2021-06-01T00:00:00Z end time 2021-06-02T00:00:00Z with timeSort increasing; duplications 1; cumulative true; cumulativeReset false; cumulativeResetStart 00:00:00; cumulativeResetEnd 23:59:59.999999; lengthGap 0; lengthVariation 0; onlyEndTime false
For meter pipe33:
Error parsing Reading #3. Detected a negative value while handling cumulative readings so all reading are rejected.
For reading #3 on meter pipe33 in pipeline: previous reading has value 48 start time 2021-06-02T00:00:00Z end time 2021-06-03T00:00:00Z and current reading has value -73 start time 2021-06-03T00:00:00Z end time 2021-06-04T00:00:00Z with timeSort increasing; duplications 1; cumulative true; cumulativeReset false; cumulativeResetStart 00:00:00; cumulativeResetEnd 23:59:59.999999; lengthGap 0; lengthVariation 0; onlyEndTime false
'] }, pipe34: { description: 'Ascending time readings with one reading start/end the same so dropped', - chaiRequest: [CHAI_READINGS_REQUEST + ".field('meterName','pipe34').field('createMeter',BooleanTypesJS.true).field('gzip',BooleanTypesJS.false)"], + chaiRequest: [CHAI_READINGS_REQUEST + ".field('meterIdentifier','pipe34').field('gzip',false)"], fileName: ['pipe34Input.csv'], + createMeter: true, responseCode: [400], responseString: ['

FAILURE

It looks like the insert of the readings had issues with some or all of the readings where the processing of the readings returned these warning(s)/error(s):


For meter pipe34: Error parsing Reading #2. Reading value gives 48 with error message:
The reading end time is not after the start time so we must drop the reading.
For reading #2 on meter pipe34 in pipeline: previous reading has value 24 start time 2021-06-01T00:00:00Z end time 2021-06-02T00:00:00Z and current reading has value 48 start time 2021-06-02T00:00:00Z end time 2021-06-02T00:00:00Z with timeSort increasing; duplications 1; cumulative false; cumulativeReset false; cumulativeResetStart 00:00:00; cumulativeResetEnd 23:59:59.999999; lengthGap 0; lengthVariation 0; onlyEndTime false

For meter pipe34: Warning parsing Reading #3. Reading value gives 72 with warning message:
There is a gap in time between this reading and the previous reading that exceeds the allowed amount of 0 seconds.
The previous reading has a different time length than the current reading and exceeds the tolerance of 0 seconds. Note this is treated only as a warning since this may be expected for certain meters.
For reading #3 on meter pipe34 in pipeline: previous reading has value 48 start time 2021-06-02T00:00:00Z end time 2021-06-02T00:00:00Z and current reading has value 72 start time 2021-06-03T00:00:00Z end time 2021-06-04T00:00:00Z with timeSort increasing; duplications 1; cumulative false; cumulativeReset false; cumulativeResetStart 00:00:00; cumulativeResetEnd 23:59:59.999999; lengthGap 0; lengthVariation 0; onlyEndTime false

Readings Dropped and should have previous messages

  1. Dropped Reading #2 for meter pipe34
'] }, pipe35: { description: 'Ascending time readings that are end only with two readings time the same so dropped', - chaiRequest: [CHAI_READINGS_REQUEST + ".field('refreshReadings',BooleanTypesJS.true).field('meterName','pipe35').field('endOnly',BooleanTypesJS.true).field('createMeter',BooleanTypesJS.true).field('gzip',BooleanTypesJS.false)"], + chaiRequest: [CHAI_READINGS_REQUEST + ".field('refreshReadings',true).field('meterIdentifier','pipe35').field('endOnly',true).field('gzip',false)"], fileName: ['pipe35Input.csv'], + createMeter: true, responseCode: [400], responseString: ['

FAILURE

It looks like the insert of the readings had issues with some or all of the readings where the processing of the readings returned these warning(s)/error(s):


For meter pipe35: Error parsing Reading #1. Reading value gives 24 with error message:
The first ever reading must be dropped when dealing only with endTimestamps.
For reading #1 on meter pipe35 in pipeline: previous reading has value 0 start time 1970-01-01T00:00:00Z end time 1970-01-01T00:00:00Z and current reading has value 24 start time 1970-01-01T00:00:00Z end time 2021-06-02T00:00:00Z with timeSort increasing; duplications 1; cumulative false; cumulativeReset false; cumulativeResetStart 00:00:00; cumulativeResetEnd 23:59:59.999999; lengthGap 0; lengthVariation 0; onlyEndTime true

For meter pipe35: Error parsing Reading #2. Reading value gives 48 with error message:
The reading end time is not after the start time so we must drop the reading. The start time came from the previous readings end time.
For reading #2 on meter pipe35 in pipeline: previous reading has value 24 start time 1970-01-01T00:00:00Z end time 2021-06-02T00:00:00Z and current reading has value 48 start time 2021-06-02T00:00:00Z end time 2021-06-02T00:00:00Z with timeSort increasing; duplications 1; cumulative false; cumulativeReset false; cumulativeResetStart 00:00:00; cumulativeResetEnd 23:59:59.999999; lengthGap 0; lengthVariation 0; onlyEndTime true

For meter pipe35: Warning parsing Reading #3. Reading value gives 72 with warning message:
The previous reading has a different time length than the current reading and exceeds the tolerance of 0 seconds. Note this is treated only as a warning since this may be expected for certain meters.
For reading #3 on meter pipe35 in pipeline: previous reading has value 48 start time 2021-06-02T00:00:00Z end time 2021-06-02T00:00:00Z and current reading has value 72 start time 2021-06-02T00:00:00Z end time 2021-06-04T00:00:00Z with timeSort increasing; duplications 1; cumulative false; cumulativeReset false; cumulativeResetStart 00:00:00; cumulativeResetEnd 23:59:59.999999; lengthGap 0; lengthVariation 0; onlyEndTime true

For meter pipe35: Warning parsing Reading #4. Reading value gives 96 with warning message:
The previous reading has a different time length than the current reading and exceeds the tolerance of 0 seconds. Note this is treated only as a warning since this may be expected for certain meters.
For reading #4 on meter pipe35 in pipeline: previous reading has value 72 start time 2021-06-02T00:00:00Z end time 2021-06-04T00:00:00Z and current reading has value 96 start time 2021-06-04T00:00:00Z end time 2021-06-05T00:00:00Z with timeSort increasing; duplications 1; cumulative false; cumulativeReset false; cumulativeResetStart 00:00:00; cumulativeResetEnd 23:59:59.999999; lengthGap 0; lengthVariation 0; onlyEndTime true

Readings Dropped and should have previous messages

  1. Dropped Reading #1 for meter pipe35
  2. Dropped Reading #2 for meter pipe35
'] }, pipe40: { description: 'Cumulative time zipped readings with header', - chaiRequest: [CHAI_READINGS_REQUEST + ".field('headerRow',BooleanTypesJS.true).field('cumulative',BooleanTypesJS.true).field('createMeter',BooleanTypesJS.true).field('meterName', 'pipe40')"], + chaiRequest: [CHAI_READINGS_REQUEST + ".field('headerRow',true).field('cumulative',true).field('meterIdentifier', 'pipe40')"], fileName: ['pipe40Input.csv.gz'], + createMeter: true, responseCode: [400], responseString: ['

FAILURE

It looks like the insert of the readings had issues with some or all of the readings where the processing of the readings returned these warning(s)/error(s):


For meter pipe40: Error parsing Reading #1. Reading value gives 24 with error message:
The first ever reading must be dropped when dealing with cumulative data.
For reading #1 on meter pipe40 in pipeline: previous reading has value 0 start time 1970-01-01T00:00:00Z end time 1970-01-01T00:00:00Z and current reading has value 24 start time 2021-06-01T00:00:00Z end time 2021-06-02T00:00:00Z with timeSort increasing; duplications 1; cumulative true; cumulativeReset false; cumulativeResetStart 00:00:00; cumulativeResetEnd 23:59:59.999999; lengthGap 0; lengthVariation 0; onlyEndTime false

Readings Dropped and should have previous messages

  1. Dropped Reading #1 for meter pipe40
'] }, pipe50: { description: 'Ascending time readings with two readings uploads where update readings', - chaiRequest: [CHAI_READINGS_REQUEST + ".field('gzip', BooleanTypesJS.false).field('createMeter',BooleanTypesJS.true).field('meterName', 'pipe50')", CHAI_READINGS_REQUEST + ".field('gzip', BooleanTypesJS.false).field('update',BooleanTypesJS.true).field('meterName', 'pipe50')"], + chaiRequest: [CHAI_READINGS_REQUEST + ".field('gzip', false).field('meterIdentifier', 'pipe50')", CHAI_READINGS_REQUEST + ".field('gzip', false).field('update',true).field('meterIdentifier', 'pipe50')"], fileName: ['pipe50AInput.csv', 'pipe50BInput.csv'], + createMeter: true, responseCode: [200, 200], responseString: ['

SUCCESS

It looks like the insert of the readings was a success.

', '

SUCCESS

It looks like the insert of the readings was a success.

However, note that the processing of the readings returned these warning(s):


For meter pipe50: Warning parsing Reading #1. Reading value gives 0 with warning message:
The current reading startTime is not after the previous reading\'s end time. Note this is treated only as a warning since readings may be sent out of order.
There is a gap in time between this reading and the previous reading that exceeds the allowed amount of 0 seconds.
For reading #1 on meter pipe50 in pipeline: previous reading has value 120 start time 2021-06-05T00:00:00Z end time 2021-06-06T00:00:00Z and current reading has value 0 start time 2021-05-31T00:00:00Z end time 2021-06-01T00:00:00Z with timeSort increasing; duplications 1; cumulative false; cumulativeReset false; cumulativeResetStart 00:00:00; cumulativeResetEnd 23:59:59.999999; lengthGap 0; lengthVariation 0; onlyEndTime false
'] }, pipe51: { description: 'Ascending time readings with two readings uploads without update so no changes', - chaiRequest: [CHAI_READINGS_REQUEST + ".field('meterName','pipe51').field('createMeter',BooleanTypesJS.true).field('gzip',BooleanTypesJS.false)", CHAI_READINGS_REQUEST + ".field('meterName','pipe51').field('gzip',BooleanTypesJS.false)"], + chaiRequest: [CHAI_READINGS_REQUEST + ".field('meterIdentifier','pipe51').field('gzip',false)", CHAI_READINGS_REQUEST + ".field('meterIdentifier','pipe51').field('gzip',false)"], fileName: ['pipe51AInput.csv', 'pipe51BInput.csv'], + createMeter: true, responseCode: [200, 200], responseString: ['

SUCCESS

It looks like the insert of the readings was a success.

', '

SUCCESS

It looks like the insert of the readings was a success.

However, note that the processing of the readings returned these warning(s):


For meter pipe51: Warning parsing Reading #1. Reading value gives 0 with warning message:
The current reading startTime is not after the previous reading\'s end time. Note this is treated only as a warning since readings may be sent out of order.
There is a gap in time between this reading and the previous reading that exceeds the allowed amount of 0 seconds.
For reading #1 on meter pipe51 in pipeline: previous reading has value 120 start time 2021-06-05T00:00:00Z end time 2021-06-06T00:00:00Z and current reading has value 0 start time 2021-05-31T00:00:00Z end time 2021-06-01T00:00:00Z with timeSort increasing; duplications 1; cumulative false; cumulativeReset false; cumulativeResetStart 00:00:00; cumulativeResetEnd 23:59:59.999999; lengthGap 0; lengthVariation 0; onlyEndTime false
'] }, pipe52: { description: 'Cumulative time readings with default reset with two uploads to add more readings with a reset', - chaiRequest: [CHAI_READINGS_REQUEST + ".field('cumulative',BooleanTypesJS.true).field('meterName','pipe52').field('createMeter',BooleanTypesJS.true).field('gzip',BooleanTypesJS.false)", CHAI_READINGS_REQUEST + ".field('cumulative',BooleanTypesJS.true).field('cumulativeReset',BooleanTypesJS.true).field('meterName','pipe52').field('gzip',BooleanTypesJS.false)"], + chaiRequest: [CHAI_READINGS_REQUEST + ".field('cumulative',true).field('meterIdentifier','pipe52').field('gzip',false)", CHAI_READINGS_REQUEST + ".field('cumulative',true).field('cumulativeReset',true).field('meterIdentifier','pipe52').field('gzip',false)"], fileName: ['pipe52AInput.csv', 'pipe52BInput.csv'], + createMeter: true, responseCode: [400, 200], responseString: ['

FAILURE

It looks like the insert of the readings had issues with some or all of the readings where the processing of the readings returned these warning(s)/error(s):


For meter pipe52: Error parsing Reading #1. Reading value gives 24 with error message:
The first ever reading must be dropped when dealing with cumulative data.
For reading #1 on meter pipe52 in pipeline: previous reading has value 0 start time 1970-01-01T00:00:00Z end time 1970-01-01T00:00:00Z and current reading has value 24 start time 2021-06-01T00:00:00Z end time 2021-06-02T00:00:00Z with timeSort increasing; duplications 1; cumulative true; cumulativeReset false; cumulativeResetStart 00:00:00; cumulativeResetEnd 23:59:59.999999; lengthGap 0; lengthVariation 0; onlyEndTime false

Readings Dropped and should have previous messages

  1. Dropped Reading #1 for meter pipe52
', '

SUCCESS

It looks like the insert of the readings was a success.

'] }, pipe60: { description: 'Cumulative time readings with three readings uploads', - chaiRequest: [CHAI_READINGS_REQUEST + ".field('gzip', BooleanTypesJS.false).field('createMeter',BooleanTypesJS.true).field('meterName', 'pipe60').field('cumulative', BooleanTypesJS.true)", CHAI_READINGS_REQUEST + ".field('gzip', BooleanTypesJS.false).field('meterName', 'pipe60').field('cumulative', BooleanTypesJS.true)", CHAI_READINGS_REQUEST + ".field('gzip', BooleanTypesJS.false).field('meterName', 'pipe60').field('cumulative', BooleanTypesJS.true)"], + chaiRequest: [CHAI_READINGS_REQUEST + ".field('gzip', false).field('meterIdentifier', 'pipe60').field('cumulative', true)", CHAI_READINGS_REQUEST + ".field('gzip', false).field('meterIdentifier', 'pipe60').field('cumulative', true)", CHAI_READINGS_REQUEST + ".field('gzip', false).field('meterIdentifier', 'pipe60').field('cumulative', true)"], fileName: ['pipe60AInput.csv', 'pipe60BInput.csv', 'pipe60CInput.csv'], + createMeter: true, responseCode: [400, 200, 200], responseString: ['

FAILURE

It looks like the insert of the readings had issues with some or all of the readings where the processing of the readings returned these warning(s)/error(s):


For meter pipe60: Error parsing Reading #1. Reading value gives 24 with error message:
The first ever reading must be dropped when dealing with cumulative data.
For reading #1 on meter pipe60 in pipeline: previous reading has value 0 start time 1970-01-01T00:00:00Z end time 1970-01-01T00:00:00Z and current reading has value 24 start time 2021-06-01T00:00:00Z end time 2021-06-02T00:00:00Z with timeSort increasing; duplications 1; cumulative true; cumulativeReset false; cumulativeResetStart 00:00:00; cumulativeResetEnd 23:59:59.999999; lengthGap 0; lengthVariation 0; onlyEndTime false

Readings Dropped and should have previous messages

  1. Dropped Reading #1 for meter pipe60
', '

SUCCESS

It looks like the insert of the readings was a success.

', '

SUCCESS

It looks like the insert of the readings was a success.

'] }, pipe70: { description: 'Create meter cumulative reset around noon with reading upload with reset at midnight which is incorrect', - chaiRequest: [CHAI_METERS_REQUEST + ".field('headerRow',BooleanTypesJS.true).field('gzip',BooleanTypesJS.false)", CHAI_READINGS_REQUEST + ".field('meterName','pipe70').field('gzip',BooleanTypesJS.false)"], + chaiRequest: [CHAI_METERS_REQUEST + ".field('headerRow',true).field('gzip',false)", CHAI_READINGS_REQUEST + ".field('meterIdentifier','pipe70').field('gzip',false)"], fileName: ['pipe70AInputMeter.csv', 'pipe70BInput.csv'], responseCode: [200, 400], responseString: ['

SUCCESS

Successfully inserted the meters.', '

FAILURE

It looks like the insert of the readings had issues with some or all of the readings where the processing of the readings returned these warning(s)/error(s):


For meter pipe70: Error parsing Reading #1. Reading value gives 24 with error message:
The first ever reading must be dropped when dealing with cumulative data.
For reading #1 on meter pipe70 in pipeline: previous reading has value 0 start time 1970-01-01T00:00:00Z end time 1970-01-01T00:00:00Z and current reading has value 24 start time 2021-06-01T00:00:00Z end time 2021-06-02T00:00:00Z with timeSort increasing; duplications 1; cumulative true; cumulativeReset true; cumulativeResetStart 11:45:00; cumulativeResetEnd 12:15:00; lengthGap 0; lengthVariation 0; onlyEndTime false

For meter pipe70: Error parsing Reading #4. Reading value of 96 gives -48 with error message:
A negative meterReading has been detected but either cumulativeReset is not enabled, or the start time and end time of this reading is out of the reset range. Reject all readings.
For reading #4 on meter pipe70 in pipeline: previous reading has value 72 start time 2021-06-03T00:00:00Z end time 2021-06-04T00:00:00Z and current reading has value -48 start time 2021-06-04T00:00:00Z end time 2021-06-05T00:00:00Z with timeSort increasing; duplications 1; cumulative true; cumulativeReset true; cumulativeResetStart 11:45:00; cumulativeResetEnd 12:15:00; lengthGap 0; lengthVariation 0; onlyEndTime false
'] }, pipe71: { description: 'Create meter cumulative reset around midnight with reading upload with reset at midnight', - chaiRequest: [CHAI_METERS_REQUEST + ".field('headerRow',BooleanTypesJS.true).field('gzip',BooleanTypesJS.false)", CHAI_READINGS_REQUEST + ".field('cumulative',BooleanTypesJS.true).field('cumulativeReset',BooleanTypesJS.true).field('cumulativeResetStart','23:45').field('cumulativeResetEnd','00:15').field('meterName','pipe71').field('gzip',BooleanTypesJS.false)"], + chaiRequest: [CHAI_METERS_REQUEST + ".field('headerRow',true).field('gzip',false)", CHAI_READINGS_REQUEST + ".field('cumulative',true).field('cumulativeReset',true).field('cumulativeResetStart','23:45').field('cumulativeResetEnd','00:15').field('meterIdentifier','pipe71').field('gzip',false)"], fileName: ['pipe71AInputMeter.csv', 'pipe71BInput.csv'], responseCode: [200, 400], responseString: ['

SUCCESS

Successfully inserted the meters.', '

FAILURE

It looks like the insert of the readings had issues with some or all of the readings where the processing of the readings returned these warning(s)/error(s):


For meter pipe71: Error parsing Reading #1. Reading value gives 24 with error message:
The first ever reading must be dropped when dealing with cumulative data.
For reading #1 on meter pipe71 in pipeline: previous reading has value 0 start time 1970-01-01T00:00:00Z end time 1970-01-01T00:00:00Z and current reading has value 24 start time 2021-06-01T00:00:00Z end time 2021-06-02T00:00:00Z with timeSort increasing; duplications 1; cumulative true; cumulativeReset true; cumulativeResetStart 23:45; cumulativeResetEnd 00:15; lengthGap 0; lengthVariation 0; onlyEndTime false

Readings Dropped and should have previous messages

  1. Dropped Reading #1 for meter pipe71
'] }, pipe72: { description: 'Create meter with modest length variation and gap with reading upload where warn on larger gaps', - chaiRequest: [CHAI_METERS_REQUEST + ".field('headerRow',BooleanTypesJS.true).field('gzip',BooleanTypesJS.false)", CHAI_READINGS_REQUEST + ".field('meterName','pipe72').field('gzip',BooleanTypesJS.false)"], + chaiRequest: [CHAI_METERS_REQUEST + ".field('headerRow',true).field('gzip',false)", CHAI_READINGS_REQUEST + ".field('meterIdentifier','pipe72').field('gzip',false)"], fileName: ['pipe72AInputMeter.csv', 'pipe72BInput.csv'], responseCode: [200, 200], responseString: ['

SUCCESS

Successfully inserted the meters.', '

SUCCESS

It looks like the insert of the readings was a success.

However, note that the processing of the readings returned these warning(s):


For meter pipe72: Warning parsing Reading #4. Reading value gives 96 with warning message:
There is a gap in time between this reading and the previous reading that exceeds the allowed amount of 60 seconds.
For reading #4 on meter pipe72 in pipeline: previous reading has value 72 start time 2021-06-03T00:00:00Z end time 2021-06-04T00:00:00Z and current reading has value 96 start time 2021-06-04T00:02:00Z end time 2021-06-05T00:00:00Z with timeSort increasing; duplications 1; cumulative false; cumulativeReset false; cumulativeResetStart 00:00:00; cumulativeResetEnd 23:59:59.999999; lengthGap 60; lengthVariation 120; onlyEndTime false

For meter pipe72: Warning parsing Reading #5. Reading value gives 120 with warning message:
The previous reading has a different time length than the current reading and exceeds the tolerance of 120 seconds. Note this is treated only as a warning since this may be expected for certain meters.
For reading #5 on meter pipe72 in pipeline: previous reading has value 96 start time 2021-06-04T00:02:00Z end time 2021-06-05T00:00:00Z and current reading has value 120 start time 2021-06-05T00:00:00Z end time 2021-06-06T00:04:00Z with timeSort increasing; duplications 1; cumulative false; cumulativeReset false; cumulativeResetStart 00:00:00; cumulativeResetEnd 23:59:59.999999; lengthGap 60; lengthVariation 120; onlyEndTime false
'] }, pipe73: { description: 'Create meter with modest length variation and gap with reading upload that changes these values so fewer warnings; also checks floating point values in reading upload', - chaiRequest: [CHAI_METERS_REQUEST + ".field('headerRow',BooleanTypesJS.true).field('gzip',BooleanTypesJS.false)", CHAI_READINGS_REQUEST + ".field('meterName','pipe73').field('lengthGap','120.1').field('lengthVariation','120.2').field('gzip',BooleanTypesJS.false)"], + chaiRequest: [CHAI_METERS_REQUEST + ".field('headerRow',true).field('gzip',false)", CHAI_READINGS_REQUEST + ".field('meterIdentifier','pipe73').field('lengthGap','120.1').field('lengthVariation','120.2').field('gzip',false)"], fileName: ['pipe73AInputMeter.csv', 'pipe73BInput.csv'], responseCode: [200, 200], responseString: ['

SUCCESS

Successfully inserted the meters.', '

SUCCESS

It looks like the insert of the readings was a success.

However, note that the processing of the readings returned these warning(s):


For meter pipe73: Warning parsing Reading #5. Reading value gives 120 with warning message:
The previous reading has a different time length than the current reading and exceeds the tolerance of 120.2 seconds. Note this is treated only as a warning since this may be expected for certain meters.
For reading #5 on meter pipe73 in pipeline: previous reading has value 96 start time 2021-06-04T00:02:00Z end time 2021-06-05T00:00:00Z and current reading has value 120 start time 2021-06-05T00:00:00Z end time 2021-06-06T00:04:00Z with timeSort increasing; duplications 1; cumulative false; cumulativeReset false; cumulativeResetStart 00:00:00; cumulativeResetEnd 23:59:59.999999; lengthGap 120.1; lengthVariation 120.2; onlyEndTime false
'] }, pipe74: { description: 'Create meter with duplication with reading upload', - chaiRequest: [CHAI_METERS_REQUEST + ".field('headerRow',BooleanTypesJS.true).field('gzip',BooleanTypesJS.false)", CHAI_READINGS_REQUEST + ".field('cumulative',BooleanTypesJS.true).field('meterName','pipe74').field('gzip',BooleanTypesJS.false)"], + chaiRequest: [CHAI_METERS_REQUEST + ".field('headerRow',true).field('gzip',false)", CHAI_READINGS_REQUEST + ".field('cumulative',true).field('meterIdentifier','pipe74').field('gzip',false)"], fileName: ['pipe74AInputMeter.csv', 'pipe74BInput.csv'], responseCode: [200, 400], responseString: ['

SUCCESS

Successfully inserted the meters.', '

FAILURE

It looks like the insert of the readings had issues with some or all of the readings where the processing of the readings returned these warning(s)/error(s):


For meter pipe74: Error parsing Reading #1. Reading value gives 24 with error message:
The first ever reading must be dropped when dealing with cumulative data.
For reading #1 on meter pipe74 in pipeline: previous reading has value 0 start time 1970-01-01T00:00:00Z end time 1970-01-01T00:00:00Z and current reading has value 24 start time 2021-06-01T00:00:00Z end time 2021-06-02T00:00:00Z with timeSort increasing; duplications 3; cumulative true; cumulativeReset false; cumulativeResetStart 00:00:00; cumulativeResetEnd 23:59:59.999999; lengthGap 0; lengthVariation 0; onlyEndTime false

Readings Dropped and should have previous messages

  1. Dropped Reading #1 for meter pipe74
'] }, pipe75: { description: 'Create meter with decreasing with reading upload', - chaiRequest: [CHAI_METERS_REQUEST + ".field('headerRow',BooleanTypesJS.true).field('gzip', BooleanTypesJS.false)", CHAI_READINGS_REQUEST + ".field('meterName','pipe75').field('gzip', BooleanTypesJS.false)"], + chaiRequest: [CHAI_METERS_REQUEST + ".field('headerRow',true).field('gzip', false)", CHAI_READINGS_REQUEST + ".field('meterIdentifier','pipe75').field('gzip', false)"], fileName: ['pipe75AInputMeter.csv', 'pipe75BInput.csv'], responseCode: [200, 200], responseString: ['

SUCCESS

Successfully inserted the meters.', '

SUCCESS

It looks like the insert of the readings was a success.

'] }, pipe76: { description: 'Create meter with end only with reading upload', - chaiRequest: [CHAI_METERS_REQUEST + ".field('headerRow',BooleanTypesJS.true).field('gzip',BooleanTypesJS.false)", CHAI_READINGS_REQUEST + ".field('meterName','pipe76').field('gzip',BooleanTypesJS.false)"], + chaiRequest: [CHAI_METERS_REQUEST + ".field('headerRow',true).field('gzip',false)", CHAI_READINGS_REQUEST + ".field('meterIdentifier','pipe76').field('gzip',false)"], fileName: ['pipe76AInputMeter.csv', 'pipe76BInput.csv'], responseCode: [200, 400], responseString: ['

SUCCESS

Successfully inserted the meters.', '

FAILURE

It looks like the insert of the readings had issues with some or all of the readings where the processing of the readings returned these warning(s)/error(s):


For meter pipe76: Error parsing Reading #1. Reading value gives 24 with error message:
The first ever reading must be dropped when dealing only with endTimestamps.
For reading #1 on meter pipe76 in pipeline: previous reading has value 0 start time 1970-01-01T00:00:00Z end time 1970-01-01T00:00:00Z and current reading has value 24 start time 1970-01-01T00:00:00Z end time 2021-06-02T00:00:00Z with timeSort increasing; duplications 1; cumulative false; cumulativeReset false; cumulativeResetStart 00:00:00; cumulativeResetEnd 23:59:59.999999; lengthGap 0; lengthVariation 0; onlyEndTime true

Readings Dropped and should have previous messages

  1. Dropped Reading #1 for meter pipe76
'] }, pipe80: { description: 'Two meter uploads where second sets cumulative with reading upload; all without headers', - chaiRequest: [CHAI_METERS_REQUEST + ".field('gzip', BooleanTypesJS.false)", CHAI_METERS_REQUEST + ".field('gzip', BooleanTypesJS.false).field('update',BooleanTypesJS.true).field('meterName', 'pipe80')", CHAI_READINGS_REQUEST + ".field('gzip', BooleanTypesJS.false).field('meterName', 'pipe80')"], + chaiRequest: [CHAI_METERS_REQUEST + ".field('gzip', false)", CHAI_METERS_REQUEST + ".field('gzip', false).field('update',true).field('meterIdentifier', 'pipe80')", CHAI_READINGS_REQUEST + ".field('gzip', false).field('meterIdentifier', 'pipe80')"], fileName: ['pipe80AInputMeter.csv', 'pipe80BInputMeter.csv', 'pipe80CInput.csv'], responseCode: [200, 200, 400], responseString: ['

SUCCESS

Successfully inserted the meters.', '

SUCCESS

Successfully inserted the meters.', '

FAILURE

It looks like the insert of the readings had issues with some or all of the readings where the processing of the readings returned these warning(s)/error(s):


For meter pipe80: Error parsing Reading #1. Reading value gives 24 with error message:
The first ever reading must be dropped when dealing with cumulative data.
For reading #1 on meter pipe80 in pipeline: previous reading has value 0 start time 1970-01-01T00:00:00Z end time 1970-01-01T00:00:00Z and current reading has value 24 start time 2021-06-01T00:00:00Z end time 2021-06-02T00:00:00Z with timeSort increasing; duplications 1; cumulative true; cumulativeReset false; cumulativeResetStart 00:00:00; cumulativeResetEnd 23:59:59.999999; lengthGap 0; lengthVariation 0; onlyEndTime false

Readings Dropped and should have previous messages

  1. Dropped Reading #1 for meter pipe80
'] }, pipe90: { description: 'Two meter uploads with header and zipped where second sets cumulative & reset, renames meter then reading upload', - chaiRequest: [CHAI_METERS_REQUEST + ".field('headerRow',BooleanTypesJS.true)", CHAI_METERS_REQUEST + ".field('headerRow',BooleanTypesJS.true).field('update',BooleanTypesJS.true).field('meterName', 'pipe90x')", CHAI_READINGS_REQUEST + ".field('gzip', BooleanTypesJS.false).field('meterName', 'pipe90')"], + chaiRequest: [CHAI_METERS_REQUEST + ".field('headerRow',true)", CHAI_METERS_REQUEST + ".field('headerRow',true).field('update',true).field('meterIdentifier', 'pipe90x')", CHAI_READINGS_REQUEST + ".field('gzip', false).field('meterIdentifier', 'pipe90')"], fileName: ['pipe90AInputMeter.csv.gz', 'pipe90BInputMeter.csv.gz', 'pipe90CInput.csv'], responseCode: [200, 200, 400], responseString: ['

SUCCESS

Successfully inserted the meters.', '

SUCCESS

Successfully inserted the meters.', '

FAILURE

It looks like the insert of the readings had issues with some or all of the readings where the processing of the readings returned these warning(s)/error(s):


For meter pipe90: Error parsing Reading #1. Reading value gives 24 with error message:
The first ever reading must be dropped when dealing with cumulative data.
For reading #1 on meter pipe90 in pipeline: previous reading has value 0 start time 1970-01-01T00:00:00Z end time 1970-01-01T00:00:00Z and current reading has value 24 start time 2021-06-01T00:00:00Z end time 2021-06-02T00:00:00Z with timeSort increasing; duplications 1; cumulative true; cumulativeReset true; cumulativeResetStart 00:00:00; cumulativeResetEnd 23:59:59.999999; lengthGap 0; lengthVariation 0; onlyEndTime false

Readings Dropped and should have previous messages

  1. Dropped Reading #1 for meter pipe90
'] }, pipe110: { description: 'Create meter with timezone with hourly reading upload into DST', - chaiRequest: [CHAI_METERS_REQUEST + ".field('headerRow',BooleanTypesJS.true).field('gzip', BooleanTypesJS.false)", CHAI_READINGS_REQUEST + ".field('meterName','pipe110').field('honorDst', BooleanTypesJS.true).field('gzip', BooleanTypesJS.false)"], + chaiRequest: [CHAI_METERS_REQUEST + ".field('headerRow',true).field('gzip', false)", CHAI_READINGS_REQUEST + ".field('meterIdentifier','pipe110').field('honorDst', true).field('gzip', false)"], fileName: ['pipe110AInputMeter.csv', 'pipe110BInput.csv'], responseCode: [200, 200], responseString: ['

SUCCESS

Successfully inserted the meters.', '

SUCCESS

It looks like the insert of the readings was a success.

However, note that the processing of the readings returned these warning(s):


For meter pipe110: Warning parsing Reading #2. Reading value gives 120 with warning message:
Reading #2 crossed into daylight savings so it needs to be split where the first part is now being used. The original reading had startTimestamp of 2022-03-13T01:00:00-06:00 endTimestamp of 2022-03-13T03:00:00-05:00 reading value of 120 and the first part has a startTimestamp of 2022-03-13T01:00:00Z endTimestamp of 2022-03-13T02:00:00Z reading value of 120. This is only a notification and should not be an issue.
For reading #2 on meter pipe110 in pipeline: previous reading has value 60 start time 2022-03-13T00:00:00Z end time 2022-03-13T01:00:00Z and current reading has value 120 start time 2022-03-13T02:00:00Z end time 2022-03-13T03:00:00Z with timeSort increasing; duplications 1; cumulative false; cumulativeReset false; cumulativeResetStart 00:00:00; cumulativeResetEnd 23:59:59.999999; lengthGap 0; lengthVariation 0; onlyEndTime false
'] }, pipe111: { description: 'Create meter with timezone with daily reading upload into DST', - chaiRequest: [CHAI_METERS_REQUEST + ".field('headerRow',BooleanTypesJS.true).field('gzip', BooleanTypesJS.false)", CHAI_READINGS_REQUEST + ".field('meterName','pipe111').field('honorDst', BooleanTypesJS.true).field('gzip', BooleanTypesJS.false)"], + chaiRequest: [CHAI_METERS_REQUEST + ".field('headerRow',true).field('gzip', false)", CHAI_READINGS_REQUEST + ".field('meterIdentifier','pipe111').field('honorDst', true).field('gzip', false)"], fileName: ['pipe111AInputMeter.csv', 'pipe111BInput.csv'], responseCode: [200, 200], responseString: ['

SUCCESS

Successfully inserted the meters.', '

SUCCESS

It looks like the insert of the readings was a success.

However, note that the processing of the readings returned these warning(s):


For meter pipe111: Warning parsing Reading #2. Reading value gives 2760 with warning message:
Reading #2 crossed into daylight savings so it needs to be split where the first part is now being used. The original reading had startTimestamp of 2022-03-13T00:00:00-06:00 endTimestamp of 2022-03-14T00:00:00-05:00 reading value of 2760 and the first part has a startTimestamp of 2022-03-13T00:00:00Z endTimestamp of 2022-03-13T02:00:00Z reading value of 240. This is only a notification and should not be an issue.
Reading #2 crossed into daylight savings so it needs to be split where the second part is now being used. The original reading had startTimestamp of 2022-03-13T00:00:00-06:00 endTimestamp of 2022-03-14T00:00:00-05:00 reading value of 2760 and the second part has a startTimestamp of 2022-03-13T03:00:00Z endTimestamp of 2022-03-14T00:00:00Z reading value of 2520. This is only a notification and should not be an issue.
For reading #2 on meter pipe111 in pipeline: previous reading has value 1440 start time 2022-03-12T00:00:00Z end time 2022-03-13T00:00:00Z and current reading has value 2760 start time 2022-03-13T00:00:00Z end time 2022-03-14T00:00:00Z with timeSort increasing; duplications 1; cumulative false; cumulativeReset false; cumulativeResetStart 00:00:00; cumulativeResetEnd 23:59:59.999999; lengthGap 0; lengthVariation 0; onlyEndTime false
'] }, pipe112: { description: 'Create meter with timezone with 15-minute reading upload into DST', - chaiRequest: [CHAI_METERS_REQUEST + ".field('headerRow',BooleanTypesJS.true).field('gzip', BooleanTypesJS.false)", CHAI_READINGS_REQUEST + ".field('meterName','pipe112').field('honorDst', BooleanTypesJS.true).field('gzip', BooleanTypesJS.false)"], + chaiRequest: [CHAI_METERS_REQUEST + ".field('headerRow',true).field('gzip', false)", CHAI_READINGS_REQUEST + ".field('meterIdentifier','pipe112').field('honorDst', true).field('gzip', false)"], fileName: ['pipe112AInputMeter.csv', 'pipe112BInput.csv'], responseCode: [200, 200], responseString: ['

SUCCESS

Successfully inserted the meters.', '

SUCCESS

It looks like the insert of the readings was a success.

However, note that the processing of the readings returned these warning(s):


For meter pipe112: Warning parsing Reading #2. Reading value gives 30 with warning message:
Reading #2 crossed into daylight savings so it needs to be split where the first part is now being used. The original reading had startTimestamp of 2022-03-13T01:45:00-06:00 endTimestamp of 2022-03-13T03:00:00-05:00 reading value of 30 and the first part has a startTimestamp of 2022-03-13T01:45:00Z endTimestamp of 2022-03-13T02:00:00Z reading value of 30. This is only a notification and should not be an issue.
For reading #2 on meter pipe112 in pipeline: previous reading has value 15 start time 2022-03-13T01:30:00Z end time 2022-03-13T01:45:00Z and current reading has value 30 start time 2022-03-13T02:45:00Z end time 2022-03-13T03:00:00Z with timeSort increasing; duplications 1; cumulative false; cumulativeReset false; cumulativeResetStart 00:00:00; cumulativeResetEnd 23:59:59.999999; lengthGap 0; lengthVariation 0; onlyEndTime false
'] }, pipe113: { description: 'Create meter with timezone with 23-minute reading upload into DST', - chaiRequest: [CHAI_METERS_REQUEST + ".field('headerRow',BooleanTypesJS.true).field('gzip', BooleanTypesJS.false)", CHAI_READINGS_REQUEST + ".field('meterName','pipe113').field('honorDst', BooleanTypesJS.true).field('gzip', BooleanTypesJS.false)"], + chaiRequest: [CHAI_METERS_REQUEST + ".field('headerRow',true).field('gzip', false)", CHAI_READINGS_REQUEST + ".field('meterIdentifier','pipe113').field('honorDst', true).field('gzip', false)"], fileName: ['pipe113AInputMeter.csv', 'pipe113BInput.csv'], responseCode: [200, 200], responseString: ['

SUCCESS

Successfully inserted the meters.', '

SUCCESS

It looks like the insert of the readings was a success.

However, note that the processing of the readings returned these warning(s):


For meter pipe113: Warning parsing Reading #2. Reading value gives 46 with warning message:
Reading #2 crossed into daylight savings so it needs to be split where the first part is now being used. The original reading had startTimestamp of 2022-03-13T01:46:00-06:00 endTimestamp of 2022-03-13T03:09:00-05:00 reading value of 46 and the first part has a startTimestamp of 2022-03-13T01:46:00Z endTimestamp of 2022-03-13T02:00:00Z reading value of 28. This is only a notification and should not be an issue.
Reading #2 crossed into daylight savings so it needs to be split where the second part is now being used. The original reading had startTimestamp of 2022-03-13T01:46:00-06:00 endTimestamp of 2022-03-13T03:09:00-05:00 reading value of 46 and the second part has a startTimestamp of 2022-03-13T03:00:00Z endTimestamp of 2022-03-13T03:09:00Z reading value of 18. This is only a notification and should not be an issue.
For reading #2 on meter pipe113 in pipeline: previous reading has value 23 start time 2022-03-13T01:23:00Z end time 2022-03-13T01:46:00Z and current reading has value 46 start time 2022-03-13T02:46:00Z end time 2022-03-13T03:09:00Z with timeSort increasing; duplications 1; cumulative false; cumulativeReset false; cumulativeResetStart 00:00:00; cumulativeResetEnd 23:59:59.999999; lengthGap 0; lengthVariation 0; onlyEndTime false
'] }, pipe114: { description: 'Create cumulative meter with timezone with 23-minute reading upload into DST', - chaiRequest: [CHAI_METERS_REQUEST + ".field('headerRow',BooleanTypesJS.true).field('gzip', BooleanTypesJS.false)", CHAI_READINGS_REQUEST + ".field('meterName','pipe114').field('honorDst', BooleanTypesJS.true).field('cumulative',BooleanTypesJS.true).field('gzip', BooleanTypesJS.false)"], + chaiRequest: [CHAI_METERS_REQUEST + ".field('headerRow',true).field('gzip', false)", CHAI_READINGS_REQUEST + ".field('meterIdentifier','pipe114').field('honorDst', true).field('cumulative',true).field('gzip', false)"], fileName: ['pipe114AInputMeter.csv', 'pipe114BInput.csv'], responseCode: [200, 400], responseString: ['

SUCCESS

Successfully inserted the meters.', '

FAILURE

It looks like the insert of the readings had issues with some or all of the readings where the processing of the readings returned these warning(s)/error(s):


For meter pipe114: Error parsing Reading #1. Reading value gives 0 with error message:
The first ever reading must be dropped when dealing with cumulative data.
For reading #1 on meter pipe114 in pipeline: previous reading has value 0 start time 1970-01-01T00:00:00Z end time 1970-01-01T00:00:00Z and current reading has value 0 start time 2022-03-13T01:00:00Z end time 2022-03-13T01:23:00Z with timeSort increasing; duplications 1; cumulative true; cumulativeReset false; cumulativeResetStart 00:00:00; cumulativeResetEnd 23:59:59.999999; lengthGap 0; lengthVariation 0; onlyEndTime false

For meter pipe114: Warning parsing Reading #3. Reading value gives 46 with warning message:
Reading #3 crossed into daylight savings so it needs to be split where the first part is now being used. The original reading had startTimestamp of 2022-03-13T01:46:00-06:00 endTimestamp of 2022-03-13T03:09:00-05:00 reading value of 46 and the first part has a startTimestamp of 2022-03-13T01:46:00Z endTimestamp of 2022-03-13T02:00:00Z reading value of 28. This is only a notification and should not be an issue.
Reading #3 crossed into daylight savings so it needs to be split where the second part is now being used. The original reading had startTimestamp of 2022-03-13T01:46:00-06:00 endTimestamp of 2022-03-13T03:09:00-05:00 reading value of 46 and the second part has a startTimestamp of 2022-03-13T03:00:00Z endTimestamp of 2022-03-13T03:09:00Z reading value of 18. This is only a notification and should not be an issue.
For reading #3 on meter pipe114 in pipeline: previous reading has value 23 start time 2022-03-13T01:23:00Z end time 2022-03-13T01:46:00Z and current reading has value 46 start time 2022-03-13T02:46:00Z end time 2022-03-13T03:09:00Z with timeSort increasing; duplications 1; cumulative true; cumulativeReset false; cumulativeResetStart 00:00:00; cumulativeResetEnd 23:59:59.999999; lengthGap 0; lengthVariation 0; onlyEndTime false

Readings Dropped and should have previous messages

  1. Dropped Reading #1 for meter pipe114
'] }, pipe115: { description: 'Create meter with timezone with end-only reading upload into DST', - chaiRequest: [CHAI_METERS_REQUEST + ".field('headerRow',BooleanTypesJS.true).field('gzip', BooleanTypesJS.false)", CHAI_READINGS_REQUEST + ".field('meterName','pipe115').field('honorDst', BooleanTypesJS.true).field('endOnly', BooleanTypesJS.true).field('gzip', BooleanTypesJS.false)"], + chaiRequest: [CHAI_METERS_REQUEST + ".field('headerRow',true).field('gzip', false)", CHAI_READINGS_REQUEST + ".field('meterIdentifier','pipe115').field('honorDst', true).field('endOnly', true).field('gzip', false)"], fileName: ['pipe115AInputMeter.csv', 'pipe115BInput.csv'], responseCode: [200, 400], responseString: ['

SUCCESS

Successfully inserted the meters.', '

FAILURE

It looks like the insert of the readings had issues with some or all of the readings where the processing of the readings returned these warning(s)/error(s):


For meter pipe115: Error parsing Reading #1. Reading value gives -99 with error message:
The first ever reading must be dropped when dealing only with endTimestamps.
For reading #1 on meter pipe115 in pipeline: previous reading has value 0 start time 1970-01-01T00:00:00Z end time 1970-01-01T00:00:00Z and current reading has value -99 start time 1970-01-01T00:00:00Z end time 2022-03-13T01:23:00Z with timeSort increasing; duplications 1; cumulative false; cumulativeReset false; cumulativeResetStart 00:00:00; cumulativeResetEnd 23:59:59.999999; lengthGap 0; lengthVariation 0; onlyEndTime true

For meter pipe115: Warning parsing Reading #3. Reading value gives 46 with warning message:
Reading #3 crossed into daylight savings so it needs to be split where the first part is now being used. The original reading had startTimestamp of 2022-03-13T01:46:00-06:00 endTimestamp of 2022-03-13T03:09:00-05:00 reading value of 46 and the first part has a startTimestamp of 2022-03-13T01:46:00Z endTimestamp of 2022-03-13T02:00:00Z reading value of 28. This is only a notification and should not be an issue.
Reading #3 crossed into daylight savings so it needs to be split where the second part is now being used. The original reading had startTimestamp of 2022-03-13T01:46:00-06:00 endTimestamp of 2022-03-13T03:09:00-05:00 reading value of 46 and the second part has a startTimestamp of 2022-03-13T03:00:00Z endTimestamp of 2022-03-13T03:09:00Z reading value of 18. This is only a notification and should not be an issue.
For reading #3 on meter pipe115 in pipeline: previous reading has value 23 start time 2022-03-13T01:23:00Z end time 2022-03-13T02:46:00Z and current reading has value 46 start time 2022-03-13T02:46:00Z end time 2022-03-13T03:09:00Z with timeSort increasing; duplications 1; cumulative false; cumulativeReset false; cumulativeResetStart 00:00:00; cumulativeResetEnd 23:59:59.999999; lengthGap 0; lengthVariation 0; onlyEndTime true

Readings Dropped and should have previous messages

  1. Dropped Reading #1 for meter pipe115
'] }, pipe116: { description: 'Create meter with timezone with hourly reading upload from DST', - chaiRequest: [CHAI_METERS_REQUEST + ".field('headerRow',BooleanTypesJS.true).field('gzip', BooleanTypesJS.false)", CHAI_READINGS_REQUEST + ".field('meterName','pipe116').field('honorDst', BooleanTypesJS.true).field('gzip', BooleanTypesJS.false)"], + chaiRequest: [CHAI_METERS_REQUEST + ".field('headerRow',true).field('gzip', false)", CHAI_READINGS_REQUEST + ".field('meterIdentifier','pipe116').field('honorDst', true).field('gzip', false)"], fileName: ['pipe116AInputMeter.csv', 'pipe116BInput.csv'], responseCode: [200, 400], responseString: ['

SUCCESS

Successfully inserted the meters.', '

FAILURE

It looks like the insert of the readings had issues with some or all of the readings where the processing of the readings returned these warning(s)/error(s):


For meter pipe116: Error parsing Reading #2. Reading value gives -1 with error message:
This reading is entirely within the shift time from daylight savings to standard time so it is dropped. The dropped reading had startTimestamp of 2022-11-06T01:00:00-05:00 and endTimestamp of 2022-11-06T01:00:00-06:00 and value of -1. This should not be an issue but the reading is lost.
For reading #2 on meter pipe116 in pipeline: previous reading has value 60 start time 2022-11-06T00:00:00Z end time 2022-11-06T01:00:00Z and current reading has value -1 start time 2022-11-06T00:00:00Z end time 2022-11-06T01:00:00Z with timeSort increasing; duplications 1; cumulative false; cumulativeReset false; cumulativeResetStart 00:00:00; cumulativeResetEnd 23:59:59.999999; lengthGap 0; lengthVariation 0; onlyEndTime false

For meter pipe116: Warning parsing Reading #3. Reading value gives 120 with warning message:
This or a previous reading crossed from daylight savings time and is the first one that does not entirely overlap a previous reading so its reading will be prorated where the original values were: startTimestamp of 2022-11-06T01:00:00-06:00 endTimestamp of 2022-11-06T02:00:00-06:00 reading value of 120. The used part has startTimestamp of 2022-11-06T01:00:00Z and endTimestamp of 2022-11-06T02:00:00Z and value of 120. This is only a notification and should not be an issue.
For reading #3 on meter pipe116 in pipeline: previous reading has value -1 start time 2022-11-06T00:00:00Z end time 2022-11-06T01:00:00Z and current reading has value 120 start time 2022-11-06T01:00:00Z end time 2022-11-06T02:00:00Z with timeSort increasing; duplications 1; cumulative false; cumulativeReset false; cumulativeResetStart 00:00:00; cumulativeResetEnd 23:59:59.999999; lengthGap 0; lengthVariation 0; onlyEndTime false

Readings Dropped and should have previous messages

  1. Dropped Reading #2 for meter pipe116
'] }, pipe117: { description: 'Create meter with timezone with daily reading upload from DST', - chaiRequest: [CHAI_METERS_REQUEST + ".field('headerRow',BooleanTypesJS.true).field('gzip', BooleanTypesJS.false)", CHAI_READINGS_REQUEST + ".field('meterName','pipe117').field('honorDst', BooleanTypesJS.true).field('gzip', BooleanTypesJS.false)"], + chaiRequest: [CHAI_METERS_REQUEST + ".field('headerRow',true).field('gzip', false)", CHAI_READINGS_REQUEST + ".field('meterIdentifier','pipe117').field('honorDst', true).field('gzip', false)"], fileName: ['pipe117AInputMeter.csv', 'pipe117BInput.csv'], responseCode: [200, 200], responseString: ['

SUCCESS

Successfully inserted the meters.', '

SUCCESS

It looks like the insert of the readings was a success.

However, note that the processing of the readings returned these warning(s):


For meter pipe117: Warning parsing Reading #2. Reading value gives 2880 with warning message:
This or a previous reading crossed from daylight savings time and is the first one that does not entirely overlap a previous reading so its reading will be prorated where the original values were: startTimestamp of 2022-11-06T00:00:00-05:00 endTimestamp of 2022-11-07T00:00:00-06:00 reading value of 3000. The used part has startTimestamp of 2022-11-06T00:00:00Z and endTimestamp of 2022-11-07T00:00:00Z and value of 2880. This is only a notification and should not be an issue.
For reading #2 on meter pipe117 in pipeline: previous reading has value 1440 start time 2022-11-05T00:00:00Z end time 2022-11-06T00:00:00Z and current reading has value 2880 start time 2022-11-06T00:00:00Z end time 2022-11-07T00:00:00Z with timeSort increasing; duplications 1; cumulative false; cumulativeReset false; cumulativeResetStart 00:00:00; cumulativeResetEnd 23:59:59.999999; lengthGap 0; lengthVariation 0; onlyEndTime false
'] }, pipe118: { description: 'Create meter with timezone with 15-minute reading upload from DST', - chaiRequest: [CHAI_METERS_REQUEST + ".field('headerRow',BooleanTypesJS.true).field('gzip', BooleanTypesJS.false)", CHAI_READINGS_REQUEST + ".field('meterName','pipe118').field('honorDst', BooleanTypesJS.true).field('gzip', BooleanTypesJS.false)"], + chaiRequest: [CHAI_METERS_REQUEST + ".field('headerRow',true).field('gzip', false)", CHAI_READINGS_REQUEST + ".field('meterIdentifier','pipe118').field('honorDst', true).field('gzip', false)"], fileName: ['pipe118AInputMeter.csv', 'pipe118BInput.csv'], responseCode: [200, 400], responseString: ['

SUCCESS

Successfully inserted the meters.', '

FAILURE

It looks like the insert of the readings had issues with some or all of the readings where the processing of the readings returned these warning(s)/error(s):


For meter pipe118: Error parsing Reading #2. Reading value gives -1 with error message:
This reading is entirely within the shift time from daylight savings to standard time so it is dropped. The dropped reading had startTimestamp of 2022-11-06T01:45:00-05:00 and endTimestamp of 2022-11-06T01:00:00-06:00 and value of -1. This should not be an issue but the reading is lost.
For reading #2 on meter pipe118 in pipeline: previous reading has value 15 start time 2022-11-06T01:30:00Z end time 2022-11-06T01:45:00Z and current reading has value -1 start time 2022-11-06T00:45:00Z end time 2022-11-06T01:00:00Z with timeSort increasing; duplications 1; cumulative false; cumulativeReset false; cumulativeResetStart 00:00:00; cumulativeResetEnd 23:59:59.999999; lengthGap 0; lengthVariation 0; onlyEndTime false

For meter pipe118: Error parsing Reading #3. Reading value gives -2 with error message:
This reading is entirely within the shift time from daylight savings to standard time so it is dropped. The dropped reading had startTimestamp of 2022-11-06T01:00:00-06:00 and endTimestamp of 2022-11-06T01:15:00-06:00 and value of -2. This should not be an issue but the reading is lost.
For reading #3 on meter pipe118 in pipeline: previous reading has value -1 start time 2022-11-06T00:45:00Z end time 2022-11-06T01:00:00Z and current reading has value -2 start time 2022-11-06T01:00:00Z end time 2022-11-06T01:15:00Z with timeSort increasing; duplications 1; cumulative false; cumulativeReset false; cumulativeResetStart 00:00:00; cumulativeResetEnd 23:59:59.999999; lengthGap 0; lengthVariation 0; onlyEndTime false

For meter pipe118: Error parsing Reading #4. Reading value gives -3 with error message:
This reading is entirely within the shift time from daylight savings to standard time so it is dropped. The dropped reading had startTimestamp of 2022-11-06T01:15:00-06:00 and endTimestamp of 2022-11-06T01:30:00-06:00 and value of -3. This should not be an issue but the reading is lost.
For reading #4 on meter pipe118 in pipeline: previous reading has value -2 start time 2022-11-06T01:00:00Z end time 2022-11-06T01:15:00Z and current reading has value -3 start time 2022-11-06T01:15:00Z end time 2022-11-06T01:30:00Z with timeSort increasing; duplications 1; cumulative false; cumulativeReset false; cumulativeResetStart 00:00:00; cumulativeResetEnd 23:59:59.999999; lengthGap 0; lengthVariation 0; onlyEndTime false

For meter pipe118: Error parsing Reading #5. Reading value gives -4 with error message:
This reading is entirely within the shift time from daylight savings to standard time so it is dropped. The dropped reading had startTimestamp of 2022-11-06T01:30:00-06:00 and endTimestamp of 2022-11-06T01:45:00-06:00 and value of -4. This should not be an issue but the reading is lost.
For reading #5 on meter pipe118 in pipeline: previous reading has value -3 start time 2022-11-06T01:15:00Z end time 2022-11-06T01:30:00Z and current reading has value -4 start time 2022-11-06T01:30:00Z end time 2022-11-06T01:45:00Z with timeSort increasing; duplications 1; cumulative false; cumulativeReset false; cumulativeResetStart 00:00:00; cumulativeResetEnd 23:59:59.999999; lengthGap 0; lengthVariation 0; onlyEndTime false

For meter pipe118: Warning parsing Reading #6. Reading value gives 30 with warning message:
This or a previous reading crossed from daylight savings time and is the first one that does not entirely overlap a previous reading so its reading will be prorated where the original values were: startTimestamp of 2022-11-06T01:45:00-06:00 endTimestamp of 2022-11-06T02:00:00-06:00 reading value of 30. The used part has startTimestamp of 2022-11-06T01:45:00Z and endTimestamp of 2022-11-06T02:00:00Z and value of 30. This is only a notification and should not be an issue.
For reading #6 on meter pipe118 in pipeline: previous reading has value -4 start time 2022-11-06T01:30:00Z end time 2022-11-06T01:45:00Z and current reading has value 30 start time 2022-11-06T01:45:00Z end time 2022-11-06T02:00:00Z with timeSort increasing; duplications 1; cumulative false; cumulativeReset false; cumulativeResetStart 00:00:00; cumulativeResetEnd 23:59:59.999999; lengthGap 0; lengthVariation 0; onlyEndTime false

Readings Dropped and should have previous messages

  1. Dropped Reading #2 for meter pipe118
  2. Dropped Reading #3 for meter pipe118
  3. Dropped Reading #4 for meter pipe118
  4. Dropped Reading #5 for meter pipe118
'] }, pipe119: { description: 'Create meter with timezone with 23-minute reading upload from DST', - chaiRequest: [CHAI_METERS_REQUEST + ".field('headerRow',BooleanTypesJS.true).field('gzip', BooleanTypesJS.false)", CHAI_READINGS_REQUEST + ".field('meterName','pipe119').field('honorDst', BooleanTypesJS.true).field('gzip', BooleanTypesJS.false)"], + chaiRequest: [CHAI_METERS_REQUEST + ".field('headerRow',true).field('gzip', false)", CHAI_READINGS_REQUEST + ".field('meterIdentifier','pipe119').field('honorDst', true).field('gzip', false)"], fileName: ['pipe119AInputMeter.csv', 'pipe119BInput.csv'], responseCode: [200, 400], responseString: ['

SUCCESS

Successfully inserted the meters.', '

FAILURE

It looks like the insert of the readings had issues with some or all of the readings where the processing of the readings returned these warning(s)/error(s):


For meter pipe119: Error parsing Reading #2. Reading value gives -1 with error message:
This reading is entirely within the shift time from daylight savings to standard time so it is dropped. The dropped reading had startTimestamp of 2022-11-06T01:46:00-05:00 and endTimestamp of 2022-11-06T01:09:00-06:00 and value of -1. This should not be an issue but the reading is lost.
For reading #2 on meter pipe119 in pipeline: previous reading has value 23 start time 2022-11-06T01:23:00Z end time 2022-11-06T01:46:00Z and current reading has value -1 start time 2022-11-06T00:46:00Z end time 2022-11-06T01:09:00Z with timeSort increasing; duplications 1; cumulative false; cumulativeReset false; cumulativeResetStart 00:00:00; cumulativeResetEnd 23:59:59.999999; lengthGap 0; lengthVariation 0; onlyEndTime false

For meter pipe119: Error parsing Reading #3. Reading value gives -2 with error message:
This reading is entirely within the shift time from daylight savings to standard time so it is dropped. The dropped reading had startTimestamp of 2022-11-06T01:09:00-06:00 and endTimestamp of 2022-11-06T01:32:00-06:00 and value of -2. This should not be an issue but the reading is lost.
For reading #3 on meter pipe119 in pipeline: previous reading has value -1 start time 2022-11-06T00:46:00Z end time 2022-11-06T01:09:00Z and current reading has value -2 start time 2022-11-06T01:09:00Z end time 2022-11-06T01:32:00Z with timeSort increasing; duplications 1; cumulative false; cumulativeReset false; cumulativeResetStart 00:00:00; cumulativeResetEnd 23:59:59.999999; lengthGap 0; lengthVariation 0; onlyEndTime false

For meter pipe119: Warning parsing Reading #4. Reading value gives 18 with warning message:
This or a previous reading crossed from daylight savings time and is the first one that does not entirely overlap a previous reading so its reading will be prorated where the original values were: startTimestamp of 2022-11-06T01:32:00-06:00 endTimestamp of 2022-11-06T01:55:00-06:00 reading value of 46. The used part has startTimestamp of 2022-11-06T01:46:00Z and endTimestamp of 2022-11-06T01:55:00Z and value of 18. This is only a notification and should not be an issue.
For reading #4 on meter pipe119 in pipeline: previous reading has value -2 start time 2022-11-06T01:09:00Z end time 2022-11-06T01:32:00Z and current reading has value 18 start time 2022-11-06T01:32:00Z end time 2022-11-06T01:55:00Z with timeSort increasing; duplications 1; cumulative false; cumulativeReset false; cumulativeResetStart 00:00:00; cumulativeResetEnd 23:59:59.999999; lengthGap 0; lengthVariation 0; onlyEndTime false

Readings Dropped and should have previous messages

  1. Dropped Reading #2 for meter pipe119
  2. Dropped Reading #3 for meter pipe119
'] }, pipe120: { description: 'Create cumulative meter with timezone with 23-minute reading upload from DST', - chaiRequest: [CHAI_METERS_REQUEST + ".field('headerRow',BooleanTypesJS.true).field('gzip', BooleanTypesJS.false)", CHAI_READINGS_REQUEST + ".field('meterName','pipe120').field('honorDst', BooleanTypesJS.true).field('cumulative',BooleanTypesJS.true).field('gzip', BooleanTypesJS.false)"], + chaiRequest: [CHAI_METERS_REQUEST + ".field('headerRow',true).field('gzip', false)", CHAI_READINGS_REQUEST + ".field('meterIdentifier','pipe120').field('honorDst', true).field('cumulative',true).field('gzip', false)"], fileName: ['pipe120AInputMeter.csv', 'pipe120BInput.csv'], responseCode: [200, 400], responseString: ['

SUCCESS

Successfully inserted the meters.', '

FAILURE

It looks like the insert of the readings had issues with some or all of the readings where the processing of the readings returned these warning(s)/error(s):


For meter pipe120: Error parsing Reading #1. Reading value gives 0 with error message:
The first ever reading must be dropped when dealing with cumulative data.
For reading #1 on meter pipe120 in pipeline: previous reading has value 0 start time 1970-01-01T00:00:00Z end time 1970-01-01T00:00:00Z and current reading has value 0 start time 2022-11-06T01:00:00Z end time 2022-11-06T01:23:00Z with timeSort increasing; duplications 1; cumulative true; cumulativeReset false; cumulativeResetStart 00:00:00; cumulativeResetEnd 23:59:59.999999; lengthGap 0; lengthVariation 0; onlyEndTime false

For meter pipe120: Error parsing Reading #3. Reading value gives 0 with error message:
This reading is entirely within the shift time from daylight savings to standard time so it is dropped. The dropped reading had startTimestamp of 2022-11-06T01:46:00-05:00 and endTimestamp of 2022-11-06T01:09:00-06:00 and value of 0. This should not be an issue but the reading is lost.
For reading #3 on meter pipe120 in pipeline: previous reading has value 23 start time 2022-11-06T01:23:00Z end time 2022-11-06T01:46:00Z and current reading has value 0 start time 2022-11-06T00:46:00Z end time 2022-11-06T01:09:00Z with timeSort increasing; duplications 1; cumulative true; cumulativeReset false; cumulativeResetStart 00:00:00; cumulativeResetEnd 23:59:59.999999; lengthGap 0; lengthVariation 0; onlyEndTime false

For meter pipe120: Error parsing Reading #4. Reading value gives 0 with error message:
This reading is entirely within the shift time from daylight savings to standard time so it is dropped. The dropped reading had startTimestamp of 2022-11-06T01:09:00-06:00 and endTimestamp of 2022-11-06T01:32:00-06:00 and value of 0. This should not be an issue but the reading is lost.
For reading #4 on meter pipe120 in pipeline: previous reading has value 0 start time 2022-11-06T00:46:00Z end time 2022-11-06T01:09:00Z and current reading has value 0 start time 2022-11-06T01:09:00Z end time 2022-11-06T01:32:00Z with timeSort increasing; duplications 1; cumulative true; cumulativeReset false; cumulativeResetStart 00:00:00; cumulativeResetEnd 23:59:59.999999; lengthGap 0; lengthVariation 0; onlyEndTime false

For meter pipe120: Warning parsing Reading #5. Reading value gives 18 with warning message:
This or a previous reading crossed from daylight savings time and is the first one that does not entirely overlap a previous reading so its reading will be prorated where the original values were: startTimestamp of 2022-11-06T01:32:00-06:00 endTimestamp of 2022-11-06T01:55:00-06:00 reading value of 46. The used part has startTimestamp of 2022-11-06T01:46:00Z and endTimestamp of 2022-11-06T01:55:00Z and value of 18. This is only a notification and should not be an issue.
For reading #5 on meter pipe120 in pipeline: previous reading has value 0 start time 2022-11-06T01:09:00Z end time 2022-11-06T01:32:00Z and current reading has value 18 start time 2022-11-06T01:32:00Z end time 2022-11-06T01:55:00Z with timeSort increasing; duplications 1; cumulative true; cumulativeReset false; cumulativeResetStart 00:00:00; cumulativeResetEnd 23:59:59.999999; lengthGap 0; lengthVariation 0; onlyEndTime false

Readings Dropped and should have previous messages

  1. Dropped Reading #1 for meter pipe120
  2. Dropped Reading #3 for meter pipe120
  3. Dropped Reading #4 for meter pipe120
'] }, pipe121: { description: 'Create end-only meter with timezone with 23-minute reading upload from DST', - chaiRequest: [CHAI_METERS_REQUEST + ".field('headerRow',BooleanTypesJS.true).field('gzip', BooleanTypesJS.false)", CHAI_READINGS_REQUEST + ".field('meterName','pipe121').field('honorDst', BooleanTypesJS.true).field('endOnly', BooleanTypesJS.true).field('gzip', BooleanTypesJS.false)"], + chaiRequest: [CHAI_METERS_REQUEST + ".field('headerRow',true).field('gzip', false)", CHAI_READINGS_REQUEST + ".field('meterIdentifier','pipe121').field('honorDst', true).field('endOnly', true).field('gzip', false)"], fileName: ['pipe121AInputMeter.csv', 'pipe121BInput.csv'], responseCode: [200, 400], responseString: ['

SUCCESS

Successfully inserted the meters.', '

FAILURE

It looks like the insert of the readings had issues with some or all of the readings where the processing of the readings returned these warning(s)/error(s):


For meter pipe121: Error parsing Reading #1. Reading value gives -99 with error message:
The first ever reading must be dropped when dealing only with endTimestamps.
For reading #1 on meter pipe121 in pipeline: previous reading has value 0 start time 1970-01-01T00:00:00Z end time 1970-01-01T00:00:00Z and current reading has value -99 start time 1970-01-01T00:00:00Z end time 2022-11-06T01:23:00Z with timeSort increasing; duplications 1; cumulative false; cumulativeReset false; cumulativeResetStart 00:00:00; cumulativeResetEnd 23:59:59.999999; lengthGap 0; lengthVariation 0; onlyEndTime true

For meter pipe121: Error parsing Reading #3. Reading value gives -1 with error message:
This reading is entirely within the shift time from daylight savings to standard time so it is dropped. The dropped reading had startTimestamp of 2022-11-06T01:46:00-05:00 and endTimestamp of 2022-11-06T01:09:00-06:00 and value of -1. This should not be an issue but the reading is lost.
For reading #3 on meter pipe121 in pipeline: previous reading has value 23 start time 2022-11-06T01:23:00Z end time 2022-11-06T01:46:00Z and current reading has value -1 start time 2022-11-06T00:46:00Z end time 2022-11-06T01:09:00Z with timeSort increasing; duplications 1; cumulative false; cumulativeReset false; cumulativeResetStart 00:00:00; cumulativeResetEnd 23:59:59.999999; lengthGap 0; lengthVariation 0; onlyEndTime true

For meter pipe121: Error parsing Reading #4. Reading value gives -2 with error message:
This reading is entirely within the shift time from daylight savings to standard time so it is dropped. The dropped reading had startTimestamp of 2022-11-06T01:09:00-06:00 and endTimestamp of 2022-11-06T01:32:00-06:00 and value of -2. This should not be an issue but the reading is lost.
For reading #4 on meter pipe121 in pipeline: previous reading has value -1 start time 2022-11-06T00:46:00Z end time 2022-11-06T01:09:00Z and current reading has value -2 start time 2022-11-06T01:09:00Z end time 2022-11-06T01:32:00Z with timeSort increasing; duplications 1; cumulative false; cumulativeReset false; cumulativeResetStart 00:00:00; cumulativeResetEnd 23:59:59.999999; lengthGap 0; lengthVariation 0; onlyEndTime true

For meter pipe121: Warning parsing Reading #5. Reading value gives 18 with warning message:
This or a previous reading crossed from daylight savings time and is the first one that does not entirely overlap a previous reading so its reading will be prorated where the original values were: startTimestamp of 2022-11-06T01:32:00-06:00 endTimestamp of 2022-11-06T01:55:00-06:00 reading value of 46. The used part has startTimestamp of 2022-11-06T01:46:00Z and endTimestamp of 2022-11-06T01:55:00Z and value of 18. This is only a notification and should not be an issue.
For reading #5 on meter pipe121 in pipeline: previous reading has value -2 start time 2022-11-06T01:09:00Z end time 2022-11-06T01:32:00Z and current reading has value 18 start time 2022-11-06T01:32:00Z end time 2022-11-06T01:55:00Z with timeSort increasing; duplications 1; cumulative false; cumulativeReset false; cumulativeResetStart 00:00:00; cumulativeResetEnd 23:59:59.999999; lengthGap 0; lengthVariation 0; onlyEndTime true

Readings Dropped and should have previous messages

  1. Dropped Reading #1 for meter pipe121
  2. Dropped Reading #3 for meter pipe121
  3. Dropped Reading #4 for meter pipe121
'] }, pipe122: { description: 'Create end-only meter with timezone with two 23-minute reading upload from DST', - chaiRequest: [CHAI_METERS_REQUEST + ".field('headerRow',BooleanTypesJS.true).field('gzip', BooleanTypesJS.false)", CHAI_READINGS_REQUEST + ".field('meterName','pipe122').field('honorDst', BooleanTypesJS.true).field('endOnly', BooleanTypesJS.true).field('gzip', BooleanTypesJS.false)", CHAI_READINGS_REQUEST + ".field('meterName','pipe122').field('honorDst', BooleanTypesJS.true).field('endOnly', BooleanTypesJS.true).field('gzip', BooleanTypesJS.false)"], + chaiRequest: [CHAI_METERS_REQUEST + ".field('headerRow',true).field('gzip', false)", CHAI_READINGS_REQUEST + ".field('meterIdentifier','pipe122').field('honorDst', true).field('endOnly', true).field('gzip', false)", CHAI_READINGS_REQUEST + ".field('meterIdentifier','pipe122').field('honorDst', true).field('endOnly', true).field('gzip', false)"], fileName: ['pipe122AInputMeter.csv', 'pipe122BInput.csv', 'pipe122CInput.csv'], responseCode: [200, 400, 400], responseString: ['

SUCCESS

Successfully inserted the meters.', '

FAILURE

It looks like the insert of the readings had issues with some or all of the readings where the processing of the readings returned these warning(s)/error(s):


For meter pipe122: Error parsing Reading #1. Reading value gives -99 with error message:
The first ever reading must be dropped when dealing only with endTimestamps.
For reading #1 on meter pipe122 in pipeline: previous reading has value 0 start time 1970-01-01T00:00:00Z end time 1970-01-01T00:00:00Z and current reading has value -99 start time 1970-01-01T00:00:00Z end time 2022-11-06T01:23:00Z with timeSort increasing; duplications 1; cumulative false; cumulativeReset false; cumulativeResetStart 00:00:00; cumulativeResetEnd 23:59:59.999999; lengthGap 0; lengthVariation 0; onlyEndTime true

For meter pipe122: Error parsing Reading #3. Reading value gives -1 with error message:
This reading is entirely within the shift time from daylight savings to standard time so it is dropped. The dropped reading had startTimestamp of 2022-11-06T01:46:00-05:00 and endTimestamp of 2022-11-06T01:09:00-06:00 and value of -1. This should not be an issue but the reading is lost.
For reading #3 on meter pipe122 in pipeline: previous reading has value 23 start time 2022-11-06T01:23:00Z end time 2022-11-06T01:46:00Z and current reading has value -1 start time 2022-11-06T00:46:00Z end time 2022-11-06T01:09:00Z with timeSort increasing; duplications 1; cumulative false; cumulativeReset false; cumulativeResetStart 00:00:00; cumulativeResetEnd 23:59:59.999999; lengthGap 0; lengthVariation 0; onlyEndTime true

Readings Dropped and should have previous messages

  1. Dropped Reading #1 for meter pipe122
  2. Dropped Reading #3 for meter pipe122
', '

FAILURE

It looks like the insert of the readings had issues with some or all of the readings where the processing of the readings returned these warning(s)/error(s):


For meter pipe122: Error parsing Reading #1. Reading value gives -2 with error message:
This reading is entirely within the shift time from daylight savings to standard time so it is dropped. The dropped reading had startTimestamp of 2022-11-06T01:09:00-06:00 and endTimestamp of 2022-11-06T01:32:00-06:00 and value of -2. This should not be an issue but the reading is lost.
For reading #1 on meter pipe122 in pipeline: previous reading has value -1 start time 2022-11-06T01:46:00Z end time 2022-11-06T01:09:00Z and current reading has value -2 start time 2022-11-06T01:09:00Z end time 2022-11-06T01:32:00Z with timeSort increasing; duplications 1; cumulative false; cumulativeReset false; cumulativeResetStart 00:00:00; cumulativeResetEnd 23:59:59.999999; lengthGap 0; lengthVariation 0; onlyEndTime true

For meter pipe122: Warning parsing Reading #2. Reading value gives 18 with warning message:
This or a previous reading crossed from daylight savings time and is the first one that does not entirely overlap a previous reading so its reading will be prorated where the original values were: startTimestamp of 2022-11-06T01:32:00-06:00 endTimestamp of 2022-11-06T01:55:00-06:00 reading value of 46. The used part has startTimestamp of 2022-11-06T01:46:00Z and endTimestamp of 2022-11-06T01:55:00Z and value of 18. This is only a notification and should not be an issue.
For reading #2 on meter pipe122 in pipeline: previous reading has value -2 start time 2022-11-06T01:09:00Z end time 2022-11-06T01:32:00Z and current reading has value 18 start time 2022-11-06T01:32:00Z end time 2022-11-06T01:55:00Z with timeSort increasing; duplications 1; cumulative false; cumulativeReset false; cumulativeResetStart 00:00:00; cumulativeResetEnd 23:59:59.999999; lengthGap 0; lengthVariation 0; onlyEndTime true

Readings Dropped and should have previous messages

  1. Dropped Reading #1 for meter pipe122
'] }, pipe123: { description: 'Create meter with timezone with 30-minute reading with gap upload from DST', - chaiRequest: [CHAI_METERS_REQUEST + ".field('headerRow',BooleanTypesJS.true).field('gzip', BooleanTypesJS.false)", CHAI_READINGS_REQUEST + ".field('meterName','pipe123').field('honorDst', BooleanTypesJS.true).field('gzip', BooleanTypesJS.false)"], + chaiRequest: [CHAI_METERS_REQUEST + ".field('headerRow',true).field('gzip', false)", CHAI_READINGS_REQUEST + ".field('meterIdentifier','pipe123').field('honorDst', true).field('gzip', false)"], fileName: ['pipe123AInputMeter.csv', 'pipe123BInput.csv'], responseCode: [200, 200], responseString: ['

SUCCESS

Successfully inserted the meters.', '

SUCCESS

It looks like the insert of the readings was a success.

However, note that the processing of the readings returned these warning(s):


For meter pipe123: Warning parsing Reading #2. Reading value gives 30 with warning message:
The reading start time is shifted and within the DST shift so it is possible that the crossing to standard time was missed and readings overlap. The current reading startTime is not after the previous reading\'s end time. Note this is treated only as a warning since readings may be sent out of order.
There is a gap in time between this reading and the previous reading that exceeds the allowed amount of 0 seconds.
For reading #2 on meter pipe123 in pipeline: previous reading has value 15 start time 2022-11-06T01:25:00Z end time 2022-11-06T01:55:00Z and current reading has value 30 start time 2022-11-06T01:30:00Z end time 2022-11-06T02:00:00Z with timeSort increasing; duplications 1; cumulative false; cumulativeReset false; cumulativeResetStart 00:00:00; cumulativeResetEnd 23:59:59.999999; lengthGap 0; lengthVariation 0; onlyEndTime false
'] }, pipe130: { description: 'Testing Readings Upload using Email', - chaiRequest: [CHAI_READINGS_REQUEST_EMAIL + ".field('createMeter', BooleanTypesJS.true).field('meterName', 'pipe130').field('gzip', BooleanTypesJS.false)"], + chaiRequest: [CHAI_READINGS_REQUEST_EMAIL + ".field('meterIdentifier', 'pipe130').field('gzip', false)"], fileName: ['pipe130Input.csv'], + createMeter: true, responseCode: [200], responseString: ['

SUCCESS

It looks like the insert of the readings was a success.

'] } @@ -499,10 +538,10 @@ for (let fileKey in testCases) { name: 'Electric_Utility', identifier: '', unitRepresent: Unit.unitRepresentType.QUANTITY, - secInRate: 3600, - typeOfUnit: Unit.unitType.METER, + secInRate: 3600, + typeOfUnit: Unit.unitType.METER, suffix: '', - displayable: Unit.displayableType.NONE, + displayable: Unit.displayableType.NONE, preferredDisplay: false, note: 'for testing' } @@ -529,10 +568,24 @@ for (let fileKey in testCases) { let expectedFile = `${fileKey}Expected.csv`; let expectedPath = `${__dirname}/csvPipeline/${expectedFile}`; let expectedBuffer = fs.readFileSync(expectedPath); + const conn = testDB.getConnection(); for (let index = 0; index < numUploads; index++) { // It would be nice to put a mocha.describe inside the loop to tell the upload starting // but that breaks the tests. // Each set of uploads must be in one mocha because the DB is reset with each test. + if (index == 0 && testCases[fileKey].hasOwnProperty('createMeter') && testCases[fileKey]['createMeter']) { + // The key to create meter is present and true so create the meter if first request. + const meter = new Meter( + undefined, // id + fileKey, // name + undefined, // url + false, // enabled + false, // displayable + Meter.type.OTHER, // type + null, // timezone + ) + meter.insert(conn); + } let inputFile = testCases[fileKey]['fileName'][index]; let inputPath = `${__dirname}/csvPipeline/${inputFile}`; let inputBuffer = fs.readFileSync(inputPath); @@ -545,7 +598,6 @@ for (let fileKey in testCases) { expect(res.text).to.equal(testCases[fileKey]['responseString'][index]); } // You do not want to check the database until all the uploads are done. - const conn = testDB.getConnection(); // Get every meter to be sure only one with correct name. const meters = await Meter.getAll(conn); expect(meters.length).to.equal(1); @@ -577,11 +629,11 @@ metersUpload: an array where each entry is a meter object that is what should be */ const testMeters = { pipe100: { - description: 'Second meter upload where incorrectly provides meter name so fails', - chaiRequest: [CHAI_METERS_REQUEST + ".field('gzip', BooleanTypesJS.false).field('headerRow',BooleanTypesJS.true)", CHAI_METERS_REQUEST + ".field('gzip', BooleanTypesJS.false).field('update',BooleanTypesJS.true).field('meterName', 'pipe100').field('headerRow',BooleanTypesJS.true)"], + description: 'Second meter upload where incorrectly provides meter identifier so fails', + chaiRequest: [CHAI_METERS_REQUEST + ".field('gzip', false).field('headerRow',true)", CHAI_METERS_REQUEST + ".field('gzip', false).field('update',true).field('meterIdentifier', 'pipe100').field('headerRow',true)"], fileName: ['pipe100InputMeter.csv', 'pipe100InputMeter.csv'], responseCode: [200, 400], - responseString: ['

SUCCESS

Successfully inserted the meters.', '

FAILURE

CSVPipelineError: Failed to upload meters due to internal OED Error: Meter name provided ("pipe100") in request with update for meters but more than one meter in CSV so not processing'], + responseString: ['

SUCCESS

Successfully inserted the meters.', '

FAILURE

CSVPipelineError: Failed to upload meters due to internal OED Error: Meter identifier provided ("pipe100") in request with update for meters but more than one meter in CSV so not processing'], metersUploaded: [ new Meter( undefined, // id @@ -648,7 +700,7 @@ const testMeters = { }, pipe101: { description: 'Second meter with same name so fails but first meter exists', - chaiRequest: [CHAI_METERS_REQUEST + ".field('gzip', BooleanTypesJS.false).field('headerRow',BooleanTypesJS.true)"], + chaiRequest: [CHAI_METERS_REQUEST + ".field('gzip', false).field('headerRow',true)"], fileName: ['pipe101InputMeter.csv'], responseCode: [400], responseString: ['

FAILURE

CSVPipelineError: Failed to upload meters due to internal OED Error: Meter name of "pipe101" got database error of: duplicate key value violates unique constraint "meters_name_key"'], @@ -687,18 +739,18 @@ const testMeters = { }, pipe102: { description: 'Update meter where name does not exist so fails', - chaiRequest: [CHAI_METERS_REQUEST + ".field('gzip', BooleanTypesJS.false).field('headerRow',BooleanTypesJS.true).field('update',BooleanTypesJS.true)"], + chaiRequest: [CHAI_METERS_REQUEST + ".field('gzip', false).field('headerRow',true).field('update',true)"], fileName: ['pipe102InputMeter.csv'], responseCode: [400], - responseString: ['

FAILURE

CSVPipelineError: Failed to upload meters due to internal OED Error: Meter name of "pipe102" does not seem to exist with update for meters and got DB error of: No data returned from the query.'], + responseString: ['

FAILURE

CSVPipelineError: Failed to upload meters due to internal OED Error: Meter identifier of "pipe102" does not seem to exist with update for meters and got DB error of: No data returned from the query.'], metersUploaded: [] }, pipe103: { - description: 'Uploading meters using Email. First succeeds then the second meter upload fails because it incorrectly provides meter name', - chaiRequest: [CHAI_METERS_REQUEST_EMAIL + ".field('gzip', BooleanTypesJS.false).field('headerRow',BooleanTypesJS.true)", CHAI_METERS_REQUEST_EMAIL + ".field('gzip', BooleanTypesJS.false).field('update',BooleanTypesJS.true).field('meterName', 'pipe100').field('headerRow',BooleanTypesJS.true)"], + description: 'Uploading meters using Email. First succeeds then the second meter upload fails because it incorrectly provides meter identifier', + chaiRequest: [CHAI_METERS_REQUEST_EMAIL + ".field('gzip', false).field('headerRow',true)", CHAI_METERS_REQUEST_EMAIL + ".field('gzip', false).field('update',true).field('meterIdentifier', 'pipe100').field('headerRow',true)"], fileName: ['pipe100InputMeter.csv', 'pipe100InputMeter.csv'], responseCode: [200, 400], - responseString: ['

SUCCESS

Successfully inserted the meters.', '

FAILURE

CSVPipelineError: Failed to upload meters due to internal OED Error: Meter name provided ("pipe100") in request with update for meters but more than one meter in CSV so not processing'], + responseString: ['

SUCCESS

Successfully inserted the meters.', '

FAILURE

CSVPipelineError: Failed to upload meters due to internal OED Error: Meter identifier provided ("pipe100") in request with update for meters but more than one meter in CSV so not processing'], metersUploaded: [ new Meter( undefined, // id @@ -777,21 +829,21 @@ for (let fileKey in testMeters) { name: 'Electric_Utility', identifier: '', unitRepresent: Unit.unitRepresentType.QUANTITY, - secInRate: 3600, - typeOfUnit: Unit.unitType.METER, + secInRate: 3600, + typeOfUnit: Unit.unitType.METER, suffix: '', - displayable: Unit.displayableType.NONE, + displayable: Unit.displayableType.NONE, preferredDisplay: false, note: 'for teting' }, - { + { name: 'kWh', identifier: '', unitRepresent: Unit.unitRepresentType.QUANTITY, - secInRate: 3600, - typeOfUnit: Unit.unitType.UNIT, + secInRate: 3600, + typeOfUnit: Unit.unitType.UNIT, suffix: '', - displayable: Unit.displayableType.ALL, + displayable: Unit.displayableType.ALL, preferredDisplay: true, note: 'for testing' } diff --git a/src/server/test/web/meters.js b/src/server/test/web/meters.js index 5a4a2203d..ea3f137ad 100644 --- a/src/server/test/web/meters.js +++ b/src/server/test/web/meters.js @@ -136,45 +136,50 @@ mocha.describe('meters API', () => { expect(res.body).to.have.lengthOf(4); expectMetersToBeEquivalent(res.body, 4, false, unitId); }); - mocha.describe('Admin role:', () => { - let token; - // Since this .before is in the middle of tests, it should not have issues as - // documented in usersTest.js. - mocha.before(async () => { - let res = await chai.request(app).post('/api/login') - .send({ username: testUser.username, password: testUser.password }); - token = res.body.token; - }); - mocha.it('returns all meters', async () => { - const conn = testDB.getConnection(); - await new Meter(undefined, 'Meter 1', '1.1.1.1', true, true, Meter.type.MAMAC, '+01', gps, - 'Identified 1', 'notes 1', 10.0, true, true, '01:01:25', '05:05:05', 5.1, 7.3, 1, 'increasing', false, - 1.0, '0001-01-01 23:59:59', '2020-07-02 01:00:10', '2020-03-05 13:15:13', unitId, unitId, - Unit.areaUnitType.METERS, '13:57:19').insert(conn); - await new Meter(undefined, 'Meter 2', '1.1.1.1', true, true, Meter.type.MAMAC, '+02', gps, - 'Identified 2', 'notes 2', 20.0, true, true, '01:01:25', '05:05:05', 5.1, 7.3, 1, 'increasing', false, - 2.0, '0001-01-01 23:59:59', '2020-07-02 01:00:10', '2020-03-05 13:15:13', unitId, unitId, - Unit.areaUnitType.METERS, '13:57:19').insert(conn); - await new Meter(undefined, 'Meter 3', '1.1.1.1', true, true, Meter.type.MAMAC, '+03', gps, - 'Identified 3', 'notes 3', 30.0, true, true, '01:01:25', '05:05:05', 5.1, 7.3, 1, 'increasing', false, - 3.0, '0001-01-01 23:59:59', '2020-07-02 01:00:10', '2020-03-05 13:15:13', unitId, unitId, - Unit.areaUnitType.METERS, '13:57:19').insert(conn); - await new Meter(undefined, 'Not Visible', '1.1.1.1', true, false, Meter.type.MAMAC, '+04', gps, - 'Identified 4', 'notes 4', 40.0, true, true, '01:01:25', '05:05:05', 5.1, 7.3, 1, 'increasing', false, - 4.0, '0001-01-01 23:59:59', '2020-07-02 01:00:10', '2020-03-05 13:15:13', unitId, unitId, - Unit.areaUnitType.METERS, '13:57:19').insert(conn); - const res = await chai.request(app).get('/api/meters').set('token', token); - expect(res).to.have.status(200); - expect(res).to.be.json; - expect(res.body).to.have.lengthOf(4); - expectMetersToBeEquivalent(res.body, 4, true, unitId); - }); + mocha.describe('Admin role & CSV role:', () => { + for (const role in User.role) { + if (User.role[role] !== User.role.OBVIUS && User.role[role] !== User.role.EXPORT) { + let token; + // Since this .before is in the middle of tests, it should not have issues as + // documented in usersTest.js. + mocha.before(async () => { + let res = await chai.request(app).post('/api/login') + .send({ username: testUser.username, password: testUser.password }); + token = res.body.token; + }); + mocha.it('returns all meters', async () => { + const conn = testDB.getConnection(); + await new Meter(undefined, 'Meter 1', '1.1.1.1', true, true, Meter.type.MAMAC, '+01', gps, + 'Identified 1', 'notes 1', 10.0, true, true, '01:01:25', '05:05:05', 5.1, 7.3, 1, 'increasing', false, + 1.0, '0001-01-01 23:59:59', '2020-07-02 01:00:10', '2020-03-05 13:15:13', unitId, unitId, + Unit.areaUnitType.METERS, '13:57:19').insert(conn); + await new Meter(undefined, 'Meter 2', '1.1.1.1', true, true, Meter.type.MAMAC, '+02', gps, + 'Identified 2', 'notes 2', 20.0, true, true, '01:01:25', '05:05:05', 5.1, 7.3, 1, 'increasing', false, + 2.0, '0001-01-01 23:59:59', '2020-07-02 01:00:10', '2020-03-05 13:15:13', unitId, unitId, + Unit.areaUnitType.METERS, '13:57:19').insert(conn); + await new Meter(undefined, 'Meter 3', '1.1.1.1', true, true, Meter.type.MAMAC, '+03', gps, + 'Identified 3', 'notes 3', 30.0, true, true, '01:01:25', '05:05:05', 5.1, 7.3, 1, 'increasing', false, + 3.0, '0001-01-01 23:59:59', '2020-07-02 01:00:10', '2020-03-05 13:15:13', unitId, unitId, + Unit.areaUnitType.METERS, '13:57:19').insert(conn); + await new Meter(undefined, 'Not Visible', '1.1.1.1', true, false, Meter.type.MAMAC, '+04', gps, + 'Identified 4', 'notes 4', 40.0, true, true, '01:01:25', '05:05:05', 5.1, 7.3, 1, 'increasing', false, + 4.0, '0001-01-01 23:59:59', '2020-07-02 01:00:10', '2020-03-05 13:15:13', unitId, unitId, + Unit.areaUnitType.METERS, '13:57:19').insert(conn); + + const res = await chai.request(app).get('/api/meters').set('token', token); + expect(res).to.have.status(200); + expect(res).to.be.json; + expect(res.body).to.have.lengthOf(4); + expectMetersToBeEquivalent(res.body, 4, true, unitId); + }); + } + } }); - mocha.describe('Non-Admin role:', () => { + mocha.describe('Export role & Obvius role:', () => { for (const role in User.role) { - if (User.role[role] !== User.role.ADMIN) { + if (User.role[role] !== User.role.ADMIN && User.role[role] !== User.role.CSV) { let token; mocha.beforeEach(async () => { // insert test user diff --git a/src/server/test/web/readingsBarGroupFlow.js b/src/server/test/web/readingsBarGroupFlow.js index 4544fc664..2a9965740 100644 --- a/src/server/test/web/readingsBarGroupFlow.js +++ b/src/server/test/web/readingsBarGroupFlow.js @@ -9,26 +9,199 @@ const { chai, mocha, app } = require('../common'); const Unit = require('../../models/Unit'); -// const { prepareTest, -// parseExpectedCsv, -// createTimeString, -// expectReadingToEqualExpected, -// getUnitId, -// ETERNITY, -// METER_ID, -// unitDatakWh, -// conversionDatakWh, -// meterDatakWh } = require('../../util/readingsUtils'); +const { prepareTest, + parseExpectedCsv, + expectReadingToEqualExpected, + getUnitId, + ETERNITY, + METER_ID, + GROUP_ID } = require('../../util/readingsUtils'); mocha.describe('readings API', () => { mocha.describe('readings test, test if data returned by API is as expected', () => { mocha.describe('for bar charts', () => { - mocha.describe('for quantity groups', () => { - - // Add BG15 here - - // Add BG16 here + mocha.describe('for flow groups', () => { + mocha.it('BG15: should have daily points for 15 + 20 minute reading intervals and flow units with +-inf start/end time & kW as kW', async () =>{ + const unitDatakW = [ + { + // u4 + name: 'kW', + identifier: '', + unitRepresent: Unit.unitRepresentType.FLOW, + secInRate: 3600, + typeOfUnit: Unit.unitType.UNIT, + suffix: '', + displayable: Unit.displayableType.ALL, + preferredDisplay: true, + note: 'kilowatts' + }, + { + // u5 + name: 'Electric', + identifier: '', + unitRepresent: Unit.unitRepresentType.FLOW, + secInRate: 3600, + typeOfUnit: Unit.unitType.METER, + suffix: '', + displayable: Unit.displayableType.NONE, + preferredDisplay: false, + note: 'special unit' + }, + ]; + const conversionDatakW = [ + { + // c4 + sourceName: 'Electric', + destinationName: 'kW', + bidirectional: false, + slope: 1, + intercept: 0, + note: 'Electric → kW' + } + ]; + const meterDatakWGroups = [ + { + name: 'meterDatakWGroups', + unit: 'Electric', + defaultGraphicUnit: 'kW', + displayable: true, + gps: undefined, + note: 'special meter', + file: 'test/web/readingsData/readings_ri_15_days_75.csv', + deleteFile: false, + readingFrequency: '15 minutes', + id: METER_ID + }, + { + name: 'meterDatakWOther', + unit: 'Electric', + defaultGraphicUnit: 'kW', + displayable: true, + gps: undefined, + note: 'special meter', + file: 'test/web/readingsData/readings_ri_20_days_75.csv', + deleteFile: false, + readingFrequency: '20 minutes', + id: (METER_ID + 1) + } + ]; + const groupDatakW = [ + { + id: GROUP_ID, + name: 'meterDatakWGroups + meterDatakWOther', + displayable: true, + note: 'special group', + defaultGraphicUnit: 'kW', + childMeters: ['meterDatakWGroups', 'meterDatakWOther'], + childGroups: [], + } + ] + //load data into database + await prepareTest(unitDatakW, conversionDatakW, meterDatakWGroups, groupDatakW); + //get unit ID since the DB could use any value. + const unitId = await getUnitId('kW'); + // Load the expected response data from the corresponding csv file + const expected = await parseExpectedCsv('src/server/test/web/readingsData/expected_bar_group_ri_15-20_mu_kW_gu_kW_st_-inf_et_inf_bd_13.csv'); + // Create a request to the API for unbounded reading times and save the response + const res = await chai.request(app).get(`/api/unitReadings/bar/groups/${GROUP_ID}`) + .query({ + timeInterval: ETERNITY.toString(), + barWidthDays: '13', + graphicUnitId: unitId }); + // Check that the API reading is equal to what it is expected to equal + expectReadingToEqualExpected(res, expected, GROUP_ID); + }); + mocha.it('BG16: should have daily points for 15 + 20 minute reading intervals and flow units with +-inf start/end time & thing as thing where rate is 36', async () => { + const unitDataThing = [ + { + // u14 + name: "Thing_36", + identifier: "", + unitRepresent: Unit.unitRepresentType.FLOW, + secInRate: 36, + typeOfUnit: Unit.unitType.METER, + suffix: "", + displayable: Unit.displayableType.NONE, + preferredDisplay: false, + note: "special unit" + }, + { + // u15 + name: "thing unit", + identifier: "", + unitRepresent: Unit.unitRepresentType.FLOW, + secInRate: 3600, + typeOfUnit: Unit.unitType.UNIT, + suffix: "", + displayable: Unit.displayableType.ALL, + preferredDisplay: false, + note: "special unit" + } + ]; + const conversionDataThing = [ + { + // c15 + sourceName: "Thing_36", + destinationName: "thing unit", + bidirectional: false, + slope: 1, + intercept: 0, + note: "Thing_36 → thing unit" + } + ]; + const meterDataThingGroups = [ + { + name: 'Thing_36 thing unit', + unit: 'Thing_36', + defaultGraphicUnit: 'thing unit', + displayable: true, + gps: undefined, + note: 'special meter', + file: 'test/web/readingsData/readings_ri_15_days_75.csv', + deleteFile: false, + readingFrequency: '15 minutes', + id: METER_ID + }, + { + name: 'Thing_36 Other', + unit: 'Thing_36', + defaultGraphicUnit: 'thing unit', + displayable: true, + gps: undefined, + note: 'special meter', + file: 'test/web/readingsData/readings_ri_20_days_75.csv', + deleteFile: false, + readingFrequency: '20 minutes', + id: (METER_ID + 1) + } + ]; + const groupThing = [ + { + id: GROUP_ID, + name: 'Thing_36 thing unit + Thing_36 Other', + displayable: true, + note: 'special group', + defaultGraphicUnit: 'thing unit', + childMeters: ['Thing_36 thing unit', 'Thing_36 Other'], + childGroups: [], + } + ] + //load data into database + await prepareTest(unitDataThing, conversionDataThing, meterDataThingGroups, groupThing); + //get unit ID since the DB could use any value. + const unitId = await getUnitId('thing unit'); + // Load the expected response data from the corresponding csv file + const expected = await parseExpectedCsv('src/server/test/web/readingsData/expected_bar_group_ri_15-20_mu_Thing36_gu_thing_st_-inf_et_inf_bd_13.csv'); + // Create a request to the API for unbounded reading times and save the response + const res = await chai.request(app).get(`/api/unitReadings/bar/groups/${GROUP_ID}`) + .query({ + timeInterval: ETERNITY.toString(), + barWidthDays: '13', + graphicUnitId: unitId }); + // Check that the API reading is equal to what it is expected to equal + expectReadingToEqualExpected(res, expected, GROUP_ID); + }); }); }); }); diff --git a/src/server/test/web/readingsBarGroupQuantity.js b/src/server/test/web/readingsBarGroupQuantity.js index 776443704..dd8e8f2bb 100644 --- a/src/server/test/web/readingsBarGroupQuantity.js +++ b/src/server/test/web/readingsBarGroupQuantity.js @@ -27,8 +27,7 @@ mocha.describe('readings API', () => { mocha.describe('readings test, test if data returned by API is as expected', () => { mocha.describe('for bar charts', () => { mocha.describe('for quantity groups', () => { - - mocha.it('BG1: 1 day bars for 15 + 20 minute reading intervals and quantity units with +-inf start/end time & kWh as kWh', async () =>{ + mocha.it('BG1: 1 day bars for 15 + 20 minute reading intervals and quantity units with +-inf start/end time & kWh as kWh', async () => { //loads data into database await prepareTest(unitDatakWh, conversionDatakWh, meterDatakWhGroups, groupDatakWh) //gets unit ID @@ -37,18 +36,35 @@ mocha.describe('readings API', () => { const expected = await parseExpectedCsv('src/server/test/web/readingsData/expected_bar_group_ri_15-20_mu_kWh_gu_kWh_st_-inf_et_inf_bd_1.csv') const res = await chai.request(app).get(`/api/unitReadings/bar/groups/${GROUP_ID}`) - .query({ - timeInterval: ETERNITY.toString(), + .query({ + timeInterval: ETERNITY.toString(), barWidthDays: '1', - graphicUnitId: unitId }); + graphicUnitId: unitId + }); // Check that the API reading is equal to what it is expected to equal expectReadingToEqualExpected(res, expected, GROUP_ID); }); - // Add BG2 here + mocha.it('BG2: 7 day bars for 15 + 20 minute reading intervals and quantity units with +-inf start/end time & kWh as kWh', async () => { + //loads data into database + await prepareTest(unitDatakWh, conversionDatakWh, meterDatakWhGroups, groupDatakWh) + //gets unit ID + const unitId = await getUnitId('kWh'); + //load data from mcsv file + const expected = await parseExpectedCsv('src/server/test/web/readingsData/expected_bar_group_ri_15-20_mu_kWh_gu_kWh_st_-inf_et_inf_bd_7.csv') + const res = await chai.request(app).get(`/api/unitReadings/bar/groups/${GROUP_ID}`) + .query({ + timeInterval: ETERNITY.toString(), + barWidthDays: '7', + graphicUnitId: unitId + }); + // Check that the API reading is equal to what it is expected to equal + expectReadingToEqualExpected(res, expected, GROUP_ID); + }); - mocha.it('BG3: 28 day bars for 15 + 20 minute reading intervals and quantity units with +-inf start/end time & kWh as kWh', async () =>{ + + mocha.it('BG3: 28 day bars for 15 + 20 minute reading intervals and quantity units with +-inf start/end time & kWh as kWh', async () => { //load data into database await prepareTest(unitDatakWh, conversionDatakWh, meterDatakWhGroups, groupDatakWh); //get unit ID since the DB could use any value. @@ -57,34 +73,52 @@ mocha.describe('readings API', () => { const expected = await parseExpectedCsv('src/server/test/web/readingsData/expected_bar_group_ri_15-20_mu_kWh_gu_kWh_st_-inf_et_inf_bd_28.csv'); // Create a request to the API for unbounded reading times and save the response const res = await chai.request(app).get(`/api/unitReadings/bar/groups/${GROUP_ID}`) - .query({ - timeInterval: ETERNITY.toString(), + .query({ + timeInterval: ETERNITY.toString(), barWidthDays: '28', - graphicUnitId: unitId }); + graphicUnitId: unitId + }); // Check that the API reading is equal to what it is expected to equal expectReadingToEqualExpected(res, expected, GROUP_ID); }); - mocha.it('BG4: 13 day bars for 15 + 20 minute reading intervals and quantity units with +-inf start/end time & kWh as kWh', async () =>{ - //load data into database - await prepareTest(unitDatakWh, conversionDatakWh, meterDatakWhGroups, groupDatakWh); - //get unit ID since the DB could use any value. - const unitId = await getUnitId('kWh'); - // Load the expected response data from the corresponding csv file - const expected = await parseExpectedCsv('src/server/test/web/readingsData/expected_bar_group_ri_15-20_mu_kWh_gu_kWh_st_-inf_et_inf_bd_13.csv'); - // Create a request to the API for unbounded reading times and save the response - const res = await chai.request(app).get(`/api/unitReadings/bar/groups/${GROUP_ID}`) - .query({ - timeInterval: ETERNITY.toString(), - barWidthDays: '13', - graphicUnitId: unitId }); - // Check that the API reading is equal to what it is expected to equal - expectReadingToEqualExpected(res, expected, GROUP_ID); + mocha.it('BG4: 13 day bars for 15 + 20 minute reading intervals and quantity units with +-inf start/end time & kWh as kWh', async () => { + //load data into database + await prepareTest(unitDatakWh, conversionDatakWh, meterDatakWhGroups, groupDatakWh); + //get unit ID since the DB could use any value. + const unitId = await getUnitId('kWh'); + // Load the expected response data from the corresponding csv file + const expected = await parseExpectedCsv('src/server/test/web/readingsData/expected_bar_group_ri_15-20_mu_kWh_gu_kWh_st_-inf_et_inf_bd_13.csv'); + // Create a request to the API for unbounded reading times and save the response + const res = await chai.request(app).get(`/api/unitReadings/bar/groups/${GROUP_ID}`) + .query({ + timeInterval: ETERNITY.toString(), + barWidthDays: '13', + graphicUnitId: unitId + }); + // Check that the API reading is equal to what it is expected to equal + expectReadingToEqualExpected(res, expected, GROUP_ID); }); - // Add BG5 here + mocha.it('BG5: 75 day bars for 15 + 20 minute reading intervals and quantity units with +-inf start/end time & kWh as kWh', async () => { + //load data into database + await prepareTest(unitDatakWh, conversionDatakWh, meterDatakWhGroups, groupDatakWh); + //get unit ID since the DB could use any value. + const unitId = await getUnitId('kWh'); + // Load the expected response data from the corresponding csv file + const expected = await parseExpectedCsv('src/server/test/web/readingsData/expected_bar_group_ri_15-20_mu_kWh_gu_kWh_st_-inf_et_inf_bd_75.csv'); + // Create a request to the API for unbounded reading times and save the response + const res = await chai.request(app).get(`/api/unitReadings/bar/groups/${GROUP_ID}`) + .query({ + timeInterval: ETERNITY.toString(), + barWidthDays: '75', + graphicUnitId: unitId + }); + // Check that the API reading is equal to what it is expected to equal + expectReadingToEqualExpected(res, expected, GROUP_ID); + }); - mocha.it('BG6: 76 day bars (no values) for 15 + 20 minute reading intervals and quantity units with +-inf start/end time & kWh as kWh', async () =>{ + mocha.it('BG6: 76 day bars (no values) for 15 + 20 minute reading intervals and quantity units with +-inf start/end time & kWh as kWh', async () => { //load data into database await prepareTest(unitDatakWh, conversionDatakWh, meterDatakWhGroups, groupDatakWh); //get unit ID since the DB could use any value. @@ -93,24 +127,338 @@ mocha.describe('readings API', () => { const expected = await parseExpectedCsv('src/server/test/web/readingsData/expected_bar_group_ri_15-20_mu_kWh_gu_kWh_st_-inf_et_inf_bd_76.csv'); // Create a request to the API for unbounded reading times and save the response const res = await chai.request(app).get(`/api/unitReadings/bar/groups/${GROUP_ID}`) - .query({ - timeInterval: ETERNITY.toString(), + .query({ + timeInterval: ETERNITY.toString(), barWidthDays: '76', - graphicUnitId: unitId }); + graphicUnitId: unitId + }); // Check that the API reading is equal to what it is expected to equal expectReadingToEqualExpected(res, expected, GROUP_ID); }); - // Add BG7 here - // Add BG8 here + mocha.it("BG7: 13 day bars for 15 + 20 minute reading intervals and quantity units with reduced, partial days & kWh as kWh", async () => { + // Load data into the database + await prepareTest(unitDatakWh, conversionDatakWh, meterDatakWhGroups, groupDatakWh); + // Get unit ID for API call + const unitId = await getUnitId('kWh'); + // Retrieve expected values from CSV file + const expected = await parseExpectedCsv('src/server/test/web/readingsData/expected_bar_group_ri_15-20_mu_kWh_gu_kWh_st_2022-08-20%07#25#35_et_2022-10-28%13#18#28_bd_13.csv'); + // Make API call and retrieve data + const res = await chai.request(app).get(`/api/unitReadings/bar/groups/${GROUP_ID}`) + .query({ + timeInterval: createTimeString('2022-08-20', '07:25:35', '2022-10-28', '13:18:28'), + barWidthDays: '13', + graphicUnitId: unitId + }); + // Compare API returned data with expected values + expectReadingToEqualExpected(res, expected, GROUP_ID); + }); + + + + mocha.it('BG8: 1 day bars for 15 + 20 minute reading intervals and quantity units with +-inf start/end time & kWh as MJ', async () => { + //Define arrays of data for units, conversions and test meter(s)/group + const unitData = unitDatakWh.concat([ + { + //u3 + name: 'MJ', + identifier: 'megaJoules', + unitRepresent: Unit.unitRepresentType.QUANTITY, + secInRate: 3600, + typeOfUnit: Unit.unitType.UNIT, + suffix: '', + displayable: Unit.displayableType.ALL, + preferredDisplay: false, + note: 'MJ' + } + ]); + const conversionData = conversionDatakWh.concat([ + { + //c2 + sourceName: 'kWh', + destinationName: 'MJ', + bidirectional: true, + slope: 3.6, + intercept: 0, + note: 'kWh → MJ' + } + ]); + //load data arrays into database + await prepareTest(unitData, conversionData, meterDatakWhGroups, groupDatakWh); + //get unit ID for API call + const unitId = await getUnitId('MJ'); + //get array of values from csv file + const expected = await parseExpectedCsv('src/server/test/web/readingsData/expected_bar_group_ri_15-20_mu_kWh_gu_MJ_st_-inf_et_inf_bd_1.csv'); + //get data from API call + const res = await chai.request(app).get(`/api/unitReadings/bar/groups/${GROUP_ID}`) + .query({ + timeInterval: ETERNITY.toString(), + barWidthDays: '1', + graphicUnitId: unitId + }); + //compare readings data to expected + expectReadingToEqualExpected(res, expected, GROUP_ID); + + }); // Add BG9 here + mocha.it('BG9: 1 day bars for 15 + 20 minute reading intervals and quantity units with +-inf start/end time & kWh as MJ reverse conversion', async () => { - // Add BG10 here + const unitData = unitDatakWh.concat([ + { + // u3 + name: 'MJ', + identifier: 'megaJoules', + unitRepresent: Unit.unitRepresentType.QUANTITY, + secInRate: 3600, + typeOfUnit: Unit.unitType.UNIT, + suffix: '', + displayable: Unit.displayableType.ALL, + preferredDisplay: true, + note: 'MJ' + } + ]); - // Add BG11 here + const conversionData = conversionDatakWh.concat([ - // Add BG12 here + { + // c6 + sourceName: 'MJ', + destinationName: 'kWh', + bidirectional: true, + slope: 1 / 3.6, + intercept: 0, + note: 'MJ → KWh' + } + ]); + const meterData = [ + { + name: 'Electric_Utility MJ Reverse', + unit: 'Electric_Utility', + displayable: true, + gps: undefined, + defaultGraphicUnit: 'MJ', + note: 'special meter', + file: 'test/web/readingsData/readings_ri_15_days_75.csv', + deleteFile: false, + readingFrequency: '15 minutes', + id: METER_ID + } + ]; + + // Load data into the database + await prepareTest(unitData, conversionData, meterDatakWhGroups, groupDatakWh, meterData); + + // Get unit ID for MJ + const unitId = await getUnitId('MJ'); + + // Load expected response data from the corresponding CSV file + const expected = await parseExpectedCsv('src/server/test/web/readingsData/expected_bar_group_ri_15-20_mu_kWh_gu_MJ_st_-inf_et_inf_bd_1.csv'); + + // Create a request to the API for unbounded reading times and save the response + const result = await chai.request(app).get(`/api/unitReadings/bar/groups/${GROUP_ID}`) + .query({ + timeInterval: ETERNITY.toString(), + barWidthDays: '1', + graphicUnitId: unitId + }); + + // Check that the API reading matches the expected data from the CSV + expectReadingToEqualExpected(result, expected, GROUP_ID); + }); + + mocha.it('BG10: 1 day bars for 15 + 20 minute reading intervals and quantity units with +-inf start/end time & kWh as BTU', async () => { + const unitData = unitDatakWh.concat([ + { + // u3 + name: 'MJ', + identifier: 'megaJoules', + unitRepresent: Unit.unitRepresentType.QUANTITY, + secInRate: 3600, + typeOfUnit: Unit.unitType.UNIT, + suffix: '', + displayable: Unit.displayableType.ALL, + preferredDisplay: false, + note: 'MJ' + }, + { + // u16 + name: 'BTU', + identifier: '', + unitRepresent: Unit.unitRepresentType.QUANTITY, + secInRate: 3600, + typeOfUnit: Unit.unitType.UNIT, + suffix: '', + displayable: Unit.displayableType.ALL, + preferredDisplay: true, + note: 'OED created standard unit' + } + ]); + + const conversionData = conversionDatakWh.concat([ + { + // c2 + sourceName: 'kWh', + destinationName: 'MJ', + bidirectional: true, + slope: 3.6, + intercept: 0, + note: 'kWh → MJ' + }, + { + // c3 + sourceName: 'MJ', + destinationName: 'BTU', + bidirectional: true, + slope: 947.8, + intercept: 0, + note: 'MJ → BTU' + } + ]); + + //load data into database + await prepareTest(unitData, conversionData, meterDatakWhGroups, groupDatakWh); + //get unit ID since the DB could use any value. + const unitId = await getUnitId('BTU'); + // Load the expected response data from the corresponding csv file + const expected = await parseExpectedCsv('src/server/test/web/readingsData/expected_bar_group_ri_15-20_mu_kWh_gu_BTU_st_-inf_et_inf_bd_1.csv'); + // Create a request to the API for unbounded reading times and save the response + const res = await chai.request(app).get(`/api/unitReadings/bar/groups/${GROUP_ID}`) + .query({ + timeInterval: ETERNITY.toString(), + barWidthDays: '1', + graphicUnitId: unitId + }); + // Check that the API reading is equal to what it is expected to equal + expectReadingToEqualExpected(res, expected, GROUP_ID); + }); + + mocha.it('BG11: 1 day bars for 15 + 20 minute reading intervals and quantity units with +-inf start/end time & kWh as BTU reverse conversion', async () => { + //load data into database + const unitData = unitDatakWh.concat([ + { + // u3 + name: 'MJ', + identifier: 'megaJoules', + unitRepresent: Unit.unitRepresentType.QUANTITY, + secInRate: 3600, + typeOfUnit: Unit.unitType.UNIT, + suffix: '', + displayable: Unit.displayableType.ALL, + preferredDisplay: false, + note: 'MJ' + }, + { + // u16 + name: 'BTU', + identifier: '', + unitRepresent: Unit.unitRepresentType.QUANTITY, + secInRate: 3600, + typeOfUnit: Unit.unitType.UNIT, + suffix: '', + displayable: Unit.displayableType.ALL, + preferredDisplay: true, + note: 'OED created standard unit' + } + ]); + const conversionData = conversionDatakWh.concat([ + { + // c6 + sourceName: 'MJ', + destinationName: 'kWh', + bidirectional: true, + slope: 1 / 3.6, + intercept: 0, + note: 'MJ → kWh' + }, + { + // c3 + sourceName: 'MJ', + destinationName: 'BTU', + bidirectional: true, + slope: 947.8, + intercept: 0, + note: 'MJ → BTU' + } + ]); + await prepareTest(unitData, conversionData, meterDatakWhGroups, groupDatakWh); + //get unit ID since the DB could use any value. + const unitId = await getUnitId('BTU'); + //Load the expected response data from the corresponding csv file + const expected = await parseExpectedCsv('src/server/test/web/readingsData/expected_bar_group_ri_15-20_mu_kWh_gu_BTU_st_-inf_et_inf_bd_1.csv'); + // Create a request to the API for unbounded reading times and save the response + const res = await chai.request(app).get(`/api/unitReadings/bar/groups/${GROUP_ID}`) + .query({ + timeInterval: ETERNITY.toString(), + barWidthDays: '1', + graphicUnitId: unitId + }); + // Check that the API reading is equal to what it is expected to equal + expectReadingToEqualExpected(res, expected, GROUP_ID); + }); + mocha.it('BG12: 1 day bars for 15 + 20 minute reading intervals and quantity units with +-inf start/end time & kWh as kg of CO2', async () => { + const unitData = unitDatakWh.concat([ + { + // u10 + name: 'kg', + identifier: '', + unitRepresent: Unit.unitRepresentType.QUANTITY, + secInRate: 3600, + typeOfUnit: Unit.unitType.UNIT, + suffix: '', + displayable: Unit.displayableType.ALL, + preferredDisplay: false, + note: 'OED created standard unit' + }, + { + // u12 + name: 'kg CO₂', + identifier: '', + unitRepresent: Unit.unitRepresentType.QUANTITY, + secInRate: 3600, + typeOfUnit: Unit.unitType.UNIT, + suffix: 'CO₂', + displayable: Unit.displayableType.ALL, + preferredDisplay: false, + note: 'special unit' + } + ]); + const conversionData = conversionDatakWh.concat([ + { + //c11 + sourceName: 'Electric_Utility', + destinationName: 'kg CO₂', + bidirectional: false, + slope: 0.709, + intercept: 0, + note: 'Electric_Utility → kg CO₂' + }, + { + //c12 + sourceName: 'kg CO₂', + destinationName: 'kg', + bidirectional: false, + slope: 1, + intercept: 0, + note: 'CO₂ → kg' + } + ]); + //load data into database + await prepareTest(unitData, conversionData, meterDatakWhGroups, groupDatakWh); + // Get unit ID for 'kg CO₂' + const unitId = await getUnitId('kg of CO₂'); + // Load expected response data from the corresponding CSV file + const expected = await parseExpectedCsv('src/server/test/web/readingsData/expected_bar_group_ri_15-20_mu_kWh_gu_kgCO2_st_-inf_et_inf_bd_1.csv'); + // Create a request to the API for unbounded reading times and save the response + const res = await chai.request(app).get(`/api/unitReadings/bar/groups/${GROUP_ID}`) + .query({ + timeInterval: ETERNITY.toString(), + barWidthDays: '1', + graphicUnitId: unitId + }); + // Check that the API reading is equal to what it is expected to equal + expectReadingToEqualExpected(res, expected, GROUP_ID); + }); // Add BG13 here @@ -120,4 +468,3 @@ mocha.describe('readings API', () => { }); }); }); - diff --git a/src/server/test/web/readingsBarMeterFlow.js b/src/server/test/web/readingsBarMeterFlow.js index adafc8175..e3930f238 100644 --- a/src/server/test/web/readingsBarMeterFlow.js +++ b/src/server/test/web/readingsBarMeterFlow.js @@ -95,8 +95,74 @@ mocha.describe('readings API', () => { expectReadingToEqualExpected(res, expected); }); - // Add B16 here - + mocha.it('B16: should have daily points for 15 minute reading intervals and flow units with +-inf start/end time & thing as thing where rate is 36', async () => { + const unitData = [ + { + // u14 + name: "Thing_36", + identifier: "", + unitRepresent: Unit.unitRepresentType.FLOW, + secInRate: 36, + typeOfUnit: Unit.unitType.METER, + suffix: "", + displayable: Unit.displayableType.NONE, + preferredDisplay: false, + note: "special unit" + }, + { + // u15 + name: "thing unit", + identifier: "", + unitRepresent: Unit.unitRepresentType.FLOW, + secInRate: 3600, + typeOfUnit: Unit.unitType.UNIT, + suffix: "", + displayable: Unit.displayableType.ALL, + preferredDisplay: false, + note: "special unit" + } + ]; + const conversionData = [ + { + // c15 + sourceName: "Thing_36", + destinationName: "thing unit", + bidirectional: false, + slope: 1, + intercept: 0, + note: "Thing_36 → thing unit" + } + ]; + const meterData = [ + { + name: 'Thing_36 thing unit', + unit: 'Thing_36', + defaultGraphicUnit: 'thing unit', + displayable: true, + gps: undefined, + note: 'special meter', + file: 'test/web/readingsData/readings_ri_15_days_75.csv', + deleteFile: false, + readingFrequency: '15 minutes', + id: METER_ID + } + ]; + // Load the data into the database + await prepareTest(unitData, conversionData, meterData); + // Get the unit ID since the DB could use any value. + const unitId = await getUnitId('thing unit'); + // Load the expected response data from the corresponding csv file + const expected = await parseExpectedCsv('src/server/test/web/readingsData/expected_bar_ri_15_mu_Thing36_gu_thing_st_-inf_et_inf_bd_13.csv'); + // Create a request to the API for unbounded reading times and save the response + const res = await chai.request(app).get(`/api/unitReadings/bar/meters/${METER_ID}`) + .query({ + timeInterval: ETERNITY.toString(), + barWidthDays: 13, + graphicUnitId: unitId + }); + // Check that the API reading is equal to what it is expected to equal + expectReadingToEqualExpected(res, expected); + }); }); }); }); diff --git a/src/server/test/web/readingsCompareGroupFlow.js b/src/server/test/web/readingsCompareGroupFlow.js index 8eeb2cf34..84293202b 100644 --- a/src/server/test/web/readingsCompareGroupFlow.js +++ b/src/server/test/web/readingsCompareGroupFlow.js @@ -7,22 +7,212 @@ See: https://github.com/OpenEnergyDashboard/DesignDocs/blob/main/testing/testing.md for information. */ const { chai, mocha, app } = require('../common'); +const Unit = require('../../models/Unit'); const { prepareTest, expectCompareToEqualExpected, getUnitId, + GROUP_ID, METER_ID, - unitDatakWh, - conversionDatakWh, - meterDatakWh } = require('../../util/readingsUtils'); + unitDatakWh, + conversionDatakWh + } = require('../../util/readingsUtils'); mocha.describe('readings API', () => { mocha.describe('readings test, test if data returned by API is as expected', () => { mocha.describe('for compare charts', () => { mocha.describe('for groups', () => { - // Add CG15 here + // Test CG15 + mocha.it('CG15: 7 day shift end 2022-10-31 17:00:00 for 15 minute reading intervals and flow units & kW as kW ', async () => { + + // unit data + const unitDatakW = [ + { + // u4 + name: 'kW', + identifier: '', + unitRepresent: Unit.unitRepresentType.FLOW, + secInRate: 3600, + typeOfUnit: Unit.unitType.UNIT, + suffix: '', + displayable: Unit.displayableType.ALL, + preferredDisplay: true, + note: 'kilowatts' + }, + { + // u5 + name: 'Electric', + identifier: '', + unitRepresent: Unit.unitRepresentType.FLOW, + secInRate: 3600, + typeOfUnit: Unit.unitType.METER, + suffix: '', + displayable: Unit.displayableType.NONE, + preferredDisplay: false, + note: 'special unit' + }, + ]; + // conversion data + const conversionDatakW = [ + { + // c4 + sourceName: 'Electric', + destinationName: 'kW', + bidirectional: false, + slope: 1, + intercept: 0, + note: 'Electric → kW' + } + ]; + // meter groups + const meterDatakWGroups = [ + { + name: 'meterDatakW', + unit: 'Electric', + defaultGraphicUnit: 'kW', + displayable: true, + gps: undefined, + note: 'special meter', + file: 'test/web/readingsData/readings_ri_15_days_75.csv', + deleteFile: false, + readingFrequency: '15 minutes', + id: METER_ID + }, + { + name: 'meterDatakWOther', + unit: 'Electric', + defaultGraphicUnit: 'kW', + displayable: true, + gps: undefined, + note: 'special meter', + file: 'test/web/readingsData/readings_ri_20_days_75.csv', + deleteFile: false, + readingFrequency: '20 minutes', + id: (METER_ID + 1) + } + ]; + // group data + const groupDatakW = [ + { + id: GROUP_ID, + name: 'meterDatakW + meterDatakWOther', + displayable: true, + note: 'special group', + defaultGraphicUnit: 'kW', + childMeters: ['meterDatakW', 'meterDatakWOther'], + childGroups: [], + } + ] + //load data into database + await prepareTest(unitDatakW, conversionDatakW, meterDatakWGroups, groupDatakW); + //get unit ID since the DB could use any value. + const unitId = await getUnitId('kW'); + const expected = [4008.97545574702, 4182.62793481036]; + //for compare, need the unitID, currentStart, currentEnd, shift + const res = await chai.request(app).get(`/api/compareReadings/groups/${GROUP_ID}`) + .query({ + curr_start: '2022-10-30 00:00:00', + curr_end: '2022-10-31 17:00:00', + shift: 'P7D', + graphicUnitId: unitId, + }); + expectCompareToEqualExpected(res, expected, GROUP_ID); + }); - // Add CG16 here + mocha.it('CG16: 7 day shift end 2022-10-31 17:00:00 for 15 minute reading intervals and flow units & thing as thing where rate is 36 ', async () => { + + const unitData = [ + { + // u14 + name: 'Thing_36', + identifier: '', + unitRepresent: Unit.unitRepresentType.FLOW, + secInRate: 36, + typeOfUnit: Unit.unitType.METER, + suffix: '', + displayable: Unit.displayableType.NONE, + preferredDisplay: false, + note: 'special unit' + }, + { + // u15 + name: 'thing unit', + identifier: '', + unitRepresent: Unit.unitRepresentType.FLOW, + secInRate: 3600, + typeOfUnit: Unit.unitType.UNIT, + suffix: '', + displayable: Unit.displayableType.ALL, + preferredDisplay: false, + note: 'special unit' + }, + ]; + const conversionData = [ + { + // c15 + sourceName: 'Thing_36', + destinationName: 'thing unit', + bidirectional: false, + slope: 1, + intercept: 0, + note: 'Thing_36 → thing unit' + } + ]; + // meter groups + const meterDatakGroups = [ + { + name: 'meterData', + unit: 'Thing_36', + defaultGraphicUnit: 'thing unit', + displayable: true, + gps: undefined, + note: 'special meter', + file: 'test/web/readingsData/readings_ri_15_days_75.csv', + deleteFile: false, + readingFrequency: '15 minutes', + id: METER_ID + }, + { + name: 'meterDataOther', + unit: 'Thing_36', + defaultGraphicUnit: 'thing unit', + displayable: true, + gps: undefined, + note: 'special meter', + file: 'test/web/readingsData/readings_ri_20_days_75.csv', + deleteFile: false, + readingFrequency: '20 minutes', + id: (METER_ID + 1) + } + ]; + // group data + const groupData = [ + { + id: GROUP_ID, + name: 'meterData + meterDataOther', + displayable: true, + note: 'special group', + defaultGraphicUnit: 'thing unit', + childMeters: ['meterData', 'meterDataOther'], + childGroups: [], + } + ] + + // load data into database + await prepareTest(unitData, conversionData, meterDatakGroups, groupData); + // Get the unit ID since the DB could use any value. + const unitId = await getUnitId('thing unit'); + const expected = [400897.545574702, 418262.793481036]; + + const res = await chai.request(app).get(`/api/compareReadings/groups/${GROUP_ID}`) + .query({ + curr_start: '2022-10-30 00:00:00', + curr_end: '2022-10-31 17:00:00', + shift: 'P7D', + graphicUnitId: unitId, + }); + expectCompareToEqualExpected(res, expected, GROUP_ID); + }); }); }); }); -}); +}); \ No newline at end of file diff --git a/src/server/test/web/readingsCompareGroupQuantity.js b/src/server/test/web/readingsCompareGroupQuantity.js index 2f3f6ceb6..41b9277de 100644 --- a/src/server/test/web/readingsCompareGroupQuantity.js +++ b/src/server/test/web/readingsCompareGroupQuantity.js @@ -14,14 +14,14 @@ const { prepareTest, unitDatakWh, conversionDatakWh, meterDatakWhGroups, - groupDatakWh } = require('../../util/readingsUtils'); + groupDatakWh } = require('../../util/readingsUtils'); mocha.describe('readings API', () => { mocha.describe('readings test, test if data returned by API is as expected', () => { mocha.describe('for compare charts', () => { mocha.describe('for groups', () => { // Test 15 minutes over all time for flow unit. - mocha.it(' 1 day shift end 2022-10-31 17:00:00 for 15 minute reading intervals and quantity units & kWh as kWh ', async () => { + mocha.it('CG1: 1 day shift end 2022-10-31 17:00:00 for 15 minute reading intervals and quantity units & kWh as kWh ', async () => { await prepareTest(unitDatakWh, conversionDatakWh, meterDatakWhGroups, groupDatakWh); // Get the unit ID since the DB could use any value. const unitId = await getUnitId('kWh'); @@ -36,8 +36,21 @@ mocha.describe('readings API', () => { }); expectCompareToEqualExpected(res, expected, GROUP_ID); }); - - // Add CG2 here + mocha.it('CG2: 7 day shift end 2022-10-31 17:00:00 for 15 minute reading intervals and quantity units & kWh as kWh ', async () => { + await prepareTest(unitDatakWh, conversionDatakWh, meterDatakWhGroups, groupDatakWh); + // Get the unit ID since the DB could use any value. + const unitId = await getUnitId('kWh'); + const expected = [14017.4841100155, 14605.4957015091]; + // for compare, need the unitID, currentStart, currentEnd, shift + const res = await chai.request(app).get(`/api/compareReadings/groups/${GROUP_ID}`) + .query({ + curr_start: '2022-10-30 00:00:00', + curr_end: '2022-10-31 17:00:00', + shift: 'P7D', + graphicUnitId: unitId + }); + expectCompareToEqualExpected(res, expected, GROUP_ID); + }); // Add CG3 here diff --git a/src/server/test/web/readingsCompareMeterFlow.js b/src/server/test/web/readingsCompareMeterFlow.js index c74447212..563825054 100644 --- a/src/server/test/web/readingsCompareMeterFlow.js +++ b/src/server/test/web/readingsCompareMeterFlow.js @@ -7,21 +7,162 @@ See: https://github.com/OpenEnergyDashboard/DesignDocs/blob/main/testing/testing.md for information. */ const { chai, mocha, app } = require('../common'); +const Unit = require('../../models/Unit'); const { prepareTest, expectCompareToEqualExpected, getUnitId, - METER_ID, - unitDatakWh, - conversionDatakWh, - meterDatakWh } = require('../../util/readingsUtils'); + METER_ID } = require('../../util/readingsUtils'); mocha.describe('readings API', () => { mocha.describe('readings test, test if data returned by API is as expected', () => { mocha.describe('for compare charts', () => { mocha.describe('for meters', () => { - // Add C15 here + mocha.it('C15: 7 day shift end 2022-10-31 17:00:00 for 15 minute reading intervals and flow units & kW as kW', async () => { + const unitData = [ + { + // u4 + name: 'kW', + identifier: '', + unitRepresent: Unit.unitRepresentType.FLOW, + secInRate: 3600, + typeOfUnit: Unit.unitType.UNIT, + suffix: '', + displayable: Unit.displayableType.ALL, + preferredDisplay: true, + note: 'kilowatts' + }, + { + // u5 + name: 'Electric', + identifier: '', + unitRepresent: Unit.unitRepresentType.FLOW, + secInRate: 3600, + typeOfUnit: Unit.unitType.METER, + suffix: '', + displayable: Unit.displayableType.NONE, + preferredDisplay: false, + note: 'special unit' + } + ]; + const conversionData = [ + { + // c4 + sourceName: 'Electric', + destinationName: 'kW', + bidirectional: false, + slope: 1, + intercept: 0, + note: 'Electric → kW' + } + ]; + const meterData = [ + { + name: 'Electric kW', + unit: 'Electric', + defaultGraphicUnit: 'kW', + displayable: true, + gps: undefined, + note: 'special meter', + file: 'test/web/readingsData/readings_ri_15_days_75.csv', + deleteFile: false, + readingFrequency: '15 minutes', + id: METER_ID + } + ]; + await prepareTest(unitData, conversionData, meterData); + const unitId = await getUnitId('kW'); + const expected = [1990.55774277443, 2057.611897078]; - // Add C16 here + const res = await chai.request(app) + .get(`/api/compareReadings/meters/${METER_ID}`) + .query({ + curr_start: '2022-10-30 00:00:00', + curr_end: '2022-10-31 17:00:00', + shift: 'P7D', + graphicUnitId: unitId + }); + + expectCompareToEqualExpected(res, expected); + }); + + mocha.it('C16: 7 day shift end 2022-10-31 17:00:00 for 15 minute reading intervals and flow units & thing as thing where rate is 36', async () => { + // These are the 2D arrays for units and conversions to feed into the database + // For Thing units. + const unitDataThing = [ + { + // u14 + name: 'Thing_36', + identifier: '', + unitRepresent: Unit.unitRepresentType.FLOW, + secInRate: 36, + typeOfUnit: Unit.unitType.METER, + suffix: '', + displayable: Unit.displayableType.NONE, + preferredDisplay: false, + note: 'special unit' + }, + { + // u15 + name: 'thing unit', + identifier: '', + unitRepresent: Unit.unitRepresentType.FLOW, + secInRate: 3600, + typeOfUnit: Unit.unitType.UNIT, + suffix: '', + displayable: Unit.displayableType.ALL, + preferredDisplay: false, + note: 'special unit' + } + ]; + + const conversionDataThing_36 = [ + { + // c15 + sourceName: 'Thing_36', + destinationName: 'thing unit', + bidirectional: false, + slope: 1, + intercept: 0, + note: 'Thing_36 → thing unit' + } + ]; + + const meterDataThing_36 = [ + { + name: 'Thing_36 thing unit', + unit: 'Thing_36', + defaultGraphicUnit: 'thing unit', + displayable: true, + gps: undefined, + note: 'special meter', + file: 'test/web/readingsData/readings_ri_15_days_75.csv', + deleteFile: false, + readingFrequency: '15 minutes', + id: METER_ID + } + ] + + // Initialize test database with "thing" data + await prepareTest(unitDataThing, conversionDataThing_36, meterDataThing_36); + + // Get the unit ID since the DB could use any value + const unitId = await getUnitId('thing unit'); + // Expected was taken from the `curr use, prev use` column for this test case, since this is a compare readings test + const expected = [199055.77427744, 205761.1897078]; + + // Create a request to the API and save the response + // Note: the api paths are located in app.js, but this specific one points to compareReadings.js + const res = await chai.request(app).get(`/api/compareReadings/meters/${METER_ID}`) + .query({ + curr_start: '2022-10-30 00:00:00', + curr_end: '2022-10-31 17:00:00', + shift: 'P7D', + graphicUnitId: unitId + }); + + // Check that the API reading is equal to what it is expected to equal + expectCompareToEqualExpected(res, expected, METER_ID); + }) }); }); }); diff --git a/src/server/test/web/readingsCompareMeterQuantity.js b/src/server/test/web/readingsCompareMeterQuantity.js index 14bd5fca2..e278afc55 100644 --- a/src/server/test/web/readingsCompareMeterQuantity.js +++ b/src/server/test/web/readingsCompareMeterQuantity.js @@ -7,6 +7,7 @@ See: https://github.com/OpenEnergyDashboard/DesignDocs/blob/main/testing/testing.md for information. */ const { chai, mocha, app } = require('../common'); +const Unit = require('../../models/Unit'); const { prepareTest, expectCompareToEqualExpected, getUnitId, @@ -36,10 +37,37 @@ mocha.describe('readings API', () => { expectCompareToEqualExpected(res, expected); }); - // Add C2 here - - // Add C3 here + mocha.it('C2: 7 day shift end 2022-10-31 17:00:00 for 15 minute reading intervals and quantity units & kWh as kWh', async () => { + await prepareTest(unitDatakWh, conversionDatakWh, meterDatakWh); + // Get the unit ID since the DB could use any value. + const unitId = await getUnitId('kWh'); + const expected = [7962.23097109771, 8230.447588312]; + // for compare, need the unitID, currentStart, currentEnd, shift + const res = await chai.request(app).get(`/api/compareReadings/meters/${METER_ID}`) + .query({ + curr_start: '2022-10-30 00:00:00', + curr_end: '2022-10-31 17:00:00', + shift: 'P7D', + graphicUnitId: unitId + }); + expectCompareToEqualExpected(res, expected); + }); + mocha.it('C3: 28 day shift end 2022-10-31 17:00:00 for 15 minute reading intervals and quantity units & kWh as kWh', async () => { + await prepareTest(unitDatakWh, conversionDatakWh, meterDatakWh); + // Get the unit ID since the DB could use any value. + const unitId = await getUnitId('kWh'); + const expected = [108269.924822581, 108889.847659507]; + // for compare, need the unitID, currentStart, currentEnd, shift + const res = await chai.request(app).get(`/api/compareReadings/meters/${METER_ID}`) + .query({ + curr_start: '2022-10-09 00:00:00', + curr_end: '2022-10-31 17:00:00', + shift: 'P28D', + graphicUnitId: unitId + }); + expectCompareToEqualExpected(res, expected); + }); // Add C4 here // Add C5 here @@ -58,7 +86,138 @@ mocha.describe('readings API', () => { // Add C13 here - // Add C14 here + mocha.it('C14: 1 day shift end 2022-10-31 17:00:00 for 15 minute reading intervals and quantity units & kWh as lbs of CO2 & chained & reversed', async () => { + const unitData = [ + // adding units u2, u10, u11, u12, u13 + { + //u2 + name: 'Electric_Utility', + identifier: '', + unitRepresent: Unit.unitRepresentType.QUANTITY, + secInRate: 3600, + typeOfUnit: Unit.unitType.METER, + suffix: '', + displayable: Unit.displayableType.NONE, + preferredDisplay: false, + note: 'special unit' + }, + { + // u10 + name: 'kg', + identifier: '', + unitRepresent: Unit.unitRepresentType.QUANTITY, + secInRate: 3600, + typeOfUnit: Unit.unitType.UNIT, + suffix: '', + displayable: Unit.displayableType.ALL, + preferredDisplay: false, + note: 'OED created standard unit' + }, + { + // u11 + name: 'metric ton', + identifier: '', + unitRepresent: Unit.unitRepresentType.QUANTITY, + secInRate: 3600, + typeOfUnit: Unit.unitType.UNIT, + suffix: '', + displayable: Unit.displayableType.ALL, + preferredDisplay: false, + note: 'OED created standard unit' + }, + { + // u12 + name: 'kg CO₂', + identifier: '', + unitRepresent: Unit.unitRepresentType.QUANTITY, + secInRate: 3600, + typeOfUnit: Unit.unitType.UNIT, + suffix: 'CO₂', + displayable: Unit.displayableType.ALL, + preferredDisplay: false, + note: 'special unit' + }, + { + // u13 + name: 'pound', + identifier: 'lb', + unitRepresent: Unit.unitRepresentType.QUANTITY, + secInRate: 3600, + typeOfUnit: Unit.unitType.UNIT, + suffix: '', + displayable: Unit.displayableType.ALL, + preferredDisplay: false, + note: 'special unit' + } + ]; + const conversionData = [ + // adding conversions c11, c12, c13, c14 + { + // c11 + sourceName: 'Electric_Utility', + destinationName: 'kg CO₂', + bidirectional: false, + slope: 0.709, + intercept: 0, + note: 'Electric_Utility → kg CO₂' + }, + { + // c12 + sourceName: 'kg CO₂', + destinationName: 'kg', + bidirectional: false, + slope: 1, + intercept: 0, + note: 'CO₂ → kg' + }, + { + // c13 + sourceName: 'kg', + destinationName: 'metric ton', + bidirectional: true, + slope: 1e-3, + intercept: 0, + note: 'kg → Metric ton' + }, + { + // c14 + sourceName: 'pound', + destinationName: 'metric ton', + bidirectional: true, + slope: 454.545454, + intercept: 0, + note: 'lbs → metric tons' + } + ]; + // redefining the meterData as the unit is different + const meterData = [ + { + name: 'Electric Utility pound of CO₂', + unit: 'Electric_Utility', + displayable: true, + gps: undefined, + note: 'special meter', + file: 'test/web/readingsData/readings_ri_15_days_75.csv', + deleteFile: false, + readingFrequency: '15 minutes', + id: METER_ID + } + ]; + // load data into database + await prepareTest(unitData, conversionData, meterData); + // Get the unit ID since the DB could use any value + const unitId = await getUnitId('pound of CO₂'); + const expected = [0.00486660462797753, 0.0052526287132491]; + // for compare, need the unitID, currentStart, currentEnd, shift + const res = await chai.request(app).get(`/api/compareReadings/meters/${METER_ID}`) + .query({ + curr_start: '2022-10-31 00:00:00', + curr_end: '2022-10-31 17:00:00', + shift: 'P1D', + graphicUnitId: unitId + }); + expectCompareToEqualExpected(res, expected); + }); }); }); }); diff --git a/src/server/test/web/readingsData/expected_bar_group_ri_15-20_mu_Thing36_gu_thing_st_-inf_et_inf_bd_13.csv b/src/server/test/web/readingsData/expected_bar_group_ri_15-20_mu_Thing36_gu_thing_st_-inf_et_inf_bd_13.csv new file mode 100644 index 000000000..8ccc6c70a --- /dev/null +++ b/src/server/test/web/readingsData/expected_bar_group_ri_15-20_mu_Thing36_gu_thing_st_-inf_et_inf_bd_13.csv @@ -0,0 +1,6 @@ +reading,start time,end time +3098568.24778472,2022-08-28 00:00:00,2022-09-10 00:00:00 +3158442.06007844,2022-09-10 00:00:00,2022-09-23 00:00:00 +3078678.36050569,2022-09-23 00:00:00,2022-10-06 00:00:00 +3096487.3496156,2022-10-06 00:00:00,2022-10-19 00:00:00 +3121656.58075156,2022-10-19 00:00:00,2022-11-01 00:00:00 \ No newline at end of file diff --git a/src/server/test/web/readingsData/expected_bar_group_ri_15-20_mu_kW_gu_kW_st_-inf_et_inf_bd_13.csv b/src/server/test/web/readingsData/expected_bar_group_ri_15-20_mu_kW_gu_kW_st_-inf_et_inf_bd_13.csv new file mode 100644 index 000000000..36b6793fc --- /dev/null +++ b/src/server/test/web/readingsData/expected_bar_group_ri_15-20_mu_kW_gu_kW_st_-inf_et_inf_bd_13.csv @@ -0,0 +1,6 @@ +reading,start time,end time +30985.6824778472,2022-08-28 00:00:00,2022-09-10 00:00:00 +31584.4206007844,2022-09-10 00:00:00,2022-09-23 00:00:00 +30786.7836050569,2022-09-23 00:00:00,2022-10-06 00:00:00 +30964.873496156,2022-10-06 00:00:00,2022-10-19 00:00:00 +31216.5658075156,2022-10-19 00:00:00,2022-11-01 00:00:00 diff --git a/src/server/test/web/readingsData/expected_bar_group_ri_15-20_mu_kWh_gu_BTU_st_-inf_et_inf_bd_1.csv b/src/server/test/web/readingsData/expected_bar_group_ri_15-20_mu_kWh_gu_BTU_st_-inf_et_inf_bd_1.csv new file mode 100644 index 000000000..1dd924a24 --- /dev/null +++ b/src/server/test/web/readingsData/expected_bar_group_ri_15-20_mu_kWh_gu_BTU_st_-inf_et_inf_bd_1.csv @@ -0,0 +1,76 @@ +reading,start time,end time +29209193.0104336,2022-08-18 00:00:00,2022-08-19 00:00:00 +25795589.743482,2022-08-19 00:00:00,2022-08-20 00:00:00 +29531280.9199285,2022-08-20 00:00:00,2022-08-21 00:00:00 +28972901.8881714,2022-08-21 00:00:00,2022-08-22 00:00:00 +28031764.7371833,2022-08-22 00:00:00,2022-08-23 00:00:00 +28802134.8350591,2022-08-23 00:00:00,2022-08-24 00:00:00 +29089482.1874503,2022-08-24 00:00:00,2022-08-25 00:00:00 +28562754.7754511,2022-08-25 00:00:00,2022-08-26 00:00:00 +26641697.0795456,2022-08-26 00:00:00,2022-08-27 00:00:00 +28730608.5257597,2022-08-27 00:00:00,2022-08-28 00:00:00 +28379171.3854016,2022-08-28 00:00:00,2022-08-29 00:00:00 +26823146.8172871,2022-08-29 00:00:00,2022-08-30 00:00:00 +29302572.2308813,2022-08-30 00:00:00,2022-08-31 00:00:00 +28155331.1018422,2022-08-31 00:00:00,2022-09-01 00:00:00 +27510358.969585,2022-09-01 00:00:00,2022-09-02 00:00:00 +29694622.443881,2022-09-02 00:00:00,2022-09-03 00:00:00 +29773433.0568812,2022-09-03 00:00:00,2022-09-04 00:00:00 +28472315.1152601,2022-09-04 00:00:00,2022-09-05 00:00:00 +26551995.2139845,2022-09-05 00:00:00,2022-09-06 00:00:00 +28947223.2033972,2022-09-06 00:00:00,2022-09-07 00:00:00 +29063083.5525401,2022-09-07 00:00:00,2022-09-08 00:00:00 +27931330.0412951,2022-09-08 00:00:00,2022-09-09 00:00:00 +28781046.7149595,2022-09-09 00:00:00,2022-09-10 00:00:00 +28364055.9788512,2022-09-10 00:00:00,2022-09-11 00:00:00 +30696225.8079846,2022-09-11 00:00:00,2022-09-12 00:00:00 +28473699.6649624,2022-09-12 00:00:00,2022-09-13 00:00:00 +29283145.4581739,2022-09-13 00:00:00,2022-09-14 00:00:00 +27570212.0168767,2022-09-14 00:00:00,2022-09-15 00:00:00 +29023763.7911641,2022-09-15 00:00:00,2022-09-16 00:00:00 +27852817.1090061,2022-09-16 00:00:00,2022-09-17 00:00:00 +29411091.2920606,2022-09-17 00:00:00,2022-09-18 00:00:00 +29604980.7864712,2022-09-18 00:00:00,2022-09-19 00:00:00 +28154458.5434468,2022-09-19 00:00:00,2022-09-20 00:00:00 +31153760.1248859,2022-09-20 00:00:00,2022-09-21 00:00:00 +27666463.2860029,2022-09-21 00:00:00,2022-09-22 00:00:00 +29795563.315951,2022-09-22 00:00:00,2022-09-23 00:00:00 +27360276.2628425,2022-09-23 00:00:00,2022-09-24 00:00:00 +27311035.5953025,2022-09-24 00:00:00,2022-09-25 00:00:00 +29095555.0733551,2022-09-25 00:00:00,2022-09-26 00:00:00 +27726783.808572,2022-09-26 00:00:00,2022-09-27 00:00:00 +27508821.5107749,2022-09-27 00:00:00,2022-09-28 00:00:00 +28655569.414136,2022-09-28 00:00:00,2022-09-29 00:00:00 +29588411.6900755,2022-09-29 00:00:00,2022-09-30 00:00:00 +28738147.3845309,2022-09-30 00:00:00,2022-10-01 00:00:00 +28123963.9046944,2022-10-01 00:00:00,2022-10-02 00:00:00 +28765104.26081,2022-10-02 00:00:00,2022-10-03 00:00:00 +27627814.6610993,2022-10-03 00:00:00,2022-10-04 00:00:00 +28735441.8490875,2022-10-04 00:00:00,2022-10-05 00:00:00 +28285434.407459,2022-10-05 00:00:00,2022-10-06 00:00:00 +28929835.0244939,2022-10-06 00:00:00,2022-10-07 00:00:00 +29599004.1223681,2022-10-07 00:00:00,2022-10-08 00:00:00 +28122739.6885322,2022-10-08 00:00:00,2022-10-09 00:00:00 +26479928.0582859,2022-10-09 00:00:00,2022-10-10 00:00:00 +28355203.9271451,2022-10-10 00:00:00,2022-10-11 00:00:00 +28026344.6838043,2022-10-11 00:00:00,2022-10-12 00:00:00 +29621879.1555374,2022-10-12 00:00:00,2022-10-13 00:00:00 +29014963.8970632,2022-10-13 00:00:00,2022-10-14 00:00:00 +28542153.0589121,2022-10-14 00:00:00,2022-10-15 00:00:00 +27600877.2424139,2022-10-15 00:00:00,2022-10-16 00:00:00 +28194214.6361847,2022-10-16 00:00:00,2022-10-17 00:00:00 +28858052.7695064,2022-10-17 00:00:00,2022-10-18 00:00:00 +28153189.1046015,2022-10-18 00:00:00,2022-10-19 00:00:00 +27184913.6111465,2022-10-19 00:00:00,2022-10-20 00:00:00 +28752720.0055364,2022-10-20 00:00:00,2022-10-21 00:00:00 +29298144.0717527,2022-10-21 00:00:00,2022-10-22 00:00:00 +27754964.2877268,2022-10-22 00:00:00,2022-10-23 00:00:00 +28742598.2237095,2022-10-23 00:00:00,2022-10-24 00:00:00 +30205570.981724,2022-10-24 00:00:00,2022-10-25 00:00:00 +31181745.6220899,2022-10-25 00:00:00,2022-10-26 00:00:00 +27286030.6182213,2022-10-26 00:00:00,2022-10-27 00:00:00 +27517284.75375,2022-10-27 00:00:00,2022-10-28 00:00:00 +28828155.312223,2022-10-28 00:00:00,2022-10-29 00:00:00 +30702649.888834,2022-10-29 00:00:00,2022-10-30 00:00:00 +28494727.6464538,2022-10-30 00:00:00,2022-10-31 00:00:00 +26683896.1942727,2022-10-31 00:00:00,2022-11-01 00:00:00 diff --git a/src/server/test/web/readingsData/expected_bar_group_ri_15-20_mu_kWh_gu_MJ_st_-inf_et_inf_bd_1.csv b/src/server/test/web/readingsData/expected_bar_group_ri_15-20_mu_kWh_gu_MJ_st_-inf_et_inf_bd_1.csv new file mode 100644 index 000000000..876346de8 --- /dev/null +++ b/src/server/test/web/readingsData/expected_bar_group_ri_15-20_mu_kWh_gu_MJ_st_-inf_et_inf_bd_1.csv @@ -0,0 +1,76 @@ +reading,start time,end time +30817.8866959629,2022-08-18 00:00:00,2022-08-19 00:00:00 +27216.2795352205,2022-08-19 00:00:00,2022-08-20 00:00:00 +31157.7135681879,2022-08-20 00:00:00,2022-08-21 00:00:00 +30568.581861333,2022-08-21 00:00:00,2022-08-22 00:00:00 +29575.6116661567,2022-08-22 00:00:00,2022-08-23 00:00:00 +30388.4098280852,2022-08-23 00:00:00,2022-08-24 00:00:00 +30691.5828101395,2022-08-24 00:00:00,2022-08-25 00:00:00 +30135.8459331621,2022-08-25 00:00:00,2022-08-26 00:00:00 +28108.9861569378,2022-08-26 00:00:00,2022-08-27 00:00:00 +30312.9442137157,2022-08-27 00:00:00,2022-08-28 00:00:00 +29942.1517043697,2022-08-28 00:00:00,2022-08-29 00:00:00 +28300.4292227127,2022-08-29 00:00:00,2022-08-30 00:00:00 +30916.4087686023,2022-08-30 00:00:00,2022-08-31 00:00:00 +29705.9834372675,2022-08-31 00:00:00,2022-09-01 00:00:00 +29025.4895226682,2022-09-01 00:00:00,2022-09-02 00:00:00 +31330.0511119235,2022-09-02 00:00:00,2022-09-03 00:00:00 +31413.2022123668,2022-09-03 00:00:00,2022-09-04 00:00:00 +30040.4253167968,2022-09-04 00:00:00,2022-09-05 00:00:00 +28014.3439691755,2022-09-05 00:00:00,2022-09-06 00:00:00 +30541.4889252978,2022-09-06 00:00:00,2022-09-07 00:00:00 +30663.7302727792,2022-09-07 00:00:00,2022-09-08 00:00:00 +29469.6455383996,2022-09-08 00:00:00,2022-09-09 00:00:00 +30366.1602816623,2022-09-09 00:00:00,2022-09-10 00:00:00 +29926.2038181591,2022-09-10 00:00:00,2022-09-11 00:00:00 +32386.8176914799,2022-09-11 00:00:00,2022-09-12 00:00:00 +30041.8861204499,2022-09-12 00:00:00,2022-09-13 00:00:00 +30895.9120681303,2022-09-13 00:00:00,2022-09-14 00:00:00 +29088.6389711719,2022-09-14 00:00:00,2022-09-15 00:00:00 +30622.2449790716,2022-09-15 00:00:00,2022-09-16 00:00:00 +29386.8085134059,2022-09-16 00:00:00,2022-09-17 00:00:00 +31030.9045073439,2022-09-17 00:00:00,2022-09-18 00:00:00 +31235.472448271,2022-09-18 00:00:00,2022-09-19 00:00:00 +29705.0628227968,2022-09-19 00:00:00,2022-09-20 00:00:00 +32869.5506698522,2022-09-20 00:00:00,2022-09-21 00:00:00 +29190.1912703132,2022-09-21 00:00:00,2022-09-22 00:00:00 +31436.5512934701,2022-09-22 00:00:00,2022-09-23 00:00:00 +28867.1410243115,2022-09-23 00:00:00,2022-09-24 00:00:00 +28815.1884314228,2022-09-24 00:00:00,2022-09-25 00:00:00 +30697.990159691,2022-09-25 00:00:00,2022-09-26 00:00:00 +29253.8339402533,2022-09-26 00:00:00,2022-09-27 00:00:00 +29023.8673884521,2022-09-27 00:00:00,2022-09-28 00:00:00 +30233.7723297489,2022-09-28 00:00:00,2022-09-29 00:00:00 +31217.9908103772,2022-09-29 00:00:00,2022-09-30 00:00:00 +30320.8982744576,2022-09-30 00:00:00,2022-10-01 00:00:00 +29672.8886945499,2022-10-01 00:00:00,2022-10-02 00:00:00 +30349.3397982802,2022-10-02 00:00:00,2022-10-03 00:00:00 +29149.4140758592,2022-10-03 00:00:00,2022-10-04 00:00:00 +30318.0437318923,2022-10-04 00:00:00,2022-10-05 00:00:00 +29843.2521707734,2022-10-05 00:00:00,2022-10-06 00:00:00 +30523.1430940008,2022-10-06 00:00:00,2022-10-07 00:00:00 +31229.1666199283,2022-10-07 00:00:00,2022-10-08 00:00:00 +29671.5970547924,2022-10-08 00:00:00,2022-10-09 00:00:00 +27938.3077213399,2022-10-09 00:00:00,2022-10-10 00:00:00 +29916.8642404992,2022-10-10 00:00:00,2022-10-11 00:00:00 +29569.8931038239,2022-10-11 00:00:00,2022-10-12 00:00:00 +31253.3014934979,2022-10-12 00:00:00,2022-10-13 00:00:00 +30612.9604315923,2022-10-13 00:00:00,2022-10-14 00:00:00 +30114.1095789324,2022-10-14 00:00:00,2022-10-15 00:00:00 +29120.9930812555,2022-10-15 00:00:00,2022-10-16 00:00:00 +29747.0084787769,2022-10-16 00:00:00,2022-10-17 00:00:00 +30447.4074377573,2022-10-17 00:00:00,2022-10-18 00:00:00 +29703.723469721,2022-10-18 00:00:00,2022-10-19 00:00:00 +28682.1202903002,2022-10-19 00:00:00,2022-10-20 00:00:00 +30336.273481258,2022-10-20 00:00:00,2022-10-21 00:00:00 +30911.7367290069,2022-10-21 00:00:00,2022-10-22 00:00:00 +29283.5664567702,2022-10-22 00:00:00,2022-10-23 00:00:00 +30325.5942432048,2022-10-23 00:00:00,2022-10-24 00:00:00 +31869.1400946656,2022-10-24 00:00:00,2022-10-25 00:00:00 +32899.0774658049,2022-10-25 00:00:00,2022-10-26 00:00:00 +28788.8063074713,2022-10-26 00:00:00,2022-10-27 00:00:00 +29032.7967437751,2022-10-27 00:00:00,2022-10-28 00:00:00 +30415.8633806953,2022-10-28 00:00:00,2022-10-29 00:00:00 +32393.5955780059,2022-10-29 00:00:00,2022-10-30 00:00:00 +30064.0722161361,2022-10-30 00:00:00,2022-10-31 00:00:00 +28153.5093841239,2022-10-31 00:00:00,2022-11-01 00:00:00 diff --git a/src/server/test/web/readingsData/expected_bar_group_ri_15-20_mu_kWh_gu_kWh_st_-inf_et_inf_bd_7.csv b/src/server/test/web/readingsData/expected_bar_group_ri_15-20_mu_kWh_gu_kWh_st_-inf_et_inf_bd_7.csv new file mode 100644 index 000000000..04befedd0 --- /dev/null +++ b/src/server/test/web/readingsData/expected_bar_group_ri_15-20_mu_kWh_gu_kWh_st_-inf_et_inf_bd_7.csv @@ -0,0 +1,11 @@ +reading,start time,end time +57744.5416303119,2022-08-23 00:00:00,2022-08-30 00:00:00 +58457.1956496669,2022-08-30 00:00:00,2022-09-06 00:00:00 +59276.647957841,2022-09-06 00:00:00,2022-09-13 00:00:00 +58879.1789750532,2022-09-13 00:00:00,2022-09-20 00:00:00 +58647.346330365,2022-09-20 00:00:00,2022-09-27 00:00:00 +58324.4920477014,2022-09-27 00:00:00,2022-10-04 00:00:00 +58177.8818425628,2022-10-04 00:00:00,2022-10-11 00:00:00 +58573.7982237878,2022-10-11 00:00:00,2022-10-18 00:00:00 +58642.2652124796,2022-10-18 00:00:00,2022-10-25 00:00:00 +58818.8114100034,2022-10-25 00:00:00,2022-11-01 00:00:00 diff --git a/src/server/test/web/readingsData/expected_bar_group_ri_15-20_mu_kWh_gu_kWh_st_-inf_et_inf_bd_75.csv b/src/server/test/web/readingsData/expected_bar_group_ri_15-20_mu_kWh_gu_kWh_st_-inf_et_inf_bd_75.csv new file mode 100644 index 000000000..aaaa30e37 --- /dev/null +++ b/src/server/test/web/readingsData/expected_bar_group_ri_15-20_mu_kWh_gu_kWh_st_-inf_et_inf_bd_75.csv @@ -0,0 +1,2 @@ +reading,start time,end time +627024.401870568,2022-08-18 00:00:00,2022-11-01 00:00:00 diff --git a/src/server/test/web/readingsData/expected_bar_group_ri_15-20_mu_kWh_gu_kWh_st_2022-08-20%07#25#35_et_2022-10-28%13#18#28_bd_13.csv b/src/server/test/web/readingsData/expected_bar_group_ri_15-20_mu_kWh_gu_kWh_st_2022-08-20%07#25#35_et_2022-10-28%13#18#28_bd_13.csv new file mode 100644 index 000000000..00136eafe --- /dev/null +++ b/src/server/test/web/readingsData/expected_bar_group_ri_15-20_mu_kWh_gu_kWh_st_2022-08-20%07#25#35_et_2022-10-28%13#18#28_bd_13.csv @@ -0,0 +1,6 @@ +reading,start time,end time +107760.512327733,2022-08-24 00:00:00,2022-09-06 00:00:00 +109904.420593228,2022-09-06 00:00:00,2022-09-19 00:00:00 +108695.813086027,2022-09-19 00:00:00,2022-10-02 00:00:00 +108469.275865337,2022-10-02 00:00:00,2022-10-15 00:00:00 +108652.290077713,2022-10-15 00:00:00,2022-10-28 00:00:00 \ No newline at end of file diff --git a/src/server/test/web/readingsData/expected_bar_group_ri_15-20_mu_kWh_gu_kgCO2_st_-inf_et_inf_bd_1.csv b/src/server/test/web/readingsData/expected_bar_group_ri_15-20_mu_kWh_gu_kgCO2_st_-inf_et_inf_bd_1.csv new file mode 100644 index 000000000..383511541 --- /dev/null +++ b/src/server/test/web/readingsData/expected_bar_group_ri_15-20_mu_kWh_gu_kgCO2_st_-inf_et_inf_bd_1.csv @@ -0,0 +1,76 @@ +reading,start time,end time +6069.41157428824,2022-08-18 00:00:00,2022-08-19 00:00:00 +5360.0950529087,2022-08-19 00:00:00,2022-08-20 00:00:00 +6136.3385888459,2022-08-20 00:00:00,2022-08-21 00:00:00 +6020.31237213475,2022-08-21 00:00:00,2022-08-22 00:00:00 +5824.75240869586,2022-08-22 00:00:00,2022-08-23 00:00:00 +5984.82849114233,2022-08-23 00:00:00,2022-08-24 00:00:00 +6044.53672566359,2022-08-24 00:00:00,2022-08-25 00:00:00 +5935.08743516999,2022-08-25 00:00:00,2022-08-26 00:00:00 +5535.90866257469,2022-08-26 00:00:00,2022-08-27 00:00:00 +5969.96595764567,2022-08-27 00:00:00,2022-08-28 00:00:00 +5896.94043288837,2022-08-28 00:00:00,2022-08-29 00:00:00 +5573.61231080648,2022-08-29 00:00:00,2022-08-30 00:00:00 +6088.81494914974,2022-08-30 00:00:00,2022-08-31 00:00:00 +5850.42840472852,2022-08-31 00:00:00,2022-09-01 00:00:00 +5716.40890876994,2022-09-01 00:00:00,2022-09-02 00:00:00 +6170.27951065381,2022-09-02 00:00:00,2022-09-03 00:00:00 +6186.65565793557,2022-09-03 00:00:00,2022-09-04 00:00:00 +5916.29487489138,2022-09-04 00:00:00,2022-09-05 00:00:00 +5517.26940948484,2022-09-05 00:00:00,2022-09-06 00:00:00 +6014.97656889892,2022-09-06 00:00:00,2022-09-07 00:00:00 +6039.05132316679,2022-09-07 00:00:00,2022-09-08 00:00:00 +5803.8829685348,2022-09-08 00:00:00,2022-09-09 00:00:00 +5980.44656658293,2022-09-09 00:00:00,2022-09-10 00:00:00 +5893.79958529856,2022-09-10 00:00:00,2022-09-11 00:00:00 +6378.403817572,2022-09-11 00:00:00,2022-09-12 00:00:00 +5916.58257205527,2022-09-12 00:00:00,2022-09-13 00:00:00 +6084.77823786232,2022-09-13 00:00:00,2022-09-14 00:00:00 +5728.84584182247,2022-09-14 00:00:00,2022-09-15 00:00:00 +6030.88102504494,2022-09-15 00:00:00,2022-09-16 00:00:00 +5787.568676668,2022-09-16 00:00:00,2022-09-17 00:00:00 +6111.36424880746,2022-09-17 00:00:00,2022-09-18 00:00:00 +6151.65276828447,2022-09-18 00:00:00,2022-09-19 00:00:00 +5850.24709482304,2022-09-19 00:00:00,2022-09-20 00:00:00 +6473.47539581256,2022-09-20 00:00:00,2022-09-21 00:00:00 +5748.84600295891,2022-09-21 00:00:00,2022-09-22 00:00:00 +6191.25412974175,2022-09-22 00:00:00,2022-09-23 00:00:00 +5685.22305173247,2022-09-23 00:00:00,2022-09-24 00:00:00 +5674.99127718854,2022-09-24 00:00:00,2022-09-25 00:00:00 +6045.79861756136,2022-09-25 00:00:00,2022-09-26 00:00:00 +5761.38007323321,2022-09-26 00:00:00,2022-09-27 00:00:00 +5716.08943844792,2022-09-27 00:00:00,2022-09-28 00:00:00 +5954.37349494221,2022-09-28 00:00:00,2022-09-29 00:00:00 +6148.2098568215,2022-09-29 00:00:00,2022-09-30 00:00:00 +5971.53246571957,2022-09-30 00:00:00,2022-10-01 00:00:00 +5843.91057900997,2022-10-01 00:00:00,2022-10-02 00:00:00 +5977.13386582796,2022-10-02 00:00:00,2022-10-03 00:00:00 +5740.81516105115,2022-10-03 00:00:00,2022-10-04 00:00:00 +5970.9702794199,2022-10-04 00:00:00,2022-10-05 00:00:00 +5877.46271918843,2022-10-05 00:00:00,2022-10-06 00:00:00 +6011.36345934627,2022-10-06 00:00:00,2022-10-07 00:00:00 +6150.41087042478,2022-10-07 00:00:00,2022-10-08 00:00:00 +5843.6561977355,2022-10-08 00:00:00,2022-10-09 00:00:00 +5502.29449289721,2022-10-09 00:00:00,2022-10-10 00:00:00 +5891.96020736497,2022-10-10 00:00:00,2022-10-11 00:00:00 +5823.6261696142,2022-10-11 00:00:00,2022-10-12 00:00:00 +6155.16409969168,2022-10-12 00:00:00,2022-10-13 00:00:00 +6029.05248499971,2022-10-13 00:00:00,2022-10-14 00:00:00 +5930.80658096196,2022-10-14 00:00:00,2022-10-15 00:00:00 +5735.21780405837,2022-10-15 00:00:00,2022-10-16 00:00:00 +5858.50805873689,2022-10-16 00:00:00,2022-10-17 00:00:00 +5996.44774260275,2022-10-17 00:00:00,2022-10-18 00:00:00 +5849.9833166756,2022-10-18 00:00:00,2022-10-19 00:00:00 +5648.7842460619,2022-10-19 00:00:00,2022-10-20 00:00:00 +5974.56052728109,2022-10-20 00:00:00,2022-10-21 00:00:00 +6087.89481690719,2022-10-21 00:00:00,2022-10-22 00:00:00 +5767.23572718058,2022-10-22 00:00:00,2022-10-23 00:00:00 +5972.45731067561,2022-10-23 00:00:00,2022-10-24 00:00:00 +6276.45009086608,2022-10-24 00:00:00,2022-10-25 00:00:00 +6479.29053423768,2022-10-25 00:00:00,2022-10-26 00:00:00 +5669.79546444366,2022-10-26 00:00:00,2022-10-27 00:00:00 +5717.84802537126,2022-10-27 00:00:00,2022-10-28 00:00:00 +5990.23531580915,2022-10-28 00:00:00,2022-10-29 00:00:00 +6379.73868466838,2022-10-29 00:00:00,2022-10-30 00:00:00 +5920.95200034458,2022-10-30 00:00:00,2022-10-31 00:00:00 +5544.67726481774,2022-10-31 00:00:00,2022-11-01 00:00:00 diff --git a/src/server/test/web/readingsData/expected_bar_ri_15_mu_Thing36_gu_thing_st_-inf_et_inf_bd_13.csv b/src/server/test/web/readingsData/expected_bar_ri_15_mu_Thing36_gu_thing_st_-inf_et_inf_bd_13.csv new file mode 100644 index 000000000..4ad59e465 --- /dev/null +++ b/src/server/test/web/readingsData/expected_bar_ri_15_mu_Thing36_gu_thing_st_-inf_et_inf_bd_13.csv @@ -0,0 +1,6 @@ +reading,start time,end time +1530114.98675756,2022-08-28 00:00:00,2022-09-10 00:00:00 +1575125.07459568,2022-09-10 00:00:00,2022-09-23 00:00:00 +1535176.60820708,2022-09-23 00:00:00,2022-10-06 00:00:00 +1539662.27909537,2022-10-06 00:00:00,2022-10-19 00:00:00 +1556034.49027915,2022-10-19 00:00:00,2022-11-01 00:00:00 diff --git a/src/server/util/readingsUtils.js b/src/server/util/readingsUtils.js index 49e54adf6..e1fb387e9 100644 --- a/src/server/util/readingsUtils.js +++ b/src/server/util/readingsUtils.js @@ -296,4 +296,4 @@ module.exports = { meterDatakWh, meterDatakWhGroups, groupDatakWh -}; +}; \ No newline at end of file