From 46f66d8542bc56a656aa89914aa5e81c1a3b7924 Mon Sep 17 00:00:00 2001 From: Vaughan Knight Date: Tue, 23 Jul 2024 17:21:56 +1000 Subject: [PATCH] 1.5 WattTime v3 Updates (#547) * doc initial set up using classic template and typescript - doc is currently copied into casdk-docs/docs to get live updates (copied and not moved so changes in main can be easily identified when rebasing once it all works) * favicon * reoganising first draft * doc link fixes * github pages pipelines to accomodate customisations * Update WattTime registration link Signed-off-by: Phil Huang * Create adopters.md Signed-off-by: tkuramoto33 <70622977+tkuramoto33@users.noreply.github.com> * Initial codespace branch and associated docs * Add Vestas. Signed-off-by: tkuramoto33 <70622977+tkuramoto33@users.noreply.github.com> * Remove a hyphen for consistent appearance Signed-off-by: tkuramoto33 <70622977+tkuramoto33@users.noreply.github.com> * Create enablement.md table of content Signed-off-by: Rintaro Ikeda <51394766+rinikeda@users.noreply.github.com> * link fix * workflow update * updated github action * ensuring there is a yarn lock file * removed working directory from setup node action * trying to have the working directory at a higher level * trying cache dependency path property * dont break the build on broken links for now * updated user * removed user * updated user to deploy pages * draft of enablement.md Signed-off-by: Rintaro Ikeda <51394766+rinikeda@users.noreply.github.com> * Update containerization.md Small typo ("arbon" => "Carbon") Signed-off-by: Richard Jackson * github token permissions updated to ensure contents write * workflows fixes * Ensuring username was not needed * Support location source setting in Helm chart Signed-off-by: Yasumasa Suenaga * Update carbon-aware-cli.md Fixes bugs with Linux scripts Signed-off-by: JasonLuuk <96975358+JasonLuuk@users.noreply.github.com> * Update overview.md Change the net url inside the prerequisites, I think the sdk requires version 6.0 net instead of the latest 8.0, which can mislead users. Signed-off-by: JasonLuuk <96975358+JasonLuuk@users.noreply.github.com> * Update quickstart.md Fix wrong links Signed-off-by: JasonLuuk <96975358+JasonLuuk@users.noreply.github.com> * verify azure function workflow not to trigger on casdk-docs changes only * Link fixes (may be related to latest version now erroring) * test doc file * restoring username and email and removing the test file * updated vs code extensions * DCO Remediation Commit for Dan Benitah I, Dan Benitah , hereby add my Signed-off-by to this commit: 44578f400c37eb20a81663f1b769308f7c511859 I, Dan Benitah , hereby add my Signed-off-by to this commit: 3ae9a001dafb9584b929bc2746a143c1a58a36ec Signed-off-by: Dan Benitah * Update the description Signed-off-by: tkuramoto33 <70622977+tkuramoto33@users.noreply.github.com> * Update the description to mention pull requests Signed-off-by: tkuramoto33 <70622977+tkuramoto33@users.noreply.github.com> * Update README.md * More updates More updates * Added plcaeholder images Added plcaeholder images * Create tests.md test coverage #413 Signed-off-by: Dan Benitah * Updated images Updates all images and compressed some images, cleaned up naming for the avif file. * More updates New images, and some updates to the copy after stepping away from it for a bit. * Updates with theory of change details Updates with theory of change details * Update README.md * More updates More updates * Added plcaeholder images Added plcaeholder images * Updated images Updates all images and compressed some images, cleaned up naming for the avif file. * More updates New images, and some updates to the copy after stepping away from it for a bit. * Updates with theory of change details Updates with theory of change details * Minor updates, included adopters page link Minor updates, included adopters page link * Further updates to clean up merge duplication * Signed-off-by: Vaughan Knight DCO Remediation Commit for Vaughan Knight I, Vaughan Knight , hereby add my Signed-off-by to this commit: da70ec4c4c1118c95826f091ea1aef3a44eba7cc I, Vaughan Knight , hereby add my Signed-off-by to this commit: 6be74ff7ac9da77bf099be359f4845ad4b3c4680 I, Vaughan Knight , hereby add my Signed-off-by to this commit: fe5cef9fa2e5ebaafb19312e9b47b5a4cc330dc9 I, Vaughan Knight , hereby add my Signed-off-by to this commit: b4a7973a003ef67748336194bbf6032445d96c32 I, Vaughan Knight , hereby add my Signed-off-by to this commit: d0a954ae137c3c7f4c28089da29223f5dc887be0 I, Vaughan Knight , hereby add my Signed-off-by to this commit: 996d0846b4d4e0de5f30c6d6336ee1aee2ee6e09 I, Vaughan Knight , hereby add my Signed-off-by to this commit: 648a1ced3cfd05437ebd67c5f8ed7eece7a22264 I, Vaughan Knight , hereby add my Signed-off-by to this commit: 581267c6286dc4e7ed4973598c6770762cd7ca13 I, Vaughan Knight , hereby add my Signed-off-by to this commit: 3c90f553a1c9c449142706f753c03f6b1d4e4886 I, Vaughan Knight , hereby add my Signed-off-by to this commit: 6c1514e8bfcbae31c492e706561fff2cd9b9bce9 I, Vaughan Knight , hereby add my Signed-off-by to this commit: 05927105f692044270301b069dfb90634fe19343 I, Vaughan Knight , hereby add my Signed-off-by to this commit: c59057c5750ead17ef09087286f83dd0d35e7fb5 I, Vaughan Knight , hereby add my Signed-off-by to this commit: 0cec58fad1ee95bfa6b59e360b791332964e7f5e I, Vaughan Knight , hereby add my Signed-off-by to this commit: 028a179ca0b4bfc0595cfe124fa4acc11c82c677 Signed-off-by: Vaughan Knight * blog posts - unpublished / placeholder posts currently sit in blog_preview sub folder * annoucementBar #416 to include disclaimer as well as CarbonHack link * CarbonHack24 Update to README.md Signed-off-by: Dan Benitah * bold disclaimer / banner message * ensuring samples get deployed as part of the docs too * working directory correction for moving samples in docs deployment workflow * normalisation of the workflow name to others * latest docusaurus updates to check the latest workflow * docusaurus broken links build warning + move all samples folder * Update CONTRIBUTING.md Signed-off-by: Sophie Trinder <144015600+Sophietn@users.noreply.github.com> * Update CONTRIBUTING.md Signed-off-by: Sophie Trinder <144015600+Sophietn@users.noreply.github.com> * Update CHANGELOG.md Signed-off-by: Dan Benitah * Update CHANGELOG.md with first draft release notes for 1.2 Signed-off-by: Dan Benitah * Update CHANGELOG.md Signed-off-by: Dan Benitah * Update CHANGELOG.md Updating release date Signed-off-by: Dan Benitah * Update WattTime registration link Signed-off-by: Phil Huang * Update containerization.md Small typo ("arbon" => "Carbon") Signed-off-by: Richard Jackson * Create adopters.md Signed-off-by: tkuramoto33 <70622977+tkuramoto33@users.noreply.github.com> * Add Vestas. Signed-off-by: tkuramoto33 <70622977+tkuramoto33@users.noreply.github.com> * Remove a hyphen for consistent appearance Signed-off-by: tkuramoto33 <70622977+tkuramoto33@users.noreply.github.com> * Update the description Signed-off-by: tkuramoto33 <70622977+tkuramoto33@users.noreply.github.com> * Update the description to mention pull requests Signed-off-by: tkuramoto33 <70622977+tkuramoto33@users.noreply.github.com> * Create enablement.md table of content Signed-off-by: Rintaro Ikeda <51394766+rinikeda@users.noreply.github.com> * draft of enablement.md Signed-off-by: Rintaro Ikeda <51394766+rinikeda@users.noreply.github.com> * Update carbon-aware-cli.md Fixes bugs with Linux scripts Signed-off-by: JasonLuuk <96975358+JasonLuuk@users.noreply.github.com> * Initial codespace branch and associated docs * updated vs code extensions * DCO Remediation Commit for Dan Benitah I, Dan Benitah , hereby add my Signed-off-by to this commit: 44578f400c37eb20a81663f1b769308f7c511859 I, Dan Benitah , hereby add my Signed-off-by to this commit: 3ae9a001dafb9584b929bc2746a143c1a58a36ec Signed-off-by: Dan Benitah * Update README.md * More updates More updates * Added plcaeholder images Added plcaeholder images * Updated images Updates all images and compressed some images, cleaned up naming for the avif file. * More updates New images, and some updates to the copy after stepping away from it for a bit. * Updates with theory of change details Updates with theory of change details * Minor updates, included adopters page link Minor updates, included adopters page link * Update README.md * More updates More updates * Added plcaeholder images Added plcaeholder images * Updated images Updates all images and compressed some images, cleaned up naming for the avif file. * More updates New images, and some updates to the copy after stepping away from it for a bit. * Updates with theory of change details Updates with theory of change details * Signed-off-by: Vaughan Knight DCO Remediation Commit for Vaughan Knight I, Vaughan Knight , hereby add my Signed-off-by to this commit: da70ec4c4c1118c95826f091ea1aef3a44eba7cc I, Vaughan Knight , hereby add my Signed-off-by to this commit: 6be74ff7ac9da77bf099be359f4845ad4b3c4680 I, Vaughan Knight , hereby add my Signed-off-by to this commit: fe5cef9fa2e5ebaafb19312e9b47b5a4cc330dc9 I, Vaughan Knight , hereby add my Signed-off-by to this commit: b4a7973a003ef67748336194bbf6032445d96c32 I, Vaughan Knight , hereby add my Signed-off-by to this commit: d0a954ae137c3c7f4c28089da29223f5dc887be0 I, Vaughan Knight , hereby add my Signed-off-by to this commit: 996d0846b4d4e0de5f30c6d6336ee1aee2ee6e09 I, Vaughan Knight , hereby add my Signed-off-by to this commit: 648a1ced3cfd05437ebd67c5f8ed7eece7a22264 I, Vaughan Knight , hereby add my Signed-off-by to this commit: 581267c6286dc4e7ed4973598c6770762cd7ca13 I, Vaughan Knight , hereby add my Signed-off-by to this commit: 3c90f553a1c9c449142706f753c03f6b1d4e4886 I, Vaughan Knight , hereby add my Signed-off-by to this commit: 6c1514e8bfcbae31c492e706561fff2cd9b9bce9 I, Vaughan Knight , hereby add my Signed-off-by to this commit: 05927105f692044270301b069dfb90634fe19343 I, Vaughan Knight , hereby add my Signed-off-by to this commit: c59057c5750ead17ef09087286f83dd0d35e7fb5 I, Vaughan Knight , hereby add my Signed-off-by to this commit: 0cec58fad1ee95bfa6b59e360b791332964e7f5e I, Vaughan Knight , hereby add my Signed-off-by to this commit: 028a179ca0b4bfc0595cfe124fa4acc11c82c677 Signed-off-by: Vaughan Knight * Update tests.md our current coverage is 74.6% so adjusting until we can improve Signed-off-by: Dan Benitah * Update README.md linking to the new banner Signed-off-by: Dan Benitah * adding the banner image Signed-off-by: Dan Benitah * Create SECURITY.md * merge and bug fixes * Qucikstart fix and overview link adjustment following move * Up Helm chart version to 1.1.0 Signed-off-by: Yasumasa Suenaga * Signed-off-by: Vaughan Knight DCO Remediation Commit for Vaughan Knight I, Vaughan Knight , hereby add my Signed-off-by to this commit: da70ec4c4c1118c95826f091ea1aef3a44eba7cc I, Vaughan Knight , hereby add my Signed-off-by to this commit: 6be74ff7ac9da77bf099be359f4845ad4b3c4680 I, Vaughan Knight , hereby add my Signed-off-by to this commit: fe5cef9fa2e5ebaafb19312e9b47b5a4cc330dc9 I, Vaughan Knight , hereby add my Signed-off-by to this commit: b4a7973a003ef67748336194bbf6032445d96c32 I, Vaughan Knight , hereby add my Signed-off-by to this commit: d0a954ae137c3c7f4c28089da29223f5dc887be0 I, Vaughan Knight , hereby add my Signed-off-by to this commit: 996d0846b4d4e0de5f30c6d6336ee1aee2ee6e09 I, Vaughan Knight , hereby add my Signed-off-by to this commit: 648a1ced3cfd05437ebd67c5f8ed7eece7a22264 I, Vaughan Knight , hereby add my Signed-off-by to this commit: 581267c6286dc4e7ed4973598c6770762cd7ca13 I, Vaughan Knight , hereby add my Signed-off-by to this commit: 3c90f553a1c9c449142706f753c03f6b1d4e4886 I, Vaughan Knight , hereby add my Signed-off-by to this commit: 6c1514e8bfcbae31c492e706561fff2cd9b9bce9 I, Vaughan Knight , hereby add my Signed-off-by to this commit: 05927105f692044270301b069dfb90634fe19343 I, Vaughan Knight , hereby add my Signed-off-by to this commit: c59057c5750ead17ef09087286f83dd0d35e7fb5 I, Vaughan Knight , hereby add my Signed-off-by to this commit: 0cec58fad1ee95bfa6b59e360b791332964e7f5e I, Vaughan Knight , hereby add my Signed-off-by to this commit: 028a179ca0b4bfc0595cfe124fa4acc11c82c677 Signed-off-by: Vaughan Knight * CarbonHack24 Update to README.md Signed-off-by: Dan Benitah * Create tests.md test coverage #413 Signed-off-by: Dan Benitah * Update tests.md our current coverage is 74.6% so adjusting until we can improve Signed-off-by: Dan Benitah * Signed-off-by: Vaughan Knight DCO Remediation Commit for Vaughan Knight I, Vaughan Knight , hereby add my Signed-off-by to this commit: da70ec4c4c1118c95826f091ea1aef3a44eba7cc I, Vaughan Knight , hereby add my Signed-off-by to this commit: 6be74ff7ac9da77bf099be359f4845ad4b3c4680 I, Vaughan Knight , hereby add my Signed-off-by to this commit: fe5cef9fa2e5ebaafb19312e9b47b5a4cc330dc9 I, Vaughan Knight , hereby add my Signed-off-by to this commit: b4a7973a003ef67748336194bbf6032445d96c32 I, Vaughan Knight , hereby add my Signed-off-by to this commit: d0a954ae137c3c7f4c28089da29223f5dc887be0 I, Vaughan Knight , hereby add my Signed-off-by to this commit: 996d0846b4d4e0de5f30c6d6336ee1aee2ee6e09 I, Vaughan Knight , hereby add my Signed-off-by to this commit: 648a1ced3cfd05437ebd67c5f8ed7eece7a22264 I, Vaughan Knight , hereby add my Signed-off-by to this commit: 581267c6286dc4e7ed4973598c6770762cd7ca13 I, Vaughan Knight , hereby add my Signed-off-by to this commit: 3c90f553a1c9c449142706f753c03f6b1d4e4886 I, Vaughan Knight , hereby add my Signed-off-by to this commit: 6c1514e8bfcbae31c492e706561fff2cd9b9bce9 I, Vaughan Knight , hereby add my Signed-off-by to this commit: 05927105f692044270301b069dfb90634fe19343 I, Vaughan Knight , hereby add my Signed-off-by to this commit: c59057c5750ead17ef09087286f83dd0d35e7fb5 I, Vaughan Knight , hereby add my Signed-off-by to this commit: 0cec58fad1ee95bfa6b59e360b791332964e7f5e I, Vaughan Knight , hereby add my Signed-off-by to this commit: 028a179ca0b4bfc0595cfe124fa4acc11c82c677 Signed-off-by: Vaughan Knight * blog updates * DCO Remediation Commit for Dan Benitah I, Dan Benitah , hereby add my Signed-off-by to this commit: 356ce0931b313eeab62d705371564ed9e8efca03 I, Dan Benitah , hereby add my Signed-off-by to this commit: 2c342a85a0c8b549b021135d6e026963bee7bbb5 I, Dan Benitah , hereby add my Signed-off-by to this commit: 04be596f3d35a8d87676a08fe9c6de6f75a75434 I, Dan Benitah , hereby add my Signed-off-by to this commit: c437b5bc23992593b081b1c742be3ec89a956f7d I, Dan Benitah , hereby add my Signed-off-by to this commit: 46958d9db7c2f5228af8695b9448ea50eaab129d I, Dan Benitah , hereby add my Signed-off-by to this commit: 21a0e16bfa1420f55fc9456a28ffc71aca39a0bb I, Dan Benitah , hereby add my Signed-off-by to this commit: 8bbe72e49b5c35ad7c8612d3d1bc417d59b2ef04 I, Dan Benitah , hereby add my Signed-off-by to this commit: 62e2a9591ebf53dd64f1d50c5e28f60c8fc486b7 I, Dan Benitah , hereby add my Signed-off-by to this commit: 5183d734eb62f6b637c711ddbe10d0207f30c945 I, Dan Benitah , hereby add my Signed-off-by to this commit: 93267d60d633e7fd4f7d9545685e970d0e272a50 I, Dan Benitah , hereby add my Signed-off-by to this commit: ee8841cf9d431e5d54fc0d15e73debd1006f184b I, Dan Benitah , hereby add my Signed-off-by to this commit: 2d7b4ddfc914923b019ac3d51b34300974fef91c I, Dan Benitah , hereby add my Signed-off-by to this commit: 68bbb2fbbf44853a4faec59d7c4898b1b6690a1d I, Dan Benitah , hereby add my Signed-off-by to this commit: 735a515a28f056fe32a5acd45997703a4169c894 I, Dan Benitah , hereby add my Signed-off-by to this commit: a31b3916da5fc7031022352c28477e1f5cb98ae8 I, Dan Benitah , hereby add my Signed-off-by to this commit: b7483b63e72e0b57eda0e0f3f901e23d515ab0c9 I, Dan Benitah , hereby add my Signed-off-by to this commit: 4cece7bbd2aa7a1fc3063d0b3e202ab032afc438 I, Dan Benitah , hereby add my Signed-off-by to this commit: 83f3073e4ba13f4fd13c4fec91a6af3f72f5e2fb I, Dan Benitah , hereby add my Signed-off-by to this commit: 4799d7a2f7aaa23056ecfc1902147e2bb2d0f2c4 I, Dan Benitah , hereby add my Signed-off-by to this commit: d2d823d39f018aad82f9a087c2e3a1be1838ca94 I, Dan Benitah , hereby add my Signed-off-by to this commit: c3d832c9c628bcfcd0eb9e0b3f42abac6b638d55 I, Dan Benitah , hereby add my Signed-off-by to this commit: ec96b524f37c7ff7716ec7f252146fc6ef0060d0 I, Dan Benitah , hereby add my Signed-off-by to this commit: 9567c45a54986746b562969936fadd562c387a2d I, Dan Benitah , hereby add my Signed-off-by to this commit: 45275148c63345b851be224630c170304f693226 I, Dan Benitah , hereby add my Signed-off-by to this commit: ff729b0542fe356419bf25625e7d60f8b1f7d9f7 I, Dan Benitah , hereby add my Signed-off-by to this commit: 1bdd2add3864d1aade424fa71de4ed5e4e44174a I, Dan Benitah , hereby add my Signed-off-by to this commit: 012ca2506822288b1728a88134659641654ee4ff I, Dan Benitah , hereby add my Signed-off-by to this commit: c7d45630ed0585be8672efd680b260b12ad83b88 I, Dan Benitah , hereby add my Signed-off-by to this commit: a51782f1327c1f09024211d0d289379a05ada224 I, Dan Benitah , hereby add my Signed-off-by to this commit: cc15035494ceaea0245aab1384465de9882a07b9 I, Dan Benitah , hereby add my Signed-off-by to this commit: 3c62bd49d58b43b67cd2adb1e9238cb9baacdc54 I, Dan Benitah , hereby add my Signed-off-by to this commit: 590f26299daf99f7531cc532aa54ab392f1103b9 Signed-off-by: Dan Benitah DCO Remediation Commit for danuw I, danuw , hereby add my Signed-off-by to this commit: fbc602cd4dede581ba7abd1cfbe024dd2a9c13c0 I, danuw , hereby add my Signed-off-by to this commit: ac1432f47ba1c8e949389d18cf491397aa9f051d I, danuw , hereby add my Signed-off-by to this commit: 8166cf283f80e7a63400b0626cc3408d639f9d25 I, danuw , hereby add my Signed-off-by to this commit: 48f117e7e9edc3f82be2e75418666c500d7994ac Signed-off-by: danuw * DCO Remediation Commit for danuw I, danuw , hereby add my Signed-off-by to this commit: fbc602cd4dede581ba7abd1cfbe024dd2a9c13c0 I, danuw , hereby add my Signed-off-by to this commit: ac1432f47ba1c8e949389d18cf491397aa9f051d I, danuw , hereby add my Signed-off-by to this commit: 8166cf283f80e7a63400b0626cc3408d639f9d25 I, danuw , hereby add my Signed-off-by to this commit: 48f117e7e9edc3f82be2e75418666c500d7994ac Signed-off-by: danuw Signed-off-by: Dan Benitah * Update baseURL docusaurus config Signed-off-by: Osama Jandali * Update docusaurus.config.js Signed-off-by: Osama Jandali * Revert changes Signed-off-by: Osama Jandali * Update domain from docusaurus Signed-off-by: Osama Jandali * Update docusaurus.config.js Signed-off-by: Osama Jandali * Create CNAME file Signed-off-by: Osama Jandali * clean up for title * Option to show the samples in the local docs using `nm run start-with-samples` . Will need deleting manually for now * seo for the doc site * docs fixes * layout update for blogs * Update SECURITY.md Updated with @Willmish recommendations for the document which were lost in a comment :+1: * DCO Remediation Commit for Vaughan Knight I, Vaughan Knight , hereby add my Signed-off-by to this commit: 2dc06f82e7bcc66854799f69340299873920df60 Signed-off-by: Vaughan Knight * Ensure pictures on the blog are correctly sized * updated links to videos * Update README.md * Change "Withing" to "Within" I'm genuinely unsure on what word this should be, possible "Using", but I'm sure it's not "Withing". Signed-off-by: Richard Jackson * Update README.md Two other typos - programatically -> programmatically - soruces -> sources Signed-off-by: Richard Jackson * first draft of release 1.3 changelog * Update README.md with updated link to overview Signed-off-by: Dan Benitah * Fix overview URL in README Signed-off-by: Szymon Duchniewicz * Update README.md Update FAQ link for Carbon Hack Signed-off-by: Russell Trow * Updates to the contribution documentation Updates to the contribution documentation * DCO Remediation Commit for Vaughan Knight I, Vaughan Knight , hereby add my Signed-off-by to this commit: f267ebabbffd7529b2fd0c9b09ad55fa79e614cd Signed-off-by: Vaughan Knight * Readme as project overview in docs, disclaimer update for graduated project, and docs deploying pipeline updates * DCO Remediation Commit for Vaughan Knight I, Vaughan Knight , hereby add my Signed-off-by to this commit: bd7ad15181b1d7f3c3f8c70585deba1df6488f44 I, Vaughan Knight , hereby add my Signed-off-by to this commit: 693beedf79a92800da2182ce977a7dd32f1170d5 I, Vaughan Knight , hereby add my Signed-off-by to this commit: 1d78756b93a9af75d548ce3b556cd76d7470c0d4 I, Vaughan Knight , hereby add my Signed-off-by to this commit: a7bc0fc47f2c1df5525c040d4ac3b54d5fd95b4e I, Vaughan Knight , hereby add my Signed-off-by to this commit: 571e21744493d6945f3c8e86a58be683b4a434a0 I, Vaughan Knight , hereby add my Signed-off-by to this commit: a07870f39713ea18af8079c3f168ced6a2468148 I, Vaughan Knight , hereby add my Signed-off-by to this commit: 0314c47d1eb4c2f8ac9be9b44554c9e248a72feb I, Vaughan Knight , hereby add my Signed-off-by to this commit: 3af6f73c2e5e21c7e67c978c676c58df363ffbe5 I, Vaughan Knight , hereby add my Signed-off-by to this commit: 932a66582e5f5f6696b8b97020ab29d4f4ce107c I, Vaughan Knight , hereby add my Signed-off-by to this commit: 9f09493d9e11905d878792093f17a955ce8c6226 I, Vaughan Knight , hereby add my Signed-off-by to this commit: 02ea4d68c35cd0309ac0bad0f0b7ec26f9d73bc9 I, Vaughan Knight , hereby add my Signed-off-by to this commit: 183a676dae57efbb9d9d575a27fde0acc3000f16 I, Vaughan Knight , hereby add my Signed-off-by to this commit: 0481081ed9f8573fa8612873db7dfc8d2644a104 Signed-off-by: Vaughan Knight * Migrate to .NET 8 https://github.com/Green-Software-Foundation/carbon-aware-sdk/pull/404 https://github.com/Green-Software-Foundation/carbon-aware-sdk/pull/420 https://github.com/Green-Software-Foundation/carbon-aware-sdk/pull/421 https://github.com/Green-Software-Foundation/carbon-aware-sdk/pull/422 Co-authored-by: Takuya Iwatsuka Signed-off-by: Yasumasa Suenaga * fix typos Signed-off-by: omahs <73983677+omahs@users.noreply.github.com> * fix typo Signed-off-by: omahs <73983677+omahs@users.noreply.github.com> * fix typo Signed-off-by: omahs <73983677+omahs@users.noreply.github.com> * fix typos Signed-off-by: omahs <73983677+omahs@users.noreply.github.com> * fix typos Signed-off-by: omahs <73983677+omahs@users.noreply.github.com> * fix typos Signed-off-by: omahs <73983677+omahs@users.noreply.github.com> * Update agenda-template.md Signed-off-by: Sophie Trinder <144015600+Sophietn@users.noreply.github.com> * Resolve NU1605 relating to System.IO.FileSystem.Primitives https://learn.microsoft.com/en-us/nuget/reference/errors-and-warnings/nu1605#example-3 Signed-off-by: Yasumasa Suenaga * Use RID rather than QEMU to build WebAPI container image https://devblogs.microsoft.com/dotnet/improving-multiplatform-container-support/ Signed-off-by: Yasumasa Suenaga * Separate OpenAPI document generation from build-env stage Signed-off-by: Yasumasa Suenaga * Create case-study-template.md Creating the case study template. Signed-off-by: Vaughan Knight * Update case-study-template.md Minor updates. Signed-off-by: Vaughan Knight * Up Helm chart version to 1.2.0 (#500) Signed-off-by: Yasumasa Suenaga * First draft of the ADR for watt time v3 changes First draft of the ADR for watt time v3 changes. Looking at path mappings and parameters. Still plenty to work on. * Initial changelog 1.4.0 (#511) Signed-off-by: Dan Benitah * overview.md: Fixed three broken links Signed-off-by: joecus1 * DCO Remediation Commit for joecus1 I, joecus1 , hereby add my Signed-off-by to this commit: c6b2c14a660e868a5d36bff6fef31aab2cbc826f Signed-off-by: joecus1 * Update enablement.md update older .Net reference Signed-off-by: nttDamien <125525959+nttDamien@users.noreply.github.com> * (fix) broken links - Update enablement.md Signed-off-by: nttDamien <125525959+nttDamien@users.noreply.github.com> * Moved ADR to correct location Moved ADR to correct location * Further updates for the watt time v2 to v3 upgrade Signed-off-by: Vaughan Knight * Further updates for the watt time v2 to v3 upgrade Signed-off-by: Vaughan Knight * Update SECURITY.md (.NET 8 upgrade) Signed-off-by: nttDamien <125525959+nttDamien@users.noreply.github.com> * Update enablement.md 2 missed references... Signed-off-by: nttDamien <125525959+nttDamien@users.noreply.github.com> * Update docusaurus.config.js removed banner's mention of Hack Signed-off-by: Dan Benitah * Update docusaurus.config.js Signed-off-by: Dan Benitah * Update 0016-watt-time-v3.md updated notes for BA Signed-off-by: Vaughan Knight * First draft of the ADR for watt time v3 changes First draft of the ADR for watt time v3 changes. Looking at path mappings and parameters. Still plenty to work on. * Moved ADR to correct location Moved ADR to correct location * Further updates for the watt time v2 to v3 upgrade Signed-off-by: Vaughan Knight * Update 0016-watt-time-v3.md updated notes for BA Signed-off-by: Vaughan Knight * Create 0016-watt-time-v3.md More updates. * Added base url to the configuration with validation Added AuthenticationBaseUrl to the configuration and updated Authentication to leverage the v3 authenication path - note the API is not updated and will require further updates. * Updated start and end configuration Updated start and end configuration to new values * Balancing Authority Parameter Renamed to Region Balancing Authority Renamed to Region. Does not include updates to API, just the Query String parameter. * Add example for 'podman play kube' (#340) * Add example for 'podman play kube' Signed-off-by: Yasumasa Suenaga * Update Swagger JSON URL Signed-off-by: Yasumasa Suenaga --------- Signed-off-by: Yasumasa Suenaga * Updates for historical data API Updates for historical data API * Removed accidental file Removed accidental file * Lots of test updates Lots of test updates, need to do some fixes. * Historical forecasts updated Historical forecasts updated * DCO Remediation Commit for Vaughan Knight I, Vaughan Knight , hereby add my Signed-off-by to this commit: b9490e425c291b75cc13ecd877a4b34ca96ab68c I, Vaughan Knight , hereby add my Signed-off-by to this commit: b991bac14c9864f331e56b739398b9675f23e37c I, Vaughan Knight , hereby add my Signed-off-by to this commit: e4f14944901e0ebecf4e4a7c8b2c543a87b37a7d I, Vaughan Knight , hereby add my Signed-off-by to this commit: b443e9eaa3cdcedd7cfab435e64f8a9999ca44d8 I, Vaughan Knight , hereby add my Signed-off-by to this commit: ab1205d699f99887e9a7e59f2bf8b0a692690df6 I, Vaughan Knight , hereby add my Signed-off-by to this commit: 7c115fa1213cbc042ffbe6e72f208a190b5d7584 I, Vaughan Knight , hereby add my Signed-off-by to this commit: e047c9a2711ef5aa2e5cb2cbb094fa21914eb8d5 I, Vaughan Knight , hereby add my Signed-off-by to this commit: aa81382704ccec7a8da108ef5c293845a9468c8f I, Vaughan Knight , hereby add my Signed-off-by to this commit: 8640c8cf242b1872701fa46b5ba338d28c09a512 I, Vaughan Knight , hereby add my Signed-off-by to this commit: 880fcf78c1e29342b17086bce6375d31a3dbb224 Signed-off-by: Vaughan Knight * Many tests reworked, a few to go Many tests reworked, a few to go. Consolidated a lot of the hand crafted json objects into objects that get serialized as the purist JsonObject format was prone to errors - in some cases tests were passing even with bad typing. * Further test updates Further test updates * Further updates, just 1 test left to remediate Further updates, just 1 test left to remediate * Updated to add authentication client to the service builder for the tests Updated to add authentication client to the service builder for the tests. All tests now passing. * Renaming of Balancing Authority to Region Renaming of Balancing Authority to Region through all code and comments. This will also need updating through documentation. * Fixed spelling error in latitude Fixed spelling error in latitude * Fixed a bug where location sources were loading twice Fixed a bug where location sources were loading twice. Added a semaphore to stop any threading issues, and also stopped it loading twice in the service configuration. * Fixed typo for method name Fixed typo for method name * DCO Remediation Commit for Vaughan Knight I, Vaughan Knight , hereby add my Signed-off-by to this commit: e324f365a3086830f3ac2d0e5ad4d37112fa5c6a I, Vaughan Knight , hereby add my Signed-off-by to this commit: be6663cba48dba0af2ace3db90519377101d2d4d I, Vaughan Knight , hereby add my Signed-off-by to this commit: 39e45a61b999f862b4b3cc7a6adb430b25dfe08d I, Vaughan Knight , hereby add my Signed-off-by to this commit: fbfcac1558d8990404c0605aad9325e220ab29c5 I, Vaughan Knight , hereby add my Signed-off-by to this commit: 9aabd27438d1d7e21de830314af6658ceaaad789 I, Vaughan Knight , hereby add my Signed-off-by to this commit: 8127a92a28c13e295d6ad618f707c1e817aa92b2 I, Vaughan Knight , hereby add my Signed-off-by to this commit: 4ff0151bd1528a6e00ff565e906d36caed86a0fb I, Vaughan Knight , hereby add my Signed-off-by to this commit: d93320f11fe205fd7d5f18cd559c05185ee84056 I, Vaughan Knight , hereby add my Signed-off-by to this commit: 91799f778b9b544575837b85f3eba27728218e1f I, Vaughan Knight , hereby add my Signed-off-by to this commit: 466581396933362dbb37c9d6f148b720a39c4599 Signed-off-by: Vaughan Knight * Updates based on code review for WattTime Tests Updates based on code review for WattTime Tests. Mostly cleanup of constants which were removed elsewhere in tests. * Cleaned up a lot of the string literals Cleaned up a lot of the string literals. They were causing too much fragility in the code base, and made it complex when updating the WattTime API. * More cleanup of some of the strings More cleanup of some of the strings. Creating consistency for using the test data on parameters and not just reponses also. * Updates to documentation and changelog Updates to documentation and changelog * Update azure-regions.json (#536) Latest azure-regions.json list Adds italynorth, polandcentral, spaincentral, mexicocentral, israelcentral, qatarcentral, brazilus, eastusstg (Also seems to remove trailing zeros in some existing coordinates) Signed-off-by: Dan Benitah * Update CHANGELOG.md for v1.5 Signed-off-by: Dan Benitah * Update CHANGELOG.md layout update Signed-off-by: Dan Benitah --------- Signed-off-by: Phil Huang Signed-off-by: tkuramoto33 <70622977+tkuramoto33@users.noreply.github.com> Signed-off-by: Rintaro Ikeda <51394766+rinikeda@users.noreply.github.com> Signed-off-by: Richard Jackson Signed-off-by: Yasumasa Suenaga Signed-off-by: JasonLuuk <96975358+JasonLuuk@users.noreply.github.com> Signed-off-by: Dan Benitah Signed-off-by: Vaughan Knight Signed-off-by: Sophie Trinder <144015600+Sophietn@users.noreply.github.com> Signed-off-by: danuw Signed-off-by: Osama Jandali Signed-off-by: Szymon Duchniewicz Signed-off-by: Russell Trow Signed-off-by: Yasumasa Suenaga Signed-off-by: omahs <73983677+omahs@users.noreply.github.com> Signed-off-by: joecus1 Signed-off-by: nttDamien <125525959+nttDamien@users.noreply.github.com> Co-authored-by: danuw Co-authored-by: Dan Benitah Co-authored-by: Phil Huang Co-authored-by: tkuramoto33 <70622977+tkuramoto33@users.noreply.github.com> Co-authored-by: Rintaro Ikeda <51394766+rinikeda@users.noreply.github.com> Co-authored-by: rinikeda Co-authored-by: Richard Jackson Co-authored-by: yasuenag Co-authored-by: JasonLuuk <96975358+JasonLuuk@users.noreply.github.com> Co-authored-by: Szymon Duchniewicz Co-authored-by: Sophie Trinder <144015600+Sophietn@users.noreply.github.com> Co-authored-by: Osama Jandali Co-authored-by: Szymon Duchniewicz Co-authored-by: Russell Trow Co-authored-by: Takuya Iwatsuka Co-authored-by: omahs <73983677+omahs@users.noreply.github.com> Co-authored-by: joecus1 Co-authored-by: nttDamien <125525959+nttDamien@users.noreply.github.com> --- CHANGELOG.md | 73 +++++ .../decisions/0016-watt-time-v3.md | 86 ++++++ .../tutorial-extras/carbon-aware-library.md | 2 +- casdk-docs/docusaurus.config.js | 2 +- samples/casdk-demo/README.md | 64 ++++ samples/casdk-demo/casdk-config.yaml.template | 6 + samples/casdk-demo/demo.sh | 55 ++++ samples/casdk-demo/demo.yaml | 47 +++ samples/casdk-demo/nginx-rp.conf | 16 + .../EmissionsForecastsCommandTests.cs | 2 +- .../mock/ElectricityMapDataSourceMocker.cs | 2 +- .../src/ElectricityMapsDataSource.cs | 2 +- .../ElectricityMapsFreeDataSourceMocker.cs | 2 +- .../mock/JsonDataSourceMocker.cs | 2 +- ...bonAware.DataSources.WattTime.Mocks.csproj | 1 + .../mock/WattTimeDataSourceMocker.cs | 111 ++++--- .../src/Client/IWattTimeClient.cs | 61 ++-- .../src/Client/WattTimeClient.cs | 106 ++++--- .../src/Client/WattTimeClientHttpException.cs | 2 +- .../ServiceCollectionExtensions.cs | 20 +- .../WattTimeClientConfiguration.cs | 17 +- .../src/Constants/Paths.cs | 6 +- .../src/Constants/QueryStrings.cs | 7 +- .../src/Constants/SignalTypes.cs | 6 + .../src/Model/BalancingAuthority.cs | 29 -- .../src/Model/Forecast.cs | 22 -- .../Model/ForecastEmissionsDataResponse.cs | 16 + .../src/Model/GridEmissionDataPoint.cs | 14 +- .../src/Model/GridEmissionsDataResponse.cs | 16 + .../src/Model/GridEmissionsMetaData.cs | 34 +++ .../src/Model/GridEmissionsModelData.cs | 16 + .../src/Model/HistoricalEmissionsData.cs | 15 + ...HistoricalForecastEmissionsDataResponse.cs | 16 + .../src/Model/RegionResponse.cs | 29 ++ .../src/WattTimeDataSource.cs | 98 ++++--- .../test/Client/TestData.cs | 81 ------ .../test/Client/WattTimeClientTests.cs | 273 +++++++++--------- .../test/Client/WattTimeTestData.cs | 128 ++++++++ .../ServiceCollectionExtensionTests.cs | 21 +- .../test/WattTimeDataSourceTests.cs | 175 +++++++---- .../src/LocationSource.cs | 43 ++- .../src/CarbonAware.WebApi.csproj | 2 + .../CarbonAwareControllerTests.cs | 2 +- .../src/Interfaces/IDataSourceMocker.cs | 2 +- .../src/Interfaces/IForecastDataSource.cs | 2 +- src/CarbonAware/src/NullForecastDataSource.cs | 2 +- .../ServiceCollectionExtensions.cs | 22 +- .../src/Handlers/ForecastHandler.cs | 2 +- .../test/Handlers/ForecastHandlerTests.cs | 4 +- src/data/location-sources/azure-regions.json | 50 +++- 50 files changed, 1254 insertions(+), 558 deletions(-) create mode 100644 casdk-docs/docs/architecture/decisions/0016-watt-time-v3.md create mode 100644 samples/casdk-demo/README.md create mode 100644 samples/casdk-demo/casdk-config.yaml.template create mode 100755 samples/casdk-demo/demo.sh create mode 100644 samples/casdk-demo/demo.yaml create mode 100644 samples/casdk-demo/nginx-rp.conf create mode 100644 src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Constants/SignalTypes.cs delete mode 100644 src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Model/BalancingAuthority.cs delete mode 100644 src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Model/Forecast.cs create mode 100644 src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Model/ForecastEmissionsDataResponse.cs create mode 100644 src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Model/GridEmissionsDataResponse.cs create mode 100644 src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Model/GridEmissionsMetaData.cs create mode 100644 src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Model/GridEmissionsModelData.cs create mode 100644 src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Model/HistoricalEmissionsData.cs create mode 100644 src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Model/HistoricalForecastEmissionsDataResponse.cs create mode 100644 src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Model/RegionResponse.cs delete mode 100644 src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/test/Client/TestData.cs create mode 100644 src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/test/Client/WattTimeTestData.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index d41501578..075990784 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,79 @@ All notable changes to the Carbon Aware SDK will be documented in this file. +## [1.5.0] - 2024-05 + +This is the WattTime v3 update. Most notable changes that may require action are for deployment configuration, and these are minor. + +### Added + +WattTime v3 API support. This is an inplace upgrade for v2. + +- [PR #532] Watt Time v3 Support ](https://github.com/Green-Software-Foundation/carbon-aware-sdk/pull/532) +- [PR #340] Add example for 'podman play kube' ](https://github.com/Green-Software-Foundation/carbon-aware-sdk/pull/340) +- [PR #536] Updated azure-regions.json with new regions ](https://github.com/Green-Software-Foundation/carbon-aware-sdk/pull/536) +- [#519] Remove hackathon sentence from our website banner ](https://github.com/Green-Software-Foundation/carbon-aware-sdk/issues/519) +- [#510] Gap Analysis for WattTime v3 ](https://github.com/Green-Software-Foundation/carbon-aware-sdk/issues/510) +- [#262] [Feature Contribution]: Publish the docker file in a docker registry ](https://github.com/Green-Software-Foundation/carbon-aware-sdk/issues/262) + +### Removed + +WattTime v2 API support due to v3 in place replacement. + +### Fixed + +- [PR #522] Remove Hack mention from the Docs's banner ](https://github.com/Green-Software-Foundation/carbon-aware-sdk/pull/522) +- [#535] [Bug]: Configuration for locations loads twice ](https://github.com/Green-Software-Foundation/carbon-aware-sdk/issues/535) +- [PR #516] Update published documentation to .NET 8 ](https://github.com/Green-Software-Foundation/carbon-aware-sdk/pull/516) +- [PR #515] overview.md: Fixed three broken links Signed-off-by: joecus1 `starttime` is now `start` and mandatory
  • `endtime` is now `end` and mandatory
  • `ba` is now `region`
  • `signal_type` added
    _Response_
  • `signal_type` added +| Forecast | Get forecast| /forecast | /forecast |
    No longer be used for historical data
    _Request_
  • `ba` is now `region`
  • `extended_forecast` removed
  • `horizon_hours` added
  • `signal_type` added
  • Historical forecasts are now at `/forecast/historical`
    _Response_
  • `signal_type` added +| Historical | Get historical forecast data | /historical (?) | /forecast/historical (?) | This changed signficantly.
    _Request_
  • `ba` is now `region`
  • `starttime` is now `start` and mandatory
  • `endtime` is now `end` and mandatory
  • `signal_type` added
    _Response_
  • `signal_type` added +| Balancing Authority From Location | Get balancing authority from location | /ba-from-loc | /region-from-loc | Check if the CA SDK uses BA at all

    _Request_
  • `name` is now `region_full_name`
  • `abbrev` is now `region`
  • `signal_type` added
    _Response_
  • `id` removed
  • `signal_type` added | +| Login | User login | https://api2.watttime.org/v2/login | https://api.watttime.org/login | Path has changed from being version specific to being no longer related to the API version.

    Updated in `WattTimeClient` to now have 2 HTTP clients to decouple versions from the login. + +### Query Strings + +#### Signal Type +Everything call takes an optional `signal_type` parameter that defaults to `co2_moer`. + +The following comes from `CarbonAware.DataSources.WattTime/src/Constants/QueryStrings.cs` and the changes are consistent with the discussion above. + +| Query String (v2) | Query String if Changed (v3) | Description | +|------------------------------------|----------------------------------|------------------------------| +| `ba` | `region` | Balancing Authority / Region | +| `starttime` | `start` | Start Time | +| `endtime` | `end` | End Time | +| `latitude` | - | Latitude | +| `longitude` | - | Longitude | +| `username` | - | Username | + +## Update Changes +With some of the changes to the code, some of the configuration will also needs to change. + +| Config (v2) | Config (v3) | Description | +|------------------------------------|----------------------------------|------------------------------| +| `BalancingAuthorityCacheTTL` | `RegionCacheTTL` | This is the cache for regions data in seconds, and has a default value of 1 day. | +| n/a | `AuthenticationBaseUrl` | **NEW** This is the base URL for the WattTime Authentication API and defaults to `https://api.watttime.org/` if not set. | + +Example below if set (note they do not have to be set) +```json +"wattTime_no-proxy": { + "Type": "WattTime", + "Username": "the_username", + "Password": "super_secret_secret", + "BaseURL": "https://api.watttime.org/v3/", + "AutenticationBaseURL": "https://api.watttime.org", // This is new but not mandatory + "RegionCacheTTL": 86400, // This is new but not mandatory + "Proxy": { + "UseProxy": false + } +``` +## Green Impact + +Neutral + diff --git a/casdk-docs/docs/tutorial-extras/carbon-aware-library.md b/casdk-docs/docs/tutorial-extras/carbon-aware-library.md index b47813f3b..2a8be2f3b 100644 --- a/casdk-docs/docs/tutorial-extras/carbon-aware-library.md +++ b/casdk-docs/docs/tutorial-extras/carbon-aware-library.md @@ -423,7 +423,7 @@ EmissionsForecast() #### Locations Each WattTime emissions data point is associated with a particular named -balancing authority. For transparency, this value is also used in +region often referred to as a balancing authority. For transparency, this value is also used in `EmissionsData` response objects. It is not overwritten to match the named datacenter provided by any request. diff --git a/casdk-docs/docusaurus.config.js b/casdk-docs/docusaurus.config.js index 8f42e12a3..54d59099e 100644 --- a/casdk-docs/docusaurus.config.js +++ b/casdk-docs/docusaurus.config.js @@ -130,7 +130,7 @@ const config = { id: 'announcementBar-0', // Increment on change // content: `⭐️ If you like Docusaurus, give it a star on GitHub and follow us on Twitter ${TwitterSvg}`, //content: `🎉️ Docusaurus v3.0 is now out! 🥳️`, - content:`\u26A0 Graduated Project: This project is a Graduated Project, supported by the Green Software Foundation. The publicly available version documented in the README is trusted by the GSF. New versions of the project may be released, or it may move to the Maintained or Archived Stage.

    🎉️ We are running a Hackathon! CarbonHack is open to all, including software practitioners and those with a passion for Green Software. Find out more on the CarbonHack website
    `, + content:`\u26A0 Graduated Project: This project is a Graduated Project, supported by the Green Software Foundation. The publicly available version documented in the README is trusted by the GSF. New versions of the project may be released, or it may move to the Maintained or Archived Stage. `, backgroundColor:'#EBF2D7', textColor:'#00524f' }, diff --git a/samples/casdk-demo/README.md b/samples/casdk-demo/README.md new file mode 100644 index 000000000..168cd5abd --- /dev/null +++ b/samples/casdk-demo/README.md @@ -0,0 +1,64 @@ +# Carbon Aware SDK demonstration on Podman + +This folder contains an example for Carbon Aware SDK and Swagger UI for the SDK. The user can demonstrate Carbon Aware SDK via Swagger UI with just one command. + +This demonstration uses `podman play kube` to deploy apps like a Kubernetes application. Deployment is defined in [demo.yaml](demo.yaml). + +## Requirements + +- Podman + +## Ports to open + +Podman creates virtual network, then all of containers in the deployment would be located flatten. So Each containers can access each other as a `localhost`. We need to consider TCP port: + +* 8080: Carbon Aware SDK +* 8081: Swagger UI +* 8082: NGINX (for reverse proxy) + +NGINX is a reverse proxy to both Carbon Aware SDK and Swagger UI. To avoid CORS error, you can access Swagger UI via NGINX ( http://localhost:8082/swagger-ui/ ). + +## Reverse proxy rule + +See [nginx-rp.conf](nginx-rp.conf) + +/ -> Carbon Aware SDK +/swagger.yaml -> OpenAPI document provided by Carbon Aware SDK +/swagger-ui/ -> Swagger UI for Carbon Aware SDK + +## How to run + +1. Set environment variables prefixed with `CASDK_`: e.g. `CASDK_DataSources__EmissionsDataSource` + +2. Start demonstration + +``` +./demo.sh start +``` + +:::warning + +* [demo.sh](demo.sh) would create `/tmp/casdk-config.yaml` which may contain credentials (e.g. API token of backend service). This file would be removed by `./demo.sh stop`. +* [demo.sh](demo.sh) would change security context of [nginx-rp.conf](nginx-rp.conf) to `container_file_t` if SELinux is enabled. It would not recover in `./demo.sh stop`, so you need to recover manually via `restorecon` if need. + +::: + +3. Access endpoints (e.g. http://localhost:8082/swagger-ui/ ) + +4. Stop demonstration + +``` +./demo.sh stop +``` + +## Example + +Run demonstration with ElectricityMapsFree datasource + +``` +export CASDK_DataSources__EmissionsDataSource=ElectricityMapsFree +export CASDK_DataSources__Configurations__ElectricityMapsFree__Type=ElectricityMapsFree +export CASDK_DataSources__Configurations__ElectricityMapsFree__token=YOUR_SECRET_TOKEN + +./demo.sh start +``` diff --git a/samples/casdk-demo/casdk-config.yaml.template b/samples/casdk-demo/casdk-config.yaml.template new file mode 100644 index 000000000..2f468c0eb --- /dev/null +++ b/samples/casdk-demo/casdk-config.yaml.template @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: casdk-config +data: + # Environment variables would be added by demo.sh diff --git a/samples/casdk-demo/demo.sh b/samples/casdk-demo/demo.sh new file mode 100755 index 000000000..aa5803577 --- /dev/null +++ b/samples/casdk-demo/demo.sh @@ -0,0 +1,55 @@ +#!/bin/sh + +THISFILE=$0 +BASEDIR=$(dirname $THISFILE) +SUBCMD=$1 +CONFIGMAP=/tmp/casdk-config.yaml + +help_and_exit () { + echo "Usage:" + echo " $THISFILE [start|stop]" + + rm -f $CONFIGMAP + exit 1 +} + +start () { + # Create ConfigMap for envvars in CASDK container + cp -f $BASEDIR/casdk-config.yaml.template $CONFIGMAP + for CASDK_ENV in `env | grep CASDK_`; do + KEY=`echo $CASDK_ENV | cut -d '=' -f 1 | sed -e 's/^CASDK_//'` + VALUE=`echo $CASDK_ENV | cut -d '=' -f '2'` + + echo " $KEY: $VALUE" >> $CONFIGMAP + done + + + # Change security context of nginx-rp.conf because it would be mounted by + # NGINX container in demo.yaml. + SELINUX_MODE=`getenforce 2>/dev/null` + if [ "$SELINUX_MODE" = 'Enforcing' ]; then + chcon -t container_file_t $BASEDIR/nginx-rp.conf + fi + + # Start Podman + # Move to BASEDIR because demo.yaml should refer nginx-rp.conf in that dir. + pushd $BASEDIR > /dev/null 2>&1 + podman play kube --configmap=$CONFIGMAP demo.yaml + popd > /dev/null 2>&1 +} + +stop () { + podman play kube --down $BASEDIR/demo.yaml + rm -f $CONFIGMAP +} + +case "$SUBCMD" in + start) + start;; + + stop) + stop;; + + *) + help_and_exit;; +esac diff --git a/samples/casdk-demo/demo.yaml b/samples/casdk-demo/demo.yaml new file mode 100644 index 000000000..1a753aa3a --- /dev/null +++ b/samples/casdk-demo/demo.yaml @@ -0,0 +1,47 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: casdk-demo + labels: + app: casdk-demo +spec: + selector: + matchLabels: + app: casdk-demo + template: + metadata: + labels: + app: casdk-demo + spec: + containers: + - name: carbon-aware-sdk + image: ghcr.io/green-software-foundation/carbon-aware-sdk:pre + envFrom: + - configMapRef: + name: casdk-config + ports: + - containerPort: 8080 + hostPort: 8080 + - name: swagger-ui + image: swaggerapi/swagger-ui + env: + - name: SWAGGER_JSON_URL + value: /swagger.yaml + - name: PORT + value: "8081" + ports: + - containerPort: 8081 + hostPort: 8081 + - name: nginx + image: nginx + ports: + - containerPort: 8082 + hostPort: 8082 + volumeMounts: + - name: rp-config + mountPath: /etc/nginx/conf.d/default.conf + volumes: + - name: rp-config + hostPath: + path: nginx-rp.conf + type: File diff --git a/samples/casdk-demo/nginx-rp.conf b/samples/casdk-demo/nginx-rp.conf new file mode 100644 index 000000000..9efbc60ef --- /dev/null +++ b/samples/casdk-demo/nginx-rp.conf @@ -0,0 +1,16 @@ +server { + listen 8082; + + location / { + proxy_pass http://localhost:8080; + } + + location /swagger.yaml { + proxy_pass http://localhost:8080/api/v1/swagger.yaml; + } + + location /swagger-ui/ { + proxy_pass http://localhost:8081/; + } + +} diff --git a/src/CarbonAware.CLI/test/integrationTests/Commands/EmissionsForecasts/EmissionsForecastsCommandTests.cs b/src/CarbonAware.CLI/test/integrationTests/Commands/EmissionsForecasts/EmissionsForecastsCommandTests.cs index 2c232d187..5daf173c8 100644 --- a/src/CarbonAware.CLI/test/integrationTests/Commands/EmissionsForecasts/EmissionsForecastsCommandTests.cs +++ b/src/CarbonAware.CLI/test/integrationTests/Commands/EmissionsForecasts/EmissionsForecastsCommandTests.cs @@ -99,7 +99,7 @@ public async Task EmissionsForecasts_RequestedAtOptions_ReturnsExpectedData() IgnoreTestForDataSource("data source does not implement '--requested-at'", DataSourceType.ElectricityMaps); // Arrange - _dataSourceMocker.SetupBatchForecastMock(); + _dataSourceMocker.SetupHistoricalBatchForecastMock(); // Act var exitCode = await InvokeCliAsync($"emissions-forecasts -l eastus -r 2022-09-01"); diff --git a/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMaps/mock/ElectricityMapDataSourceMocker.cs b/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMaps/mock/ElectricityMapDataSourceMocker.cs index aa36674d7..b10a0a288 100644 --- a/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMaps/mock/ElectricityMapDataSourceMocker.cs +++ b/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMaps/mock/ElectricityMapDataSourceMocker.cs @@ -126,7 +126,7 @@ public void SetupDataMock(DateTimeOffset start, DateTimeOffset end, string locat SetupResponseGivenGetRequest(Paths.PastRange, pastRange); } - public void SetupBatchForecastMock() + public void SetupHistoricalBatchForecastMock() { throw new NotImplementedException(); } diff --git a/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMaps/src/ElectricityMapsDataSource.cs b/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMaps/src/ElectricityMapsDataSource.cs index 01e494bbd..acfc1339e 100644 --- a/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMaps/src/ElectricityMapsDataSource.cs +++ b/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMaps/src/ElectricityMapsDataSource.cs @@ -72,7 +72,7 @@ private static EmissionsForecast ToEmissionsForecast(Location location, Forecast } /// - public async Task GetCarbonIntensityForecastAsync(Location location, DateTimeOffset requestedAt) + public async Task GetHistoricalCarbonIntensityForecastAsync(Location location, DateTimeOffset requestedAt) { await Task.Run(() => true); throw new NotImplementedException(); diff --git a/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMapsFree/mock/ElectricityMapsFreeDataSourceMocker.cs b/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMapsFree/mock/ElectricityMapsFreeDataSourceMocker.cs index f8a645aca..7ddf800b1 100644 --- a/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMapsFree/mock/ElectricityMapsFreeDataSourceMocker.cs +++ b/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMapsFree/mock/ElectricityMapsFreeDataSourceMocker.cs @@ -49,7 +49,7 @@ public void SetupForecastMock() throw new NotImplementedException(); } - public void SetupBatchForecastMock() + public void SetupHistoricalBatchForecastMock() { throw new NotImplementedException(); } diff --git a/src/CarbonAware.DataSources/CarbonAware.DataSources.Json/mock/JsonDataSourceMocker.cs b/src/CarbonAware.DataSources/CarbonAware.DataSources.Json/mock/JsonDataSourceMocker.cs index ee7bf4ba5..5e09ba3de 100644 --- a/src/CarbonAware.DataSources/CarbonAware.DataSources.Json/mock/JsonDataSourceMocker.cs +++ b/src/CarbonAware.DataSources/CarbonAware.DataSources.Json/mock/JsonDataSourceMocker.cs @@ -42,7 +42,7 @@ public void Initialize() { } public void Reset() { } public void Dispose() { } - public void SetupBatchForecastMock() + public void SetupHistoricalBatchForecastMock() { throw new NotImplementedException(); } diff --git a/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/mock/CarbonAware.DataSources.WattTime.Mocks.csproj b/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/mock/CarbonAware.DataSources.WattTime.Mocks.csproj index 061c0cbab..1649e1ed9 100644 --- a/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/mock/CarbonAware.DataSources.WattTime.Mocks.csproj +++ b/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/mock/CarbonAware.DataSources.WattTime.Mocks.csproj @@ -13,6 +13,7 @@ + diff --git a/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/mock/WattTimeDataSourceMocker.cs b/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/mock/WattTimeDataSourceMocker.cs index 21ce7f66e..c21e759f0 100644 --- a/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/mock/WattTimeDataSourceMocker.cs +++ b/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/mock/WattTimeDataSourceMocker.cs @@ -1,6 +1,7 @@ -using CarbonAware.Interfaces; +using CarbonAware.DataSources.WattTime.Client.Tests; using CarbonAware.DataSources.WattTime.Constants; using CarbonAware.DataSources.WattTime.Model; +using CarbonAware.Interfaces; using System.Net; using System.Net.Mime; using System.Text.Json; @@ -13,11 +14,11 @@ internal class WattTimeDataSourceMocker : IDataSourceMocker { protected WireMockServer _server; - private static readonly BalancingAuthority defaultBalancingAuthority = new() + private static readonly RegionResponse defaultRegion = new() { - Id = 12345, - Abbreviation = "TEST_BA", - Name = "Test Balancing Authority" + Region = WattTimeTestData.Constants.Region, + RegionFullName = WattTimeTestData.Constants.RegionFullName, + SignalType = WattTimeTestData.Constants.SignalType }; private static readonly LoginResult defaultLoginResult = new() { Token = "myDefaultToken123" }; @@ -26,6 +27,8 @@ public WattTimeDataSourceMocker() { _server = WireMockServer.Start(); Environment.SetEnvironmentVariable("DataSources__Configurations__WattTime__BaseURL", _server.Url!); + Environment.SetEnvironmentVariable("DataSources__Configurations__WattTime__AuthenticationBaseUrl", _server.Url!); + Initialize(); } @@ -39,20 +42,30 @@ public void SetupDataMock(DateTimeOffset start, DateTimeOffset end, string locat { var newDataPoint = new GridEmissionDataPoint() { - BalancingAuthorityAbbreviation = defaultBalancingAuthority.Abbreviation, PointTime = pointTime, - Value = 999.99F, - Version = "1.0", - Datatype = "dt", - Frequency = 300, - Market = "mkt", + Value = WattTimeTestData.Constants.Value, + Version = WattTimeTestData.Constants.Version, + Frequency = WattTimeTestData.Constants.Frequency, + Market = WattTimeTestData.Constants.Market }; data.Add(newDataPoint); pointTime = newDataPoint.PointTime + duration; } - SetupResponseGivenGetRequest(Paths.Data, JsonSerializer.Serialize(data)); + var meta = new GridEmissionsMetaData() + { + Region = defaultRegion.Region, + SignalType = WattTimeTestData.Constants.SignalType + }; + + var gridEmissionsResponse = new GridEmissionsDataResponse() + { + Data = data, + Meta = meta + }; + + SetupResponseGivenGetRequest(Paths.Data, JsonSerializer.Serialize(gridEmissionsResponse)); } public void SetupForecastMock() @@ -63,75 +76,93 @@ public void SetupForecastMock() var start = new DateTimeOffset(((curr.Ticks + d.Ticks - 1) / d.Ticks) * d.Ticks, TimeSpan.Zero); var end = start + TimeSpan.FromDays(1.0); var pointTime = start; - var ForecastData = new List(); + var forecastData = new List(); var currValue = 200.0F; while (pointTime < end) { var newForecastPoint = new GridEmissionDataPoint() { - BalancingAuthorityAbbreviation = defaultBalancingAuthority.Abbreviation, - Datatype = "dt", - Frequency = 300, - Market = "mkt", + Frequency = WattTimeTestData.Constants.Frequency, + Market = WattTimeTestData.Constants.Market, PointTime = start, Value = currValue, - Version = "1.0" + Version = WattTimeTestData.Constants.Version }; newForecastPoint.PointTime = pointTime; newForecastPoint.Value = currValue; - ForecastData.Add(newForecastPoint); + forecastData.Add(newForecastPoint); pointTime = pointTime + TimeSpan.FromMinutes(5); currValue = currValue + 5.0F; } - var forecast = new Forecast() + var meta = new GridEmissionsMetaData() { - ForecastData = ForecastData, - GeneratedAt = new DateTimeOffset(2022, 1, 1, 0, 0, 0, TimeSpan.Zero) + Region = defaultRegion.Region, + SignalType = WattTimeTestData.Constants.SignalType, + GeneratedAt = WattTimeTestData.Constants.GeneratedAt }; - SetupResponseGivenGetRequest(Paths.Forecast, JsonSerializer.Serialize(forecast)); + + var forecastResponse = new ForecastEmissionsDataResponse() + { + Data = forecastData, + Meta = meta + }; + + SetupResponseGivenGetRequest(Paths.Forecast, JsonSerializer.Serialize(forecastResponse)); } - public void SetupBatchForecastMock() + public void SetupHistoricalBatchForecastMock() { var start = new DateTimeOffset(2021, 9, 1, 8, 30, 0, TimeSpan.Zero); var end = start + TimeSpan.FromDays(1.0); var pointTime = start; - var ForecastData = new List(); + var forecastData = new List(); var currValue = 200.0F; while (pointTime < end) { var newForecastPoint = new GridEmissionDataPoint() { - BalancingAuthorityAbbreviation = defaultBalancingAuthority.Abbreviation, - Datatype = "dt", - Frequency = 300, - Market = "mkt", + Frequency = WattTimeTestData.Constants.Frequency, + Market = WattTimeTestData.Constants.Market, PointTime = start, Value = currValue, - Version = "1.0" + Version = WattTimeTestData.Constants.Version }; newForecastPoint.PointTime = pointTime; newForecastPoint.Value = currValue; - ForecastData.Add(newForecastPoint); + forecastData.Add(newForecastPoint); pointTime = pointTime + TimeSpan.FromMinutes(5); currValue = currValue + 5.0F; } - var forecastData = new List { - new Forecast() + var meta = new GridEmissionsMetaData() + { + Region = defaultRegion.Region, + SignalType = WattTimeTestData.Constants.SignalType, + GeneratedAt = WattTimeTestData.Constants.GeneratedAt + }; + + var historicalForecastResponse = new HistoricalForecastEmissionsDataResponse() + { + Data = new List() { - ForecastData = ForecastData, - GeneratedAt = new DateTimeOffset(2022, 1, 1, 0, 0, 0, TimeSpan.Zero) - } + new HistoricalEmissionsData() + { + Forecast = forecastData, + GeneratedAt = WattTimeTestData.Constants.GeneratedAt + } + }, + Meta = meta }; - SetupResponseGivenGetRequest(Paths.Forecast, JsonSerializer.Serialize(forecastData)); + + + SetupResponseGivenGetRequest(Paths.ForecastHistorical, JsonSerializer.Serialize(historicalForecastResponse)); } public void Initialize() { - SetupBaMock(); + SetupRegionMock(); SetupLoginMock(); } @@ -156,8 +187,8 @@ private void SetupResponseGivenGetRequest(string path, string body) .WithBody(body) ); } - private void SetupBaMock(BalancingAuthority? content = null) => - SetupResponseGivenGetRequest(Paths.BalancingAuthorityFromLocation, JsonSerializer.Serialize(content ?? defaultBalancingAuthority)); + private void SetupRegionMock(RegionResponse? content = null) => + SetupResponseGivenGetRequest(Paths.RegionFromLocation, JsonSerializer.Serialize(content ?? defaultRegion)); private void SetupLoginMock(LoginResult? content = null) => SetupResponseGivenGetRequest(Paths.Login, JsonSerializer.Serialize(content ?? defaultLoginResult)); diff --git a/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Client/IWattTimeClient.cs b/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Client/IWattTimeClient.cs index fbe1fdef1..f69cbf11c 100644 --- a/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Client/IWattTimeClient.cs +++ b/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Client/IWattTimeClient.cs @@ -8,92 +8,93 @@ namespace CarbonAware.DataSources.WattTime.Client; internal interface IWattTimeClient { public const string NamedClient = "WattTimeClient"; + public const string NamedAuthenticationClient = "WattTimeAuthenticationClient"; /// - /// Async method to get observed emission data for a given balancing authority and time period. + /// Async method to get observed emission data for a given region and time period. /// - /// Balancing authority abbreviation + /// Region abbreviation /// Start time of the time period /// End time of the time period /// An which contains all emissions data points in a period. /// Can be thrown when errors occur connecting to WattTime client. See the WattTimeClientException class for documentation of expected status codes. - Task> GetDataAsync(string balancingAuthorityAbbreviation, DateTimeOffset startTime, DateTimeOffset endTime); + Task GetDataAsync(string regionAbbreviation, DateTimeOffset startTime, DateTimeOffset endTime); /// - /// Async method to get observed emission data for a given balancing authority and time period. + /// Async method to get observed emission data for a given region and time period. /// - /// Balancing authority + /// Region /// Start time of the time period /// End time of the time period /// An which contains all emissions data points in a period. /// Can be thrown when errors occur connecting to WattTime client. See the WattTimeClientException class for documentation of expected status codes. - Task> GetDataAsync(BalancingAuthority balancingAuthority, DateTimeOffset startTime, DateTimeOffset endTime); + Task GetDataAsync(RegionResponse region, DateTimeOffset startTime, DateTimeOffset endTime); /// - /// Async method to get the most recent 24 hour forecasted emission data for a given balancing authority. + /// Async method to get the most recent 24 hour forecasted emission data for a given region. /// - /// Balancing authority abbreviation + /// region abbreviation /// An which contains forecasted emissions data points. /// Can be thrown when errors occur connecting to WattTime client. See the WattTimeClientException class for documentation of expected status codes. - Task GetCurrentForecastAsync(string balancingAuthorityAbbreviation); + Task GetCurrentForecastAsync(string regionAbbreviation); /// - /// Async method to get the most recent 24 hour forecasted emission data for a given balancing authority. + /// Async method to get the most recent 24 hour forecasted emission data for a given region. /// - /// Balancing authority + /// region /// An which contains forecasted emissions data points. /// Can be thrown when errors occur connecting to WattTime client. See the WattTimeClientException class for documentation of expected status codes. - Task GetCurrentForecastAsync(BalancingAuthority balancingAuthority); + Task GetCurrentForecastAsync(RegionResponse region); /// - /// Async method to get generated forecast at requested time and balancing authority. + /// Async method to get generated forecast at requested time and region. /// - /// Balancing authority abbreviation + /// region abbreviation /// The historical time used to fetch the most recent forecast generated as of that time. /// An which contains forecasted emissions data points or null if no Forecast generated at the requested time. /// Can be thrown when errors occur connecting to WattTime client. See the WattTimeClientException class for documentation of expected status codes. - Task GetForecastOnDateAsync(string balancingAuthorityAbbreviation, DateTimeOffset requestedAt); + Task GetForecastOnDateAsync(string region, DateTimeOffset requestedAt); /// - /// Async method to get generated forecast at requested time and balancing authority. + /// Async method to get generated forecast at requested time and region. /// - /// Balancing authority + /// region /// The historical time used to fetch the most recent forecast generated as of that time. /// An which contains forecasted emissions data points or null if no Forecast generated at the requested time. /// Can be thrown when errors occur connecting to WattTime client. See the WattTimeClientException class for documentation of expected status codes. - Task GetForecastOnDateAsync(BalancingAuthority balancingAuthority, DateTimeOffset requestedAt); + Task GetForecastOnDateAsync(RegionResponse region, DateTimeOffset requestedAt); /// - /// Async method to get the balancing authority for a given location. + /// Async method to get the region for a given location. /// /// Latitude of the location /// Longitude of the location - /// An which contains the balancing authority details. + /// An which contains the region details. /// Can be thrown when errors occur connecting to WattTime client. See the WattTimeClientException class for documentation of expected status codes. - Task GetBalancingAuthorityAsync(string latitude, string longitude); + Task GetRegionAsync(string latitude, string longitude); /// - /// Async method to get the balancing authority abbreviation for a given location. + /// Async method to get the region abbreviation for a given location. /// /// Latitude of the location /// Longitude of the location - /// A string which contains the balancing authority details. + /// A string which contains the region details. /// Can be thrown when errors occur connecting to WattTime client. See the WattTimeClientException class for documentation of expected status codes. - Task GetBalancingAuthorityAbbreviationAsync(string latitude, string longitude); + Task GetRegionAbbreviationAsync(string latitude, string longitude); /// - /// Async method to get binary data (representing a zip file) of the historical emissions data for the given balancing authority. + /// Async method to get binary data (representing a zip file) of the historical emissions data for the given region. /// - /// Balancing authority abbreviation + /// region abbreviation /// An which contains the binary data stream of the .zip file. /// Can be thrown when errors occur connecting to WattTime client. See the WattTimeClientException class for documentation of expected status codes. - Task GetHistoricalDataAsync(string balancingAuthorityAbbreviation); + Task GetHistoricalDataAsync(string regionAbbreviation); /// - /// Async method to get binary data (representing a zip file) of the historical emissions data for the given balancing authority. + /// Async method to get binary data (representing a zip file) of the historical emissions data for the given region. /// - /// Balancing authority + /// region /// An which contains the data Stream of the .zip file. /// Can be thrown when errors occur connecting to WattTime client. See the WattTimeClientException class for documentation of expected status codes. - Task GetHistoricalDataAsync(BalancingAuthority balancingAuthority); + Task GetHistoricalDataAsync(RegionResponse region); } diff --git a/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Client/WattTimeClient.cs b/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Client/WattTimeClient.cs index ed4e10e5d..3dba1c647 100644 --- a/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Client/WattTimeClient.cs +++ b/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Client/WattTimeClient.cs @@ -25,6 +25,7 @@ internal class WattTimeClient : IWattTimeClient }; private HttpClient _client; + private HttpClient _authenticationClient; private IOptionsMonitor _configurationMonitor { get; } @@ -37,123 +38,133 @@ internal class WattTimeClient : IWattTimeClient public WattTimeClient(IHttpClientFactory factory, IOptionsMonitor configurationMonitor, ILogger log, IMemoryCache memoryCache) { _client = factory.CreateClient(IWattTimeClient.NamedClient); + _authenticationClient = factory.CreateClient(IWattTimeClient.NamedAuthenticationClient); + _configurationMonitor = configurationMonitor; _log = log; _configuration.Validate(); _client.BaseAddress = new Uri(this._configuration.BaseUrl); _client.DefaultRequestHeaders.Accept.Clear(); _client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue(MediaTypeNames.Application.Json)); + + _authenticationClient.BaseAddress = new Uri(this._configuration.AuthenticationBaseUrl); + _authenticationClient.DefaultRequestHeaders.Accept.Clear(); + _authenticationClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue(MediaTypeNames.Application.Json)); + _memoryCache = memoryCache; } /// - public async Task> GetDataAsync(string balancingAuthorityAbbreviation, DateTimeOffset startTime, DateTimeOffset endTime) + public async Task GetDataAsync(string regionAbbreviation, DateTimeOffset startTime, DateTimeOffset endTime) { _log.LogInformation("Requesting grid emission data using start time {startTime} and endTime {endTime}", startTime, endTime); var parameters = new Dictionary() { - { QueryStrings.BalancingAuthorityAbbreviation, balancingAuthorityAbbreviation }, + { QueryStrings.Region, regionAbbreviation }, { QueryStrings.StartTime, startTime.ToUniversalTime().ToString("O", CultureInfo.InvariantCulture) }, - { QueryStrings.EndTime, endTime.ToUniversalTime().ToString("O", CultureInfo.InvariantCulture) } + { QueryStrings.EndTime, endTime.ToUniversalTime().ToString("O", CultureInfo.InvariantCulture) }, + { QueryStrings.SignalType, SignalTypes.co2_moer}, }; var tags = new Dictionary() { - { QueryStrings.BalancingAuthorityAbbreviation, balancingAuthorityAbbreviation } + { QueryStrings.Region, regionAbbreviation } }; using (var result = await this.MakeRequestGetStreamAsync(Paths.Data, parameters, tags)) { - return await JsonSerializer.DeserializeAsync>(result, _options) ?? throw new WattTimeClientException($"Error getting forecasts for {balancingAuthorityAbbreviation}"); + return await JsonSerializer.DeserializeAsync(result, _options) ?? throw new WattTimeClientException($"Error getting forecasts for {regionAbbreviation}"); } } /// - public Task> GetDataAsync(BalancingAuthority balancingAuthority, DateTimeOffset startTime, DateTimeOffset endTime) + public Task GetDataAsync(RegionResponse region, DateTimeOffset startTime, DateTimeOffset endTime) { - return this.GetDataAsync(balancingAuthority.Abbreviation, startTime, endTime); + return this.GetDataAsync(region.Region, startTime, endTime); } /// - public async Task GetCurrentForecastAsync(string balancingAuthorityAbbreviation) + public async Task GetCurrentForecastAsync(string region) { - _log.LogInformation("Requesting current forecast from balancing authority {balancingAuthority}", balancingAuthorityAbbreviation); + _log.LogInformation("Requesting current forecast from region: {region}", region); var parameters = new Dictionary() { - { QueryStrings.BalancingAuthorityAbbreviation, balancingAuthorityAbbreviation } + { QueryStrings.Region, region }, + { QueryStrings.SignalType, SignalTypes.co2_moer } }; var tags = new Dictionary() { - { QueryStrings.BalancingAuthorityAbbreviation, balancingAuthorityAbbreviation } + { QueryStrings.Region, region } }; var result = await this.MakeRequestGetStreamAsync(Paths.Forecast, parameters, tags); - var forecast = await JsonSerializer.DeserializeAsync(result, _options) ?? throw new WattTimeClientException($"Error getting forecast for {balancingAuthorityAbbreviation}"); + var forecast = await JsonSerializer.DeserializeAsync(result, _options) ?? throw new WattTimeClientException($"Error getting forecast for {region}"); return forecast; } /// - public Task GetCurrentForecastAsync(BalancingAuthority balancingAuthority) + public Task GetCurrentForecastAsync(RegionResponse region) { - return this.GetCurrentForecastAsync(balancingAuthority.Abbreviation); + return this.GetCurrentForecastAsync(region.Region); } /// - public async Task GetForecastOnDateAsync(string balancingAuthorityAbbreviation, DateTimeOffset requestedAt) + public async Task GetForecastOnDateAsync(string region, DateTimeOffset requestedAt) { - _log.LogInformation($"Requesting forecast from balancingAuthority {balancingAuthorityAbbreviation} generated at {requestedAt}."); + _log.LogInformation($"Requesting forecast from region {region} generated at {requestedAt}."); var parameters = new Dictionary() { - { QueryStrings.BalancingAuthorityAbbreviation, balancingAuthorityAbbreviation }, + { QueryStrings.Region, region }, { QueryStrings.StartTime, requestedAt.ToUniversalTime().ToString("O", CultureInfo.InvariantCulture) }, - { QueryStrings.EndTime, requestedAt.ToUniversalTime().ToString("O", CultureInfo.InvariantCulture) } + { QueryStrings.EndTime, requestedAt.ToUniversalTime().ToString("O", CultureInfo.InvariantCulture) }, + { QueryStrings.SignalType, SignalTypes.co2_moer } }; var tags = new Dictionary() { - { QueryStrings.BalancingAuthorityAbbreviation, balancingAuthorityAbbreviation } + { QueryStrings.Region, region } }; - using (var result = await this.MakeRequestGetStreamAsync(Paths.Forecast, parameters, tags)) + using (var result = await this.MakeRequestGetStreamAsync(Paths.ForecastHistorical, parameters, tags)) { - var forecasts = await JsonSerializer.DeserializeAsync>(result, _options) ?? throw new WattTimeClientException($"Error getting forecasts for {balancingAuthorityAbbreviation}"); - return forecasts.FirstOrDefault(); + var historicalForecastResponse = await JsonSerializer.DeserializeAsync(result, _options) ?? throw new WattTimeClientException($"Error getting forecasts for {region}"); + return historicalForecastResponse; } } /// - public Task GetForecastOnDateAsync(BalancingAuthority balancingAuthority, DateTimeOffset requestedAt) + public Task GetForecastOnDateAsync(RegionResponse region, DateTimeOffset requestedAt) { - return this.GetForecastOnDateAsync(balancingAuthority.Abbreviation, requestedAt); + return this.GetForecastOnDateAsync(region.Region, requestedAt); } /// - public async Task GetBalancingAuthorityAsync(string latitude, string longitude) + public async Task GetRegionAsync(string latitude, string longitude) { - _log.LogInformation("Requesting balancing authority for lattitude {lattitude} and longitude {longitude}", latitude, longitude); - return await GetBalancingAuthorityFromCacheAsync(latitude, longitude); + _log.LogInformation("Requesting region for latitude {latitude} and longitude {longitude}", latitude, longitude); + return await GetRegionFromCacheAsync(latitude, longitude); } /// - public async Task GetBalancingAuthorityAbbreviationAsync(string latitude, string longitude) + public async Task GetRegionAbbreviationAsync(string latitude, string longitude) { - return (await this.GetBalancingAuthorityAsync(latitude, longitude))?.Abbreviation; + return (await this.GetRegionAsync(latitude, longitude))?.Region; } /// - public async Task GetHistoricalDataAsync(string balancingAuthorityAbbreviation) + public async Task GetHistoricalDataAsync(string regionAbbreviation) { - _log.LogInformation("Requesting historical data for balancing authority {balancingAuthority}", balancingAuthorityAbbreviation); + _log.LogInformation("Requesting historical data for region {regionAbbreviation}", regionAbbreviation); var parameters = new Dictionary() { - { QueryStrings.BalancingAuthorityAbbreviation, balancingAuthorityAbbreviation } + { QueryStrings.Region, regionAbbreviation } }; var url = BuildUrlWithQueryString(Paths.Historical, parameters); @@ -168,9 +179,9 @@ public async Task GetHistoricalDataAsync(string balancingAuthorityAbbrev } /// - public Task GetHistoricalDataAsync(BalancingAuthority balancingAuthority) + public Task GetHistoricalDataAsync(RegionResponse region) { - return this.GetHistoricalDataAsync(balancingAuthority.Abbreviation); + return this.GetHistoricalDataAsync(region.Region); } private async Task GetAsyncWithAuthRetry(string uriPath) @@ -215,7 +226,7 @@ private async Task UpdateAuthTokenAsync() _log.LogInformation("Attempting to log in user {username}", this._configuration.Username); this.SetBasicAuthenticationHeader(); - HttpResponseMessage response = await this._client.GetAsync(Paths.Login); + HttpResponseMessage response = await this._authenticationClient.GetAsync(Paths.Login); LoginResult? data = null; @@ -238,7 +249,8 @@ private async Task UpdateAuthTokenAsync() private void SetBasicAuthenticationHeader() { var authToken = Encoding.UTF8.GetBytes($"{this._configuration.Username}:{this._configuration.Password}"); - this._client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(AuthenticationHeaderTypes.Basic, Convert.ToBase64String(authToken)); + //this._client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(AuthenticationHeaderTypes.Basic, Convert.ToBase64String(authToken)); + this._authenticationClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(AuthenticationHeaderTypes.Basic, Convert.ToBase64String(authToken)); } internal void SetBearerAuthenticationHeader(string token) @@ -281,28 +293,30 @@ private string BuildUrlWithQueryString(string url, IDictionary q return result; } - private async Task GetBalancingAuthorityFromCacheAsync(string latitude, string longitude) + private async Task GetRegionFromCacheAsync(string latitude, string longitude) { var key = new Tuple(latitude, longitude); - var balancingAuthority = await this._memoryCache.GetOrCreateAsync(key, async entry => + var region = await this._memoryCache.GetOrCreateAsync(key, async entry => { var parameters = new Dictionary() { { QueryStrings.Latitude, latitude }, - { QueryStrings.Longitude, longitude } + { QueryStrings.Longitude, longitude }, + { QueryStrings.SignalType, SignalTypes.co2_moer} }; var tags = new Dictionary() { { QueryStrings.Latitude, latitude }, - { QueryStrings.Longitude, longitude } + { QueryStrings.Longitude, longitude }, + { QueryStrings.SignalType, SignalTypes.co2_moer } }; - var result = await this.MakeRequestGetStreamAsync(Paths.BalancingAuthorityFromLocation, parameters, tags); - var baValue = await JsonSerializer.DeserializeAsync(result, _options) ?? throw new WattTimeClientException($"Error getting Balancing Authority for latitude {latitude} and longitude {longitude}"); - entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(_configuration.BalancingAuthorityCacheTTL); - entry.Value = baValue; - return baValue; + var result = await this.MakeRequestGetStreamAsync(Paths.RegionFromLocation, parameters, tags); + var regionResponse = await JsonSerializer.DeserializeAsync(result, _options) ?? throw new WattTimeClientException($"Error getting Region for latitude {latitude} and longitude {longitude}"); + entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(_configuration.RegionCacheTTL); + entry.Value = regionResponse; + return regionResponse; }); - return balancingAuthority; + return region; } } diff --git a/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Client/WattTimeClientHttpException.cs b/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Client/WattTimeClientHttpException.cs index 39f9cacf4..58e31eb8c 100644 --- a/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Client/WattTimeClientHttpException.cs +++ b/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Client/WattTimeClientHttpException.cs @@ -22,7 +22,7 @@ public WattTimeClientHttpException(string message, HttpResponseMessage response) /// Gets the status code for the exception. See remarks for the status codes that can be returned. /// /// - /// 400: Returned when the lattitude/longitude provided aren't associated with a known balancing authority. + /// 400: Returned when the lattitude/longitude provided aren't associated with a known region. /// 401: Returned when no authorization header is passed. You should not expect to receive this status code. /// 403: Returned when an invalid username or password is used for login. Please check your configuration and verify your account when this error is received. /// 429: Returned when the number of requests has exceeded the WattTime rate limit, currently at 3,000 per rolling 5 minute window. For current limits, see https://www.watttime.org/api-documentation/#restrictions diff --git a/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Configuration/ServiceCollectionExtensions.cs b/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Configuration/ServiceCollectionExtensions.cs index b3ba9d358..d05f1a33a 100644 --- a/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Configuration/ServiceCollectionExtensions.cs +++ b/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Configuration/ServiceCollectionExtensions.cs @@ -39,6 +39,7 @@ private static void AddWattTimeClient(IServiceCollection services, IConfiguratio }); var httpClientBuilder = services.AddHttpClient(IWattTimeClient.NamedClient); + var authenticationClientBuilder = services.AddHttpClient(IWattTimeClient.NamedAuthenticationClient); var Proxy = configSection.GetSection("Proxy").Get(); if (Proxy != null && Proxy.UseProxy == true) @@ -47,15 +48,18 @@ private static void AddWattTimeClient(IServiceCollection services, IConfiguratio { throw new Exceptions.ConfigurationException("Proxy Url is not configured."); } - httpClientBuilder.ConfigurePrimaryHttpMessageHandler(() => - new HttpClientHandler() { - Proxy = new WebProxy { - Address = new Uri(Proxy.Url), - Credentials = new NetworkCredential(Proxy.Username, Proxy.Password), - BypassProxyOnLocal = true - } + var handler = new HttpClientHandler() + { + Proxy = new WebProxy + { + Address = new Uri(Proxy.Url), + Credentials = new NetworkCredential(Proxy.Username, Proxy.Password), + BypassProxyOnLocal = true } - ); + }; + + httpClientBuilder.ConfigurePrimaryHttpMessageHandler(() => handler ); + authenticationClientBuilder.ConfigurePrimaryHttpMessageHandler(() => handler ); } services.TryAddSingleton(); } diff --git a/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Configuration/WattTimeClientConfiguration.cs b/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Configuration/WattTimeClientConfiguration.cs index 746cd2b4c..a586523ee 100644 --- a/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Configuration/WattTimeClientConfiguration.cs +++ b/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Configuration/WattTimeClientConfiguration.cs @@ -23,13 +23,19 @@ internal class WattTimeClientConfiguration /// /// Gets or sets the base url to use when connecting to WattTime /// - public string BaseUrl { get; set; } = "https://api2.watttime.org/v2/"; + public string BaseUrl { get; set; } = "https://api.watttime.org/v3/"; /// - /// Gets or sets the cached expiration time (in seconds) for a BalancingAuthority instance. + /// Authentication base url. This changed between v2 and v3 + /// to be different to the API base url. + /// + public string AuthenticationBaseUrl { get; set; } = "https://api.watttime.org/"; + + /// + /// Gets or sets the cached expiration time (in seconds) for a Region instance. /// It defaults to 86400 secs. /// - public int BalancingAuthorityCacheTTL { get; set; } = 86400; + public int RegionCacheTTL { get; set; } = 86400; /// /// Validate that this object is properly configured. @@ -51,6 +57,11 @@ public void Validate() throw new ConfigurationException($"{Key}:{nameof(this.BaseUrl)} is not a valid absolute url."); } + if (!Uri.IsWellFormedUriString(this.AuthenticationBaseUrl, UriKind.Absolute)) + { + throw new ConfigurationException($"{Key}:{nameof(this.AuthenticationBaseUrl)} is not a valid absolute url."); + } + // Validate credential encoding/decoding with UTF8 if (!Encoding.UTF8.GetString(Encoding.UTF8.GetBytes(this.Username)).Equals(this.Username)) { diff --git a/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Constants/Paths.cs b/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Constants/Paths.cs index d564a9c26..b726c2c97 100644 --- a/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Constants/Paths.cs +++ b/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Constants/Paths.cs @@ -2,9 +2,11 @@ internal class Paths { - public const string Data = "data"; + public const string Data = "historical"; public const string Forecast = "forecast"; - public const string BalancingAuthorityFromLocation = "ba-from-loc"; + + public const string ForecastHistorical = "forecast/historical"; + public const string RegionFromLocation = "region-from-loc"; public const string Login = "login"; public const string Historical = "historical"; } diff --git a/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Constants/QueryStrings.cs b/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Constants/QueryStrings.cs index 4aad4aa6f..93d32d473 100644 --- a/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Constants/QueryStrings.cs +++ b/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Constants/QueryStrings.cs @@ -2,10 +2,11 @@ internal class QueryStrings { - public const string BalancingAuthorityAbbreviation = "ba"; - public const string StartTime = "starttime"; - public const string EndTime = "endtime"; + public const string Region = "region"; + public const string StartTime = "start"; + public const string EndTime = "end"; public const string Latitude = "latitude"; public const string Longitude = "longitude"; public const string Username = "username"; + public const string SignalType = "signal_type"; } diff --git a/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Constants/SignalTypes.cs b/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Constants/SignalTypes.cs new file mode 100644 index 000000000..3ff905da5 --- /dev/null +++ b/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Constants/SignalTypes.cs @@ -0,0 +1,6 @@ +namespace CarbonAware.DataSources.WattTime.Constants; + +internal class SignalTypes +{ + public const string co2_moer = "co2_moer"; +} diff --git a/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Model/BalancingAuthority.cs b/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Model/BalancingAuthority.cs deleted file mode 100644 index 7560e0653..000000000 --- a/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Model/BalancingAuthority.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System.Text.Json.Serialization; - -namespace CarbonAware.DataSources.WattTime.Model; - -/// -/// The details of the balancing authority (BA) serving a particular location. -/// -[Serializable] -internal record BalancingAuthority -{ - /// - /// Balancing authority abbreviation. - /// - [JsonPropertyName("abbrev")] - public string Abbreviation { get; set; } = string.Empty; - - /// - /// Unique WattTime id for the region. - /// - [JsonPropertyName("id")] - public int Id { get; set; } - - /// - /// Human readable name/description for the region. - /// - [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; - -} diff --git a/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Model/Forecast.cs b/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Model/Forecast.cs deleted file mode 100644 index e7c5209e6..000000000 --- a/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Model/Forecast.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System.Text.Json.Serialization; - -namespace CarbonAware.DataSources.WattTime.Model; - -/// -/// An emissions forecast for a given time period. -/// -[Serializable] -internal record Forecast -{ - /// - /// DateTime indicating when the forecast was generated. - /// - [JsonPropertyName("generated_at")] - public DateTimeOffset GeneratedAt { get; set; } - - /// - /// List of GridEmissionDataPoints representing the predicted values for those points in time. - /// - [JsonPropertyName("forecast")] - public List ForecastData { get; set; } = new List(); -} diff --git a/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Model/ForecastEmissionsDataResponse.cs b/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Model/ForecastEmissionsDataResponse.cs new file mode 100644 index 000000000..9b4bd6597 --- /dev/null +++ b/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Model/ForecastEmissionsDataResponse.cs @@ -0,0 +1,16 @@ +using System.Text.Json.Serialization; + +namespace CarbonAware.DataSources.WattTime.Model; + +[Serializable] +internal record ForecastEmissionsDataResponse +{ + [JsonPropertyName("data")] + public List Data { get; set; } = new List(); + + + [JsonPropertyName("meta")] + public GridEmissionsMetaData Meta { get; set; } +} + + diff --git a/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Model/GridEmissionDataPoint.cs b/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Model/GridEmissionDataPoint.cs index 7a9debb83..3f0c0d9e0 100644 --- a/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Model/GridEmissionDataPoint.cs +++ b/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Model/GridEmissionDataPoint.cs @@ -3,23 +3,11 @@ namespace CarbonAware.DataSources.WattTime.Model; /// -/// An object describing the emissions for a given time period and balancing authority. +/// An object describing the emissions for a given time period and region. /// [Serializable] internal record GridEmissionDataPoint { - /// - /// Balancing authority abbreviation - /// - [JsonPropertyName("ba")] - public string BalancingAuthorityAbbreviation { get; set; } = string.Empty; - - /// - /// Type of data. eg MOER - /// - [JsonPropertyName("datatype")] - public string? Datatype { get; set; } - /// /// Duration in seconds for which the data is valid from point_time. /// diff --git a/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Model/GridEmissionsDataResponse.cs b/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Model/GridEmissionsDataResponse.cs new file mode 100644 index 000000000..e50eccb80 --- /dev/null +++ b/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Model/GridEmissionsDataResponse.cs @@ -0,0 +1,16 @@ +using System.Text.Json.Serialization; + +namespace CarbonAware.DataSources.WattTime.Model; + +[Serializable] +internal record GridEmissionsDataResponse +{ + [JsonPropertyName("data")] + public List Data { get; set; } = new List(); + + + [JsonPropertyName("meta")] + public GridEmissionsMetaData Meta { get; set; } +} + + diff --git a/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Model/GridEmissionsMetaData.cs b/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Model/GridEmissionsMetaData.cs new file mode 100644 index 000000000..d5abb4117 --- /dev/null +++ b/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Model/GridEmissionsMetaData.cs @@ -0,0 +1,34 @@ +using System.Text.Json.Serialization; + +namespace CarbonAware.DataSources.WattTime.Model; + +[Serializable] +internal record GridEmissionsMetaData +{ + [JsonPropertyName("data_point_periods_second")] + public int DataPointPeriodSeconds { get; set; } + + /// + /// Region (abbreviation) + /// + [JsonPropertyName("region")] + public string Region { get; set; } = string.Empty; + + /// + /// Signal Type. eg MOER + /// + [JsonPropertyName("signal_type")] + public string? SignalType { get; set; } + + [JsonPropertyName("model")] + public GridEmissionsModelData? Model { get; set; } + + [JsonPropertyName("units")] + public string? Units { get; set; } + + [JsonPropertyName("generated_at_period_seconds")] + public int? GeneratedAtPeriodSeconds { get; set; } + + [JsonPropertyName("generated_at")] + public DateTimeOffset GeneratedAt { get; set; } = DateTimeOffset.MinValue; +} \ No newline at end of file diff --git a/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Model/GridEmissionsModelData.cs b/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Model/GridEmissionsModelData.cs new file mode 100644 index 000000000..7830079a7 --- /dev/null +++ b/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Model/GridEmissionsModelData.cs @@ -0,0 +1,16 @@ +using System.Text.Json.Serialization; + +namespace CarbonAware.DataSources.WattTime.Model; + +/// +/// Data type used to capture the model as part of an EmissionsDataResponse +/// +[Serializable] +internal record GridEmissionsModelData +{ + [JsonPropertyName("date")] + public DateTime Date { get; set; } + + [JsonPropertyName("type")] + public string? Type { get; set; } = String.Empty; +} \ No newline at end of file diff --git a/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Model/HistoricalEmissionsData.cs b/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Model/HistoricalEmissionsData.cs new file mode 100644 index 000000000..5c82f01a6 --- /dev/null +++ b/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Model/HistoricalEmissionsData.cs @@ -0,0 +1,15 @@ +using System.Text.Json.Serialization; + +namespace CarbonAware.DataSources.WattTime.Model; + +[Serializable] +internal class HistoricalEmissionsData +{ + [JsonPropertyName("generated_at")] + public DateTimeOffset GeneratedAt { get; set; } = DateTimeOffset.MinValue; + + [JsonPropertyName("forecast")] + public List Forecast { get; set; } = new List(); + +} + diff --git a/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Model/HistoricalForecastEmissionsDataResponse.cs b/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Model/HistoricalForecastEmissionsDataResponse.cs new file mode 100644 index 000000000..212e8170a --- /dev/null +++ b/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Model/HistoricalForecastEmissionsDataResponse.cs @@ -0,0 +1,16 @@ +using System.Text.Json.Serialization; + +namespace CarbonAware.DataSources.WattTime.Model; + +[Serializable] +internal record HistoricalForecastEmissionsDataResponse +{ + [JsonPropertyName("data")] + public List Data { get; set; } = new List(); + + + [JsonPropertyName("meta")] + public GridEmissionsMetaData Meta { get; set; } = new GridEmissionsMetaData(); +} + + diff --git a/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Model/RegionResponse.cs b/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Model/RegionResponse.cs new file mode 100644 index 000000000..ee334c67e --- /dev/null +++ b/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Model/RegionResponse.cs @@ -0,0 +1,29 @@ +using System.Text.Json.Serialization; + +namespace CarbonAware.DataSources.WattTime.Model; + +/// +/// The details of the region serving a particular location. +/// +[Serializable] +internal record RegionResponse +{ + /// + /// Region abbreviation. + /// + [JsonPropertyName("region")] + public string Region { get; set; } = string.Empty; + + /// + /// Signal Type + /// + [JsonPropertyName("signal_type")] + public string SignalType { get; set; } = string.Empty; + + /// + /// Human readable name/description for the region. + /// + [JsonPropertyName("region_full_name")] + public string RegionFullName { get; set; } = string.Empty; + +} diff --git a/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/WattTimeDataSource.cs b/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/WattTimeDataSource.cs index b639bd2e7..cca777aaa 100644 --- a/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/WattTimeDataSource.cs +++ b/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/WattTimeDataSource.cs @@ -4,7 +4,6 @@ using CarbonAware.Interfaces; using CarbonAware.Model; using Microsoft.Extensions.Logging; -using System.Diagnostics; namespace CarbonAware.DataSources.WattTime; @@ -36,7 +35,7 @@ internal class WattTimeDataSource : IEmissionsDataSource, IForecastDataSource /// /// The logger for the datasource /// The WattTime Client - /// The location source to be used to convert a location to BA's. + /// The location source to be used to convert a location to named region's. public WattTimeDataSource(ILogger logger, IWattTimeClient client, ILocationSource locationSource) { this.Logger = logger; @@ -48,7 +47,7 @@ public WattTimeDataSource(ILogger logger, IWattTimeClient cl public async Task> GetCarbonIntensityAsync(IEnumerable locations, DateTimeOffset periodStartTime, DateTimeOffset periodEndTime) { this.Logger.LogInformation("Getting carbon intensity for locations {locations} for period {periodStartTime} to {periodEndTime}.", locations, periodStartTime, periodEndTime); - List result = new (); + List result = new(); foreach (var location in locations) { IEnumerable interimResult = await GetCarbonIntensityAsync(location, periodStartTime, periodEndTime); @@ -61,14 +60,14 @@ public async Task> GetCarbonIntensityAsync(IEnumerabl public async Task> GetCarbonIntensityAsync(Location location, DateTimeOffset periodStartTime, DateTimeOffset periodEndTime) { Logger.LogInformation($"Getting carbon intensity for location {location} for period {periodStartTime} to {periodEndTime}."); - var balancingAuthority = await this.GetBalancingAuthority(location); + var region = await this.GetRegion(location); var (newStartTime, newEndTime) = IntervalHelper.ExtendTimeByWindow(periodStartTime, periodEndTime, MinSamplingWindow); - var data = await this.WattTimeClient.GetDataAsync(balancingAuthority, newStartTime, newEndTime); + var historicalResponse = await this.WattTimeClient.GetDataAsync(region, newStartTime, newEndTime); if (Logger.IsEnabled(LogLevel.Debug)) { - Logger.LogDebug($"Found {data.Count()} total forecasts for location {location} for period {periodStartTime} to {periodEndTime}."); + Logger.LogDebug($"Found {historicalResponse.Data.Count()} total forecasts for location {location} for period {periodStartTime} to {periodEndTime}."); } - var windowData = ConvertToEmissionsData(data); + var windowData = ConvertToEmissionsData(historicalResponse); var filteredData = IntervalHelper.FilterByDuration(windowData, periodStartTime, periodEndTime); if (!filteredData.Any()) @@ -82,18 +81,18 @@ public async Task> GetCarbonIntensityAsync(Location l public async Task GetCurrentCarbonIntensityForecastAsync(Location location) { this.Logger.LogInformation($"Getting carbon intensity forecast for location {location}"); - var balancingAuthority = await this.GetBalancingAuthority(location); - var forecast = await this.WattTimeClient.GetCurrentForecastAsync(balancingAuthority); + var region = await this.GetRegion(location); + var forecast = await this.WattTimeClient.GetCurrentForecastAsync(region); return ForecastToEmissionsForecast(forecast, location, DateTimeOffset.UtcNow); } /// - public async Task GetCarbonIntensityForecastAsync(Location location, DateTimeOffset requestedAt) + public async Task GetHistoricalCarbonIntensityForecastAsync(Location location, DateTimeOffset requestedAt) { this.Logger.LogInformation($"Getting carbon intensity forecast for location {location} requested at {requestedAt}"); - var balancingAuthority = await this.GetBalancingAuthority(location); + var region = await this.GetRegion(location); var roundedRequestedAt = TimeToLowestIncrement(requestedAt); - var forecast = await this.WattTimeClient.GetForecastOnDateAsync(balancingAuthority, roundedRequestedAt); + var forecast = await this.WattTimeClient.GetForecastOnDateAsync(region, roundedRequestedAt); if (forecast == null) { var ex = new ArgumentException($"No forecast was generated at the requested time {roundedRequestedAt}"); @@ -101,22 +100,41 @@ public async Task GetCarbonIntensityForecastAsync(Location lo throw ex; } // keep input from the user. - return ForecastToEmissionsForecast(forecast, location, requestedAt); + return HistoricalForecastToEmissionsForecast(forecast, location, requestedAt); } - private EmissionsForecast ForecastToEmissionsForecast(Forecast forecast, Location location, DateTimeOffset requestedAt) + private EmissionsForecast HistoricalForecastToEmissionsForecast(HistoricalForecastEmissionsDataResponse historicalForecast, Location location, DateTimeOffset requestedAt) { - var duration = GetDurationFromGridEmissionDataPoints(forecast.ForecastData); - var forecastData = forecast.ForecastData.Select(e => new EmissionsData() + var duration = GetDurationFromGridEmissionDataPoints(historicalForecast.Data[0].Forecast); + var forecastData = historicalForecast.Data[0].Forecast.Select(e => new EmissionsData() { - Location = e.BalancingAuthorityAbbreviation, + Location = historicalForecast.Meta.Region, Rating = ConvertMoerToGramsPerKilowattHour(e.Value), Time = e.PointTime, Duration = duration }); var emissionsForecast = new EmissionsForecast() { - GeneratedAt = forecast.GeneratedAt, + GeneratedAt = historicalForecast.Data[0].GeneratedAt, + Location = location, + ForecastData = forecastData + }; + return emissionsForecast; + } + + private EmissionsForecast ForecastToEmissionsForecast(ForecastEmissionsDataResponse forecast, Location location, DateTimeOffset requestedAt) + { + var duration = GetDurationFromGridEmissionDataPoints(forecast.Data); + var forecastData = forecast.Data.Select(e => new EmissionsData() + { + Location = forecast.Meta.Region, + Rating = ConvertMoerToGramsPerKilowattHour(e.Value), + Time = e.PointTime, + Duration = duration + }); + var emissionsForecast = new EmissionsForecast() + { + GeneratedAt = forecast.Meta.GeneratedAt, Location = location, ForecastData = forecastData }; @@ -128,26 +146,26 @@ internal double ConvertMoerToGramsPerKilowattHour(double value) return value * LBS_TO_GRAMS_CONVERSION_FACTOR / MWH_TO_KWH_CONVERSION_FACTOR; } - private IEnumerable ConvertToEmissionsData(IEnumerable gridEmissionDataPoints) + private IEnumerable ConvertToEmissionsData(GridEmissionsDataResponse gridEmissionDataPoints) { - var defaultDuration = GetDurationFromGridEmissionDataPointsOrDefault(gridEmissionDataPoints, TimeSpan.Zero); - + var defaultDuration = GetDurationFromGridEmissionDataPointsOrDefault(gridEmissionDataPoints.Data, TimeSpan.Zero); + // Linq statement to convert WattTime forecast data into EmissionsData for the CarbonAware SDK. - return gridEmissionDataPoints.Select(e => new EmissionsData() - { - Location = e.BalancingAuthorityAbbreviation, - Rating = ConvertMoerToGramsPerKilowattHour(e.Value), - Time = e.PointTime, - Duration = FrequencyToTimeSpanOrDefault(e.Frequency, defaultDuration) - }); + return gridEmissionDataPoints.Data.Select(e => new EmissionsData() + { + Location = gridEmissionDataPoints.Meta.Region, + Rating = ConvertMoerToGramsPerKilowattHour(e.Value), + Time = e.PointTime, + Duration = FrequencyToTimeSpanOrDefault(e.Frequency, defaultDuration) + }); } private TimeSpan GetDurationFromGridEmissionDataPoints(IEnumerable gridEmissionDataPoints) { - var firstPoint = gridEmissionDataPoints.FirstOrDefault(); + var firstPoint = gridEmissionDataPoints.FirstOrDefault(); var secondPoint = gridEmissionDataPoints.Skip(1)?.FirstOrDefault(); - var first = firstPoint ?? throw new WattTimeClientException("Too few data points returned"); + var first = firstPoint ?? throw new WattTimeClientException("Too few data points returned"); var second = secondPoint ?? throw new WattTimeClientException("Too few data points returned"); // Handle chronological and reverse-chronological data by using `.Duration()` to get @@ -157,13 +175,13 @@ private TimeSpan GetDurationFromGridEmissionDataPoints(IEnumerable gridEmissionDataPoints, TimeSpan defaultValue) { - try + try { return GetDurationFromGridEmissionDataPoints(gridEmissionDataPoints); } - catch (WattTimeClientException) + catch (WattTimeClientException) { - return defaultValue; + return defaultValue; } } @@ -172,23 +190,23 @@ private TimeSpan FrequencyToTimeSpanOrDefault(int? frequency, TimeSpan defaultVa return (frequency != null) ? TimeSpan.FromSeconds((double)frequency) : defaultValue; } - private async Task GetBalancingAuthority(Location location) + private async Task GetRegion(Location location) { - BalancingAuthority balancingAuthority; + RegionResponse region; try { var geolocation = await this.LocationSource.ToGeopositionLocationAsync(location); - balancingAuthority = await WattTimeClient.GetBalancingAuthorityAsync(geolocation.LatitudeAsCultureInvariantString(), geolocation.LongitudeAsCultureInvariantString()); + region = await WattTimeClient.GetRegionAsync(geolocation.LatitudeAsCultureInvariantString(), geolocation.LongitudeAsCultureInvariantString()); } - catch(Exception ex) when (ex is LocationConversionException || ex is WattTimeClientHttpException) + catch (Exception ex) when (ex is LocationConversionException || ex is WattTimeClientHttpException) { - Logger.LogError(ex, "Failed to convert the location {location} into a Balancing Authority.", location); + Logger.LogError(ex, "Failed to convert the location {location} into a Region.", location); throw; } - Logger.LogDebug("Converted location {location} to balancing authority {balancingAuthorityAbbreviation}", location, balancingAuthority.Abbreviation); + Logger.LogDebug("Converted location {location} to region {region}", location, region.Region); - return balancingAuthority; + return region; } private DateTimeOffset TimeToLowestIncrement(DateTimeOffset date, int minutes = 5) diff --git a/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/test/Client/TestData.cs b/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/test/Client/TestData.cs deleted file mode 100644 index 90f6f1b8d..000000000 --- a/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/test/Client/TestData.cs +++ /dev/null @@ -1,81 +0,0 @@ -using System; -using System.Text.Json.Nodes; - -namespace CarbonAware.DataSources.WattTime.Client.Tests; - -internal static class TestData -{ - internal static string GetGridDataJsonString() - { - var json = new JsonArray( - new JsonObject - { - ["ba"] = "ba", - ["datatype"] = "dt", - ["frequency"] = 300, - ["market"] = "mkt", - ["point_time"] = new DateTimeOffset(2099, 1, 1, 0, 0, 0, TimeSpan.Zero), - ["value"] = 999.99, - ["version"] = "1.0" - } - ); - - return json.ToString(); - } - - internal static string GetCurrentForecastJsonString() - { - - var json = new JsonObject - { - ["generated_at"] = new DateTimeOffset(2099, 1, 1, 0, 0, 0, TimeSpan.Zero), - ["forecast"] = new JsonArray - { - new JsonObject - { - ["ba"] = "ba", - ["point_time"] = new DateTimeOffset(2099, 1, 1, 0, 0, 0, TimeSpan.Zero), - ["value"] = 999.99, - ["version"] = "1.0" - } - } - }; - - return json.ToString(); - } - - internal static string GetForecastByDateJsonString() - { - var json = new JsonArray - { - new JsonObject - { - ["generated_at"] = new DateTimeOffset(2099, 1, 1, 0, 0, 0, TimeSpan.Zero), - ["forecast"] = new JsonArray - { - new JsonObject - { - ["ba"] = "ba", - ["point_time"] = new DateTimeOffset(2099, 1, 1, 0, 0, 0, TimeSpan.Zero), - ["value"] = 999.99, - ["version"] = "1.0" - } - } - } - }; - - return json.ToString(); - } - - internal static string GetBalancingAuthorityJsonString() - { - var json = new JsonObject - { - ["id"] = "12345", - ["abbrev"] = "TEST_BA", - ["name"] = "Test Balancing Authority" - }; - - return json.ToString(); - } -} \ No newline at end of file diff --git a/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/test/Client/WattTimeClientTests.cs b/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/test/Client/WattTimeClientTests.cs index eff67195e..03557348d 100644 --- a/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/test/Client/WattTimeClientTests.cs +++ b/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/test/Client/WattTimeClientTests.cs @@ -1,4 +1,5 @@ using CarbonAware.DataSources.WattTime.Configuration; +using CarbonAware.DataSources.WattTime.Constants; using CarbonAware.DataSources.WattTime.Model; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; @@ -33,7 +34,8 @@ class WattTimeClientTests private string BasicAuthValue { get; set; } - private readonly string DefaultTokenValue = "myDefaultToken123"; + private readonly string _DEFAULT_TOKEN_VALUE = "myDefaultToken123"; + private readonly string _BASE_WATTTIME_LOGIN_URL = "https://api.watttime.org/login"; private IMemoryCache MemoryCache { get; set; } @@ -55,7 +57,7 @@ public void Initialize() .Returns(() => { var client = Handler.CreateClient(); - client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", this.DefaultTokenValue); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _DEFAULT_TOKEN_VALUE); return client; }); @@ -70,12 +72,12 @@ public void AllPublicMethods_ThrowsWhenInvalidLogin() this.BasicAuthValue = "invalid"; var client = new WattTimeClient(this.HttpClientFactory, this.Options.Object, this.Log.Object, this.MemoryCache); - Assert.ThrowsAsync(async () => await client.GetDataAsync("ba", new DateTimeOffset(), new DateTimeOffset())); - Assert.ThrowsAsync(async () => await client.GetCurrentForecastAsync("ba")); - Assert.ThrowsAsync(async () => await client.GetForecastOnDateAsync("ba", new DateTimeOffset())); - Assert.ThrowsAsync(async () => await client.GetBalancingAuthorityAsync("lat", "long")); - Assert.ThrowsAsync(async () => await client.GetBalancingAuthorityAbbreviationAsync("lat", "long")); - Assert.ThrowsAsync(async () => await client.GetHistoricalDataAsync("ba")); + Assert.ThrowsAsync(async () => await client.GetDataAsync(WattTimeTestData.Constants.Region, new DateTimeOffset(), new DateTimeOffset())); + Assert.ThrowsAsync(async () => await client.GetCurrentForecastAsync(WattTimeTestData.Constants.Region)); + Assert.ThrowsAsync(async () => await client.GetForecastOnDateAsync(WattTimeTestData.Constants.Region, new DateTimeOffset())); + Assert.ThrowsAsync(async () => await client.GetRegionAsync("lat", "long")); + Assert.ThrowsAsync(async () => await client.GetRegionAbbreviationAsync("lat", "long")); + Assert.ThrowsAsync(async () => await client.GetHistoricalDataAsync(WattTimeTestData.Constants.Region)); } [Test] @@ -84,16 +86,16 @@ public void AllPublicMethods_ThrowClientException_WhenNull() this.SetupBasicHandlers("null"); var client = new WattTimeClient(this.HttpClientFactory, this.Options.Object, this.Log.Object, this.MemoryCache); - client.SetBearerAuthenticationHeader(this.DefaultTokenValue); - var ba = new BalancingAuthority() { Abbreviation = "balauth" }; - - Assert.ThrowsAsync(async () => await client.GetBalancingAuthorityAsync("lat", "long")); - Assert.ThrowsAsync(async () => await client.GetDataAsync(ba.Abbreviation, new DateTimeOffset(), new DateTimeOffset())); - Assert.ThrowsAsync(async () => await client.GetDataAsync(ba, new DateTimeOffset(), new DateTimeOffset())); - Assert.ThrowsAsync(async () => await client.GetCurrentForecastAsync(ba.Abbreviation)); - Assert.ThrowsAsync(async () => await client.GetCurrentForecastAsync(ba)); - Assert.ThrowsAsync(async () => await client.GetForecastOnDateAsync(ba.Abbreviation, new DateTimeOffset())); - Assert.ThrowsAsync(async () => await client.GetForecastOnDateAsync(ba, new DateTimeOffset())); + client.SetBearerAuthenticationHeader(_DEFAULT_TOKEN_VALUE); + var region = new RegionResponse() { Region = WattTimeTestData.Constants.Region }; + + Assert.ThrowsAsync(async () => await client.GetRegionAsync("lat", "long")); + Assert.ThrowsAsync(async () => await client.GetDataAsync(region.Region, new DateTimeOffset(), new DateTimeOffset())); + Assert.ThrowsAsync(async () => await client.GetDataAsync(region, new DateTimeOffset(), new DateTimeOffset())); + Assert.ThrowsAsync(async () => await client.GetCurrentForecastAsync(region.Region)); + Assert.ThrowsAsync(async () => await client.GetCurrentForecastAsync(region)); + Assert.ThrowsAsync(async () => await client.GetForecastOnDateAsync(region.Region, new DateTimeOffset())); + Assert.ThrowsAsync(async () => await client.GetForecastOnDateAsync(region, new DateTimeOffset())); } [Test] @@ -102,16 +104,16 @@ public void AllPublicMethods_ThrowJsonException_WhenBadJsonIsReturned() this.SetupBasicHandlers("This is bad json"); var client = new WattTimeClient(this.HttpClientFactory, this.Options.Object, this.Log.Object, this.MemoryCache); - client.SetBearerAuthenticationHeader(this.DefaultTokenValue); - var ba = new BalancingAuthority() { Abbreviation = "balauth" }; - - Assert.ThrowsAsync(async () => await client.GetBalancingAuthorityAsync("lat", "long")); - Assert.ThrowsAsync(async () => await client.GetDataAsync(ba.Abbreviation, new DateTimeOffset(), new DateTimeOffset())); - Assert.ThrowsAsync(async () => await client.GetDataAsync(ba, new DateTimeOffset(), new DateTimeOffset())); - Assert.ThrowsAsync(async () => await client.GetCurrentForecastAsync(ba.Abbreviation)); - Assert.ThrowsAsync(async () => await client.GetCurrentForecastAsync(ba)); - Assert.ThrowsAsync(async () => await client.GetForecastOnDateAsync(ba.Abbreviation, new DateTimeOffset())); - Assert.ThrowsAsync(async () => await client.GetForecastOnDateAsync(ba, new DateTimeOffset())); + client.SetBearerAuthenticationHeader(_DEFAULT_TOKEN_VALUE); + var region = new RegionResponse() { Region = WattTimeTestData.Constants.Region }; + + Assert.ThrowsAsync(async () => await client.GetRegionAsync("lat", "long")); + Assert.ThrowsAsync(async () => await client.GetDataAsync(region.Region, new DateTimeOffset(), new DateTimeOffset())); + Assert.ThrowsAsync(async () => await client.GetDataAsync(region, new DateTimeOffset(), new DateTimeOffset())); + Assert.ThrowsAsync(async () => await client.GetCurrentForecastAsync(region.Region)); + Assert.ThrowsAsync(async () => await client.GetCurrentForecastAsync(region)); + Assert.ThrowsAsync(async () => await client.GetForecastOnDateAsync(region.Region, new DateTimeOffset())); + Assert.ThrowsAsync(async () => await client.GetForecastOnDateAsync(region, new DateTimeOffset())); } [Test] @@ -120,52 +122,51 @@ public async Task GetDataAsync_DeserializesExpectedResponse() this.AddHandlers_Auth(); this.AddHandler_RequestResponse(r => { - return r.RequestUri!.ToString().Equals("https://api2.watttime.org/v2/data?ba=balauth&starttime=2022-04-22T00%3a00%3a00.0000000%2b00%3a00&endtime=2022-04-22T00%3a00%3a00.0000000%2b00%3a00") && r.Method == HttpMethod.Get; - }, System.Net.HttpStatusCode.OK, TestData.GetGridDataJsonString()); + return r.RequestUri!.ToString().Equals($"https://api.watttime.org/v3/historical?region={WattTimeTestData.Constants.Region}&start=2022-04-22T00%3a00%3a00.0000000%2b00%3a00&end=2022-04-22T00%3a00%3a00.0000000%2b00%3a00&signal_type=co2_moer") && r.Method == HttpMethod.Get; + }, System.Net.HttpStatusCode.OK, WattTimeTestData.GetGridDataResponseJsonString()); var client = new WattTimeClient(this.HttpClientFactory, this.Options.Object, this.Log.Object, this.MemoryCache); - client.SetBearerAuthenticationHeader(this.DefaultTokenValue); - - var data = await client.GetDataAsync("balauth", new DateTimeOffset(2022, 4, 22, 0, 0, 0, TimeSpan.Zero), new DateTimeOffset(2022, 4, 22, 0, 0, 0, TimeSpan.Zero)); - - Assert.IsTrue(data.Count() > 0); - var gridDataPoint = data.ToList().First(); - Assert.AreEqual("ba", gridDataPoint.BalancingAuthorityAbbreviation); - Assert.AreEqual("dt", gridDataPoint.Datatype); - Assert.AreEqual(300, gridDataPoint.Frequency); - Assert.AreEqual("mkt", gridDataPoint.Market); - Assert.AreEqual(new DateTimeOffset(2099, 1, 1, 0, 0, 0, TimeSpan.Zero), gridDataPoint.PointTime); - Assert.AreEqual("999.99", gridDataPoint.Value.ToString("0.00", CultureInfo.InvariantCulture)); //Format float to avoid precision issues - Assert.AreEqual("1.0", gridDataPoint.Version); + client.SetBearerAuthenticationHeader(_DEFAULT_TOKEN_VALUE); + + var emissionsResponse = await client.GetDataAsync(WattTimeTestData.Constants.Region, new DateTimeOffset(2022, 4, 22, 0, 0, 0, TimeSpan.Zero), new DateTimeOffset(2022, 4, 22, 0, 0, 0, TimeSpan.Zero)); + + Assert.IsTrue(emissionsResponse.Data.Count() > 0); + var meta = emissionsResponse.Meta; + Assert.AreEqual(WattTimeTestData.Constants.Region, meta.Region); + Assert.AreEqual(WattTimeTestData.Constants.SignalType, meta.SignalType); + var gridDataPoint = emissionsResponse.Data.ToList().First(); + Assert.AreEqual(WattTimeTestData.Constants.Frequency, gridDataPoint.Frequency); + Assert.AreEqual(WattTimeTestData.Constants.Market, gridDataPoint.Market); + Assert.AreEqual(WattTimeTestData.Constants.PointTime, gridDataPoint.PointTime); + Assert.AreEqual(WattTimeTestData.Constants.Value.ToString("0.00", CultureInfo.InvariantCulture), gridDataPoint.Value.ToString("0.00", CultureInfo.InvariantCulture)); //Format float to avoid precision issues + Assert.AreEqual(WattTimeTestData.Constants.Version, gridDataPoint.Version); } [Test] public async Task GetDataAsync_RefreshesTokenWhenExpired() { - this.SetupBasicHandlers(TestData.GetGridDataJsonString(), "REFRESHTOKEN"); + this.SetupBasicHandlers(WattTimeTestData.GetGridDataResponseJsonString(), "REFRESHTOKEN"); var client = new WattTimeClient(this.HttpClientFactory, this.Options.Object, this.Log.Object, this.MemoryCache); - client.SetBearerAuthenticationHeader(this.DefaultTokenValue); + client.SetBearerAuthenticationHeader(_DEFAULT_TOKEN_VALUE); - var data = await client.GetDataAsync("balauth", new DateTimeOffset(), new DateTimeOffset()); + var emissionsResponse = await client.GetDataAsync(WattTimeTestData.Constants.Region, new DateTimeOffset(), new DateTimeOffset()); - Assert.IsTrue(data.Count() > 0); - var gridDataPoint = data.ToList().First(); - Assert.AreEqual("ba", gridDataPoint.BalancingAuthorityAbbreviation); + Assert.IsTrue(emissionsResponse.Data.Count() > 0); + Assert.AreEqual(WattTimeTestData.Constants.Region, emissionsResponse.Meta.Region); } [Test] public async Task GetDataAsync_RefreshesTokenWhenNoneSet() { - this.SetupBasicHandlers(TestData.GetGridDataJsonString(), "REFRESHTOKEN"); + this.SetupBasicHandlers(WattTimeTestData.GetGridDataResponseJsonString(), "REFRESHTOKEN"); var client = new WattTimeClient(this.HttpClientFactory, this.Options.Object, this.Log.Object, this.MemoryCache); - var data = await client.GetDataAsync("balauth", new DateTimeOffset(), new DateTimeOffset()); + var gridEmissionsResponse = await client.GetDataAsync(WattTimeTestData.Constants.Region, new DateTimeOffset(), new DateTimeOffset()); - Assert.IsTrue(data.Count() > 0); - var gridDataPoint = data.ToList().First(); - Assert.AreEqual("ba", gridDataPoint.BalancingAuthorityAbbreviation); + Assert.IsTrue(gridEmissionsResponse.Data.Count() > 0); + Assert.AreEqual(WattTimeTestData.Constants.Region, gridEmissionsResponse.Meta.Region); } [Test] @@ -174,43 +175,41 @@ public async Task GetCurrentForecastAsync_DeserializesExpectedResponse() this.AddHandlers_Auth(); this.AddHandler_RequestResponse(r => { - return r.RequestUri!.ToString().Equals("https://api2.watttime.org/v2/forecast?ba=balauth") && r.Method == HttpMethod.Get; - }, System.Net.HttpStatusCode.OK, TestData.GetCurrentForecastJsonString()); + return r.RequestUri!.ToString().Equals($"https://api.watttime.org/v3/forecast?region={WattTimeTestData.Constants.Region}&signal_type=co2_moer") && r.Method == HttpMethod.Get; + }, System.Net.HttpStatusCode.OK, WattTimeTestData.GetCurrentForecastJsonString()); var client = new WattTimeClient(this.HttpClientFactory, this.Options.Object, this.Log.Object, this.MemoryCache); - client.SetBearerAuthenticationHeader(this.DefaultTokenValue); + client.SetBearerAuthenticationHeader(_DEFAULT_TOKEN_VALUE); - var ba = new BalancingAuthority() { Abbreviation = "balauth" }; + var forecastResponse = await client.GetCurrentForecastAsync(WattTimeTestData.Constants.Region); + var overloadedForecast = await client.GetCurrentForecastAsync(WattTimeTestData.Constants.Region); - var forecast = await client.GetCurrentForecastAsync(ba.Abbreviation); - var overloadedForecast = await client.GetCurrentForecastAsync(ba); + Assert.AreEqual(forecastResponse.Meta.GeneratedAt, overloadedForecast.Meta.GeneratedAt); + Assert.AreEqual(forecastResponse.Data.First(), overloadedForecast.Data.First()); - Assert.AreEqual(forecast.GeneratedAt, overloadedForecast.GeneratedAt); - Assert.AreEqual(forecast.ForecastData.First(), overloadedForecast.ForecastData.First()); + Assert.IsNotNull(forecastResponse); + Assert.AreEqual(WattTimeTestData.Constants.GeneratedAt, forecastResponse?.Meta.GeneratedAt); + Assert.AreEqual(WattTimeTestData.Constants.Region, forecastResponse?.Meta.Region); + var forecastDataPoint = forecastResponse?.Data.First(); - Assert.IsNotNull(forecast); - Assert.AreEqual(new DateTimeOffset(2099, 1, 1, 0, 0, 0, TimeSpan.Zero), forecast?.GeneratedAt); - var forecastDataPoint = forecast?.ForecastData.First(); - Assert.AreEqual("ba", forecastDataPoint?.BalancingAuthorityAbbreviation); - Assert.AreEqual(new DateTimeOffset(2099, 1, 1, 0, 0, 0, TimeSpan.Zero), forecastDataPoint?.PointTime); - Assert.AreEqual("999.99", forecastDataPoint?.Value.ToString("0.00", CultureInfo.InvariantCulture)); //Format float to avoid precision issues - Assert.AreEqual("1.0", forecastDataPoint?.Version); + Assert.AreEqual(WattTimeTestData.Constants.PointTime, forecastDataPoint?.PointTime); + Assert.AreEqual(WattTimeTestData.Constants.Value.ToString("0.00", CultureInfo.InvariantCulture), forecastDataPoint?.Value.ToString("0.00", CultureInfo.InvariantCulture)); //Format float to avoid precision issues + Assert.AreEqual(WattTimeTestData.Constants.Version, forecastDataPoint?.Version); } [Test] public async Task GetCurrentForecastAsync_RefreshesTokenWhenExpired() { - this.SetupBasicHandlers(TestData.GetCurrentForecastJsonString(), "REFRESHTOKEN"); + this.SetupBasicHandlers(WattTimeTestData.GetCurrentForecastJsonString(), "REFRESHTOKEN"); var client = new WattTimeClient(this.HttpClientFactory, this.Options.Object, this.Log.Object, this.MemoryCache); - client.SetBearerAuthenticationHeader(this.DefaultTokenValue); + client.SetBearerAuthenticationHeader(_DEFAULT_TOKEN_VALUE); - var forecast = await client.GetCurrentForecastAsync("balauth"); + var forecastResponse = await client.GetCurrentForecastAsync(WattTimeTestData.Constants.Region); - Assert.IsNotNull(forecast); - Assert.AreEqual(new DateTimeOffset(2099, 1, 1, 0, 0, 0, TimeSpan.Zero), forecast?.GeneratedAt); - var forecastDataPoint = forecast?.ForecastData.First(); - Assert.AreEqual("ba", forecastDataPoint?.BalancingAuthorityAbbreviation); + Assert.IsNotNull(forecastResponse); + Assert.AreEqual(WattTimeTestData.Constants.GeneratedAt, forecastResponse?.Meta.GeneratedAt); + Assert.AreEqual(WattTimeTestData.Constants.Region, forecastResponse?.Meta.Region); } [Test] @@ -224,16 +223,15 @@ public async Task GetCurrentForecastAsync_RefreshesTokenWhenNoneSet() client.DefaultRequestHeaders.Authorization = null; // Null authorization header return client; }); - this.SetupBasicHandlers(TestData.GetCurrentForecastJsonString(), "REFRESHTOKEN"); + this.SetupBasicHandlers(WattTimeTestData.GetCurrentForecastJsonString(), "REFRESHTOKEN"); var client = new WattTimeClient(this.HttpClientFactory, this.Options.Object, this.Log.Object, this.MemoryCache); - var forecast = await client.GetCurrentForecastAsync("balauth"); + var forecastResponse = await client.GetCurrentForecastAsync(WattTimeTestData.Constants.Region); - Assert.IsNotNull(forecast); - Assert.AreEqual(new DateTimeOffset(2099, 1, 1, 0, 0, 0, TimeSpan.Zero), forecast?.GeneratedAt); - var forecastDataPoint = forecast?.ForecastData.First(); - Assert.AreEqual("ba", forecastDataPoint?.BalancingAuthorityAbbreviation); + Assert.IsNotNull(forecastResponse); + Assert.AreEqual(WattTimeTestData.Constants.GeneratedAt, forecastResponse?.Meta.GeneratedAt); + Assert.AreEqual(WattTimeTestData.Constants.Region, forecastResponse?.Meta.Region); } [Test] @@ -242,37 +240,38 @@ public async Task GetForecastOnDateAsync_DeserializesExpectedResponse() this.AddHandlers_Auth(); this.AddHandler_RequestResponse(r => { - return r.RequestUri!.ToString().Equals("https://api2.watttime.org/v2/forecast?ba=balauth&starttime=2022-04-22T00%3a00%3a00.0000000%2b00%3a00&endtime=2022-04-22T00%3a00%3a00.0000000%2b00%3a00") && r.Method == HttpMethod.Get; - }, System.Net.HttpStatusCode.OK, TestData.GetForecastByDateJsonString()); + return r.RequestUri!.ToString().Equals($"https://api.watttime.org/v3/forecast/historical?region={WattTimeTestData.Constants.Region}&start=2022-04-22T00%3a00%3a00.0000000%2b00%3a00&end=2022-04-22T00%3a00%3a00.0000000%2b00%3a00&signal_type=co2_moer") && r.Method == HttpMethod.Get; + }, System.Net.HttpStatusCode.OK, WattTimeTestData.GetHistoricalForecastDataJsonString()); var client = new WattTimeClient(this.HttpClientFactory, this.Options.Object, this.Log.Object, this.MemoryCache); - client.SetBearerAuthenticationHeader(this.DefaultTokenValue); - var ba = new BalancingAuthority() { Abbreviation = "balauth" }; + client.SetBearerAuthenticationHeader(_DEFAULT_TOKEN_VALUE); + var region = new RegionResponse() { Region = WattTimeTestData.Constants.Region }; - var forecast = await client.GetForecastOnDateAsync(ba.Abbreviation, new DateTimeOffset(2022, 4, 22, 0, 0, 0, TimeSpan.Zero)); - var overloadedForecast = await client.GetForecastOnDateAsync(ba, new DateTimeOffset(2022, 4, 22, 0, 0, 0, TimeSpan.Zero)); + var forecastResponse = await client.GetForecastOnDateAsync(region.Region, new DateTimeOffset(2022, 4, 22, 0, 0, 0, TimeSpan.Zero)); + var overloadedForecast = await client.GetForecastOnDateAsync(region, new DateTimeOffset(2022, 4, 22, 0, 0, 0, TimeSpan.Zero)); - Assert.AreEqual(forecast!.GeneratedAt, overloadedForecast!.GeneratedAt); - Assert.AreEqual(forecast.ForecastData.First(), overloadedForecast.ForecastData.First()); + Assert.AreEqual(forecastResponse!.Meta.GeneratedAt, overloadedForecast!.Meta.GeneratedAt); + Assert.AreEqual(forecastResponse.Data[0].Forecast.First(), overloadedForecast.Data[0].Forecast.First()); - Assert.AreEqual(new DateTimeOffset(2099, 1, 1, 0, 0, 0, TimeSpan.Zero), forecast.GeneratedAt); - var forecastDataPoint = forecast.ForecastData.ToList().First(); - Assert.AreEqual("ba", forecastDataPoint.BalancingAuthorityAbbreviation); - Assert.AreEqual(new DateTimeOffset(2099, 1, 1, 0, 0, 0, TimeSpan.Zero), forecastDataPoint.PointTime); - Assert.AreEqual("999.99", forecastDataPoint.Value.ToString("0.00", CultureInfo.InvariantCulture)); //Format float to avoid precision issues - Assert.AreEqual("1.0", forecastDataPoint.Version); + Assert.AreEqual(WattTimeTestData.Constants.GeneratedAt, forecastResponse.Meta.GeneratedAt); + Assert.AreEqual(WattTimeTestData.Constants.Region, forecastResponse.Meta.Region); + + var forecastDataPoint = forecastResponse.Data[0].Forecast.ToList().First(); + Assert.AreEqual(WattTimeTestData.Constants.PointTime, forecastDataPoint.PointTime); + Assert.AreEqual(WattTimeTestData.Constants.Value.ToString("0.00", CultureInfo.InvariantCulture), forecastDataPoint.Value.ToString("0.00", CultureInfo.InvariantCulture)); //Format float to avoid precision issues + Assert.AreEqual(WattTimeTestData.Constants.Version, forecastDataPoint.Version); } [Test] public async Task GetForecastOnDateAsync_RefreshesTokenWhenExpired() { - this.SetupBasicHandlers(TestData.GetForecastByDateJsonString(), "REFRESHTOKEN"); + this.SetupBasicHandlers(WattTimeTestData.GetHistoricalForecastDataJsonString(), "REFRESHTOKEN"); var client = new WattTimeClient(this.HttpClientFactory, this.Options.Object, this.Log.Object, this.MemoryCache); - client.SetBearerAuthenticationHeader(this.DefaultTokenValue); + client.SetBearerAuthenticationHeader(_DEFAULT_TOKEN_VALUE); - var forecast = await client.GetForecastOnDateAsync("balauth", new DateTimeOffset()); - Assert.AreEqual(new DateTimeOffset(2099, 1, 1, 0, 0, 0, TimeSpan.Zero), forecast!.GeneratedAt); + var forecastResponse = await client.GetForecastOnDateAsync(WattTimeTestData.Constants.Region, new DateTimeOffset()); + Assert.AreEqual(WattTimeTestData.Constants.GeneratedAt, forecastResponse!.Meta.GeneratedAt); } [Test] @@ -287,51 +286,52 @@ public async Task GetForecastOnDateAsync_RefreshesTokenWhenNoneSet() return client; }); - this.SetupBasicHandlers(TestData.GetForecastByDateJsonString(), "REFRESHTOKEN"); + this.SetupBasicHandlers(WattTimeTestData.GetHistoricalForecastDataJsonString(), "REFRESHTOKEN"); var client = new WattTimeClient(this.HttpClientFactory, this.Options.Object, this.Log.Object, this.MemoryCache); - var forecast = await client.GetForecastOnDateAsync("balauth", new DateTimeOffset()); + var forecastResponse = await client.GetForecastOnDateAsync(WattTimeTestData.Constants.Region, new DateTimeOffset()); - Assert.AreEqual(new DateTimeOffset(2099, 1, 1, 0, 0, 0, TimeSpan.Zero), forecast!.GeneratedAt); + Assert.AreEqual(WattTimeTestData.Constants.GeneratedAt, forecastResponse!.Meta.GeneratedAt); } [Test] - public async Task GetBalancingAuthorityAsync_DeserializesExpectedResponse() + public async Task GetRegionAsync_DeserializesExpectedResponse() { this.AddHandlers_Auth(); this.AddHandler_RequestResponse(r => { - return r.RequestUri!.ToString().Equals("https://api2.watttime.org/v2/ba-from-loc?latitude=lat&longitude=long") && r.Method == HttpMethod.Get; - }, System.Net.HttpStatusCode.OK, TestData.GetBalancingAuthorityJsonString()); + return r.RequestUri!.ToString().Equals("https://api.watttime.org/v3/region-from-loc?latitude=lat&longitude=long&signal_type=co2_moer") && r.Method == HttpMethod.Get; + }, System.Net.HttpStatusCode.OK, WattTimeTestData.GetRegionJsonString()); + var client = new WattTimeClient(this.HttpClientFactory, this.Options.Object, this.Log.Object, this.MemoryCache); - client.SetBearerAuthenticationHeader(this.DefaultTokenValue); + client.SetBearerAuthenticationHeader(_DEFAULT_TOKEN_VALUE); - var ba = await client.GetBalancingAuthorityAsync("lat", "long"); + var regionResponse = await client.GetRegionAsync("lat", "long"); - Assert.IsNotNull(ba); - Assert.AreEqual(12345, ba?.Id); - Assert.AreEqual("TEST_BA", ba?.Abbreviation); - Assert.AreEqual("Test Balancing Authority", ba?.Name); + Assert.IsNotNull(regionResponse); + Assert.AreEqual(WattTimeTestData.Constants.Region, regionResponse?.Region); + Assert.AreEqual(WattTimeTestData.Constants.RegionFullName, regionResponse?.RegionFullName); + Assert.AreEqual(SignalTypes.co2_moer, regionResponse?.SignalType); } [Test] - public async Task GetBalancingAuthorityAsync_RefreshesTokenWhenExpired() + public async Task GetRegionAsync_RefreshesTokenWhenExpired() { - this.SetupBasicHandlers(TestData.GetBalancingAuthorityJsonString(), "REFRESHTOKEN"); + this.SetupBasicHandlers(WattTimeTestData.GetRegionJsonString(), "REFRESHTOKEN"); var client = new WattTimeClient(this.HttpClientFactory, this.Options.Object, this.Log.Object, this.MemoryCache); - client.SetBearerAuthenticationHeader(this.DefaultTokenValue); + client.SetBearerAuthenticationHeader(_DEFAULT_TOKEN_VALUE); - var ba = await client.GetBalancingAuthorityAsync("lat", "long"); + var regionResponse = await client.GetRegionAsync("lat", "long"); - Assert.IsNotNull(ba); - Assert.AreEqual(12345, ba?.Id); + Assert.IsNotNull(regionResponse); + Assert.AreEqual(WattTimeTestData.Constants.SignalType, regionResponse?.SignalType); } [Test] - public async Task GetBalancingAuthorityAsync_RefreshesTokenWhenNoneSet() + public async Task GetRegionAsync_RefreshesTokenWhenNoneSet() { // Override http client mock to remove authorization header Mock.Get(this.HttpClientFactory).Setup(x => x.CreateClient(IWattTimeClient.NamedClient)) @@ -342,14 +342,14 @@ public async Task GetBalancingAuthorityAsync_RefreshesTokenWhenNoneSet() return client; }); - this.SetupBasicHandlers(TestData.GetBalancingAuthorityJsonString(), "REFRESHTOKEN"); + this.SetupBasicHandlers(WattTimeTestData.GetRegionJsonString(), "REFRESHTOKEN"); var client = new WattTimeClient(this.HttpClientFactory, this.Options.Object, this.Log.Object, this.MemoryCache); - var ba = await client.GetBalancingAuthorityAsync("lat", "long"); + var regionResponse = await client.GetRegionAsync("lat", "long"); - Assert.IsNotNull(ba); - Assert.AreEqual(12345, ba?.Id); + Assert.IsNotNull(regionResponse); + Assert.AreEqual(WattTimeTestData.Constants.SignalType, regionResponse?.SignalType); } [Test] @@ -360,9 +360,9 @@ public async Task GetHistoricalDataAsync_StreamsExpectedContent() this.SetupBasicHandlers(new StreamContent(testStream)); var client = new WattTimeClient(this.HttpClientFactory, this.Options.Object, this.Log.Object, this.MemoryCache); - client.SetBearerAuthenticationHeader(this.DefaultTokenValue); + client.SetBearerAuthenticationHeader(_DEFAULT_TOKEN_VALUE); - var result = await client.GetHistoricalDataAsync("ba"); + var result = await client.GetHistoricalDataAsync(WattTimeTestData.Constants.Region); var sr = new StreamReader(result); string streamResult = sr.ReadToEnd(); @@ -386,10 +386,8 @@ public async Task GetHistoricalDataAsync_RefreshesTokenWhenExpired() this.SetupBasicHandlers(new StreamContent(testStream), "REFRESHTOKEN"); - var client = new WattTimeClient(this.HttpClientFactory, this.Options.Object, this.Log.Object, this.MemoryCache); - - var result = await client.GetHistoricalDataAsync("ba"); + var result = await client.GetHistoricalDataAsync(WattTimeTestData.Constants.Region); var sr = new StreamReader(result); string streamResult = sr.ReadToEnd(); @@ -405,9 +403,9 @@ public async Task GetHistoricalDataAsync_RefreshesTokenWhenNoneSet() this.SetupBasicHandlers(new StreamContent(testStream), "REFRESHTOKEN"); var client = new WattTimeClient(this.HttpClientFactory, this.Options.Object, this.Log.Object, this.MemoryCache); - client.SetBearerAuthenticationHeader(this.DefaultTokenValue); + client.SetBearerAuthenticationHeader(_DEFAULT_TOKEN_VALUE); - var result = await client.GetHistoricalDataAsync("ba"); + var result = await client.GetHistoricalDataAsync(WattTimeTestData.Constants.Region); var sr = new StreamReader(result); string streamResult = sr.ReadToEnd(); @@ -420,7 +418,7 @@ public async Task GetHistoricalDataAsync_RefreshesTokenWhenNoneSet() */ private void AddHandlers_Auth(string? validToken = null) { - validToken ??= this.DefaultTokenValue; + validToken ??= _DEFAULT_TOKEN_VALUE; AddHandler_RequestResponse(r => { @@ -429,27 +427,28 @@ private void AddHandlers_Auth(string? validToken = null) AddHandler_RequestResponse(r => { - return (r.RequestUri == new Uri("https://api2.watttime.org/v2/login") && ($"Basic {this.BasicAuthValue}".Equals(r.Headers.Authorization?.ToString()))); + return (r.RequestUri == new Uri(_BASE_WATTTIME_LOGIN_URL) && ($"Basic {this.BasicAuthValue}".Equals(r.Headers.Authorization?.ToString()))); }, System.Net.HttpStatusCode.OK, "{\"token\":\"" + validToken + "\"}"); AddHandler_RequestResponse(r => { - return !(r.RequestUri == new Uri("https://api2.watttime.org/v2/login") && ($"Basic {this.BasicAuthValue}".Equals(r.Headers.Authorization?.ToString()))) && r.Headers.Authorization?.ToString() != $"Bearer {validToken}"; + return !(r.RequestUri == new Uri(_BASE_WATTTIME_LOGIN_URL) && ($"Basic {this.BasicAuthValue}".Equals(r.Headers.Authorization?.ToString()))) && r.Headers.Authorization?.ToString() != $"Bearer {validToken}"; }, System.Net.HttpStatusCode.Forbidden); } + /** * Helper to add client handlers for auth and basic content return */ private void SetupBasicHandlers(StreamContent responseContent, string? validToken = null) { - validToken ??= this.DefaultTokenValue; + validToken ??= _DEFAULT_TOKEN_VALUE; AddHandlers_Auth(validToken); // Catch-all for "requesting url that is not login and has valid token" this.Handler - .SetupRequest(r => r.RequestUri != new Uri("https://api2.watttime.org/v2/login") && r.Headers.Authorization?.ToString() == $"Bearer {validToken}") + .SetupRequest(r => r.RequestUri != new Uri(_BASE_WATTTIME_LOGIN_URL) && r.Headers.Authorization?.ToString() == $"Bearer {validToken}") .ReturnsResponse(System.Net.HttpStatusCode.OK, responseContent); } @@ -458,12 +457,12 @@ private void SetupBasicHandlers(StreamContent responseContent, string? validToke */ private void SetupBasicHandlers(string responseContent, string? validToken = null) { - validToken ??= this.DefaultTokenValue; + validToken ??= _DEFAULT_TOKEN_VALUE; AddHandlers_Auth(validToken); // Catch-all for "requesting url that is not login and has valid token" - AddHandler_RequestResponse(r => r.RequestUri != new Uri("https://api2.watttime.org/v2/login") && r.Headers.Authorization?.ToString() == $"Bearer {validToken}", System.Net.HttpStatusCode.OK, responseContent); + AddHandler_RequestResponse(r => r.RequestUri != new Uri(_BASE_WATTTIME_LOGIN_URL) && r.Headers.Authorization?.ToString() == $"Bearer {validToken}", System.Net.HttpStatusCode.OK, responseContent); } /** diff --git a/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/test/Client/WattTimeTestData.cs b/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/test/Client/WattTimeTestData.cs new file mode 100644 index 000000000..d46143e4f --- /dev/null +++ b/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/test/Client/WattTimeTestData.cs @@ -0,0 +1,128 @@ +using CarbonAware.DataSources.WattTime.Constants; +using CarbonAware.DataSources.WattTime.Model; +using System; +using System.Collections.Generic; +using System.Text.Json; + +namespace CarbonAware.DataSources.WattTime.Client.Tests; + +public static class WattTimeTestData +{ + public class Constants + { + public const string Region = "TEST_REGION"; + public const string RegionFullName = "Test Region Full Name"; + public const string Market = "mkt"; + public static DateTimeOffset GeneratedAt = new DateTimeOffset(2099, 1, 1, 0, 0, 0, TimeSpan.Zero); + public static DateTimeOffset PointTime = new DateTimeOffset(2099, 1, 1, 0, 0, 0, TimeSpan.Zero); + public static DateTime Date = new DateTime(2099, 1, 1, 0, 0, 0); + public const float Value = 999.99f; + public const string Version = "1.0"; + public const string SignalType = SignalTypes.co2_moer; + public const int Frequency = 300; + } + + internal static string GetGridDataResponseJsonString() + { + return JsonSerializer.Serialize(_GetGridDataResponse()); + } + private static GridEmissionsDataResponse _GetGridDataResponse() + { + var gridEmissionDataResponse = new GridEmissionsDataResponse() + { + Meta = _GetGridDataMetaResponse(), + Data = _GetGridEmissionDataPoints() + }; + return gridEmissionDataResponse; + } + + private static GridEmissionsMetaData _GetGridDataMetaResponse() + { + var gridEmissionsMetaData = new GridEmissionsMetaData() + { + Region = Constants.Region, + GeneratedAt = Constants.GeneratedAt, + GeneratedAtPeriodSeconds = 30, + Model = new GridEmissionsModelData() + { + Date = Constants.Date, + Type = SignalTypes.co2_moer + }, + DataPointPeriodSeconds = 30, + SignalType = SignalTypes.co2_moer, + Units = "co2_moer" + }; + + return gridEmissionsMetaData; + } + private static List _GetGridEmissionDataPoints() + { + return new List() + { + _GetGridEmissionDataPoint() + }; + } + + private static GridEmissionDataPoint _GetGridEmissionDataPoint() + { + return new GridEmissionDataPoint() + { + Frequency = 300, + Market = Constants.Market, + PointTime = Constants.PointTime, + Value = Constants.Value, + Version = Constants.Version + }; + } + + internal static string GetCurrentForecastJsonString() + { + return JsonSerializer.Serialize(_GetCurrentForecastEmissionsDataResponse()); + } + + private static ForecastEmissionsDataResponse _GetCurrentForecastEmissionsDataResponse() + { + return new ForecastEmissionsDataResponse() + { + Data = _GetGridEmissionDataPoints(), + Meta = _GetGridDataMetaResponse() + }; + } + + + internal static string GetHistoricalForecastDataJsonString() + { + return JsonSerializer.Serialize(_GetHistoricalForecastEmissionsDataResponse()); + } + + private static HistoricalForecastEmissionsDataResponse _GetHistoricalForecastEmissionsDataResponse() + { + return new HistoricalForecastEmissionsDataResponse() + { + Meta = _GetGridDataMetaResponse(), + Data = new List + { + new HistoricalEmissionsData() + { + Forecast = _GetGridEmissionDataPoints(), + GeneratedAt = Constants.GeneratedAt + } + } + }; + } + + internal static string GetRegionJsonString() + { + return JsonSerializer.Serialize(_GetRegion()); + } + + private static RegionResponse _GetRegion() + { + return new RegionResponse() + { + Region = Constants.Region, + RegionFullName = Constants.RegionFullName, + SignalType = SignalTypes.co2_moer + }; + } +} \ No newline at end of file diff --git a/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/test/Configuration/ServiceCollectionExtensionTests.cs b/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/test/Configuration/ServiceCollectionExtensionTests.cs index b3f845e3a..f6ee78614 100644 --- a/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/test/Configuration/ServiceCollectionExtensionTests.cs +++ b/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/test/Configuration/ServiceCollectionExtensionTests.cs @@ -26,8 +26,10 @@ class ServiceCollectionExtensionTests private readonly string ProxyPassword = $"DataSources:Configurations:WattTimeTest:Proxy:Password"; private readonly string UseProxyKey = $"DataSources:Configurations:WattTimeTest:Proxy:UseProxy"; + + [Test] - public void ClientProxyTest_With_Invalid_ProxyURL_ThrowsException() + public void ClientProxyTest_With_Missing_ProxyURL_ThrowsException() { // Arrange var settings = new Dictionary { @@ -35,7 +37,6 @@ public void ClientProxyTest_With_Invalid_ProxyURL_ThrowsException() { EmissionsDataSourceKey, EmissionsDataSourceValue }, { UsernameKey, Username }, { PasswordKey, Password }, - { ProxyUrl, "http://fakeproxy:8080" }, { UseProxyKey, "true" }, }; @@ -44,16 +45,14 @@ public void ClientProxyTest_With_Invalid_ProxyURL_ThrowsException() .AddEnvironmentVariables() .Build(); var serviceCollection = new ServiceCollection(); - serviceCollection.AddWattTimeForecastDataSource(configuration.DataSources()); - var serviceProvider = serviceCollection.BuildServiceProvider(); - var client = serviceProvider.GetRequiredService(); // Act & Assert - Assert.ThrowsAsync(async () => await client.GetBalancingAuthorityAsync("lat", "long")); + Assert.Throws(() => serviceCollection.AddWattTimeForecastDataSource(configuration.DataSources())); + Assert.Throws(() => serviceCollection.AddWattTimeEmissionsDataSource(configuration.DataSources())); } [Test] - public void ClientProxyTest_With_Missing_ProxyURL_ThrowsException() + public void ClientProxyTest_With_Invalid_ProxyURL_ThrowsException() { // Arrange var settings = new Dictionary { @@ -61,6 +60,7 @@ public void ClientProxyTest_With_Missing_ProxyURL_ThrowsException() { EmissionsDataSourceKey, EmissionsDataSourceValue }, { UsernameKey, Username }, { PasswordKey, Password }, + { ProxyUrl, "http://fakeproxy:8080" }, { UseProxyKey, "true" }, }; @@ -69,10 +69,13 @@ public void ClientProxyTest_With_Missing_ProxyURL_ThrowsException() .AddEnvironmentVariables() .Build(); var serviceCollection = new ServiceCollection(); + serviceCollection.AddWattTimeForecastDataSource(configuration.DataSources()); + + var serviceProvider = serviceCollection.BuildServiceProvider(); + var client = serviceProvider.GetRequiredService(); // Act & Assert - Assert.Throws(() => serviceCollection.AddWattTimeForecastDataSource(configuration.DataSources())); - Assert.Throws(() => serviceCollection.AddWattTimeEmissionsDataSource(configuration.DataSources())); + Assert.ThrowsAsync(async () => await client.GetRegionAsync("lat", "long")); } [TestCase(true, TestName = "ClientProxyTest, successful: denotes adding WattTime data sources using proxy.")] diff --git a/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/test/WattTimeDataSourceTests.cs b/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/test/WattTimeDataSourceTests.cs index 9d721cf56..8675ee4da 100644 --- a/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/test/WattTimeDataSourceTests.cs +++ b/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/test/WattTimeDataSourceTests.cs @@ -1,4 +1,6 @@ using CarbonAware.DataSources.WattTime.Client; +using CarbonAware.DataSources.WattTime.Client.Tests; +using CarbonAware.DataSources.WattTime.Constants; using CarbonAware.DataSources.WattTime.Model; using CarbonAware.Exceptions; using CarbonAware.Interfaces; @@ -25,7 +27,7 @@ class WattTimeDataSourceTests private Mock LocationSource { get; set; } private Location DefaultLocation { get; set; } - private BalancingAuthority DefaultBalancingAuthority { get; set; } + private RegionResponse DefaultRegion { get; set; } private DateTimeOffset DefaultDataStartTime { get; set; } // Magic floating point tolerance to allow for minuscule differences in floating point arithmetic. @@ -46,9 +48,9 @@ public void Setup() this.DataSource = new WattTimeDataSource(this.Logger.Object, this.WattTimeClient.Object, this.LocationSource.Object); this.DefaultLocation = new Location() { Name = "eastus" }; - this.DefaultBalancingAuthority = new BalancingAuthority() { Abbreviation = "BA" }; + this.DefaultRegion = new RegionResponse() { Region = "TEST_REGION", RegionFullName = "Test Region Full Name", SignalType = SignalTypes.co2_moer }; this.DefaultDataStartTime = new DateTimeOffset(2022, 4, 18, 12, 32, 42, TimeSpan.FromHours(-6)); - MockBalancingAuthorityLocationMapping(); + MockRegionLocationMapping(); } [Test] @@ -60,13 +62,13 @@ public async Task GetCarbonIntensity_ReturnsResultsWhenRecordsFound() var lbsPerMwhEmissions = 10; var gPerKwhEmissions = this.DataSource.ConvertMoerToGramsPerKilowattHour(lbsPerMwhEmissions); - var emissionData = GenerateDataPoints(1, value: lbsPerMwhEmissions); + var emissionDataResponse = GenerateGridEmissionsResponse(1, value: lbsPerMwhEmissions); this.WattTimeClient.Setup(w => w.GetDataAsync( - this.DefaultBalancingAuthority, + this.DefaultRegion, It.IsAny(), It.IsAny()) - ).ReturnsAsync(() => emissionData); + ).ReturnsAsync(() => emissionDataResponse); var result = await this.DataSource.GetCarbonIntensityAsync(new List() { this.DefaultLocation }, startDate, endDate); @@ -76,7 +78,7 @@ public async Task GetCarbonIntensity_ReturnsResultsWhenRecordsFound() var first = result.First(); Assert.IsNotNull(first); Assert.AreEqual(gPerKwhEmissions, first.Rating); - Assert.AreEqual(this.DefaultBalancingAuthority.Abbreviation, first.Location); + Assert.AreEqual(this.DefaultRegion.Region, first.Location); Assert.AreEqual(startDate, first.Time); this.LocationSource.Verify(r => r.ToGeopositionLocationAsync(this.DefaultLocation)); @@ -89,10 +91,10 @@ public async Task GetCarbonIntensity_ReturnsEmptyListWhenNoRecordsFound() var endDate = startDate.AddMinutes(1); this.WattTimeClient.Setup(w => w.GetDataAsync( - this.DefaultBalancingAuthority, - startDate, - endDate) - ).ReturnsAsync(() => new List()); + this.DefaultRegion, + It.IsAny(), + It.IsAny()) + ).ReturnsAsync(() => new GridEmissionsDataResponse()); var result = await this.DataSource.GetCarbonIntensityAsync(new List() { this.DefaultLocation }, startDate, endDate); @@ -118,35 +120,34 @@ public async Task GetCarbonIntensityForecastAsync_ReturnsResultsWhenRecordsFound // Arrange var startDate = this.DefaultDataStartTime; var endDate = startDate.AddMinutes(1); - var generatedAt = new DateTimeOffset(2022, 4, 18, 12, 30, 00, TimeSpan.FromHours(-6)); + var generatedAt = WattTimeTestData.Constants.GeneratedAt;// new DateTimeOffset(2022, 4, 18, 12, 30, 00, TimeSpan.FromHours(-6)); var lbsPerMwhEmissions = 10; var gPerKwhEmissions = this.DataSource.ConvertMoerToGramsPerKilowattHour(lbsPerMwhEmissions); var expectedDuration = TimeSpan.FromMinutes(5); - var emissionData = GenerateDataPoints(2, value: lbsPerMwhEmissions); - var forecast = new Forecast() - { - GeneratedAt = generatedAt, - ForecastData = emissionData - }; + var forecastResponse = GenerateForecastResponse(2, value: lbsPerMwhEmissions); + forecastResponse.Meta.GeneratedAt = generatedAt; + + var historicalForecastResponse = GenerateHistoricalForecastResponse(2, value: lbsPerMwhEmissions); + historicalForecastResponse.Meta.GeneratedAt = generatedAt; EmissionsForecast result; if (getCurrentForecast) { - this.WattTimeClient.Setup(w => w.GetCurrentForecastAsync(this.DefaultBalancingAuthority) - ).ReturnsAsync(() => forecast); + this.WattTimeClient.Setup(w => w.GetCurrentForecastAsync(this.DefaultRegion) + ).ReturnsAsync(() => forecastResponse); // Act result = await this.DataSource.GetCurrentCarbonIntensityForecastAsync(this.DefaultLocation); } else { - this.WattTimeClient.Setup(w => w.GetForecastOnDateAsync(this.DefaultBalancingAuthority, generatedAt) - ).ReturnsAsync(() => forecast); + this.WattTimeClient.Setup(w => w.GetForecastOnDateAsync(this.DefaultRegion, generatedAt) + ).ReturnsAsync(() => historicalForecastResponse); // Act - result = await this.DataSource.GetCarbonIntensityForecastAsync(this.DefaultLocation, generatedAt); + result = await this.DataSource.GetHistoricalCarbonIntensityForecastAsync(this.DefaultLocation, generatedAt); } // Assert @@ -158,13 +159,13 @@ public async Task GetCarbonIntensityForecastAsync_ReturnsResultsWhenRecordsFound var lastDataPoint = result.ForecastData.Last(); Assert.IsNotNull(firstDataPoint); Assert.AreEqual(gPerKwhEmissions, firstDataPoint.Rating); - Assert.AreEqual(this.DefaultBalancingAuthority.Abbreviation, firstDataPoint.Location); + Assert.AreEqual(this.DefaultRegion.Region, firstDataPoint.Location); Assert.AreEqual(startDate, firstDataPoint.Time); Assert.AreEqual(expectedDuration, firstDataPoint.Duration); Assert.IsNotNull(lastDataPoint); Assert.AreEqual(gPerKwhEmissions, lastDataPoint.Rating); - Assert.AreEqual(this.DefaultBalancingAuthority.Abbreviation, lastDataPoint.Location); + Assert.AreEqual(this.DefaultRegion.Region, lastDataPoint.Location); Assert.AreEqual(startDate + expectedDuration, lastDataPoint.Time); Assert.AreEqual(expectedDuration, lastDataPoint.Duration); @@ -177,18 +178,18 @@ public void GetCarbonIntensityForecastAsync_ThrowsWhenRegionNotFound() this.LocationSource.Setup(l => l.ToGeopositionLocationAsync(this.DefaultLocation)).Throws(); Assert.ThrowsAsync(async () => await this.DataSource.GetCurrentCarbonIntensityForecastAsync(this.DefaultLocation)); - Assert.ThrowsAsync(async () => await this.DataSource.GetCarbonIntensityForecastAsync(this.DefaultLocation, new DateTimeOffset())); + Assert.ThrowsAsync(async () => await this.DataSource.GetHistoricalCarbonIntensityForecastAsync(this.DefaultLocation, new DateTimeOffset())); } [Test] - public void GetCarbonIntensityForecastAsync_ThrowsWhenNoForecastFoundForReuqestedTime() + public void GetHistoricalCarbonIntensityForecastAsync_ThrowsWhenNoForecastFoundForReuqestedTime() { var generatedAt = new DateTimeOffset(); - this.WattTimeClient.Setup(w => w.GetForecastOnDateAsync(this.DefaultBalancingAuthority, generatedAt)).Returns(Task.FromResult(null)); + this.WattTimeClient.Setup(w => w.GetForecastOnDateAsync(this.DefaultRegion, generatedAt)).Returns(Task.FromResult(null)); // The datasource throws an exception if no forecasts are found at the requested generatedAt time. - Assert.ThrowsAsync(async () => await this.DataSource.GetCarbonIntensityForecastAsync(this.DefaultLocation, generatedAt)); + Assert.ThrowsAsync(async () => await this.DataSource.GetHistoricalCarbonIntensityForecastAsync(this.DefaultLocation, generatedAt)); } [TestCase(0, TestName = "GetCurrentCarbonIntensityForecastAsync throws for: No datapoints")] @@ -196,16 +197,11 @@ public void GetCarbonIntensityForecastAsync_ThrowsWhenNoForecastFoundForReuqeste public void GetCurrentCarbonIntensityForecastAsync_ThrowsWhenTooFewDatapointsReturned(int numDataPoints) { // Arrange - var emissionData = GenerateDataPoints(numDataPoints); + var forecastResponse = GenerateForecastResponse(numDataPoints); + forecastResponse.Meta.GeneratedAt = DateTimeOffset.Now; - var forecast = new Forecast() - { - GeneratedAt = DateTimeOffset.Now, - ForecastData = emissionData - }; - - this.WattTimeClient.Setup(w => w.GetCurrentForecastAsync(this.DefaultBalancingAuthority) - ).ReturnsAsync(() => forecast); + this.WattTimeClient.Setup(w => w.GetCurrentForecastAsync(this.DefaultRegion) + ).ReturnsAsync(() => forecastResponse); Assert.ThrowsAsync(async () => await this.DataSource.GetCurrentCarbonIntensityForecastAsync(this.DefaultLocation)); } @@ -219,27 +215,24 @@ public async Task GetCarbonIntensityForecastAsync_RequiredAtRounded(string reque var requestedAt = DateTimeOffset.Parse(requested); var expectedAt = DateTimeOffset.Parse(expected); - var emissionData = GenerateDataPoints(2, startTime: requestedAt); - var forecast = new Forecast() - { - GeneratedAt = expectedAt, - ForecastData = emissionData - }; + var forecastResponse = GenerateHistoricalForecastResponse(2, startTime: requestedAt); + forecastResponse.Meta.GeneratedAt = expectedAt; + - this.WattTimeClient.Setup(w => w.GetForecastOnDateAsync(this.DefaultBalancingAuthority, expectedAt) - ).ReturnsAsync(() => forecast); + this.WattTimeClient.Setup(w => w.GetForecastOnDateAsync(this.DefaultRegion, expectedAt) + ).ReturnsAsync(() => forecastResponse); // Act - var result = await this.DataSource.GetCarbonIntensityForecastAsync(this.DefaultLocation, requestedAt); - + var result = await this.DataSource.GetHistoricalCarbonIntensityForecastAsync(this.DefaultLocation, requestedAt); + // Assert Assert.IsNotNull(result); this.WattTimeClient.Verify(w => w.GetForecastOnDateAsync( - It.IsAny(), It.Is(date => date.Equals(expectedAt))), Times.Once); + It.IsAny(), It.Is(date => date.Equals(expectedAt))), Times.Once); } [DatapointSource] - public float[] moerValues = new float[] { 0.0F, 10.0F, 100.0F, 1000.0F, 596.1367F}; + public float[] moerValues = new float[] { 0.0F, 10.0F, 100.0F, 1000.0F, 596.1367F }; [Theory] public void GetCarbonIntensity_ConvertsMoerToGramsPerKwh(float lbsPerMwhEmissions) @@ -265,37 +258,98 @@ public async Task GetCarbonIntensity_CalculatesDurationBasedOnFrequency(double[] // Arrange var startDate = this.DefaultDataStartTime; var endDate = startDate.AddMinutes(10); - var emissionData = GenerateDataPoints(frequencyValues.Length); - for( int i = 0; i < frequencyValues.Length; i++) + var emissionResponse = GenerateGridEmissionsResponse(frequencyValues.Length); + for (int i = 0; i < frequencyValues.Length; i++) { - emissionData[i].Frequency = frequencyValues[i]; + emissionResponse.Data[i].Frequency = frequencyValues[i]; } - + List expectedDurationList = durationValues.ToList(); this.WattTimeClient.Setup(w => w.GetDataAsync( - It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny()) - ).ReturnsAsync(() => emissionData); + ).ReturnsAsync(() => emissionResponse); // Act var result = await this.DataSource.GetCarbonIntensityAsync(new List() { this.DefaultLocation }, startDate, endDate); // Assert List actualDurationList = result.Select(e => e.Duration.TotalSeconds).ToList(); - + CollectionAssert.AreEqual(expectedDurationList, actualDurationList); } - private void MockBalancingAuthorityLocationMapping() + private void MockRegionLocationMapping() { this.LocationSource.Setup(r => r.ToGeopositionLocationAsync(this.DefaultLocation)).Returns(Task.FromResult(this.DefaultLocation)); var latitude = this.DefaultLocation.Latitude.ToString(); var longitude = this.DefaultLocation.Longitude.ToString(); - this.WattTimeClient.Setup(w => w.GetBalancingAuthorityAsync(latitude!, longitude!) - ).ReturnsAsync(() => this.DefaultBalancingAuthority); + this.WattTimeClient.Setup(w => w.GetRegionAsync(latitude!, longitude!) + ).ReturnsAsync(() => this.DefaultRegion); + } + + private GridEmissionsDataResponse GenerateGridEmissionsResponse(int numberOfDatapoints, float value = 10, DateTimeOffset startTime = default) + { + var data = GenerateDataPoints(numberOfDatapoints, value, startTime); + var meta = new GridEmissionsMetaData() + { + Region = this.DefaultRegion.Region, + SignalType = SignalTypes.co2_moer + }; + + var response = new GridEmissionsDataResponse() + { + Data = data, + Meta = meta + }; + + return response; + } + + private HistoricalForecastEmissionsDataResponse GenerateHistoricalForecastResponse(int numberOfDatapoints, float value = 10, DateTimeOffset startTime = default) + { + var data = GenerateDataPoints(numberOfDatapoints, value, startTime); + var meta = new GridEmissionsMetaData() + { + Region = this.DefaultRegion.Region, + SignalType = SignalTypes.co2_moer + }; + + var response = new HistoricalForecastEmissionsDataResponse() + { + Data = new List() + { + new HistoricalEmissionsData() + { + Forecast = data, + GeneratedAt = WattTimeTestData.Constants.GeneratedAt + } + }, + Meta = meta + }; + + return response; + } + + private ForecastEmissionsDataResponse GenerateForecastResponse(int numberOfDatapoints, float value = 10, DateTimeOffset startTime = default) + { + var data = GenerateDataPoints(numberOfDatapoints, value, startTime); + var meta = new GridEmissionsMetaData() + { + Region = this.DefaultRegion.Region, + SignalType = SignalTypes.co2_moer + }; + + var response = new ForecastEmissionsDataResponse() + { + Data = data, + Meta = meta + }; + + return response; } private List GenerateDataPoints(int numberOfDatapoints, float value = 10, DateTimeOffset startTime = default) @@ -307,7 +361,6 @@ private List GenerateDataPoints(int numberOfDatapoints, f { var dataPoint = new GridEmissionDataPoint() { - BalancingAuthorityAbbreviation = this.DefaultBalancingAuthority.Abbreviation, PointTime = pointTime, Value = value, Frequency = defaultFrequency diff --git a/src/CarbonAware.LocationSources/src/LocationSource.cs b/src/CarbonAware.LocationSources/src/LocationSource.cs index 793b09dd8..96eb5e5a7 100644 --- a/src/CarbonAware.LocationSources/src/LocationSource.cs +++ b/src/CarbonAware.LocationSources/src/LocationSource.cs @@ -6,6 +6,7 @@ using Microsoft.Extensions.Options; using System.Reflection; using System.Text.Json; +using System.Threading; namespace CarbonAware.LocationSources; @@ -22,7 +23,6 @@ internal class LocationSource : ILocationSource private LocationDataSourcesConfiguration _configuration => _configurationMonitor.CurrentValue; - /// /// Creates a new instance of the class. /// @@ -61,14 +61,21 @@ private async Task LoadLocationJsonFileAsync() var sourceFiles = !_configuration.LocationSourceFiles.Any() ? DiscoverFiles() : _configuration.LocationSourceFiles; foreach (var source in sourceFiles) { - using Stream stream = GetStreamFromFileLocation(source); - var namedGeoMap = await JsonSerializer.DeserializeAsync>(stream, options); - foreach (var locationKey in namedGeoMap!.Keys) + if (File.Exists(source.DataFileLocation!)) + { + using Stream stream = GetStreamFromFileLocation(source); + var namedGeoMap = await JsonSerializer.DeserializeAsync>(stream, options); + foreach (var locationKey in namedGeoMap!.Keys) + { + var geoInstance = namedGeoMap[locationKey]; + geoInstance.AssertValid(); + var key = BuildKey(source, locationKey); + AddToLocationMap(key, geoInstance, source.DataFileLocation, keyCounter); + } + } + else { - var geoInstance = namedGeoMap[locationKey]; - geoInstance.AssertValid(); - var key = BuildKey(source, locationKey); - AddToLocationMap(key, geoInstance, source.DataFileLocation, keyCounter); + _logger.LogError($"Configured data source not found at '{source.DataFileLocation}'"); } } } @@ -78,11 +85,27 @@ private String BuildKey(LocationSourceFile locationData, string locationName) return $"{locationData.Prefix}{locationData.Delimiter}{locationName}"; } + /// + /// Semaphore is to stop any concurrent loading of the file, which + /// could in turn update the list twice and result in duplicate entries, + /// in turn resulting in suffixed keys to avoid clashes when not + /// required. + /// + private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1); private async Task LoadLocationFromFileIfNotPresentAsync() { - if (!_allLocations.Any()) + await _semaphore.WaitAsync(); + + try + { + if (!_allLocations.Any()) + { + await LoadLocationJsonFileAsync(); + } + } + finally { - await LoadLocationJsonFileAsync(); + _semaphore.Release(); } } diff --git a/src/CarbonAware.WebApi/src/CarbonAware.WebApi.csproj b/src/CarbonAware.WebApi/src/CarbonAware.WebApi.csproj index de8904092..789c6c428 100644 --- a/src/CarbonAware.WebApi/src/CarbonAware.WebApi.csproj +++ b/src/CarbonAware.WebApi/src/CarbonAware.WebApi.csproj @@ -15,6 +15,7 @@ + + diff --git a/src/CarbonAware.WebApi/test/integrationTests/CarbonAwareControllerTests.cs b/src/CarbonAware.WebApi/test/integrationTests/CarbonAwareControllerTests.cs index 975d00d04..fc19dde48 100644 --- a/src/CarbonAware.WebApi/test/integrationTests/CarbonAwareControllerTests.cs +++ b/src/CarbonAware.WebApi/test/integrationTests/CarbonAwareControllerTests.cs @@ -280,7 +280,7 @@ public async Task EmissionsForecastsBatch_SupportedDataSources_ReturnsOk(string var expectedRequestedAt = DateTimeOffset.Parse(reqAt); var expectedDataStartAt = DateTimeOffset.Parse(start); var expectedDataEndAt = DateTimeOffset.Parse(end); - _dataSourceMocker?.SetupBatchForecastMock(); + _dataSourceMocker?.SetupHistoricalBatchForecastMock(); var inputData = Enumerable.Range(0, nelems).Select(x => new { requestedAt = reqAt, diff --git a/src/CarbonAware/src/Interfaces/IDataSourceMocker.cs b/src/CarbonAware/src/Interfaces/IDataSourceMocker.cs index ae28a30f0..a8557bca3 100644 --- a/src/CarbonAware/src/Interfaces/IDataSourceMocker.cs +++ b/src/CarbonAware/src/Interfaces/IDataSourceMocker.cs @@ -15,7 +15,7 @@ internal interface IDataSourceMocker public abstract void SetupDataMock(DateTimeOffset start, DateTimeOffset end, string location); public abstract void SetupForecastMock(); - public abstract void SetupBatchForecastMock(); + public abstract void SetupHistoricalBatchForecastMock(); /// /// Initializes the DataSourceMocker with clean setup diff --git a/src/CarbonAware/src/Interfaces/IForecastDataSource.cs b/src/CarbonAware/src/Interfaces/IForecastDataSource.cs index cd8d06c08..964b0e18e 100644 --- a/src/CarbonAware/src/Interfaces/IForecastDataSource.cs +++ b/src/CarbonAware/src/Interfaces/IForecastDataSource.cs @@ -14,5 +14,5 @@ internal interface IForecastDataSource /// The location that should be used for getting the forecast. /// The historical time used to fetch the most recent forecast generated as of that time. /// A forecasted emissions object for the given location generated at the given time. - Task GetCarbonIntensityForecastAsync(Location location, DateTimeOffset requestedAt); + Task GetHistoricalCarbonIntensityForecastAsync(Location location, DateTimeOffset requestedAt); } \ No newline at end of file diff --git a/src/CarbonAware/src/NullForecastDataSource.cs b/src/CarbonAware/src/NullForecastDataSource.cs index 7b4288dbd..9492e3d31 100644 --- a/src/CarbonAware/src/NullForecastDataSource.cs +++ b/src/CarbonAware/src/NullForecastDataSource.cs @@ -4,7 +4,7 @@ namespace CarbonAware; internal class NullForecastDataSource : IForecastDataSource { - public Task GetCarbonIntensityForecastAsync(Location location, DateTimeOffset requestedAt) + public Task GetHistoricalCarbonIntensityForecastAsync(Location location, DateTimeOffset requestedAt) { throw new ArgumentException("ForecastDataSource is not configured"); } diff --git a/src/GSF.CarbonAware/src/Configuration/ServiceCollectionExtensions.cs b/src/GSF.CarbonAware/src/Configuration/ServiceCollectionExtensions.cs index 440e657b4..e5d0e0ead 100644 --- a/src/GSF.CarbonAware/src/Configuration/ServiceCollectionExtensions.cs +++ b/src/GSF.CarbonAware/src/Configuration/ServiceCollectionExtensions.cs @@ -6,6 +6,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; +using System.Linq; namespace GSF.CarbonAware.Configuration; @@ -26,21 +27,34 @@ private static IServiceCollection ConfigureLocationDataSourcesConfiguration(thi /// public static IServiceCollection AddEmissionsServices(this IServiceCollection services, IConfiguration configuration) { - services.ConfigureLocationDataSourcesConfiguration(configuration); - services.TryAddSingleton(); + AddLocationService(services, configuration); services.AddDataSourceService(configuration); services.TryAddSingleton(); services.TryAddSingleton(); return services; } + /// + /// This stops the location configuration being loaded twice if needed for + /// historical emissions and forecasted emissions services. + /// + /// + /// + private static void AddLocationService(IServiceCollection services, IConfiguration configuration) + { + if (!services.Any(x => x.ServiceType == typeof(ILocationSource))) + { + services.ConfigureLocationDataSourcesConfiguration(configuration); + services.TryAddSingleton(); + } + } + /// /// Add services needed in order to use an Forecast service. /// public static IServiceCollection AddForecastServices(this IServiceCollection services, IConfiguration configuration) { - services.ConfigureLocationDataSourcesConfiguration(configuration); - services.TryAddSingleton(); + AddLocationService(services, configuration); services.AddDataSourceService(configuration); services.TryAddSingleton(); services.TryAddSingleton(); diff --git a/src/GSF.CarbonAware/src/Handlers/ForecastHandler.cs b/src/GSF.CarbonAware/src/Handlers/ForecastHandler.cs index adc40f4de..fa8b413ab 100644 --- a/src/GSF.CarbonAware/src/Handlers/ForecastHandler.cs +++ b/src/GSF.CarbonAware/src/Handlers/ForecastHandler.cs @@ -73,7 +73,7 @@ public async Task GetForecastByDateAsync(string location, Dat { parameters.SetRequiredProperties(PropertyName.SingleLocation, PropertyName.Requested); parameters.Validate(); - var forecast = await _forecastDataSource.GetCarbonIntensityForecastAsync(parameters.SingleLocation, parameters.Requested); + var forecast = await _forecastDataSource.GetHistoricalCarbonIntensityForecastAsync(parameters.SingleLocation, parameters.Requested); var emissionsForecast = ProcessAndValidateForecast(forecast, parameters); return emissionsForecast; } diff --git a/src/GSF.CarbonAware/test/Handlers/ForecastHandlerTests.cs b/src/GSF.CarbonAware/test/Handlers/ForecastHandlerTests.cs index d0057fe48..d87733311 100644 --- a/src/GSF.CarbonAware/test/Handlers/ForecastHandlerTests.cs +++ b/src/GSF.CarbonAware/test/Handlers/ForecastHandlerTests.cs @@ -207,7 +207,7 @@ private static Mock CreateForecastByDateDataSource(global:: { var datasource = new Mock(); datasource - .Setup(x => x.GetCarbonIntensityForecastAsync(It.IsAny(), requested)) + .Setup(x => x.GetHistoricalCarbonIntensityForecastAsync(It.IsAny(), requested)) .ReturnsAsync(data); return datasource; @@ -221,7 +221,7 @@ private static Mock SetupMockDataSourceThatThrows() .ThrowsAsync(new CarbonAware.Exceptions.CarbonAwareException("")); datasource - .Setup(x => x.GetCarbonIntensityForecastAsync(It.IsAny(), It.IsAny())) + .Setup(x => x.GetHistoricalCarbonIntensityForecastAsync(It.IsAny(), It.IsAny())) .ThrowsAsync(new CarbonAware.Exceptions.CarbonAwareException("", It.IsAny())); return datasource; diff --git a/src/data/location-sources/azure-regions.json b/src/data/location-sources/azure-regions.json index 0e6c31b3e..19f4df113 100644 --- a/src/data/location-sources/azure-regions.json +++ b/src/data/location-sources/azure-regions.json @@ -60,8 +60,8 @@ "Name": "centralus" }, "southafricanorth": { - "Latitude": "-25.731340", - "Longitude": "28.218370", + "Latitude": "-25.73134", + "Longitude": "28.21837", "Name": "southafricanorth" }, "centralindia": { @@ -81,7 +81,7 @@ }, "koreacentral": { "Latitude": "37.5665", - "Longitude": "126.9780", + "Longitude": "126.978", "Name": "koreacentral" }, "canadacentral": { @@ -91,7 +91,7 @@ }, "francecentral": { "Latitude": "46.3772", - "Longitude": "2.3730", + "Longitude": "2.373", "Name": "francecentral" }, "germanywestcentral": { @@ -99,11 +99,31 @@ "Longitude": "8.682127", "Name": "germanywestcentral" }, + "italynorth": { + "Latitude": "45.46888", + "Longitude": "9.18109", + "Name": "italynorth" + }, "norwayeast": { "Latitude": "59.913868", "Longitude": "10.752245", "Name": "norwayeast" }, + "polandcentral": { + "Latitude": "52.23334", + "Longitude": "21.01666", + "Name": "polandcentral" + }, + "spaincentral": { + "Latitude": "40.4259", + "Longitude": "3.4209", + "Name": "spaincentral" + }, + "mexicocentral": { + "Latitude": "20.588818", + "Longitude": "-100.389888", + "Name": "mexicocentral" + }, "brazilsouth": { "Latitude": "-23.55", "Longitude": "-46.633", @@ -114,6 +134,26 @@ "Longitude": "-78.3889", "Name": "eastus2euap" }, + "israelcentral": { + "Latitude": "31.2655698", + "Longitude": "33.4506633", + "Name": "israelcentral" + }, + "qatarcentral": { + "Latitude": "25.551462", + "Longitude": "51.439327", + "Name": "qatarcentral" + }, + "brazilus": { + "Latitude": "0", + "Longitude": "0", + "Name": "brazilus" + }, + "eastusstg": { + "Latitude": "37.3719", + "Longitude": "-79.8164", + "Name": "eastusstg" + }, "northcentralus": { "Latitude": "41.8819", "Longitude": "-87.6278", @@ -145,7 +185,7 @@ "Name": "centraluseuap" }, "westcentralus": { - "Latitude": "40.890", + "Latitude": "40.89", "Longitude": "-110.234", "Name": "westcentralus" },